diff --git a/moto/s3/models.py b/moto/s3/models.py index 921f92d7c..67b40d5e0 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -3,6 +3,7 @@ import base64 import datetime import hashlib import copy +import itertools from moto.core import BaseBackend from moto.core.utils import iso_8601_datetime, rfc_1123_datetime @@ -194,6 +195,18 @@ class S3Backend(BaseBackend): def get_bucket_versioning(self, bucket_name): return self.buckets[bucket_name].versioning_status + def get_bucket_versions(self, bucket_name, delimiter=None, + encoding_type=None, + key_marker=None, + max_keys=None, + version_id_marker=None): + bucket = self.buckets[bucket_name] + + if any((delimiter, encoding_type, key_marker, version_id_marker)): + raise NotImplementedError( + "Called get_bucket_versions with some of delimiter, encoding_type, key_marker, version_id_marker") + + return itertools.chain(*(l for _, l in bucket.keys.iterlists())) def set_key(self, bucket_name, key_name, value, storage=None, etag=None): key_name = clean_key_name(key_name) @@ -223,11 +236,16 @@ class S3Backend(BaseBackend): key.append_to_value(value) return key - def get_key(self, bucket_name, key_name): + def get_key(self, bucket_name, key_name, version_id=None): key_name = clean_key_name(key_name) bucket = self.get_bucket(bucket_name) if bucket: - return bucket.keys.get(key_name) + if version_id is None: + return bucket.keys.get(key_name) + else: + for key in bucket.keys.getlist(key_name): + if str(key._version_id) == str(version_id): + return key def initiate_multipart(self, bucket_name, key_name): bucket = self.buckets[bucket_name] diff --git a/moto/s3/responses.py b/moto/s3/responses.py index 833c25db8..9e657430f 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -77,6 +77,33 @@ class ResponseObject(object): versioning = self.backend.get_bucket_versioning(bucket_name) template = Template(S3_BUCKET_GET_VERSIONING) return 200, headers, template.render(status=versioning) + elif 'versions' in querystring: + delimiter = querystring.get('delimiter', [None])[0] + encoding_type = querystring.get('encoding-type', [None])[0] + key_marker = querystring.get('key-marker', [None])[0] + max_keys = querystring.get('max-keys', [None])[0] + prefix = querystring.get('prefix', [None])[0] + version_id_marker = querystring.get('version-id-marker', [None])[0] + + bucket = self.backend.get_bucket(bucket_name) + versions = self.backend.get_bucket_versions( + bucket_name, + delimiter=delimiter, + encoding_type=encoding_type, + key_marker=key_marker, + max_keys=max_keys, + version_id_marker=version_id_marker + ) + template = Template(S3_BUCKET_GET_VERSIONS) + return 200, headers, template.render( + key_list=versions, + bucket=bucket, + prefix='', + max_keys='', + delimiter='', + is_truncated='false', + ) + bucket = self.backend.get_bucket(bucket_name) if bucket: prefix = querystring.get('prefix', [None])[0] @@ -236,7 +263,9 @@ class ResponseObject(object): count=len(parts), parts=parts ) - key = self.backend.get_key(bucket_name, key_name) + version_id = query.get('versionId', [None])[0] + key = self.backend.get_key( + bucket_name, key_name, version_id=version_id) if key: headers.update(key.metadata) return 200, headers, key.value @@ -424,12 +453,14 @@ S3_DELETE_BUCKET_WITH_ITEMS_ERROR = """ """ S3_BUCKET_VERSIONING = """ + {{ bucket_versioning_status }} """ S3_BUCKET_GET_VERSIONING = """ + {% if status is none %} {% else %} @@ -438,6 +469,32 @@ S3_BUCKET_GET_VERSIONING = """ {% endif %} """ + +S3_BUCKET_GET_VERSIONS = """ + + {{ bucket.name }} + {{ prefix }} + {{ key_marker }} + {{ max_keys }} + {{ is_truncated }} + {% for key in key_list %} + + {{ key.name }} + {{ key._version_id }} + false + {{ key.last_modified_ISO8601 }} + {{ key.etag }} + {{ key.size }} + {{ key.storage_class }} + + 75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a + webfile + + + {% endfor %} + +""" + S3_DELETE_KEYS_RESPONSE = """ {% for k in deleted %} diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 5a656147c..8102f33c2 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -543,3 +543,29 @@ def test_key_version(): key = bucket.get_key('the-key') key.version_id.should.equal('1') + + +@mock_s3 +def test_list_versions(): + conn = boto.connect_s3('the_key', 'the_secret') + bucket = conn.create_bucket('foobar') + bucket.configure_versioning(versioning=True) + + key = Key(bucket, 'the-key') + key.version_id.should.be.none + key.set_contents_from_string("Version 1") + key.version_id.should.equal('0') + key.set_contents_from_string("Version 2") + key.version_id.should.equal('1') + + versions = list(bucket.list_versions()) + + versions.should.have.length_of(2) + + versions[0].name.should.equal('the-key') + versions[0].version_id.should.equal('0') + versions[0].get_contents_as_string().should.equal("Version 1") + + versions[1].name.should.equal('the-key') + versions[1].version_id.should.equal('1') + versions[1].get_contents_as_string().should.equal("Version 2")