Statistics
| Branch: | Tag: | Revision:

root / pithos / backends / simple.py @ 104626e3

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