blob: a26d391d1ce678cd2811e73cba40cd9f7e82d994 [file] [log] [blame] [raw]
local component = require("component")
local unicode = require("unicode")
local filesystem, fileStream = {}, {}
local isAutorunEnabled = nil
local mtab = {name="", children={}, links={}}
local function segments(path)
path = path:gsub("\\", "/")
repeat local n; path, n = path:gsub("//", "/") until n == 0
local parts = {}
for part in path:gmatch("[^/]+") do
table.insert(parts, part)
end
local i = 1
while i <= #parts do
if parts[i] == "." then
table.remove(parts, i)
elseif parts[i] == ".." then
table.remove(parts, i)
i = i - 1
if i > 0 then
table.remove(parts, i)
else
i = 1
end
else
i = i + 1
end
end
return parts
end
local function saveConfig()
local root = filesystem.get("/")
if root and not root.isReadOnly() then
filesystem.makeDirectory("/etc")
local f = io.open("/etc/filesystem.cfg", "w")
if f then
f:write("autorun="..tostring(isAutorunEnabled))
f:close()
end
end
end
local function findNode(path, create, depth)
checkArg(1, path, "string")
depth = depth or 0
if depth > 100 then
error("link cycle detected")
end
local parts = segments(path)
local node = mtab
while #parts > 0 do
local part = parts[1]
if not node.children[part] then
if node.links[part] then
return findNode(filesystem.concat(node.links[part], table.concat(parts, "/", 2)), create, depth + 1)
else
if create then
node.children[part] = {name=part, parent=node, children={}, links={}}
else
local vnode, vrest = node, table.concat(parts, "/")
local rest = vrest
while node and not node.fs do
rest = filesystem.concat(node.name, rest)
node = node.parent
end
return node, rest, vnode, vrest
end
end
end
node = node.children[part]
table.remove(parts, 1)
end
local vnode, vrest = node, nil
local rest = nil
while node and not node.fs do
rest = rest and filesystem.concat(node.name, rest) or node.name
node = node.parent
end
return node, rest, vnode, vrest
end
local function removeEmptyNodes(node)
while node and node.parent and not node.fs and not next(node.children) and not next(node.links) do
node.parent.children[node.name] = nil
node = node.parent
end
end
-------------------------------------------------------------------------------
function filesystem.isAutorunEnabled()
if isAutorunEnabled == nil then
local env = {}
local config = loadfile("/etc/filesystem.cfg", nil, env)
if config then
pcall(config)
isAutorunEnabled = not not env.autorun
else
isAutorunEnabled = true
end
saveConfig()
end
return isAutorunEnabled
end
function filesystem.setAutorunEnabled(value)
checkArg(1, value, "boolean")
isAutorunEnabled = value
saveConfig()
end
function filesystem.segments(path)
return segments(path)
end
function filesystem.canonical(path)
local result = table.concat(segments(path), "/")
if unicode.sub(path, 1, 1) == "/" then
return "/" .. result
else
return result
end
end
function filesystem.concat(pathA, pathB, ...)
checkArg(1, pathA, "string")
local function concat(n, a, b, ...)
if not b then
return a
end
checkArg(n, b, "string")
return concat(n + 1, a .. "/" .. b, ...)
end
return filesystem.canonical(concat(2, pathA, pathB, ...))
end
function filesystem.get(path)
local node, rest = findNode(path)
if node.fs then
local proxy = component.proxy(node.fs)
path = ""
while node and node.parent do
path = filesystem.concat(node.name, path)
node = node.parent
end
path = filesystem.canonical(path)
if path ~= "/" then
path = "/" .. path
end
return proxy, path
end
return nil, "no such file system"
end
function filesystem.isLink(path)
local node, rest, vnode, vrest = findNode(filesystem.path(path))
return not vrest and vnode.links[filesystem.name(path)] ~= nil
end
function filesystem.link(target, linkpath)
checkArg(1, target, "string")
checkArg(2, linkpath, "string")
if filesystem.exists(linkpath) then
return nil, "file already exists"
end
local node, rest, vnode, vrest = findNode(filesystem.path(linkpath), true)
vnode.links[filesystem.name(linkpath)] = target
return true
end
function filesystem.mount(fs, path)
checkArg(1, fs, "string", "table")
if type(fs) == "string" then
fs = filesystem.proxy(fs)
end
assert(type(fs) == "table", "bad argument #1 (file system proxy or address expected)")
checkArg(2, path, "string")
if path ~= "/" and filesystem.exists(path) then
return nil, "file already exists"
end
local node, rest, vnode, vrest = findNode(path, true)
if vnode.fs then
return nil, "another filesystem is already mounted here"
end
vnode.fs = fs.address
return true
end
function filesystem.mounts()
local function path(node)
local result = "/"
while node and node.parent do
for name, child in pairs(node.parent.children) do
if child == node then
result = "/" .. name .. result
break
end
end
node = node.parent
end
return result
end
local queue = {mtab}
return function()
if #queue == 0 then
return nil
else
while #queue > 0 do
local node = table.remove(queue)
for _, child in pairs(node.children) do
table.insert(queue, child)
end
if node.fs then
return component.proxy(node.fs), path(node)
end
end
end
end
end
function filesystem.path(path)
local parts = segments(path)
local result = table.concat(parts, "/", 1, #parts - 1) .. "/"
if unicode.sub(path, 1, 1) == "/" and unicode.sub(result, 1, 1) ~= "/" then
return "/" .. result
else
return result
end
end
function filesystem.name(path)
local parts = segments(path)
return parts[#parts]
end
function filesystem.proxy(filter)
checkArg(1, filter, "string")
local address
for c in component.list("filesystem") do
if component.invoke(c, "getLabel") == filter then
address = c
break
end
if c:sub(1, filter:len()) == filter then
address = c
break
end
end
if not address then
return nil, "no such file system"
end
return component.proxy(address)
end
function filesystem.umount(fsOrPath)
checkArg(1, fsOrPath, "string", "table")
if type(fsOrPath) == "string" then
local node, rest, vnode, vrest = findNode(fsOrPath)
if not vrest and vnode.fs then
vnode.fs = nil
removeEmptyNodes(vnode)
return true
end
end
local function unmount(address)
local queue = {mtab}
for proxy, path in filesystem.mounts() do
if string.sub(proxy.address, 1, address:len()) == address then
local node, rest, vnode, vrest = findNode(path)
vnode.fs = nil
removeEmptyNodes(vnode)
return true
end
end
end
local address = type(fsOrPath) == "table" and fsOrPath.address or fsOrPath
local result = false
while unmount(address) do result = true end
return result
end
function filesystem.exists(path)
local node, rest, vnode, vrest = findNode(path)
if not vrest or vnode.links[vrest] then -- virtual directory or symbolic link
return true
end
if node and node.fs then
return component.proxy(node.fs).exists(rest)
end
return false
end
function filesystem.size(path)
local node, rest, vnode, vrest = findNode(path)
if not vnode.fs and (not vrest or vnode.links[vrest]) then
return 0 -- virtual directory or symlink
end
if node.fs and rest then
return component.proxy(node.fs).size(rest)
end
return 0 -- no such file or directory
end
function filesystem.isDirectory(path)
local node, rest, vnode, vrest = findNode(path)
if not vnode.fs and not vrest then
return true -- virtual directory
end
if node.fs then
return not rest or component.proxy(node.fs).isDirectory(rest)
end
return false
end
function filesystem.lastModified(path)
local node, rest, vnode, vrest = findNode(path)
if not vnode.fs and not vrest then
return 0 -- virtual directory
end
if node.fs and rest then
return component.proxy(node.fs).lastModified(rest)
end
return 0 -- no such file or directory
end
function filesystem.list(path)
local node, rest, vnode, vrest = findNode(path)
if not vnode.fs and vrest and not (node and node.fs) then
return nil, "no such file or directory"
end
local result, reason
if node and node.fs then
result, reason = component.proxy(node.fs).list(rest or "")
end
result = result or {}
if not vrest then
for k in pairs(vnode.children) do
table.insert(result, k .. "/")
end
for k in pairs(vnode.links) do
table.insert(result, k)
end
end
table.sort(result)
local i, f = 1, nil
while i <= #result do
if result[i] == f then
table.remove(result, i)
else
f = result[i]
end
i = i + 1
end
local i = 0
return function()
i = i + 1
return result[i]
end
end
function filesystem.makeDirectory(path)
if filesystem.exists(path) then
return nil, "file or directory with that name already exists"
end
local node, rest = findNode(path)
if node.fs and rest then
return component.proxy(node.fs).makeDirectory(rest)
end
if node.fs then
return nil, "virtual directory with that name already exists"
end
return nil, "cannot create a directory in a virtual directory"
end
function filesystem.remove(path)
local node, rest, vnode, vrest = findNode(filesystem.path(path))
local name = filesystem.name(path)
if vnode.children[name] then
vnode.children[name] = nil
removeEmptyNodes(vnode)
return true
elseif vnode.links[name] then
vnode.links[name] = nil
removeEmptyNodes(vnode)
return true
else
node, rest = findNode(path)
if node.fs and rest then
return component.proxy(node.fs).remove(rest)
end
return nil, "no such file or directory"
end
end
function filesystem.rename(oldPath, newPath)
if filesystem.isLink(oldPath) then
local node, rest, vnode, vrest = findNode(filesystem.path(oldPath))
local target = vnode.links[filesystem.name(oldPath)]
local result, reason = filesystem.link(target, newPath)
if result then
filesystem.remove(oldPath)
end
return result, reason
else
local oldNode, oldRest = findNode(oldPath)
local newNode, newRest = findNode(newPath)
if oldNode.fs and oldRest and newNode.fs and newRest then
if oldNode.fs == newNode.fs then
return component.proxy(oldNode.fs).rename(oldRest, newRest)
else
local result, reason = filesystem.copy(oldPath, newPath)
if result then
return filesystem.remove(oldPath)
else
return nil, reason
end
end
end
return nil, "trying to read from or write to virtual directory"
end
end
function filesystem.copy(fromPath, toPath)
if filesystem.isDirectory(fromPath) then
return nil, "cannot copy folders"
end
local input, reason = io.open(fromPath, "rb")
if not input then
return nil, reason
end
local output, reason = io.open(toPath, "wb")
if not output then
input:close()
return nil, reason
end
repeat
local buffer, reason = input:read(1024)
if not buffer and reason then
return nil, reason
elseif buffer then
local result, reason = output:write(buffer)
if not result then
input:close()
output:close()
return nil, reason
end
end
until not buffer
input:close()
output:close()
return true
end
function fileStream:close()
if self.handle then
component.proxy(self.fs).close(self.handle)
self.handle = nil
end
end
function fileStream:read(n)
if not self.handle then
return nil, "file is closed"
end
return component.proxy(self.fs).read(self.handle, n)
end
function fileStream:seek(whence, offset)
if not self.handle then
return nil, "file is closed"
end
return component.proxy(self.fs).seek(self.handle, whence, offset)
end
function fileStream:write(str)
if not self.handle then
return nil, "file is closed"
end
return component.proxy(self.fs).write(self.handle, str)
end
function filesystem.open(path, mode)
checkArg(1, path, "string")
mode = tostring(mode or "r")
checkArg(2, mode, "string")
assert(({r=true, rb=true, w=true, wb=true, a=true, ab=true})[mode],
"bad argument #2 (r[b], w[b] or a[b] expected, got " .. mode .. ")")
local node, rest = findNode(path)
if not node.fs or not rest then
return nil, "file not found"
end
local handle, reason = component.proxy(node.fs).open(rest, mode)
if not handle then
return nil, reason
end
local stream = {fs = node.fs, handle = handle}
local function cleanup(self)
if not self.handle then return end
local proxy = component.proxy(self.fs)
if proxy then pcall(proxy.close, self.handle) end
end
local metatable = {__index = fileStream,
__gc = cleanup,
__metatable = "filestream"}
return setmetatable(stream, metatable)
end
-------------------------------------------------------------------------------
return filesystem