Revision 4d15c94e

b/snf-pithos-app/pithos/api/functions.py
788 788
    copy_from = request.META.get('HTTP_X_COPY_FROM')
789 789
    move_from = request.META.get('HTTP_X_MOVE_FROM')
790 790
    if copy_from or move_from:
791
        delimiter = request.GET.get('delimiter')
791 792
        content_length = get_content_length(request) # Required by the API.
792 793
        
793 794
        src_account = request.META.get('HTTP_X_SOURCE_ACCOUNT')
......
799 800
            except ValueError:
800 801
                raise BadRequest('Invalid X-Move-From header')
801 802
            version_id = copy_or_move_object(request, src_account, src_container, src_name,
802
                                                v_account, v_container, v_object, move=True)
803
                                                v_account, v_container, v_object, move=True, delimiter=delimiter)
803 804
        else:
804 805
            try:
805 806
                src_container, src_name = split_container_object_string(copy_from)
806 807
            except ValueError:
807 808
                raise BadRequest('Invalid X-Copy-From header')
808 809
            version_id = copy_or_move_object(request, src_account, src_container, src_name,
809
                                                v_account, v_container, v_object, move=False)
810
                                                v_account, v_container, v_object, move=False, delimiter=delimiter)
810 811
        response = HttpResponse(status=201)
811 812
        response['X-Object-Version'] = version_id
812 813
        return response
......
967 968
            raise ItemNotFound('Container or object does not exist')
968 969
        validate_matching_preconditions(request, meta)
969 970
    
971
    delimiter = request.GET.get('delimiter')
972
    
970 973
    version_id = copy_or_move_object(request, v_account, v_container, v_object,
971
                                        dest_account, dest_container, dest_name, move=False)
974
                                        dest_account, dest_container, dest_name, move=False, delimiter=delimiter)
972 975
    response = HttpResponse(status=201)
973 976
    response['X-Object-Version'] = version_id
974 977
    return response
......
1003 1006
            raise ItemNotFound('Container or object does not exist')
1004 1007
        validate_matching_preconditions(request, meta)
1005 1008
    
1009
    delimiter = request.GET.get('delimiter')
1010
    
1006 1011
    version_id = copy_or_move_object(request, v_account, v_container, v_object,
1007
                                        dest_account, dest_container, dest_name, move=True)
1012
                                        dest_account, dest_container, dest_name, move=True, delimiter=delimiter)
1008 1013
    response = HttpResponse(status=201)
1009 1014
    response['X-Object-Version'] = version_id
1010 1015
    return response
......
1221 1226
    #                       badRequest (400)
1222 1227
    
1223 1228
    until = get_int_parameter(request.GET.get('until'))
1229
    delimiter = request.GET.get('delimiter')
1230
    
1224 1231
    try:
1225 1232
        request.backend.delete_object(request.user_uniq, v_account, v_container,
1226
                                        v_object, until)
1233
                                        v_object, until, delimiter=delimiter)
1227 1234
    except NotAllowedError:
1228 1235
        raise Forbidden('Not allowed')
1229 1236
    except NameError:
b/snf-pithos-app/pithos/api/util.py
320 320
        raise ValueError
321 321
    return s[:pos], s[(pos + 1):]
322 322

  
323
def copy_or_move_object(request, src_account, src_container, src_name, dest_account, dest_container, dest_name, move=False):
323
def copy_or_move_object(request, src_account, src_container, src_name, dest_account, dest_container, dest_name, move=False, delimiter=None):
324 324
    """Copy or move an object."""
325 325
    
326 326
    if 'ignore_content_type' in request.GET and 'CONTENT_TYPE' in request.META:
......
331 331
        if move:
332 332
            version_id = request.backend.move_object(request.user_uniq, src_account, src_container, src_name,
333 333
                                                        dest_account, dest_container, dest_name,
334
                                                        content_type, 'pithos', meta, False, permissions)
334
                                                        content_type, 'pithos', meta, False, permissions, delimiter)
335 335
        else:
