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")