Change conflict (409) replies format to text.
[pithos] / pithos / backends / simple.py
index 4970052..6d6efa2 100644 (file)
@@ -39,8 +39,8 @@ import hashlib
 import binascii
 
 from base import NotAllowedError, BaseBackend
-from pithos.lib.hashfiler import Mapper, Blocker
-
+from lib.hashfiler import Mapper, Blocker
+from django.utils.encoding import smart_unicode, smart_str
 
 logger = logging.getLogger(__name__)
 
@@ -70,7 +70,9 @@ class SimpleBackend(BaseBackend):
     Uses SQLite for storage.
     """
     
-    def __init__(self, db):
+    # TODO: Create account if not present in all functions.
+    
+    def __init__(self, db, db_options):
         self.hash_algorithm = 'sha256'
         self.block_size = 4 * 1024 * 1024 # 4MB
         
@@ -130,17 +132,29 @@ class SimpleBackend(BaseBackend):
         self.mapper = Mapper(**params)
     
     @backend_method
+    def list_accounts(self, user, marker=None, limit=10000):
+        """Return a list of accounts the user can access."""
+        
+        allowed = self._allowed_accounts(user)
+        start, limit = self._list_limits(allowed, marker, limit)
+        return allowed[start:start + limit]
+    
+    @backend_method
     def get_account_meta(self, user, account, until=None):
         """Return a dictionary with the account metadata."""
         
         logger.debug("get_account_meta: %s %s", account, until)
         if user != account:
-            raise NotAllowedError
+            if until or account not in self._allowed_accounts(user):
+                raise NotAllowedError
+        else:
+            self._create_account(user, account)
         try:
             version_id, mtime = self._get_accountinfo(account, until)
         except NameError:
+            # Account does not exist before until.
             version_id = None
-            mtime = 0
+            mtime = until
         count, bytes, tstamp = self._get_pathstats(account, until)
         if mtime > tstamp:
             tstamp = mtime
@@ -158,12 +172,14 @@ class SimpleBackend(BaseBackend):
         row = c.fetchone()
         count = row[0]
         
-        meta = self._get_metadata(account, version_id)
-        meta.update({'name': account, 'count': count, 'bytes': bytes})
-        if modified:
-            meta.update({'modified': modified})
-        if until is not None:
-            meta.update({'until_timestamp': tstamp})
+        if user != account:
+            meta = {'name': account}
+        else:
+            meta = self._get_metadata(account, version_id)
+            meta.update({'name': account, 'count': count, 'bytes': bytes})
+            if until is not None:
+                meta.update({'until_timestamp': tstamp})
+        meta.update({'modified': modified})
         return meta
     
     @backend_method
@@ -181,7 +197,10 @@ class SimpleBackend(BaseBackend):
         
         logger.debug("get_account_groups: %s", account)
         if user != account:
-            raise NotAllowedError
+            if account not in self._allowed_accounts(user):
+                raise NotAllowedError
+            return {}
+        self._create_account(user, account)
         return self._get_groups(account)
     
     @backend_method
@@ -191,6 +210,7 @@ class SimpleBackend(BaseBackend):
         logger.debug("update_account_groups: %s %s %s", account, groups, replace)
         if user != account:
             raise NotAllowedError
+        self._create_account(user, account)
         self._check_groups(groups)
         self._put_groups(account, groups, replace)
     
@@ -207,7 +227,7 @@ class SimpleBackend(BaseBackend):
             pass
         else:
             raise NameError('Account already exists')
-        version_id = self._put_version(account, user)
+        self._put_version(account, user)
     
     @backend_method
     def delete_account(self, user, account):
@@ -224,24 +244,22 @@ class SimpleBackend(BaseBackend):
         self._del_groups(account)
     
     @backend_method
-    def list_containers(self, user, account, marker=None, limit=10000, until=None):
+    def list_containers(self, user, account, marker=None, limit=10000, shared=False, until=None):
         """Return a list of containers existing under an account."""
         
         logger.debug("list_containers: %s %s %s %s", account, marker, limit, until)
         if user != account:
-            if until:
+            if until or account not in self._allowed_accounts(user):
                 raise NotAllowedError
-            containers = self._allowed_containers(user, account)
-            start = 0
-            if marker:
-                try:
-                    start = containers.index(marker) + 1
-                except ValueError:
-                    pass
-            if not limit or limit > 10000:
-                limit = 10000
-            return containers[start:start + limit]
-        return self._list_objects(account, '', '/', marker, limit, False, [], until)
+            allowed = self._allowed_containers(user, account)
+            start, limit = self._list_limits(allowed, marker, limit)
+            return allowed[start:start + limit]
+        else:
+            if shared:
+                allowed = [x.split('/', 2)[1] for x in self._shared_paths(account)]
+                start, limit = self._list_limits(allowed, marker, limit)
+                return allowed[start:start + limit]
+        return [x[0] for x in self._list_objects(account, '', '/', marker, limit, False, [], until)]
     
     @backend_method
     def get_container_meta(self, user, account, container, until=None):
@@ -249,7 +267,8 @@ class SimpleBackend(BaseBackend):
         
         logger.debug("get_container_meta: %s %s %s", account, container, until)
         if user != account:
-            raise NotAllowedError
+            if until or container not in self._allowed_containers(user, account):
+                raise NotAllowedError
         path, version_id, mtime = self._get_containerinfo(account, container, until)
         count, bytes, tstamp = self._get_pathstats(path, until)
         if mtime > tstamp:
@@ -261,10 +280,13 @@ class SimpleBackend(BaseBackend):
             if mtime > modified:
                 modified = mtime
         
-        meta = self._get_metadata(path, version_id)
-        meta.update({'name': container, 'count': count, 'bytes': bytes, 'modified': modified})
-        if until is not None:
-            meta.update({'until_timestamp': tstamp})
+        if user != account:
+            meta = {'name': container, 'modified': modified}
+        else:
+            meta = self._get_metadata(path, version_id)
+            meta.update({'name': container, 'count': count, 'bytes': bytes, 'modified': modified})
+            if until is not None:
+                meta.update({'until_timestamp': tstamp})
         return meta
     
     @backend_method
@@ -283,7 +305,9 @@ class SimpleBackend(BaseBackend):
         
         logger.debug("get_container_policy: %s %s", account, container)
         if user != account:
-            raise NotAllowedError
+            if container not in self._allowed_containers(user, account):
+                raise NotAllowedError
+            return {}
         path = self._get_containerinfo(account, container)[0]
         return self._get_policy(path)
     
@@ -319,8 +343,8 @@ class SimpleBackend(BaseBackend):
             raise NameError('Container already exists')
         if policy:
             self._check_policy(policy)
-        path = os.path.join(account, container)
-        version_id = self._put_version(path, user)
+        path = '/'.join((account, container))
+        version_id = self._put_version(path, user)[0]
         for k, v in self.default_policy.iteritems():
             if k not in policy:
                 policy[k] = v
@@ -356,27 +380,45 @@ class SimpleBackend(BaseBackend):
         self._copy_version(user, account, account, True, False) # New account version (for timestamp update).
     
     @backend_method
-    def list_objects(self, user, account, container, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, keys=[], until=None):
+    def list_objects(self, user, account, container, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, keys=[], shared=False, until=None):
         """Return a list of objects existing under a container."""
         
-        logger.debug("list_objects: %s %s %s %s %s %s %s %s %s", account, container, prefix, delimiter, marker, limit, virtual, keys, until)
+        logger.debug("list_objects: %s %s %s %s %s %s %s %s %s %s", account, container, prefix, delimiter, marker, limit, virtual, keys, shared, until)
+        allowed = []
         if user != account:
-            raise NotAllowedError
+            if until:
+                raise NotAllowedError
+            allowed = self._allowed_paths(user, '/'.join((account, container)))
+            if not allowed:
+                raise NotAllowedError
+        else:
+            if shared:
+                allowed = self._shared_paths('/'.join((account, container)))
         path, version_id, mtime = self._get_containerinfo(account, container, until)
-        return self._list_objects(path, prefix, delimiter, marker, limit, virtual, keys, until)
+        return self._list_objects(path, prefix, delimiter, marker, limit, virtual, keys, until, allowed)
     
     @backend_method
     def list_object_meta(self, user, account, container, until=None):
         """Return a list with all the container's object meta keys."""
         
         logger.debug("list_object_meta: %s %s %s", account, container, until)
