Merge with Zash
[prosody.git] / util / datamanager.lua
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 pcall = pcall;
15 local log = require "util.logger".init("datamanager");
16 local io_open = io.open;
17 local os_remove = os.remove;
18 local os_rename = os.rename;
19 local tostring, tonumber = tostring, tonumber;
20 local error = error;
21 local next = next;
22 local t_insert = table.insert;
23 local t_concat = table.concat;
24 local envloadfile = require"util.envload".envloadfile;
25 local serialize = require "util.serialization".serialize;
26 local path_separator = assert ( package.config:match ( "^([^\n]+)" ) , "package.config not in standard form" ) -- Extract directory seperator from package.config (an undocumented string that comes with lua)
27 local lfs = require "lfs";
28 local prosody = prosody;
29 local raw_mkdir;
30 local fallocate;
31
32 if prosody.platform == "posix" then
33         raw_mkdir = require "util.pposix".mkdir; -- Doesn't trample on umask
34         fallocate = require "util.pposix".fallocate;
35 else
36         raw_mkdir = lfs.mkdir;
37 end
38
39 if not fallocate then -- Fallback
40         function fallocate(f, offset, len)
41                 -- This assumes that current position == offset
42                 local fake_data = (" "):rep(len);
43                 local ok, msg = f:write(fake_data);
44                 if not ok then
45                         return ok, msg;
46                 end
47                 f:seek(offset);
48                 return true;
49         end
50 end
51
52 module "datamanager"
53
54 ---- utils -----
55 local encode, decode;
56 do
57         local urlcodes = setmetatable({}, { __index = function (t, k) t[k] = char(tonumber("0x"..k)); return t[k]; end });
58
59         decode = function (s)
60                 return s and (s:gsub("+", " "):gsub("%%([a-fA-F0-9][a-fA-F0-9])", urlcodes));
61         end
62
63         encode = function (s)
64                 return s and (s:gsub("%W", function (c) return format("%%%02x", c:byte()); end));
65         end
66 end
67
68 local _mkdir = {};
69 local function mkdir(path)
70         path = path:gsub("/", path_separator); -- TODO as an optimization, do this during path creation rather than here
71         if not _mkdir[path] then
72                 raw_mkdir(path);
73                 _mkdir[path] = true;
74         end
75         return path;
76 end
77
78 local data_path = (prosody and prosody.paths and prosody.paths.data) or ".";
79 local callbacks = {};
80
81 ------- API -------------
82
83 function set_data_path(path)
84         log("debug", "Setting data path to: %s", path);
85         data_path = path;
86 end
87
88 local function callback(username, host, datastore, data)
89         for _, f in ipairs(callbacks) do
90                 username, host, datastore, data = f(username, host, datastore, data);
91                 if username == false then break; end
92         end
93
94         return username, host, datastore, data;
95 end
96 function add_callback(func)
97         if not callbacks[func] then -- Would you really want to set the same callback more than once?
98                 callbacks[func] = true;
99                 callbacks[#callbacks+1] = func;
100                 return true;
101         end
102 end
103 function remove_callback(func)
104         if callbacks[func] then
105                 for i, f in ipairs(callbacks) do
106                         if f == func then
107                                 callbacks[i] = nil;
108                                 callbacks[f] = nil;
109                                 return true;
110                         end
111                 end
112         end
113 end
114
115 function getpath(username, host, datastore, ext, create)
116         ext = ext or "dat";
117         host = (host and encode(host)) or "_global";
118         username = username and encode(username);
119         if username then
120                 if create then mkdir(mkdir(mkdir(data_path).."/"..host).."/"..datastore); end
121                 return format("%s/%s/%s/%s.%s", data_path, host, datastore, username, ext);
122         elseif host then
123                 if create then mkdir(mkdir(data_path).."/"..host); end
124                 return format("%s/%s/%s.%s", data_path, host, datastore, ext);
125         else
126                 if create then mkdir(data_path); end
127                 return format("%s/%s.%s", data_path, datastore, ext);
128         end
129 end
130
131 function load(username, host, datastore)
132         local data, ret = envloadfile(getpath(username, host, datastore), {});
133         if not data then
134                 local mode = lfs.attributes(getpath(username, host, datastore), "mode");
135                 if not mode then
136                         log("debug", "Assuming empty %s storage ('%s') for user: %s@%s", datastore, ret, username or "nil", host or "nil");
137                         return nil;
138                 else -- file exists, but can't be read
139                         -- TODO more detailed error checking and logging?
140                         log("error", "Failed to load %s storage ('%s') for user: %s@%s", datastore, ret, username or "nil", host or "nil");
141                         return nil, "Error reading storage";
142                 end
143         end
144
145         local success, ret = pcall(data);
146         if not success then
147                 log("error", "Unable to load %s storage ('%s') for user: %s@%s", datastore, ret, username or "nil", host or "nil");
148                 return nil, "Error reading storage";
149         end
150         return ret;
151 end
152
153 local function atomic_store(filename, data)
154         local scratch = filename.."~";
155         local f, ok, msg;
156         repeat
157                 f, msg = io_open(scratch, "w");
158                 if not f then break end
159
160                 ok, msg = f:write(data);
161                 if not ok then break end
162
163                 ok, msg = f:close();
164                 if not ok then break end
165
166                 return os_rename(scratch, filename);
167         until false;
168
169         -- Cleanup
170         if f then f:close(); end
171         os_remove(scratch);
172         return nil, msg;
173 end
174
175 function store(username, host, datastore, data)
176         if not data then
177                 data = {};
178         end
179
180         username, host, datastore, data = callback(username, host, datastore, data);
181         if username == false then
182                 return true; -- Don't save this data at all
183         end
184
185         -- save the datastore
186         local d = "return " .. serialize(data) .. ";\n";
187         local ok, msg = atomic_store(getpath(username, host, datastore, nil, true), d);
188         if not ok then
189                 log("error", "Unable to write to %s storage ('%s') for user: %s@%s", datastore, msg, username or "nil", host or "nil");
190                 return nil, "Error saving to storage";
191         end
192         if next(data) == nil then -- try to delete empty datastore
193                 log("debug", "Removing empty %s datastore for user %s@%s", datastore, username or "nil", host or "nil");
194                 os_remove(getpath(username, host, datastore));
195         end
196         -- we write data even when we are deleting because lua doesn't have a
197         -- platform independent way of checking for non-exisitng files
198         return true;
199 end
200
201 function list_append(username, host, datastore, data)
202         if not data then return; end
203         if callback(username, host, datastore) == false then return true; end
204         -- save the datastore
205         local f, msg = io_open(getpath(username, host, datastore, "list", true), "a");
206         if not f then
207                 log("error", "Unable to write to %s storage ('%s') for user: %s@%s", datastore, msg, username or "nil", host or "nil");
208                 return;
209         end
210         local data = "item(" ..  serialize(data) .. ");\n";
211         local pos = f:seek("end");
212         local ok, msg = fallocate(f, pos, #data);
213         f:seek("set", pos);
214         if ok then
215                 f:write(data);
216         else
217                 log("error", "Unable to write to %s storage ('%s') for user: %s@%s", datastore, msg, username or "nil", host or "nil");
218                 return ok, msg;
219         end
220         f:close();
221         return true;
222 end
223
224 function list_store(username, host, datastore, data)
225         if not data then
226                 data = {};
227         end
228         if callback(username, host, datastore) == false then return true; end
229         -- save the datastore
230         local d = {};
231         for _, item in ipairs(data) do
232                 d[#d+1] = "item(" .. serialize(item) .. ");\n";
233         end
234         local ok, msg = atomic_store(getpath(username, host, datastore, "list", true), t_concat(d));
235         if not ok then
236                 log("error", "Unable to write to %s storage ('%s') for user: %s@%s", datastore, msg, username or "nil", host or "nil");
237                 return;
238         end
239         if next(data) == nil then -- try to delete empty datastore
240                 log("debug", "Removing empty %s datastore for user %s@%s", datastore, username or "nil", host or "nil");
241                 os_remove(getpath(username, host, datastore, "list"));
242         end
243         -- we write data even when we are deleting because lua doesn't have a
244         -- platform independent way of checking for non-exisitng files
245         return true;
246 end
247
248 function list_load(username, host, datastore)
249         local items = {};
250         local data, ret = envloadfile(getpath(username, host, datastore, "list"), {item = function(i) t_insert(items, i); end});
251         if not data then
252                 local mode = lfs.attributes(getpath(username, host, datastore, "list"), "mode");
253                 if not mode then
254                         log("debug", "Assuming empty %s storage ('%s') for user: %s@%s", datastore, ret, username or "nil", host or "nil");
255                         return nil;
256                 else -- file exists, but can't be read
257                         -- TODO more detailed error checking and logging?
258                         log("error", "Failed to load %s storage ('%s') for user: %s@%s", datastore, ret, username or "nil", host or "nil");
259                         return nil, "Error reading storage";
260                 end
261         end
262
263         local success, ret = pcall(data);
264         if not success then
265                 log("error", "Unable to load %s storage ('%s') for user: %s@%s", datastore, ret, username or "nil", host or "nil");
266                 return nil, "Error reading storage";
267         end
268         return items;
269 end
270
271 function list_stores(username, host)
272         if not host then
273                 return nil, "bad argument #2 to 'list_stores' (string expected, got nothing)";
274         end
275         local list = {};
276         local host_dir = format("%s/%s/", data_path, encode(host));
277         for node in lfs.dir(host_dir) do
278                 if not node:match"^%." then -- dots should be encoded, this is probably . or ..
279                         local store = decode(node);
280                         local path = host_dir..node;
281                         if username == true then
282                                 if lfs.attributes(path, "mode") == "directory" then
283                                         list[#list+1] = store;
284                                 end
285                         elseif username then
286                                 if lfs.attributes(getpath(username, host, store), "mode")
287                                         or lfs.attributes(getpath(username, host, store, "list"), "mode") then
288                                         list[#list+1] = store;
289                                 end
290                         elseif lfs.attributes(path, "mode") == "file" then
291                                 list[#list+1] = store:gsub("%.[dalist]+$","");
292                         end
293                 end
294         end
295         return list;
296 end
297
298 function purge(username, host)
299         local host_dir = format("%s/%s/", data_path, encode(host));
300         local deleted = 0;
301         for file in lfs.dir(host_dir) do
302                 if lfs.attributes(host_dir..file, "mode") == "directory" then
303                         local store = decode(file);
304                         deleted = deleted + (os_remove(getpath(username, host, store)) and 1 or 0);
305                         deleted = deleted + (os_remove(getpath(username, host, store, "list")) and 1 or 0);
306                         -- We this will generate loads of "No such file or directory", but do we care?
307                 end
308         end
309         return deleted > 0, deleted;
310 end
311
312 return _M;