Statistics
| Branch: | Tag: | Revision:

root / pithos / backends / simple.py @ c428326e

History | View | Annotate | Download (36.7 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 hashlib
39
import binascii
40

    
41
from base import NotAllowedError, BaseBackend
42
from pithos.lib.hashfiler import Mapper, Blocker
43

    
44

    
45
logger = logging.getLogger(__name__)
46

    
47

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