Statistics
| Branch: | Tag: | Revision:

root / pithos / backends / simple.py @ a6eb13e9

History | View | Annotate | Download (26.6 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, version)[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
        perm_path, perms = self._get_permissions(path)
239
        if path == perm_path:
240
            return perms
241
        return {}
242
    
243
    def update_object_permissions(self, user, account, container, name, permissions):
244
        """Update the permissions associated with the object."""
245
        
246
        logger.debug("update_object_permissions: %s %s %s %s", account, container, name, permissions)
247
        path = self._get_objectinfo(account, container, name)[0]
248
        r, w = self._check_permissions(path, permissions)
249
        self._put_permissions(path, r, w)
250
    
251
    def get_object_hashmap(self, user, account, container, name, version=None):
252
        """Return the object's size and a list with partial hashes."""
253
        
254
        logger.debug("get_object_hashmap: %s %s %s %s", account, container, name, version)
255
        path, version_id, mtime, size = self._get_objectinfo(account, container, name, version)
256
        sql = 'select block_id from hashmaps where version_id = ? order by pos asc'
257
        c = self.con.execute(sql, (version_id,))
258
        hashmap = [x[0] for x in c.fetchall()]
259
        return size, hashmap
260
    
261
    def update_object_hashmap(self, user, account, container, name, size, hashmap, meta={}, replace_meta=False, permissions={}):
262
        """Create/update an object with the specified size and partial hashes."""
263
        
264
        logger.debug("update_object_hashmap: %s %s %s %s %s", account, container, name, size, hashmap)
265
        path = self._get_containerinfo(account, container)[0]
266
        path = os.path.join(path, name)
267
        if permissions:
268
            r, w = self._check_permissions(path, permissions)
269
        src_version_id, dest_version_id = self._copy_version(path, path, not replace_meta, False)
270
        sql = 'update versions set size = ? where version_id = ?'
271
        self.con.execute(sql, (size, dest_version_id))
272
        # TODO: Check for block_id existence.
273
        for i in range(len(hashmap)):
274
            sql = 'insert or replace into hashmaps (version_id, pos, block_id) values (?, ?, ?)'
275
            self.con.execute(sql, (dest_version_id, i, hashmap[i]))
276
        for k, v in meta.iteritems():
277
            sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
278
            self.con.execute(sql, (dest_version_id, k, v))
279
        if permissions:
280
            sql = 'insert or replace into permissions (name, read, write) values (?, ?, ?)'
281
            self.con.execute(sql, (path, r, w))
282
        self.con.commit()
283
    
284
    def copy_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions={}, src_version=None):
285
        """Copy an object's data and metadata."""
286
        
287
        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)
288
        self._get_containerinfo(account, src_container)
289
        if src_version is None:
290
            src_path = self._get_objectinfo(account, src_container, src_name)[0]
291
        else:
292
            src_path = os.path.join(account, src_container, src_name)
293
        dest_path = self._get_containerinfo(account, dest_container)[0]
294
        dest_path = os.path.join(dest_path, dest_name)
295
        if permissions:
296
            r, w = self._check_permissions(dest_path, permissions)
297
        src_version_id, dest_version_id = self._copy_version(src_path, dest_path, not replace_meta, True, src_version)
298
        for k, v in dest_meta.iteritems():
299
            sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
300
            self.con.execute(sql, (dest_version_id, k, v))
301
        if permissions:
302
            sql = 'insert or replace into permissions (name, read, write) values (?, ?, ?)'
303
            self.con.execute(sql, (dest_path, r, w))
304
        self.con.commit()
305
    
306
    def move_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions={}):
307
        """Move an object's data and metadata."""
308
        
309
        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)
310
        self.copy_object(user, account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, permissions, None)
311
        self.delete_object(user, account, src_container, src_name)
312
    
313
    def delete_object(self, user, account, container, name):
314
        """Delete an object."""
315
        
316
        logger.debug("delete_object: %s %s %s", account, container, name)
