Merge "account to account copy implementation"
This commit is contained in:
commit
8329095f11
@ -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 '
|
||||
'<container name>/<object name>')
|
||||
|
||||
|
||||
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 '
|
||||
'<container name>/<object name>')
|
||||
|
||||
|
||||
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'
|
||||
'<container name>/<object name>')
|
||||
body='Account name cannot contain slashes')
|
||||
return account
|
||||
|
@ -41,7 +41,8 @@ from swift.common.utils import (
|
||||
normalize_delete_at_timestamp, public, quorum_size, get_expirer_container)
|
||||
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, \
|
||||
@ -600,11 +601,14 @@ 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_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()
|
||||
|
||||
@ -614,8 +618,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)
|
||||
@ -633,6 +639,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:
|
||||
@ -647,6 +654,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']
|
||||
@ -771,8 +780,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']
|
||||
@ -893,27 +903,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 '
|
||||
'<container name>/<object name>')
|
||||
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']
|
||||
|
@ -357,7 +357,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:
|
||||
|
@ -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 = {}
|
||||
|
@ -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
|
||||
@ -246,6 +247,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
|
||||
|
@ -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'})
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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)])
|
||||
|
||||
@ -2909,6 +2935,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',
|
||||
@ -2920,6 +2959,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',
|
||||
@ -2930,6 +2982,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'},
|
||||
@ -2942,6 +3005,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'},
|
||||
@ -2954,6 +3031,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'},
|
||||
@ -2966,6 +3057,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',
|
||||
@ -2977,6 +3082,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',
|
||||
@ -2992,6 +3110,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',
|
||||
@ -3002,6 +3136,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',
|
||||
@ -3013,6 +3158,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',
|
||||
@ -3023,6 +3180,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',
|
||||
@ -3038,6 +3206,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',
|
||||
@ -3075,6 +3259,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'},
|
||||
@ -3097,6 +3294,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'},
|
||||
@ -3108,6 +3318,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'},
|
||||
@ -3119,14 +3342,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',
|
||||
@ -3138,6 +3382,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'},
|
||||
@ -3148,6 +3403,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'},
|
||||
@ -3158,6 +3424,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'},
|
||||
@ -3173,6 +3450,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'},
|
||||
@ -3195,6 +3488,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')
|
||||
@ -3213,6 +3529,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 = {}
|
||||
@ -3238,6 +3573,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):
|
||||
|
Loading…
Reference in New Issue
Block a user