+        allowed = []
         if user != account:
-            raise NotAllowedError
+            if until:
+                raise NotAllowedError
+            allowed = self._allowed_paths(user, '/'.join((account, container)))
+            if not allowed:
+                raise NotAllowedError
         path, version_id, mtime = self._get_containerinfo(account, container, until)
         sql = '''select distinct m.key from (%s) o, metadata m
                     where m.version_id = o.version_id and o.name like ?'''
         sql = sql % self._sql_until(until)
-        c = self.con.execute(sql, (path + '/%',))
+        param = (path + '/%',)
+        if allowed:
+            for x in allowed:
+                sql += ' and o.name like ?'
+                param += (x,)
+        c = self.con.execute(sql, param)
         return [x[0] for x in c.fetchall()]
     
     @backend_method
@@ -404,7 +446,7 @@ class SimpleBackend(BaseBackend):
         logger.debug("update_object_meta: %s %s %s %s %s", account, container, name, meta, replace)
         self._can_write(user, account, container, name)
         path, version_id, muser, mtime, size = self._get_objectinfo(account, container, name)
-        self._put_metadata(user, path, meta, replace)
+        return self._put_metadata(user, path, meta, replace)
     
     @backend_method
     def get_object_permissions(self, user, account, container, name):
@@ -471,7 +513,7 @@ class SimpleBackend(BaseBackend):
             ie.data = missing
             raise ie
         path = self._get_containerinfo(account, container)[0]