336 336
            version_id = request.backend.copy_object(request.user_uniq, src_account, src_container, src_name,
337 337
                                                        dest_account, dest_container, dest_name,
338
                                                        content_type, 'pithos', meta, False, permissions, src_version)
338
                                                        content_type, 'pithos', meta, False, permissions, src_version, delimiter)
339 339
    except NotAllowedError:
340 340
        raise Forbidden('Not allowed')
341 341
    except (NameError, IndexError):
b/snf-pithos-backend/pithos/backends/base.py
169 169
        """
170 170
        return
171 171
    
172
    def list_containers(self, user, account, marker=None, limit=10000, shared=False, until=None):
172
    def list_containers(self, user, account, marker=None, limit=10000, shared=False, until=None, public=False):
173 173
        """Return a list of container names existing under an account.
174 174
        
175 175
        Parameters:
......
179 179
            
180 180
            'shared': Only list containers with permissions set
181 181
            
182
            'public': Only list containers containing public objects
183
            
182 184
        
183 185
        Raises:
184 186
            NotAllowedError: Operation not permitted
......
284 286
        """
285 287
        return
286 288
    
287
    def list_objects(self, user, account, container, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, domain=None, keys=[], shared=False, until=None, size_range=None):
289
    def list_objects(self, user, account, container, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, domain=None, keys=[], shared=False, until=None, size_range=None, public=False):
288 290
        """Return a list of object (name, version_id) tuples existing under a container.
289 291
        
290 292
        Parameters:
......
312 314
             
313 315
            'size_range': Include objects with byte size in (from, to).
314 316
                          Use None to specify unlimited
317
            
318
            'public': Only list public objects
319
             
315 320
        
316 321
        Raises:
317 322
            NotAllowedError: Operation not permitted
......
489 494
        """Update an object's checksum."""
490 495
        return
491 496
    
492
    def copy_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta={}, replace_meta=False, permissions=None, src_version=None):
497
    def copy_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta={}, replace_meta=False, permissions=None, src_version=None, delimiter=None):
493 498
        """Copy an object's data and metadata and return the new version.
494 499
        
495 500
        Parameters:
......
502 507
            'permissions': New object permissions
503 508
            
504 509
            'src_version': Copy from the version provided
510
            
511
            'delimiter': Copy objects whose path starts with src_name + delimiter
505 512
        
506 513
        Raises:
507 514
            NotAllowedError: Operation not permitted
......
516 523
        """
517 524
        return ''
518 525
    
519
    def move_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta={}, replace_meta=False, permissions=None):
526
    def move_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta={}, replace_meta=False, permissions=None, delimiter=None):
520 527
        """Move an object's data and metadata and return the new version.
521 528
        
522 529
        Parameters:
......
527 534
            'replace_meta': Replace metadata instead of update
528 535
            
529 536
            'permissions': New object permissions
537
            
538
            'delimiter': Move objects whose path starts with src_name + delimiter
530 539
        
531 540
        Raises:
532 541
            NotAllowedError: Operation not permitted
......
539 548
        """
540 549
        return ''
541 550
    
542
    def delete_object(self, user, account, container, name, until=None):
551
    def delete_object(self, user, account, container, name, until=None, delimiter=None):
543 552
        """Delete/purge an object.
544 553
        
554
        Parameters:
555
            'delimiter': Delete objects whose path starting with name + delimiter
556
        
545 557
        Raises:
546 558
            NotAllowedError: Operation not permitted
547 559
            
b/snf-pithos-backend/pithos/backends/lib/sqlite/node.py
201 201
            return r[0]
202 202
        return None
203 203
    
204
    def node_lookup_bulk(self, paths):
205
    	"""Lookup the current nodes for the given paths.
206
           Return () if the path is not found.
207
        """
208
        
209
        placeholders = ','.join('?' for path in paths)
210
        q = "select path, node from nodes where path in (%s)" % placeholders
211
        self.execute(q, paths)
212
        return self.fetchall()
213
    
204 214
    def node_get_properties(self, node):
205 215
        """Return the node's (parent, path).
