Statistics
| Branch: | Tag: | Revision:

root / pithos / backends / simple.py @ e0f916bb

History | View | Annotate | Download (35.3 kB)

1
# Copyright 2011 GRNET S.A. All rights reserved.
2
# 
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
# 
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
# 
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
# 
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
# 
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import os
35
import time
36
import sqlite3
37
import logging
38
import types
39
import hashlib
40
import shutil
41
import pickle
42

    
43
from base import NotAllowedError, BaseBackend
44

    
45

    
46
logger = logging.getLogger(__name__)
47

    
48

    
49
class SimpleBackend(BaseBackend):
50
    """A simple backend.
51
    
52
    Uses SQLite for storage.
53
    """
54
    
55
    # TODO: Automatic/manual clean-up after a time interval.
56
    
57
    def __init__(self, db):
58
        self.hash_algorithm = 'sha1'
59
        self.block_size = 128 * 1024 # 128KB
60
        
61
        self.default_policy = {'quota': 0, 'versioning': 'auto'}
62
        
63
        basepath = os.path.split(db)[0]
64
        if basepath and not os.path.exists(basepath):
65
            os.makedirs(basepath)
66
        
67
        self.con = sqlite3.connect(db, check_same_thread=False)
68
        sql = '''create table if not exists versions (
69
                    version_id integer primary key,
70
                    name text,
71
                    user text,
72
                    tstamp datetime default current_timestamp,
73
                    size integer default 0,
74
                    hide integer default 0)'''
75
        self.con.execute(sql)
76
        sql = '''create table if not exists metadata (
77
                    version_id integer, key text, value text, primary key (version_id, key))'''
78
        self.con.execute(sql)
79
        sql = '''create table if not exists blocks (
80
                    block_id text, data blob, primary key (block_id))'''
81
        self.con.execute(sql)
82
        sql = '''create table if not exists hashmaps (
83
                    version_id integer, pos integer, block_id text, primary key (version_id, pos))'''
84
        self.con.execute(sql)
85
        sql = '''create table if not exists groups (
86
                    account text, name text, users text, primary key (account, name))'''
87
        self.con.execute(sql)
88
        sql = '''create table if not exists policy (
89
                    name text, key text, value text, primary key (name, key))'''
90
        self.con.execute(sql)
91
        sql = '''create table if not exists permissions (
92
                    name text, read text, write text, primary key (name))'''
93
        self.con.execute(sql)
94
        sql = '''create table if not exists public (
95
                    name text, primary key (name))'''
96
        self.con.execute(sql)
97
        self.con.commit()
98
    
99
    def get_account_meta(self, user, account, until=None):
100
        """Return a dictionary with the account metadata."""
101
        
102
        logger.debug("get_account_meta: %s %s", account, until)
103
        if user != account:
104
            raise NotAllowedError
105
        try:
106
            version_id, mtime = self._get_accountinfo(account, until)
107
        except NameError:
108
            version_id = None
109
            mtime = 0
110
        count, bytes, tstamp = self._get_pathstats(account, until)
111
        if mtime > tstamp:
112
            tstamp = mtime
113
        if until is None:
114
            modified = tstamp
115
        else:
116
            modified = self._get_pathstats(account)[2] # Overall last modification
117
            if mtime > modified:
118
                modified = mtime
119
        
120
        # Proper count.
121
        sql = 'select count(name) from (%s) where name glob ? and not name glob ?'
122
        sql = sql % self._sql_until(until)
123
        c = self.con.execute(sql, (account + '/*', account + '/*/*'))
124
        row = c.fetchone()
125
        count = row[0]
126
        
127
        meta = self._get_metadata(account, version_id)
128
        meta.update({'name': account, 'count': count, 'bytes': bytes})
129
        if modified:
130
            meta.update({'modified': modified})
131
        if until is not None:
132
            meta.update({'until_timestamp': tstamp})
133
        return meta
134
    
135
    def update_account_meta(self, user, account, meta, replace=False):
136
        """Update the metadata associated with the account."""
137
        
138
        logger.debug("update_account_meta: %s %s %s", account, meta, replace)