317
        path, version_id, mtime, size = self._get_objectinfo(account, container, name)
318
        self._put_version(path, 0, 1)
319
        sql = 'delete from permissions where name = ?'
320
        self.con.execute(sql, (path,))
321
        self.con.commit()
322
    
323
    def list_versions(self, user, account, container, name):
324
        """Return a list of all (version, version_timestamp) tuples for an object."""
325
        
326
        logger.debug("list_versions: %s %s %s", account, container, name)
327
        # This will even show deleted versions.
328
        path = os.path.join(account, container, name)
329
        sql = '''select distinct version_id, strftime('%s', tstamp) from versions where name = ? and hide = 0'''
330
        c = self.con.execute(sql, (path,))
331
        return [(int(x[0]), int(x[1])) for x in c.fetchall()]
332
    
333
    def get_block(self, hash):
334
        """Return a block's data."""
335
        
336
        logger.debug("get_block: %s", hash)
337
        c = self.con.execute('select data from blocks where block_id = ?', (hash,))
338
        row = c.fetchone()
339
        if row:
340
            return str(row[0])
341
        else:
342
            raise NameError('Block does not exist')
343
    
344
    def put_block(self, data):
345
        """Create a block and return the hash."""
346
        
347
        logger.debug("put_block: %s", len(data))
348
        h = hashlib.new(self.hash_algorithm)
349
        h.update(data.rstrip('\x00'))
350
        hash = h.hexdigest()
351
        sql = 'insert or ignore into blocks (block_id, data) values (?, ?)'
352
        self.con.execute(sql, (hash, buffer(data)))
353
        self.con.commit()
354
        return hash
355
    
356
    def update_block(self, hash, data, offset=0):
357
        """Update a known block and return the hash."""
358
        
359
        logger.debug("update_block: %s %s %s", hash, len(data), offset)
360
        if offset == 0 and len(data) == self.block_size:
361
            return self.put_block(data)
362
        src_data = self.get_block(hash)
363
        bs = self.block_size
364
        if offset < 0 or offset > bs or offset + len(data) > bs:
365
            raise IndexError('Offset or data outside block limits')
366
        dest_data = src_data[:offset] + data + src_data[offset + len(data):]
367
        return self.put_block(dest_data)
368
    
369
    def _sql_until(self, until=None):
370
        """Return the sql to get the latest versions until the timestamp given."""
371
        if until is None:
372
            until = int(time.time())
373
        sql = '''select version_id, name, strftime('%s', tstamp) as tstamp, size from versions v
374
                    where version_id = (select max(version_id) from versions
375
                                        where v.name = name and tstamp <= datetime(%s, 'unixepoch'))
376
                    and hide = 0'''
377
        return sql % ('%s', until)
378
    
379
    def _get_pathstats(self, path, until=None):
380
        """Return count and sum of size of everything under path and latest timestamp."""
381
        
382
        sql = 'select count(version_id), total(size), max(tstamp) from (%s) where name like ?'
383
        sql = sql % self._sql_until(until)
384
        c = self.con.execute(sql, (path + '/%',))
385
        row = c.fetchone()
386
        tstamp = row[2] if row[2] is not None else 0
387
        return int(row[0]), int(row[1]), int(tstamp)
388
    
389
    def _get_version(self, path, version=None):
390
        if version is None:
391
            sql = '''select version_id, strftime('%s', tstamp), size, hide from versions where name = ?
392
                        order by version_id desc limit 1'''
393
            c = self.con.execute(sql, (path,))
394
            row = c.fetchone()
395
            if not row or int(row[3]):
396
                raise NameError('Object does not exist')
397
        else:
398
            sql = '''select version_id, strftime('%s', tstamp), size from versions where name = ?
399
                        and version_id = ?'''
400
            c = self.con.execute(sql, (path, version))
401
            row = c.fetchone()
402
            if not row:
403
                raise IndexError('Version does not exist')
404
        return str(row[0]), int(row[1]), int(row[2])
405
    
