| 1 | -- Prosody IM |
| 2 | -- Copyright (C) 2008-2010 Matthew Wild |
| 3 | -- Copyright (C) 2008-2010 Waqas Hussain |
| 4 | -- |
| 5 | -- This project is MIT/X11 licensed. Please see the |
| 6 | -- COPYING file in the source package for more information. |
| 7 | -- |
| 8 | |
| 9 | |
| 10 | local format = string.format; |
| 11 | local setmetatable, type = setmetatable, type; |
| 12 | local pairs, ipairs = pairs, ipairs; |
| 13 | local char = string.char; |
| 14 | local loadfile, setfenv, pcall = loadfile, setfenv, pcall; |
| 15 | local log = require "util.logger".init("datamanager"); |
| 16 | local io_open = io.open; |
| 17 | local os_remove = os.remove; |
| 18 | local tostring, tonumber = tostring, tonumber; |
| 19 | local error = error; |
| 20 | local next = next; |
| 21 | local t_insert = table.insert; |
| 22 | local append = require "util.serialization".append; |
| 23 | local path_separator = "/"; if os.getenv("WINDIR") then path_separator = "\\" end |
| 24 | local lfs = require "lfs"; |
| 25 | local raw_mkdir; |
| 26 | |
| 27 | if prosody.platform == "posix" then |
| 28 | raw_mkdir = require "util.pposix".mkdir; -- Doesn't trample on umask |
| 29 | else |
| 30 | raw_mkdir = lfs.mkdir; |
| 31 | end |
| 32 | |
| 33 | module "datamanager" |
| 34 | |
| 35 | ---- utils ----- |
| 36 | local encode, decode; |
| 37 | do |
| 38 | local urlcodes = setmetatable({}, { __index = function (t, k) t[k] = char(tonumber("0x"..k)); return t[k]; end }); |
| 39 | |
| 40 | decode = function (s) |
| 41 | return s and (s:gsub("+", " "):gsub("%%([a-fA-F0-9][a-fA-F0-9])", urlcodes)); |
| 42 | end |
| 43 | |
| 44 | encode = function (s) |
| 45 | return s and (s:gsub("%W", function (c) return format("%%%02x", c:byte()); end)); |
| 46 | end |
| 47 | end |
| 48 | |
| 49 | local _mkdir = {}; |
| 50 | local function mkdir(path) |
| 51 | path = path:gsub("/", path_separator); -- TODO as an optimization, do this during path creation rather than here |
| 52 | if not _mkdir[path] then |
| 53 | raw_mkdir(path); |
| 54 | _mkdir[path] = true; |
| 55 | end |
| 56 | return path; |
| 57 | end |
| 58 | |
| 59 | local data_path = "data"; |
| 60 | local callbacks = {}; |
| 61 | |
| 62 | ------- API ------------- |
| 63 | |
| 64 | function set_data_path(path) |
| 65 | log("debug", "Setting data path to: %s", path); |
| 66 | data_path = path; |
| 67 | end |
| 68 | |
| 69 | local function callback(username, host, datastore, data) |
| 70 | for _, f in ipairs(callbacks) do |
| 71 | username, host, datastore, data = f(username, host, datastore, data); |
| 72 | if username == false then break; end |
| 73 | end |
| 74 | |
| 75 | return username, host, datastore, data; |
| 76 | end |
| 77 | function add_callback(func) |
| 78 | if not callbacks[func] then -- Would you really want to set the same callback more than once? |
| 79 | callbacks[func] = true; |
| 80 | callbacks[#callbacks+1] = func; |
| 81 | return true; |
| 82 | end |
| 83 | end |
| 84 | function remove_callback(func) |
| 85 | if callbacks[func] then |
| 86 | for i, f in ipairs(callbacks) do |
| 87 | if f == func then |
| 88 | callbacks[i] = nil; |
| 89 | callbacks[f] = nil; |
| 90 | return true; |
| 91 | end |
| 92 | end |
| 93 | end |
| 94 | end |
| 95 | |
| 96 | function getpath(username, host, datastore, ext, create) |
| 97 | ext = ext or "dat"; |
| 98 | host = (host and encode(host)) or "_global"; |
| 99 | username = username and encode(username); |
| 100 | if username then |
| 101 | if create then mkdir(mkdir(mkdir(data_path).."/"..host).."/"..datastore); end |
| 102 | return format("%s/%s/%s/%s.%s", data_path, host, datastore, username, ext); |
| 103 | elseif host then |
| 104 | if create then mkdir(mkdir(data_path).."/"..host); end |
| 105 | return format("%s/%s/%s.%s", data_path, host, datastore, ext); |
| 106 | else |
| 107 | if create then mkdir(data_path); end |
| 108 | return format("%s/%s.%s", data_path, datastore, ext); |
| 109 | end |
| 110 | end |
| 111 | |
| 112 | function load(username, host, datastore) |
| 113 | local data, ret = loadfile(getpath(username, host, datastore)); |
| 114 | if not data then |
| 115 | local mode = lfs.attributes(getpath(username, host, datastore), "mode"); |
| 116 | if not mode then |
| 117 | log("debug", "Failed to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil")); |
| 118 | return nil; |
| 119 | else -- file exists, but can't be read |
| 120 | -- TODO more detailed error checking and logging? |
| 121 | log("error", "Failed to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil")); |
| 122 | return nil, "Error reading storage"; |
| 123 | end |
| 124 | end |
| 125 | setfenv(data, {}); |
| 126 | local success, ret = pcall(data); |
| 127 | if not success then |
| 128 | log("error", "Unable to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil")); |
| 129 | return nil, "Error reading storage"; |
| 130 | end |
| 131 | return ret; |
| 132 | end |
| 133 | |
| 134 | function store(username, host, datastore, data) |
| 135 | if not data then |
| 136 | data = {}; |
| 137 | end |
| 138 | |
| 139 | username, host, datastore, data = callback(username, host, datastore, data); |
| 140 | if username == false then |
| 141 | return true; -- Don't save this data at all |
| 142 | end |
| 143 | |
| 144 | -- save the datastore |
| 145 | local f, msg = io_open(getpath(username, host, datastore, nil, true), "w+"); |
| 146 | if not f then |
| 147 | log("error", "Unable to write to "..datastore.." storage ('"..msg.."') for user: "..(username or "nil").."@"..(host or "nil")); |
| 148 | return nil, "Error saving to storage"; |
| 149 | end |
| 150 | f:write("return "); |
| 151 | append(f, data); |
| 152 | f:close(); |
| 153 | if next(data) == nil then -- try to delete empty datastore |
| 154 | log("debug", "Removing empty %s datastore for user %s@%s", datastore, username or "nil", host or "nil"); |
| 155 | os_remove(getpath(username, host, datastore)); |
| 156 | end |
| 157 | -- we write data even when we are deleting because lua doesn't have a |
| 158 | -- platform independent way of checking for non-exisitng files |
| 159 | return true; |
| 160 | end |
| 161 | |
| 162 | function list_append(username, host, datastore, data) |
| 163 | if not data then return; end |
| 164 | if callback(username, host, datastore) == false then return true; end |
| 165 | -- save the datastore |
| 166 | local f, msg = io_open(getpath(username, host, datastore, "list", true), "a+"); |
| 167 | if not f then |
| 168 | log("error", "Unable to write to "..datastore.." storage ('"..msg.."') for user: "..(username or "nil").."@"..(host or "nil")); |
| 169 | return; |
| 170 | end |
| 171 | f:write("item("); |
| 172 | append(f, data); |
| 173 | f:write(");\n"); |
| 174 | f:close(); |
| 175 | return true; |
| 176 | end |
| 177 | |
| 178 | function list_store(username, host, datastore, data) |
| 179 | if not data then |
| 180 | data = {}; |
| 181 | end |
| 182 | if callback(username, host, datastore) == false then return true; end |
| 183 | -- save the datastore |
| 184 | local f, msg = io_open(getpath(username, host, datastore, "list", true), "w+"); |
| 185 | if not f then |
| 186 | log("error", "Unable to write to "..datastore.." storage ('"..msg.."') for user: "..(username or "nil").."@"..(host or "nil")); |
| 187 | return; |
| 188 | end |
| 189 | for _, d in ipairs(data) do |
| 190 | f:write("item("); |
| 191 | append(f, d); |
| 192 | f:write(");\n"); |
| 193 | end |
| 194 | f:close(); |
| 195 | if next(data) == nil then -- try to delete empty datastore |
| 196 | log("debug", "Removing empty %s datastore for user %s@%s", datastore, username or "nil", host or "nil"); |
| 197 | os_remove(getpath(username, host, datastore, "list")); |
| 198 | end |
| 199 | -- we write data even when we are deleting because lua doesn't have a |
| 200 | -- platform independent way of checking for non-exisitng files |
| 201 | return true; |
| 202 | end |
| 203 | |
| 204 | function list_load(username, host, datastore) |
| 205 | local data, ret = loadfile(getpath(username, host, datastore, "list")); |
| 206 | if not data then |
| 207 | local mode = lfs.attributes(getpath(username, host, datastore, "list"), "mode"); |
| 208 | if not mode then |
| 209 | log("debug", "Failed to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil")); |
| 210 | return nil; |
| 211 | else -- file exists, but can't be read |
| 212 | -- TODO more detailed error checking and logging? |
| 213 | log("error", "Failed to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil")); |
| 214 | return nil, "Error reading storage"; |
| 215 | end |
| 216 | end |
| 217 | local items = {}; |
| 218 | setfenv(data, {item = function(i) t_insert(items, i); end}); |
| 219 | local success, ret = pcall(data); |
| 220 | if not success then |
| 221 | log("error", "Unable to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil")); |
| 222 | return nil, "Error reading storage"; |
| 223 | end |
| 224 | return items; |
| 225 | end |
| 226 | |
| 227 | return _M; |