Revision 104626e3

b/docs/source/devguide.rst
303 303
last_modified               The last object modification date (regardless of version)
304 304
x_object_version            The object's version identifier
305 305
x_object_version_timestamp  The object's version timestamp
306
x_object_modified_by        The user that committed the object's version
306 307
x_object_manifest           Object parts prefix in ``<container>/<object>`` form (optional)
307 308
x_object_public             Object is publicly accessible (optional) (**TBD**)
308 309
x_object_meta_*             Optional user defined metadata
......
429 430
Content-Disposition         The presentation style of the object (optional)
430 431
X-Object-Version            The object's version identifier
431 432
X-Object-Version-Timestamp  The object's version timestamp
433
X-Object-Modified-By        The user that comitted the object's version
432 434
X-Object-Manifest           Object parts prefix in ``<container>/<object>`` form (optional)
433 435
X-Object-Public             Object is publicly accessible (optional) (**TBD**)
434 436
X-Object-Meta-*             Optional user defined metadata
......
485 487
    <hash>...</hash>
486 488
  </object>
487 489

  
488
Version lists include the version identifier and timestamp for each available object version. Version identifiers are integers, with the only requirement that newer versions have a larger identifier than previous ones.
490
Version lists include the version identifier and timestamp for each available object version. Version identifiers can be arbitrary strings, so use the timestamp to find newer versions.
489 491

  
490 492
Example ``format=json`` reply:
491 493

  
......
518 520
Content-Disposition         The presentation style of the object (optional)
519 521
X-Object-Version            The object's version identifier
520 522
X-Object-Version-Timestamp  The object's version timestamp
523
X-Object-Modified-By        The user that comitted the object's version
521 524
X-Object-Manifest           Object parts prefix in ``<container>/<object>`` form (optional)
522 525
X-Object-Public             Object is publicly accessible (optional) (**TBD**)
523 526
X-Object-Meta-*             Optional user defined metadata
......
718 721
* Object versions - parameter ``version`` in HEAD/GET (list versions with GET), ``X-Object-Version-*`` meta in replies, ``X-Source-Version`` in PUT/COPY.
719 722
* Publicly accessible objects via ``https://hostname/public``. Control with ``X-Object-Public`` (**TBD**).
720 723
* Large object support with ``X-Object-Manifest``.
724
* Trace the user that created/modified an object with ``X-Object-Modified-By``.
721 725

  
722 726
Clarifications/suggestions:
723 727

  
b/pithos/api/functions.py
411 411
    #                       unauthorized (401),
412 412
    #                       badRequest (400)
413 413
    
414
    version = get_int_parameter(request, 'version')
414
    version = request.GET.get('version')
415 415
    try:
416 416
        meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
417 417
        if version is None:
......
443 443
    #                       badRequest (400),
444 444
    #                       notModified (304)
445 445
    
446
    version = get_int_parameter(request, 'version')
446
    version = request.GET.get('version')
447 447
    
448 448
    # Reply with the version list. Do this first, as the object may be deleted.
449
    if version is None and request.GET.get('version') == 'list':
449
    if version == 'list':
450 450
        if request.serialization == 'text':
451 451
            raise BadRequest('No format specified for version list.')
452 452
        
b/pithos/api/tests.py
83 83
                'Date',
84 84
                'X-Object-Manifest',
85 85
                'Content-Range',
86
                'X-Object-Modified-By',
86 87
                'X-Object-Version',
87 88
                'X-Object-Version-Timestamp',)}
