diff --git a/docs/API.md b/docs/API.md index 5e434a75a..41234ffa4 100644 --- a/docs/API.md +++ b/docs/API.md @@ -614,7 +614,7 @@ __Parameters__ __Example__ ```py -minio.set_bucket_versioning("my-bucketname", VersioningConfig("Enabled")) +minio.set_bucket_versioning("my-bucketname", VersioningConfig(ENABLED)) ``` diff --git a/examples/get_bucket_versioning.py b/examples/get_bucket_versioning.py new file mode 100644 index 000000000..4e4141287 --- /dev/null +++ b/examples/get_bucket_versioning.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# MinIO Python Library for Amazon S3 Compatible Cloud Storage. +# Copyright (C) 2020 MinIO, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY and my-bucketname are +# dummy values, please replace them with original values. + +from minio import Minio + +client = Minio( + "play.min.io", + access_key="Q3AM3UQ867SPQQA43P2F", + secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG", +) + +config = client.get_bucket_versioning("my-bucketname") +print(config.status) diff --git a/examples/set_bucket_versioning.py b/examples/set_bucket_versioning.py new file mode 100644 index 000000000..78af5761f --- /dev/null +++ b/examples/set_bucket_versioning.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# MinIO Python Library for Amazon S3 Compatible Cloud Storage. +# Copyright (C) 2020 MinIO, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY and my-bucketname are +# dummy values, please replace them with original values. + +from minio import Minio +from minio.commonconfig import ENABLED +from minio.versioningconfig import VersioningConfig + +client = Minio( + "play.min.io", + access_key="Q3AM3UQ867SPQQA43P2F", + secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG", +) + +client.set_bucket_versioning("my-bucketname", VersioningConfig(ENABLED)) diff --git a/minio/__init__.py b/minio/__init__.py index 290667516..bf0925cfd 100644 --- a/minio/__init__.py +++ b/minio/__init__.py @@ -36,6 +36,6 @@ # pylint: disable=unused-import from .api import Minio from .copy_conditions import CopyConditions -from .definitions import Bucket, Object, VersioningConfig +from .definitions import Bucket, Object from .error import InvalidResponseError, S3Error, ServerError from .post_policy import PostPolicy diff --git a/minio/api.py b/minio/api.py index b4d91870d..01d4d02bf 100644 --- a/minio/api.py +++ b/minio/api.py @@ -58,17 +58,17 @@ parse_list_parts, parse_location_constraint, parse_multi_delete_response, parse_multipart_upload_result, - parse_new_multipart_upload, parse_versioning_config) + parse_new_multipart_upload) from .replicationconfig import ReplicationConfig from .select import SelectObjectReader from .signer import (AMZ_DATE_FORMAT, SIGN_V4_ALGORITHM, get_credential_string, post_presign_v4, presign_v4, sign_v4_s3) from .sse import SseCustomerKey from .thread_pool import ThreadPool +from .versioningconfig import VersioningConfig from .xml import marshal, unmarshal from .xml_marshal import (marshal_bucket_notifications, marshal_complete_multipart, - marshal_versioning_config, xml_marshal_bucket_constraint, xml_marshal_bucket_encryption, xml_marshal_delete_objects, xml_marshal_select, @@ -876,11 +876,13 @@ def set_bucket_versioning(self, bucket_name, config): Example:: minio.set_bucket_versioning( - "my-bucketname", VersioningConfig("Enabled"), + "my-bucketname", VersioningConfig(ENABLED), ) """ check_bucket_name(bucket_name) - body = marshal_versioning_config(config) + if not isinstance(config, VersioningConfig): + raise ValueError("config must be VersioningConfig type") + body = marshal(config) self._execute( "PUT", bucket_name, @@ -906,8 +908,7 @@ def get_bucket_versioning(self, bucket_name): bucket_name, query_params={"versioning": ""}, ) - - return parse_versioning_config(response.data) + return unmarshal(VersioningConfig, response.data.decode()) def fput_object(self, bucket_name, object_name, file_path, content_type='application/octet-stream', diff --git a/minio/definitions.py b/minio/definitions.py index be135c934..af4e142d4 100644 --- a/minio/definitions.py +++ b/minio/definitions.py @@ -472,25 +472,3 @@ def __init__(self, root): self.parts = [ Part(part_element) for part_element in root.findall("Part") ] - - -class VersioningConfig: - """Bucket versioning configuration.""" - - def __init__(self, status, mfa_delete=None): - if status: - status = status.title() - if status not in ["", "Enabled", "Suspended"]: - raise ValueError("status must be empty, Enabled or Suspended.") - self._status = status - self._mfa_delete = mfa_delete - - @property - def status(self): - """Get status.""" - return self._status or "Off" - - @property - def mfa_delete(self): - """Get MFA delete.""" - return self._mfa_delete diff --git a/minio/parsers.py b/minio/parsers.py index 94d3bee7b..1a1da9aed 100644 --- a/minio/parsers.py +++ b/minio/parsers.py @@ -31,8 +31,7 @@ from xml.etree.ElementTree import ParseError from .definitions import (Bucket, CopyObjectResult, ListMultipartUploadsResult, - ListPartsResult, MultipartUploadResult, Object, - VersioningConfig) + ListPartsResult, MultipartUploadResult, Object) # minio specific. from .error import MultiDeleteError, S3Error from .helpers import RFC3339, RFC3339NANO @@ -488,12 +487,3 @@ def parse_list_multipart_uploads(data): def parse_list_parts(data): """Parse ListParts API resppnse XML.""" return ListPartsResult(S3Element.fromstring("ListPartsResult", data)) - - -def parse_versioning_config(data): - """Decode XML data into :class:`VersioningConfig `""" - root = S3Element.fromstring("VersioningConfiguration", data) - return VersioningConfig( - root.get_child_text("Status"), - root.get_child_text("MFADelete"), - ) diff --git a/minio/versioningconfig.py b/minio/versioningconfig.py new file mode 100644 index 000000000..488b4cc09 --- /dev/null +++ b/minio/versioningconfig.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# MinIO Python Library for Amazon S3 Compatible Cloud Storage, (C) +# 2020 MinIO, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Request/response of PutBucketVersioning and GetBucketVersioning APIs.""" + +from __future__ import absolute_import + +from .commonconfig import DISABLED, ENABLED +from .xml import Element, SubElement, findtext + +OFF = "Off" +SUSPENDED = "Suspended" + + +class VersioningConfig: + """Versioning configuration.""" + + def __init__(self, status=None, mfa_delete=None): + if status is not None and status not in [ENABLED, SUSPENDED]: + raise ValueError( + "status must be {0} or {1}".format(ENABLED, SUSPENDED), + ) + if mfa_delete is not None and mfa_delete not in [ENABLED, DISABLED]: + raise ValueError( + "MFA delete must be {0} or {1}".format(ENABLED, DISABLED), + ) + self._status = status + self._mfa_delete = mfa_delete + + @property + def status(self): + """Get status.""" + return self._status or OFF + + @property + def mfa_delete(self): + """Get MFA delete.""" + return self._mfa_delete + + @classmethod + def fromxml(cls, element): + """Create new object with values from XML element.""" + status = findtext(element, "Status") + mfa_delete = findtext(element, "MFADelete") + return cls(status, mfa_delete) + + def toxml(self, element): + """Convert to XML.""" + element = Element("VersioningConfiguration") + if self._status: + SubElement(element, "Status", self._status) + if self._mfa_delete: + SubElement(element, "MFADelete", self._mfa_delete) + return element diff --git a/minio/xml_marshal.py b/minio/xml_marshal.py index 969212b87..da922cec4 100644 --- a/minio/xml_marshal.py +++ b/minio/xml_marshal.py @@ -409,14 +409,3 @@ def xml_marshal_delete_objects(keys): SubElement(element, "VersionId", version_id) return _get_xml_data(root) - - -def marshal_versioning_config(config): - """Encode versioning configuration to XML bytes.""" - root = Element("VersioningConfiguration", with_namespace=True) - SubElement(root, "Status", config.status) - if config.mfa_delete is not None: - SubElement( - root, "MFADelete", "Enabled" if config.mfa_delete else "Disabled", - ) - return _get_xml_data(root) diff --git a/tests/functional/tests.py b/tests/functional/tests.py index 91cf0ae69..4a7b3d492 100644 --- a/tests/functional/tests.py +++ b/tests/functional/tests.py @@ -38,13 +38,15 @@ import certifi import urllib3 -from minio import CopyConditions, Minio, PostPolicy, VersioningConfig +from minio import CopyConditions, Minio, PostPolicy +from minio.commonconfig import ENABLED from minio.error import S3Error from minio.select.helpers import calculate_crc from minio.select.options import (CSVInput, CSVOutput, InputSerialization, OutputSerialization, RequestProgress, SelectObjectOptions) from minio.sse import SseCustomerKey +from minio.versioningconfig import VersioningConfig if sys.version_info[0] == 2: from datetime import tzinfo # pylint: disable=ungrouped-imports @@ -808,7 +810,7 @@ def _test_stat_object(log_entry, sse=None, version_check=False): try: if version_check: _CLIENT.set_bucket_versioning( - bucket_name, VersioningConfig("Enabled"), + bucket_name, VersioningConfig(ENABLED), ) # Put/Upload a streaming object of 1 MiB reader = LimitedRandomReader(length) @@ -878,7 +880,7 @@ def _test_remove_object(log_entry, version_check=False): try: if version_check: _CLIENT.set_bucket_versioning( - bucket_name, VersioningConfig("Enabled"), + bucket_name, VersioningConfig(ENABLED), ) _, version_id = _CLIENT.put_object( bucket_name, object_name, LimitedRandomReader(length), length, @@ -919,7 +921,7 @@ def _test_get_object(log_entry, sse=None, version_check=False): try: if version_check: _CLIENT.set_bucket_versioning( - bucket_name, VersioningConfig("Enabled"), + bucket_name, VersioningConfig(ENABLED), ) _, version_id = _CLIENT.put_object( bucket_name, object_name, LimitedRandomReader(length), @@ -972,7 +974,7 @@ def _test_fget_object(log_entry, sse=None, version_check=False): try: if version_check: _CLIENT.set_bucket_versioning( - bucket_name, VersioningConfig("Enabled"), + bucket_name, VersioningConfig(ENABLED), ) _, version_id = _CLIENT.put_object( bucket_name, object_name, LimitedRandomReader(length), @@ -1099,7 +1101,7 @@ def _test_list_objects(log_entry, use_api_v1=False, version_check=False): try: if version_check: _CLIENT.set_bucket_versioning( - bucket_name, VersioningConfig("Enabled"), + bucket_name, VersioningConfig(ENABLED), ) size = 1 * KB _, version_id1 = _CLIENT.put_object( @@ -1479,7 +1481,7 @@ def test_presigned_get_object_version( # pylint: disable=invalid-name _CLIENT.make_bucket(bucket_name) version_id = None try: - _CLIENT.set_bucket_versioning(bucket_name, VersioningConfig("Enabled")) + _CLIENT.set_bucket_versioning(bucket_name, VersioningConfig(ENABLED)) size = 1 * KB _, version_id = _CLIENT.put_object( bucket_name, object_name, LimitedRandomReader(size), size, @@ -1846,7 +1848,7 @@ def _test_remove_objects(log_entry, version_check=False): try: if version_check: _CLIENT.set_bucket_versioning( - bucket_name, VersioningConfig("Enabled"), + bucket_name, VersioningConfig(ENABLED), ) size = 1 * KB # Upload some new objects to prepare for multi-object delete test. diff --git a/tests/unit/versioningconfig.py b/tests/unit/versioningconfig.py new file mode 100644 index 000000000..0b4b68cce --- /dev/null +++ b/tests/unit/versioningconfig.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# MinIO Python Library for Amazon S3 Compatible Cloud Storage, +# (C) 2020 MinIO, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase + +from nose.tools import eq_ + +from minio import xml +from minio.commonconfig import DISABLED, ENABLED +from minio.versioningconfig import OFF, SUSPENDED, VersioningConfig + + +class VersioningConfigTest(TestCase): + def test_config(self): + config = VersioningConfig(ENABLED) + xml.marshal(config) + + config = xml.unmarshal( + VersioningConfig, + """ +""", + ) + xml.marshal(config) + eq_(config.status, OFF) + + config = xml.unmarshal( + VersioningConfig, + """ + Enabled +""", + ) + xml.marshal(config) + eq_(config.status, ENABLED) + + config = xml.unmarshal( + VersioningConfig, + """ + Suspended + Disabled +""", + ) + xml.marshal(config) + eq_(config.status, SUSPENDED) + eq_(config.mfa_delete, DISABLED)