-        path = os.path.join(path, name)
+        path = '/'.join((path, name))
         if permissions is not None:
             r, w = self._check_permissions(path, permissions)
         src_version_id, dest_version_id = self._copy_version(user, path, path, not replace_meta, False)
@@ -483,6 +525,7 @@ class SimpleBackend(BaseBackend):
             self.con.execute(sql, (dest_version_id, k, v))
         if permissions is not None:
             self._put_permissions(path, r, w)
+        return dest_version_id
     
     @backend_method
     def copy_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None, src_version=None):
@@ -497,9 +540,9 @@ class SimpleBackend(BaseBackend):
         if src_version is None:
             src_path = self._get_objectinfo(account, src_container, src_name)[0]
         else:
-            src_path = os.path.join(account, src_container, src_name)
+            src_path = '/'.join((account, src_container, src_name))
         dest_path = self._get_containerinfo(account, dest_container)[0]
-        dest_path = os.path.join(dest_path, dest_name)
+        dest_path = '/'.join((dest_path, dest_name))
         if permissions is not None:
             r, w = self._check_permissions(dest_path, permissions)
         src_version_id, dest_version_id = self._copy_version(user, src_path, dest_path, not replace_meta, True, src_version)
@@ -508,14 +551,16 @@ class SimpleBackend(BaseBackend):
             self.con.execute(sql, (dest_version_id, k, v))
         if permissions is not None:
             self._put_permissions(dest_path, r, w)
+        return dest_version_id
     
     @backend_method
     def move_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None):
         """Move an object's data and metadata."""
         
         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)
-        self.copy_object(user, account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, permissions, None)
+        dest_version_id = self.copy_object(user, account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, permissions, None)
         self.delete_object(user, account, src_container, src_name)
+        return dest_version_id
     
     @backend_method
     def delete_object(self, user, account, container, name, until=None):
@@ -526,7 +571,7 @@ class SimpleBackend(BaseBackend):
             raise NotAllowedError
         
         if until is not None:
-            path = os.path.join(account, container, name)
+            path = '/'.join((account, container, name))
             sql = '''select version_id from versions where name = ? and tstamp <= ?'''
             c = self.con.execute(sql, (path, until))
             for v in [x[0] in c.fetchall()]:
@@ -550,7 +595,7 @@ class SimpleBackend(BaseBackend):
         logger.debug("list_versions: %s %s %s", account, container, name)
         self._can_read(user, account, container, name)
         # This will even show deleted versions.
-        path = os.path.join(account, container, name)
+        path = '/'.join((account, container, name))
         sql = '''select distinct version_id, tstamp from versions where name = ? and hide = 0'''
         c = self.con.execute(sql, (path,))
         return [(int(x[0]), int(x[1])) for x in c.fetchall()]
