diff --git a/oss2/__init__.py b/oss2/__init__.py index 2bf125c7..03d8c35b 100644 --- a/oss2/__init__.py +++ b/oss2/__init__.py @@ -1,4 +1,4 @@ -__version__ = '2.6.1' +__version__ = '2.6.2' from . import models, exceptions diff --git a/oss2/api.py b/oss2/api.py index b5898f13..d696d549 100644 --- a/oss2/api.py +++ b/oss2/api.py @@ -349,6 +349,7 @@ class Bucket(_Base): STAT = 'stat' BUCKET_INFO = 'bucketInfo' PROCESS = 'x-oss-process' + TAGGING = 'tagging' def __init__(self, auth, endpoint, bucket_name, is_cname=False, @@ -1595,6 +1596,50 @@ def process_object(self, key, process): resp = self.__do_object('POST', key, params={Bucket.PROCESS: ''}, data=process_data) logger.debug("Process object done, req_id: {0}, status_code: {1}".format(resp.request_id, resp.status)) return ProcessObjectResult(resp) + + def put_object_tagging(self, key, tagging, headers=None): + """ + + :param str key: 上传tagging的对象名称,不能为空。 + + :param tagging: tag 标签内容 + :type tagging: :class:`ObjectTagging ` 对象 + + :return: :class:`RequestResult ` + """ + logger.debug("Start to put object tagging, bucket: {0}, key: {1}, tagging: {2}".format( + self.bucket_name, to_string(key), tagging)) + + if headers is not None: + headers = http.CaseInsensitiveDict(headers) + + data = self.__convert_data(ObjectTagging, xml_utils.to_put_object_tagging, tagging) + resp = self.__do_object('PUT', key, data=data, params={Bucket.TAGGING: ''}, headers=headers) + + return RequestResult(resp) + + def get_object_tagging(self, key): + + """ + :param str key: 要获取tagging的对象名称 + :return: :class:`ObjectTagging ` + """ + logger.debug("Start to get object tagging, bucket: {0}, key: {1}".format( + self.bucket_name, to_string(key))) + resp = self.__do_object('GET', key, params={Bucket.TAGGING: ''}) + + return self._parse_result(resp, xml_utils.parse_get_object_tagging, GetObjectTaggingResult) + + def delete_object_tagging(self, key): + """ + :param str key: 要删除tagging的对象名称 + :return: :class:`RequestResult ` + """ + logger.debug("Start to delete object tagging, bucket: {0}, key: {1}".format( + self.bucket_name, to_string(key))) + resp = self.__do_object('DELETE', key, params={Bucket.TAGGING: ''}) + logger.debug("Delete object tagging done, req_id: {0}, status_code: {1}".format(resp.request_id, resp.status)) + return RequestResult(resp) def _get_bucket_config(self, config): """获得Bucket某项配置,具体哪种配置由 `config` 指定。该接口直接返回 `RequestResult` 对象。 diff --git a/oss2/auth.py b/oss2/auth.py index f56c83bb..aaf6fe13 100644 --- a/oss2/auth.py +++ b/oss2/auth.py @@ -72,7 +72,7 @@ class Auth(AuthBase): 'response-expires', 'response-content-disposition', 'cors', 'lifecycle', 'restore', 'qos', 'referer', 'stat', 'bucketInfo', 'append', 'position', 'security-token', 'live', 'comp', 'status', 'vod', 'startTime', 'endTime', 'x-oss-process', - 'symlink', 'callback', 'callback-var'] + 'symlink', 'callback', 'callback-var', 'tagging'] ) def _sign_request(self, req, bucket_name, key): diff --git a/oss2/headers.py b/oss2/headers.py index 5ddf76ca..6de18b3c 100644 --- a/oss2/headers.py +++ b/oss2/headers.py @@ -30,6 +30,9 @@ OSS_SERVER_SIDE_ENCRYPTION = "x-oss-server-side-encryption" OSS_SERVER_SIDE_ENCRYPTION_KEY_ID = "x-oss-server-side-encryption-key-id" +OSS_OBJECT_TAGGING = "x-oss-tagging" +OSS_OBJECT_TAGGING_COPY_DIRECTIVE = "x-oss-tagging-directive" + class RequestHeader(dict): def __init__(self, *arg, **kw): diff --git a/oss2/models.py b/oss2/models.py index aab16790..2b8d62ca 100644 --- a/oss2/models.py +++ b/oss2/models.py @@ -9,7 +9,7 @@ from .utils import http_to_unixtime, make_progress_adapter, make_crc_adapter from .exceptions import ClientError, InconsistentError -from .compat import urlunquote, to_string +from .compat import urlunquote, to_string, urlquote from .select_response import SelectResponseAdapter from .headers import * import json @@ -552,6 +552,10 @@ class LifecycleRule(object): :param expiration: 过期删除操作。 :type expiration: :class:`LifecycleExpiration` :param status: 启用还是禁止该规则。可选值为 `LifecycleRule.ENABLED` 或 `LifecycleRule.DISABLED` + :param storage_transitions: 存储类型转换规则 + :type storage_transitions: :class:`StorageTransition` + :param tagging: object tagging 规则 + :type tagging: :class:`ObjectTagging` """ ENABLED = 'Enabled' @@ -560,13 +564,14 @@ class LifecycleRule(object): def __init__(self, id, prefix, status=ENABLED, expiration=None, abort_multipart_upload=None, - storage_transitions=None): + storage_transitions=None, tagging=None): self.id = id self.prefix = prefix self.status = status self.expiration = expiration self.abort_multipart_upload = abort_multipart_upload self.storage_transitions = storage_transitions + self.tagging = tagging class BucketLifecycle(object): @@ -878,3 +883,70 @@ def __init__(self, resp): self.object = result['object'] if 'status' in result: self.process_status = result['status'] + +_MAX_OBJECT_TAGGING_KEY_LENGTH=128 +_MAX_OBJECT_TAGGING_VALUE_LENGTH=256 + +class ObjectTagging(object): + + def __init__(self, tagging_rules=None): + + self.tag_set = tagging_rules or ObjectTaggingRule() + + def __str__(self): + + tag_str = "" + + tagging_rule = self.tag_set.tagging_rule + + for key in tagging_rule: + tag_str += key + tag_str += ":" + tagging_rule[key] + " " + + return tag_str + +class ObjectTaggingRule(object): + + def __init__(self): + self.tagging_rule = dict() + + def add(self, key, value): + + if key is None or key == '': + raise ClientError("ObjectTagging key should not be empty") + + if len(key) > _MAX_OBJECT_TAGGING_KEY_LENGTH: + raise ClientError("ObjectTagging key is too long") + + if len(value) > _MAX_OBJECT_TAGGING_VALUE_LENGTH: + raise ClientError("ObjectTagging value is too long") + + self.tagging_rule[key] = value + + def delete(self, key): + del self.tagging_rule[key] + + def len(self): + return len(self.tagging_rule) + + def to_query_string(self): + query_string = '' + + for key in self.tagging_rule: + query_string += urlquote(key) + query_string += '=' + query_string += urlquote(self.tagging_rule[key]) + query_string += '&' + + if len(query_string) == 0: + return '' + else: + query_string = query_string[:-1] + + return query_string + +class GetObjectTaggingResult(RequestResult, ObjectTagging): + + def __init__(self, resp): + RequestResult.__init__(self, resp) + ObjectTagging.__init__(self) diff --git a/oss2/xml_utils.py b/oss2/xml_utils.py index 07801e8a..d723d9b7 100644 --- a/oss2/xml_utils.py +++ b/oss2/xml_utils.py @@ -29,7 +29,9 @@ Owner, AccessControlList, AbortMultipartUpload, - StorageTransition) + StorageTransition, + ObjectTagging, + ObjectTaggingRule) from .compat import urlunquote, to_unicode, to_string from .utils import iso8601_to_unixtime, date_to_iso8601, iso8601_to_date @@ -422,21 +424,36 @@ def parse_lifecycle_storage_transitions(storage_transition_nodes): return storage_transitions +def parse_lifecycle_object_taggings(lifecycle_tagging_nodes): + + if lifecycle_tagging_nodes is None: + return ObjectTagging() + + tagging_rule = ObjectTaggingRule() + for tag_node in lifecycle_tagging_nodes: + key = _find_tag(tag_node, 'Key') + value = _find_tag(tag_node, 'Value') + tagging_rule.add(key, value) + + return ObjectTagging(tagging_rule) def parse_get_bucket_lifecycle(result, body): root = ElementTree.fromstring(body) + url_encoded = _is_url_encoding(root) for rule_node in root.findall('Rule'): expiration = parse_lifecycle_expiration(rule_node.find('Expiration')) abort_multipart_upload = parse_lifecycle_abort_multipart_upload(rule_node.find('AbortMultipartUpload')) storage_transitions = parse_lifecycle_storage_transitions(rule_node.findall('Transition')) + tagging = parse_lifecycle_object_taggings(rule_node.findall('Tag')) rule = LifecycleRule( _find_tag(rule_node, 'ID'), _find_tag(rule_node, 'Prefix'), status=_find_tag(rule_node, 'Status'), expiration=expiration, abort_multipart_upload=abort_multipart_upload, - storage_transitions=storage_transitions + storage_transitions=storage_transitions, + tagging=tagging ) result.rules.append(rule) @@ -567,6 +584,13 @@ def to_put_bucket_lifecycle(bucket_lifecycle): _add_text_child(storage_transition_node, 'CreatedBeforeDate', date_to_iso8601(storage_transition.created_before_date)) + tagging = rule.tagging + if tagging: + tagging_rule = tagging.tag_set.tagging_rule + for key in tagging.tag_set.tagging_rule: + tag_node = ElementTree.SubElement(rule_node, 'Tag') + _add_text_child(tag_node, 'Key', key) + _add_text_child(tag_node, 'Value', tagging_rule[key]) return _node_to_string(root) @@ -741,3 +765,32 @@ def to_get_select_json_object_meta(json_meta_param): raise SelectOperationClientError("The json_meta_param contains unsupported key " + key, "") return _node_to_string(root) + +def to_put_object_tagging(object_tagging): + root = ElementTree.Element("Tagging") + tag_set = ElementTree.SubElement(root, "TagSet") + + for item in object_tagging.tag_set.tagging_rule: + tag_xml = ElementTree.SubElement(tag_set, "Tag") + _add_text_child(tag_xml, 'Key', item) + _add_text_child(tag_xml, 'Value', object_tagging.tag_set.tagging_rule[item]) + + return _node_to_string(root) + +def parse_get_object_tagging(result, body): + root = ElementTree.fromstring(body) + url_encoded = _is_url_encoding(root) + tagset_node = root.find('TagSet') + + if tagset_node is None: + return result + + tagging_rules = ObjectTaggingRule() + for tag_node in tagset_node.findall('Tag'): + key = _find_object(tag_node, 'Key', url_encoded) + value = _find_object(tag_node, 'Value', url_encoded) + tagging_rules.add(key, value) + + result.tag_set = tagging_rules + return result + diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/common.py b/tests/common.py index eb39edee..d1ca9dce 100644 --- a/tests/common.py +++ b/tests/common.py @@ -16,7 +16,7 @@ OSS_ID = os.getenv("OSS_TEST_ACCESS_KEY_ID") OSS_SECRET = os.getenv("OSS_TEST_ACCESS_KEY_SECRET") OSS_ENDPOINT = os.getenv("OSS_TEST_ENDPOINT") -OSS_BUCKET = os.getenv("OSS_TEST_BUCKET") +OSS_TEST_BUCKET = os.getenv("OSS_TEST_BUCKET") OSS_CNAME = os.getenv("OSS_TEST_CNAME") OSS_CMK = os.getenv("OSS_TEST_CMK") OSS_REGION = os.getenv("OSS_TEST_REGION", "cn-hangzhou") @@ -30,7 +30,11 @@ def random_string(n): return ''.join(random.choice(string.ascii_lowercase) for i in range(n)) - +OSS_BUCKET = '' +if OSS_TEST_BUCKET is None: + OSS_BUCKET = 'aliyun-oss-python-sdk-'+random_string(10) +else: + OSS_BUCKET = OSS_TEST_BUCKET + random_string(10) def random_bytes(n): return oss2.to_bytes(random_string(n)) @@ -82,7 +86,7 @@ def setUp(self): global OSS_AUTH_VERSION OSS_AUTH_VERSION = os.getenv('OSS_TEST_AUTH_VERSION') - + self.bucket = oss2.Bucket(oss2.make_auth(OSS_ID, OSS_SECRET, OSS_AUTH_VERSION), OSS_ENDPOINT, OSS_BUCKET) try: diff --git a/tests/test_bucket.py b/tests/test_bucket.py index 8f75849b..21765e70 100644 --- a/tests/test_bucket.py +++ b/tests/test_bucket.py @@ -254,6 +254,8 @@ def test_lifecycle_abort_multipart_upload_days(self): self.assertEqual(1, len(result.rules)) self.assertEqual(356, result.rules[0].abort_multipart_upload.days) + self.assertEqual(0, result.rules[0].tagging.tag_set.len()) + self.bucket.delete_bucket_lifecycle() def test_lifecycle_abort_multipart_upload_date(self): @@ -273,6 +275,8 @@ def test_lifecycle_abort_multipart_upload_date(self): self.assertEqual(1, len(result.rules)) self.assertEqual(datetime.date(2016, 12, 20), result.rules[0].abort_multipart_upload.created_before_date) + self.assertEqual(0, result.rules[0].tagging.tag_set.len()) + self.bucket.delete_bucket_lifecycle() def test_lifecycle_storage_transitions_mixed(self): @@ -306,6 +310,8 @@ def test_lifecycle_storage_transitions_days(self): self.assertEqual(1, len(result.rules[0].storage_transitions)) self.assertEqual(356, result.rules[0].storage_transitions[0].days) + self.assertEqual(0, result.rules[0].tagging.tag_set.len()) + self.bucket.delete_bucket_lifecycle() def test_lifecycle_storage_transitions_more_days(self): @@ -325,6 +331,7 @@ def test_lifecycle_storage_transitions_more_days(self): result = self.bucket.get_bucket_lifecycle() self.assertEqual(1, len(result.rules)) self.assertEqual(2, len(result.rules[0].storage_transitions)) + self.assertEqual(0, result.rules[0].tagging.tag_set.len()) if result.rules[0].storage_transitions[0].storage_class == oss2.BUCKET_STORAGE_CLASS_IA: self.assertEqual(355, result.rules[0].storage_transitions[0].days) self.assertEqual(356, result.rules[0].storage_transitions[1].days) @@ -353,8 +360,40 @@ def test_lifecycle_storage_transitions_date(self): self.assertEqual(1, len(result.rules[0].storage_transitions)) self.assertEqual(datetime.date(2016, 12, 20), result.rules[0].storage_transitions[0].created_before_date) + self.assertEqual(0, result.rules[0].tagging.tag_set.len()) + + self.bucket.delete_bucket_lifecycle() + + def test_lifecycle_object_tagging(self): + from oss2.models import LifecycleExpiration, LifecycleRule, BucketLifecycle, StorageTransition, ObjectTagging, ObjectTaggingRule + + rule = LifecycleRule(random_string(10), 'aaaaaaaaaaa/', + status=LifecycleRule.ENABLED, + expiration=LifecycleExpiration(created_before_date=datetime.date(2016, 12, 25))) + rule.storage_transitions = [StorageTransition(created_before_date=datetime.date(2016, 12, 20), + storage_class=oss2.BUCKET_STORAGE_CLASS_IA)] + + tagging_rule = ObjectTaggingRule() + tagging_rule.add('test_key', 'test_value') + tagging = ObjectTagging(tagging_rule) + + rule.tagging = tagging + + lifecycle = BucketLifecycle([rule]) + + self.bucket.put_bucket_lifecycle(lifecycle) + wait_meta_sync() + result = self.bucket.get_bucket_lifecycle() + self.assertEqual(1, len(result.rules)) + self.assertEqual(1, len(result.rules[0].storage_transitions)) + self.assertEqual(datetime.date(2016, 12, 20), result.rules[0].storage_transitions[0].created_before_date) + + self.assertEqual(1, result.rules[0].tagging.tag_set.len()) + self.assertEqual('test_value', result.rules[0].tagging.tag_set.tagging_rule['test_key']) + self.bucket.delete_bucket_lifecycle() + def test_lifecycle_all_without_object_expiration(self): from oss2.models import LifecycleRule, BucketLifecycle, AbortMultipartUpload, StorageTransition @@ -407,6 +446,101 @@ def test_lifecycle_all(self): self.bucket.delete_bucket_lifecycle() + def test_lifecycle_object_tagging_exceptions_wrong_key(self): + + from oss2.models import LifecycleExpiration, LifecycleRule, BucketLifecycle, StorageTransition, ObjectTagging, ObjectTaggingRule + + rule = LifecycleRule(random_string(10), '中文前缀/', + status=LifecycleRule.ENABLED, + expiration=LifecycleExpiration(created_before_date=datetime.date(2016, 12, 25))) + rule.storage_transitions = [StorageTransition(created_before_date=datetime.date(2016, 12, 20), + storage_class=oss2.BUCKET_STORAGE_CLASS_IA)] + + tagging = ObjectTagging() + + tagging.tag_set.tagging_rule[129*'a'] = 'test' + + rule.tagging = tagging + + lifecycle = BucketLifecycle([rule]) + + try: + # do not return error,but the lifecycle rule doesn't take effect + result = self.bucket.put_bucket_lifecycle(lifecycle) + except oss2.exceptions.OssError: + self.assertFalse(True, "put lifecycle with tagging should fail ,but success") + + del tagging.tag_set.tagging_rule[129*'a'] + + tagging.tag_set.tagging_rule['%&'] = 'test' + lifecycle.rules[0].tagging = tagging + try: + # do not return error,but the lifecycle rule doesn't take effect + result = self.bucket.put_bucket_lifecycle(lifecycle) + self.assertFalse(True, "put lifecycle with tagging should fail ,but success") + except oss2.exceptions.OssError: + pass + + def test_lifecycle_object_tagging_exceptions_wrong_value(self): + + from oss2.models import LifecycleExpiration, LifecycleRule, BucketLifecycle, StorageTransition, ObjectTagging, ObjectTaggingRule + + rule = LifecycleRule(random_string(10), '中文前缀/', + status=LifecycleRule.ENABLED, + expiration=LifecycleExpiration(created_before_date=datetime.date(2016, 12, 25))) + rule.storage_transitions = [StorageTransition(created_before_date=datetime.date(2016, 12, 20), + storage_class=oss2.BUCKET_STORAGE_CLASS_IA)] + + tagging = ObjectTagging() + + tagging.tag_set.tagging_rule['test'] = 257*'a' + + rule.tagging = tagging + + lifecycle = BucketLifecycle([rule]) + + try: + # do not return error,but the lifecycle rule doesn't take effect + result = self.bucket.put_bucket_lifecycle(lifecycle) + except oss2.exceptions.OssError: + self.assertFalse(True, "put lifecycle with tagging should fail ,but success") + + tagging.tag_set.tagging_rule['test'] = ')%' + rule.tagging = tagging + lifecycle = BucketLifecycle([rule]) + try: + # do not return error,but the lifecycle rule doesn't take effect + result = self.bucket.put_bucket_lifecycle(lifecycle) + self.assertFalse(True, "put lifecycle with tagging should fail ,but success") + except oss2.exceptions.OssError: + pass + def test_lifecycle_object_tagging_exceptions_too_much_rules(self): + + from oss2.models import LifecycleExpiration, LifecycleRule, BucketLifecycle, StorageTransition, ObjectTagging, ObjectTaggingRule + + rule = LifecycleRule(random_string(10), '中文前缀/', + status=LifecycleRule.ENABLED, + expiration=LifecycleExpiration(created_before_date=datetime.date(2016, 12, 25))) + rule.storage_transitions = [StorageTransition(created_before_date=datetime.date(2016, 12, 20), + storage_class=oss2.BUCKET_STORAGE_CLASS_IA)] + + tagging = ObjectTagging() + for i in range(1, 20): + key='test_key_'+str(i) + value='test_value_'+str(i) + tagging.tag_set.tagging_rule[key]=value + + + rule.tagging = tagging + + lifecycle = BucketLifecycle([rule]) + + try: + # do not return error,but the lifecycle rule doesn't take effect + result = self.bucket.put_bucket_lifecycle(lifecycle) + except oss2.exceptions.OssError: + self.assertFalse(True, "put lifecycle with tagging should fail ,but success") + def test_cors(self): rule = oss2.models.CorsRule(allowed_origins=['*'], allowed_methods=['HEAD', 'GET'], diff --git a/tests/test_mock_bucket.py b/tests/test_mock_bucket.py index 0f012175..81265218 100644 --- a/tests/test_mock_bucket.py +++ b/tests/test_mock_bucket.py @@ -420,6 +420,8 @@ def test_get_lifecycle_date(self, do_request): self.assertEqual(rule.expiration.date, date) self.assertEqual(rule.expiration.days, None) + self.assertEqual(0, rule.tagging.tag_set.len()) + @patch('oss2.Session.do_request') def test_get_lifecycle_days(self, do_request): from oss2.models import LifecycleRule @@ -470,6 +472,8 @@ def test_get_lifecycle_days(self, do_request): self.assertEqual(rule.expiration.date, None) self.assertEqual(rule.expiration.days, days) + self.assertEqual(0, rule.tagging.tag_set.len()) + @patch('oss2.Session.do_request') def test_get_lifecycle_storage_transition(self, do_request): from oss2.models import LifecycleRule @@ -535,6 +539,8 @@ def test_get_lifecycle_storage_transition(self, do_request): self.assertEqual(rule.storage_transitions[2].created_before_date, date1) self.assertEqual(rule.storage_transitions[2].storage_class, oss2.BUCKET_STORAGE_CLASS_ARCHIVE) + self.assertEqual(0, rule.tagging.tag_set.len()) + @patch('oss2.Session.do_request') def test_get_lifecycle_abort_multipart_days(self, do_request): from oss2.models import LifecycleRule @@ -634,6 +640,65 @@ def test_get_lifecycle_abort_multipart_date(self, do_request): self.assertEqual(rule.status, status) self.assertEqual(rule.abort_multipart_upload.created_before_date, date1) + @patch('oss2.Session.do_request') + def test_get_lifecycle_tagging(self, do_request): + from oss2.models import LifecycleRule + + request_text = '''GET /?lifecycle= HTTP/1.1 +Host: ming-oss-share.oss-cn-hangzhou.aliyuncs.com +Accept-Encoding: identity +Connection: keep-alive +date: Sat, 12 Dec 2015 00:35:38 GMT +User-Agent: aliyun-sdk-python/2.0.2(Windows/7/;3.3.3) +Accept: */* +authorization: OSS ZCDmm7TPZKHtx77j:mr0QeREuAcoeK0rSWBnobrzu6uU=''' + + response_text = '''HTTP/1.1 200 OK +Server: AliyunOSS +Date: Sat, 12 Dec 2015 00:35:38 GMT +Content-Type: application/xml +Content-Length: 277 +Connection: keep-alive +x-oss-request-id: 566B6BDA010B7A4314D1614A + + + + + {0} + {1} + {2} + k1v1 + k2v2 + k3v3 + + {3} + + +''' + + id = 'whatever' + prefix = 'lifecycle rule 1' + status = LifecycleRule.DISABLED + date = datetime.date(2015, 12, 25) + + req_info = unittests.common.mock_response(do_request, response_text.format(id, prefix, status, '2015-12-25T00:00:00.000Z')) + result = unittests.common.bucket().get_bucket_lifecycle() + + self.assertRequest(req_info, request_text) + + rule = result.rules[0] + self.assertEqual(rule.id, id) + self.assertEqual(rule.prefix, prefix) + self.assertEqual(rule.status, status) + self.assertEqual(rule.expiration.date, date) + self.assertEqual(rule.expiration.days, None) + + tagging_rule = rule.tagging.tag_set.tagging_rule + self.assertEqual(3, rule.tagging.tag_set.len()) + self.assertEqual('v1', tagging_rule['k1']) + self.assertEqual('v2', tagging_rule['k2']) + self.assertEqual('v3', tagging_rule['k3']) + @patch('oss2.Session.do_request') def test_get_stat(self, do_request): request_text = '''GET /?stat HTTP/1.1 diff --git a/tests/test_mock_object.py b/tests/test_mock_object.py index 55a1b086..871862eb 100644 --- a/tests/test_mock_object.py +++ b/tests/test_mock_object.py @@ -164,6 +164,47 @@ def make_append_object(position, content): return request_text, response_text +def make_get_object_tagging(): + request_text = '''GET /sjbhlsgsbecvlpbf?tagging HTTP/1.1 + Host: ming-oss-share.oss-cn-hangzhou.aliyuncs.com + Accept-Encoding: identity + Connection: keep-alive + date: Sat, 12 Dec 2015 00:35:53 GMT + User-Agent: aliyun-sdk-python/2.0.2(Windows/7/;3.3.3) + Accept: */* + authorization: OSS ZCDmm7TPZKHtx77j:PAedG7U86ZxQ2WTB+GdpSltoiTI=''' + + response_text = '''HTTP/1.1 200 OK + Server: AliyunOSS + Date: Sat, 12 Dec 2015 00:35:53 GMT + Content-Type: text/plain + Content-Length: 278 + Connection: keep-alive + x-oss-request-id: 566B6BE93A7B8CFD53D4BAA3 + Accept-Ranges: bytes + ETag: "D80CF0E5BE2436514894D64B2BCFB2AE" + Last-Modified: Sat, 12 Dec 2015 00:35:53 GMT +x-oss-object-type: Normal + + + + + +k1 +v1 + + +k2 +v2 + + +k3 +v3 + + +''' + + return request_text, response_text class TestObject(unittests.common.OssTestCase): @patch('oss2.Session.do_request') @@ -932,6 +973,23 @@ def test_crypto_operation_exception(self, do_request): 'http://oss-cn-hangzhou.aliyuncs.com', '123', None) + @patch('oss2.Session.do_request') + def test_get_object_tagging(self, do_request): + + request_text, response_text = make_get_object_tagging() + + req_info = unittests.common.mock_response(do_request, response_text) + + result = unittests.common.bucket().get_object_tagging('sjbhlsgsbecvlpbf') + + req_info = unittests.common.mock_response(do_request, response_text) + + self.assertEqual(3, result.tag_set.len()) + self.assertEqual('v1', result.tag_set.tagging_rule['k1']) + self.assertEqual('v2', result.tag_set.tagging_rule['k2']) + self.assertEqual('v3', result.tag_set.tagging_rule['k3']) + + # for ci def test_oss_utils_negative(self): try: diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 3bf840d0..b620af89 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -5,6 +5,8 @@ from oss2.utils import calc_obj_crc_from_parts from common import * +from oss2.headers import OSS_OBJECT_TAGGING, OSS_OBJECT_TAGGING_COPY_DIRECTIVE +from oss2.compat import urlunquote, urlquote class TestMultipart(OssTestCase): @@ -97,6 +99,113 @@ def test_upload_part_copy(self): self.assertEqual(len(content_got), len(content)) self.assertEqual(content_got, content) + def test_init_multipart_with_object_tagging_exceptions(self): + + key = self.random_key() + + headers=dict() + # wrong key + tag_str='=a&b=a' + headers[OSS_OBJECT_TAGGING] = tag_str + try: + resp = self.bucket.init_multipart_upload(key, headers=headers) + self.assertFalse(True, 'should get a exception') + except oss2.exceptions.OssError: + pass + + # wrong key + long_str=129*'a' + tag_str=long_str+'=b&b=a' + headers[OSS_OBJECT_TAGGING] = tag_str + try: + resp = self.bucket.init_multipart_upload(key, headers=headers) + self.assertFalse(True, 'should get a exception') + except oss2.exceptions.OssError: + pass + + + # wrong value + tag_str='a=&b=c' + headers[OSS_OBJECT_TAGGING] = tag_str + try: + resp = self.bucket.init_multipart_upload(key, headers=headers) + except oss2.exceptions.OssError: + self.assertFalse(True, 'should get a exception') + + + # wrong value + long_str=257*'a' + tag_str = 'a='+long_str+'&b=a' + headers[OSS_OBJECT_TAGGING] = tag_str + try: + resp = self.bucket.init_multipart_upload(key, headers=headers) + self.assertFalse(True, 'should get a exception') + except oss2.exceptions.OssError: + pass + + + # dup kv + tag_str='a=b&a=b&a=b' + headers[OSS_OBJECT_TAGGING] = tag_str + try: + resp = self.bucket.init_multipart_upload(key, headers=headers) + self.assertFalse(True, 'should get a exception') + except oss2.exceptions.OssError: + pass + + + # max+1 kv pairs + tag_str='a1=b1&a2=b2&a3=b4&a4=b4&a5=b5&a6=b6&a7=b7&a8=b8&a9=b9&a10=b10&a11=b11&a12=b12' + headers[OSS_OBJECT_TAGGING] = tag_str + try: + resp = self.bucket.init_multipart_upload(key, headers=headers) + self.assertFalse(True, 'should get a exception') + except oss2.exceptions.OssError: + pass + + def test_multipart_with_object_tagging(self): + + key = self.random_key() + content = random_bytes(128 * 1024) + + tag_str='' + + tag_key1=urlquote('+:/') + tag_value1=urlquote('.-') + tag_str = tag_key1+'='+tag_value1 + + tag_ke2=urlquote(' + ') + tag_value2=urlquote(u'中文'.encode('UTF-8')) + tag_str += '&'+tag_ke2+'='+tag_value2 + + headers=dict() + headers[OSS_OBJECT_TAGGING] = tag_str + + parts = [] + upload_id = self.bucket.init_multipart_upload(key, headers=headers).upload_id + + headers = {'Content-Md5': oss2.utils.content_md5(content)} + + result = self.bucket.upload_part(key, upload_id, 1, content, headers=headers) + parts.append(oss2.models.PartInfo(1, result.etag, size=len(content), part_crc=result.crc)) + self.assertTrue(result.crc is not None) + + complete_result = self.bucket.complete_multipart_upload(key, upload_id, parts) + + object_crc = calc_obj_crc_from_parts(parts) + self.assertTrue(complete_result.crc is not None) + self.assertEqual(object_crc, result.crc) + + result = self.bucket.get_object(key) + self.assertEqual(content, result.read()) + + result = self.bucket.get_object_tagging(key) + + self.assertEqual(2, result.tag_set.len()) + self.assertEqual('.-', result.tag_set.tagging_rule['+:/']) + self.assertEqual('中文', result.tag_set.tagging_rule[' + ']) + + result = self.bucket.delete_object_tagging(key) if __name__ == '__main__': unittest.main() diff --git a/tests/test_object.py b/tests/test_object.py index 31d22417..1821371c 100644 --- a/tests/test_object.py +++ b/tests/test_object.py @@ -7,9 +7,13 @@ import base64 from oss2.exceptions import (ClientError, RequestError, NoSuchBucket, OpenApiServerError, - NotFound, NoSuchKey, Conflict, PositionNotEqualToLength, ObjectNotAppendable) + NotFound, NoSuchKey, Conflict, PositionNotEqualToLength, ObjectNotAppendable) from oss2.compat import is_py2, is_py33 +from oss2.models import ObjectTagging, ObjectTaggingRule +from oss2.headers import OSS_OBJECT_TAGGING, OSS_OBJECT_TAGGING_COPY_DIRECTIVE +from oss2.compat import urlunquote, urlquote + from common import * @@ -208,6 +212,9 @@ def test_file_empty(self): self.assertTrue(filecmp.cmp(input_filename, output_filename)) + os.remove(input_filename) + os.remove(output_filename) + def test_streaming(self): src_key = self.random_key('.src') dst_key = self.random_key('.dst') @@ -887,7 +894,423 @@ def test_process_object(self): self.assertEqual(result.object, dest_key) result = self.bucket.object_exists(dest_key) self.assertEqual(result, True) + + def test_object_tagging_client_error(self): + + rule = ObjectTaggingRule() + self.assertRaises(oss2.exceptions.ClientError, rule.add, 129*'a', 'test') + self.assertRaises(oss2.exceptions.ClientError, rule.add, 'test', 257*'a') + self.assertRaises(oss2.exceptions.ClientError, rule.add, None, 'test') + self.assertRaises(oss2.exceptions.ClientError, rule.add, '', 'test') + self.assertRaises(KeyError, rule.delete, 'not_exist') + + def test_object_tagging_wrong_key(self): + + tagging = ObjectTagging() + tagging.tag_set.tagging_rule[129*'a'] = 'test' + + key = self.random_key('.dat') + result = self.bucket.put_object(key, "test") + + try: + result = self.bucket.put_object_tagging(key, tagging) + self.assertFalse(True, 'should get exception') + except oss2.exceptions.OssError: + pass + + tagging.tag_set.delete(129*'a') + + self.assertTrue( 129*'a' not in tagging.tag_set.tagging_rule ) + + tagging.tag_set.tagging_rule['%@abc'] = 'abc' + + try: + result = self.bucket.put_object_tagging(key, tagging) + self.assertFalse(True, 'should get exception') + except oss2.exceptions.OssError: + pass + + tagging.tag_set.delete('%@abc') + + self.assertTrue( '%@abc' not in tagging.tag_set.tagging_rule ) + + tagging.tag_set.tagging_rule[''] = 'abc' + + try: + result = self.bucket.put_object_tagging(key, tagging) + self.assertFalse(True, 'should get exception') + except oss2.exceptions.OssError: + pass + + + def test_object_tagging_wrong_value(self): + + tagging = ObjectTagging() + + tagging.tag_set.tagging_rule['test'] = 257*'a' + + key = self.random_key('.dat') + + result = self.bucket.put_object(key, "test") + + try: + result = self.bucket.put_object_tagging(key, tagging) + self.assertFalse(True, 'should get exception') + except oss2.exceptions.OssError: + pass + + tagging.tag_set.tagging_rule['test']= '%abc' + + try: + result = self.bucket.put_object_tagging(key, tagging) + self.assertFalse(True, 'should get exception') + except oss2.exceptions.OssError: + pass + + tagging.tag_set.tagging_rule['test']= '' + + try: + result = self.bucket.put_object_tagging(key, tagging) + except oss2.exceptions.OssError: + self.assertFalse(True, 'should get exception') + + def test_object_tagging_wrong_rule_num(self): + + key = self.random_key('.dat') + result = self.bucket.put_object(key, "test") + + tagging = ObjectTagging(None) + for i in range(0,12): + key='test_'+str(i) + value='test_'+str(i) + tagging.tag_set.add(key, value) + + try: + result = self.bucket.put_object_tagging(key, tagging) + self.assertFalse(True, 'should get exception') + except oss2.exceptions.OssError: + pass + + def test_object_tagging(self): + + key = self.random_key('.dat') + result = self.bucket.put_object(key, "test") + + try: + result=self.bucket.get_object_tagging(key) + self.assertEqual(0, result.tag_set.len()) + except oss2.exceptions.OssError: + self.assertFalse(True, "should get exception") + + rule = ObjectTaggingRule() + key1=128*'a' + value1=256*'a' + rule.add(key1, value1) + + key2='+-.:' + value2='_/' + rule.add(key2, value2) + + tagging = ObjectTagging(rule) + result = self.bucket.put_object_tagging(key, tagging) + self.assertTrue(200, result.status) + + result = self.bucket.get_object_tagging(key) + + self.assertEqual(2, result.tag_set.len()) + self.assertEqual(256*'a', result.tag_set.tagging_rule[128*'a']) + self.assertEqual('_/', result.tag_set.tagging_rule['+-.:']) + + def test_put_object_with_tagging(self): + + key = self.random_key('.dat') + headers = dict() + headers[OSS_OBJECT_TAGGING] = "k1=v1&k2=v2&k3=v3" + + result = self.bucket.put_object(key, 'test', headers=headers) + self.assertEqual(200, result.status) + + result = self.bucket.get_object_tagging(key) + self.assertEqual(3, result.tag_set.len()) + self.assertEqual('v1', result.tag_set.tagging_rule['k1']) + self.assertEqual('v2', result.tag_set.tagging_rule['k2']) + self.assertEqual('v3', result.tag_set.tagging_rule['k3']) + + result = self.bucket.delete_object_tagging(key) + + self.assertEqual(204, result.status) + + result = self.bucket.get_object_tagging(key) + self.assertEqual(0, result.tag_set.len()) + + def test_copy_object_with_tagging(self): + + #key = self.random_key('.dat') + key = 'aaaaaaaaaaaaaa' + headers = dict() + headers[OSS_OBJECT_TAGGING] = "k1=v1&k2=v2&k3=v3" + + result = self.bucket.put_object(key, 'test', headers=headers) + self.assertEqual(200, result.status) + + result = self.bucket.get_object_tagging(key) + self.assertEqual(3, result.tag_set.len()) + self.assertEqual('v1', result.tag_set.tagging_rule['k1']) + self.assertEqual('v2', result.tag_set.tagging_rule['k2']) + self.assertEqual('v3', result.tag_set.tagging_rule['k3']) + + headers=dict() + headers[OSS_OBJECT_TAGGING_COPY_DIRECTIVE] = 'COPY' + result = self.bucket.copy_object(self.bucket.bucket_name, key, key+'_test', headers=headers) + + result = self.bucket.get_object_tagging(key+'_test') + self.assertEqual(3, result.tag_set.len()) + self.assertEqual('v1', result.tag_set.tagging_rule['k1']) + self.assertEqual('v2', result.tag_set.tagging_rule['k2']) + self.assertEqual('v3', result.tag_set.tagging_rule['k3']) + + tag_key1 = u' +/ ' + tag_value1 = u'中文' + tag_str = urlquote(tag_key1.encode('UTF-8')) + '=' + urlquote(tag_value1.encode('UTF-8')) + + tag_key2 = u'中文' + tag_value2 = u'test++/' + tag_str += '&' + urlquote(tag_key2.encode('UTF-8')) + '=' + urlquote(tag_value2.encode('UTF-8')) + + headers[OSS_OBJECT_TAGGING] = tag_str + headers[OSS_OBJECT_TAGGING_COPY_DIRECTIVE] = 'REPLACE' + result = self.bucket.copy_object(self.bucket.bucket_name, key, key+'_test', headers=headers) + + result = self.bucket.get_object_tagging(key+'_test') + self.assertEqual(2, result.tag_set.len()) + self.assertEqual('中文', result.tag_set.tagging_rule[' +/ ']) + self.assertEqual('test++/', result.tag_set.tagging_rule['中文']) + + def test_append_object_with_tagging(self): + key = self.random_key() + content1 = random_bytes(512) + content2 = random_bytes(128) + + result = self.bucket.append_object(key, 0, content1, init_crc=0) + self.assertEqual(result.next_position, len(content1)) + self.assertTrue(result.crc is not None) + + try: + self.bucket.append_object(key, 0, content2) + except PositionNotEqualToLength as e: + self.assertEqual(e.next_position, len(content1)) + else: + self.assertTrue(False) + + result = self.bucket.append_object(key, len(content1), content2, init_crc=result.crc) + self.assertEqual(result.next_position, len(content1) + len(content2)) + self.assertTrue(result.crc is not None) + + self.bucket.delete_object(key) + + rule = ObjectTaggingRule() + self.assertEqual('', rule.to_query_string()) + + rule.add('key1', 'value1') + self.assertEqual(rule.to_query_string(), 'key1=value1') + + rule.add(128*'a', 256*'b') + rule.add('+-/', ':+:') + self.assertEqual(rule.to_query_string(), 128*'a' + '=' + 256*'b' + '&%2B-/=%3A%2B%3A&key1=value1') + + headers = dict() + headers[OSS_OBJECT_TAGGING] = rule.to_query_string() + + result = self.bucket.append_object(key, 0, content1, init_crc=0, headers=headers) + self.assertEqual(result.next_position, len(content1)) + self.assertTrue(result.crc is not None) + + result = self.bucket.append_object(key, len(content1), content2, init_crc=result.crc) + self.assertEqual(result.next_position, len(content1) + len(content2)) + self.assertTrue(result.crc is not None) + + result = self.bucket.get_object_tagging(key) + self.assertEqual(3, result.tag_set.len()) + + tagging_rule = result.tag_set.tagging_rule + self.assertEqual('value1', tagging_rule['key1']) + self.assertEqual(256*'b', tagging_rule[128*'a']) + self.assertEqual(':+:', tagging_rule['+-/']) + + def test_append_object_with_tagging_wrong_num(self): + key = self.random_key() + content1 = random_bytes(512) + content2 = random_bytes(128) + + result = self.bucket.append_object(key, 0, content1, init_crc=0) + self.assertEqual(result.next_position, len(content1)) + self.assertTrue(result.crc is not None) + + try: + self.bucket.append_object(key, 0, content2) + except PositionNotEqualToLength as e: + self.assertEqual(e.next_position, len(content1)) + else: + self.assertTrue(False) + + result = self.bucket.append_object(key, len(content1), content2, init_crc=result.crc) + self.assertEqual(result.next_position, len(content1) + len(content2)) + self.assertTrue(result.crc is not None) + + self.bucket.delete_object(key) + + # append object with wrong tagging kv num, but not in + # first call, it will be ignored + rule = ObjectTaggingRule() + self.assertEqual('', rule.to_query_string()) + + for i in range(0, 15): + tag_key = 'key' + str(i) + tag_value = 'value' + str(i) + rule.add(tag_key, tag_value) + + headers = dict() + headers[OSS_OBJECT_TAGGING] = rule.to_query_string() + + self.assertEqual(rule.to_query_string(), 'key9=value9&key8=value8&key3=value3&' + + 'key2=value2&key1=value1&key0=value0&key7=value7&key6=value6&key5=value5&' + + 'key4=value4&key14=value14&key13=value13&key12=value12&key11=value11&key10=value10') + + result = self.bucket.append_object(key, 0, content1, init_crc=0) + self.assertEqual(result.next_position, len(content1)) + self.assertTrue(result.crc is not None) + + result = self.bucket.append_object(key, len(content1), content2, init_crc=result.crc, headers=headers) + + result_tagging = self.bucket.get_object_tagging(key) + self.assertEqual(0, result_tagging.tag_set.len()) + + rule.delete('key1') + rule.delete('key2') + rule.delete('key3') + rule.delete('key4') + rule.delete('key5') + rule.delete('key6') + + self.assertEqual(9, rule.len()) + + headers[OSS_OBJECT_TAGGING] = rule.to_query_string() + + try: + result = self.bucket.append_object(key, len(content1)+len(content2), + content2, init_crc=result.crc, headers=headers) + except oss2.exceptions.OssError: + self.assertFalse(True, 'should not get exception') + + result = self.bucket.get_object_tagging(key) + self.assertEqual(0, result.tag_set.len()) + + self.bucket.delete_object(key) + + wait_meta_sync() + + # append object with wrong tagging kv num in first call, + # it will be fail + rule = ObjectTaggingRule() + self.assertEqual('', rule.to_query_string()) + + for i in range(0, 15): + tag_key = 'key' + str(i) + tag_value = 'value' + str(i) + rule.add(tag_key, tag_value) + + headers = dict() + headers[OSS_OBJECT_TAGGING] = rule.to_query_string() + + try: + self.bucket.append_object(key, 0, content1, init_crc=0, headers=headers) + self.assertFalse(True, 'should get exception') + except oss2.exceptions.OssError: + pass + def test_put_symlink_with_tagging(self): + key = self.random_key() + symlink = self.random_key() + content = 'hello' + + self.bucket.put_object(key, content) + + rule = ObjectTaggingRule() + self.assertEqual('', rule.to_query_string()) + + rule.add('key1', 'value1') + self.assertTrue(rule.to_query_string() != '') + + rule.add(128*'a', 256*'b') + rule.add('+-/', ':+:') + + headers = dict() + headers[OSS_OBJECT_TAGGING] = rule.to_query_string() + + # put symlink normal + self.bucket.put_symlink(key, symlink, headers=headers) + + result = self.bucket.get_object_tagging(symlink) + self.assertEqual(3, result.tag_set.len()) + + tagging_rule = result.tag_set.tagging_rule + self.assertEqual('value1', tagging_rule['key1']) + self.assertEqual(256*'b', tagging_rule[128*'a']) + self.assertEqual(':+:', tagging_rule['+-/']) + + result = self.bucket.delete_object(symlink) + self.assertEqual(2, int(result.status)/100) + + def test_put_symlink_with_tagging_with_wrong_num(self): + key = self.random_key() + symlink = self.random_key() + content = 'hello' + self.bucket.put_object(key, content) + + rule = ObjectTaggingRule() + self.assertEqual('', rule.to_query_string()) + + for i in range(0, 15): + tag_key = 'key' + str(i) + tag_value = 'value' + str(i) + rule.add(tag_key, tag_value) + + headers = dict() + headers[OSS_OBJECT_TAGGING] = rule.to_query_string() + + try: + self.bucket.put_symlink(key, symlink, headers=headers) + self.assertFalse(True, 'should get exception') + except: + pass + + rule.delete('key1') + rule.delete('key2') + rule.delete('key3') + rule.delete('key4') + rule.delete('key5') + rule.delete('key6') + + headers[OSS_OBJECT_TAGGING] = rule.to_query_string() + + try: + result = self.bucket.put_symlink(key, symlink, headers=headers) + except: + self.assertFalse(True, 'should not get exception') + + head_result = self.bucket.head_object(symlink) + self.assertEqual(head_result.content_length, len(content)) + self.assertEqual(head_result.etag, '5D41402ABC4B2A76B9719D911017C592') + + # put symlink with meta + self.bucket.put_symlink(key, symlink, headers={'x-oss-meta-key1': 'value1', + 'x-oss-meta-KEY2': 'value2'}) + + head_result = self.bucket.head_object(symlink) + self.assertEqual(head_result.content_length, len(content)) + self.assertEqual(head_result.etag, '5D41402ABC4B2A76B9719D911017C592') + self.assertEqual(head_result.headers['x-oss-meta-key1'], 'value1') + self.assertEqual(head_result.headers['x-oss-meta-key2'], 'value2') class TestSign(TestObject): """ diff --git a/tests/test_upload.py b/tests/test_upload.py index 6a851234..2ae62547 100644 --- a/tests/test_upload.py +++ b/tests/test_upload.py @@ -258,6 +258,58 @@ def check_not_sane(key, value): check_not_sane('key', None) check_not_sane('parts', None) + def test_upload_large_with_tagging(self): + + from oss2.compat import urlquote + + key = random_string(16) + content = random_bytes(5 * 100 * 1024) + + pathname = self._prepare_temp_file(content) + + headers = dict() + tagging_header = oss2.headers.OSS_OBJECT_TAGGING + + key1 = 128*'a' + value1 = 256*'b' + + key2 = '+-:/' + value2 = ':+:' + + key3 = '中文' + value3 = '++中文++' + + tag_str = key1 + '=' + value1 + tag_str += '&' + urlquote(key2) + '=' + urlquote(value2) + tag_str += '&' + urlquote(key3) + '=' + urlquote(value3) + + headers[tagging_header] = tag_str + + result = oss2.resumable_upload(self.bucket, key, pathname, multipart_threshold=200 * 1024, num_threads=3, headers=headers) + + self.assertTrue(result is not None) + self.assertTrue(result.etag is not None) + self.assertTrue(result.request_id is not None) + + result = self.bucket.get_object(key) + self.assertEqual(content, result.read()) + self.assertEqual(result.headers['x-oss-object-type'], 'Multipart') + + result = self.bucket.get_object_tagging(key) + + self.assertEqual(3, result.tag_set.len()) + tagging_rule = result.tag_set.tagging_rule + self.assertEqual(256*'b', tagging_rule[128*'a']) + self.assertEqual(':+:', tagging_rule['+-:/']) + self.assertEqual('++中文++', tagging_rule['中文']) + + self.bucket.delete_object_tagging(key) + + result = self.bucket.get_object_tagging(key) + + self.assertEqual(0, result.tag_set.len()) + self.bucket.delete_object(key) + if __name__ == '__main__': unittest.main()