Merge branch 'master' of https://code.grnet.gr/git/pithos
[pithos] / pithos / backends / simple.py
1 # Copyright 2011 GRNET S.A. All rights reserved.
2
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10
11 #   2. Redistributions in binary form must reproduce the above
12 #      copyright notice, this list of conditions and the following
13 #      disclaimer in the documentation and/or other materials
14 #      provided with the distribution.
15
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
33
34 import os
35 import time
36 import sqlite3
37 import logging
38 import types
39 import hashlib
40 import shutil
41 import pickle
42
43 from base import NotAllowedError, BaseBackend
44
45
46 logger = logging.getLogger(__name__)
47
48
49 class SimpleBackend(BaseBackend):
50     """A simple backend.
51     
52     Uses SQLite for storage.
53     """
54     
55     # TODO: Automatic/manual clean-up after a time interval.
56     
57     def __init__(self, db):
58         self.hash_algorithm = 'sha1'
59         self.block_size = 128 * 1024 # 128KB
60         
61         basepath = os.path.split(db)[0]
62         if basepath and not os.path.exists(basepath):
63             os.makedirs(basepath)
64         
65         self.con = sqlite3.connect(db, check_same_thread=False)
66         sql = '''create table if not exists versions (
67                     version_id integer primary key,
68                     name text,
69                     user text,
70                     tstamp datetime default current_timestamp,
71                     size integer default 0,
72                     hide integer default 0)'''
73         self.con.execute(sql)
74         sql = '''create table if not exists metadata (
75                     version_id integer, key text, value text, primary key (version_id, key))'''
76         self.con.execute(sql)
77         sql = '''create table if not exists blocks (
78                     block_id text, data blob, primary key (block_id))'''
79         self.con.execute(sql)
80         sql = '''create table if not exists hashmaps (
81                     version_id integer, pos integer, block_id text, primary key (version_id, pos))'''
82         self.con.execute(sql)
83         sql = '''create table if not exists groups (
84                     account text, name text, users text, primary key (account, name))'''
85         self.con.execute(sql)
86         sql = '''create table if not exists permissions (
87                     name text, read text, write text, primary key (name))'''
88         self.con.execute(sql)
89         sql = '''create table if not exists policy (
90                     name text, key text, value text, primary key (name, key))'''
91         self.con.execute(sql)
92         self.con.commit()
93     
94     def get_account_meta(self, user, account, until=None):
95         """Return a dictionary with the account metadata."""
96         
97         logger.debug("get_account_meta: %s %s", account, until)
98         if user != account:
99             raise NotAllowedError
100         try:
101             version_id, mtime = self._get_accountinfo(account, until)
102         except NameError:
103             version_id = None
104             mtime = 0
105         count, bytes, tstamp = self._get_pathstats(account, until)
106         if mtime > tstamp:
107             tstamp = mtime
108         if until is None:
109             modified = tstamp
110         else:
111             modified = self._get_pathstats(account)[2] # Overall last modification
112             if mtime > modified:
113                 modified = mtime
114         
115         # Proper count.
116         sql = 'select count(name) from (%s) where name glob ? and not name glob ?'
117         sql = sql % self._sql_until(until)
118         c = self.con.execute(sql, (account + '/*', account + '/*/*'))
119         row = c.fetchone()
120         count = row[0]
121         
122         meta = self._get_metadata(account, version_id)
123         meta.update({'name': account, 'count': count, 'bytes': bytes})
124         if modified:
125             meta.update({'modified': modified})
126         if until is not None:
127             meta.update({'until_timestamp': tstamp})
128         return meta
129     
130     def update_account_meta(self, user, account, meta, replace=False):
131         """Update the metadata associated with the account."""
132         
133         logger.debug("update_account_meta: %s %s %s", account, meta, replace)
134         if user != account:
135             raise NotAllowedError
136         self._put_metadata(user, account, meta, replace)
137     
138     def get_account_groups(self, user, account):
139         """Return a dictionary with the user groups defined for this account."""
140         
141         logger.debug("get_account_groups: %s", account)
142         if user != account:
143             raise NotAllowedError
144         return self._get_groups(account)
145     
146     def update_account_groups(self, user, account, groups, replace=False):
147         """Update the groups associated with the account."""
148         
149         logger.debug("update_account_groups: %s %s %s", account, groups, replace)
150         if user != account:
151             raise NotAllowedError
152         for k, v in groups.iteritems():
153             if True in [False or ',' in x for x in v]:
154                 raise ValueError('Bad characters in groups')
155         if replace:
156             sql = 'delete from groups where account = ?'
157             self.con.execute(sql, (account,))
158         for k, v in groups.iteritems():
159             if len(v) == 0:
160                 if not replace:
161                     sql = 'delete from groups where account = ? and name = ?'
162                     self.con.execute(sql, (account, k))
163             else:
164                 sql = 'insert or replace into groups (account, name, users) values (?, ?, ?)'
165                 self.con.execute(sql, (account, k, ','.join(v)))
166         self.con.commit()
167     
168     def delete_account(self, user, account):
169         """Delete the account with the given name."""
170         
171         logger.debug("delete_account: %s", account)
172         if user != account:
173             raise NotAllowedError
174         count, bytes, tstamp = self._get_pathstats(account)
175         if count > 0:
176             raise IndexError('Account is not empty')
177         self._del_path(account) # Point of no return.
178     
179     def list_containers(self, user, account, marker=None, limit=10000, until=None):
180         """Return a list of containers existing under an account."""
181         
182         logger.debug("list_containers: %s %s %s %s", account, marker, limit, until)
183         if user != account:
184             raise NotAllowedError
185         return self._list_objects(account, '', '/', marker, limit, False, [], until)
186     
187     def get_container_meta(self, user, account, container, until=None):
188         """Return a dictionary with the container metadata."""
189         
190         logger.debug("get_container_meta: %s %s %s", account, container, until)
191         if user != account:
192             raise NotAllowedError
193         path, version_id, mtime = self._get_containerinfo(account, container, until)
194         count, bytes, tstamp = self._get_pathstats(path, until)
195         if mtime > tstamp:
196             tstamp = mtime
197         if until is None:
198             modified = tstamp
199         else:
200             modified = self._get_pathstats(path)[2] # Overall last modification
201             if mtime > modified:
202                 modified = mtime
203         
204         meta = self._get_metadata(path, version_id)
205         meta.update({'name': container, 'count': count, 'bytes': bytes, 'modified': modified})
206         if until is not None:
207             meta.update({'until_timestamp': tstamp})
208         return meta
209     
210     def update_container_meta(self, user, account, container, meta, replace=False):
211         """Update the metadata associated with the container."""
212         
213         logger.debug("update_container_meta: %s %s %s %s", account, container, meta, replace)
214         if user != account:
215             raise NotAllowedError
216         path, version_id, mtime = self._get_containerinfo(account, container)
217         self._put_metadata(user, path, meta, replace)
218     
219     def get_container_policy(self, user, account, container):
220         """Return a dictionary with the container policy."""
221         
222         logger.debug("get_container_policy: %s %s", account, container)
223         return {}
224     
225     def update_container_policy(self, user, account, container, policy, replace=False):
226         """Update the policy associated with the account."""
227         
228         logger.debug("update_container_policy: %s %s %s %s", account, container, policy, replace)
229         return
230     
231     def put_container(self, user, account, container, policy=None):
232         """Create a new container with the given name."""
233         
234         logger.debug("put_container: %s %s %s", account, container, policy)
235         if user != account:
236             raise NotAllowedError
237         try:
238             path, version_id, mtime = self._get_containerinfo(account, container)
239         except NameError:
240             path = os.path.join(account, container)
241             version_id = self._put_version(path, user)
242         else:
243             raise NameError('Container already exists')
244     
245     def delete_container(self, user, account, container):
246         """Delete the container with the given name."""
247         
248         logger.debug("delete_container: %s %s", account, container)
249         if user != account:
250             raise NotAllowedError
251         path, version_id, mtime = self._get_containerinfo(account, container)
252         count, bytes, tstamp = self._get_pathstats(path)
253         if count > 0:
254             raise IndexError('Container is not empty')
255         self._del_path(path) # Point of no return.
256         self._copy_version(user, account, account, True, True) # New account version.
257     
258     def list_objects(self, user, account, container, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, keys=[], until=None):
259         """Return a list of objects existing under a container."""
260         
261         logger.debug("list_objects: %s %s %s %s %s %s %s", account, container, prefix, delimiter, marker, limit, until)
262         if user != account:
263             raise NotAllowedError
264         path, version_id, mtime = self._get_containerinfo(account, container, until)
265         return self._list_objects(path, prefix, delimiter, marker, limit, virtual, keys, until)
266     
267     def list_object_meta(self, user, account, container, until=None):
268         """Return a list with all the container's object meta keys."""
269         
270         logger.debug("list_object_meta: %s %s %s", account, container, until)
271         if user != account:
272             raise NotAllowedError
273         path, version_id, mtime = self._get_containerinfo(account, container, until)
274         sql = '''select distinct m.key from (%s) o, metadata m
275                     where m.version_id = o.version_id and o.name like ?'''
276         sql = sql % self._sql_until(until)
277         c = self.con.execute(sql, (path + '/%',))
278         return [x[0] for x in c.fetchall()]
279     
280     def get_object_meta(self, user, account, container, name, version=None):
281         """Return a dictionary with the object metadata."""
282         
283         logger.debug("get_object_meta: %s %s %s %s", account, container, name, version)
284         self._can_read(user, account, container, name)
285         path, version_id, muser, mtime, size = self._get_objectinfo(account, container, name, version)
286         if version is None:
287             modified = mtime
288         else:
289             modified = self._get_version(path, version)[2] # Overall last modification
290         
291         meta = self._get_metadata(path, version_id)
292         meta.update({'name': name, 'bytes': size})
293         meta.update({'version': version_id, 'version_timestamp': mtime})
294         meta.update({'modified': modified, 'modified_by': muser})
295         return meta
296     
297     def update_object_meta(self, user, account, container, name, meta, replace=False):
298         """Update the metadata associated with the object."""
299         
300         logger.debug("update_object_meta: %s %s %s %s %s", account, container, name, meta, replace)
301         self._can_write(user, account, container, name)
302         path, version_id, muser, mtime, size = self._get_objectinfo(account, container, name)
303         self._put_metadata(user, path, meta, replace)
304     
305     def get_object_permissions(self, user, account, container, name):
306         """Return the path from which this object gets its permissions from,\
307         along with a dictionary containing the permissions."""
308         
309         logger.debug("get_object_permissions: %s %s %s", account, container, name)
310         self._can_read(user, account, container, name)
311         path = self._get_objectinfo(account, container, name)[0]
312         return self._get_permissions(path)
313     
314     def update_object_permissions(self, user, account, container, name, permissions):
315         """Update the permissions associated with the object."""
316         
317         logger.debug("update_object_permissions: %s %s %s %s", account, container, name, permissions)
318         if user != account:
319             raise NotAllowedError
320         path = self._get_objectinfo(account, container, name)[0]
321         r, w = self._check_permissions(path, permissions)
322         self._put_permissions(path, r, w)
323     
324     def get_object_public(self, user, account, container, name):
325         """Return the public URL of the object if applicable."""
326         
327         logger.debug("get_object_public: %s %s %s", account, container, name)
328         return None
329     
330     def update_object_public(self, user, account, container, name, public):
331         """Update the public status of the object."""
332         
333         logger.debug("update_object_public: %s %s %s %s", account, container, name, public)
334         return
335     
336     def get_object_hashmap(self, user, account, container, name, version=None):
337         """Return the object's size and a list with partial hashes."""
338         
339         logger.debug("get_object_hashmap: %s %s %s %s", account, container, name, version)
340         self._can_read(user, account, container, name)
341         path, version_id, muser, mtime, size = self._get_objectinfo(account, container, name, version)
342         sql = 'select block_id from hashmaps where version_id = ? order by pos asc'
343         c = self.con.execute(sql, (version_id,))
344         hashmap = [x[0] for x in c.fetchall()]
345         return size, hashmap
346     
347     def update_object_hashmap(self, user, account, container, name, size, hashmap, meta={}, replace_meta=False, permissions=None):
348         """Create/update an object with the specified size and partial hashes."""
349         
350         logger.debug("update_object_hashmap: %s %s %s %s %s", account, container, name, size, hashmap)
351         if permissions is not None and user != account:
352             raise NotAllowedError
353         self._can_write(user, account, container, name)
354         missing = []
355         for i in range(len(hashmap)):
356             sql = 'select count(*) from blocks where block_id = ?'
357             c = self.con.execute(sql, (hashmap[i],))
358             if c.fetchone()[0] == 0:
359                 missing.append(hashmap[i])
360         if missing:
361             ie = IndexError()
362             ie.data = missing
363             raise ie
364         path = self._get_containerinfo(account, container)[0]
365         path = os.path.join(path, name)
366         if permissions is not None:
367             r, w = self._check_permissions(path, permissions)
368         src_version_id, dest_version_id = self._copy_version(user, path, path, not replace_meta, False)
369         sql = 'update versions set size = ? where version_id = ?'
370         self.con.execute(sql, (size, dest_version_id))
371         # TODO: Check for block_id existence.
372         for i in range(len(hashmap)):
373             sql = 'insert or replace into hashmaps (version_id, pos, block_id) values (?, ?, ?)'
374             self.con.execute(sql, (dest_version_id, i, hashmap[i]))
375         for k, v in meta.iteritems():
376             sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
377             self.con.execute(sql, (dest_version_id, k, v))
378         if permissions is not None:
379             sql = 'insert or replace into permissions (name, read, write) values (?, ?, ?)'
380             self.con.execute(sql, (path, r, w))
381         self.con.commit()
382     
383     def copy_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None, src_version=None):
384         """Copy an object's data and metadata."""
385         
386         logger.debug("copy_object: %s %s %s %s %s %s %s %s %s", account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, permissions, src_version)
387         if permissions is not None and user != account:
388             raise NotAllowedError
389         self._can_read(user, account, src_container, src_name)
390         self._can_write(user, account, dest_container, dest_name)
391         self._get_containerinfo(account, src_container)
392         if src_version is None:
393             src_path = self._get_objectinfo(account, src_container, src_name)[0]
394         else:
395             src_path = os.path.join(account, src_container, src_name)
396         dest_path = self._get_containerinfo(account, dest_container)[0]
397         dest_path = os.path.join(dest_path, dest_name)
398         if permissions is not None:
399             r, w = self._check_permissions(dest_path, permissions)
400         src_version_id, dest_version_id = self._copy_version(user, src_path, dest_path, not replace_meta, True, src_version)
401         for k, v in dest_meta.iteritems():
402             sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
403             self.con.execute(sql, (dest_version_id, k, v))
404         if permissions is not None:
405             sql = 'insert or replace into permissions (name, read, write) values (?, ?, ?)'
406             self.con.execute(sql, (dest_path, r, w))
407         self.con.commit()
408     
409     def move_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None):
410         """Move an object's data and metadata."""
411         
412         logger.debug("move_object: %s %s %s %s %s %s %s %s", account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, permissions)
413         self.copy_object(user, account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, permissions, None)
414         self.delete_object(user, account, src_container, src_name)
415     
416     def delete_object(self, user, account, container, name):
417         """Delete an object."""
418         
419         logger.debug("delete_object: %s %s %s", account, container, name)
420         if user != account:
421             raise NotAllowedError
422         path = self._get_objectinfo(account, container, name)[0]
423         self._put_version(path, user, 0, 1)
424         sql = 'delete from permissions where name = ?'
425         self.con.execute(sql, (path,))
426         self.con.commit()
427     
428     def list_versions(self, user, account, container, name):
429         """Return a list of all (version, version_timestamp) tuples for an object."""
430         
431         logger.debug("list_versions: %s %s %s", account, container, name)
432         self._can_read(user, account, container, name)
433         # This will even show deleted versions.
434         path = os.path.join(account, container, name)
435         sql = '''select distinct version_id, strftime('%s', tstamp) from versions where name = ? and hide = 0'''
436         c = self.con.execute(sql, (path,))
437         return [(int(x[0]), int(x[1])) for x in c.fetchall()]
438     
439     def get_block(self, hash):
440         """Return a block's data."""
441         
442         logger.debug("get_block: %s", hash)
443         c = self.con.execute('select data from blocks where block_id = ?', (hash,))
444         row = c.fetchone()
445         if row:
446             return str(row[0])
447         else:
448             raise NameError('Block does not exist')
449     
450     def put_block(self, data):
451         """Create a block and return the hash."""
452         
453         logger.debug("put_block: %s", len(data))
454         h = hashlib.new(self.hash_algorithm)
455         h.update(data.rstrip('\x00'))
456         hash = h.hexdigest()
457         sql = 'insert or ignore into blocks (block_id, data) values (?, ?)'
458         self.con.execute(sql, (hash, buffer(data)))
459         self.con.commit()
460         return hash
461     
462     def update_block(self, hash, data, offset=0):
463         """Update a known block and return the hash."""
464         
465         logger.debug("update_block: %s %s %s", hash, len(data), offset)
466         if offset == 0 and len(data) == self.block_size:
467             return self.put_block(data)
468         src_data = self.get_block(hash)
469         bs = self.block_size
470         if offset < 0 or offset > bs or offset + len(data) > bs:
471             raise IndexError('Offset or data outside block limits')
472         dest_data = src_data[:offset] + data + src_data[offset + len(data):]
473         return self.put_block(dest_data)
474     
475     def _sql_until(self, until=None):
476         """Return the sql to get the latest versions until the timestamp given."""
477         if until is None:
478             until = int(time.time())
479         sql = '''select version_id, name, strftime('%s', tstamp) as tstamp, size from versions v
480                     where version_id = (select max(version_id) from versions
481                                         where v.name = name and tstamp <= datetime(%s, 'unixepoch'))
482                     and hide = 0'''
483         return sql % ('%s', until)
484     
485     def _get_pathstats(self, path, until=None):
486         """Return count and sum of size of everything under path and latest timestamp."""
487         
488         sql = 'select count(version_id), total(size), max(tstamp) from (%s) where name like ?'
489         sql = sql % self._sql_until(until)
490         c = self.con.execute(sql, (path + '/%',))
491         row = c.fetchone()
492         tstamp = row[2] if row[2] is not None else 0
493         return int(row[0]), int(row[1]), int(tstamp)
494     
495     def _get_version(self, path, version=None):
496         if version is None:
497             sql = '''select version_id, user, strftime('%s', tstamp), size, hide from versions where name = ?
498                         order by version_id desc limit 1'''
499             c = self.con.execute(sql, (path,))
500             row = c.fetchone()
501             if not row or int(row[4]):
502                 raise NameError('Object does not exist')
503         else:
504             # The database (sqlite) will not complain if the version is not an integer.
505             sql = '''select version_id, user, strftime('%s', tstamp), size from versions where name = ?
506                         and version_id = ?'''
507             c = self.con.execute(sql, (path, version))
508             row = c.fetchone()
509             if not row:
510                 raise IndexError('Version does not exist')
511         return str(row[0]), str(row[1]), int(row[2]), int(row[3])
512     
513     def _put_version(self, path, user, size=0, hide=0):
514         sql = 'insert into versions (name, user, size, hide) values (?, ?, ?, ?)'
515         id = self.con.execute(sql, (path, user, size, hide)).lastrowid
516         self.con.commit()
517         return str(id)
518     
519     def _copy_version(self, user, src_path, dest_path, copy_meta=True, copy_data=True, src_version=None):
520         if src_version is not None:
521             src_version_id, muser, mtime, size = self._get_version(src_path, src_version)
522         else:
523             # Latest or create from scratch.
524             try:
525                 src_version_id, muser, mtime, size = self._get_version(src_path)
526             except NameError:
527                 src_version_id = None
528                 size = 0
529         if not copy_data:
530             size = 0
531         dest_version_id = self._put_version(dest_path, user, size)
532         if copy_meta and src_version_id is not None:
533             sql = 'insert into metadata select %s, key, value from metadata where version_id = ?'
534             sql = sql % dest_version_id
535             self.con.execute(sql, (src_version_id,))
536         if copy_data and src_version_id is not None:
537             sql = 'insert into hashmaps select %s, pos, block_id from hashmaps where version_id = ?'
538             sql = sql % dest_version_id
539             self.con.execute(sql, (src_version_id,))
540         self.con.commit()
541         return src_version_id, dest_version_id
542     
543     def _get_versioninfo(self, account, container, name, until=None):
544         """Return path, latest version, associated timestamp and size until the timestamp given."""
545         
546         p = (account, container, name)
547         try:
548             p = p[:p.index(None)]
549         except ValueError:
550             pass
551         path = os.path.join(*p)
552         sql = '''select version_id, tstamp, size from (%s) where name = ?'''
553         sql = sql % self._sql_until(until)
554         c = self.con.execute(sql, (path,))
555         row = c.fetchone()
556         if row is None:
557             raise NameError('Path does not exist')
558         return path, str(row[0]), int(row[1]), int(row[2])
559     
560     def _get_accountinfo(self, account, until=None):
561         try:
562             path, version_id, mtime, size = self._get_versioninfo(account, None, None, until)
563             return version_id, mtime
564         except:
565             raise NameError('Account does not exist')
566     
567     def _get_containerinfo(self, account, container, until=None):
568         try:
569             path, version_id, mtime, size = self._get_versioninfo(account, container, None, until)
570             return path, version_id, mtime
571         except:
572             raise NameError('Container does not exist')
573     
574     def _get_objectinfo(self, account, container, name, version=None):
575         path = os.path.join(account, container, name)
576         version_id, muser, mtime, size = self._get_version(path, version)
577         return path, version_id, muser, mtime, size
578     
579     def _get_metadata(self, path, version):
580         sql = 'select key, value from metadata where version_id = ?'
581         c = self.con.execute(sql, (version,))
582         return dict(c.fetchall())
583     
584     def _put_metadata(self, user, path, meta, replace=False):
585         """Create a new version and store metadata."""
586         
587         src_version_id, dest_version_id = self._copy_version(user, path, path, not replace, True)
588         for k, v in meta.iteritems():
589             if not replace and v == '':
590                 sql = 'delete from metadata where version_id = ? and key = ?'
591                 self.con.execute(sql, (dest_version_id, k))
592             else:
593                 sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
594                 self.con.execute(sql, (dest_version_id, k, v))
595         self.con.commit()
596     
597     def _get_groups(self, account):
598         sql = 'select name, users from groups where account = ?'
599         c = self.con.execute(sql, (account,))
600         return dict([(x[0], x[1].split(',')) for x in c.fetchall()])
601     
602     def _is_allowed(self, user, account, container, name, op='read'):
603         if user == account:
604             return True
605         path = os.path.join(account, container, name)
606         perm_path, perms = self._get_permissions(path)
607         
608         # Expand groups.
609         for x in ('read', 'write'):
610             g_perms = []
611             for y in perms.get(x, []):
612                 if ':' in y:
613                     g_account, g_name = y.split(':', 1)
614                     groups = self._get_groups(g_account)
615                     if g_name in groups:
616                         g_perms += groups[g_name]
617                 else:
618                     g_perms.append(y)
619             perms[x] = g_perms
620         
621         if op == 'read' and user in perms.get('read', []):
622             return True
623         if user in perms.get('write', []):
624             return True
625         return False
626     
627     def _can_read(self, user, account, container, name):
628         if not self._is_allowed(user, account, container, name, 'read'):
629             raise NotAllowedError
630     
631     def _can_write(self, user, account, container, name):
632         if not self._is_allowed(user, account, container, name, 'write'):
633             raise NotAllowedError
634     
635     def _check_permissions(self, path, permissions):
636         # Check for existing permissions.
637         sql = '''select name from permissions
638                     where name != ? and (name like ? or ? like name || ?)'''
639         c = self.con.execute(sql, (path, path + '%', path, '%'))
640         rows = c.fetchall()
641         if rows:
642             raise AttributeError('Permissions already set')
643         
644         # Format given permissions.
645         if len(permissions) == 0:
646             return '', ''
647         r = permissions.get('read', [])
648         w = permissions.get('write', [])
649         if True in [False or ',' in x for x in r]:
650             raise ValueError('Bad characters in read permissions')
651         if True in [False or ',' in x for x in w]:
652             raise ValueError('Bad characters in write permissions')
653         return ','.join(r), ','.join(w)
654     
655     def _get_permissions(self, path):
656         # Check for permissions at path or above.
657         sql = 'select name, read, write from permissions where ? like name || ?'
658         c = self.con.execute(sql, (path, '%'))
659         row = c.fetchone()
660         if not row:
661             return path, {}
662         
663         name, r, w = row
664         ret = {}
665         if w != '':
666             ret['write'] = w.split(',')
667         if r != '':
668             ret['read'] = r.split(',')
669         return name, ret
670     
671     def _put_permissions(self, path, r, w):
672         if r == '' and w == '':
673             sql = 'delete from permissions where name = ?'
674             self.con.execute(sql, (path,))
675         else:
676             sql = 'insert or replace into permissions (name, read, write) values (?, ?, ?)'
677             self.con.execute(sql, (path, r, w))
678         self.con.commit()
679     
680     def _list_objects(self, path, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, keys=[], until=None):
681         cont_prefix = path + '/'
682         if keys and len(keys) > 0:
683             sql = '''select distinct o.name, o.version_id from (%s) o, metadata m where o.name like ? and
684                         m.version_id = o.version_id and m.key in (%s) order by o.name'''
685             sql = sql % (self._sql_until(until), ', '.join('?' * len(keys)))
686             param = (cont_prefix + prefix + '%',) + tuple(keys)
687         else:
688             sql = 'select name, version_id from (%s) where name like ? order by name'
689             sql = sql % self._sql_until(until)
690             param = (cont_prefix + prefix + '%',)
691         c = self.con.execute(sql, param)
692         objects = [(x[0][len(cont_prefix):], x[1]) for x in c.fetchall()]
693         if delimiter:
694             pseudo_objects = []
695             for x in objects:
696                 pseudo_name = x[0]
697                 i = pseudo_name.find(delimiter, len(prefix))
698                 if not virtual:
699                     # If the delimiter is not found, or the name ends
700                     # with the delimiter's first occurence.
701                     if i == -1 or len(pseudo_name) == i + len(delimiter):
702                         pseudo_objects.append(x)
703                 else:
704                     # If the delimiter is found, keep up to (and including) the delimiter.
705                     if i != -1:
706                         pseudo_name = pseudo_name[:i + len(delimiter)]
707                     if pseudo_name not in [y[0] for y in pseudo_objects]:
708                         if pseudo_name == x[0]:
709                             pseudo_objects.append(x)
710                         else:
711                             pseudo_objects.append((pseudo_name, None))
712             objects = pseudo_objects
713         
714         start = 0
715         if marker:
716             try:
717                 start = [x[0] for x in objects].index(marker) + 1
718             except ValueError:
719                 pass
720         if not limit or limit > 10000:
721             limit = 10000
722         return objects[start:start + limit]
723     
724     def _del_path(self, path):
725         sql = '''delete from hashmaps where version_id in
726                     (select version_id from versions where name = ?)'''
727         self.con.execute(sql, (path,))
728         sql = '''delete from metadata where version_id in
729                     (select version_id from versions where name = ?)'''
730         self.con.execute(sql, (path,))
731         sql = '''delete from versions where name = ?'''
732         self.con.execute(sql, (path,))
733         sql = '''delete from permissions where name like ?'''
734         self.con.execute(sql, (path + '%',)) # Redundant.
735         self.con.commit()