1 # Copyright 2011 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
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.
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.
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.
41 from base import NotAllowedError, BaseBackend
42 from lib.hashfiler import Mapper, Blocker
43 from django.utils.encoding import smart_unicode, smart_str
45 logger = logging.getLogger(__name__)
47 def backend_method(func=None, autocommit=1):
50 return backend_method(func, autocommit)
55 def fn(self, *args, **kw):
56 self.con.execute('begin deferred')
58 ret = func(self, *args, **kw)
67 class SimpleBackend(BaseBackend):
70 Uses SQLite for storage.
73 # TODO: Create account if not present in all functions.
75 def __init__(self, db):
76 self.hash_algorithm = 'sha256'
77 self.block_size = 4 * 1024 * 1024 # 4MB
79 self.default_policy = {'quota': 0, 'versioning': 'auto'}
81 basepath = os.path.split(db)[0]
82 if basepath and not os.path.exists(basepath):
84 if not os.path.isdir(basepath):
85 raise RuntimeError("Cannot open database at '%s'" % (db,))
87 self.con = sqlite3.connect(basepath + '/db', check_same_thread=False)
89 sql = '''pragma foreign_keys = on'''
92 sql = '''create table if not exists versions (
93 version_id integer primary key,
96 tstamp integer not null,
97 size integer default 0,
98 hide integer default 0)'''
100 sql = '''create table if not exists metadata (
104 primary key (version_id, key)
105 foreign key (version_id) references versions(version_id)
106 on delete cascade)'''
107 self.con.execute(sql)
108 sql = '''create table if not exists policy (
109 name text, key text, value text, primary key (name, key))'''
110 self.con.execute(sql)
112 # Access control tables.
113 sql = '''create table if not exists groups (
114 account text, gname text, user text)'''
115 self.con.execute(sql)
116 sql = '''create table if not exists permissions (
117 name text, op text, user text)'''
118 self.con.execute(sql)
119 sql = '''create table if not exists public (
120 name text, primary key (name))'''
121 self.con.execute(sql)
125 params = {'blocksize': self.block_size,
126 'blockpath': basepath + '/blocks',
127 'hashtype': self.hash_algorithm}
128 self.blocker = Blocker(**params)
130 params = {'mappath': basepath + '/maps',
131 'namelen': self.blocker.hashlen}
132 self.mapper = Mapper(**params)
135 def list_accounts(self, user, marker=None, limit=10000):
136 """Return a list of accounts the user can access."""
138 allowed = self._allowed_accounts(user)
139 start, limit = self._list_limits(allowed, marker, limit)
140 return allowed[start:start + limit]
143 def get_account_meta(self, user, account, until=None):
144 """Return a dictionary with the account metadata."""
146 logger.debug("get_account_meta: %s %s", account, until)
148 if until or account not in self._allowed_accounts(user):
149 raise NotAllowedError
151 self._create_account(user, account)
153 version_id, mtime = self._get_accountinfo(account, until)
155 # Account does not exist before until.
158 count, bytes, tstamp = self._get_pathstats(account, until)
164 modified = self._get_pathstats(account)[2] # Overall last modification
169 sql = 'select count(name) from (%s) where name glob ? and not name glob ?'
170 sql = sql % self._sql_until(until)
171 c = self.con.execute(sql, (account + '/*', account + '/*/*'))
176 meta = {'name': account}
178 meta = self._get_metadata(account, version_id)
179 meta.update({'name': account, 'count': count, 'bytes': bytes})
180 if until is not None:
181 meta.update({'until_timestamp': tstamp})
182 meta.update({'modified': modified})
186 def update_account_meta(self, user, account, meta, replace=False):
187 """Update the metadata associated with the account."""
189 logger.debug("update_account_meta: %s %s %s", account, meta, replace)
191 raise NotAllowedError
192 self._put_metadata(user, account, meta, replace, False)
195 def get_account_groups(self, user, account):
196 """Return a dictionary with the user groups defined for this account."""
198 logger.debug("get_account_groups: %s", account)
200 if account not in self._allowed_accounts(user):
201 raise NotAllowedError
203 self._create_account(user, account)
204 return self._get_groups(account)
207 def update_account_groups(self, user, account, groups, replace=False):
208 """Update the groups associated with the account."""
210 logger.debug("update_account_groups: %s %s %s", account, groups, replace)
212 raise NotAllowedError
213 self._create_account(user, account)
214 self._check_groups(groups)
215 self._put_groups(account, groups, replace)
218 def put_account(self, user, account):
219 """Create a new account with the given name."""
221 logger.debug("put_account: %s", account)
223 raise NotAllowedError
225 version_id, mtime = self._get_accountinfo(account)
229 raise NameError('Account already exists')
230 self._put_version(account, user)
233 def delete_account(self, user, account):
234 """Delete the account with the given name."""
236 logger.debug("delete_account: %s", account)
238 raise NotAllowedError
239 count = self._get_pathstats(account)[0]
241 raise IndexError('Account is not empty')
242 sql = 'delete from versions where name = ?'
243 self.con.execute(sql, (account,))
244 self._del_groups(account)
247 def list_containers(self, user, account, marker=None, limit=10000, shared=False, until=None):
248 """Return a list of containers existing under an account."""
250 logger.debug("list_containers: %s %s %s %s", account, marker, limit, until)
252 if until or account not in self._allowed_accounts(user):
253 raise NotAllowedError
254 allowed = self._allowed_containers(user, account)
255 start, limit = self._list_limits(allowed, marker, limit)
256 return allowed[start:start + limit]
259 allowed = [x.split('/', 2)[1] for x in self._shared_paths(account)]
260 start, limit = self._list_limits(allowed, marker, limit)
261 return allowed[start:start + limit]
262 return [x[0] for x in self._list_objects(account, '', '/', marker, limit, False, [], until)]
265 def get_container_meta(self, user, account, container, until=None):
266 """Return a dictionary with the container metadata."""
268 logger.debug("get_container_meta: %s %s %s", account, container, until)
270 if until or container not in self._allowed_containers(user, account):
271 raise NotAllowedError
272 path, version_id, mtime = self._get_containerinfo(account, container, until)
273 count, bytes, tstamp = self._get_pathstats(path, until)
279 modified = self._get_pathstats(path)[2] # Overall last modification
284 meta = {'name': container, 'modified': modified}
286 meta = self._get_metadata(path, version_id)
287 meta.update({'name': container, 'count': count, 'bytes': bytes, 'modified': modified})
288 if until is not None:
289 meta.update({'until_timestamp': tstamp})
293 def update_container_meta(self, user, account, container, meta, replace=False):
294 """Update the metadata associated with the container."""
296 logger.debug("update_container_meta: %s %s %s %s", account, container, meta, replace)
298 raise NotAllowedError
299 path, version_id, mtime = self._get_containerinfo(account, container)
300 self._put_metadata(user, path, meta, replace, False)
303 def get_container_policy(self, user, account, container):
304 """Return a dictionary with the container policy."""
306 logger.debug("get_container_policy: %s %s", account, container)
308 if container not in self._allowed_containers(user, account):
309 raise NotAllowedError
311 path = self._get_containerinfo(account, container)[0]
312 return self._get_policy(path)
315 def update_container_policy(self, user, account, container, policy, replace=False):
316 """Update the policy associated with the account."""
318 logger.debug("update_container_policy: %s %s %s %s", account, container, policy, replace)
320 raise NotAllowedError
321 path = self._get_containerinfo(account, container)[0]
322 self._check_policy(policy)
324 for k, v in self.default_policy.iteritems():
327 for k, v in policy.iteritems():
328 sql = 'insert or replace into policy (name, key, value) values (?, ?, ?)'
329 self.con.execute(sql, (path, k, v))
332 def put_container(self, user, account, container, policy=None):
333 """Create a new container with the given name."""
335 logger.debug("put_container: %s %s %s", account, container, policy)
337 raise NotAllowedError
339 path, version_id, mtime = self._get_containerinfo(account, container)
343 raise NameError('Container already exists')
345 self._check_policy(policy)
346 path = '/'.join((account, container))
347 version_id = self._put_version(path, user)[0]
348 for k, v in self.default_policy.iteritems():
351 for k, v in policy.iteritems():
352 sql = 'insert or replace into policy (name, key, value) values (?, ?, ?)'
353 self.con.execute(sql, (path, k, v))
356 def delete_container(self, user, account, container, until=None):
357 """Delete/purge the container with the given name."""
359 logger.debug("delete_container: %s %s %s", account, container, until)
361 raise NotAllowedError
362 path, version_id, mtime = self._get_containerinfo(account, container)
364 if until is not None:
365 sql = '''select version_id from versions where name like ? and tstamp <= ?
366 and version_id not in (select version_id from (%s))'''
367 sql = sql % self._sql_until() # Do not delete current versions.
368 c = self.con.execute(sql, (path + '/%', until))
369 for v in [x[0] for x in c.fetchall()]:
373 count = self._get_pathstats(path)[0]
375 raise IndexError('Container is not empty')
376 sql = 'delete from versions where name = ? or name like ?' # May contain hidden items.
377 self.con.execute(sql, (path, path + '/%',))
378 sql = 'delete from policy where name = ?'
379 self.con.execute(sql, (path,))
380 self._copy_version(user, account, account, True, False) # New account version (for timestamp update).
383 def list_objects(self, user, account, container, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, keys=[], shared=False, until=None):
384 """Return a list of objects existing under a container."""
386 logger.debug("list_objects: %s %s %s %s %s %s %s %s %s %s", account, container, prefix, delimiter, marker, limit, virtual, keys, shared, until)
390 raise NotAllowedError
391 allowed = self._allowed_paths(user, '/'.join((account, container)))
393 raise NotAllowedError
396 allowed = self._shared_paths('/'.join((account, container)))
397 path, version_id, mtime = self._get_containerinfo(account, container, until)
398 return self._list_objects(path, prefix, delimiter, marker, limit, virtual, keys, until, allowed)
401 def list_object_meta(self, user, account, container, until=None):
402 """Return a list with all the container's object meta keys."""
404 logger.debug("list_object_meta: %s %s %s", account, container, until)
408 raise NotAllowedError
409 allowed = self._allowed_paths(user, '/'.join((account, container)))
411 raise NotAllowedError
412 path, version_id, mtime = self._get_containerinfo(account, container, until)
413 sql = '''select distinct m.key from (%s) o, metadata m
414 where m.version_id = o.version_id and o.name like ?'''
415 sql = sql % self._sql_until(until)
416 param = (path + '/%',)
419 sql += ' and o.name like ?'
421 c = self.con.execute(sql, param)
422 return [x[0] for x in c.fetchall()]
425 def get_object_meta(self, user, account, container, name, version=None):
426 """Return a dictionary with the object metadata."""
428 logger.debug("get_object_meta: %s %s %s %s", account, container, name, version)
429 self._can_read(user, account, container, name)
430 path, version_id, muser, mtime, size = self._get_objectinfo(account, container, name, version)
434 modified = self._get_version(path, version)[2] # Overall last modification
436 meta = self._get_metadata(path, version_id)
437 meta.update({'name': name, 'bytes': size})
438 meta.update({'version': version_id, 'version_timestamp': mtime})
439 meta.update({'modified': modified, 'modified_by': muser})
443 def update_object_meta(self, user, account, container, name, meta, replace=False):
444 """Update the metadata associated with the object."""
446 logger.debug("update_object_meta: %s %s %s %s %s", account, container, name, meta, replace)
447 self._can_write(user, account, container, name)
448 path, version_id, muser, mtime, size = self._get_objectinfo(account, container, name)
449 self._put_metadata(user, path, meta, replace)
452 def get_object_permissions(self, user, account, container, name):
453 """Return the path from which this object gets its permissions from,\
454 along with a dictionary containing the permissions."""
456 logger.debug("get_object_permissions: %s %s %s", account, container, name)
457 self._can_read(user, account, container, name)
458 path = self._get_objectinfo(account, container, name)[0]
459 return self._get_permissions(path)
462 def update_object_permissions(self, user, account, container, name, permissions):
463 """Update the permissions associated with the object."""
465 logger.debug("update_object_permissions: %s %s %s %s", account, container, name, permissions)
467 raise NotAllowedError
468 path = self._get_objectinfo(account, container, name)[0]
469 r, w = self._check_permissions(path, permissions)
470 self._put_permissions(path, r, w)
473 def get_object_public(self, user, account, container, name):
474 """Return the public URL of the object if applicable."""
476 logger.debug("get_object_public: %s %s %s", account, container, name)
477 self._can_read(user, account, container, name)
478 path = self._get_objectinfo(account, container, name)[0]
479 if self._get_public(path):
480 return '/public/' + path
484 def update_object_public(self, user, account, container, name, public):
485 """Update the public status of the object."""
487 logger.debug("update_object_public: %s %s %s %s", account, container, name, public)
488 self._can_write(user, account, container, name)
489 path = self._get_objectinfo(account, container, name)[0]
490 self._put_public(path, public)
493 def get_object_hashmap(self, user, account, container, name, version=None):
494 """Return the object's size and a list with partial hashes."""
496 logger.debug("get_object_hashmap: %s %s %s %s", account, container, name, version)
497 self._can_read(user, account, container, name)
498 path, version_id, muser, mtime, size = self._get_objectinfo(account, container, name, version)
499 hashmap = self.mapper.map_retr(version_id)
500 return size, [binascii.hexlify(x) for x in hashmap]
503 def update_object_hashmap(self, user, account, container, name, size, hashmap, meta={}, replace_meta=False, permissions=None):
504 """Create/update an object with the specified size and partial hashes."""
506 logger.debug("update_object_hashmap: %s %s %s %s %s", account, container, name, size, hashmap)
507 if permissions is not None and user != account:
508 raise NotAllowedError
509 self._can_write(user, account, container, name)
510 missing = self.blocker.block_ping([binascii.unhexlify(x) for x in hashmap])
515 path = self._get_containerinfo(account, container)[0]
516 path = '/'.join((path, name))
517 if permissions is not None:
518 r, w = self._check_permissions(path, permissions)
519 src_version_id, dest_version_id = self._copy_version(user, path, path, not replace_meta, False)
520 sql = 'update versions set size = ? where version_id = ?'
521 self.con.execute(sql, (size, dest_version_id))
522 self.mapper.map_stor(dest_version_id, [binascii.unhexlify(x) for x in hashmap])
523 for k, v in meta.iteritems():
524 sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
525 self.con.execute(sql, (dest_version_id, k, v))
526 if permissions is not None:
527 self._put_permissions(path, r, w)
530 def copy_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None, src_version=None):
531 """Copy an object's data and metadata."""
533 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)
534 if permissions is not None and user != account:
535 raise NotAllowedError
536 self._can_read(user, account, src_container, src_name)
537 self._can_write(user, account, dest_container, dest_name)
538 self._get_containerinfo(account, src_container)
539 if src_version is None:
540 src_path = self._get_objectinfo(account, src_container, src_name)[0]
542 src_path = '/'.join((account, src_container, src_name))
543 dest_path = self._get_containerinfo(account, dest_container)[0]
544 dest_path = '/'.join((dest_path, dest_name))
545 if permissions is not None:
546 r, w = self._check_permissions(dest_path, permissions)
547 src_version_id, dest_version_id = self._copy_version(user, src_path, dest_path, not replace_meta, True, src_version)
548 for k, v in dest_meta.iteritems():
549 sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
550 self.con.execute(sql, (dest_version_id, k, v))
551 if permissions is not None:
552 self._put_permissions(dest_path, r, w)
555 def move_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None):
556 """Move an object's data and metadata."""
558 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)
559 self.copy_object(user, account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, permissions, None)
560 self.delete_object(user, account, src_container, src_name)
563 def delete_object(self, user, account, container, name, until=None):
564 """Delete/purge an object."""
566 logger.debug("delete_object: %s %s %s %s", account, container, name, until)
568 raise NotAllowedError
570 if until is not None:
571 path = '/'.join((account, container, name))
572 sql = '''select version_id from versions where name = ? and tstamp <= ?'''
573 c = self.con.execute(sql, (path, until))
574 for v in [x[0] in c.fetchall()]:
577 version_id = self._get_version(path)[0]
581 self._del_sharing(path)
584 path = self._get_objectinfo(account, container, name)[0]
585 self._put_version(path, user, 0, 1)
586 self._del_sharing(path)
589 def list_versions(self, user, account, container, name):
590 """Return a list of all (version, version_timestamp) tuples for an object."""
592 logger.debug("list_versions: %s %s %s", account, container, name)
593 self._can_read(user, account, container, name)
594 # This will even show deleted versions.
595 path = '/'.join((account, container, name))
596 sql = '''select distinct version_id, tstamp from versions where name = ? and hide = 0'''
597 c = self.con.execute(sql, (path,))
598 return [(int(x[0]), int(x[1])) for x in c.fetchall()]
600 @backend_method(autocommit=0)
601 def get_block(self, hash):
602 """Return a block's data."""
604 logger.debug("get_block: %s", hash)
605 blocks = self.blocker.block_retr((binascii.unhexlify(hash),))
607 raise NameError('Block does not exist')
610 @backend_method(autocommit=0)
611 def put_block(self, data):
612 """Create a block and return the hash."""
614 logger.debug("put_block: %s", len(data))
615 hashes, absent = self.blocker.block_stor((data,))
616 return binascii.hexlify(hashes[0])
618 @backend_method(autocommit=0)
619 def update_block(self, hash, data, offset=0):
620 """Update a known block and return the hash."""
622 logger.debug("update_block: %s %s %s", hash, len(data), offset)
623 if offset == 0 and len(data) == self.block_size:
624 return self.put_block(data)
625 h, e = self.blocker.block_delta(binascii.unhexlify(hash), ((offset, data),))
626 return binascii.hexlify(h)
628 def _sql_until(self, until=None):
629 """Return the sql to get the latest versions until the timestamp given."""
631 until = int(time.time())
632 sql = '''select version_id, name, tstamp, size from versions v
633 where version_id = (select max(version_id) from versions
634 where v.name = name and tstamp <= %s)
636 return sql % (until,)
638 def _get_pathstats(self, path, until=None):
639 """Return count and sum of size of everything under path and latest timestamp."""
641 sql = 'select count(version_id), total(size), max(tstamp) from (%s) where name like ?'
642 sql = sql % self._sql_until(until)
643 c = self.con.execute(sql, (path + '/%',))
645 tstamp = row[2] if row[2] is not None else 0
646 return int(row[0]), int(row[1]), int(tstamp)
648 def _get_version(self, path, version=None):
650 sql = '''select version_id, user, tstamp, size, hide from versions where name = ?
651 order by version_id desc limit 1'''
652 c = self.con.execute(sql, (path,))
654 if not row or int(row[4]):
655 raise NameError('Object does not exist')
657 # The database (sqlite) will not complain if the version is not an integer.
658 sql = '''select version_id, user, tstamp, size from versions where name = ?
659 and version_id = ?'''
660 c = self.con.execute(sql, (path, version))
663 raise IndexError('Version does not exist')
664 return smart_str(row[0]), smart_str(row[1]), int(row[2]), int(row[3])
666 def _put_version(self, path, user, size=0, hide=0):
667 tstamp = int(time.time())
668 sql = 'insert into versions (name, user, tstamp, size, hide) values (?, ?, ?, ?, ?)'
669 id = self.con.execute(sql, (path, user, tstamp, size, hide)).lastrowid
670 return str(id), tstamp
672 def _copy_version(self, user, src_path, dest_path, copy_meta=True, copy_data=True, src_version=None):
673 if src_version is not None:
674 src_version_id, muser, mtime, size = self._get_version(src_path, src_version)
676 # Latest or create from scratch.
678 src_version_id, muser, mtime, size = self._get_version(src_path)
680 src_version_id = None
684 dest_version_id = self._put_version(dest_path, user, size)[0]
685 if copy_meta and src_version_id is not None:
686 sql = 'insert into metadata select %s, key, value from metadata where version_id = ?'
687 sql = sql % dest_version_id
688 self.con.execute(sql, (src_version_id,))
689 if copy_data and src_version_id is not None:
690 # TODO: Copy properly.
691 hashmap = self.mapper.map_retr(src_version_id)
692 self.mapper.map_stor(dest_version_id, hashmap)
693 return src_version_id, dest_version_id
695 def _get_versioninfo(self, account, container, name, until=None):
696 """Return path, latest version, associated timestamp and size until the timestamp given."""
698 p = (account, container, name)
700 p = p[:p.index(None)]
704 sql = '''select version_id, tstamp, size from (%s) where name = ?'''
705 sql = sql % self._sql_until(until)
706 c = self.con.execute(sql, (path,))
709 raise NameError('Path does not exist')
710 return path, str(row[0]), int(row[1]), int(row[2])
712 def _get_accountinfo(self, account, until=None):
714 path, version_id, mtime, size = self._get_versioninfo(account, None, None, until)
715 return version_id, mtime
717 raise NameError('Account does not exist')
719 def _get_containerinfo(self, account, container, until=None):
721 path, version_id, mtime, size = self._get_versioninfo(account, container, None, until)
722 return path, version_id, mtime
724 raise NameError('Container does not exist')
726 def _get_objectinfo(self, account, container, name, version=None):
727 path = '/'.join((account, container, name))
728 version_id, muser, mtime, size = self._get_version(path, version)
729 return path, version_id, muser, mtime, size
731 def _create_account(self, user, account):
733 self._get_accountinfo(account)
735 self._put_version(account, user)
737 def _get_metadata(self, path, version):
738 sql = 'select key, value from metadata where version_id = ?'
739 c = self.con.execute(sql, (version,))
740 return dict(c.fetchall())
742 def _put_metadata(self, user, path, meta, replace=False, copy_data=True):
743 """Create a new version and store metadata."""
745 src_version_id, dest_version_id = self._copy_version(user, path, path, not replace, copy_data)
746 for k, v in meta.iteritems():
747 if not replace and v == '':
748 sql = 'delete from metadata where version_id = ? and key = ?'
749 self.con.execute(sql, (dest_version_id, k))
751 sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
752 self.con.execute(sql, (dest_version_id, k, v))
754 def _check_policy(self, policy):
755 for k in policy.keys():
757 policy[k] = self.default_policy.get(k)
758 for k, v in policy.iteritems():
760 q = int(v) # May raise ValueError.
763 elif k == 'versioning':
764 if v not in ['auto', 'manual', 'none']:
769 def _get_policy(self, path):
770 sql = 'select key, value from policy where name = ?'
771 c = self.con.execute(sql, (path,))
772 return dict(c.fetchall())
774 def _list_limits(self, listing, marker, limit):
778 start = listing.index(marker) + 1
781 if not limit or limit > 10000:
785 def _list_objects(self, path, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, keys=[], until=None, allowed=[]):
786 cont_prefix = path + '/'
787 if keys and len(keys) > 0:
788 sql = '''select distinct o.name, o.version_id from (%s) o, metadata m where o.name like ? and
789 m.version_id = o.version_id and m.key in (%s)'''
790 sql = sql % (self._sql_until(until), ', '.join('?' * len(keys)))
791 param = (cont_prefix + prefix + '%',) + tuple(keys)
793 sql += ' and (' + ' or '.join(('o.name like ?',) * len(allowed)) + ')'
794 param += tuple([x + '%' for x in allowed])
795 sql += ' order by o.name'
797 sql = 'select name, version_id from (%s) where name like ?'
798 sql = sql % self._sql_until(until)
799 param = (cont_prefix + prefix + '%',)
801 sql += ' and (' + ' or '.join(('name like ?',) * len(allowed)) + ')'
802 param += tuple([x + '%' for x in allowed])
803 sql += ' order by name'
804 c = self.con.execute(sql, param)
805 objects = [(x[0][len(cont_prefix):], x[1]) for x in c.fetchall()]
810 i = pseudo_name.find(delimiter, len(prefix))
812 # If the delimiter is not found, or the name ends
813 # with the delimiter's first occurence.
814 if i == -1 or len(pseudo_name) == i + len(delimiter):
815 pseudo_objects.append(x)
817 # If the delimiter is found, keep up to (and including) the delimiter.
819 pseudo_name = pseudo_name[:i + len(delimiter)]
820 if pseudo_name not in [y[0] for y in pseudo_objects]:
821 if pseudo_name == x[0]:
822 pseudo_objects.append(x)
824 pseudo_objects.append((pseudo_name, None))
825 objects = pseudo_objects
827 start, limit = self._list_limits([x[0] for x in objects], marker, limit)
828 return objects[start:start + limit]
830 def _del_version(self, version):
831 self.mapper.map_remv(version)
832 sql = 'delete from versions where version_id = ?'
833 self.con.execute(sql, (version,))
835 # Access control functions.
837 def _check_groups(self, groups):
839 # for k, v in groups.iteritems():
840 # if True in [False or ',' in x for x in v]:
841 # raise ValueError('Bad characters in groups')
844 def _get_groups(self, account):
845 sql = 'select gname, user from groups where account = ?'
846 c = self.con.execute(sql, (account,))
848 for gname, user in c.fetchall():
849 if gname not in groups:
851 groups[gname].append(user)
854 def _put_groups(self, account, groups, replace=False):
856 self._del_groups(account)
857 for k, v in groups.iteritems():
858 sql = 'delete from groups where account = ? and gname = ?'
859 self.con.execute(sql, (account, k))
861 sql = 'insert into groups (account, gname, user) values (?, ?, ?)'
862 self.con.executemany(sql, [(account, k, x) for x in v])
864 def _del_groups(self, account):
865 sql = 'delete from groups where account = ?'
866 self.con.execute(sql, (account,))
868 def _check_permissions(self, path, permissions):
869 # Check for existing permissions.
870 sql = '''select name from permissions
871 where name != ? and (name like ? or ? like name || ?)'''
872 c = self.con.execute(sql, (path, path + '%', path, '%'))
875 ae = AttributeError()
879 # Format given permissions.
880 if len(permissions) == 0:
882 r = permissions.get('read', [])
883 w = permissions.get('write', [])
885 # if True in [False or ',' in x for x in r]:
886 # raise ValueError('Bad characters in read permissions')
887 # if True in [False or ',' in x for x in w]:
888 # raise ValueError('Bad characters in write permissions')
891 def _get_permissions(self, path):
892 # Check for permissions at path or above.
893 sql = 'select name, op, user from permissions where ? like name || ?'
894 c = self.con.execute(sql, (path, '%'))
896 perms = {} # Return nothing, if nothing is set.
897 for row in c.fetchall():
903 perms[op].append(user)
906 def _put_permissions(self, path, r, w):
907 sql = 'delete from permissions where name = ?'
908 self.con.execute(sql, (path,))
909 sql = 'insert into permissions (name, op, user) values (?, ?, ?)'
911 self.con.executemany(sql, [(path, 'read', x) for x in r])
913 self.con.executemany(sql, [(path, 'write', x) for x in w])
915 def _get_public(self, path):
916 sql = 'select name from public where name = ?'
917 c = self.con.execute(sql, (path,))
923 def _put_public(self, path, public):
925 sql = 'delete from public where name = ?'
927 sql = 'insert or replace into public (name) values (?)'
928 self.con.execute(sql, (path,))
930 def _del_sharing(self, path):
931 sql = 'delete from permissions where name = ?'
932 self.con.execute(sql, (path,))
933 sql = 'delete from public where name = ?'
934 self.con.execute(sql, (path,))
936 def _is_allowed(self, user, account, container, name, op='read'):
937 if smart_unicode(user) == smart_unicode(account):
939 path = '/'.join((account, container, name))
940 if op == 'read' and self._get_public(path):
942 perm_path, perms = self._get_permissions(path)
945 for x in ('read', 'write'):
947 for y in perms.get(x, []):
949 g_account, g_name = y.split(':', 1)
950 groups = self._get_groups(g_account)
951 if g_name in groups.keys():
952 g_perms.update(groups[g_name])
957 user = smart_unicode(user, strings_only=True)
958 if op == 'read' and ('*' in perms['read'] or user in perms['read']):
960 if '*' in perms['write'] or user in perms['write']:
964 def _can_read(self, user, account, container, name):
965 if not self._is_allowed(user, account, container, name, 'read'):
966 raise NotAllowedError
968 def _can_write(self, user, account, container, name):
969 if not self._is_allowed(user, account, container, name, 'write'):
970 raise NotAllowedError
972 def _allowed_paths(self, user, prefix=None):
973 sql = '''select distinct name from permissions where (user = ?
974 or user in (select account || ':' || gname from groups where user = ?))'''
977 sql += ' and name like ?'
978 param += (prefix + '/%',)
979 c = self.con.execute(sql, param)
980 return [x[0] for x in c.fetchall()]
982 def _allowed_accounts(self, user):
984 for path in self._allowed_paths(user):
985 allow.add(path.split('/', 1)[0])
988 def _allowed_containers(self, user, account):
990 for path in self._allowed_paths(user, account):
991 allow.add(path.split('/', 2)[1])
994 def _shared_paths(self, prefix):
995 sql = 'select distinct name from permissions where name like ?'
996 c = self.con.execute(sql, (prefix + '/%',))
997 return [x[0] for x in c.fetchall()]