database.lua 6.1 KB
Newer Older
1 2 3 4 5 6
local _G = require "_G"
local assert = _G.assert
local ipairs = _G.ipairs
local loadfile = _G.loadfile
local pairs = _G.pairs
local pcall = _G.pcall
7
local select = _G.select
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
local setmetatable = _G.setmetatable

local os = require "os"
local removefile = os.remove
local renamefile = os.rename
local tmpname = os.tmpname

local io = require "io"
local open = io.open

local lfs = require "lfs"
local listdir = lfs.dir
local getattribute = lfs.attributes
local makedir = lfs.mkdir
local removedir = lfs.rmdir

local Viewer = require "loop.debug.Viewer"

local oo = require "openbus.util.oo"
local class = oo.class


local Serializer = Viewer{ nolabels = true }


local function createpath(path)
34 35 36 37 38 39 40 41 42 43 44 45 46
  local result, errmsg = getattribute(path, "mode")
  if result == "directory" then
    result, errmsg = true, nil
  elseif result ~= nil then
    result, errmsg = false, "'"..path..
                            "' expected to be directory (got "..result..")"
  elseif errmsg:match("^cannot obtain information") then
    result, errmsg = makedir(path)
    if not result then
      errmsg = "unable to create directory '"..path.."' ("..errmsg..")"
    end
  end
  return result, errmsg
47 48 49
end

local function removepath(path)
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
  for filename in listdir(path) do
    local filepath = path..filename
    if filename:find("%.lua$")
    and getattribute(filepath, "mode") == "file" then
      local ok, errmsg = removefile(filepath)
      if ok == nil then
        return nil, "unable to remove file '"..filepath.."' ("..errmsg..")"
      end
    end
  end
  local ok, errmsg = removedir(path)
  if ok == nil then
    return nil, "unable to remove directory '"..path.."' ("..errmsg..")"
  end
  return true
65 66 67
end

local function loadfrom_cont(path, ok, ...)
68 69 70
  if ok then return ... end
  local errmsg = ...
  return nil, "corrupted file '"..path.."' ("..errmsg..")"
71 72
end
local function loadfrom(path)
73 74 75 76 77 78 79 80
  local result, errmsg = loadfile(path, "t", {})
  if result == nil then
    if errmsg:find("No such file or directory") then
      return
    end
    return nil, "unable to load file '"..path.."' ("..errmsg..")"
  end
  return loadfrom_cont(path, pcall(result))
81 82 83
end

local function saveto(path, ...)
Renato Figueiro Maia's avatar
Renato Figueiro Maia committed
84 85 86 87 88
  local temp = path..".tmp" -- must be in the same path
                            -- of the final file because
                            -- 'os.rename' can only
                            -- rename files in the same
                            -- file system.
89 90 91 92 93 94 95 96 97 98
  local result, errmsg = open(temp, "w")
  if result == nil then
    errmsg = "unable to create temporary file '"..temp.."' ("..errmsg..")"
  else
    local file = result
    result, errmsg = file:write("return ", Serializer:tostring(...))
    file:close()
    if result == nil then
      errmsg = "unable to write temporary file '"..temp.."' ("..errmsg..")"
    else
99
      local code
100
      result, errmsg, code = renamefile(temp, path)
101
      if result == nil then
102 103
        if errmsg:find("File exists", 1, true) and code == 17 then
          ---[[
104
          assert(removefile(path))
105 106 107 108 109
          --[=[--]]
          result, errmsg = assert(renamefile(path, path .. ".delete.me"))
          assert(removefile(path .. ".delete.me"))
          --]=]
          result, errmsg = renamefile(temp, path)
110 111 112
        else
          errmsg = "unable to replace file '"..path.."' (with file "..errmsg..")"
        end
113 114 115 116 117
      end
    end
    removefile(temp)
  end
  return result, errmsg
118 119 120
end

local function closeobject(obj)
121 122
  obj.path = nil
  setmetatable(obj, nil)
123 124 125 126 127 128
end


local Table = class()

function Table:__init()
129 130 131 132 133 134 135 136 137 138
  function self.iterator(next)
    local file = next()
    while file ~= nil do
      local path = self.path..file
      if getattribute(path, "mode") == "file" then
        return file:match("^(.+)%.lua$"), assert(loadfrom(path))
      end
      file = next()
    end
  end
139 140 141
end

function Table:getentry(key)
142
  return loadfrom(self.path..key..".lua")
143 144 145
end

function Table:setentry(key, ...)
146
  return saveto(self.path..key..".lua", ...)
147 148
end

149
function Table:setentryfield(key, field, ...)
150 151 152 153 154 155 156 157
  local path = self.path..key..".lua"
  local result, errmsg = loadfrom(path)
  if result == nil then
    if errmsg ~= nil then
      return result, errmsg
    end
    result = {}
  end
158 159
  local count = select("#", ...)
  local value = select(count, ...)
160
  local place = result
161
  for i = 1, count-1 do
162 163 164 165 166 167 168
    local value = place[field]
    if value == nil then
      value = {}
      place[field] = value
    end
    place = value
    field = select(i, ...)
169
  end
170
  place[field] = value
171 172
  result, errmsg = saveto(path, result)
  return result, errmsg
173 174 175
end

function Table:removeentry(key)
176 177 178 179 180 181
  local path = self.path..key..".lua"
  local ok, errmsg = removefile(path)
  if ok == nil then
    return nil, "unable to remove file '"..path.."' ("..errmsg..")"
  end
  return true
182 183 184
end

function Table:ientries()
185
  return self.iterator, listdir(self.path)
186 187 188
end

function Table:remove()
189 190 191 192 193 194 195
  local result, errmsg = removepath(self.path)
  if result then
    self.base.tables[self.name] = nil
    self.base, self.name = nil, nil
    closeobject(self)
  end
  return result, errmsg
196 197 198 199 200 201 202 203
end


local TableNamePat = "[^.][^/\\?*]*"

local DataBase = class()

function DataBase:__init()
204 205 206 207 208 209 210 211 212 213
  local path = self.path
  local tables = {}
  for name in listdir(path) do
    local tablepath = path..name.."/"
    if name:match(TableNamePat)
    and getattribute(tablepath, "mode") == "directory" then
      tables[name] = Table{ base = self, name = name, path = tablepath }
    end
  end
  self.tables = tables
214 215 216
end

function DataBase:gettable(name)
217 218 219 220 221 222 223 224 225 226 227
  local table = self.tables[name]
  if table == nil then
    local path = self.path..name
    local result, errmsg = createpath(path)
    if not result then
      return nil, errmsg
    end
    table = Table{ base = self, name = name, path = path.."/" }
    self.tables[name] = table
  end
  return table
228 229 230
end

function DataBase:itables()
231
  return pairs(self.tables)
232 233 234
end

function DataBase:close()
235 236 237 238 239
  for name, table in pairs(self.tables) do
    closeobject(table)
  end
  closeobject(self)
  return true
240 241 242 243 244 245
end


local module = {}

function module.open(path)
246 247 248
  local result, errmsg = createpath(path)
  if not result then return nil, errmsg end
  return DataBase{ path = path.."/" }
249 250 251
end

return module