Support cross-account copy and move.
authorAntony Chazapis <chazapis@gmail.com>
Wed, 28 Sep 2011 12:19:33 +0000 (15:19 +0300)
committerAntony Chazapis <chazapis@gmail.com>
Wed, 28 Sep 2011 12:19:33 +0000 (15:19 +0300)
Fixes #1241

docs/source/devguide.rst
pithos/api/functions.py
pithos/api/util.py
pithos/backends/base.py
pithos/backends/modular.py
pithos/backends/simple.py

index adab85f..c424d64 100644 (file)
@@ -27,6 +27,7 @@ Revision                   Description
 =========================  ================================
 0.7 (Sept 28, 2011)        Suggest upload/download methods using hashmaps.
 \                          Propose syncing algorithm.
+\                          Support cross-account object copy and move.
 0.6 (Sept 13, 2011)        Reply with Merkle hash as the ETag when updating objects.
 \                          Include version id in object replace/change replies.
 \                          Change conflict (409) replies format to text.
@@ -723,6 +724,7 @@ Content-Type          The MIME content type of the object
 Transfer-Encoding     Set to ``chunked`` to specify incremental uploading (if used, ``Content-Length`` is ignored)
 X-Copy-From           The source path in the form ``/<container>/<object>``
 X-Move-From           The source path in the form ``/<container>/<object>``
+X-Source-Account      The source account to copy/move from
 X-Source-Version      The source version to copy from
 Content-Encoding      The encoding of the object (optional)
 Content-Disposition   The presentation style of the object (optional)
@@ -773,6 +775,7 @@ Request Header Name   Value
 If-Match              Proceed if ETags match with object
 If-None-Match         Proceed if ETags don't match with object
 Destination           The destination path in the form ``/<container>/<object>``
+Destination-Account   The destination account to copy to
 Content-Type          The MIME content type of the object (optional)
 Content-Encoding      The encoding of the object (optional)
 Content-Disposition   The presentation style of the object (optional)
@@ -972,6 +975,7 @@ List of differences from the OOS API:
 * Object versions - parameter ``version`` in ``HEAD``/``GET`` (list versions with ``GET``), ``X-Object-Version-*`` meta in replies, ``X-Source-Version`` in ``PUT``/``COPY``.
 * Sharing/publishing with ``X-Object-Sharing``, ``X-Object-Public`` at the object level. Cross-user operations are allowed - controlled by sharing directives. Available actions in cross-user requests are reported with ``X-Object-Allowed-To``. Permissions may include groups defined with ``X-Account-Group-*`` at the account level. These apply to the object - not its versions.
 * Support for prefix-based inheritance when enforcing permissions. Parent object carrying the authorization directives is reported in ``X-Object-Shared-By``.
+* Copy and move between accounts with ``X-Source-Account`` and ``Destination-Account`` headers.
 * Large object support with ``X-Object-Manifest``.
 * Trace the user that created/modified an object with ``X-Object-Modified-By``.
 * Purge container/object history with the ``until`` parameter in ``DELETE``.
index e7a0824..f1c569c 100644 (file)
@@ -728,18 +728,23 @@ def object_write(request, v_account, v_container, v_object):
     if copy_from or move_from:
         content_length = get_content_length(request) # Required by the API.
         
+        src_account = smart_unicode(request.META.get('HTTP_X_SOURCE_ACCOUNT'), strings_only=True)
+        if not src_account:
+            src_account = request.user
         if move_from:
             try:
                 src_container, src_name = split_container_object_string(move_from)
             except ValueError:
                 raise BadRequest('Invalid X-Move-From header')
-            version_id = copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=True)
+            version_id = copy_or_move_object(request, src_account, src_container, src_name,
+                                                v_account, v_container, v_object, move=True)
         else:
             try:
                 src_container, src_name = split_container_object_string(copy_from)
             except ValueError:
                 raise BadRequest('Invalid X-Copy-From header')
-            version_id = copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=False)
+            version_id = copy_or_move_object(request, src_account, src_container, src_name,
+                                                v_account, v_container, v_object, move=False)
         response = HttpResponse(status=201)
         response['X-Object-Version'] = version_id
         return response
@@ -875,7 +880,10 @@ def object_copy(request, v_account, v_container, v_object):
     #                       unauthorized (401),
     #                       badRequest (400)
     
-    dest_path = request.META.get('HTTP_DESTINATION')
+    dest_account = smart_unicode(request.META.get('HTTP_DESTINATION_ACCOUNT'), strings_only=True)
+    if not dest_account:
+        dest_account = request.user
+    dest_path = smart_unicode(request.META.get('HTTP_DESTINATION'), strings_only=True)
     if not dest_path:
         raise BadRequest('Missing Destination header')
     try:
