Statistics
| Branch: | Tag: | Revision:

root / pithos / backends / simple.py @ 3436eeb0

History | View | Annotate | Download (26.3 kB)

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

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

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