Report allowed actions in cross-user object requests, with the 'X-Object-Allowed...
[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 hashlib
39 import binascii
40
41 from base import NotAllowedError, BaseBackend
42 from lib.hashfiler import Mapper, Blocker
43 from django.utils.encoding import smart_unicode, smart_str
44
45 logger = logging.getLogger(__name__)
46
47 def backend_method(func=None, autocommit=1):
48     if func is None:
49         def fn(func):
50             return backend_method(func, autocommit)
51         return fn
52
53     if not autocommit:
54         return func
55     def fn(self, *args, **kw):
56         self.con.execute('begin deferred')
57         try:
58             ret = func(self, *args, **kw)
59             self.con.commit()
60             return ret
61         except:
62             self.con.rollback()
63             raise
64     return fn
65
66
67 class SimpleBackend(BaseBackend):
68     """A simple backend.
69     
70     Uses SQLite for storage.
71     """
72     
73     # TODO: Create account if not present in all functions.
74     
75     def __init__(self, path, db):
76         self.hash_algorithm = 'sha256'
77         self.block_size = 4 * 1024 * 1024 # 4MB
78         
79         self.default_policy = {'quota': 0, 'versioning': 'auto'}
80         
81         if path and not os.path.exists(path):
82             os.makedirs(path)
83         if not os.path.isdir(path):
84             raise RuntimeError("Cannot open path '%s'" % (path,))
85         
86         self.con = sqlite3.connect(db, check_same_thread=False)
87         
88         sql = '''pragma foreign_keys = on'''
89         self.con.execute(sql)
90         
91         sql = '''create table if not exists versions (
92                     version_id integer primary key,
93                     name text,
94                     user text,
95                     tstamp integer not null,
96                     size integer default 0,
97                     hide integer default 0)'''
98         self.con.execute(sql)
99         sql = '''create table if not exists metadata (
100                     version_id integer,
101                     key text,
102                     value text,
103                     primary key (version_id, key)
104                     foreign key (version_id) references versions(version_id)
105                     on delete cascade)'''
106         self.con.execute(sql)
107         sql = '''create table if not exists policy (
108                     name text, key text, value text, primary key (name, key))'''
109         self.con.execute(sql)
110         
111         # Access control tables.
112         sql = '''create table if not exists groups (
113                     account text, gname text, user text)'''
114         self.con.execute(sql)
115         sql = '''create table if not exists permissions (
116                     name text, op text, user text)'''
117         self.con.execute(sql)
118         sql = '''create table if not exists public (
119                     name text, primary key (name))'''
120         self.con.execute(sql)
121         
122         self.con.commit()
123         
124         params = {'blocksize': self.block_size,
125                   'blockpath': os.path.join(path + '/blocks'),
126                   'hashtype': self.hash_algorithm}
127         self.blocker = Blocker(**params)
128         
129         params = {'mappath': os.path.join(path + '/maps'),
130                   'namelen': self.blocker.hashlen}
131         self.mapper = Mapper(**params)
132     
133     @backend_method
134     def list_accounts(self, user, marker=None, limit=10000):
135         """Return a list of accounts the user can access."""
136         
137         allowed = self._allowed_accounts(user)
138         start, limit = self._list_limits(allowed, marker, limit)
139         return allowed[start:start + limit]
140     
141     @backend_method
142     def get_account_meta(self, user, account, until=None):
143         """Return a dictionary with the account metadata."""
144         
145         logger.debug("get_account_meta: %s %s", account, until)
146         if user != account:
147             if until or account not in self._allowed_accounts(user):
148                 raise NotAllowedError
149         else:
150             self._create_account(user, account)
151         try:
152             version_id, mtime = self._get_accountinfo(account, until)
153         except NameError:
154             # Account does not exist before until.
155             version_id = None
156             mtime = until
157         count, bytes, tstamp = self._get_pathstats(account, until)
158         if mtime > tstamp:
159             tstamp = mtime
160         if until is None:
161             modified = tstamp
162         else:
163             modified = self._get_pathstats(account)[2] # Overall last modification
164             if mtime > modified:
165                 modified = mtime
166         
167         # Proper count.
168         sql = 'select count(name) from (%s) where name glob ? and not name glob ?'
169         sql = sql % self._sql_until(until)
170         c = self.con.execute(sql, (account + '/*', account + '/*/*'))
171         row = c.fetchone()
172         count = row[0]
173         
174         if user != account:
175             meta = {'name': account}
176         else:
177             meta = self._get_metadata(account, version_id)
178             meta.update({'name': account, 'count': count, 'bytes': bytes})
179             if until is not None:
180                 meta.update({'until_timestamp': tstamp})
181         meta.update({'modified': modified})
182         return meta
183     
184     @backend_method
185     def update_account_meta(self, user, account, meta, replace=False):
186         """Update the metadata associated with the account."""
187         
188         logger.debug("update_account_meta: %s %s %s", account, meta, replace)
189         if user != account:
190             raise NotAllowedError
191         self._put_metadata(user, account, meta, replace, False)
192     
193     @backend_method
194     def get_account_groups(self, user, account):
195         """Return a dictionary with the user groups defined for this account."""
196         
197         logger.debug("get_account_groups: %s", account)
198         if user != account:
199             if account not in self._allowed_accounts(user):
200                 raise NotAllowedError
201             return {}
202         self._create_account(user, account)
203         return self._get_groups(account)
204     
205     @backend_method
206     def update_account_groups(self, user, account, groups, replace=False):
207         """Update the groups associated with the account."""
208         
209         logger.debug("update_account_groups: %s %s %s", account, groups, replace)
210         if user != account:
211             raise NotAllowedError
212         self._create_account(user, account)
213         self._check_groups(groups)
214         self._put_groups(account, groups, replace)
215     
216     @backend_method
217     def put_account(self, user, account):
218         """Create a new account with the given name."""
219         
220         logger.debug("put_account: %s", account)
221         if user != account:
222             raise NotAllowedError
223         try:
224             version_id, mtime = self._get_accountinfo(account)
225         except NameError:
226             pass
227         else:
228             raise NameError('Account already exists')
229         self._put_version(account, user)
230     
231     @backend_method
232     def delete_account(self, user, account):
233         """Delete the account with the given name."""
234         
235         logger.debug("delete_account: %s", account)
236         if user != account:
237             raise NotAllowedError
238         count = self._get_pathstats(account)[0]
239         if count > 0:
240             raise IndexError('Account is not empty')
241         sql = 'delete from versions where name = ?'
242         self.con.execute(sql, (account,))
243         self._del_groups(account)
244     
245     @backend_method
246     def list_containers(self, user, account, marker=None, limit=10000, shared=False, until=None):
247         """Return a list of containers existing under an account."""
248         
249         logger.debug("list_containers: %s %s %s %s", account, marker, limit, until)
250         if user != account:
251             if until or account not in self._allowed_accounts(user):
252                 raise NotAllowedError
253             allowed = self._allowed_containers(user, account)
254             start, limit = self._list_limits(allowed, marker, limit)
255             return allowed[start:start + limit]
256         else:
257             if shared:
258                 allowed = [x.split('/', 2)[1] for x in self._shared_paths(account)]
259                 start, limit = self._list_limits(allowed, marker, limit)
260                 return allowed[start:start + limit]
261         return [x[0] for x in self._list_objects(account, '', '/', marker, limit, False, [], until)]
262     
263     @backend_method
264     def get_container_meta(self, user, account, container, until=None):
265         """Return a dictionary with the container metadata."""
266         
267         logger.debug("get_container_meta: %s %s %s", account, container, until)
268         if user != account:
269             if until or container not in self._allowed_containers(user, account):
270                 raise NotAllowedError
271         path, version_id, mtime = self._get_containerinfo(account, container, until)
272         count, bytes, tstamp = self._get_pathstats(path, until)
273         if mtime > tstamp:
274             tstamp = mtime
275         if until is None:
276             modified = tstamp
277         else:
278             modified = self._get_pathstats(path)[2] # Overall last modification
279             if mtime > modified:
280                 modified = mtime
281         
282         if user != account:
283             meta = {'name': container, 'modified': modified}
284         else:
285             meta = self._get_metadata(path, version_id)
286             meta.update({'name': container, 'count': count, 'bytes': bytes, 'modified': modified})
287             if until is not None:
288                 meta.update({'until_timestamp': tstamp})
289         return meta
290     
291     @backend_method
292     def update_container_meta(self, user, account, container, meta, replace=False):
293         """Update the metadata associated with the container."""
294         
295         logger.debug("update_container_meta: %s %s %s %s", account, container, meta, replace)
296         if user != account:
297             raise NotAllowedError
298         path, version_id, mtime = self._get_containerinfo(account, container)
299         self._put_metadata(user, path, meta, replace, False)
300     
301     @backend_method
302     def get_container_policy(self, user, account, container):
303         """Return a dictionary with the container policy."""
304         
305         logger.debug("get_container_policy: %s %s", account, container)
306         if user != account:
307             if container not in self._allowed_containers(user, account):
308                 raise NotAllowedError
309             return {}
310         path = self._get_containerinfo(account, container)[0]
311         return self._get_policy(path)
312     
313     @backend_method
314     def update_container_policy(self, user, account, container, policy, replace=False):
315         """Update the policy associated with the account."""
316         
317         logger.debug("update_container_policy: %s %s %s %s", account, container, policy, replace)
318         if user != account:
319             raise NotAllowedError
320         path = self._get_containerinfo(account, container)[0]
321         self._check_policy(policy)
322         if replace:
323             for k, v in self.default_policy.iteritems():
324                 if k not in policy:
325                     policy[k] = v
326         for k, v in policy.iteritems():
327             sql = 'insert or replace into policy (name, key, value) values (?, ?, ?)'
328             self.con.execute(sql, (path, k, v))
329     
330     @backend_method
331     def put_container(self, user, account, container, policy=None):
332         """Create a new container with the given name."""
333         
334         logger.debug("put_container: %s %s %s", account, container, policy)
335         if user != account:
336             raise NotAllowedError
337         try:
338             path, version_id, mtime = self._get_containerinfo(account, container)
339         except NameError:
340             pass
341         else:
342             raise NameError('Container already exists')
343         if policy:
344             self._check_policy(policy)
345         path = '/'.join((account, container))
346         version_id = self._put_version(path, user)[0]
347         for k, v in self.default_policy.iteritems():
348             if k not in policy:
349                 policy[k] = v
350         for k, v in policy.iteritems():
351             sql = 'insert or replace into policy (name, key, value) values (?, ?, ?)'
352             self.con.execute(sql, (path, k, v))
353     
354     @backend_method
355     def delete_container(self, user, account, container, until=None):
356         """Delete/purge the container with the given name."""
357         
358         logger.debug("delete_container: %s %s %s", account, container, until)
359         if user != account:
360             raise NotAllowedError
361         path, version_id, mtime = self._get_containerinfo(account, container)
362         
363         if until is not None:
364             sql = '''select version_id from versions where name like ? and tstamp <= ?
365                         and version_id not in (select version_id from (%s))'''
366             sql = sql % self._sql_until() # Do not delete current versions.
367             c = self.con.execute(sql, (path + '/%', until))
368             for v in [x[0] for x in c.fetchall()]:
369                 self._del_version(v)
370             return
371         
372         count = self._get_pathstats(path)[0]
373         if count > 0:
374             raise IndexError('Container is not empty')
375         sql = 'delete from versions where name = ? or name like ?' # May contain hidden items.
376         self.con.execute(sql, (path, path + '/%',))
377         sql = 'delete from policy where name = ?'
378         self.con.execute(sql, (path,))
379         self._copy_version(user, account, account, True, False) # New account version (for timestamp update).
380     
381     @backend_method
382     def list_objects(self, user, account, container, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, keys=[], shared=False, until=None):
383         """Return a list of objects existing under a container."""
384         
385         logger.debug("list_objects: %s %s %s %s %s %s %s %s %s %s", account, container, prefix, delimiter, marker, limit, virtual, keys, shared, until)
386         allowed = []
387         if user != account:
388             if until:
389                 raise NotAllowedError
390             allowed = self._allowed_paths(user, '/'.join((account, container)))
391             if not allowed:
392                 raise NotAllowedError
393         else:
394             if shared:
395                 allowed = self._shared_paths('/'.join((account, container)))
396         path, version_id, mtime = self._get_containerinfo(account, container, until)
397         return self._list_objects(path, prefix, delimiter, marker, limit, virtual, keys, until, allowed)
398     
399     @backend_method
400     def list_object_meta(self, user, account, container, until=None):
401         """Return a list with all the container's object meta keys."""
402         
403         logger.debug("list_object_meta: %s %s %s", account, container, until)
404         allowed = []
405         if user != account:
406             if until:
407                 raise NotAllowedError
408             allowed = self._allowed_paths(user, '/'.join((account, container)))
409             if not allowed:
410                 raise NotAllowedError
411         path, version_id, mtime = self._get_containerinfo(account, container, until)
412         sql = '''select distinct m.key from (%s) o, metadata m
413                     where m.version_id = o.version_id and o.name like ?'''
414         sql = sql % self._sql_until(until)
415         param = (path + '/%',)
416         if allowed:
417             for x in allowed:
418                 sql += ' and o.name like ?'
419                 param += (x,)
420         c = self.con.execute(sql, param)
421         return [x[0] for x in c.fetchall()]
422     
423     @backend_method
424     def get_object_meta(self, user, account, container, name, version=None):
425         """Return a dictionary with the object metadata."""
426         
427         logger.debug("get_object_meta: %s %s %s %s", account, container, name, version)
428         self._can_read(user, account, container, name)
429         path, version_id, muser, mtime, size = self._get_objectinfo(account, container, name, version)
430         if version is None:
431             modified = mtime
432         else:
433             modified = self._get_version(path, version)[2] # Overall last modification
434         
435         meta = self._get_metadata(path, version_id)
436         meta.update({'name': name, 'bytes': size})
437         meta.update({'version': version_id, 'version_timestamp': mtime})
438         meta.update({'modified': modified, 'modified_by': muser})
439         return meta
440     
441     @backend_method
442     def update_object_meta(self, user, account, container, name, meta, replace=False):
443         """Update the metadata associated with the object."""
444         
445         logger.debug("update_object_meta: %s %s %s %s %s", account, container, name, meta, replace)
446         self._can_write(user, account, container, name)
447         path, version_id, muser, mtime, size = self._get_objectinfo(account, container, name)
448         return self._put_metadata(user, path, meta, replace)
449     
450     @backend_method
451     def get_object_permissions(self, user, account, container, name):
452         """Return the action allowed on the object, the path
453         from which the object gets its permissions from,
454         along with a dictionary containing the permissions."""
455         
456         logger.debug("get_object_permissions: %s %s %s", account, container, name)
457         allowed = 'write'
458         if user != account:
459             if self._is_allowed(user, account, container, name, 'write'):
460                 allowed = 'write'
461             elif self._is_allowed(user, account, container, name, 'read'):
462                 allowed = 'read'
463             else:
464                 raise NotAllowedError
465         path = self._get_objectinfo(account, container, name)[0]
466         return (allowed,) + self._get_permissions(path)
467     
468     @backend_method
469     def update_object_permissions(self, user, account, container, name, permissions):
470         """Update the permissions associated with the object."""
471         
472         logger.debug("update_object_permissions: %s %s %s %s", account, container, name, permissions)
473         if user != account:
474             raise NotAllowedError
475         path = self._get_objectinfo(account, container, name)[0]
476         r, w = self._check_permissions(path, permissions)
477         self._put_permissions(path, r, w)
478     
479     @backend_method
480     def get_object_public(self, user, account, container, name):
481         """Return the public URL of the object if applicable."""
482         
483         logger.debug("get_object_public: %s %s %s", account, container, name)
484         self._can_read(user, account, container, name)
485         path = self._get_objectinfo(account, container, name)[0]
486         if self._get_public(path):
487             return '/public/' + path
488         return None
489     
490     @backend_method
491     def update_object_public(self, user, account, container, name, public):
492         """Update the public status of the object."""
493         
494         logger.debug("update_object_public: %s %s %s %s", account, container, name, public)
495         self._can_write(user, account, container, name)
496         path = self._get_objectinfo(account, container, name)[0]
497         self._put_public(path, public)
498     
499     @backend_method
500     def get_object_hashmap(self, user, account, container, name, version=None):
501         """Return the object's size and a list with partial hashes."""
502         
503         logger.debug("get_object_hashmap: %s %s %s %s", account, container, name, version)
504         self._can_read(user, account, container, name)
505         path, version_id, muser, mtime, size = self._get_objectinfo(account, container, name, version)
506         hashmap = self.mapper.map_retr(version_id)
507         return size, [binascii.hexlify(x) for x in hashmap]
508     
509     @backend_method
510     def update_object_hashmap(self, user, account, container, name, size, hashmap, meta={}, replace_meta=False, permissions=None):
511         """Create/update an object with the specified size and partial hashes."""
512         
513         logger.debug("update_object_hashmap: %s %s %s %s %s", account, container, name, size, hashmap)
514         if permissions is not None and user != account:
515             raise NotAllowedError
516         self._can_write(user, account, container, name)
517         missing = self.blocker.block_ping([binascii.unhexlify(x) for x in hashmap])
518         if missing:
519             ie = IndexError()
520             ie.data = [binascii.hexlify(x) for x in missing]
521             raise ie
522         path = self._get_containerinfo(account, container)[0]
523         path = '/'.join((path, name))
524         if permissions is not None:
525             r, w = self._check_permissions(path, permissions)
526         src_version_id, dest_version_id = self._copy_version(user, path, path, not replace_meta, False)
527         sql = 'update versions set size = ? where version_id = ?'
528         self.con.execute(sql, (size, dest_version_id))
529         self.mapper.map_stor(dest_version_id, [binascii.unhexlify(x) for x in hashmap])
530         for k, v in meta.iteritems():
531             sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
532             self.con.execute(sql, (dest_version_id, k, v))
533         if permissions is not None:
534             self._put_permissions(path, r, w)
535         return dest_version_id
536     
537     @backend_method
538     def copy_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None, src_version=None):
539         """Copy an object's data and metadata."""
540         
541         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)
542         if permissions is not None and user != account:
543             raise NotAllowedError
544         self._can_read(user, account, src_container, src_name)
545         self._can_write(user, account, dest_container, dest_name)
546         self._get_containerinfo(account, src_container)
547         if src_version is None:
548             src_path = self._get_objectinfo(account, src_container, src_name)[0]
549         else:
550             src_path = '/'.join((account, src_container, src_name))
551         dest_path = self._get_containerinfo(account, dest_container)[0]
552         dest_path = '/'.join((dest_path, dest_name))
553         if permissions is not None:
554             r, w = self._check_permissions(dest_path, permissions)
555         src_version_id, dest_version_id = self._copy_version(user, src_path, dest_path, not replace_meta, True, src_version)
556         for k, v in dest_meta.iteritems():
557             sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
558             self.con.execute(sql, (dest_version_id, k, v))
559         if permissions is not None:
560             self._put_permissions(dest_path, r, w)
561         return dest_version_id
562     
563     @backend_method
564     def move_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None):
565         """Move an object's data and metadata."""
566         
567         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)
568         dest_version_id = self.copy_object(user, account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, permissions, None)
569         self.delete_object(user, account, src_container, src_name)
570         return dest_version_id
571     
572     @backend_method
573     def delete_object(self, user, account, container, name, until=None):
574         """Delete/purge an object."""
575         
576         logger.debug("delete_object: %s %s %s %s", account, container, name, until)
577         if user != account:
578             raise NotAllowedError
579         
580         if until is not None:
581             path = '/'.join((account, container, name))
582             sql = '''select version_id from versions where name = ? and tstamp <= ?'''
583             c = self.con.execute(sql, (path, until))
584             for v in [x[0] in c.fetchall()]:
585                 self._del_version(v)
586             try:
587                 version_id = self._get_version(path)[0]
588             except NameError:
589                 pass
590             else:
591                 self._del_sharing(path)
592             return
593         
594         path = self._get_objectinfo(account, container, name)[0]
595         self._put_version(path, user, 0, 1)
596         self._del_sharing(path)
597     
598     @backend_method
599     def list_versions(self, user, account, container, name):
600         """Return a list of all (version, version_timestamp) tuples for an object."""
601         
602         logger.debug("list_versions: %s %s %s", account, container, name)
603         self._can_read(user, account, container, name)
604         # This will even show deleted versions.
605         path = '/'.join((account, container, name))
606         sql = '''select distinct version_id, tstamp from versions where name = ? and hide = 0'''
607         c = self.con.execute(sql, (path,))
608         return [(int(x[0]), int(x[1])) for x in c.fetchall()]
609     
610     @backend_method(autocommit=0)
611     def get_block(self, hash):
612         """Return a block's data."""
613         
614         logger.debug("get_block: %s", hash)
615         blocks = self.blocker.block_retr((binascii.unhexlify(hash),))
616         if not blocks:
617             raise NameError('Block does not exist')
618         return blocks[0]
619     
620     @backend_method(autocommit=0)
621     def put_block(self, data):
622         """Store a block and return the hash."""
623         
624         logger.debug("put_block: %s", len(data))
625         hashes, absent = self.blocker.block_stor((data,))
626         return binascii.hexlify(hashes[0])
627     
628     @backend_method(autocommit=0)
629     def update_block(self, hash, data, offset=0):
630         """Update a known block and return the hash."""
631         
632         logger.debug("update_block: %s %s %s", hash, len(data), offset)
633         if offset == 0 and len(data) == self.block_size:
634             return self.put_block(data)
635         h, e = self.blocker.block_delta(binascii.unhexlify(hash), ((offset, data),))
636         return binascii.hexlify(h)
637     
638     def _sql_until(self, until=None):
639         """Return the sql to get the latest versions until the timestamp given."""
640         if until is None:
641             until = int(time.time())
642         sql = '''select version_id, name, tstamp, size from versions v
643                     where version_id = (select max(version_id) from versions
644                                         where v.name = name and tstamp <= %s)
645                     and hide = 0'''
646         return sql % (until,)
647     
648     def _get_pathstats(self, path, until=None):
649         """Return count and sum of size of everything under path and latest timestamp."""
650         
651         sql = 'select count(version_id), total(size), max(tstamp) from (%s) where name like ?'
652         sql = sql % self._sql_until(until)
653         c = self.con.execute(sql, (path + '/%',))
654         row = c.fetchone()
655         tstamp = row[2] if row[2] is not None else 0
656         return int(row[0]), int(row[1]), int(tstamp)
657     
658     def _get_version(self, path, version=None):
659         if version is None:
660             sql = '''select version_id, user, tstamp, size, hide from versions where name = ?
661                         order by version_id desc limit 1'''
662             c = self.con.execute(sql, (path,))
663             row = c.fetchone()
664             if not row or int(row[4]):
665                 raise NameError('Object does not exist')
666         else:
667             # The database (sqlite) will not complain if the version is not an integer.
668             sql = '''select version_id, user, tstamp, size from versions where name = ?
669                         and version_id = ?'''
670             c = self.con.execute(sql, (path, version))
671             row = c.fetchone()
672             if not row:
673                 raise IndexError('Version does not exist')
674         return smart_str(row[0]), smart_str(row[1]), int(row[2]), int(row[3])
675     
676     def _put_version(self, path, user, size=0, hide=0):
677         tstamp = int(time.time())
678         sql = 'insert into versions (name, user, tstamp, size, hide) values (?, ?, ?, ?, ?)'
679         id = self.con.execute(sql, (path, user, tstamp, size, hide)).lastrowid
680         return str(id), tstamp
681     
682     def _copy_version(self, user, src_path, dest_path, copy_meta=True, copy_data=True, src_version=None):
683         if src_version is not None:
684             src_version_id, muser, mtime, size = self._get_version(src_path, src_version)
685         else:
686             # Latest or create from scratch.
687             try:
688                 src_version_id, muser, mtime, size = self._get_version(src_path)
689             except NameError:
690                 src_version_id = None
691                 size = 0
692         if not copy_data:
693             size = 0
694         dest_version_id = self._put_version(dest_path, user, size)[0]
695         if copy_meta and src_version_id is not None:
696             sql = 'insert into metadata select %s, key, value from metadata where version_id = ?'
697             sql = sql % dest_version_id
698             self.con.execute(sql, (src_version_id,))
699         if copy_data and src_version_id is not None:
700             # TODO: Copy properly.
701             hashmap = self.mapper.map_retr(src_version_id)
702             self.mapper.map_stor(dest_version_id, hashmap)
703         return src_version_id, dest_version_id
704     
705     def _get_versioninfo(self, account, container, name, until=None):
706         """Return path, latest version, associated timestamp and size until the timestamp given."""
707         
708         p = (account, container, name)
709         try:
710             p = p[:p.index(None)]
711         except ValueError:
712             pass
713         path = '/'.join(p)
714         sql = '''select version_id, tstamp, size from (%s) where name = ?'''
715         sql = sql % self._sql_until(until)
716         c = self.con.execute(sql, (path,))
717         row = c.fetchone()
718         if row is None:
719             raise NameError('Path does not exist')
720         return path, str(row[0]), int(row[1]), int(row[2])
721     
722     def _get_accountinfo(self, account, until=None):
723         try:
724             path, version_id, mtime, size = self._get_versioninfo(account, None, None, until)
725             return version_id, mtime
726         except:
727             raise NameError('Account does not exist')
728     
729     def _get_containerinfo(self, account, container, until=None):
730         try:
731             path, version_id, mtime, size = self._get_versioninfo(account, container, None, until)
732             return path, version_id, mtime
733         except:
734             raise NameError('Container does not exist')
735     
736     def _get_objectinfo(self, account, container, name, version=None):
737         path = '/'.join((account, container, name))
738         version_id, muser, mtime, size = self._get_version(path, version)
739         return path, version_id, muser, mtime, size
740     
741     def _create_account(self, user, account):
742         try:
743             self._get_accountinfo(account)
744         except NameError:
745             self._put_version(account, user)
746     
747     def _get_metadata(self, path, version):
748         sql = 'select key, value from metadata where version_id = ?'
749         c = self.con.execute(sql, (version,))
750         return dict(c.fetchall())
751     
752     def _put_metadata(self, user, path, meta, replace=False, copy_data=True):
753         """Create a new version and store metadata."""
754         
755         src_version_id, dest_version_id = self._copy_version(user, path, path, not replace, copy_data)
756         for k, v in meta.iteritems():
757             if not replace and v == '':
758                 sql = 'delete from metadata where version_id = ? and key = ?'
759                 self.con.execute(sql, (dest_version_id, k))
760             else:
761                 sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
762                 self.con.execute(sql, (dest_version_id, k, v))
763         return dest_version_id
764     
765     def _check_policy(self, policy):
766         for k in policy.keys():
767             if policy[k] == '':
768                 policy[k] = self.default_policy.get(k)
769         for k, v in policy.iteritems():
770             if k == 'quota':
771                 q = int(v) # May raise ValueError.
772                 if q < 0:
773                     raise ValueError
774             elif k == 'versioning':
775                 if v not in ['auto', 'manual', 'none']:
776                     raise ValueError
777             else:
778                 raise ValueError
779     
780     def _get_policy(self, path):
781         sql = 'select key, value from policy where name = ?'
782         c = self.con.execute(sql, (path,))
783         return dict(c.fetchall())
784     
785     def _list_limits(self, listing, marker, limit):
786         start = 0
787         if marker:
788             try:
789                 start = listing.index(marker) + 1
790             except ValueError:
791                 pass
792         if not limit or limit > 10000:
793             limit = 10000
794         return start, limit
795     
796     def _list_objects(self, path, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, keys=[], until=None, allowed=[]):
797         cont_prefix = path + '/'
798         if keys and len(keys) > 0:
799             sql = '''select distinct o.name, o.version_id from (%s) o, metadata m where o.name like ? and
800                         m.version_id = o.version_id and m.key in (%s)'''
801             sql = sql % (self._sql_until(until), ', '.join('?' * len(keys)))
802             param = (cont_prefix + prefix + '%',) + tuple(keys)
803             if allowed:
804                 sql += ' and (' + ' or '.join(('o.name like ?',) * len(allowed)) + ')'
805                 param += tuple([x + '%' for x in allowed])
806             sql += ' order by o.name'
807         else:
808             sql = 'select name, version_id from (%s) where name like ?'
809             sql = sql % self._sql_until(until)
810             param = (cont_prefix + prefix + '%',)
811             if allowed:
812                 sql += ' and (' + ' or '.join(('name like ?',) * len(allowed)) + ')'
813                 param += tuple([x + '%' for x in allowed])
814             sql += ' order by name'
815         c = self.con.execute(sql, param)
816         objects = [(x[0][len(cont_prefix):], x[1]) for x in c.fetchall()]
817         if delimiter:
818             pseudo_objects = []
819             for x in objects:
820                 pseudo_name = x[0]
821                 i = pseudo_name.find(delimiter, len(prefix))
822                 if not virtual:
823                     # If the delimiter is not found, or the name ends
824                     # with the delimiter's first occurence.
825                     if i == -1 or len(pseudo_name) == i + len(delimiter):
826                         pseudo_objects.append(x)
827                 else:
828                     # If the delimiter is found, keep up to (and including) the delimiter.
829                     if i != -1:
830                         pseudo_name = pseudo_name[:i + len(delimiter)]
831                     if pseudo_name not in [y[0] for y in pseudo_objects]:
832                         if pseudo_name == x[0]:
833                             pseudo_objects.append(x)
834                         else:
835                             pseudo_objects.append((pseudo_name, None))
836             objects = pseudo_objects
837         
838         start, limit = self._list_limits([x[0] for x in objects], marker, limit)
839         return objects[start:start + limit]
840     
841     def _del_version(self, version):
842         self.mapper.map_remv(version)
843         sql = 'delete from versions where version_id = ?'
844         self.con.execute(sql, (version,))
845     
846     # Access control functions.
847     
848     def _check_groups(self, groups):
849         # Example follows.
850         # for k, v in groups.iteritems():
851         #     if True in [False or ',' in x for x in v]:
852         #         raise ValueError('Bad characters in groups')
853         pass
854     
855     def _get_groups(self, account):
856         sql = 'select gname, user from groups where account = ?'
857         c = self.con.execute(sql, (account,))
858         groups = {}
859         for gname, user in c.fetchall():
860             if gname not in groups:
861                 groups[gname] = []
862             groups[gname].append(user)
863         return groups
864     
865     def _put_groups(self, account, groups, replace=False):
866         if replace:
867             self._del_groups(account)
868         for k, v in groups.iteritems():
869             sql = 'delete from groups where account = ? and gname = ?'
870             self.con.execute(sql, (account, k))
871             if v:
872                 sql = 'insert into groups (account, gname, user) values (?, ?, ?)'
873                 self.con.executemany(sql, [(account, k, x) for x in v])
874     
875     def _del_groups(self, account):
876         sql = 'delete from groups where account = ?'
877         self.con.execute(sql, (account,))
878     
879     def _check_permissions(self, path, permissions):
880         # Check for existing permissions.
881         sql = '''select name from permissions
882                     where name != ? and (name like ? or ? like name || ?)'''
883         c = self.con.execute(sql, (path, path + '%', path, '%'))
884         rows = c.fetchall()
885         if rows:
886             ae = AttributeError()
887             ae.data = rows
888             raise ae
889         
890         # Format given permissions.
891         if len(permissions) == 0:
892             return [], []
893         r = permissions.get('read', [])
894         w = permissions.get('write', [])
895         # Examples follow.
896         # if True in [False or ',' in x for x in r]:
897         #     raise ValueError('Bad characters in read permissions')
898         # if True in [False or ',' in x for x in w]:
899         #     raise ValueError('Bad characters in write permissions')
900         return r, w
901     
902     def _get_permissions(self, path):
903         # Check for permissions at path or above.
904         sql = 'select name, op, user from permissions where ? like name || ?'
905         c = self.con.execute(sql, (path, '%'))
906         name = path
907         perms = {} # Return nothing, if nothing is set.
908         for row in c.fetchall():
909             name = row[0]
910             op = row[1]
911             user = row[2]
912             if op not in perms:
913                 perms[op] = []
914             perms[op].append(user)
915         return name, perms
916     
917     def _put_permissions(self, path, r, w):
918         sql = 'delete from permissions where name = ?'
919         self.con.execute(sql, (path,))
920         sql = 'insert into permissions (name, op, user) values (?, ?, ?)'
921         if r:
922             self.con.executemany(sql, [(path, 'read', x) for x in r])
923         if w:
924             self.con.executemany(sql, [(path, 'write', x) for x in w])
925     
926     def _get_public(self, path):
927         sql = 'select name from public where name = ?'
928         c = self.con.execute(sql, (path,))
929         row = c.fetchone()
930         if not row:
931             return False
932         return True
933     
934     def _put_public(self, path, public):
935         if not public:
936             sql = 'delete from public where name = ?'
937         else:
938             sql = 'insert or replace into public (name) values (?)'
939         self.con.execute(sql, (path,))
940     
941     def _del_sharing(self, path):
942         sql = 'delete from permissions where name = ?'
943         self.con.execute(sql, (path,))
944         sql = 'delete from public where name = ?'
945         self.con.execute(sql, (path,))
946     
947     def _is_allowed(self, user, account, container, name, op='read'):
948         if smart_unicode(user) == smart_unicode(account):
949             return True
950         path = '/'.join((account, container, name))
951         if op == 'read' and self._get_public(path):
952             return True
953         perm_path, perms = self._get_permissions(path)
954         
955         # Expand groups.
956         for x in ('read', 'write'):
957             g_perms = set()
958             for y in perms.get(x, []):
959                 if ':' in y:
960                     g_account, g_name = y.split(':', 1)
961                     groups = self._get_groups(g_account)
962                     if g_name in groups.keys():
963                         g_perms.update(groups[g_name])
964                 else:
965                     g_perms.add(y)
966             perms[x] = g_perms
967         
968         user = smart_unicode(user, strings_only=True)
969         if op == 'read' and ('*' in perms['read'] or user in perms['read']):
970             return True
971         if '*' in perms['write'] or user in perms['write']:
972             return True
973         return False
974     
975     def _can_read(self, user, account, container, name):
976         if not self._is_allowed(user, account, container, name, 'read'):
977             raise NotAllowedError
978     
979     def _can_write(self, user, account, container, name):
980         if not self._is_allowed(user, account, container, name, 'write'):
981             raise NotAllowedError
982     
983     def _allowed_paths(self, user, prefix=None):
984         sql = '''select distinct name from permissions 
985                  where (user = '*' or
986                         user = ? or
987                         user in (select account || ':' || gname from groups where user = ?))'''
988         param = (user, user)
989         if prefix:
990             sql += ' and name like ?'
991             param += (prefix + '/%',)
992         c = self.con.execute(sql, param)
993         return [x[0] for x in c.fetchall()]
994     
995     def _allowed_accounts(self, user):
996         allow = set()
997         for path in self._allowed_paths(user):
998             allow.add(path.split('/', 1)[0])
999         return sorted(allow)
1000     
1001     def _allowed_containers(self, user, account):
1002         allow = set()
1003         for path in self._allowed_paths(user, account):
1004             allow.add(path.split('/', 2)[1])
1005         return sorted(allow)
1006     
1007     def _shared_paths(self, prefix):
1008         sql = 'select distinct name from permissions where name like ?'
1009         c = self.con.execute(sql, (prefix + '/%',))
1010         return [x[0] for x in c.fetchall()]