88 89
        self.contentTypes = {'xml':'application/xml',
b/pithos/api/util.py
140 140
    response['Content-Type'] = meta.get('Content-Type', 'application/octet-stream')
141 141
    response['Last-Modified'] = http_date(int(meta['modified']))
142 142
    if not public:
143
        response['X-Object-Modified-By'] = meta['modified_by']
143 144
        response['X-Object-Version'] = meta['version']
144
        response['X-Object-Version-Timestamp'] = meta['version_timestamp']
145
        response['X-Object-Version-Timestamp'] = http_date(int(meta['version_timestamp']))
145 146
        for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
146 147
            response[k.encode('utf-8')] = meta[k].encode('utf-8')
147 148
        for k in ('Content-Encoding', 'Content-Disposition', 'X-Object-Manifest', 'X-Object-Sharing', 'X-Object-Shared-By'):
b/pithos/backends/base.py
182 182
            'name': The object name
183 183
            'bytes': The total data size
184 184
            'modified': Last modification timestamp (overall)
185
            'modified_by': The user that committed the object (version requested)
185 186
            'version': The version identifier
186 187
            'version_timestamp': The version's modification timestamp
187 188
        
b/pithos/backends/simple.py
66 66
        sql = '''create table if not exists versions (
67 67
                    version_id integer primary key,
68 68
                    name text,
69
                    user text,
69 70
                    tstamp datetime default current_timestamp,
70 71
                    size integer default 0,
71 72
                    hide integer default 0)'''
......
137 138
        logger.debug("update_account_meta: %s %s %s", account, meta, replace)
138 139
        if user != account:
139 140
            raise NotAllowedError
140
        self._put_metadata(account, meta, replace)
141
        self._put_metadata(user, account, meta, replace)
141 142
    
142 143
    def list_containers(self, user, account, marker=None, limit=10000, until=None):
143 144
        """Return a list of containers existing under an account."""
......
157 158
            path, version_id, mtime = self._get_containerinfo(account, container)
158 159
        except NameError:
159 160
            path = os.path.join(account, container)
160
            version_id = self._put_version(path)
161
            version_id = self._put_version(path, user)
161 162
        else:
162 163
            raise NameError('Container already exists')
163 164
    
......
172 173
        if count > 0:
173 174
            raise IndexError('Container is not empty')
174 175
        self._del_path(path) # Point of no return.
175
        self._copy_version(account, account, True, True) # New account version.
176
        self._copy_version(user, account, account, True, True) # New account version.
176 177
    
177 178
    def get_container_meta(self, user, account, container, until=None):
178 179
        """Return a dictionary with the container metadata."""
......
204 205
        if user != account:
205 206
            raise NotAllowedError
206 207
        path, version_id, mtime = self._get_containerinfo(account, container)
207
        self._put_metadata(path, meta, replace)
208
        self._put_metadata(user, path, meta, replace)
208 209
    
209 210
    def list_objects(self, user, account, container, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, keys=[], until=None):
210 211
        """Return a list of objects existing under a container."""
......
233 234
        
234 235
        logger.debug("get_object_meta: %s %s %s %s", account, container, name, version)
235 236
        self._can_read(user, account, container, name)
236
        path, version_id, mtime, size = self._get_objectinfo(account, container, name, version)
237
        path, version_id, muser, mtime, size = self._get_objectinfo(account, container, name, version)
237 238
        if version is None:
238 239
            modified = mtime
239 240
        else:
240
            modified = self._get_version(path, version)[1] # Overall last modification
241
            modified = self._get_version(path, version)[2] # Overall last modification
241 242
        
242 243
        meta = self._get_metadata(path, version_id)
243
        meta.update({'name': name, 'bytes': size, 'version': version_id, 'version_timestamp': mtime, 'modified': modified})
244
        meta.update({'name': name, 'bytes': size})
245
        meta.update({'version': version_id, 'version_timestamp': mtime})
246
        meta.update({'modified': modified, 'modified_by': muser})
244 247
        return meta
245 248
    
246 249
    def update_object_meta(self, user, account, container, name, meta, replace=False):
......
248 251
        
249 252
        logger.debug("update_object_meta: %s %s %s %s %s", account, container, name, meta, replace)
250 253
        self._can_write(user, account, container, name)
251
        path, version_id, mtime, size = self._get_objectinfo(account, container, name)
252
        self._put_metadata(path, meta, replace)
254
        path, version_id, muser, mtime, size = self._get_objectinfo(account, container, name)
255
        self._put_metadata(user, path, meta, replace)
253 256
    
254 257
    def get_object_permissions(self, user, account, container, name):
255 258
        """Return the path from which this object gets its permissions from,\
......
275 278
        
276 279
        logger.debug("get_object_hashmap: %s %s %s %s", account, container, name, version)
277 280
        self._can_read(user, account, container, name)
278
        path, version_id, mtime, size = self._get_objectinfo(account, container, name, version)
281
        path, version_id, muser, mtime, size = self._get_objectinfo(account, container, name, version)
279 282
        sql = 'select block_id from hashmaps where version_id = ? order by pos asc'
280 283
        c = self.con.execute(sql, (version_id,))
281 284
        hashmap = [x[0] for x in c.fetchall()]
......
292 295
        path = os.path.join(path, name)
293 296
        if permissions is not None:
294 297
            r, w = self._check_permissions(path, permissions)
295
        src_version_id, dest_version_id = self._copy_version(path, path, not replace_meta, False)
298
        src_version_id, dest_version_id = self._copy_version(user, path, path, not replace_meta, False)
296 299
        sql = 'update versions set size = ? where version_id = ?'
297 300
        self.con.execute(sql, (size, dest_version_id))
298 301
        # TODO: Check for block_id existence.
......
324 327
        dest_path = os.path.join(dest_path, dest_name)
325 328
        if permissions is not None:
326 329
            r, w = self._check_permissions(dest_path, permissions)
327
        src_version_id, dest_version_id = self._copy_version(src_path, dest_path, not replace_meta, True, src_version)
330
        src_version_id, dest_version_id = self._copy_version(user, src_path, dest_path, not replace_meta, True, src_version)
328 331
        for k, v in dest_meta.iteritems():
329 332
            sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
330 333
            self.con.execute(sql, (dest_version_id, k, v))
......
346 349
        logger.debug("delete_object: %s %s %s", account, container, name)
347 350
        if user != account:
348 351
            raise NotAllowedError
349
        path, version_id, mtime, size = self._get_objectinfo(account, container, name)
350
        self._put_version(path, 0, 1)
352
        path = self._get_objectinfo(account, container, name)[0]
353
        self._put_version(path, user, 0, 1)
351 354
        sql = 'delete from permissions where name = ?'
352 355
        self.con.execute(sql, (path,))
353 356
        self.con.commit()
......
421 424
    
422 425
    def _get_version(self, path, version=None):
423 426
        if version is None:
424
            sql = '''select version_id, strftime('%s', tstamp), size, hide from versions where name = ?
427
            sql = '''select version_id, user, strftime('%s', tstamp), size, hide from versions where name = ?
425 428
                        order by version_id desc limit 1'''
426 429
            c = self.con.execute(sql, (path,))
427 430
            row = c.fetchone()
428
            if not row or int(row[3]):
431
            if not row or int(row[4]):
429 432
                raise NameError('Object does not exist')
430 433
        else:
431
            sql = '''select version_id, strftime('%s', tstamp), size from versions where name = ?
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 = ?
432 436
                        and version_id = ?'''
433 437
            c = self.con.execute(sql, (path, version))
434 438
            row = c.fetchone()
435 439
            if not row:
436 440
                raise IndexError('Version does not exist')
437
        return str(row[0]), int(row[1]), int(row[2])
441
        return str(row[0]), str(row[1]), int(row[2]), int(row[3])
438 442
    
439
    def _put_version(self, path, size=0, hide=0):
440
        sql = 'insert into versions (name, size, hide) values (?, ?, ?)'
441
        id = self.con.execute(sql, (path, size, hide)).lastrowid
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
442 446
        self.con.commit()
443 447
        return str(id)
444 448
    
445
    def _copy_version(self, src_path, dest_path, copy_meta=True, copy_data=True, src_version=None):
449
    def _copy_version(self, user, src_path, dest_path, copy_meta=True, copy_data=True, src_version=None):
446 450
        if src_version is not None:
447
            src_version_id, mtime, size = self._get_version(src_path, src_version)
451
            src_version_id, muser, mtime, size = self._get_version(src_path, src_version)
448 452
        else:
449 453
            # Latest or create from scratch.
450 454
            try:
451
                src_version_id, mtime, size = self._get_version(src_path)
455
                src_version_id, muser, mtime, size = self._get_version(src_path)
452 456
            except NameError:
453 457
                src_version_id = None
454 458
                size = 0
455 459
        if not copy_data:
456 460
            size = 0
457
        dest_version_id = self._put_version(dest_path, size)
461
        dest_version_id = self._put_version(dest_path, user, size)
458 462
        if copy_meta and src_version_id is not None:
459 463
            sql = 'insert into metadata select %s, key, value from metadata where version_id = ?'
460 464
            sql = sql % dest_version_id
......
499 503
    
500 504
    def _get_objectinfo(self, account, container, name, version=None):
501 505
        path = os.path.join(account, container, name)
502
        version_id, mtime, size = self._get_version(path, version)
503
        return path, version_id, mtime, size
506
        version_id, muser, mtime, size = self._get_version(path, version)
507
        return path, version_id, muser, mtime, size
504 508
    
505 509
    def _get_metadata(self, path, version):
506 510
        sql = 'select key, value from metadata where version_id = ?'
507 511
        c = self.con.execute(sql, (version,))
508 512
        return dict(c.fetchall())
509 513
    
510
    def _put_metadata(self, path, meta, replace=False):
514
    def _put_metadata(self, user, path, meta, replace=False):
511 515
        """Create a new version and store metadata."""
512 516
        
513
        src_version_id, dest_version_id = self._copy_version(path, path, not replace, True)
517
        src_version_id, dest_version_id = self._copy_version(user, path, path, not replace, True)
514 518
        for k, v in meta.iteritems():
515 519
            if not replace and v == '':
516 520
                sql = 'delete from metadata where version_id = ? and key = ?'

Also available in: Unified diff