139
        if user != account:
140
            raise NotAllowedError
141
        self._put_metadata(user, account, meta, replace)
142
    
143
    def get_account_groups(self, user, account):
144
        """Return a dictionary with the user groups defined for this account."""
145
        
146
        logger.debug("get_account_groups: %s", account)
147
        if user != account:
148
            raise NotAllowedError
149
        return self._get_groups(account)
150
    
151
    def update_account_groups(self, user, account, groups, replace=False):
152
        """Update the groups associated with the account."""
153
        
154
        logger.debug("update_account_groups: %s %s %s", account, groups, replace)
155
        if user != account:
156
            raise NotAllowedError
157
        for k, v in groups.iteritems():
158
            if True in [False or ',' in x for x in v]:
159
                raise ValueError('Bad characters in groups')
160
        if replace:
161
            sql = 'delete from groups where account = ?'
162
            self.con.execute(sql, (account,))
163
        for k, v in groups.iteritems():
164
            if len(v) == 0:
165
                if not replace:
166
                    sql = 'delete from groups where account = ? and name = ?'
167
                    self.con.execute(sql, (account, k))
168
            else:
169
                sql = 'insert or replace into groups (account, name, users) values (?, ?, ?)'
170
                self.con.execute(sql, (account, k, ','.join(v)))
171
        self.con.commit()
172
    
173
    def delete_account(self, user, account):
174
        """Delete the account with the given name."""
175
        
176
        logger.debug("delete_account: %s", account)
177
        if user != account:
178
            raise NotAllowedError
179
        count, bytes, tstamp = self._get_pathstats(account)
180
        if count > 0:
181
            raise IndexError('Account is not empty')
182
        self._del_path(account) # Point of no return.
183
    
184
    def list_containers(self, user, account, marker=None, limit=10000, until=None):
185
        """Return a list of containers existing under an account."""
186
        
187
        logger.debug("list_containers: %s %s %s %s", account, marker, limit, until)
188
        if user != account:
189
            raise NotAllowedError
190
        return self._list_objects(account, '', '/', marker, limit, False, [], until)
191
    
192
    def get_container_meta(self, user, account, container, until=None):
193
        """Return a dictionary with the container metadata."""
194
        
195
        logger.debug("get_container_meta: %s %s %s", account, container, until)
196
        if user != account:
197
            raise NotAllowedError
198
        path, version_id, mtime = self._get_containerinfo(account, container, until)
199
        count, bytes, tstamp = self._get_pathstats(path, until)
200
        if mtime > tstamp:
201
            tstamp = mtime
202
        if until is None:
203
            modified = tstamp
204
        else:
205
            modified = self._get_pathstats(path)[2] # Overall last modification
206
            if mtime > modified:
207
                modified = mtime
208
        
209
        meta = self._get_metadata(path, version_id)
210
        meta.update({'name': container, 'count': count, 'bytes': bytes, 'modified': modified})
211
        if until is not None:
212
            meta.update({'until_timestamp': tstamp})
213
        return meta
214
    
215
    def update_container_meta(self, user, account, container, meta, replace=False):
216
        """Update the metadata associated with the container."""
217
        
218
        logger.debug("update_container_meta: %s %s %s %s", account, container, meta, replace)
219
        if user != account:
220
            raise NotAllowedError
221
        path, version_id, mtime = self._get_containerinfo(account, container)
222
        self._put_metadata(user, path, meta, replace)
223
    
224
    def get_container_policy(self, user, account, container):
225
        """Return a dictionary with the container policy."""
226
        
227
        logger.debug("get_container_policy: %s %s", account, container)
228
        if user != account:
229
            raise NotAllowedError
230
        path = self._get_containerinfo(account, container)[0]
231
        return self._get_policy(path)
232
    
233
    def update_container_policy(self, user, account, container, policy, replace=False):
234
        """Update the policy associated with the account."""
235
        
236
        logger.debug("update_container_policy: %s %s %s %s", account, container, policy, replace)
237
        if user != account:
238
            raise NotAllowedError
239
        path = self._get_containerinfo(account, container)[0]
