Statistics
| Branch: | Tag: | Revision:

root / pithos / backends / simple.py @ 84846143

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