--- Tables mapped to the filing system -- Only string keys are permitted; package.dirsep characters are -- converted to underscores. -- Values are stored as strings (converted by tostring). -- As with disk operations, a table's elements must be set to nil -- (deleted) before the table itself can be set to nil. module ("fstable", package.seeall) require "io_ext" require "table_ext" require "io_ext" require "lfs" require "posix" local function fsnext (dir) local f repeat f = dir:next () until f ~= "." and f ~= ".." return f end -- Metamethods for persistent tables local metatable = {} metatable.__index = function (t, k) local path = io.catfile (getmetatable (t).directory, k) local attrs = lfs.attributes (path) if attrs then if attrs.mode == "file" then return io.slurp (path) elseif attrs.mode == "directory" then return new (path) end end return attrs end metatable.__newindex = function (t, k, v) local ty = type (v) if ty == "thread" or ty == "function" or ty == "userdata" then error ("cannot persist a " .. ty .. "") elseif type (k) ~= "string" then error ("keys of persistent tables must be of type string") else k = string.gsub (k, package.dirsep, "_") local path = io.catfile (getmetatable (t).directory, k) local vm = getmetatable (v) if v == nil then os.remove (path) elseif type (v) ~= "table" then local h = io.open (path, "w") h:write (tostring (v)) h:close () elseif type (vm) == "table" and vm.metatable == metatable then -- To match Lua semantics we'd hardlink, but that's not allowed for directories local ok, errmsg = posix.link (vm.directory, path, true) else local ok, errmsg = lfs.mkdir (path) if not ok then error (errmsg) end new (path, v) end end end metatable.__pairs = function (t) local _, dir = lfs.dir (getmetatable (t).directory) return function (dir) local f = fsnext (dir) if f then return f, t[f] end end, dir end metatable.__ipairs = function (t) local _, dir = lfs.dir (getmetatable (t).directory) return function (dir, i) local f = fsnext (dir) if f then return i + 1, f end end, dir, 0 end --- Bind a directory to a table -- @param path directory path -- @param t table to merge with directory -- @return table bound to directory function new (path, t) if not path:find ("^" .. package.dirsep) then path = io.catfile (lfs.currentdir (), path) end if lfs.attributes (path, "mode") ~= "directory" then error ("`" .. path .. "' does not exist or is not a directory") end local m = table.clone (metatable) m.directory = path m.metatable = metatable local d = setmetatable ({}, m) if t then for i, v in pairs (t) do d[i] = v end end return d end