240
        self._check_policy(policy)
241
        if replace:
242
            for k, v in self.default_policy.iteritems():
243
                if k not in policy:
244
                    policy[k] = v
245
        for k, v in policy.iteritems():
246
            sql = 'insert or replace into policy (name, key, value) values (?, ?, ?)'
247
            self.con.execute(sql, (path, k, v))
248
        self.con.commit()
249
    
250
    def put_container(self, user, account, container, policy=None):
251
        """Create a new container with the given name."""
252
        
253
        logger.debug("put_container: %s %s %s", account, container, policy)
254
        if user != account:
255
            raise NotAllowedError
256
        try:
257
            path, version_id, mtime = self._get_containerinfo(account, container)
258
        except NameError:
259
            pass
260
        else:
261
            raise NameError('Container already exists')
262
        if policy:
263
            self._check_policy(policy)
264
        path = os.path.join(account, container)
265
        version_id = self._put_version(path, user)
266
        for k, v in self.default_policy.iteritems():
267
            if k not in policy:
268
                policy[k] = v
269
        for k, v in policy.iteritems():
270
            sql = 'insert or replace into policy (name, key, value) values (?, ?, ?)'
271
            self.con.execute(sql, (path, k, v))
272
        self.con.commit()
273
    
274
    def delete_container(self, user, account, container):
275
        """Delete the container with the given name."""
276
        
277
        logger.debug("delete_container: %s %s", account, container)
278
        if user != account:
279
            raise NotAllowedError
280
        path, version_id, mtime = self._get_containerinfo(account, container)
281
        count, bytes, tstamp = self._get_pathstats(path)
282
        if count > 0:
283
            raise IndexError('Container is not empty')
284
        self._del_path(path) # Point of no return.
285
        self._copy_version(user, account, account, True, True) # New account version.
286
    
287
    def list_objects(self, user, account, container, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, keys=[], until=None):
288
        """Return a list of objects existing under a container."""
289
        
290
        logger.debug("list_objects: %s %s %s %s %s %s %s", account, container, prefix, delimiter, marker, limit, until)
291
        if user != account:
292
            raise NotAllowedError
293
        path, version_id, mtime = self._get_containerinfo(account, container, until)
294
        return self._list_objects(path, prefix, delimiter, marker, limit, virtual, keys, until)
295
    
296
    def list_object_meta(self, user, account, container, until=None):
297
        """Return a list with all the container's object meta keys."""
298
        
299
        logger.debug("list_object_meta: %s %s %s", account, container, until)
300
        if user != account:
301
            raise NotAllowedError
302
        path, version_id, mtime = self._get_containerinfo(account, container, until)
303
        sql = '''select distinct m.key from (%s) o, metadata m
304
                    where m.version_id = o.version_id and o.name like ?'''
305
        sql = sql % self._sql_until(until)
306
        c = self.con.execute(sql, (path + '/%',))
307
        return [x[0] for x in c.fetchall()]
308
    
309
    def get_object_meta(self, user, account, container, name, version=None):
310
        """Return a dictionary with the object metadata."""
311
        
312
        logger.debug("get_object_meta: %s %s %s %s", account, container, name, version)
313
        self._can_read(user, account, container, name)
314
        path, version_id, muser, mtime, size = self._get_objectinfo(account, container, name, version)
315
        if version is None:
316
            modified = mtime
317
        else:
318
            modified = self._get_version(path, version)[2] # Overall last modification
319
        
320
        meta = self._get_metadata(path, version_id)
321
        meta.update({'name': name, 'bytes': size})
322
        meta.update({'version': version_id, 'version_timestamp': mtime})
323
        meta.update({'modified': modified, 'modified_by': muser})
324
        return meta
325
    
326
    def update_object_meta(self, user, account, container, name, meta, replace=False):
327
        """Update the metadata associated with the object."""
328
        
329
        logger.debug("update_object_meta: %s %s %s %s %s", account, container, name, meta, replace)
330
        self._can_write(user, account, container, name)
331
        path, version_id, muser, mtime, size = self._get_objectinfo(account, container, name)
