Fixes #447
[pithos] / pithos / backends / dummy.py
1 import os\r
2 import sqlite3\r
3 import json\r
4 import logging\r
5 import types\r
6 import hashlib\r
7 import shutil\r
8 \r
9 logger = logging.getLogger(__name__)\r
10 formatter = logging.Formatter('[%(levelname)s] %(message)s')\r
11 handler = logging.FileHandler('backend.out')\r
12 handler.setFormatter(formatter)\r
13 logger.addHandler(handler)\r
14 \r
15 class BackEnd:\r
16 \r
17     logger = None\r
18     \r
19     def __init__(self, basepath, log_file='backend.out', log_level=logging.DEBUG):\r
20         self.basepath = basepath\r
21         \r
22         # TODO: Manage log_file.\r
23         logger.setLevel(log_level)\r
24         \r
25         if not os.path.exists(basepath):\r
26             os.makedirs(basepath)\r
27         db = os.path.join(basepath, 'db')\r
28         self.con = sqlite3.connect(db)\r
29         # Create tables\r
30         sql = '''create table if not exists objects(name text)'''\r
31         self.con.execute(sql)\r
32         sql = '''create table if not exists metadata(object_id int, name text, value text)'''\r
33         self.con.execute(sql)\r
34         self.con.commit()\r
35     \r
36     # TODO: Create/delete account?\r
37     # TODO: Catch OSError exceptions.\r
38     \r
39     def get_account_meta(self, account):\r
40         """\r
41         returns a dictionary with the account metadata\r
42         """\r
43         logger.debug("get_account_meta: %s", account)\r
44         fullname = os.path.join(self.basepath, account)\r
45         if not os.path.exists(fullname):\r
46             raise NameError('Account does not exist')\r
47         contents = os.listdir(fullname)\r
48         count = len(contents)\r
49         size = os.stat(fullname).st_size\r
50         meta = self.__get_metadata(account)\r
51         meta.update({'name': account, 'count': count, 'bytes': size})\r
52         return meta\r
53 \r
54     def update_account_meta(self, account, meta):\r
55         """\r
56         updates the metadata associated with the account\r
57         """\r
58         logger.debug("update_account_meta: %s %s", account, meta)\r
59         fullname = os.path.join(self.basepath, account)\r
60         if not os.path.exists(fullname):\r
61             os.makedirs(fullname)\r
62         self.__put_metadata(account, meta)\r
63         return\r
64     \r
65     def create_container(self, account, name):\r
66         """\r
67         creates a new container with the given name\r
68         if it doesn't exist under the basepath\r
69         """\r
70         logger.debug("create_container: %s %s", account, name)\r
71         fullname = os.path.join(self.basepath, account, name)\r
72         if not os.path.exists(fullname):\r
73             os.makedirs(fullname)\r
74         else:\r
75             raise NameError('Container already exists')\r
76         return\r
77     \r
78     def delete_container(self, account, name):\r
79         """\r
80         deletes the container with the given name\r
81         if it exists under the basepath and is empty\r
82         """\r
83         logger.debug("delete_container: %s %s", account, name)\r
84         fullname = os.path.join(self.basepath, account, name)\r
85         if not os.path.exists(fullname):\r
86             raise NameError('Container does not exist')\r
87         if os.listdir(fullname):\r
88             raise Exception('Container is not empty')\r
89         else:\r
90             os.rmdir(fullname)\r
91             self.__del_dbpath(os.path.join(account, name))\r
92         return\r
93     \r
94     def get_container_meta(self, account, name):\r
95         """\r
96         returns a dictionary with the container metadata\r
97         """\r
98         logger.debug("get_container_meta: %s %s", account, name)\r
99         fullname = os.path.join(self.basepath, account, name)\r
100         if not os.path.exists(fullname):\r
101             raise NameError('Container does not exist')\r
102         contents = os.listdir(fullname)\r
103         count = len(contents)\r
104         size = os.stat(fullname).st_size\r
105         meta = self.__get_metadata(os.path.join(account, name))\r
106         meta.update({'name': name, 'count': count, 'bytes': size})\r
107         return meta\r
108     \r
109     def update_container_meta(self, account, name, meta):\r
110         """\r
111         updates the metadata associated with the container\r
112         """\r
113         logger.debug("update_container_meta: %s %s %s", account, name, meta)\r
114         fullname = os.path.join(self.basepath, account, name)\r
115         if not os.path.exists(fullname):\r
116             raise NameError('Container does not exist')\r
117         self.__put_metadata(os.path.join(account, name), meta)\r
118         return\r
119     \r
120     def list_containers(self, account, marker = None, limit = 10000):\r
121         """\r
122         returns a list of at most limit (default = 10000) containers \r
123         starting from the next item after the optional marker\r
124         """\r
125         logger.debug("list_containers: %s %s %s", account, marker, limit)\r
126         fullname = os.path.join(self.basepath, account)\r
127         if not os.path.exists(fullname):\r
128             raise NameError('Account does not exist')\r
129         containers = os.listdir(fullname)\r
130         start = 0\r
131         if marker:\r
132             try:\r
133                 start = containers.index(marker) + 1\r
134             except ValueError:\r
135                 pass\r
136         if not limit or limit > 10000:\r
137             limit = 10000\r
138         \r
139         return containers[start:start + limit]\r
140     \r
141     def list_objects(self, account, container, prefix = '', delimiter = None, marker = None, limit = 10000):\r
142         """\r
143         returns a list of objects existing under a container\r
144         """\r
145         logger.info("list_objects: %s %s %s %s %s %s", account, container, prefix, delimiter, marker, limit)\r
146         fullname = os.path.join(self.basepath, account, container)\r
147         if not os.path.exists(fullname):\r
148             raise NameError('Container does not exist')\r
149         \r
150         while prefix.startswith('/'):\r
151             prefix = prefix[1:]\r
152         # TODO: Test this with various prefixes. Does '//' bother it?\r
153         prefix = os.path.join(account, container, prefix)\r
154         c = self.con.execute('select * from objects where name like ''?'' order by name', (os.path.join(prefix, '%'),))\r
155         objects = [x[0][len(prefix):] for x in c.fetchall()]\r
156         if delimiter:\r
157             pseudo_objects = {}\r
158             for x in objects:\r
159                 pseudo_name = x\r
160                 i = pseudo_name.find(delimiter)\r
161                 if i != -1:\r
162                     pseudo_name = pseudo_name[:i]\r
163                 # TODO: Virtual directories.\r
164                 pseudo_objects[pseudo_name] = x\r
165             objects = pseudo_objects.keys()\r
166         \r
167         start = 0\r
168         if marker:\r
169             try:\r
170                 start = objects.index(marker)\r
171             except ValueError:\r
172                 pass\r
173         if not limit or limit > 10000:\r
174             limit = 10000\r
175         \r
176         return objects[start:start + limit]\r
177     \r
178     def get_object_meta(self, account, container, name, keys = None):\r
179         """\r
180         returns a dictionary with the object metadata\r
181         """\r
182         logger.info("get_object_meta: %s %s %s %s", account, container, name, keys)\r
183         fullname = os.path.join(self.basepath, account, container)\r
184         if not os.path.exists(fullname):\r
185             raise NameError('Container does not exist')\r
186         \r
187         link = self.__get_linkinfo(os.path.join(account, container, name))\r
188         location = os.path.join(self.basepath, account, container, link)\r
189         size = os.path.getsize(location)\r
190         mtime = os.path.getmtime(location)\r
191         meta = self.__get_metadata(os.path.join(account, container, name))\r
192         meta.update({'name': name, 'bytes': size, 'last_modified': mtime})\r
193         if 'hash' not in meta:\r
194             meta['hash'] = self.__object_hash(location)\r
195         if 'content_type' not in meta:\r
196             meta['content_type'] = 'application/octet-stream'\r
197         return meta\r
198     \r
199     def update_object_meta(self, account, container, name, meta):\r
200         """\r
201         updates the metadata associated with the object\r
202         """\r
203         logger.info("update_object_meta: %s %s %s %s", account, container, name, meta)\r
204         fullname = os.path.join(self.basepath, account, container)\r
205         if not os.path.exists(fullname):\r
206             raise NameError('Container does not exist')\r
207         self.__put_metadata(os.path.join(account, container, name), meta)\r
208         return\r
209     \r
210     def get_object(self, account, container, name, offset = 0, length = -1):\r
211         """\r
212         returns the object data\r
213         """\r
214         logger.info("get_object: %s %s %s %s %s", account, container, name, offset, length)\r
215         fullname = os.path.join(self.basepath, account, container)\r
216         if not os.path.exists(fullname):\r
217             raise NameError('Container does not exist')\r
218         \r
219         link = self.__get_linkinfo(os.path.join(account, container, name))\r
220         location = os.path.join(self.basepath, account, container, link)\r
221         f = open(location, 'r')\r
222         if offset:\r
223             f.seek(offset)\r
224         data = f.read(length)\r
225         f.close()\r
226         return data\r
227 \r
228     def update_object(self, account, container, name, data, offset = 0):\r
229         """\r
230         creates/updates an object with the specified data\r
231         """\r
232         logger.info("put_object: %s %s %s %s %s", account, container, name, data, offset)\r
233         fullname = os.path.join(self.basepath, account, container)\r
234         if not os.path.exists(fullname):\r
235             raise NameError('Container does not exist')\r
236 \r
237         try:\r
238             link = self.__get_linkinfo(os.path.join(account, container, name))\r
239         except NameError:\r
240             # new object\r
241             link = self.__put_linkinfo(os.path.join(account, container, name))\r
242         location = os.path.join(self.basepath, account, container, link)\r
243         f = open(location, 'w')\r
244         if offset:\r
245             f.seek(offset)\r
246         f.write(data)\r
247         f.close()\r
248         self.__put_metadata(os.path.join(account, container, name), {'hash': self.__object_hash(location)})\r
249         return\r
250     \r
251     def copy_object(self, account, src_container, src_name, dest_container, dest_name):\r
252         """\r
253         copies an object\r
254         """\r
255         logger.info("copy_object: %s %s %s %s %s", account, src_container, src_name, dest_container, dest_name)\r
256         link = self.__get_linkinfo(os.path.join(account, src_container, src_name))\r
257         src_location = os.path.join(self.basepath, account, src_container, link)\r
258         \r
259         dest_fullname = os.path.join(self.basepath, account, dest_container)\r
260         if not os.path.exists(dest_fullname):\r
261             raise NameError('Destination container does not exist')        \r
262         try:\r
263             link = self.__get_linkinfo(os.path.join(account, dest_container, dest_name))\r
264         except NameError:\r
265             # new object\r
266             link = self.__put_linkinfo(os.path.join(account, dest_container, dest_name))\r
267         dest_location = os.path.join(self.basepath, account, dest_container, link)\r
268         \r
269         shutil.copyfile(src_location, dest_location)\r
270         return\r
271     \r
272     def delete_object(self, account, container, name):\r
273         """\r
274         deletes an object\r
275         """\r
276         logger.info("delete_object: %s %s %s", account, container, name)\r
277         fullname = os.path.join(self.basepath, account, container)\r
278         if not os.path.exists(fullname):\r
279             raise NameError('Container does not exist')\r
280         \r
281         # delete object data\r
282         link = self.__get_linkinfo(os.path.join(account, container, name))\r
283         location = os.path.join(self.basepath, account, container, link)\r
284         try:\r
285             os.remove(location)\r
286         except:\r
287             pass\r
288         # delete object metadata\r
289         self.__del_dbpath(os.path.join(account, container, name))\r
290         return\r
291 \r
292     def __get_linkinfo(self, path):\r
293         c = self.con.execute('select rowid from objects where name=''?''', (path,))\r
294         row = c.fetchone()\r
295         if row:\r
296             return str(row[0])\r
297         else:\r
298             raise NameError('Requested path not found')\r
299     \r
300     def __put_linkinfo(self, path):\r
301         id = self.con.execute('insert into objects (name) values (?)', (path,)).lastrowid\r
302         self.con.commit()\r
303         return str(id)\r
304     \r
305     def __get_metadata(self, path):\r
306         c = self.con.execute('select m.name, m.value from metadata m, objects o where o.rowid = m.object_id and o.name = ''?''', (path,))\r
307         return dict(c.fetchall())\r
308     \r
309     def __put_metadata(self, path, meta):\r
310         c = self.con.execute('select rowid from objects where name=''?''', (path,))\r
311         row = c.fetchone()\r
312         if row:\r
313             link = str(row[0])\r
314         else:\r
315             link = self.con.execute('insert into objects (name) values (?)', (path,)).lastrowid      \r
316         for k, v in meta.iteritems():\r
317             if type(v) != types.StringType:\r
318                 v = json.dumps(v)\r
319             self.con.execute('insert or replace into metadata (object_id, name, value) values (?, ?, ?)', (link, k, v))\r
320         self.con.commit()\r
321         return\r
322 \r
323     def __del_dbpath(self, path):\r
324         self.con.execute('delete from metadata where object_id in (select rowid from objects where name = ''?'')', (path,))\r
325         self.con.execute('delete from objects where name = ''?''', (path,))\r
326         self.con.commit()\r
327         return\r
328     \r
329     def __object_hash(self, location, block_size = 8192):\r
330         md5 = hashlib.md5()\r
331         f = open(location, 'r')\r
332         while True:\r
333             data = f.read(block_size)\r
334             if not data:\r
335                 break\r
336             md5.update(data)\r
337         f.close()\r
338         return md5.hexdigest()\r