@@ -567,7 +612,7 @@ class SimpleBackend(BaseBackend):
     
     @backend_method(autocommit=0)
     def put_block(self, data):
-        """Create a block and return the hash."""
+        """Store a block and return the hash."""
         
         logger.debug("put_block: %s", len(data))
         hashes, absent = self.blocker.block_stor((data,))
@@ -619,13 +664,13 @@ class SimpleBackend(BaseBackend):
             row = c.fetchone()
             if not row:
                 raise IndexError('Version does not exist')
-        return str(row[0]), str(row[1]), int(row[2]), int(row[3])
+        return smart_str(row[0]), smart_str(row[1]), int(row[2]), int(row[3])
     
     def _put_version(self, path, user, size=0, hide=0):
         tstamp = int(time.time())
         sql = 'insert into versions (name, user, tstamp, size, hide) values (?, ?, ?, ?, ?)'
         id = self.con.execute(sql, (path, user, tstamp, size, hide)).lastrowid
-        return str(id)
+        return str(id), tstamp
     
     def _copy_version(self, user, src_path, dest_path, copy_meta=True, copy_data=True, src_version=None):
         if src_version is not None:
@@ -639,7 +684,7 @@ class SimpleBackend(BaseBackend):
                 size = 0
         if not copy_data:
             size = 0
-        dest_version_id = self._put_version(dest_path, user, size)
+        dest_version_id = self._put_version(dest_path, user, size)[0]
         if copy_meta and src_version_id is not None:
             sql = 'insert into metadata select %s, key, value from metadata where version_id = ?'
             sql = sql % dest_version_id
@@ -658,7 +703,7 @@ class SimpleBackend(BaseBackend):
             p = p[:p.index(None)]
         except ValueError:
             pass
-        path = os.path.join(*p)
+        path = '/'.join(p)
         sql = '''select version_id, tstamp, size from (%s) where name = ?'''
         sql = sql % self._sql_until(until)
         c = self.con.execute(sql, (path,))
@@ -682,10 +727,16 @@ class SimpleBackend(BaseBackend):
             raise NameError('Container does not exist')
     
     def _get_objectinfo(self, account, container, name, version=None):
-        path = os.path.join(account, container, name)
+        path = '/'.join((account, container, name))
         version_id, muser, mtime, size = self._get_version(path, version)
         return path, version_id, muser, mtime, size
     
+    def _create_account(self, user, account):
+        try:
+            self._get_accountinfo(account)
+        except NameError:
+            self._put_version(account, user)
+    
     def _get_metadata(self, path, version):
         sql = 'select key, value from metadata where version_id = ?'
         c = self.con.execute(sql, (version,))
@@ -702,6 +753,7 @@ class SimpleBackend(BaseBackend):
             else:
                 sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
                 self.con.execute(sql, (dest_version_id, k, v))
+        return dest_version_id
     
     def _check_policy(self, policy):
         for k in policy.keys():
@@ -723,17 +775,36 @@ class SimpleBackend(BaseBackend):
         c = self.con.execute(sql, (path,))
         return dict(c.fetchall())
     
