Commit d5561725 authored by Antony Chazapis's avatar Antony Chazapis

Support cross-account copy and move.

Fixes #1241
parent 28b9bc28
......@@ -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``.
......
......@@ -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
......
......@@ -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:
......
......@@ -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:
......
......@@ -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):
......
......@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment