Add account groups, merge into sharing. Fix tests.
[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         path = self._get_containerinfo(account, container)[0]
355         path = os.path.join(path, name)
356         if permissions is not None:
357             r, w = self._check_permissions(path, permissions)
358         src_version_id, dest_version_id = self._copy_version(user, path, path, not replace_meta, False)
359         sql = 'update versions set size = ? where version_id = ?'
360         self.con.execute(sql, (size, dest_version_id))
361         # TODO: Check for block_id existence.
362         for i in range(len(hashmap)):
363             sql = 'insert or replace into hashmaps (version_id, pos, block_id) values (?, ?, ?)'
364             self.con.execute(sql, (dest_version_id, i, hashmap[i]))
365         for k, v in meta.iteritems():
366             sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
367             self.con.execute(sql, (dest_version_id, k, v))
368         if permissions is not None:
369             sql = 'insert or replace into permissions (name, read, write) values (?, ?, ?)'
370             self.con.execute(sql, (path, r, w))
371         self.con.commit()
372     
373     def copy_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None, src_version=None):
374         """Copy an object's data and metadata."""
375         
376         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)
377         if permissions is not None and user != account:
378             raise NotAllowedError
379         self._can_read(user, account, src_container, src_name)
380         self._can_write(user, account, dest_container, dest_name)
381         self._get_containerinfo(account, src_container)
382         if src_version is None:
383             src_path = self._get_objectinfo(account, src_container, src_name)[0]
384         else:
385             src_path = os.path.join(account, src_container, src_name)
386         dest_path = self._get_containerinfo(account, dest_container)[0]
387         dest_path = os.path.join(dest_path, dest_name)
388         if permissions is not None:
389             r, w = self._check_permissions(dest_path, permissions)
390         src_version_id, dest_version_id = self._copy_version(user, src_path, dest_path, not replace_meta, True, src_version)
391         for k, v in dest_meta.iteritems():
392             sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
393             self.con.execute(sql, (dest_version_id, k, v))
394         if permissions is not None:
395             sql = 'insert or replace into permissions (name, read, write) values (?, ?, ?)'
396             self.con.execute(sql, (dest_path, r, w))
397         self.con.commit()
398     
399     def move_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None):
400         """Move an object's data and metadata."""
401         
402         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)
403         self.copy_object(user, account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, permissions, None)
404         self.delete_object(user, account, src_container, src_name)
405     
406     def delete_object(self, user, account, container, name):
407         """Delete an object."""
408         
409         logger.debug("delete_object: %s %s %s", account, container, name)
410         if user != account:
411             raise NotAllowedError
412         path = self._get_objectinfo(account, container, name)[0]
413         self._put_version(path, user, 0, 1)
414         sql = 'delete from permissions where name = ?'
415         self.con.execute(sql, (path,))
416         self.con.commit()
417     
418     def list_versions(self, user, account, container, name):
419         """Return a list of all (version, version_timestamp) tuples for an object."""
420         
421         logger.debug("list_versions: %s %s %s", account, container, name)
422         self._can_read(user, account, container, name)
423         # This will even show deleted versions.
424         path = os.path.join(account, container, name)
425         sql = '''select distinct version_id, strftime('%s', tstamp) from versions where name = ? and hide = 0'''
426         c = self.con.execute(sql, (path,))
427         return [(int(x[0]), int(x[1])) for x in c.fetchall()]
428     
429     def get_block(self, hash):
430         """Return a block's data."""
431         
432         logger.debug("get_block: %s", hash)
433         c = self.con.execute('select data from blocks where block_id = ?', (hash,))
434         row = c.fetchone()
435         if row:
436             return str(row[0])
437         else:
438             raise NameError('Block does not exist')
439     
440     def put_block(self, data):
441         """Create a block and return the hash."""
442         
443         logger.debug("put_block: %s", len(data))
444         h = hashlib.new(self.hash_algorithm)
445         h.update(data.rstrip('\x00'))
446         hash = h.hexdigest()
447         sql = 'insert or ignore into blocks (block_id, data) values (?, ?)'
448         self.con.execute(sql, (hash, buffer(data)))
449         self.con.commit()
450         return hash
451     
452     def update_block(self, hash, data, offset=0):
453         """Update a known block and return the hash."""
454         
455         logger.debug("update_block: %s %s %s", hash, len(data), offset)
456         if offset == 0 and len(data) == self.block_size:
457             return self.put_block(data)
458         src_data = self.get_block(hash)
459         bs = self.block_size
460         if offset < 0 or offset > bs or offset + len(data) > bs:
461             raise IndexError('Offset or data outside block limits')
462         dest_data = src_data[:offset] + data + src_data[offset + len(data):]
463         return self.put_block(dest_data)
464     
465     def _sql_until(self, until=None):
466         """Return the sql to get the latest versions until the timestamp given."""
467         if until is None:
468             until = int(time.time())
469         sql = '''select version_id, name, strftime('%s', tstamp) as tstamp, size from versions v
470                     where version_id = (select max(version_id) from versions
471                                         where v.name = name and tstamp <= datetime(%s, 'unixepoch'))
472                     and hide = 0'''
473         return sql % ('%s', until)
474     
475     def _get_pathstats(self, path, until=None):
476         """Return count and sum of size of everything under path and latest timestamp."""
477         
478         sql = 'select count(version_id), total(size), max(tstamp) from (%s) where name like ?'
479         sql = sql % self._sql_until(until)
480         c = self.con.execute(sql, (path + '/%',))
481         row = c.fetchone()
482         tstamp = row[2] if row[2] is not None else 0
483         return int(row[0]), int(row[1]), int(tstamp)
484     
485     def _get_version(self, path, version=None):
486         if version is None:
487             sql = '''select version_id, user, strftime('%s', tstamp), size, hide from versions where name = ?
488                         order by version_id desc limit 1'''
489             c = self.con.execute(sql, (path,))
490             row = c.fetchone()
491             if not row or int(row[4]):
492                 raise NameError('Object does not exist')
493         else:
494             # The database (sqlite) will not complain if the version is not an integer.
495             sql = '''select version_id, user, strftime('%s', tstamp), size from versions where name = ?
496                         and version_id = ?'''
497             c = self.con.execute(sql, (path, version))
498             row = c.fetchone()
499             if not row:
500                 raise IndexError('Version does not exist')
501         return str(row[0]), str(row[1]), int(row[2]), int(row[3])
502     
503     def _put_version(self, path, user, size=0, hide=0):
504         sql = 'insert into versions (name, user, size, hide) values (?, ?, ?, ?)'
505         id = self.con.execute(sql, (path, user, size, hide)).lastrowid
506         self.con.commit()
507         return str(id)
508     
509     def _copy_version(self, user, src_path, dest_path, copy_meta=True, copy_data=True, src_version=None):
510         if src_version is not None:
511             src_version_id, muser, mtime, size = self._get_version(src_path, src_version)
512         else:
513             # Latest or create from scratch.
514             try:
515                 src_version_id, muser, mtime, size = self._get_version(src_path)
516             except NameError:
517                 src_version_id = None
518                 size = 0
519         if not copy_data:
520             size = 0
521         dest_version_id = self._put_version(dest_path, user, size)
522         if copy_meta and src_version_id is not None:
523             sql = 'insert into metadata select %s, key, value from metadata where version_id = ?'
524             sql = sql % dest_version_id
525             self.con.execute(sql, (src_version_id,))
526         if copy_data and src_version_id is not None:
527             sql = 'insert into hashmaps select %s, pos, block_id from hashmaps where version_id = ?'
528             sql = sql % dest_version_id
529             self.con.execute(sql, (src_version_id,))
530         self.con.commit()
531         return src_version_id, dest_version_id
532     
533     def _get_versioninfo(self, account, container, name, until=None):
534         """Return path, latest version, associated timestamp and size until the timestamp given."""
535         
536         p = (account, container, name)
537         try:
538             p = p[:p.index(None)]
539         except ValueError:
540             pass
541         path = os.path.join(*p)
542         sql = '''select version_id, tstamp, size from (%s) where name = ?'''
543         sql = sql % self._sql_until(until)
544         c = self.con.execute(sql, (path,))
545         row = c.fetchone()
546         if row is None:
547             raise NameError('Path does not exist')
548         return path, str(row[0]), int(row[1]), int(row[2])
549     
550     def _get_accountinfo(self, account, until=None):
551         try:
552             path, version_id, mtime, size = self._get_versioninfo(account, None, None, until)
553             return version_id, mtime
554         except:
555             raise NameError('Account does not exist')
556     
557     def _get_containerinfo(self, account, container, until=None):
558         try:
559             path, version_id, mtime, size = self._get_versioninfo(account, container, None, until)
560             return path, version_id, mtime
561         except:
562             raise NameError('Container does not exist')
563     
564     def _get_objectinfo(self, account, container, name, version=None):
565         path = os.path.join(account, container, name)
566         version_id, muser, mtime, size = self._get_version(path, version)
567         return path, version_id, muser, mtime, size
568     
569     def _get_metadata(self, path, version):
570         sql = 'select key, value from metadata where version_id = ?'
571         c = self.con.execute(sql, (version,))
572         return dict(c.fetchall())
573     
574     def _put_metadata(self, user, path, meta, replace=False):
575         """Create a new version and store metadata."""
576         
577         src_version_id, dest_version_id = self._copy_version(user, path, path, not replace, True)
578         for k, v in meta.iteritems():
579             if not replace and v == '':
580                 sql = 'delete from metadata where version_id = ? and key = ?'
581                 self.con.execute(sql, (dest_version_id, k))
582             else:
583                 sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
584                 self.con.execute(sql, (dest_version_id, k, v))
585         self.con.commit()
586     
587     def _get_groups(self, account):
588         sql = 'select name, users from groups where account = ?'
589         c = self.con.execute(sql, (account,))
590         return dict([(x[0], x[1].split(',')) for x in c.fetchall()])
591     
592     def _is_allowed(self, user, account, container, name, op='read'):
593         if user == account:
594             return True
595         path = os.path.join(account, container, name)
596         perm_path, perms = self._get_permissions(path)
597         
598         # Expand groups.
599         for x in ('read', 'write'):
600             g_perms = []
601             for y in perms.get(x, []):
602                 if ':' in y:
603                     g_account, g_name = y.split(':', 1)
604                     groups = self._get_groups(g_account)
605                     if g_name in groups:
606                         g_perms += groups[g_name]
607                 else:
608                     g_perms.append(y)
609             perms[x] = g_perms
610         
611         if op == 'read' and user in perms.get('read', []):
612             return True
613         if user in perms.get('write', []):
614             return True
615         return False
616     
617     def _can_read(self, user, account, container, name):
618         if not self._is_allowed(user, account, container, name, 'read'):
619             raise NotAllowedError
620     
621     def _can_write(self, user, account, container, name):
622         if not self._is_allowed(user, account, container, name, 'write'):
623             raise NotAllowedError
624     
625     def _check_permissions(self, path, permissions):
626         # Check for existing permissions.
627         sql = '''select name from permissions
628                     where name != ? and (name like ? or ? like name || ?)'''
629         c = self.con.execute(sql, (path, path + '%', path, '%'))
630         rows = c.fetchall()
631         if rows:
632             raise AttributeError('Permissions already set')
633         
634         # Format given permissions.
635         if len(permissions) == 0:
636             return '', ''
637         r = permissions.get('read', [])
638         w = permissions.get('write', [])
639         if True in [False or ',' in x for x in r]:
640             raise ValueError('Bad characters in read permissions')
641         if True in [False or ',' in x for x in w]:
642             raise ValueError('Bad characters in write permissions')
643         return ','.join(r), ','.join(w)
644     
645     def _get_permissions(self, path):
646         # Check for permissions at path or above.
647         sql = 'select name, read, write from permissions where ? like name || ?'
648         c = self.con.execute(sql, (path, '%'))
649         row = c.fetchone()
650         if not row:
651             return path, {}
652         
653         name, r, w = row
654         ret = {}
655         if w != '':
656             ret['write'] = w.split(',')
657         if r != '':
658             ret['read'] = r.split(',')
659         return name, ret
660     
661     def _put_permissions(self, path, r, w):
662         if r == '' and w == '':
663             sql = 'delete from permissions where name = ?'
664             self.con.execute(sql, (path,))
665         else:
666             sql = 'insert or replace into permissions (name, read, write) values (?, ?, ?)'
667             self.con.execute(sql, (path, r, w))
668         self.con.commit()
669     
670     def _list_objects(self, path, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, keys=[], until=None):
671         cont_prefix = path + '/'
672         if keys and len(keys) > 0:
673             sql = '''select distinct o.name, o.version_id from (%s) o, metadata m where o.name like ? and
674                         m.version_id = o.version_id and m.key in (%s) order by o.name'''
675             sql = sql % (self._sql_until(until), ', '.join('?' * len(keys)))
676             param = (cont_prefix + prefix + '%',) + tuple(keys)
677         else:
678             sql = 'select name, version_id from (%s) where name like ? order by name'
679             sql = sql % self._sql_until(until)
680             param = (cont_prefix + prefix + '%',)
681         c = self.con.execute(sql, param)
682         objects = [(x[0][len(cont_prefix):], x[1]) for x in c.fetchall()]
683         if delimiter:
684             pseudo_objects = []
685             for x in objects:
686                 pseudo_name = x[0]
687                 i = pseudo_name.find(delimiter, len(prefix))
688                 if not virtual:
689                     # If the delimiter is not found, or the name ends
690                     # with the delimiter's first occurence.
691                     if i == -1 or len(pseudo_name) == i + len(delimiter):
692                         pseudo_objects.append(x)
693                 else:
694                     # If the delimiter is found, keep up to (and including) the delimiter.
695                     if i != -1:
696                         pseudo_name = pseudo_name[:i + len(delimiter)]
697                     if pseudo_name not in [y[0] for y in pseudo_objects]:
698                         if pseudo_name == x[0]:
699                             pseudo_objects.append(x)
700                         else:
701                             pseudo_objects.append((pseudo_name, None))
702             objects = pseudo_objects
703         
704         start = 0
705         if marker:
706             try:
707                 start = [x[0] for x in objects].index(marker) + 1
708             except ValueError:
709                 pass
710         if not limit or limit > 10000:
711             limit = 10000
712         return objects[start:start + limit]
713     
714     def _del_path(self, path):
715         sql = '''delete from hashmaps where version_id in
716                     (select version_id from versions where name = ?)'''
717         self.con.execute(sql, (path,))
718         sql = '''delete from metadata where version_id in
719                     (select version_id from versions where name = ?)'''
720         self.con.execute(sql, (path,))
721         sql = '''delete from versions where name = ?'''
722         self.con.execute(sql, (path,))
723         sql = '''delete from permissions where name like ?'''
724         self.con.execute(sql, (path + '%',)) # Redundant.
725         self.con.commit()