2 -- Copyright (C) 2008-2010 Matthew Wild
3 -- Copyright (C) 2008-2010 Waqas Hussain
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
10 local format = string.format;
11 local setmetatable
= setmetatable
;
12 local ipairs
= ipairs
;
13 local char
= string.char
;
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 tonumber = tonumber;
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 lfs
= require
"lfs";
27 -- Extract directory separator from package.config (an undocumented string that comes with lua)
28 local path_separator
= assert ( package
.config
:match ( "^([^\n]+)" ) , "package.config not in standard form" )
30 local prosody
= prosody
;
32 local raw_mkdir
= lfs
.mkdir
;
36 local pposix
= require
"util.pposix";
37 raw_mkdir
= pposix
.mkdir
or raw_mkdir
; -- Doesn't trample on umask
38 atomic_append
= pposix
.atomic_append
;
39 ENOENT
= pposix
.ENOENT
or ENOENT
;
46 local encode
, decode
, store_encode
;
48 local urlcodes
= setmetatable({}, { __index
= function (t
, k
) t
[k
] = char(tonumber(k
, 16)); return t
[k
]; end });
51 return s
and (s
:gsub("%%(%x%x)", urlcodes
));
55 return s
and (s
:gsub("%W", function (c
) return format("%%%02x", c
:byte()); end));
58 -- Special encode function for store names, which historically were unencoded.
59 -- All currently known stores use a-z and underscore, so this one preserves underscores.
60 store_encode
= function (s
)
61 return s
and (s
:gsub("[^_%w]", function (c
) return format("%%%02x", c
:byte()); end));
65 if not atomic_append
then
66 function atomic_append(f
, data
)
68 if not f
:write(data
) or not f
:flush() then
70 f
:write((" "):rep(#data
));
72 return nil, "write-failed";
79 local function mkdir(path
)
80 path
= path
:gsub("/", path_separator
); -- TODO as an optimization, do this during path creation rather than here
81 if not _mkdir
[path
] then
88 local data_path
= (prosody
and prosody
.paths
and prosody
.paths
.data
) or ".";
91 ------- API -------------
93 local function set_data_path(path
)
94 log("debug", "Setting data path to: %s", path
);
98 local function callback(username
, host
, datastore
, data
)
99 for _
, f
in ipairs(callbacks
) do
100 username
, host
, datastore
, data
= f(username
, host
, datastore
, data
);
101 if username
== false then break; end
104 return username
, host
, datastore
, data
;
106 local function add_callback(func
)
107 if not callbacks
[func
] then -- Would you really want to set the same callback more than once?
108 callbacks
[func
] = true;
109 callbacks
[#callbacks
+1] = func
;
113 local function remove_callback(func
)
114 if callbacks
[func
] then
115 for i
, f
in ipairs(callbacks
) do
125 local function getpath(username
, host
, datastore
, ext
, create
)
127 host
= (host
and encode(host
)) or "_global";
128 username
= username
and encode(username
);
129 datastore
= store_encode(datastore
);
131 if create
then mkdir(mkdir(mkdir(data_path
).."/"..host
).."/"..datastore
); end
132 return format("%s/%s/%s/%s.%s", data_path
, host
, datastore
, username
, ext
);
134 if create
then mkdir(mkdir(data_path
).."/"..host
); end
135 return format("%s/%s/%s.%s", data_path
, host
, datastore
, ext
);
139 local function load(username
, host
, datastore
)
140 local data
, err
, errno
= envloadfile(getpath(username
, host
, datastore
), {});
142 if errno
== ENOENT
then
143 -- No such file, ok to ignore
146 log("error", "Failed to load %s storage ('%s') for user: %s@%s", datastore
, err
, username
or "nil", host
or "nil");
147 return nil, "Error reading storage";
150 local success
, ret
= pcall(data
);
152 log("error", "Unable to load %s storage ('%s') for user: %s@%s", datastore
, ret
, username
or "nil", host
or "nil");
153 return nil, "Error reading storage";
158 local function atomic_store(filename
, data
)
159 local scratch
= filename
.."~";
160 local f
, ok
, msg
, errno
;
162 f
, msg
, errno
= io_open(scratch
, "w");
167 ok
, msg
= f
:write(data
);
180 return os_rename(scratch
, filename
);
183 if prosody
and prosody
.platform
~= "posix" then
184 -- os.rename does not overwrite existing files on Windows
185 -- TODO We could use Transactional NTFS on Vista and above
186 function atomic_store(filename
, data
)
187 local f
, err
= io_open(filename
, "w");
188 if not f
then return f
, err
; end
189 local ok
, msg
= f
:write(data
);
190 if not ok
then f
:close(); return ok
, msg
; end
195 local function store(username
, host
, datastore
, data
)
200 username
, host
, datastore
, data
= callback(username
, host
, datastore
, data
);
201 if username
== false then
202 return true; -- Don't save this data at all
205 -- save the datastore
206 local d
= "return " .. serialize(data
) .. ";\n";
207 local mkdir_cache_cleared
;
209 local ok
, msg
= atomic_store(getpath(username
, host
, datastore
, nil, true), d
);
211 if not mkdir_cache_cleared
then -- We may need to recreate a removed directory
213 mkdir_cache_cleared
= true;
215 log("error", "Unable to write to %s storage ('%s') for user: %s@%s", datastore
, msg
, username
or "nil", host
or "nil");
216 return nil, "Error saving to storage";
219 if next(data
) == nil then -- try to delete empty datastore
220 log("debug", "Removing empty %s datastore for user %s@%s", datastore
, username
or "nil", host
or "nil");
221 os_remove(getpath(username
, host
, datastore
));
223 -- we write data even when we are deleting because lua doesn't have a
224 -- platform independent way of checking for non-exisitng files
229 -- Append a blob of data to a file
230 local function append(username
, host
, datastore
, ext
, data
)
231 if type(data
) ~= "string" then return; end
232 local filename
= getpath(username
, host
, datastore
, ext
, true);
234 local f
= io_open(filename
, "r+");
236 return atomic_store(filename
, data
);
237 -- File did probably not exist, let's create it
240 local pos
= f
:seek("end");
242 local ok
, msg
= atomic_append(f
, data
);
246 return ok
, msg
, "write";
251 return ok
, msg
, "close";
257 local function list_append(username
, host
, datastore
, data
)
258 if not data
then return; end
259 if callback(username
, host
, datastore
) == false then return true; end
260 -- save the datastore
262 data
= "item(" .. serialize(data
) .. ");\n";
263 local ok
, msg
, where
= append(username
, host
, datastore
, "list", data
);
265 log("error", "Unable to write to %s storage ('%s' in %s) for user: %s@%s",
266 datastore
, msg
, where
, username
or "nil", host
or "nil");
272 local function list_store(username
, host
, datastore
, data
)
276 if callback(username
, host
, datastore
) == false then return true; end
277 -- save the datastore
279 for i
, item
in ipairs(data
) do
280 d
[i
] = "item(" .. serialize(item
) .. ");\n";
282 local ok
, msg
= atomic_store(getpath(username
, host
, datastore
, "list", true), t_concat(d
));
284 log("error", "Unable to write to %s storage ('%s') for user: %s@%s", datastore
, msg
, username
or "nil", host
or "nil");
287 if next(data
) == nil then -- try to delete empty datastore
288 log("debug", "Removing empty %s datastore for user %s@%s", datastore
, username
or "nil", host
or "nil");
289 os_remove(getpath(username
, host
, datastore
, "list"));
291 -- we write data even when we are deleting because lua doesn't have a
292 -- platform independent way of checking for non-exisitng files
296 local function list_load(username
, host
, datastore
)
298 local data
, err
, errno
= envloadfile(getpath(username
, host
, datastore
, "list"), {item
= function(i
) t_insert(items
, i
); end});
300 if errno
== ENOENT
then
301 -- No such file, ok to ignore
304 log("error", "Failed to load %s storage ('%s') for user: %s@%s", datastore
, err
, username
or "nil", host
or "nil");
305 return nil, "Error reading storage";
308 local success
, ret
= pcall(data
);
310 log("error", "Unable to load %s storage ('%s') for user: %s@%s", datastore
, ret
, username
or "nil", host
or "nil");
311 return nil, "Error reading storage";
321 local function users(host
, store
, typ
) -- luacheck: ignore 431/store
322 typ
= type_map
[typ
or "keyval"];
323 local store_dir
= format("%s/%s/%s", data_path
, encode(host
), store
);
325 local mode
, err
= lfs
.attributes(store_dir
, "mode");
327 return function() log("debug", "%s", err
or (store_dir
.. " does not exist")) end
329 local next, state
= lfs
.dir(store_dir
); -- luacheck: ignore 431/next 431/state
330 return function(state
) -- luacheck: ignore 431/state
331 for node
in next, state
do
332 local file
, ext
= node
:match("^(.*)%.([dalist]+)$");
333 if file
and ext
== typ
then
340 local function stores(username
, host
, typ
)
341 typ
= type_map
[typ
or "keyval"];
342 local store_dir
= format("%s/%s/", data_path
, encode(host
));
344 local mode
, err
= lfs
.attributes(store_dir
, "mode");
346 return function() log("debug", err
or (store_dir
.. " does not exist")) end
348 local next, state
= lfs
.dir(store_dir
); -- luacheck: ignore 431/next 431/state
349 return function(state
) -- luacheck: ignore 431/state
350 for node
in next, state
do
351 if not node
:match
"^%." then
352 if username
== true then
353 if lfs
.attributes(store_dir
..node
, "mode") == "directory" then
357 local store_name
= decode(node
);
358 if lfs
.attributes(getpath(username
, host
, store_name
, typ
), "mode") then
361 elseif lfs
.attributes(node
, "mode") == "file" then
362 local file
, ext
= node
:match("^(.*)%.([dalist]+)$");
372 local function do_remove(path
)
373 local ok
, err
= os_remove(path
);
374 if not ok
and lfs
.attributes(path
, "mode") then
380 local function purge(username
, host
)
381 local host_dir
= format("%s/%s/", data_path
, encode(host
));
382 local ok
, iter
, state
, var
= pcall(lfs
.dir
, host_dir
);
387 for file
in iter
, state
, var
do
388 if lfs
.attributes(host_dir
..file
, "mode") == "directory" then
389 local store_name
= decode(file
);
390 local ok
, err
= do_remove(getpath(username
, host
, store_name
));
391 if not ok
then errs
[#errs
+1] = err
; end
393 local ok
, err
= do_remove(getpath(username
, host
, store_name
, "list"));
394 if not ok
then errs
[#errs
+1] = err
; end
397 return #errs
== 0, t_concat(errs
, ", ");
401 set_data_path
= set_data_path
;
402 add_callback
= add_callback
;
403 remove_callback
= remove_callback
;
408 store_raw
= atomic_store
;
409 list_append
= list_append
;
410 list_store
= list_store
;
411 list_load
= list_load
;
415 path_decode
= decode
;
416 path_encode
= encode
;