332
        self._put_metadata(user, path, meta, replace)
333
    
334
    def get_object_permissions(self, user, account, container, name):
335
        """Return the path from which this object gets its permissions from,\
336
        along with a dictionary containing the permissions."""
337
        
338
        logger.debug("get_object_permissions: %s %s %s", account, container, name)
339
        self._can_read(user, account, container, name)
340
        path = self._get_objectinfo(account, container, name)[0]
341
        return self._get_permissions(path)
342
    
343
    def update_object_permissions(self, user, account, container, name, permissions):
344
        """Update the permissions associated with the object."""
345
        
346
        logger.debug("update_object_permissions: %s %s %s %s", account, container, name, permissions)
347
        if user != account:
348
            raise NotAllowedError
349
        path = self._get_objectinfo(account, container, name)[0]
350
        r, w = self._check_permissions(path, permissions)
351
        self._put_permissions(path, r, w)
352
    
353
    def get_object_public(self, user, account, container, name):
354
        """Return the public URL of the object if applicable."""
355
        
356
        logger.debug("get_object_public: %s %s %s", account, container, name)
357
        self._can_read(user, account, container, name)
358
        path = self._get_objectinfo(account, container, name)[0]
359
        if self._get_public(path):
360
            return '/public/' + path
361
        return None
362
    
363
    def update_object_public(self, user, account, container, name, public):
364
        """Update the public status of the object."""
365
        
366
        logger.debug("update_object_public: %s %s %s %s", account, container, name, public)
367
        self._can_write(user, account, container, name)
368
        path = self._get_objectinfo(account, container, name)[0]
369
        self._put_public(path, public)
370
    
371
    def get_object_hashmap(self, user, account, container, name, version=None):
372
        """Return the object's size and a list with partial hashes."""
373
        
374
        logger.debug("get_object_hashmap: %s %s %s %s", account, container, name, version)
375
        self._can_read(user, account, container, name)
376
        path, version_id, muser, mtime, size = self._get_objectinfo(account, container, name, version)
377
        sql = 'select block_id from hashmaps where version_id = ? order by pos asc'
378
        c = self.con.execute(sql, (version_id,))
379
        hashmap = [x[0] for x in c.fetchall()]
380
        return size, hashmap
381
    
382
    def update_object_hashmap(self, user, account, container, name, size, hashmap, meta={}, replace_meta=False, permissions=None):
383
        """Create/update an object with the specified size and partial hashes."""
384
        
385
        logger.debug("update_object_hashmap: %s %s %s %s %s", account, container, name, size, hashmap)
386
        if permissions is not None and user != account:
387
            raise NotAllowedError
388
        self._can_write(user, account, container, name)
389
        missing = []
390
        for i in range(len(hashmap)):
391
            sql = 'select count(*) from blocks where block_id = ?'
392
            c = self.con.execute(sql, (hashmap[i],))
393
            if c.fetchone()[0] == 0:
394
                missing.append(hashmap[i])
395
        if missing:
396
            ie = IndexError()
397
            ie.data = missing
398
            raise ie
399
        path = self._get_containerinfo(account, container)[0]
400
        path = os.path.join(path, name)
401
        if permissions is not None:
402
            r, w = self._check_permissions(path, permissions)
403
        src_version_id, dest_version_id = self._copy_version(user, path, path, not replace_meta, False)
404
        sql = 'update versions set size = ? where version_id = ?'
405
        self.con.execute(sql, (size, dest_version_id))
406
        # TODO: Check for block_id existence.
407
        for i in range(len(hashmap)):
408
            sql = 'insert or replace into hashmaps (version_id, pos, block_id) values (?, ?, ?)'
409
            self.con.execute(sql, (dest_version_id, i, hashmap[i]))
410
        for k, v in meta.iteritems():
411
            sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
412
            self.con.execute(sql, (dest_version_id, k, v))
413
        if permissions is not None:
414
            sql = 'insert or replace into permissions (name, read, write) values (?, ?, ?)'
415
            self.con.execute(sql, (path, r, w))
416
        self.con.commit()