206 216
           Return None if the node is not found.
......
500 510
        if props is not None:
501 511
            return props
502 512
        return None
513

  
514
    def version_lookup_bulk(self, nodes, before=inf, cluster=0):
515
        """Lookup the current versions of the given nodes.
516
           Return a list with their properties:
517
           (serial, node, hash, size, type, source, mtime, muser, uuid, checksum, cluster).
518
        """
519
        
520
        placeholders = ','.join('?' for node in nodes)
521
        q = ("select serial, node, hash, size, type, source, mtime, muser, uuid, checksum, cluster "
522
             "from versions "
523
             "where serial in (select max(serial) "
524
                             "from versions "
525
                             "where node in (%s) and mtime < ? group by node) "
526
             "and cluster = ?" % placeholders)
527
        args = nodes
528
        args.extend((before, cluster))
529
        self.execute(q, args)
530
        return self.fetchall()
503 531
    
504 532
    def version_get_properties(self, serial, keys=(), propnames=_propnames):
505 533
        """Return a sequence of values for the properties of
......
656 684
        
657 685
        subqlist = []
658 686
        args = []
659
        print pathq
660 687
        for path, match in pathq:
661 688
            if match == MATCH_PREFIX:
662 689
                subqlist.append("n.path like ? escape '\\'")
b/snf-pithos-backend/pithos/backends/lib/sqlite/permissions.py
112 112
        self.xfeature_destroy(path)
113 113
        self.public_unset(path)
114 114
    
115
    def access_clear_bulk(self, paths):
116
        """Revoke access to path (both permissions and public)."""
117
        
118
        self.xfeature_destroy_bulk(paths)
119
        self.public_unset_bulk(paths)
120
    
115 121
    def access_check(self, path, access, member):
116 122
        """Return true if the member has this access to the path."""
117 123
        
b/snf-pithos-backend/pithos/backends/lib/sqlite/public.py
58 58
        q = "update public set active = 0 where path = ?"
59 59
        self.execute(q, (path,))
60 60
    
61
    def public_unset_bulk(self, paths):
62
        placeholders = ','.join('?' for path in paths)
63
        q = "update public set active = 0 where path in (%s)" % placeholders
64
        self.execute(q, paths)
65
    
61 66
    def public_get(self, path):
62 67
        q = "select public_id from public where path = ? and active = 1"
63 68
        self.execute(q, (path,))
b/snf-pithos-backend/pithos/backends/lib/sqlite/xfeatures.py
99 99
        q = "delete from xfeatures where path = ?"
100 100
        self.execute(q, (path,))
101 101
    
102
    def xfeature_destroy_bulk(self, paths):
103
        """Destroy features and all their key, value pairs."""
104
        
105
        placeholders = ','.join('?' for path in paths)
106
        q = "delete from xfeatures where path in (%s)" % placeholders
107
        self.execute(q, paths)
108
    
102 109
    def feature_dict(self, feature):
103 110
        """Return a dict mapping keys to list of values for feature."""
104 111
        
b/snf-pithos-backend/pithos/backends/modular.py
476 476
        allowed = self._get_formatted_paths(allowed)
477 477
        return self._list_object_properties(node, path, prefix, delimiter, marker, limit, virtual, domain, keys, until, size_range, allowed, all_props)
478 478
    
479
    def _list_objects_no_limit(self, user, account, container, prefix, delimiter, virtual, domain, keys, shared, until, size_range, all_props, public):
480
        objects = []
481
        while True:
482
            marker = objects[-1] if objects else None
483
            limit = 10000
484
            l = self._list_objects(user, account, container, prefix, delimiter, marker, limit, virtual, domain, keys, shared, until, size_range, all_props, public)
485
            objects.extend(l)
486
            if not l or len(l) < limit:
487
                break
488
        return objects
489
    
479 490
    def _list_object_permissions(self, user, account, container, prefix, shared, public):
480 491
        allowed = []
481 492
        path = '/'.join((account, container, prefix)).rstrip('/')
......
724 735
            if x[self.SERIAL] >= int(version) and x[self.HASH] == props[self.HASH] and x[self.SIZE] == props[self.SIZE]:
725 736
                self.node.version_put_property(x[self.SERIAL], 'checksum', checksum)
726 737
    
727
    def _copy_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, dest_domain=None, dest_meta={}, replace_meta=False, permissions=None, src_version=None, is_move=False):
738
    def _copy_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, dest_domain=None, dest_meta={}, replace_meta=False, permissions=None, src_version=None, is_move=False, delimiter=None):
739
        dest_version_ids = []
728 740
        self._can_read(user, src_account, src_container, src_name)
729 741
        path, node = self._lookup_object(src_account, src_container, src_name)
730 742
        # TODO: Will do another fetch of the properties in duplicate version...
......
732 744
        src_version_id = props[self.SERIAL]
733 745
        hash = props[self.HASH]
734 746
        size = props[self.SIZE]
735
        
736 747
        is_copy = not is_move and (src_account, src_container, src_name) != (dest_account, dest_container, dest_name) # New uuid.
737
        dest_version_id = self._update_object_hash(user, dest_account, dest_container, dest_name, size, type, hash, None, dest_domain, dest_meta, replace_meta, permissions, src_node=node, src_version_id=src_version_id, is_copy=is_copy)
738
        return dest_version_id
748
        dest_version_ids.append(self._update_object_hash(user, dest_account, dest_container, dest_name, size, type, hash, None, dest_domain, dest_meta, replace_meta, permissions, src_node=node, src_version_id=src_version_id, is_copy=is_copy))
749
        if is_move:
750
        	self._delete_object(user, src_account, src_container, src_name)
751
        
752
        if delimiter:
753
            prefix = src_name + delimiter if not src_name.endswith(delimiter) else src_name
754
            src_names = self._list_objects_no_limit(user, src_account, src_container, prefix, delimiter=None, virtual=True, domain=None, keys=[], shared=False, until=None, size_range=None, all_props=True, public=False)
755
            paths = [elem[0] for elem in src_names]
756
            nodes = [elem[2] for elem in src_names]
757
            # TODO: Will do another fetch of the properties in duplicate version...
758
            props = self._get_versions(nodes) # Check to see if source exists.
759
            
760
            for prop, path, node in zip(props, paths, nodes):
761
                src_version_id = prop[self.SERIAL]
762
                hash = prop[self.HASH]
763
                vtype = prop[self.TYPE]
764
                dest_prefix = dest_name + delimiter if not dest_name.endswith(delimiter) else dest_name
765
                vdest_name = path.replace(prefix, dest_prefix, 1)
766
                dest_version_ids.append(self._update_object_hash(user, dest_account, dest_container, vdest_name, size, vtype, hash, None, dest_domain, dest_meta, replace_meta, permissions, src_node=node, src_version_id=src_version_id, is_copy=is_copy))
767
                if is_move:
768
                	self._delete_object(user, src_account, src_container, path)
769
        return dest_version_ids[0] if len(dest_version_ids) == 1 else dest_version_ids
739 770
    
740 771
    @backend_method
741
    def copy_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta={}, replace_meta=False, permissions=None, src_version=None):
772
    def copy_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta={}, replace_meta=False, permissions=None, src_version=None, delimiter=None):
742 773
        """Copy an object's data and metadata."""