@@ -895,7 +903,8 @@ def object_copy(request, v_account, v_container, v_object):
             raise ItemNotFound('Container or object does not exist')
         validate_matching_preconditions(request, meta)
     
-    version_id = copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=False)
+    version_id = copy_or_move_object(request, v_account, v_container, v_object,
+                                        dest_account, dest_container, dest_name, move=False)
     response = HttpResponse(status=201)
     response['X-Object-Version'] = version_id
     return response
@@ -908,7 +917,10 @@ def object_move(request, v_account, v_container, v_object):
     #                       unauthorized (401),
     #                       badRequest (400)
     
-    dest_path = request.META.get('HTTP_DESTINATION')
+    dest_account = smart_unicode(request.META.get('HTTP_DESTINATION_ACCOUNT'), strings_only=True)
+    if not dest_account:
+        dest_account = request.user
+    dest_path = smart_unicode(request.META.get('HTTP_DESTINATION'), strings_only=True)
     if not dest_path:
         raise BadRequest('Missing Destination header')
     try:
@@ -927,7 +939,8 @@ def object_move(request, v_account, v_container, v_object):
             raise ItemNotFound('Container or object does not exist')
         validate_matching_preconditions(request, meta)
     
-    version_id = copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=True)
+    version_id = copy_or_move_object(request, v_account, v_container, v_object,
+                                        dest_account, dest_container, dest_name, move=True)
     response = HttpResponse(status=201)
     response['X-Object-Version'] = version_id
     return response
index 6ff6a98..9e4dc3a 100644 (file)
@@ -281,20 +281,21 @@ def split_container_object_string(s):
         raise ValueError
     return s[:pos], s[(pos + 1):]
 
-def copy_or_move_object(request, v_account, src_container, src_name, dest_container, dest_name, move=False):
+def copy_or_move_object(request, src_account, src_container, src_name, dest_account, dest_container, dest_name, move=False):
     """Copy or move an object."""
     
     meta, permissions, public = get_object_headers(request)
-    src_version = request.META.get('HTTP_X_SOURCE_VERSION')    
+    print '---', meta, permissions, public
+    src_version = request.META.get('HTTP_X_SOURCE_VERSION')
     try:
         if move:
-            version_id = request.backend.move_object(request.user, v_account,
-                            src_container, src_name, dest_container, dest_name,
-                            meta, False, permissions)
+            version_id = request.backend.move_object(request.user, src_account, src_container, src_name,
+                                                        dest_account, dest_container, dest_name,
+                                                        meta, False, permissions)
         else:
-            version_id = request.backend.copy_object(request.user, v_account,
-                            src_container, src_name, dest_container, dest_name,
-                            meta, False, permissions, src_version)
+            version_id = request.backend.copy_object(request.user, src_account, src_container, src_name,
+                                                        dest_account, dest_container, dest_name,
+                                                        meta, False, permissions, src_version)
     except NotAllowedError:
         raise Unauthorized('Access denied')
     except (NameError, IndexError):
@@ -305,8 +306,7 @@ def copy_or_move_object(request, v_account, src_container, src_name, dest_contai
         raise Conflict(json.dumps(e.data))
     if public is not None:
         try:
-            request.backend.update_object_public(request.user, v_account,
-                                            dest_container, dest_name, public)
+            request.backend.update_object_public(request.user, dest_account, dest_container, dest_name, public)
         except NotAllowedError:
             raise Unauthorized('Access denied')
         except NameError:
index 9fa962c..2bd5eab 100644 (file)
@@ -403,7 +403,7 @@ class BaseBackend(object):
         """
         return ''
     
-    def copy_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None, src_version=None):
+    def copy_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None, src_version=None):
         """Copy an object's data and metadata and return the new version.
         
         Parameters:
@@ -428,7 +428,7 @@ class BaseBackend(object):
         """
         return ''
     
-    def move_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None):
+    def move_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None):
         """Move an object's data and metadata and return the new version.
         
         Parameters:
index 726dcf0..5475fe2 100644 (file)
@@ -509,17 +509,17 @@ class ModularBackend(BaseBackend):
             self.permissions.access_set(path, permissions)
         return dest_version_id
     
-    def _copy_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None, src_version=None):
-        if permissions is not None and user != account:
+    def _copy_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None, src_version=None):
+        if permissions is not None and user != dest_account:
             raise NotAllowedError