417
    
418
    def copy_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None, src_version=None):
419
        """Copy an object's data and metadata."""
420
        
421
        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)
422
        if permissions is not None and user != account:
423
            raise NotAllowedError
424
        self._can_read(user, account, src_container, src_name)
425
        self._can_write(user, account, dest_container, dest_name)
426
        self._get_containerinfo(account, src_container)
427
        if src_version is None:
428
            src_path = self._get_objectinfo(account, src_container, src_name)[0]
429
        else:
430
            src_path = os.path.join(account, src_container, src_name)
431
        dest_path = self._get_containerinfo(account, dest_container)[0]
432
        dest_path = os.path.join(dest_path, dest_name)
433
        if permissions is not None:
434
            r, w = self._check_permissions(dest_path, permissions)
435
        src_version_id, dest_version_id = self._copy_version(user, src_path, dest_path, not replace_meta, True, src_version)
436
        for k, v in dest_meta.iteritems():
437
            sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
438
            self.con.execute(sql, (dest_version_id, k, v))
439
        if permissions is not None:
440
            sql = 'insert or replace into permissions (name, read, write) values (?, ?, ?)'
441
            self.con.execute(sql, (dest_path, r, w))
442
        self.con.commit()
443
    
444
    def move_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None):
445
        """Move an object's data and metadata."""
446
        
447
        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)
448
        self.copy_object(user, account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, permissions, None)
449
        self.delete_object(user, account, src_container, src_name)
450
    
451
    def delete_object(self, user, account, container, name):
452
        """Delete an object."""
453
        
454
        logger.debug("delete_object: %s %s %s", account, container, name)
455
        if user != account:
456
            raise NotAllowedError
457
        path = self._get_objectinfo(account, container, name)[0]
458
        self._put_version(path, user, 0, 1)
459
        sql = 'delete from permissions where name = ?'
460
        self.con.execute(sql, (path,))
461
        self.con.commit()
462
    
463
    def list_versions(self, user, account, container, name):
464
        """Return a list of all (version, version_timestamp) tuples for an object."""
465
        
466
        logger.debug("list_versions: %s %s %s", account, container, name)
467
        self._can_read(user, account, container, name)
468
        # This will even show deleted versions.
469
        path = os.path.join(account, container, name)
470
        sql = '''select distinct version_id, strftime('%s', tstamp) from versions where name = ? and hide = 0'''
471
        c = self.con.execute(sql, (path,))
472
        return [(int(x[0]), int(x[1])) for x in c.fetchall()]
473
    
474
    def get_block(self, hash):
475
        """Return a block's data."""
476
        
477
        logger.debug("get_block: %s", hash)
478
        c = self.con.execute('select data from blocks where block_id = ?', (hash,))
479
        row = c.fetchone()
480
        if row:
481
            return str(row[0])
482
        else:
483
            raise NameError('Block does not exist')
484
    
485
    def put_block(self, data):
486
        """Create a block and return the hash."""
487
        
488
        logger.debug("put_block: %s", len(data))
489
        h = hashlib.new(self.hash_algorithm)
490
        h.update(data.rstrip('\x00'))
491
        hash = h.hexdigest()
492
        sql = 'insert or ignore into blocks (block_id, data) values (?, ?)'
493
        self.con.execute(sql, (hash, buffer(data)))
494
        self.con.commit()
495
        return hash
496
    
497
    def update_block(self, hash, data, offset=0):
498
        """Update a known block and return the hash."""
499
        
500
        logger.debug("update_block: %s %s %s", hash, len(data), offset)
501
        if offset == 0 and len(data) == self.block_size:
502
            return self.put_block(data)
503
        src_data = self.get_block(hash)
504
        bs = self.block_size
505
        if offset < 0 or offset > bs or offset + len(data) > bs:
506
            raise IndexError('Offset or data outside block limits')
507
        dest_data = src_data[:offset] + data + src_data[offset + len(data):]
508
        return self.put_block(dest_data)
509
    
510
    def _sql_until(self, until=None):
511
        """Return the sql to get the latest versions until the timestamp given."""