743 774
        
744
        logger.debug("copy_object: %s %s %s %s %s %s %s %s %s %s %s %s %s", user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta, replace_meta, permissions, src_version)
745
        dest_version_id = self._copy_object(user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta, replace_meta, permissions, src_version, False)
775
        logger.debug("copy_object: %s %s %s %s %s %s %s %s %s %s %s %s %s %s", user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta, replace_meta, permissions, src_version, delimiter)
776
        dest_version_id = self._copy_object(user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta, replace_meta, permissions, src_version, False, delimiter)
746 777
        return dest_version_id
747 778
    
748 779
    @backend_method
749
    def move_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta={}, replace_meta=False, permissions=None):
780
    def move_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta={}, replace_meta=False, permissions=None, delimiter=None):
750 781
        """Move an object's data and metadata."""
751 782
        
752
        logger.debug("move_object: %s %s %s %s %s %s %s %s %s %s %s %s", user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta, replace_meta, permissions)
783
        logger.debug("move_object: %s %s %s %s %s %s %s %s %s %s %s %s %s", user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta, replace_meta, permissions, delimiter)
753 784
        if user != src_account:
754 785
            raise NotAllowedError
755
        dest_version_id = self._copy_object(user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta, replace_meta, permissions, None, True)