-        self._can_read(user, account, src_container, src_name)
-        self._can_write(user, account, dest_container, dest_name)
-        src_path, src_node = self._lookup_object(account, src_container, src_name)
+        self._can_read(user, src_account, src_container, src_name)
+        self._can_write(user, dest_account, dest_container, dest_name)
+        src_path, src_node = self._lookup_object(src_account, src_container, src_name)
         self._get_version(src_node, src_version)
         if permissions is not None:
-            dest_path = '/'.join((account, container, name))
+            dest_path = '/'.join((dest_account, dest_container, dest_name))
             self._check_permissions(dest_path, permissions)
-        dest_path, dest_node = self._put_object_node(account, dest_container, dest_name)
+        dest_path, dest_node = self._put_object_node(dest_account, dest_container, dest_name)
         src_version_id, dest_version_id = self._copy_version(user, src_node, src_version, dest_node)
         if src_version_id is not None:
             self._copy_data(src_version_id, dest_version_id)
@@ -531,19 +531,21 @@ class ModularBackend(BaseBackend):
         return dest_version_id
     
     @backend_method
-    def copy_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None, src_version=None):
+    def copy_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None, src_version=None):
         """Copy an object's data and metadata."""
         
-        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)
-        return self._copy_object(user, account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, permissions, src_version)
+        logger.debug("copy_object: %s %s %s %s %s %s %s %s %s %s", src_account, src_container, src_name, dest_account, dest_container, dest_name, dest_meta, replace_meta, permissions, src_version)
+        return self._copy_object(user, src_account, src_container, src_name, dest_account, dest_container, dest_name, dest_meta, replace_meta, permissions, src_version)
     
     @backend_method
-    def move_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None):
+    def move_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None):
         """Move an object's data and metadata."""
         
-        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)
-        dest_version_id = self._copy_object(user, account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, permissions, None)
-        self._delete_object(user, account, src_container, src_name)
+        logger.debug("move_object: %s %s %s %s %s %s %s %s %s", src_account, src_container, src_name, dest_account, dest_container, dest_name, dest_meta, replace_meta, permissions)
+        if user != src_account:
+            raise NotAllowedError
+        dest_version_id = self._copy_object(user, src_account, src_container, src_name, dest_account, dest_container, dest_name, dest_meta, replace_meta, permissions, None)
+        self._delete_object(user, src_account, src_container, src_name)
         return dest_version_id
     
     def _delete_object(self, user, account, container, name, until=None):
index 1e2f8a1..5bde78f 100644 (file)
@@ -535,20 +535,20 @@ class SimpleBackend(BaseBackend):
         return dest_version_id
     
     @backend_method
-    def copy_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None, src_version=None):
+    def copy_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None, src_version=None):
         """Copy an object's data and metadata."""
         
-        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)
-        if permissions is not None and user != account:
+        logger.debug("copy_object: %s %s %s %s %s %s %s %s %s %s", src_account, src_container, src_name, dest_account, dest_container, dest_name, dest_meta, replace_meta, permissions, src_version)
+        if permissions is not None and user != dest_account:
             raise NotAllowedError
-        self._can_read(user, account, src_container, src_name)
-        self._can_write(user, account, dest_container, dest_name)
-        self._get_containerinfo(account, src_container)
+        self._can_read(user, src_account, src_container, src_name)
+        self._can_write(user, dest_account, dest_container, dest_name)
+        self._get_containerinfo(src_account, src_container)
         if src_version is None:
-            src_path = self._get_objectinfo(account, src_container, src_name)[0]
+            src_path = self._get_objectinfo(src_account, src_container, src_name)[0]
         else:
-            src_path = '/'.join((account, src_container, src_name))
-        dest_path = self._get_containerinfo(account, dest_container)[0]
+            src_path = '/'.join((src_account, src_container, src_name))
+        dest_path = self._get_containerinfo(dest_account, dest_container)[0]
         dest_path = '/'.join((dest_path, dest_name))
         if permissions is not None:
             r, w = self._check_permissions(dest_path, permissions)
@@ -561,12 +561,12 @@ class SimpleBackend(BaseBackend):
         return dest_version_id
     
     @backend_method
-    def move_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None):
+    def move_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions=None):
         """Move an object's data and metadata."""
         
-        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)
-        dest_version_id = self.copy_object(user, account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, permissions, None)
-        self.delete_object(user, account, src_container, src_name)
+        logger.debug("move_object: %s %s %s %s %s %s %s %s %s", src_account, src_container, src_name, dest_account, dest_container, dest_name, dest_meta, replace_meta, permissions)
+        dest_version_id = self.copy_object(user, src_account, src_container, src_name, dest_account, dest_container, dest_name, dest_meta, replace_meta, permissions, None)
+        self.delete_object(user, src_account, src_container, src_name)
         return dest_version_id
     
     @backend_method