512
        if until is None:
513
            until = int(time.time())
514
        sql = '''select version_id, name, strftime('%s', tstamp) as tstamp, size from versions v
515
                    where version_id = (select max(version_id) from versions
516
                                        where v.name = name and tstamp <= datetime(%s, 'unixepoch'))
517
                    and hide = 0'''
518
        return sql % ('%s', until)
519
    
520
    def _get_pathstats(self, path, until=None):
521
        """Return count and sum of size of everything under path and latest timestamp."""
522
        
523
        sql = 'select count(version_id), total(size), max(tstamp) from (%s) where name like ?'
524
        sql = sql % self._sql_until(until)
525
        c = self.con.execute(sql, (path + '/%',))
526
        row = c.fetchone()
527
        tstamp = row[2] if row[2] is not None else 0
528
        return int(row[0]), int(row[1]), int(tstamp)
529
    
530
    def _get_version(self, path, version=None):
531
        if version is None:
532
            sql = '''select version_id, user, strftime('%s', tstamp), size, hide from versions where name = ?
533
                        order by version_id desc limit 1'''
534
            c = self.con.execute(sql, (path,))
535
            row = c.fetchone()
536
            if not row or int(row[4]):
537
                raise NameError('Object does not exist')
538
        else:
539
            # The database (sqlite) will not complain if the version is not an integer.
540
            sql = '''select version_id, user, strftime('%s', tstamp), size from versions where name = ?
541
                        and version_id = ?'''
542
            c = self.con.execute(sql, (path, version))
543
            row = c.fetchone()
544
            if not row:
545
                raise IndexError('Version does not exist')
546
        return str(row[0]), str(row[1]), int(row[2]), int(row[3])
547
    
548
    def _put_version(self, path, user, size=0, hide=0):
549
        sql = 'insert into versions (name, user, size, hide) values (?, ?, ?, ?)'
550
        id = self.con.execute(sql, (path, user, size, hide)).lastrowid
551
        self.con.commit()
552
        return str(id)
553
    
554
    def _copy_version(self, user, src_path, dest_path, copy_meta=True, copy_data=True, src_version=None):
555
        if src_version is not None:
556
            src_version_id, muser, mtime, size = self._get_version(src_path, src_version)
557
        else:
558
            # Latest or create from scratch.
559
            try:
560
                src_version_id, muser, mtime, size = self._get_version(src_path)
561
            except NameError:
562
                src_version_id = None
563
                size = 0
564
        if not copy_data:
565
            size = 0
566
        dest_version_id = self._put_version(dest_path, user, size)
567
        if copy_meta and src_version_id is not None:
568
            sql = 'insert into metadata select %s, key, value from metadata where version_id = ?'
569
            sql = sql % dest_version_id
570
            self.con.execute(sql, (src_version_id,))
571
        if copy_data and src_version_id is not None:
572
            sql = 'insert into hashmaps select %s, pos, block_id from hashmaps where version_id = ?'
573
            sql = sql % dest_version_id
574
            self.con.execute(sql, (src_version_id,))
575
        self.con.commit()
576
        return src_version_id, dest_version_id
577
    
578
    def _get_versioninfo(self, account, container, name, until=None):
579
        """Return path, latest version, associated timestamp and size until the timestamp given."""
580
        
581
        p = (account, container, name)
582
        try:
583
            p = p[:p.index(None)]
584
        except ValueError:
585
            pass
586
        path = os.path.join(*p)
587
        sql = '''select version_id, tstamp, size from (%s) where name = ?'''
588
        sql = sql % self._sql_until(until)
589
        c = self.con.execute(sql, (path,))
590
        row = c.fetchone()
591
        if row is None:
592
            raise NameError('Path does not exist')
593
        return path, str(row[0]), int(row[1]), int(row[2])
594
    
595
    def _get_accountinfo(self, account, until=None):
596
        try:
597
            path, version_id, mtime, size = self._get_versioninfo(account, None, None, until)
598
            return version_id, mtime
599
        except:
600
            raise NameError('Account does not exist')
601
    
602
    def _get_containerinfo(self, account, container, until=None):