406
    def _put_version(self, path, size=0, hide=0):
407
        sql = 'insert into versions (name, size, hide) values (?, ?, ?)'
408
        id = self.con.execute(sql, (path, size, hide)).lastrowid
409
        self.con.commit()
410
        return str(id)
411
    
412
    def _copy_version(self, src_path, dest_path, copy_meta=True, copy_data=True, src_version=None):
413
        if src_version is not None:
414
            src_version_id, mtime, size = self._get_version(src_path, src_version)
415
        else:
416
            # Latest or create from scratch.
417
            try:
418
                src_version_id, mtime, size = self._get_version(src_path)
419
            except NameError:
420
                src_version_id = None
421
                size = 0
422
        if not copy_data:
423
            size = 0
424
        dest_version_id = self._put_version(dest_path, size)
425
        if copy_meta and src_version_id is not None:
426
            sql = 'insert into metadata select %s, key, value from metadata where version_id = ?'
427
            sql = sql % dest_version_id
428
            self.con.execute(sql, (src_version_id,))
429
        if copy_data and src_version_id is not None:
430
            sql = 'insert into hashmaps select %s, pos, block_id from hashmaps where version_id = ?'
431
            sql = sql % dest_version_id
432
            self.con.execute(sql, (src_version_id,))
433
        self.con.commit()
434
        return src_version_id, dest_version_id
435
    
436
    def _get_versioninfo(self, account, container, name, until=None):
437
        """Return path, latest version, associated timestamp and size until the timestamp given."""
438
        
439
        p = (account, container, name)
440
        try:
441
            p = p[:p.index(None)]
442
        except ValueError:
443
            pass
444
        path = os.path.join(*p)
445
        sql = '''select version_id, tstamp, size from (%s) where name = ?'''
446
        sql = sql % self._sql_until(until)
447
        c = self.con.execute(sql, (path,))
448
        row = c.fetchone()
449
        if row is None:
450
            raise NameError('Path does not exist')
451
        return path, str(row[0]), int(row[1]), int(row[2])
452
    
453
    def _get_accountinfo(self, account, until=None):
454
        try:
455
            path, version_id, mtime, size = self._get_versioninfo(account, None, None, until)
456
            return version_id, mtime
457
        except:
458
            raise NameError('Account does not exist')
459
    
460
    def _get_containerinfo(self, account, container, until=None):
461
        try:
462
            path, version_id, mtime, size = self._get_versioninfo(account, container, None, until)
463
            return path, version_id, mtime
464
        except:
465
            raise NameError('Container does not exist')
466
    
467
    def _get_objectinfo(self, account, container, name, version=None):
468
        path = os.path.join(account, container, name)
469
        version_id, mtime, size = self._get_version(path, version)
470
        return path, version_id, mtime, size
471
    
472
    def _get_metadata(self, path, version):
473
        sql = 'select key, value from metadata where version_id = ?'
474
        c = self.con.execute(sql, (version,))
475
        return dict(c.fetchall())
476
    
477
    def _put_metadata(self, path, meta, replace=False):
478
        """Create a new version and store metadata."""
479
        
480
        src_version_id, dest_version_id = self._copy_version(path, path, not replace, True)
481
        for k, v in meta.iteritems():
482
            if not replace and v == '':
483
                sql = 'delete from metadata where version_id = ? and key = ?'
484
                self.con.execute(sql, (dest_version_id, k))
485
            else:
486
                sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
487
                self.con.execute(sql, (dest_version_id, k, v))
488
        self.con.commit()
489
    
490
    def _can_read(self, user, path):
491
        return True
492
    
493
    def _can_write(self, user, path):
494
        return True
495
    
496
    def _check_permissions(self, path, permissions):
497
        # Check for existing permissions.
498
        sql = '''select name from permissions
499
                    where name != ? and (name like ? or ? like name || ?)'''
500
        c = self.con.execute(sql, (path, path + '%', path, '%'))
501
        rows = c.fetchall()
502
        if rows:
