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, path, db):
76 self.hash_algorithm = 'sha256'
77 self.block_size = 4 * 1024 * 1024 # 4MB
79 self.default_policy = {'quota': 0, 'versioning': 'auto'}
81 if path and not os.path.exists(path):
83 if not os.path.isdir(path):
84 raise RuntimeError("Cannot open path '%s'" % (path,))
86 self.con = sqlite3.connect(db, check_same_thread=False)
88 sql = '''pragma foreign_keys = on'''
91 sql = '''create table if not exists versions (
92 version_id integer primary key,
95 tstamp integer not null,
96 size integer default 0,
97 hide integer default 0)'''
99 sql = '''create table if not exists metadata (
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)
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)
124 params = {'blocksize': self.block_size,
125 'blockpath': os.path.join(path + '/blocks'),
126 'hashtype': self.hash_algorithm}
127 self.blocker = Blocker(**params)
129 params = {'mappath': os.path.join(path + '/maps'),
130 'namelen': self.blocker.hashlen}
131 self.mapper = Mapper(**params)
134 def list_accounts(self, user, marker=None, limit=10000):
135 """Return a list of accounts the user can access."""
137 allowed = self._allowed_accounts(user)
138 start, limit = self._list_limits(allowed, marker, limit)
139 return allowed[start:start + limit]
142 def get_account_meta(self, user, account, until=None):
143 """Return a dictionary with the account metadata."""
145 logger.debug("get_account_meta: %s %s", account, until)
147 if until or account not in self._allowed_accounts(user):
148 raise NotAllowedError
150 self._create_account(user, account)
152 version_id, mtime = self._get_accountinfo(account, until)
154 # Account does not exist before until.
157 count, bytes, tstamp = self._get_pathstats(account, until)
163 modified = self._get_pathstats(account)[2] # Overall last modification
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 + '/*/*'))
175 meta = {'name': account}
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})
185 def update_account_meta(self, user, account, meta, replace=False):
186 """Update the metadata associated with the account."""
188 logger.debug("update_account_meta: %s %s %s", account, meta, replace)
190 raise NotAllowedError
191 self._put_metadata(user, account, meta, replace, False)
194 def get_account_groups(self, user, account):
195 """Return a dictionary with the user groups defined for this account."""
197 logger.debug("get_account_groups: %s", account)
199 if account not in self._allowed_accounts(user):
200 raise NotAllowedError
202 self._create_account(user, account)
203 return self._get_groups(account)
206 def update_account_groups(self, user, account, groups, replace=False):
207 """Update the groups associated with the account."""
209 logger.debug("update_account_groups: %s %s %s", account, groups, replace)
211 raise NotAllowedError
212 self._create_account(user, account)
213 self._check_groups(groups)
214 self._put_groups(account, groups, replace)
217 def put_account(self, user, account):
218 """Create a new account with the given name."""
220 logger.debug("put_account: %s", account)
222 raise NotAllowedError
224 version_id, mtime = self._get_accountinfo(account)
228 raise NameError('Account already exists')
229 self._put_version(account, user)
232 def delete_account(self, user, account):
233 """Delete the account with the given name."""
235 logger.debug("delete_account: %s", account)
237 raise NotAllowedError
238 count = self._get_pathstats(account)[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)
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."""
249 logger.debug("list_containers: %s %s %s %s", account, marker, limit, until)
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]
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)]
264 def get_container_meta(self, user, account, container, until=None):
265 """Return a dictionary with the container metadata."""
267 logger.debug("get_container_meta: %s %s %s", account, container, until)
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)
278 modified = self._get_pathstats(path)[2] # Overall last modification
283 meta = {'name': container, 'modified': modified}
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})
292 def update_container_meta(self, user, account, container, meta, replace=False):
293 """Update the metadata associated with the container."""
295 logger.debug("update_container_meta: %s %s %s %s", account, container, meta, replace)
297 raise NotAllowedError
298 path, version_id, mtime = self._get_containerinfo(account, container)
299 self._put_metadata(user, path, meta, replace, False)
302 def get_container_policy(self, user, account, container):
303 """Return a dictionary with the container policy."""
305 logger.debug("get_container_policy: %s %s", account, container)
307 if container not in self._allowed_containers(user, account):
308 raise NotAllowedError
310 path = self._get_containerinfo(account, container)[0]
311 return self._get_policy(path)
314 def update_container_policy(self, user, account, container, policy, replace=False):
315 """Update the policy associated with the account."""
317 logger.debug("update_container_policy: %s %s %s %s", account, container, policy, replace)
319 raise NotAllowedError
320 path = self._get_containerinfo(account, container)[0]
321 self._check_policy(policy)
323 for k, v in self.default_policy.iteritems():
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))
331 def put_container(self, user, account, container, policy=None):
332 """Create a new container with the given name."""
334 logger.debug("put_container: %s %s %s", account, container, policy)
336 raise NotAllowedError
338 path, version_id, mtime = self._get_containerinfo(account, container)
342 raise NameError('Container already exists')
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():
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))
355 def delete_container(self, user, account, container, until=None):
356 """Delete/purge the container with the given name."""
358 logger.debug("delete_container: %s %s %s", account, container, until)
360 raise NotAllowedError
361 path, version_id, mtime = self._get_containerinfo(account, container)
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()]:
372 count = self._get_pathstats(path)[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).
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."""
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)
389 raise NotAllowedError
390 allowed = self._allowed_paths(user, '/'.join((account, container)))
392 raise NotAllowedError
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)
400 def list_object_meta(self, user, account, container, until=None):
401 """Return a list with all the container's object meta keys."""
403 logger.debug("list_object_meta: %s %s %s", account, container, until)
407 raise NotAllowedError
408 allowed = self._allowed_paths(user, '/'.join((account, container)))
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 + '/%',)
418 sql += ' and o.name like ?'
420 c = self.con.execute(sql, param)
421 return [x[0] for x in c.fetchall()]
424 def get_object_meta(self, user, account, container, name, version=None):
425 """Return a dictionary with the object metadata."""
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)
433 modified = self._get_version(path, version)[2] # Overall last modification
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})
442 def update_object_meta(self, user, account, container, name, meta, replace=False):
443 """Update the metadata associated with the object."""
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)
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."""
456 logger.debug("get_object_permissions: %s %s %s", account, container, name)
459 if self._is_allowed(user, account, container, name, 'write'):
461 elif self._is_allowed(user, account, container, name, 'read'):
464 raise NotAllowedError
465 path = self._get_objectinfo(account, container, name)[0]
466 return (allowed,) + self._get_permissions(path)
469 def update_object_permissions(self, user, account, container, name, permissions):
470 """Update the permissions associated with the object."""
472 logger.debug("update_object_permissions: %s %s %s %s", account, container, name, permissions)
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)
480 def get_object_public(self, user, account, container, name):
481 """Return the public URL of the object if applicable."""
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
491 def update_object_public(self, user, account, container, name, public):
492 """Update the public status of the object."""
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)
500 def get_object_hashmap(self, user, account, container, name, version=None):
501 """Return the object's size and a list with partial hashes."""
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]
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."""
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])
520 ie.data = [binascii.hexlify(x) for x in missing]
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
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."""
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]
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
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."""
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
573 def delete_object(self, user, account, container, name, until=None):
574 """Delete/purge an object."""
576 logger.debug("delete_object: %s %s %s %s", account, container, name, until)
578 raise NotAllowedError
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()]:
587 version_id = self._get_version(path)[0]
591 self._del_sharing(path)
594 path = self._get_objectinfo(account, container, name)[0]
595 self._put_version(path, user, 0, 1)
596 self._del_sharing(path)
599 def list_versions(self, user, account, container, name):
600 """Return a list of all (version, version_timestamp) tuples for an object."""
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()]
610 @backend_method(autocommit=0)
611 def get_block(self, hash):
612 """Return a block's data."""
614 logger.debug("get_block: %s", hash)
615 blocks = self.blocker.block_retr((binascii.unhexlify(hash),))
617 raise NameError('Block does not exist')
620 @backend_method(autocommit=0)
621 def put_block(self, data):
622 """Store a block and return the hash."""
624 logger.debug("put_block: %s", len(data))
625 hashes, absent = self.blocker.block_stor((data,))
626 return binascii.hexlify(hashes[0])
628 @backend_method(autocommit=0)
629 def update_block(self, hash, data, offset=0):
630 """Update a known block and return the hash."""
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)
638 def _sql_until(self, until=None):
639 """Return the sql to get the latest versions until the timestamp given."""
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)
646 return sql % (until,)
648 def _get_pathstats(self, path, until=None):
649 """Return count and sum of size of everything under path and latest timestamp."""
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 + '/%',))
655 tstamp = row[2] if row[2] is not None else 0
656 return int(row[0]), int(row[1]), int(tstamp)
658 def _get_version(self, path, version=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,))
664 if not row or int(row[4]):
665 raise NameError('Object does not exist')
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))
673 raise IndexError('Version does not exist')
674 return smart_str(row[0]), smart_str(row[1]), int(row[2]), int(row[3])
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
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)
686 # Latest or create from scratch.
688 src_version_id, muser, mtime, size = self._get_version(src_path)
690 src_version_id = None
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
705 def _get_versioninfo(self, account, container, name, until=None):
706 """Return path, latest version, associated timestamp and size until the timestamp given."""
708 p = (account, container, name)
710 p = p[:p.index(None)]
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,))
719 raise NameError('Path does not exist')
720 return path, str(row[0]), int(row[1]), int(row[2])
722 def _get_accountinfo(self, account, until=None):
724 path, version_id, mtime, size = self._get_versioninfo(account, None, None, until)
725 return version_id, mtime
727 raise NameError('Account does not exist')
729 def _get_containerinfo(self, account, container, until=None):
731 path, version_id, mtime, size = self._get_versioninfo(account, container, None, until)
732 return path, version_id, mtime
734 raise NameError('Container does not exist')
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
741 def _create_account(self, user, account):
743 self._get_accountinfo(account)
745 self._put_version(account, user)
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())
752 def _put_metadata(self, user, path, meta, replace=False, copy_data=True):
753 """Create a new version and store metadata."""
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))
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
765 def _check_policy(self, policy):
766 for k in policy.keys():
768 policy[k] = self.default_policy.get(k)
769 for k, v in policy.iteritems():
771 q = int(v) # May raise ValueError.
774 elif k == 'versioning':
775 if v not in ['auto', 'manual', 'none']:
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())
785 def _list_limits(self, listing, marker, limit):
789 start = listing.index(marker) + 1
792 if not limit or limit > 10000:
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)
804 sql += ' and (' + ' or '.join(('o.name like ?',) * len(allowed)) + ')'
805 param += tuple([x + '%' for x in allowed])
806 sql += ' order by o.name'
808 sql = 'select name, version_id from (%s) where name like ?'
809 sql = sql % self._sql_until(until)
810 param = (cont_prefix + prefix + '%',)
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()]
821 i = pseudo_name.find(delimiter, len(prefix))
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)
828 # If the delimiter is found, keep up to (and including) the delimiter.
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)
835 pseudo_objects.append((pseudo_name, None))
836 objects = pseudo_objects
838 start, limit = self._list_limits([x[0] for x in objects], marker, limit)
839 return objects[start:start + limit]
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,))
846 # Access control functions.
848 def _check_groups(self, groups):
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')
855 def _get_groups(self, account):
856 sql = 'select gname, user from groups where account = ?'
857 c = self.con.execute(sql, (account,))
859 for gname, user in c.fetchall():
860 if gname not in groups:
862 groups[gname].append(user)
865 def _put_groups(self, account, groups, replace=False):
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))
872 sql = 'insert into groups (account, gname, user) values (?, ?, ?)'
873 self.con.executemany(sql, [(account, k, x) for x in v])
875 def _del_groups(self, account):
876 sql = 'delete from groups where account = ?'
877 self.con.execute(sql, (account,))
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, '%'))
886 ae = AttributeError()
890 # Format given permissions.
891 if len(permissions) == 0:
893 r = permissions.get('read', [])
894 w = permissions.get('write', [])
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')
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, '%'))
907 perms = {} # Return nothing, if nothing is set.
908 for row in c.fetchall():
914 perms[op].append(user)
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 (?, ?, ?)'
922 self.con.executemany(sql, [(path, 'read', x) for x in r])
924 self.con.executemany(sql, [(path, 'write', x) for x in w])
926 def _get_public(self, path):
927 sql = 'select name from public where name = ?'
928 c = self.con.execute(sql, (path,))
934 def _put_public(self, path, public):
936 sql = 'delete from public where name = ?'
938 sql = 'insert or replace into public (name) values (?)'
939 self.con.execute(sql, (path,))
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,))
947 def _is_allowed(self, user, account, container, name, op='read'):
948 if smart_unicode(user) == smart_unicode(account):
950 path = '/'.join((account, container, name))
951 if op == 'read' and self._get_public(path):
953 perm_path, perms = self._get_permissions(path)
956 for x in ('read', 'write'):
958 for y in perms.get(x, []):
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])
968 user = smart_unicode(user, strings_only=True)
969 if op == 'read' and ('*' in perms['read'] or user in perms['read']):
971 if '*' in perms['write'] or user in perms['write']:
975 def _can_read(self, user, account, container, name):
976 if not self._is_allowed(user, account, container, name, 'read'):
977 raise NotAllowedError
979 def _can_write(self, user, account, container, name):
980 if not self._is_allowed(user, account, container, name, 'write'):
981 raise NotAllowedError
983 def _allowed_paths(self, user, prefix=None):
984 sql = '''select distinct name from permissions
987 user in (select account || ':' || gname from groups where user = ?))'''
990 sql += ' and name like ?'
991 param += (prefix + '/%',)
992 c = self.con.execute(sql, param)
993 return [x[0] for x in c.fetchall()]
995 def _allowed_accounts(self, user):
997 for path in self._allowed_paths(user):
998 allow.add(path.split('/', 1)[0])
1001 def _allowed_containers(self, user, account):
1003 for path in self._allowed_paths(user, account):
1004 allow.add(path.split('/', 2)[1])
1005 return sorted(allow)
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()]