603
        try:
604
            path, version_id, mtime, size = self._get_versioninfo(account, container, None, until)
605
            return path, version_id, mtime
606
        except:
607
            raise NameError('Container does not exist')
608
    
609
    def _get_objectinfo(self, account, container, name, version=None):
610
        path = os.path.join(account, container, name)
611
        version_id, muser, mtime, size = self._get_version(path, version)
612
        return path, version_id, muser, mtime, size
613
    
614
    def _get_metadata(self, path, version):
615
        sql = 'select key, value from metadata where version_id = ?'
616
        c = self.con.execute(sql, (version,))
617
        return dict(c.fetchall())
618
    
619
    def _put_metadata(self, user, path, meta, replace=False):
620
        """Create a new version and store metadata."""
621
        
622
        src_version_id, dest_version_id = self._copy_version(user, path, path, not replace, True)
623
        for k, v in meta.iteritems():
624
            if not replace and v == '':
625
                sql = 'delete from metadata where version_id = ? and key = ?'
626
                self.con.execute(sql, (dest_version_id, k))
627
            else:
628
                sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
629
                self.con.execute(sql, (dest_version_id, k, v))
630
        self.con.commit()
631
    
632
    def _get_groups(self, account):
633
        sql = 'select name, users from groups where account = ?'
634
        c = self.con.execute(sql, (account,))
635
        return dict([(x[0], x[1].split(',')) for x in c.fetchall()])
636
    
637
    def _check_policy(self, policy):
638
        for k in policy.keys():
639
            if policy[k] == '':
640
                policy[k] = self.default_policy.get(k)
641
        for k, v in policy.iteritems():
642
            if k == 'quota':
643
                q = int(v) # May raise ValueError.
644
                if q < 0:
645
                    raise ValueError
646
            elif k == 'versioning':
647
                if v not in ['auto', 'manual', 'none']:
648
                    raise ValueError
649
            else:
650
                raise ValueError
651
    
652
    def _get_policy(self, path):
653
        sql = 'select key, value from policy where name = ?'
654
        c = self.con.execute(sql, (path,))
655
        return dict(c.fetchall())
656
    
657
    def _is_allowed(self, user, account, container, name, op='read'):
658
        if user == account:
659
            return True
660
        path = os.path.join(account, container, name)
661
        if op == 'read' and self._get_public(path):
662
            return True
663
        perm_path, perms = self._get_permissions(path)
664
        
665
        # Expand groups.
666
        for x in ('read', 'write'):
667
            g_perms = []
668
            for y in perms.get(x, []):
669
                if ':' in y:
670
                    g_account, g_name = y.split(':', 1)
671
                    groups = self._get_groups(g_account)
672
                    if g_name in groups:
673
                        g_perms += groups[g_name]
674
                else:
675
                    g_perms.append(y)
676
            perms[x] = g_perms
677
        
678
        if op == 'read' and user in perms.get('read', []):
679
            return True
680
        if user in perms.get('write', []):
681
            return True
682
        return False
683
    
684
    def _can_read(self, user, account, container, name):
685
        if not self._is_allowed(user, account, container, name, 'read'):
686
            raise NotAllowedError
687
    
688
    def _can_write(self, user, account, container, name):
689
        if not self._is_allowed(user, account, container, name, 'write'):
690
            raise NotAllowedError
691
    
692
    def _check_permissions(self, path, permissions):
693
        # Check for existing permissions.
694
        sql = '''select name from permissions
695
                    where name != ? and (name like ? or ? like name || ?)'''
696
        c = self.con.execute(sql, (path, path + '%', path, '%'))
697
        rows = c.fetchall()
698
        if rows:
699
            raise AttributeError('Permissions already set')
700
        
701
        # Format given permissions.
702
        if len(permissions) == 0:
703
            return '', ''
704
        r = permissions.get('read', [])
705
        w = permissions.get('write', [])
706
        if True in [False or ',' in x for x in r]:
707
            raise ValueError('Bad characters in read permissions')
708
        if True in [False or ',' in x for x in w]:
