summaryrefslogtreecommitdiffstats
path: root/acf2/persistence
diff options
context:
space:
mode:
authorKaarle Ritvanen <kaarle.ritvanen@datakunkku.fi>2013-10-08 18:50:56 +0300
committerKaarle Ritvanen <kaarle.ritvanen@datakunkku.fi>2013-10-08 18:50:56 +0300
commit33728ad3382d74281412d4556561d479bb88832b (patch)
treee8a9b2798dec96d820715f729989e91f9e7d5e12 /acf2/persistence
parent3e48dd63e8bdf0c2641cfb73e6b20bea8c466ff8 (diff)
downloadacf2-33728ad3382d74281412d4556561d479bb88832b.tar.bz2
acf2-33728ad3382d74281412d4556561d479bb88832b.tar.xz
changed module paths from acf to acf2v0.1.0
Diffstat (limited to 'acf2/persistence')
-rw-r--r--acf2/persistence/backends/augeas.lua153
-rw-r--r--acf2/persistence/backends/files.lua107
-rw-r--r--acf2/persistence/backends/json.lua79
-rw-r--r--acf2/persistence/backends/null.lua10
-rw-r--r--acf2/persistence/backends/volatile.lua46
-rw-r--r--acf2/persistence/init.lua79
-rw-r--r--acf2/persistence/util.lua22
7 files changed, 496 insertions, 0 deletions
diff --git a/acf2/persistence/backends/augeas.lua b/acf2/persistence/backends/augeas.lua
new file mode 100644
index 0000000..010a52b
--- /dev/null
+++ b/acf2/persistence/backends/augeas.lua
@@ -0,0 +1,153 @@
+--[[
+Copyright (c) 2012-2013 Kaarle Ritvanen
+See LICENSE file for license details
+--]]
+
+local topology = require('acf2.model.root').topology
+local pth = require('acf2.path')
+
+local util = require('acf2.util')
+local copy = util.copy
+
+
+local function aug_path(path) return pth.join('/files', unpack(path)) end
+
+local function strip_name(name)
+ return type(name) == 'string' and name:match('^[^][/=)%s]+') or name
+end
+
+local function ipath(path, index) return path..'['..index..']' end
+
+
+local backend = require('acf2.object').class()
+
+function backend:init() self.aug = require('augeas').init() end
+
+function backend:find(path, leaf)
+ util.map(
+ function(comp)
+ assert(
+ comp == strip_name(comp) and (
+ type(comp) == 'number' or not comp:match('^%.+$')
+ )
+ )
+ end,
+ path
+ )
+ local res = aug_path(path)
+
+ if #self.aug:match(res) == 0 and #path > 1 and leaf then
+ local index = path[#path]
+ if type(index) == 'number' then
+ local ppath = copy(path)
+ table.remove(ppath)
+ ppath = aug_path(ppath)
+
+ if #self.aug:match(ppath) > 0 and #self.aug:match(
+ ppath..'/*'
+ ) == 0 then
+ return ipath(ppath, index), ppath, index
+ end
+ end
+ end
+
+ return res
+end
+
+function backend:get(path, top)
+ local tpe = top and top.type
+ local leaf = tpe and tpe ~= 'table'
+ local apath, mvpath = self:find(path, leaf or not tpe)
+
+ local matches = self.aug:match(apath)
+ if mvpath then
+ assert(#matches < 2)
+ leaf = true
+ end
+
+ if #matches == 0 then return end
+
+ if #matches > 1 then
+ assert(not leaf)
+ local res = {}
+ path = copy(path)
+ for i, _ in ipairs(matches) do
+ table.insert(path, i)
+ if self:get(path) then table.insert(res, i) end
+ table.remove(path)
+ end
+ return res
+ end
+
+ local value = self.aug:get(matches[1])
+ if value then return tpe == 'table' and {1} or value end
+ if leaf then return end
+
+ local names = {}
+ for _, child in ipairs(self.aug:match(apath..'/*')) do
+ names[strip_name(pth.name(child))] = true
+ end
+ return util.keys(names)
+end
+
+function backend:set(mods)
+ local gcpaths = {}
+
+ for _, mod in ipairs(mods) do
+ local path, value = unpack(mod)
+
+ local delete = value == nil
+ self.aug:rm(aug_path(path)..(delete and '' or '/*'))
+
+ local apath, mvpath, index = self:find(path, type(value) ~= 'table')
+ local mpath = mvpath or apath
+
+ if not delete then
+ if #self.aug:match(mpath) == 0 then
+
+ local function order(path)
+ return topology('/augeas'..path).order
+ end
+ local ord = order(pth.join('/', unpack(path)))
+
+ for _, sibling in ipairs(self.aug:match(pth.parent(mpath)..'/*')) do
+ if order(strip_name(sibling):sub(7, -1)) > ord then
+ self.aug:insert(sibling, pth.name(mpath), true)
+ break
+ end
+ end
+ end
+
+ if mvpath then
+ local size = #self.aug:match(mvpath)
+ while size < index do
+ self.aug:insert(ipath(mvpath, size), pth.name(mvpath))
+ size = size + 1
+ end
+ end
+ end
+
+ if type(value) == 'table' then value = nil end
+ if not delete or mvpath then self.aug:set(apath, value)
+ elseif apath > '/' then apath = pth.parent(apath) end
+
+ if delete or value == '' then gcpaths[mpath] = true end
+ end
+
+ local function gc(path)
+ if path == '/' or #self.aug:match(path..'/*') > 0 then return end
+ if self.aug:rm(path.."[. = '']") > 0 then gc(pth.parent(path)) end
+ end
+ for p, _ in pairs(gcpaths) do gc(p) end
+
+ if self.aug:save() ~= 0 then
+ print('Augeas save failed')
+ for _, ep in ipairs(self.aug:match('/augeas//error')) do
+ print(ep, self.aug:get(ep))
+ end
+ assert(false)
+ end
+end
+
+
+return backend
diff --git a/acf2/persistence/backends/files.lua b/acf2/persistence/backends/files.lua
new file mode 100644
index 0000000..70ff33e
--- /dev/null
+++ b/acf2/persistence/backends/files.lua
@@ -0,0 +1,107 @@
+--[[
+Copyright (c) 2012-2013 Kaarle Ritvanen
+See LICENSE file for license details
+--]]
+
+local topology = require('acf2.model.root').topology
+local pth = require('acf2.path')
+local util = require('acf2.persistence.util')
+local copy = require('acf2.util').copy
+
+local posix = require('posix')
+local stringy = require('stringy')
+
+
+local function get_scope(top)
+ if not top or top.type ~= 'reference' or not pth.is_unique(top.scope) then
+ return
+ end
+
+ return stringy.startswith(
+ top.scope, '/files/'
+ ) and top.scope:sub(7, -1) or nil
+end
+
+
+local backend = require('acf2.object').class()
+
+-- TODO cache expiration
+function backend:init() self.cache = {} end
+
+function backend:get(path, top)
+ local name = pth.join('/', unpack(path))
+
+ if not self.cache[name] then
+ local t = posix.stat(name, 'type')
+ if not t then return end
+
+ if t == 'regular' then
+ self.cache[name] = util.read_file(name)
+
+ elseif t == 'link' then
+ -- TODO handle relative symlinks
+ local target = posix.readlink(name)
+ assert(target)
+
+ local scope = get_scope(top)
+ assert(scope)
+ scope = scope..'/'
+
+ local slen = scope:len()
+ assert(target:sub(1, slen) == scope)
+ return target:sub(slen + 1, -1)
+
+ elseif t == 'directory' then
+ local res = {}
+ for _, fname in ipairs(posix.dir(name)) do
+ if not ({['.']=true, ['..']=true})[fname] then
+ table.insert(res, pth.name(fname))
+ end
+ end
+ return res
+
+ else error('Unsupported file type: '..name) end
+ end
+
+ return self.cache[name]
+end
+
+function backend:set(mods)
+ for _, mod in pairs(mods) do
+ local path, value = unpack(mod)
+ local name = pth.join('/', unpack(path))
+
+ if value == nil then
+ print('DEL', name)
+
+ local t = posix.stat(name, 'type')
+ if t == 'directory' then
+ assert(posix.rmdir(name))
+ elseif t then assert(os.remove(name)) end
+
+ self.cache[name] = nil
+
+ elseif type(value) == 'table' then
+ assert(posix.mkdir(name))
+
+ else
+ local scope = get_scope(topology('/files'..name))
+
+ if scope then
+ -- TODO use relative symlink
+ os.remove(name)
+ assert(posix.link(pth.to_absolute(value, scope), name, true))
+
+ else
+ local file = util.open_file(name, 'w')
+ file:write(tostring(value))
+ file:close()
+
+ self.cache[name] = value
+ end
+ end
+ end
+end
+
+
+return backend
diff --git a/acf2/persistence/backends/json.lua b/acf2/persistence/backends/json.lua
new file mode 100644
index 0000000..809d947
--- /dev/null
+++ b/acf2/persistence/backends/json.lua
@@ -0,0 +1,79 @@
+--[[
+Copyright (c) 2012-2013 Kaarle Ritvanen
+See LICENSE file for license details
+--]]
+
+local pth = require('acf2.path')
+local Cache = require('acf2.persistence.backends.volatile')
+local util = require('acf2.persistence.util')
+local copy = require('acf2.util').copy
+
+local json = require('cjson')
+local posix = require('posix')
+
+
+local backend = require('acf2.object').class()
+
+function backend:init()
+ -- TODO cache expiration
+ self.cache = {}
+ self.dirty = {}
+end
+
+function backend:split_path(path)
+ local fpath = copy(path)
+ local jpath = {}
+ local res
+
+ while #fpath > 0 do
+ local fp = pth.join('/', unpack(fpath))
+ if self.cache[fp] then return fp, jpath end
+ table.insert(jpath, 1, fpath[#fpath])
+ table.remove(fpath)
+ end
+
+ fpath = '/'
+
+ while true do
+ fpath = pth.join(fpath, jpath[1])
+ table.remove(jpath, 1)
+
+ local t = posix.stat(fpath, 'type')
+ if t == 'link' then t = posix.stat(posix.readlink(fpath), 'type') end
+ if not t or not ({directory=true, regular=true})[t] then
+ error('File or directory does not exist: '..fpath)
+ end
+
+ if t == 'regular' then return fpath, jpath end
+
+ assert(#jpath > 0)
+ end
+end
+
+function backend:get(path, top)
+ local fpath, jpath = self:split_path(path)
+ if not self.cache[fpath] then
+ self.cache[fpath] = Cache(json.decode(util.read_file(fpath)))
+ end
+ return self.cache[fpath]:get(jpath, top)
+end
+
+function backend:set(mods)
+ local dirty = {}
+
+ for _, mod in ipairs(mods) do
+ local path, value = unpack(mod)
+ local fpath, jpath = self:split_path(path)
+ self.cache[fpath]:_set(jpath, value)
+ dirty[fpath] = true
+ end
+
+ for path, _ in pairs(dirty) do
+ local file = util.open_file(path, 'w')
+ file:write(json.encode(self.cache[path]:_get{}))
+ file:close()
+ end
+end
+
+
+return backend
diff --git a/acf2/persistence/backends/null.lua b/acf2/persistence/backends/null.lua
new file mode 100644
index 0000000..118f35b
--- /dev/null
+++ b/acf2/persistence/backends/null.lua
@@ -0,0 +1,10 @@
+--[[
+Copyright (c) 2012-2013 Kaarle Ritvanen
+See LICENSE file for license details
+--]]
+
+local backend = require('acf2.object').class()
+function backend:get(path, top) if #path == 0 then return {} end end
+function backend:set(mods) end
+
+return backend
diff --git a/acf2/persistence/backends/volatile.lua b/acf2/persistence/backends/volatile.lua
new file mode 100644
index 0000000..e33068d
--- /dev/null
+++ b/acf2/persistence/backends/volatile.lua
@@ -0,0 +1,46 @@
+--[[
+Copyright (c) 2012-2013 Kaarle Ritvanen
+See LICENSE file for license details
+--]]
+
+local util = require('acf2.util')
+
+
+local backend = require('acf2.object').class()
+
+function backend:init(data) self.data = data or {} end
+
+function backend:_get(path)
+ local res = self.data
+ for _, comp in ipairs(path) do
+ if res == nil then return end
+ assert(type(res) == 'table')
+ res = res[comp]
+ end
+ return res
+end
+
+function backend:get(path, top)
+ local res = self:_get(path)
+ return type(res) == 'table' and util.keys(res) or res
+end
+
+function backend:_set(path, value)
+ if type(value) == 'table' then value = {} end
+
+ if #path == 0 then self.data = value
+
+ else
+ local comps = util.copy(path)
+ local name = comps[#comps]
+ table.remove(comps)
+ self:_get(comps)[name] = value
+ end
+end
+
+function backend:set(mods)
+ for _, mod in ipairs(mods) do self:_set(unpack(mod)) end
+end
+
+
+return backend
diff --git a/acf2/persistence/init.lua b/acf2/persistence/init.lua
new file mode 100644
index 0000000..1dca61d
--- /dev/null
+++ b/acf2/persistence/init.lua
@@ -0,0 +1,79 @@
+--[[
+Copyright (c) 2012-2013 Kaarle Ritvanen
+See LICENSE file for license details
+--]]
+
+local M = {}
+
+local loadmods = require('acf2.loader')
+local topology = require('acf2.model.root').topology
+local object = require('acf2.object')
+local pth = require('acf2.path')
+local util = require('acf2.util')
+
+local stringy = require('stringy')
+
+
+local DataStore = object.class(
+ require('acf2.transaction.backend').TransactionBackend
+)
+
+function DataStore:init()
+ object.super(self, DataStore):init()
+ self.backends = util.map(
+ function(m) return m() end,
+ loadmods('persistence/backends')
+ )
+end
+
+function DataStore:split_path(path)
+ local comps = pth.split(path)
+ local backend = self.backends[comps[1]]
+ assert(backend)
+ table.remove(comps, 1)
+ return backend, comps
+end
+
+function DataStore:get(path)
+ local backend, comps = self:split_path(path)
+ local top = topology(path)
+
+ local res = backend:get(comps, top)
+
+ if top then
+ local t = top.type
+ if t and res ~= nil then
+ local atype = type(res)
+
+ if t == 'table' then assert(atype == 'table')
+
+ else
+ assert(atype ~= 'table')
+
+ if t == 'string' then res = tostring(res)
+ elseif t == 'number' then res = tonumber(res)
+ elseif t == 'boolean' then
+ res = (res and res ~= 'false') and true or false
+ elseif t == 'reference' then assert(atype == 'string')
+ else assert(false) end
+ end
+ end
+ end
+
+ return util.copy(res), self.mod_time[path] or 0
+end
+
+function DataStore:_set_multiple(mods)
+ local bms = {}
+
+ for _, mod in ipairs(mods) do
+ local path, value = unpack(mod)
+ local backend, comps = self:split_path(path)
+ table.insert(util.setdefault(bms, backend, {}), {comps, value})
+ end
+
+ for backend, bm in pairs(bms) do backend:set(bm) end
+end
+
+
+return DataStore()
diff --git a/acf2/persistence/util.lua b/acf2/persistence/util.lua
new file mode 100644
index 0000000..a657411
--- /dev/null
+++ b/acf2/persistence/util.lua
@@ -0,0 +1,22 @@
+--[[
+Copyright (c) 2012-2013 Kaarle Ritvanen
+See LICENSE file for license details
+--]]
+
+local M = {}
+
+function M.open_file(path, mode)
+ local file = io.open(path, mode)
+ if not file then error('Cannot open file: '..path) end
+ return file
+end
+
+function M.read_file(path)
+ local file = M.open_file(path)
+ local data = ''
+ for line in file:lines() do data = data..line end
+ file:close()
+ return data
+end
+
+return M