Statistics
| Branch: | Tag: | Revision:

root / pithos / backends / simple.py @ a156c8b3

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