756
        if (src_account, src_container, src_name) != (dest_account, dest_container, dest_name):
757
            self._delete_object(user, src_account, src_container, src_name)
786
        dest_version_id = self._copy_object(user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta, replace_meta, permissions, None, True, delimiter)
758 787
        return dest_version_id
759 788
    
760
    def _delete_object(self, user, account, container, name, until=None, prefix='', delimiter=None):
789
    def _delete_object(self, user, account, container, name, until=None, delimiter=None):
761 790
        if user != account:
762 791
            raise NotAllowedError
763 792
        
......
791 820
            self._report_size_change(user, account, -del_size, {'action': 'object delete'})
792 821
        self._report_object_change(user, account, path, details={'action': 'object delete'})
793 822
        self.permissions.access_clear(path)
823
        
824
        if delimiter:
825
            prefix = name + delimiter if not name.endswith(delimiter) else name
826
            src_names = self._list_objects_no_limit(user, account, container, prefix, delimiter=None, virtual=True, domain=None, keys=[], shared=False, until=None, size_range=None, all_props=True, public=False)
827
            paths = []
828
            for t in src_names:
829
            	path = '/'.join((account, container, t[0]))
830
            	node = t[2]
831
                src_version_id, dest_version_id = self._put_version_duplicate(user, node, size=0, type='', hash=None, checksum='', cluster=CLUSTER_DELETED)
832
                del_size = self._apply_versioning(account, container, src_version_id)
833
                if del_size:
834
                    self._report_size_change(user, account, -del_size, {'action': 'object delete'})
835
                self._report_object_change(user, account, path, details={'action': 'object delete'})
836
                paths.append(path)
837
            self.permissions.access_clear_bulk(paths)
794 838
    
795 839
    @backend_method
796
    def (self, user, account, container, name, until=None, prefix='', delimiter=None):
840
    def delete_object(self, user, account, container, name, until=None, prefix='', delimiter=None):
797 841
        """Delete/purge an object."""
798 842
        
799 843
        logger.debug("delete_object: %s %s %s %s %s %s %s", user, account, container, name, until, prefix, delimiter)
800
        self._delete_object(user, account, container, name, until)
844
        self._delete_object(user, account, container, name, until, delimiter)
801 845
    
802 846
    @backend_method
803 847
    def list_versions(self, user, account, container, name):
......
936 980
            if props is None or props[self.CLUSTER] == CLUSTER_DELETED:
937 981
                raise IndexError('Version does not exist')
938 982
        return props
983

  
984
    def _get_versions(self, nodes, version=None):
985
        if version is None:
986
            props = self.node.version_lookup_bulk(nodes, inf, CLUSTER_NORMAL)
987
            if not props:
988
                raise NameError('Object does not exist')
989
        else:
990
            try:
991
                version = int(version)
992
            except ValueError:
993
                raise IndexError('Version does not exist')
994
            props = self.node.version_get_properties(version)
995
            if props is None or props[self.CLUSTER] == CLUSTER_DELETED:
996
                raise IndexError('Version does not exist')
997
        return props
939 998
    
940 999
    def _put_version_duplicate(self, user, node, src_node=None, size=None, type=None, hash=None, checksum=None, cluster=CLUSTER_NORMAL, is_copy=False):
941 1000
        """Create a new version of the node."""

Also available in: Unified diff