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 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;