503
            raise AttributeError('Permissions already set')
504
        
505
        # Format given permissions set.
506
        r = permissions.get('read', [])
507
        w = permissions.get('write', [])
508
        if True in [False or ',' in x for x in r]:
509
            raise ValueError('Bad characters in read permissions')
510
        if True in [False or ',' in x for x in w]:
511
            raise ValueError('Bad characters in write permissions')
512
        r = ','.join(r)
513
        w = ','.join(w)
514
        if 'private' in permissions:
515
            r = ''
516
            w = ''
517
        return r, w
518
    
519
    def _get_permissions(self, path):
520
        # Check for permissions at path or above.
521
        sql = 'select name, read, write from permissions where ? like name || ?'
522
        c = self.con.execute(sql, (path, '%'))
523
        row = c.fetchone()
524
        if not row:
525
            return path, {}
526
        
527
        name, r, w = row
528
        if r == '' and w == '':
529
            return {'private': True}
530
        ret = {}
531
        if w != '':
532
            ret['write'] = w.split(',')
533
        if r != '':
534
            ret['read'] = r.split(',')        
535
        return name, ret
536
    
537
    def _put_permissions(self, path, r, w):
538
        sql = 'insert or replace into permissions (name, read, write) values (?, ?, ?)'
539
        self.con.execute(sql, (path, r, w))
540
        self.con.commit()
541
    
542
    def _list_objects(self, path, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, keys=[], until=None):
543
        cont_prefix = path + '/'
544
        if keys and len(keys) > 0:
545
            sql = '''select distinct o.name, o.version_id from (%s) o, metadata m where o.name like ? and
546
                        m.version_id = o.version_id and m.key in (%s) order by o.name'''
547
            sql = sql % (self._sql_until(until), ', '.join('?' * len(keys)))
548
            param = (cont_prefix + prefix + '%',) + tuple(keys)
549
        else:
550
            sql = 'select name, version_id from (%s) where name like ? order by name'
551
            sql = sql % self._sql_until(until)
552
            param = (cont_prefix + prefix + '%',)
553
        c = self.con.execute(sql, param)
554
        objects = [(x[0][len(cont_prefix):], x[1]) for x in c.fetchall()]
555
        if delimiter:
556
            pseudo_objects = []
557
            for x in objects:
558
                pseudo_name = x[0]
559
                i = pseudo_name.find(delimiter, len(prefix))
560
                if not virtual:
561
                    # If the delimiter is not found, or the name ends
562
                    # with the delimiter's first occurence.
563
                    if i == -1 or len(pseudo_name) == i + len(delimiter):
564
                        pseudo_objects.append(x)
565
                else:
566
                    # If the delimiter is found, keep up to (and including) the delimiter.
567
                    if i != -1:
568
                        pseudo_name = pseudo_name[:i + len(delimiter)]
569
                    if pseudo_name not in [y[0] for y in pseudo_objects]:
570
                        if pseudo_name == x[0]:
571
                            pseudo_objects.append(x)
572
                        else:
573
                            pseudo_objects.append((pseudo_name, None))
574
            objects = pseudo_objects
575
        
576
        start = 0
577
        if marker:
578
            try:
579
                start = [x[0] for x in objects].index(marker) + 1
580
            except ValueError:
581
                pass
582
        if not limit or limit > 10000:
583
            limit = 10000
584
        return objects[start:start + limit]
585
    
586
    def _del_path(self, path):
587
        sql = '''delete from hashmaps where version_id in
588
                    (select version_id from versions where name = ?)'''
589
        self.con.execute(sql, (path,))
590
        sql = '''delete from metadata where version_id in
591
                    (select version_id from versions where name = ?)'''
592
        self.con.execute(sql, (path,))
593
        sql = '''delete from versions where name = ?'''
594
        self.con.execute(sql, (path,))
595
        sql = '''delete from permissions where name like ?'''
596
        self.con.execute(sql, (path + '%',)) # Redundant.
597
        self.con.commit()