util.datamanager: Added support for hooks to override behavior.
[prosody.git] / util / datamanager.lua
1 -- Prosody IM
2 -- Copyright (C) 2008-2009 Matthew Wild
3 -- Copyright (C) 2008-2009 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 raw_mkdir;
25
26 if prosody.platform == "posix" then
27         raw_mkdir = require "util.pposix".mkdir; -- Doesn't trample on umask
28 else
29         raw_mkdir = require "lfs".mkdir;
30 end
31
32 module "datamanager"
33
34 ---- utils -----
35 local encode, decode;
36 do
37         local urlcodes = setmetatable({}, { __index = function (t, k) t[k] = char(tonumber("0x"..k)); return t[k]; end });
38
39         decode = function (s)
40                 return s and (s:gsub("+", " "):gsub("%%([a-fA-F0-9][a-fA-F0-9])", urlcodes));
41         end
42
43         encode = function (s)
44                 return s and (s:gsub("%W", function (c) return format("%%%02x", c:byte()); end));
45         end
46 end
47
48 local _mkdir = {};
49 local function mkdir(path)
50         path = path:gsub("/", path_separator); -- TODO as an optimization, do this during path creation rather than here
51         if not _mkdir[path] then
52                 raw_mkdir(path);
53                 _mkdir[path] = true;
54         end
55         return path;
56 end
57
58 local data_path = "data";
59 local callbacks = {};
60
61 ------- API -------------
62
63 local _set_data_path;
64 function set_data_path(path)
65         if _set_data_path then return _set_data_path(path); end
66         log("debug", "Setting data path to: %s", path);
67         data_path = path;
68 end
69
70 local function callback(username, host, datastore, data)
71         for _, f in ipairs(callbacks) do
72                 username, host, datastore, data = f(username, host, datastore, data);
73                 if username == false then break; end
74         end
75         
76         return username, host, datastore, data;
77 end
78 local _add_callback;
79 function add_callback(func)
80         if _add_callback then return _add_callback(func); end
81         if not callbacks[func] then -- Would you really want to set the same callback more than once?
82                 callbacks[func] = true;
83                 callbacks[#callbacks+1] = func;
84                 return true;
85         end
86 end
87 local _remove_callback;
88 function remove_callback(func)
89         if _remove_callback then return _remove_callback(func); end
90         if callbacks[func] then
91                 for i, f in ipairs(callbacks) do
92                         if f == func then
93                                 callbacks[i] = nil;
94                                 callbacks[f] = nil;
95                                 return true;
96                         end
97                 end
98         end
99 end
100
101 local _getpath;
102 function getpath(username, host, datastore, ext, create)
103         if _getpath then return _getpath(username, host, datastore, ext, create); end
104         ext = ext or "dat";
105         host = (host and encode(host)) or "_global";
106         username = username and encode(username);
107         if username then
108                 if create then mkdir(mkdir(mkdir(data_path).."/"..host).."/"..datastore); end
109                 return format("%s/%s/%s/%s.%s", data_path, host, datastore, username, ext);
110         elseif host then
111                 if create then mkdir(mkdir(data_path).."/"..host); end
112                 return format("%s/%s/%s.%s", data_path, host, datastore, ext);
113         else
114                 if create then mkdir(data_path); end
115                 return format("%s/%s.%s", data_path, datastore, ext);
116         end
117 end
118
119 local _load;
120 function load(username, host, datastore)
121         if _load then return _load(username, host, datastore); end
122         local data, ret = loadfile(getpath(username, host, datastore));
123         if not data then
124                 log("debug", "Failed to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil"));
125                 return nil;
126         end
127         setfenv(data, {});
128         local success, ret = pcall(data);
129         if not success then
130                 log("error", "Unable to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil"));
131                 return nil;
132         end
133         return ret;
134 end
135
136 local _store;
137 function store(username, host, datastore, data)
138         if _store then return _store(username, host, datastore, data); end
139         if not data then
140                 data = {};
141         end
142
143         username, host, datastore, data = callback(username, host, datastore, data);
144         if username == false then
145                 return true; -- Don't save this data at all
146         end
147
148         -- save the datastore
149         local f, msg = io_open(getpath(username, host, datastore, nil, true), "w+");
150         if not f then
151                 log("error", "Unable to write to "..datastore.." storage ('"..msg.."') for user: "..(username or "nil").."@"..(host or "nil"));
152                 return;
153         end
154         f:write("return ");
155         append(f, data);
156         f:close();
157         if next(data) == nil then -- try to delete empty datastore
158                 log("debug", "Removing empty %s datastore for user %s@%s", datastore, username or "nil", host or "nil");
159                 os_remove(getpath(username, host, datastore));
160         end
161         -- we write data even when we are deleting because lua doesn't have a
162         -- platform independent way of checking for non-exisitng files
163         return true;
164 end
165
166 local _list_append;
167 function list_append(username, host, datastore, data)
168         if _list_append then return _list_append(username, host, datastore, data); end
169         if not data then return; end
170         if callback(username, host, datastore) == false then return true; end
171         -- save the datastore
172         local f, msg = io_open(getpath(username, host, datastore, "list", true), "a+");
173         if not f then
174                 log("error", "Unable to write to "..datastore.." storage ('"..msg.."') for user: "..(username or "nil").."@"..(host or "nil"));
175                 return;
176         end
177         f:write("item(");
178         append(f, data);
179         f:write(");\n");
180         f:close();
181         return true;
182 end
183
184 local _list_store;
185 function list_store(username, host, datastore, data)
186         if _list_store then return _list_store(username, host, datastore, data); end
187         if not data then
188                 data = {};
189         end
190         if callback(username, host, datastore) == false then return true; end
191         -- save the datastore
192         local f, msg = io_open(getpath(username, host, datastore, "list", true), "w+");
193         if not f then
194                 log("error", "Unable to write to "..datastore.." storage ('"..msg.."') for user: "..(username or "nil").."@"..(host or "nil"));
195                 return;
196         end
197         for _, d in ipairs(data) do
198                 f:write("item(");
199                 append(f, d);
200                 f:write(");\n");
201         end
202         f:close();
203         if next(data) == nil then -- try to delete empty datastore
204                 log("debug", "Removing empty %s datastore for user %s@%s", datastore, username or "nil", host or "nil");
205                 os_remove(getpath(username, host, datastore, "list"));
206         end
207         -- we write data even when we are deleting because lua doesn't have a
208         -- platform independent way of checking for non-exisitng files
209         return true;
210 end
211
212 local _list_load;
213 function list_load(username, host, datastore)
214         if _list_load then return _list_load(username, host, datastore); end
215         local data, ret = loadfile(getpath(username, host, datastore, "list"));
216         if not data then
217                 log("debug", "Failed to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil"));
218                 return nil;
219         end
220         local items = {};
221         setfenv(data, {item = function(i) t_insert(items, i); end});
222         local success, ret = pcall(data);
223         if not success then
224                 log("error", "Unable to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil"));
225                 return nil;
226         end
227         return items;
228 end
229
230 function set(t)
231         _set_data_path = t.set_data_path;
232         _add_callback = t.add_callback;
233         _remove_callback = t.remove_callback;
234         _getpath = t.getpath;
235         _load = t.load;
236         _store = t.store;
237         _list_append = t.list_append;
238         _list_store = t.list_store;
239         _list_load = t.list_load;
240 end
241 function get()
242         return {
243                 set_data_path = _set_data_path;
244                 add_callback = _add_callback;
245                 remove_callback = _remove_callback;
246                 getpath = _getpath;
247                 load = _load;
248                 store = _store;
249                 list_append = _list_append;
250                 list_store = _list_store;
251                 list_load = _list_load;
252         };
253 end
254
255 return _M;