709
            raise ValueError('Bad characters in write permissions')
710
        return ','.join(r), ','.join(w)
711
    
712
    def _get_permissions(self, path):
713
        # Check for permissions at path or above.
714
        sql = 'select name, read, write from permissions where ? like name || ?'
715
        c = self.con.execute(sql, (path, '%'))
716
        row = c.fetchone()
717
        if not row:
718
            return path, {}
719
        
720
        name, r, w = row
721
        ret = {}
722
        if w != '':
723
            ret['write'] = w.split(',')
724
        if r != '':
725
            ret['read'] = r.split(',')
726
        return name, ret
727
    
728
    def _put_permissions(self, path, r, w):
729
        if r == '' and w == '':
730
            sql = 'delete from permissions where name = ?'
731
            self.con.execute(sql, (path,))
732
        else:
733
            sql = 'insert or replace into permissions (name, read, write) values (?, ?, ?)'
734
            self.con.execute(sql, (path, r, w))
735
        self.con.commit()
736
    
737
    def _get_public(self, path):
738
        sql = 'select name from public where name = ?'
739
        c = self.con.execute(sql, (path,))
740
        row = c.fetchone()
741
        if not row:
742
            return False
743
        return True
744
    
745
    def _put_public(self, path, public):
746
        if not public:
747
            sql = 'delete from public where name = ?'
748
        else:
749
            sql = 'insert or replace into public (name) values (?)'
750
        self.con.execute(sql, (path,))
751
        self.con.commit()
752
    
753
    def _list_objects(self, path, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, keys=[], until=None):
754
        cont_prefix = path + '/'
755
        if keys and len(keys) > 0:
756
            sql = '''select distinct o.name, o.version_id from (%s) o, metadata m where o.name like ? and
757
                        m.version_id = o.version_id and m.key in (%s) order by o.name'''
758
            sql = sql % (self._sql_until(until), ', '.join('?' * len(keys)))
759
            param = (cont_prefix + prefix + '%',) + tuple(keys)
760
        else:
761
            sql = 'select name, version_id from (%s) where name like ? order by name'
762
            sql = sql % self._sql_until(until)
763
            param = (cont_prefix + prefix + '%',)
764
        c = self.con.execute(sql, param)
765
        objects = [(x[0][len(cont_prefix):], x[1]) for x in c.fetchall()]
766
        if delimiter:
767
            pseudo_objects = []
768
            for x in objects:
769
                pseudo_name = x[0]
770
                i = pseudo_name.find(delimiter, len(prefix))
771
                if not virtual:
772
                    # If the delimiter is not found, or the name ends
773
                    # with the delimiter's first occurence.
774
                    if i == -1 or len(pseudo_name) == i + len(delimiter):
775
                        pseudo_objects.append(x)
776
                else:
777
                    # If the delimiter is found, keep up to (and including) the delimiter.
778
                    if i != -1:
779
                        pseudo_name = pseudo_name[:i + len(delimiter)]
780
                    if pseudo_name not in [y[0] for y in pseudo_objects]:
781
                        if pseudo_name == x[0]:
782
                            pseudo_objects.append(x)
783
                        else:
784
                            pseudo_objects.append((pseudo_name, None))
785
            objects = pseudo_objects
786
        
787
        start = 0
788
        if marker:
789
            try:
790
                start = [x[0] for x in objects].index(marker) + 1
791
            except ValueError:
792
                pass
793
        if not limit or limit > 10000:
794
            limit = 10000
795
        return objects[start:start + limit]
796
    
797
    def _del_path(self, path):
798
        sql = '''delete from hashmaps where version_id in
799
                    (select version_id from versions where name = ?)'''
800
        self.con.execute(sql, (path,))
801
        sql = '''delete from metadata where version_id in
802
                    (select version_id from versions where name = ?)'''
803
        self.con.execute(sql, (path,))
804
        sql = '''delete from versions where name = ?'''
805
        self.con.execute(sql, (path,))
806
        sql = '''delete from permissions where name like ?'''
807
        self.con.execute(sql, (path + '%',)) # Redundant.
808
        self.con.commit()