-    def _list_objects(self, path, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, keys=[], until=None):
+    def _list_limits(self, listing, marker, limit):
+        start = 0
+        if marker:
+            try:
+                start = listing.index(marker) + 1
+            except ValueError:
+                pass
+        if not limit or limit > 10000:
+            limit = 10000
+        return start, limit
+    
+    def _list_objects(self, path, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, keys=[], until=None, allowed=[]):
         cont_prefix = path + '/'
         if keys and len(keys) > 0:
             sql = '''select distinct o.name, o.version_id from (%s) o, metadata m where o.name like ? and
-                        m.version_id = o.version_id and m.key in (%s) order by o.name'''
+                        m.version_id = o.version_id and m.key in (%s)'''
             sql = sql % (self._sql_until(until), ', '.join('?' * len(keys)))
             param = (cont_prefix + prefix + '%',) + tuple(keys)
+            if allowed:
+                sql += ' and (' + ' or '.join(('o.name like ?',) * len(allowed)) + ')'
+                param += tuple([x + '%' for x in allowed])
+            sql += ' order by o.name'
         else:
-            sql = 'select name, version_id from (%s) where name like ? order by name'
+            sql = 'select name, version_id from (%s) where name like ?'
             sql = sql % self._sql_until(until)
             param = (cont_prefix + prefix + '%',)
+            if allowed:
+                sql += ' and (' + ' or '.join(('name like ?',) * len(allowed)) + ')'
+                param += tuple([x + '%' for x in allowed])
+            sql += ' order by name'
         c = self.con.execute(sql, param)
         objects = [(x[0][len(cont_prefix):], x[1]) for x in c.fetchall()]
         if delimiter:
@@ -757,14 +828,7 @@ class SimpleBackend(BaseBackend):
                             pseudo_objects.append((pseudo_name, None))
             objects = pseudo_objects
         
-        start = 0
-        if marker:
-            try:
-                start = [x[0] for x in objects].index(marker) + 1
-            except ValueError:
-                pass
-        if not limit or limit > 10000:
-            limit = 10000
+        start, limit = self._list_limits([x[0] for x in objects], marker, limit)
         return objects[start:start + limit]
     
     def _del_version(self, version):
@@ -785,10 +849,10 @@ class SimpleBackend(BaseBackend):
         sql = 'select gname, user from groups where account = ?'
         c = self.con.execute(sql, (account,))
         groups = {}
-        for row in c.fetchall():
-            if row[0] not in groups:
-                groups[row[0]] = []
-            groups[row[0]].append(row[1])
+        for gname, user in c.fetchall():
+            if gname not in groups:
+                groups[gname] = []
+            groups[gname].append(user)
         return groups
     
     def _put_groups(self, account, groups, replace=False):
@@ -810,10 +874,10 @@ class SimpleBackend(BaseBackend):
         sql = '''select name from permissions
                     where name != ? and (name like ? or ? like name || ?)'''
         c = self.con.execute(sql, (path, path + '%', path, '%'))
-        row = c.fetchone()
-        if row:
+        rows = c.fetchall()
+        if rows:
             ae = AttributeError()
-            ae.data = row[0]
+            ae.data = rows
             raise ae
         
         # Format given permissions.
@@ -836,9 +900,11 @@ class SimpleBackend(BaseBackend):
         perms = {} # Return nothing, if nothing is set.
         for row in c.fetchall():
             name = row[0]
-            if row[1] not in perms:
-                perms[row[1]] = []
-            perms[row[1]].append(row[2])
+            op = row[1]
+            user = row[2]
+            if op not in perms:
+                perms[op] = []
+            perms[op].append(user)
         return name, perms
     
     def _put_permissions(self, path, r, w):
@@ -872,9 +938,9 @@ class SimpleBackend(BaseBackend):
         self.con.execute(sql, (path,))
     
     def _is_allowed(self, user, account, container, name, op='read'):
-        if user == account:
+        if smart_unicode(user) == smart_unicode(account):
             return True
-        path = os.path.join(account, container, name)
+        path = '/'.join((account, container, name))
         if op == 'read' and self._get_public(path):
             return True
         perm_path, perms = self._get_permissions(path)
@@ -886,12 +952,13 @@ class SimpleBackend(BaseBackend):
                 if ':' in y:
                     g_account, g_name = y.split(':', 1)
                     groups = self._get_groups(g_account)
-                    if g_name in groups:
+                    if g_name in groups.keys():
                         g_perms.update(groups[g_name])
                 else:
                     g_perms.add(y)
             perms[x] = g_perms
         
+        user = smart_unicode(user, strings_only=True)
         if op == 'read' and ('*' in perms['read'] or user in perms['read']):
             return True
         if '*' in perms['write'] or user in perms['write']:
@@ -927,3 +994,8 @@ class SimpleBackend(BaseBackend):
         for path in self._allowed_paths(user, account):
             allow.add(path.split('/', 2)[1])
         return sorted(allow)
+    
+    def _shared_paths(self, prefix):
+        sql = 'select distinct name from permissions where name like ?'
+        c = self.con.execute(sql, (prefix + '/%',))
+        return [x[0] for x in c.fetchall()]