From 43ac76373a353fe74a520108a198b0b563c4f3a2 Mon Sep 17 00:00:00 2001 From: Constantine Peresypkin Date: Wed, 30 Apr 2014 15:00:49 +0300 Subject: [PATCH] account to account copy implementation Adds ability to copy objects between different accounts (on server side) Adds new header to `PUT` request: `X-Copy-From-Account: ` Account name corresponds to the last part of storage URL. Adds new header to `COPY` request: `Destination-Account: ` Account name corresponds to the last part of storage URL. If your storage URL is: http://server:8080/v1/AUTH_test Then the account name is `AUTH_test` These headers should be used alongside `X-Copy-From` and `Destination` headers The legacy headers should specify `/` path as usual. DocImpact Change-Id: I0285fe6a47df9e699ac20ae4a83b0bf23829e1e6 --- swift/common/constraints.py | 70 ++++- swift/proxy/controllers/obj.py | 52 ++-- swift/proxy/server.py | 3 +- test/functional/swift_test_client.py | 30 ++- test/functional/test_object.py | 115 ++++++++- test/functional/tests.py | 319 +++++++++++++++++++++++ test/unit/common/test_constraints.py | 35 +++ test/unit/proxy/test_server.py | 365 ++++++++++++++++++++++++++- 8 files changed, 953 insertions(+), 36 deletions(-) diff --git a/swift/common/constraints.py b/swift/common/constraints.py index 24dcbf034c..e4e678991c 100644 --- a/swift/common/constraints.py +++ b/swift/common/constraints.py @@ -248,6 +248,31 @@ def check_utf8(string): return False +def check_path_header(req, name, length, error_msg): + """ + Validate that the value of path-like header is + well formatted. We assume the caller ensures that + specific header is present in req.headers. + + :param req: HTTP request object + :param name: header name + :param length: length of path segment check + :param error_msg: error message for client + :returns: A tuple with path parts according to length + :raise: HTTPPreconditionFailed if header value + is not well formatted. + """ + src_header = unquote(req.headers.get(name)) + if not src_header.startswith('/'): + src_header = '/' + src_header + try: + return utils.split_path(src_header, length, length, True) + except ValueError: + raise HTTPPreconditionFailed( + request=req, + body=error_msg) + + def check_copy_from_header(req): """ Validate that the value from x-copy-from header is @@ -259,13 +284,42 @@ def check_copy_from_header(req): :raise: HTTPPreconditionFailed if x-copy-from value is not well formatted. """ - src_header = unquote(req.headers.get('X-Copy-From')) - if not src_header.startswith('/'): - src_header = '/' + src_header - try: - return utils.split_path(src_header, 2, 2, True) - except ValueError: + return check_path_header(req, 'X-Copy-From', 2, + 'X-Copy-From header must be of the form ' + '/') + + +def check_destination_header(req): + """ + Validate that the value from destination header is + well formatted. We assume the caller ensures that + destination header is present in req.headers. + + :param req: HTTP request object + :returns: A tuple with container name and object name + :raise: HTTPPreconditionFailed if destination value + is not well formatted. + """ + return check_path_header(req, 'Destination', 2, + 'Destination header must be of the form ' + '/') + + +def check_account_format(req, account): + """ + Validate that the header contains valid account name. + We assume the caller ensures that + destination header is present in req.headers. + + :param req: HTTP request object + :returns: A properly encoded account name + :raise: HTTPPreconditionFailed if account header + is not well formatted. + """ + if isinstance(account, unicode): + account = account.encode('utf-8') + if '/' in account: raise HTTPPreconditionFailed( request=req, - body='X-Copy-From header must be of the form' - '/') + body='Account name cannot contain slashes') + return account diff --git a/swift/proxy/controllers/obj.py b/swift/proxy/controllers/obj.py index 7b3eba440e..ed8045cfa3 100644 --- a/swift/proxy/controllers/obj.py +++ b/swift/proxy/controllers/obj.py @@ -41,7 +41,8 @@ from swift.common.utils import ( normalize_delete_at_timestamp, public, quorum_size) from swift.common.bufferedhttp import http_connect from swift.common.constraints import check_metadata, check_object_creation, \ - check_copy_from_header + check_copy_from_header, check_destination_header, \ + check_account_format from swift.common import constraints from swift.common.exceptions import ChunkReadTimeout, \ ChunkWriteTimeout, ConnectionTimeout, ListingIterNotFound, \ @@ -588,12 +589,15 @@ class ObjectController(Controller): if req.environ.get('swift.orig_req_method', req.method) != 'POST': req.environ.setdefault('swift.log_info', []).append( 'x-copy-from:%s' % source_header) - src_container_name, src_obj_name = check_copy_from_header(req) ver, acct, _rest = req.split_path(2, 3, True) - if isinstance(acct, unicode): - acct = acct.encode('utf-8') - source_header = '/%s/%s/%s/%s' % (ver, acct, - src_container_name, src_obj_name) + src_account_name = req.headers.get('X-Copy-From-Account', None) + if src_account_name: + src_account_name = check_account_format(req, src_account_name) + else: + src_account_name = acct + src_container_name, src_obj_name = check_copy_from_header(req) + source_header = '/%s/%s/%s/%s' % (ver, src_account_name, + src_container_name, src_obj_name) source_req = req.copy_get() # make sure the source request uses it's container_info @@ -602,8 +606,10 @@ class ObjectController(Controller): source_req.headers['X-Newest'] = 'true' orig_obj_name = self.object_name orig_container_name = self.container_name + orig_account_name = self.account_name self.object_name = src_obj_name self.container_name = src_container_name + self.account_name = src_account_name sink_req = Request.blank(req.path_info, environ=req.environ, headers=req.headers) source_resp = self.GET(source_req) @@ -621,6 +627,7 @@ class ObjectController(Controller): return source_resp self.object_name = orig_obj_name self.container_name = orig_container_name + self.account_name = orig_account_name data_source = iter(source_resp.app_iter) sink_req.content_length = source_resp.content_length if sink_req.content_length is None: @@ -635,6 +642,8 @@ class ObjectController(Controller): # we no longer need the X-Copy-From header del sink_req.headers['X-Copy-From'] + if 'X-Copy-From-Account' in sink_req.headers: + del sink_req.headers['X-Copy-From-Account'] if not content_type_manually_set: sink_req.headers['Content-Type'] = \ source_resp.headers['Content-Type'] @@ -763,8 +772,9 @@ class ObjectController(Controller): resp = self.best_response(req, statuses, reasons, bodies, _('Object PUT'), etag=etag) if source_header: - resp.headers['X-Copied-From'] = quote( - source_header.split('/', 3)[3]) + acct, path = source_header.split('/', 3)[2:4] + resp.headers['X-Copied-From-Account'] = quote(acct) + resp.headers['X-Copied-From'] = quote(path) if 'last-modified' in source_resp.headers: resp.headers['X-Copied-From-Last-Modified'] = \ source_resp.headers['last-modified'] @@ -885,27 +895,25 @@ class ObjectController(Controller): @delay_denial def COPY(self, req): """HTTP COPY request handler.""" - dest = req.headers.get('Destination') - if not dest: + if not req.headers.get('Destination'): return HTTPPreconditionFailed(request=req, body='Destination header required') - dest = unquote(dest) - if not dest.startswith('/'): - dest = '/' + dest - try: - _junk, dest_container, dest_object = dest.split('/', 2) - except ValueError: - return HTTPPreconditionFailed( - request=req, - body='Destination header must be of the form ' - '/') - source = '/' + self.container_name + '/' + self.object_name + dest_account = self.account_name + if 'Destination-Account' in req.headers: + dest_account = req.headers.get('Destination-Account') + dest_account = check_account_format(req, dest_account) + req.headers['X-Copy-From-Account'] = self.account_name + self.account_name = dest_account + del req.headers['Destination-Account'] + dest_container, dest_object = check_destination_header(req) + source = '/%s/%s' % (self.container_name, self.object_name) self.container_name = dest_container self.object_name = dest_object # re-write the existing request as a PUT instead of creating a new one # since this one is already attached to the posthooklogger req.method = 'PUT' - req.path_info = '/v1/' + self.account_name + dest + req.path_info = '/v1/%s/%s/%s' % \ + (dest_account, dest_container, dest_object) req.headers['Content-Length'] = 0 req.headers['X-Copy-From'] = quote(source) del req.headers['Destination'] diff --git a/swift/proxy/server.py b/swift/proxy/server.py index 572e47df4e..c6e7fb78bd 100644 --- a/swift/proxy/server.py +++ b/swift/proxy/server.py @@ -355,7 +355,8 @@ class Application(object): # controller's method indicates it'd like to gather more # information and try again later. resp = req.environ['swift.authorize'](req) - if not resp: + if not resp and not req.headers.get('X-Copy-From-Account') \ + and not req.headers.get('Destination-Account'): # No resp means authorized, no delayed recheck required. del req.environ['swift.authorize'] else: diff --git a/test/functional/swift_test_client.py b/test/functional/swift_test_client.py index 2c35520900..941dfbbf73 100644 --- a/test/functional/swift_test_client.py +++ b/test/functional/swift_test_client.py @@ -174,8 +174,10 @@ class Connection(object): # unicode and this would cause troubles when doing # no_safe_quote query. self.storage_url = str('/%s/%s' % (x[3], x[4])) - + self.account_name = str(x[4]) + self.auth_user = auth_user self.storage_token = storage_token + self.user_acl = '%s:%s' % (self.account, self.username) self.http_connect() return self.storage_url, self.storage_token @@ -664,6 +666,32 @@ class File(Base): return self.conn.make_request('COPY', self.path, hdrs=headers, parms=parms) == 201 + def copy_account(self, dest_account, dest_cont, dest_file, + hdrs=None, parms=None, cfg=None): + if hdrs is None: + hdrs = {} + if parms is None: + parms = {} + if cfg is None: + cfg = {} + if 'destination' in cfg: + headers = {'Destination': cfg['destination']} + elif cfg.get('no_destination'): + headers = {} + else: + headers = {'Destination-Account': dest_account, + 'Destination': '%s/%s' % (dest_cont, dest_file)} + headers.update(hdrs) + + if 'Destination-Account' in headers: + headers['Destination-Account'] = \ + urllib.quote(headers['Destination-Account']) + if 'Destination' in headers: + headers['Destination'] = urllib.quote(headers['Destination']) + + return self.conn.make_request('COPY', self.path, hdrs=headers, + parms=parms) == 201 + def delete(self, hdrs=None, parms=None): if hdrs is None: hdrs = {} diff --git a/test/functional/test_object.py b/test/functional/test_object.py index 6b29800515..72f18e9155 100755 --- a/test/functional/test_object.py +++ b/test/functional/test_object.py @@ -35,6 +35,7 @@ class TestObject(unittest.TestCase): self.containers = [] self._create_container(self.container) + self._create_container(self.container, use_account=2) self.obj = uuid4().hex @@ -47,7 +48,7 @@ class TestObject(unittest.TestCase): resp.read() self.assertEqual(resp.status, 201) - def _create_container(self, name=None, headers=None): + def _create_container(self, name=None, headers=None, use_account=1): if not name: name = uuid4().hex self.containers.append(name) @@ -58,7 +59,7 @@ class TestObject(unittest.TestCase): conn.request('PUT', parsed.path + '/' + name, '', new_headers) return check_response(conn) - resp = retry(put, name) + resp = retry(put, name, use_account=use_account) resp.read() self.assertEqual(resp.status, 201) return name @@ -207,6 +208,116 @@ class TestObject(unittest.TestCase): resp.read() self.assertEqual(resp.status, 204) + def test_copy_between_accounts(self): + if tf.skip: + raise SkipTest + + source = '%s/%s' % (self.container, self.obj) + dest = '%s/%s' % (self.container, 'test_copy') + + # get contents of source + def get_source(url, token, parsed, conn): + conn.request('GET', + '%s/%s' % (parsed.path, source), + '', {'X-Auth-Token': token}) + return check_response(conn) + resp = retry(get_source) + source_contents = resp.read() + self.assertEqual(resp.status, 200) + self.assertEqual(source_contents, 'test') + + acct = tf.parsed[0].path.split('/', 2)[2] + + # copy source to dest with X-Copy-From-Account + def put(url, token, parsed, conn): + conn.request('PUT', '%s/%s' % (parsed.path, dest), '', + {'X-Auth-Token': token, + 'Content-Length': '0', + 'X-Copy-From-Account': acct, + 'X-Copy-From': source}) + return check_response(conn) + # try to put, will not succeed + # user does not have permissions to read from source + resp = retry(put, use_account=2) + self.assertEqual(resp.status, 403) + + # add acl to allow reading from source + def post(url, token, parsed, conn): + conn.request('POST', '%s/%s' % (parsed.path, self.container), '', + {'X-Auth-Token': token, + 'X-Container-Read': tf.swift_test_perm[1]}) + return check_response(conn) + resp = retry(post) + self.assertEqual(resp.status, 204) + + # retry previous put, now should succeed + resp = retry(put, use_account=2) + self.assertEqual(resp.status, 201) + + # contents of dest should be the same as source + def get_dest(url, token, parsed, conn): + conn.request('GET', + '%s/%s' % (parsed.path, dest), + '', {'X-Auth-Token': token}) + return check_response(conn) + resp = retry(get_dest, use_account=2) + dest_contents = resp.read() + self.assertEqual(resp.status, 200) + self.assertEqual(dest_contents, source_contents) + + # delete the copy + def delete(url, token, parsed, conn): + conn.request('DELETE', '%s/%s' % (parsed.path, dest), '', + {'X-Auth-Token': token}) + return check_response(conn) + resp = retry(delete, use_account=2) + resp.read() + self.assertEqual(resp.status, 204) + # verify dest does not exist + resp = retry(get_dest, use_account=2) + resp.read() + self.assertEqual(resp.status, 404) + + acct_dest = tf.parsed[1].path.split('/', 2)[2] + + # copy source to dest with COPY + def copy(url, token, parsed, conn): + conn.request('COPY', '%s/%s' % (parsed.path, source), '', + {'X-Auth-Token': token, + 'Destination-Account': acct_dest, + 'Destination': dest}) + return check_response(conn) + # try to copy, will not succeed + # user does not have permissions to write to destination + resp = retry(copy) + resp.read() + self.assertEqual(resp.status, 403) + + # add acl to allow write to destination + def post(url, token, parsed, conn): + conn.request('POST', '%s/%s' % (parsed.path, self.container), '', + {'X-Auth-Token': token, + 'X-Container-Write': tf.swift_test_perm[0]}) + return check_response(conn) + resp = retry(post, use_account=2) + self.assertEqual(resp.status, 204) + + # now copy will succeed + resp = retry(copy) + resp.read() + self.assertEqual(resp.status, 201) + + # contents of dest should be the same as source + resp = retry(get_dest, use_account=2) + dest_contents = resp.read() + self.assertEqual(resp.status, 200) + self.assertEqual(dest_contents, source_contents) + + # delete the copy + resp = retry(delete, use_account=2) + resp.read() + self.assertEqual(resp.status, 204) + def test_public_object(self): if tf.skip: raise SkipTest diff --git a/test/functional/tests.py b/test/functional/tests.py index 399200b92e..9f2f0359ac 100644 --- a/test/functional/tests.py +++ b/test/functional/tests.py @@ -25,6 +25,7 @@ import time import unittest import urllib import uuid +from copy import deepcopy import eventlet from nose import SkipTest @@ -790,9 +791,22 @@ class TestFileEnv(object): def setUp(cls): cls.conn = Connection(tf.config) cls.conn.authenticate() + cls.account = Account(cls.conn, tf.config.get('account', + tf.config['username'])) + # creating another account and connection + # for account to account copy tests + config2 = deepcopy(tf.config) + config2['account'] = tf.config['account2'] + config2['username'] = tf.config['username2'] + config2['password'] = tf.config['password2'] + cls.conn2 = Connection(config2) + cls.conn2.authenticate() + cls.account = Account(cls.conn, tf.config.get('account', tf.config['username'])) cls.account.delete_containers() + cls.account2 = cls.conn2.get_account() + cls.account2.delete_containers() cls.container = cls.account.container(Utils.create_name()) if not cls.container.create(): @@ -846,6 +860,62 @@ class TestFile(Base): self.assert_(file_item.initialize()) self.assert_(metadata == file_item.metadata) + def testCopyAccount(self): + # makes sure to test encoded characters + source_filename = 'dealde%2Fl04 011e%204c8df/flash.png' + file_item = self.env.container.file(source_filename) + + metadata = {Utils.create_ascii_name(): Utils.create_name()} + + data = file_item.write_random() + file_item.sync_metadata(metadata) + + dest_cont = self.env.account.container(Utils.create_name()) + self.assert_(dest_cont.create()) + + acct = self.env.conn.account_name + # copy both from within and across containers + for cont in (self.env.container, dest_cont): + # copy both with and without initial slash + for prefix in ('', '/'): + dest_filename = Utils.create_name() + + file_item = self.env.container.file(source_filename) + file_item.copy_account(acct, + '%s%s' % (prefix, cont), + dest_filename) + + self.assert_(dest_filename in cont.files()) + + file_item = cont.file(dest_filename) + + self.assert_(data == file_item.read()) + self.assert_(file_item.initialize()) + self.assert_(metadata == file_item.metadata) + + dest_cont = self.env.account2.container(Utils.create_name()) + self.assert_(dest_cont.create(hdrs={ + 'X-Container-Write': self.env.conn.user_acl + })) + + acct = self.env.conn2.account_name + # copy both with and without initial slash + for prefix in ('', '/'): + dest_filename = Utils.create_name() + + file_item = self.env.container.file(source_filename) + file_item.copy_account(acct, + '%s%s' % (prefix, dest_cont), + dest_filename) + + self.assert_(dest_filename in dest_cont.files()) + + file_item = dest_cont.file(dest_filename) + + self.assert_(data == file_item.read()) + self.assert_(file_item.initialize()) + self.assert_(metadata == file_item.metadata) + def testCopy404s(self): source_filename = Utils.create_name() file_item = self.env.container.file(source_filename) @@ -884,6 +954,77 @@ class TestFile(Base): '%s%s' % (prefix, Utils.create_name()), Utils.create_name())) + def testCopyAccount404s(self): + acct = self.env.conn.account_name + acct2 = self.env.conn2.account_name + source_filename = Utils.create_name() + file_item = self.env.container.file(source_filename) + file_item.write_random() + + dest_cont = self.env.account.container(Utils.create_name()) + self.assert_(dest_cont.create(hdrs={ + 'X-Container-Read': self.env.conn2.user_acl + })) + dest_cont2 = self.env.account2.container(Utils.create_name()) + self.assert_(dest_cont2.create(hdrs={ + 'X-Container-Write': self.env.conn.user_acl, + 'X-Container-Read': self.env.conn.user_acl + })) + + for acct, cont in ((acct, dest_cont), (acct2, dest_cont2)): + for prefix in ('', '/'): + # invalid source container + source_cont = self.env.account.container(Utils.create_name()) + file_item = source_cont.file(source_filename) + self.assert_(not file_item.copy_account( + acct, + '%s%s' % (prefix, self.env.container), + Utils.create_name())) + if acct == acct2: + # there is no such source container + # and foreign user can have no permission to read it + self.assert_status(403) + else: + self.assert_status(404) + + self.assert_(not file_item.copy_account( + acct, + '%s%s' % (prefix, cont), + Utils.create_name())) + self.assert_status(404) + + # invalid source object + file_item = self.env.container.file(Utils.create_name()) + self.assert_(not file_item.copy_account( + acct, + '%s%s' % (prefix, self.env.container), + Utils.create_name())) + if acct == acct2: + # there is no such object + # and foreign user can have no permission to read it + self.assert_status(403) + else: + self.assert_status(404) + + self.assert_(not file_item.copy_account( + acct, + '%s%s' % (prefix, cont), + Utils.create_name())) + self.assert_status(404) + + # invalid destination container + file_item = self.env.container.file(source_filename) + self.assert_(not file_item.copy_account( + acct, + '%s%s' % (prefix, Utils.create_name()), + Utils.create_name())) + if acct == acct2: + # there is no such destination container + # and foreign user can have no permission to write there + self.assert_status(403) + else: + self.assert_status(404) + def testCopyNoDestinationHeader(self): source_filename = Utils.create_name() file_item = self.env.container.file(source_filename) @@ -938,6 +1079,49 @@ class TestFile(Base): self.assert_(file_item.initialize()) self.assert_(metadata == file_item.metadata) + def testCopyFromAccountHeader(self): + acct = self.env.conn.account_name + src_cont = self.env.account.container(Utils.create_name()) + self.assert_(src_cont.create(hdrs={ + 'X-Container-Read': self.env.conn2.user_acl + })) + source_filename = Utils.create_name() + file_item = src_cont.file(source_filename) + + metadata = {} + for i in range(1): + metadata[Utils.create_ascii_name()] = Utils.create_name() + file_item.metadata = metadata + + data = file_item.write_random() + + dest_cont = self.env.account.container(Utils.create_name()) + self.assert_(dest_cont.create()) + dest_cont2 = self.env.account2.container(Utils.create_name()) + self.assert_(dest_cont2.create(hdrs={ + 'X-Container-Write': self.env.conn.user_acl + })) + + for cont in (src_cont, dest_cont, dest_cont2): + # copy both with and without initial slash + for prefix in ('', '/'): + dest_filename = Utils.create_name() + + file_item = cont.file(dest_filename) + file_item.write(hdrs={'X-Copy-From-Account': acct, + 'X-Copy-From': '%s%s/%s' % ( + prefix, + src_cont.name, + source_filename)}) + + self.assert_(dest_filename in cont.files()) + + file_item = cont.file(dest_filename) + + self.assert_(data == file_item.read()) + self.assert_(file_item.initialize()) + self.assert_(metadata == file_item.metadata) + def testCopyFromHeader404s(self): source_filename = Utils.create_name() file_item = self.env.container.file(source_filename) @@ -969,6 +1153,52 @@ class TestFile(Base): self.env.container.name, source_filename)}) self.assert_status(404) + def testCopyFromAccountHeader404s(self): + acct = self.env.conn2.account_name + src_cont = self.env.account2.container(Utils.create_name()) + self.assert_(src_cont.create(hdrs={ + 'X-Container-Read': self.env.conn.user_acl + })) + source_filename = Utils.create_name() + file_item = src_cont.file(source_filename) + file_item.write_random() + dest_cont = self.env.account.container(Utils.create_name()) + self.assert_(dest_cont.create()) + + for prefix in ('', '/'): + # invalid source container + file_item = dest_cont.file(Utils.create_name()) + self.assertRaises(ResponseError, file_item.write, + hdrs={'X-Copy-From-Account': acct, + 'X-Copy-From': '%s%s/%s' % + (prefix, + Utils.create_name(), + source_filename)}) + # looks like cached responses leak "not found" + # to un-authorized users, not going to fix it now, but... + self.assert_status([403, 404]) + + # invalid source object + file_item = self.env.container.file(Utils.create_name()) + self.assertRaises(ResponseError, file_item.write, + hdrs={'X-Copy-From-Account': acct, + 'X-Copy-From': '%s%s/%s' % + (prefix, + src_cont, + Utils.create_name())}) + self.assert_status(404) + + # invalid destination container + dest_cont = self.env.account.container(Utils.create_name()) + file_item = dest_cont.file(Utils.create_name()) + self.assertRaises(ResponseError, file_item.write, + hdrs={'X-Copy-From-Account': acct, + 'X-Copy-From': '%s%s/%s' % + (prefix, + src_cont, + source_filename)}) + self.assert_status(404) + def testNameLimit(self): limit = load_constraint('max_object_name_length') @@ -1591,6 +1821,30 @@ class TestDlo(Base): file_contents, "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffff") + def test_copy_account(self): + # dlo use same account and same container only + acct = self.env.conn.account_name + # Adding a new segment, copying the manifest, and then deleting the + # segment proves that the new object is really the concatenated + # segments and not just a manifest. + f_segment = self.env.container.file("%s/seg_lowerf" % + (self.env.segment_prefix)) + f_segment.write('ffffffffff') + try: + man1_item = self.env.container.file('man1') + man1_item.copy_account(acct, + self.env.container.name, + "copied-man1") + finally: + # try not to leave this around for other tests to stumble over + f_segment.delete() + + file_item = self.env.container.file('copied-man1') + file_contents = file_item.read() + self.assertEqual( + file_contents, + "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffff") + def test_copy_manifest(self): # Copying the manifest should result in another manifest try: @@ -1787,6 +2041,14 @@ class TestSloEnv(object): def setUp(cls): cls.conn = Connection(tf.config) cls.conn.authenticate() + config2 = deepcopy(tf.config) + config2['account'] = tf.config['account2'] + config2['username'] = tf.config['username2'] + config2['password'] = tf.config['password2'] + cls.conn2 = Connection(config2) + cls.conn2.authenticate() + cls.account2 = cls.conn2.get_account() + cls.account2.delete_containers() if cls.slo_enabled is None: cls.slo_enabled = 'slo' in cluster_info @@ -1969,6 +2231,29 @@ class TestSlo(Base): copied_contents = copied.read(parms={'multipart-manifest': 'get'}) self.assertEqual(4 * 1024 * 1024 + 1, len(copied_contents)) + def test_slo_copy_account(self): + acct = self.env.conn.account_name + # same account copy + file_item = self.env.container.file("manifest-abcde") + file_item.copy_account(acct, self.env.container.name, "copied-abcde") + + copied = self.env.container.file("copied-abcde") + copied_contents = copied.read(parms={'multipart-manifest': 'get'}) + self.assertEqual(4 * 1024 * 1024 + 1, len(copied_contents)) + + # copy to different account + acct = self.env.conn2.account_name + dest_cont = self.env.account2.container(Utils.create_name()) + self.assert_(dest_cont.create(hdrs={ + 'X-Container-Write': self.env.conn.user_acl + })) + file_item = self.env.container.file("manifest-abcde") + file_item.copy_account(acct, dest_cont, "copied-abcde") + + copied = dest_cont.file("copied-abcde") + copied_contents = copied.read(parms={'multipart-manifest': 'get'}) + self.assertEqual(4 * 1024 * 1024 + 1, len(copied_contents)) + def test_slo_copy_the_manifest(self): file_item = self.env.container.file("manifest-abcde") file_item.copy(self.env.container.name, "copied-abcde-manifest-only", @@ -1981,6 +2266,40 @@ class TestSlo(Base): except ValueError: self.fail("COPY didn't copy the manifest (invalid json on GET)") + def test_slo_copy_the_manifest_account(self): + acct = self.env.conn.account_name + # same account + file_item = self.env.container.file("manifest-abcde") + file_item.copy_account(acct, + self.env.container.name, + "copied-abcde-manifest-only", + parms={'multipart-manifest': 'get'}) + + copied = self.env.container.file("copied-abcde-manifest-only") + copied_contents = copied.read(parms={'multipart-manifest': 'get'}) + try: + json.loads(copied_contents) + except ValueError: + self.fail("COPY didn't copy the manifest (invalid json on GET)") + + # different account + acct = self.env.conn2.account_name + dest_cont = self.env.account2.container(Utils.create_name()) + self.assert_(dest_cont.create(hdrs={ + 'X-Container-Write': self.env.conn.user_acl + })) + file_item.copy_account(acct, + dest_cont, + "copied-abcde-manifest-only", + parms={'multipart-manifest': 'get'}) + + copied = dest_cont.file("copied-abcde-manifest-only") + copied_contents = copied.read(parms={'multipart-manifest': 'get'}) + try: + json.loads(copied_contents) + except ValueError: + self.fail("COPY didn't copy the manifest (invalid json on GET)") + def test_slo_get_the_manifest(self): manifest = self.env.container.file("manifest-abcde") got_body = manifest.read(parms={'multipart-manifest': 'get'}) diff --git a/test/unit/common/test_constraints.py b/test/unit/common/test_constraints.py index 753f858de7..262121e1ec 100644 --- a/test/unit/common/test_constraints.py +++ b/test/unit/common/test_constraints.py @@ -260,6 +260,41 @@ class TestConstraints(unittest.TestCase): self.assertRaises(HTTPException, constraints.check_copy_from_header, req) + def test_validate_destination(self): + req = Request.blank( + '/v/a/c/o', + headers={'destination': 'c/o2'}) + src_cont, src_obj = constraints.check_destination_header(req) + self.assertEqual(src_cont, 'c') + self.assertEqual(src_obj, 'o2') + req = Request.blank( + '/v/a/c/o', + headers={'destination': 'c/subdir/o2'}) + src_cont, src_obj = constraints.check_destination_header(req) + self.assertEqual(src_cont, 'c') + self.assertEqual(src_obj, 'subdir/o2') + req = Request.blank( + '/v/a/c/o', + headers={'destination': '/c/o2'}) + src_cont, src_obj = constraints.check_destination_header(req) + self.assertEqual(src_cont, 'c') + self.assertEqual(src_obj, 'o2') + + def test_validate_bad_destination(self): + req = Request.blank( + '/v/a/c/o', + headers={'destination': 'bad_object'}) + self.assertRaises(HTTPException, + constraints.check_destination_header, req) + + def test_check_account_format(self): + req = Request.blank( + '/v/a/c/o', + headers={'X-Copy-From-Account': 'account/with/slashes'}) + self.assertRaises(HTTPException, + constraints.check_account_format, + req, req.headers['X-Copy-From-Account']) + class TestConstraintsConfig(unittest.TestCase): diff --git a/test/unit/proxy/test_server.py b/test/unit/proxy/test_server.py index 65a476ff14..11256b7c2b 100644 --- a/test/unit/proxy/test_server.py +++ b/test/unit/proxy/test_server.py @@ -179,6 +179,20 @@ def do_setup(the_object_server): 'x-trans-id': 'test'}) resp = conn.getresponse() assert(resp.status == 201) + # Create another account + # used for account-to-account tests + ts = normalize_timestamp(time.time()) + partition, nodes = prosrv.account_ring.get_nodes('a1') + for node in nodes: + conn = swift.proxy.controllers.obj.http_connect(node['ip'], + node['port'], + node['device'], + partition, 'PUT', + '/a1', + {'X-Timestamp': ts, + 'x-trans-id': 'test'}) + resp = conn.getresponse() + assert(resp.status == 201) # Create containers, 1 per test policy sock = connect_tcp(('localhost', prolis.getsockname()[1])) fd = sock.makefile() @@ -188,6 +202,18 @@ def do_setup(the_object_server): fd.flush() headers = readuntil2crlfs(fd) exp = 'HTTP/1.1 201' + assert headers[:len(exp)] == exp, "Expected '%s', encountered '%s'" % ( + exp, headers[:len(exp)]) + # Create container in other account + # used for account-to-account tests + sock = connect_tcp(('localhost', prolis.getsockname()[1])) + fd = sock.makefile() + fd.write('PUT /v1/a1/c1 HTTP/1.1\r\nHost: localhost\r\n' + 'Connection: close\r\nX-Auth-Token: t\r\n' + 'Content-Length: 0\r\n\r\n') + fd.flush() + headers = readuntil2crlfs(fd) + exp = 'HTTP/1.1 201' assert headers[:len(exp)] == exp, "Expected '%s', encountered '%s'" % ( exp, headers[:len(exp)]) @@ -2870,6 +2896,19 @@ class TestObjectController(unittest.TestCase): self.assertEquals(resp.status_int, 201) self.assertEquals(resp.headers['x-copied-from'], 'c/o') + def test_basic_put_with_x_copy_from_account(self): + req = Request.blank('/v1/a1/c1/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={'Content-Length': '0', + 'X-Copy-From': 'c/o', + 'X-Copy-From-Account': 'a'}) + status_list = (200, 200, 200, 200, 200, 200, 200, 201, 201, 201) + # acct cont acc1 con1 objc objc objc obj obj obj + with self.controller_context(req, *status_list) as controller: + resp = controller.PUT(req) + self.assertEquals(resp.status_int, 201) + self.assertEquals(resp.headers['x-copied-from'], 'c/o') + self.assertEquals(resp.headers['x-copied-from-account'], 'a') + def test_basic_put_with_x_copy_from_across_container(self): req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, headers={'Content-Length': '0', @@ -2881,6 +2920,19 @@ class TestObjectController(unittest.TestCase): self.assertEquals(resp.status_int, 201) self.assertEquals(resp.headers['x-copied-from'], 'c2/o') + def test_basic_put_with_x_copy_from_across_container_and_account(self): + req = Request.blank('/v1/a1/c1/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={'Content-Length': '0', + 'X-Copy-From': 'c2/o', + 'X-Copy-From-Account': 'a'}) + status_list = (200, 200, 200, 200, 200, 200, 200, 201, 201, 201) + # acct cont acc1 con1 objc objc objc obj obj obj + with self.controller_context(req, *status_list) as controller: + resp = controller.PUT(req) + self.assertEquals(resp.status_int, 201) + self.assertEquals(resp.headers['x-copied-from'], 'c2/o') + self.assertEquals(resp.headers['x-copied-from-account'], 'a') + def test_copy_non_zero_content_length(self): req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, headers={'Content-Length': '5', @@ -2891,6 +2943,17 @@ class TestObjectController(unittest.TestCase): resp = controller.PUT(req) self.assertEquals(resp.status_int, 400) + def test_copy_non_zero_content_length_with_account(self): + req = Request.blank('/v1/a1/c1/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={'Content-Length': '5', + 'X-Copy-From': 'c/o', + 'X-Copy-From-Account': 'a'}) + status_list = (200, 200) + # acct cont + with self.controller_context(req, *status_list) as controller: + resp = controller.PUT(req) + self.assertEquals(resp.status_int, 400) + def test_copy_with_slashes_in_x_copy_from(self): # extra source path parsing req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, @@ -2903,6 +2966,20 @@ class TestObjectController(unittest.TestCase): self.assertEquals(resp.status_int, 201) self.assertEquals(resp.headers['x-copied-from'], 'c/o/o2') + def test_copy_with_slashes_in_x_copy_from_and_account(self): + # extra source path parsing + req = Request.blank('/v1/a1/c1/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={'Content-Length': '0', + 'X-Copy-From': 'c/o/o2', + 'X-Copy-From-Account': 'a'}) + status_list = (200, 200, 200, 200, 200, 200, 200, 201, 201, 201) + # acct cont acc1 con1 objc objc objc obj obj obj + with self.controller_context(req, *status_list) as controller: + resp = controller.PUT(req) + self.assertEquals(resp.status_int, 201) + self.assertEquals(resp.headers['x-copied-from'], 'c/o/o2') + self.assertEquals(resp.headers['x-copied-from-account'], 'a') + def test_copy_with_spaces_in_x_copy_from(self): # space in soure path req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, @@ -2915,6 +2992,20 @@ class TestObjectController(unittest.TestCase): self.assertEquals(resp.status_int, 201) self.assertEquals(resp.headers['x-copied-from'], 'c/o%20o2') + def test_copy_with_spaces_in_x_copy_from_and_account(self): + # space in soure path + req = Request.blank('/v1/a1/c1/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={'Content-Length': '0', + 'X-Copy-From': 'c/o%20o2', + 'X-Copy-From-Account': 'a'}) + status_list = (200, 200, 200, 200, 200, 200, 200, 201, 201, 201) + # acct cont acc1 con1 objc objc objc obj obj obj + with self.controller_context(req, *status_list) as controller: + resp = controller.PUT(req) + self.assertEquals(resp.status_int, 201) + self.assertEquals(resp.headers['x-copied-from'], 'c/o%20o2') + self.assertEquals(resp.headers['x-copied-from-account'], 'a') + def test_copy_with_leading_slash_in_x_copy_from(self): # repeat tests with leading / req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, @@ -2927,6 +3018,20 @@ class TestObjectController(unittest.TestCase): self.assertEquals(resp.status_int, 201) self.assertEquals(resp.headers['x-copied-from'], 'c/o') + def test_copy_with_leading_slash_in_x_copy_from_and_account(self): + # repeat tests with leading / + req = Request.blank('/v1/a1/c1/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={'Content-Length': '0', + 'X-Copy-From': '/c/o', + 'X-Copy-From-Account': 'a'}) + status_list = (200, 200, 200, 200, 200, 200, 200, 201, 201, 201) + # acct cont acc1 con1 objc objc objc obj obj obj + with self.controller_context(req, *status_list) as controller: + resp = controller.PUT(req) + self.assertEquals(resp.status_int, 201) + self.assertEquals(resp.headers['x-copied-from'], 'c/o') + self.assertEquals(resp.headers['x-copied-from-account'], 'a') + def test_copy_with_leading_slash_and_slashes_in_x_copy_from(self): req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, headers={'Content-Length': '0', @@ -2938,6 +3043,19 @@ class TestObjectController(unittest.TestCase): self.assertEquals(resp.status_int, 201) self.assertEquals(resp.headers['x-copied-from'], 'c/o/o2') + def test_copy_with_leading_slash_and_slashes_in_x_copy_from_acct(self): + req = Request.blank('/v1/a1/c1/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={'Content-Length': '0', + 'X-Copy-From': '/c/o/o2', + 'X-Copy-From-Account': 'a'}) + status_list = (200, 200, 200, 200, 200, 200, 200, 201, 201, 201) + # acct cont acc1 con1 objc objc objc obj obj obj + with self.controller_context(req, *status_list) as controller: + resp = controller.PUT(req) + self.assertEquals(resp.status_int, 201) + self.assertEquals(resp.headers['x-copied-from'], 'c/o/o2') + self.assertEquals(resp.headers['x-copied-from-account'], 'a') + def test_copy_with_no_object_in_x_copy_from(self): req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, headers={'Content-Length': '0', @@ -2953,6 +3071,22 @@ class TestObjectController(unittest.TestCase): raise self.fail('Invalid X-Copy-From did not raise ' 'client error') + def test_copy_with_no_object_in_x_copy_from_and_account(self): + req = Request.blank('/v1/a1/c1/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={'Content-Length': '0', + 'X-Copy-From': '/c', + 'X-Copy-From-Account': 'a'}) + status_list = (200, 200) + # acct cont + with self.controller_context(req, *status_list) as controller: + try: + controller.PUT(req) + except HTTPException as resp: + self.assertEquals(resp.status_int // 100, 4) # client error + else: + raise self.fail('Invalid X-Copy-From did not raise ' + 'client error') + def test_copy_server_error_reading_source(self): req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, headers={'Content-Length': '0', @@ -2963,6 +3097,17 @@ class TestObjectController(unittest.TestCase): resp = controller.PUT(req) self.assertEquals(resp.status_int, 503) + def test_copy_server_error_reading_source_and_account(self): + req = Request.blank('/v1/a1/c1/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={'Content-Length': '0', + 'X-Copy-From': '/c/o', + 'X-Copy-From-Account': 'a'}) + status_list = (200, 200, 200, 200, 503, 503, 503) + # acct cont acct cont objc objc objc + with self.controller_context(req, *status_list) as controller: + resp = controller.PUT(req) + self.assertEquals(resp.status_int, 503) + def test_copy_not_found_reading_source(self): req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, headers={'Content-Length': '0', @@ -2974,6 +3119,18 @@ class TestObjectController(unittest.TestCase): resp = controller.PUT(req) self.assertEquals(resp.status_int, 404) + def test_copy_not_found_reading_source_and_account(self): + req = Request.blank('/v1/a1/c1/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={'Content-Length': '0', + 'X-Copy-From': '/c/o', + 'X-Copy-From-Account': 'a'}) + # not found + status_list = (200, 200, 200, 200, 404, 404, 404) + # acct cont acct cont objc objc objc + with self.controller_context(req, *status_list) as controller: + resp = controller.PUT(req) + self.assertEquals(resp.status_int, 404) + def test_copy_with_some_missing_sources(self): req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, headers={'Content-Length': '0', @@ -2984,6 +3141,17 @@ class TestObjectController(unittest.TestCase): resp = controller.PUT(req) self.assertEquals(resp.status_int, 201) + def test_copy_with_some_missing_sources_and_account(self): + req = Request.blank('/v1/a1/c1/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={'Content-Length': '0', + 'X-Copy-From': '/c/o', + 'X-Copy-From-Account': 'a'}) + status_list = (200, 200, 200, 200, 404, 404, 200, 201, 201, 201) + # acct cont acct cont objc objc objc obj obj obj + with self.controller_context(req, *status_list) as controller: + resp = controller.PUT(req) + self.assertEquals(resp.status_int, 201) + def test_copy_with_object_metadata(self): req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, headers={'Content-Length': '0', @@ -2999,6 +3167,22 @@ class TestObjectController(unittest.TestCase): self.assertEquals(resp.headers.get('x-object-meta-ours'), 'okay') self.assertEquals(resp.headers.get('x-delete-at'), '9876543210') + def test_copy_with_object_metadata_and_account(self): + req = Request.blank('/v1/a1/c1/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={'Content-Length': '0', + 'X-Copy-From': '/c/o', + 'X-Object-Meta-Ours': 'okay', + 'X-Copy-From-Account': 'a'}) + # test object metadata + status_list = (200, 200, 200, 200, 200, 200, 200, 201, 201, 201) + # acct cont acct cont objc objc objc obj obj obj + with self.controller_context(req, *status_list) as controller: + resp = controller.PUT(req) + self.assertEquals(resp.status_int, 201) + self.assertEquals(resp.headers.get('x-object-meta-test'), 'testing') + self.assertEquals(resp.headers.get('x-object-meta-ours'), 'okay') + self.assertEquals(resp.headers.get('x-delete-at'), '9876543210') + def test_copy_source_larger_than_max_file_size(self): req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, headers={'Content-Length': '0', @@ -3036,6 +3220,19 @@ class TestObjectController(unittest.TestCase): self.assertEquals(resp.status_int, 201) self.assertEquals(resp.headers['x-copied-from'], 'c/o') + def test_basic_COPY_account(self): + req = Request.blank('/v1/a/c/o', + environ={'REQUEST_METHOD': 'COPY'}, + headers={'Destination': 'c1/o2', + 'Destination-Account': 'a1'}) + status_list = (200, 200, 200, 200, 200, 200, 200, 201, 201, 201) + # acct cont acct cont objc objc objc obj obj obj + with self.controller_context(req, *status_list) as controller: + resp = controller.COPY(req) + self.assertEquals(resp.status_int, 201) + self.assertEquals(resp.headers['x-copied-from'], 'c/o') + self.assertEquals(resp.headers['x-copied-from-account'], 'a') + def test_COPY_across_containers(self): req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'COPY'}, @@ -3058,6 +3255,19 @@ class TestObjectController(unittest.TestCase): self.assertEquals(resp.status_int, 201) self.assertEquals(resp.headers['x-copied-from'], 'c/o/o2') + def test_COPY_account_source_with_slashes_in_name(self): + req = Request.blank('/v1/a/c/o/o2', + environ={'REQUEST_METHOD': 'COPY'}, + headers={'Destination': 'c1/o', + 'Destination-Account': 'a1'}) + status_list = (200, 200, 200, 200, 200, 200, 200, 201, 201, 201) + # acct cont acct cont objc objc objc obj obj obj + with self.controller_context(req, *status_list) as controller: + resp = controller.COPY(req) + self.assertEquals(resp.status_int, 201) + self.assertEquals(resp.headers['x-copied-from'], 'c/o/o2') + self.assertEquals(resp.headers['x-copied-from-account'], 'a') + def test_COPY_destination_leading_slash(self): req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'COPY'}, @@ -3069,6 +3279,19 @@ class TestObjectController(unittest.TestCase): self.assertEquals(resp.status_int, 201) self.assertEquals(resp.headers['x-copied-from'], 'c/o') + def test_COPY_account_destination_leading_slash(self): + req = Request.blank('/v1/a/c/o', + environ={'REQUEST_METHOD': 'COPY'}, + headers={'Destination': '/c1/o', + 'Destination-Account': 'a1'}) + status_list = (200, 200, 200, 200, 200, 200, 200, 201, 201, 201) + # acct cont acct cont objc objc objc obj obj obj + with self.controller_context(req, *status_list) as controller: + resp = controller.COPY(req) + self.assertEquals(resp.status_int, 201) + self.assertEquals(resp.headers['x-copied-from'], 'c/o') + self.assertEquals(resp.headers['x-copied-from-account'], 'a') + def test_COPY_source_with_slashes_destination_leading_slash(self): req = Request.blank('/v1/a/c/o/o2', environ={'REQUEST_METHOD': 'COPY'}, @@ -3080,14 +3303,35 @@ class TestObjectController(unittest.TestCase): self.assertEquals(resp.status_int, 201) self.assertEquals(resp.headers['x-copied-from'], 'c/o/o2') + def test_COPY_account_source_with_slashes_destination_leading_slash(self): + req = Request.blank('/v1/a/c/o/o2', + environ={'REQUEST_METHOD': 'COPY'}, + headers={'Destination': '/c1/o', + 'Destination-Account': 'a1'}) + status_list = (200, 200, 200, 200, 200, 200, 200, 201, 201, 201) + # acct cont acct cont objc objc objc obj obj obj + with self.controller_context(req, *status_list) as controller: + resp = controller.COPY(req) + self.assertEquals(resp.status_int, 201) + self.assertEquals(resp.headers['x-copied-from'], 'c/o/o2') + self.assertEquals(resp.headers['x-copied-from-account'], 'a') + def test_COPY_no_object_in_destination(self): req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'COPY'}, headers={'Destination': 'c_o'}) status_list = [] # no requests needed with self.controller_context(req, *status_list) as controller: - resp = controller.COPY(req) - self.assertEquals(resp.status_int, 412) + self.assertRaises(HTTPException, controller.COPY, req) + + def test_COPY_account_no_object_in_destination(self): + req = Request.blank('/v1/a/c/o', + environ={'REQUEST_METHOD': 'COPY'}, + headers={'Destination': 'c_o', + 'Destination-Account': 'a1'}) + status_list = [] # no requests needed + with self.controller_context(req, *status_list) as controller: + self.assertRaises(HTTPException, controller.COPY, req) def test_COPY_server_error_reading_source(self): req = Request.blank('/v1/a/c/o', @@ -3099,6 +3343,17 @@ class TestObjectController(unittest.TestCase): resp = controller.COPY(req) self.assertEquals(resp.status_int, 503) + def test_COPY_account_server_error_reading_source(self): + req = Request.blank('/v1/a/c/o', + environ={'REQUEST_METHOD': 'COPY'}, + headers={'Destination': '/c1/o', + 'Destination-Account': 'a1'}) + status_list = (200, 200, 200, 200, 503, 503, 503) + # acct cont acct cont objc objc objc + with self.controller_context(req, *status_list) as controller: + resp = controller.COPY(req) + self.assertEquals(resp.status_int, 503) + def test_COPY_not_found_reading_source(self): req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'COPY'}, @@ -3109,6 +3364,17 @@ class TestObjectController(unittest.TestCase): resp = controller.COPY(req) self.assertEquals(resp.status_int, 404) + def test_COPY_account_not_found_reading_source(self): + req = Request.blank('/v1/a/c/o', + environ={'REQUEST_METHOD': 'COPY'}, + headers={'Destination': '/c1/o', + 'Destination-Account': 'a1'}) + status_list = (200, 200, 200, 200, 404, 404, 404) + # acct cont acct cont objc objc objc + with self.controller_context(req, *status_list) as controller: + resp = controller.COPY(req) + self.assertEquals(resp.status_int, 404) + def test_COPY_with_some_missing_sources(self): req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'COPY'}, @@ -3119,6 +3385,17 @@ class TestObjectController(unittest.TestCase): resp = controller.COPY(req) self.assertEquals(resp.status_int, 201) + def test_COPY_account_with_some_missing_sources(self): + req = Request.blank('/v1/a/c/o', + environ={'REQUEST_METHOD': 'COPY'}, + headers={'Destination': '/c1/o', + 'Destination-Account': 'a1'}) + status_list = (200, 200, 200, 200, 404, 404, 200, 201, 201, 201) + # acct cont acct cont objc objc objc obj obj obj + with self.controller_context(req, *status_list) as controller: + resp = controller.COPY(req) + self.assertEquals(resp.status_int, 201) + def test_COPY_with_metadata(self): req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'COPY'}, @@ -3134,6 +3411,22 @@ class TestObjectController(unittest.TestCase): self.assertEquals(resp.headers.get('x-object-meta-ours'), 'okay') self.assertEquals(resp.headers.get('x-delete-at'), '9876543210') + def test_COPY_account_with_metadata(self): + req = Request.blank('/v1/a/c/o', + environ={'REQUEST_METHOD': 'COPY'}, + headers={'Destination': '/c1/o', + 'X-Object-Meta-Ours': 'okay', + 'Destination-Account': 'a1'}) + status_list = (200, 200, 200, 200, 200, 200, 200, 201, 201, 201) + # acct cont acct cont objc objc objc obj obj obj + with self.controller_context(req, *status_list) as controller: + resp = controller.COPY(req) + self.assertEquals(resp.status_int, 201) + self.assertEquals(resp.headers.get('x-object-meta-test'), + 'testing') + self.assertEquals(resp.headers.get('x-object-meta-ours'), 'okay') + self.assertEquals(resp.headers.get('x-delete-at'), '9876543210') + def test_COPY_source_larger_than_max_file_size(self): req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'COPY'}, @@ -3156,6 +3449,29 @@ class TestObjectController(unittest.TestCase): resp = controller.COPY(req) self.assertEquals(resp.status_int, 413) + def test_COPY_account_source_larger_than_max_file_size(self): + req = Request.blank('/v1/a/c/o', + environ={'REQUEST_METHOD': 'COPY'}, + headers={'Destination': '/c1/o', + 'Destination-Account': 'a1'}) + + class LargeResponseBody(object): + + def __len__(self): + return constraints.MAX_FILE_SIZE + 1 + + def __getitem__(self, key): + return '' + + copy_from_obj_body = LargeResponseBody() + status_list = (200, 200, 200, 200, 200) + # acct cont objc objc objc + kwargs = dict(body=copy_from_obj_body) + with self.controller_context(req, *status_list, + **kwargs) as controller: + resp = controller.COPY(req) + self.assertEquals(resp.status_int, 413) + def test_COPY_newest(self): with save_globals(): controller = proxy_server.ObjectController(self.app, 'a', 'c', 'o') @@ -3174,6 +3490,25 @@ class TestObjectController(unittest.TestCase): self.assertEquals(resp.headers['x-copied-from-last-modified'], '3') + def test_COPY_account_newest(self): + with save_globals(): + controller = proxy_server.ObjectController(self.app, 'a', 'c', 'o') + req = Request.blank('/v1/a/c/o', + environ={'REQUEST_METHOD': 'COPY'}, + headers={'Destination': '/c1/o', + 'Destination-Account': 'a1'}) + req.account = 'a' + controller.object_name = 'o' + set_http_connect(200, 200, 200, 200, 200, 200, 200, 201, 201, 201, + #act cont acct cont objc objc objc obj obj obj + timestamps=('1', '1', '1', '1', '3', '2', '1', + '4', '4', '4')) + self.app.memcache.store = {} + resp = controller.COPY(req) + self.assertEquals(resp.status_int, 201) + self.assertEquals(resp.headers['x-copied-from-last-modified'], + '3') + def test_COPY_delete_at(self): with save_globals(): given_headers = {} @@ -3199,6 +3534,32 @@ class TestObjectController(unittest.TestCase): self.assertTrue('X-Delete-At-Partition' in given_headers) self.assertTrue('X-Delete-At-Container' in given_headers) + def test_COPY_account_delete_at(self): + with save_globals(): + given_headers = {} + + def fake_connect_put_node(nodes, part, path, headers, + logger_thread_locals): + given_headers.update(headers) + + controller = proxy_server.ObjectController(self.app, 'a', + 'c', 'o') + controller._connect_put_node = fake_connect_put_node + set_http_connect(200, 200, 200, 200, 200, 200, 200, 201, 201, 201) + self.app.memcache.store = {} + req = Request.blank('/v1/a/c/o', + environ={'REQUEST_METHOD': 'COPY'}, + headers={'Destination': '/c1/o', + 'Destination-Account': 'a1'}) + + self.app.update_request(req) + controller.COPY(req) + self.assertEquals(given_headers.get('X-Delete-At'), '9876543210') + self.assertTrue('X-Delete-At-Host' in given_headers) + self.assertTrue('X-Delete-At-Device' in given_headers) + self.assertTrue('X-Delete-At-Partition' in given_headers) + self.assertTrue('X-Delete-At-Container' in given_headers) + def test_chunked_put(self): class ChunkedFile(object):