Merge pull request #681 from nirvdrum/s3_versioning_models

S3 versioning models
This commit is contained in:
Wesley Beary 2012-01-26 15:14:55 -08:00
commit cfd96c6cc5
17 changed files with 839 additions and 18 deletions

View File

@ -1,5 +1,6 @@
require 'fog/core/model'
require 'fog/aws/models/storage/files'
require 'fog/aws/models/storage/versions'
module Fog
module Storage
@ -58,6 +59,26 @@ module Fog
@payer = new_payer
end
def versioning?
requires :key
data = connection.get_bucket_versioning(key)
data.body['VersioningConfiguration']['Status'] == 'Enabled'
end
def versioning=(new_versioning)
requires :key
connection.put_bucket_versioning(key, new_versioning ? 'Enabled' : 'Suspended')
end
def versions
@versions ||= begin
Fog::Storage::AWS::Versions.new(
:directory => self,
:connection => connection
)
end
end
def public=(new_public)
if new_public
@acl = 'public-read'

View File

@ -1,4 +1,5 @@
require 'fog/core/model'
require 'fog/aws/models/storage/versions'
module Fog
module Storage
@ -22,6 +23,7 @@ module Fog
attribute :owner, :aliases => 'Owner'
attribute :storage_class, :aliases => ['x-amz-storage-class', 'StorageClass']
attribute :encryption, :aliases => 'x-amz-server-side-encryption'
attribute :version, :aliases => 'x-amz-version-id'
def acl=(new_acl)
valid_acls = ['private', 'public-read', 'public-read-write', 'authenticated-read']
@ -54,9 +56,10 @@ module Fog
target_directory.files.head(target_file_key)
end
def destroy
def destroy(options = {})
requires :directory, :key
connection.delete_object(directory.key, key)
attributes[:body] = nil if options['versionId'] == version
connection.delete_object(directory.key, key, options)
true
end
@ -130,6 +133,15 @@ module Fog
collection.get_https_url(key, expires, options)
end
def versions
@versions ||= begin
Fog::Storage::AWS::Versions.new(
:file => self,
:connection => connection
)
end
end
private
def directory=(new_directory)

View File

@ -0,0 +1,36 @@
require 'fog/core/model'
module Fog
module Storage
class AWS
class Version < Fog::Model
identity :version, :aliases => 'VersionId'
attribute :key, :aliases => 'Key'
attribute :last_modified, :aliases => ['Last-Modified', 'LastModified']
attribute :latest, :aliases => 'IsLatest', :type => :boolean
attribute :content_length, :aliases => ['Content-Length', 'Size'], :type => :integer
attribute :delete_marker, :type => :boolean
def file
@file ||= if collection.file
collection.file.directory.files.get(key, 'versionId' => version)
else
collection.directory.files.get(key, 'versionId' => version)
end
end
def destroy
if collection.file
collection.connection.delete_object(collection.file.directory.key, key, 'versionId' => version)
else
collection.connection.delete_object(collection.directory.key, key, 'versionId' => version)
end
end
end
end
end
end

View File

@ -0,0 +1,38 @@
require 'fog/core/collection'
require 'fog/aws/models/storage/version'
module Fog
module Storage
class AWS
class Versions < Fog::Collection
attribute :file
attribute :directory
model Fog::Storage::AWS::Version
def all
data = if file
connection.get_bucket_object_versions(file.directory.key, 'prefix' => file.key).body['Versions']
else
connection.get_bucket_object_versions(directory.key).body['Versions']
end
load(data)
end
def new(attributes = {})
version_type = attributes.keys.first
model = super(attributes[version_type])
model.delete_marker = version_type == 'DeleteMarker'
model
end
end
end
end
end

View File

@ -17,6 +17,12 @@ module Fog
# http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTObjectDELETE.html
def delete_object(bucket_name, object_name, options = {})
if version_id = options.delete('versionId')
path = "#{CGI.escape(object_name)}?versionId=#{CGI.escape(version_id)}"
else
path = CGI.escape(object_name)
end
headers = options
request({
:expects => 204,
@ -24,7 +30,7 @@ module Fog
:host => "#{bucket_name}.#{@host}",
:idempotent => true,
:method => 'DELETE',
:path => CGI.escape(object_name)
:path => path
})
end
@ -36,7 +42,55 @@ module Fog
response = Excon::Response.new
if bucket = self.data[:buckets][bucket_name]
response.status = 204
bucket[:objects].delete(object_name)
version_id = options.delete('versionId')
if bucket[:versioning]
bucket[:objects][object_name] ||= []
if version_id
version = bucket[:objects][object_name].find { |object| object['VersionId'] == version_id}
# S3 special cases the 'null' value to not error out if no such version exists.
if version || (version_id == 'null')
bucket[:objects][object_name].delete(version)
bucket[:objects].delete(object_name) if bucket[:objects][object_name].empty?
response.headers['x-amz-delete-marker'] = 'true' if version[:delete_marker]
response.headers['x-amz-version-id'] = version_id
else
response.status = 400
response.body = invalid_version_id_payload(version_id)
end
else
delete_marker = {
:delete_marker => true,
'Key' => object_name,
'VersionId' => bucket[:versioning] == 'Enabled' ? Fog::Mock.random_base64(32) : 'null',
'Last-Modified' => Fog::Time.now.to_date_header
}
# When versioning is suspended, a delete marker is placed if the last object ID is not the value 'null',
# otherwise the last object is replaced.
if bucket[:versioning] == 'Suspended' && bucket[:objects][object_name].last['VersionId'] == 'null'
bucket[:objects][object_name].pop
end
bucket[:objects][object_name] << delete_marker
response.headers['x-amz-delete-marker'] = 'true'
response.headers['x-amz-version-id'] = delete_marker['VersionId']
end
else
if version_id && version_id != 'null'
response.status = 400
response.body = invalid_version_id_payload(version_id)
else
bucket[:objects].delete(object_name)
response.headers['x-amz-version-id'] = 'null'
end
end
else
response.status = 404
raise(Excon::Errors.status_error({:expects => 204}, response))
@ -44,6 +98,21 @@ module Fog
response
end
private
def invalid_version_id_payload(version_id)
{
'Error' => {
'Code' => 'InvalidArgument',
'Message' => 'Invalid version id specified',
'ArgumentValue' => version_id,
'ArgumentName' => 'versionId',
'RequestId' => Fog::Mock.random_hex(16),
'HostId' => Fog::Mock.random_base64(65)
}
}
end
end
end
end

View File

@ -69,11 +69,12 @@ module Fog
end
response = Excon::Response.new
if bucket = self.data[:buckets][bucket_name]
contents = bucket[:objects].values.sort {|x,y| x['Key'] <=> y['Key']}.reject do |object|
contents = bucket[:objects].values.collect(&:last).sort {|x,y| x['Key'] <=> y['Key']}.reject do |object|
(prefix && object['Key'][0...prefix.length] != prefix) ||
(marker && object['Key'] <= marker) ||
(delimiter && object['Key'][(prefix ? prefix.length : 0)..-1].include?(delimiter) \
&& common_prefixes << object['Key'].sub(/^(#{prefix}[^#{delimiter}]+.).*/, '\1'))
&& common_prefixes << object['Key'].sub(/^(#{prefix}[^#{delimiter}]+.).*/, '\1')) ||
object.has_key?(:delete_marker)
end.map do |object|
data = object.reject {|key, value| !['ETag', 'Key', 'StorageClass'].include?(key)}
data.merge!({

View File

@ -65,11 +65,98 @@ module Fog
:idempotent => true,
:method => 'GET',
:parser => Fog::Parsers::Storage::AWS::GetBucketObjectVersions.new,
:query => {'versions' => nil}.merge!(options)
})
:query => {'versions' => nil}.merge!(options) })
end
end
class Mock
def get_bucket_object_versions(bucket_name, options = {})
delimiter, key_marker, max_keys, prefix, version_id_marker = \
options['delimiter'], options['key_marker'], options['max_keys'],options['prefix'],options['version_id_marker']
unless bucket_name
raise ArgumentError.new('bucket_name is required')
end
response = Excon::Response.new
# Invalid arguments.
if version_id_marker && !key_marker
response.status = 400
response.body = {
'Error' => {
'Code' => 'InvalidArgument',
'Message' => 'A version-id marker cannot be specified without a key marker.',
'ArgumentValue' => version_id_marker,
'RequestId' => Fog::Mock.random_hex(16),
'HostId' => Fog::Mock.random_base64(65)
}
}
# Valid case.
# TODO: (nirvdrum 12/15/11) It's not clear to me how to actually use version-id-marker, so I didn't implement it below.
elsif bucket = self.data[:buckets][bucket_name]
contents = bucket[:objects].values.flatten.sort {|x,y| x['Key'] <=> y['Key']}.reject do |object|
(prefix && object['Key'][0...prefix.length] != prefix) ||
(key_marker && object['Key'] <= key_marker) ||
(delimiter && object['Key'][(prefix ? prefix.length : 0)..-1].include?(delimiter) \
&& common_prefixes << object['Key'].sub(/^(#{prefix}[^#{delimiter}]+.).*/, '\1'))
end.map do |object|
if object.has_key?(:delete_marker)
tag_name = 'DeleteMarker'
extracted_attrs = ['Key', 'VersionId']
else
tag_name = 'Version'
extracted_attrs = ['ETag', 'Key', 'StorageClass', 'VersionId']
end
data = {}
data[tag_name] = object.reject { |key, value| !extracted_attrs.include?(key) }
data[tag_name].merge!({
'LastModified' => Time.parse(object['Last-Modified']),
'Owner' => bucket['Owner'],
'IsLatest' => object == bucket[:objects][object['Key']].last
})
data[tag_name]['Size'] = object['Content-Length'].to_i if tag_name == 'Version'
data
end
max_keys = max_keys || 1000
size = [max_keys, 1000].min
truncated_contents = contents[0...size]
response.status = 200
response.body = {
'Versions' => truncated_contents,
'IsTruncated' => truncated_contents.size != contents.size,
'KeyMarker' => key_marker,
'VersionIdMarker' => version_id_marker,
'MaxKeys' => max_keys,
'Name' => bucket['Name'],
'Prefix' => prefix
}
if max_keys && max_keys < response.body['Versions'].length
response.body['IsTruncated'] = true
response.body['Versions'] = response.body['Versions'][0...max_keys]
end
# Missing bucket case.
else
response.status = 403
response.body = {
'Error' => {
'Code' => 'AccessDenied',
'Message' => 'AccessDenied',
'RequestId' => Fog::Mock.random_hex(16),
'HostId' => Fog::Mock.random_base64(65)
}
}
end
response
end
end
end
end
end

View File

@ -33,7 +33,37 @@ module Fog
:query => {'versioning' => nil}
})
end
end
class Mock
def get_bucket_versioning(bucket_name)
response = Excon::Response.new
bucket = self.data[:buckets][bucket_name]
if bucket
response.status = 200
if bucket[:versioning]
response.body = { 'VersioningConfiguration' => { 'Status' => bucket[:versioning] } }
else
response.body = { 'VersioningConfiguration' => {} }
end
else
response.status = 404
response.body = {
'Error' => {
'Code' => 'NoSuchBucket',
'Message' => 'The specified bucket does not exist',
'BucketName' => bucket_name,
'RequestId' => Fog::Mock.random_hex(16),
'HostId' => Fog::Mock.random_base64(65)
}
}
end
response
end
end
end
end

View File

@ -62,15 +62,24 @@ module Fog
class Mock # :nodoc:all
def get_object(bucket_name, object_name, options = {}, &block)
version_id = options.delete('versionId')
unless bucket_name
raise ArgumentError.new('bucket_name is required')
end
unless object_name
raise ArgumentError.new('object_name is required')
end
response = Excon::Response.new
if (bucket = self.data[:buckets][bucket_name])
if (object = bucket[:objects][object_name])
object = nil
if bucket[:objects].has_key?(object_name)
object = version_id ? bucket[:objects][object_name].find { |object| object['VersionId'] == version_id} : bucket[:objects][object_name].last
end
if (object && !object[:delete_marker])
if options['If-Match'] && options['If-Match'] != object['ETag']
response.status = 412
elsif options['If-Modified-Since'] && options['If-Modified-Since'] > Time.parse(object['Last-Modified'])
@ -87,6 +96,9 @@ module Fog
response.headers[key] = value
end
end
response.headers['x-amz-version-id'] = object['VersionId'] if bucket[:versioning]
unless block_given?
response.body = object[:body]
else
@ -99,6 +111,18 @@ module Fog
end
end
end
elsif version_id && !object
response.status = 400
response.body = {
'Error' => {
'Code' => 'InvalidArgument',
'Message' => 'Invalid version id specified',
'ArgumentValue' => version_id,
'ArgumentName' => 'versionId',
'RequestId' => Fog::Mock.random_hex(16),
'HostId' => Fog::Mock.random_base64(65)
}
}
else
response.status = 404
response.body = "...<Code>NoSuchKey<\/Code>..."

View File

@ -29,7 +29,43 @@ DATA
:query => {'versioning' => nil}
})
end
end
class Mock
def put_bucket_versioning(bucket_name, status)
response = Excon::Response.new
bucket = self.data[:buckets][bucket_name]
if bucket
if ['Enabled', 'Suspended'].include?(status)
bucket[:versioning] = status
response.status = 200
else
response.status = 400
response.body = {
'Error' => {
'Code' => 'MalformedXML',
'Message' => 'The XML you provided was not well-formed or did not validate against our published schema',
'RequestId' => Fog::Mock.random_hex(16),
'HostId' => Fog::Mock.random_base64(65)
}
}
end
else
response.status = 403
response.body = {
'Error' => {
'Code' => 'AccessDenied',
'Message' => 'AccessDenied',
'RequestId' => Fog::Mock.random_hex(16),
'HostId' => Fog::Mock.random_base64(65)
}
}
end
response
end
end
end
end

View File

@ -32,9 +32,9 @@ module Fog
def put_object(bucket_name, object_name, data, options = {})
data = Fog::Storage.parse_data(data)
headers = data[:headers].merge!(options)
request({
:body => data[:body],
:expects => 200,
request({ :body => data[:body],
:expects => 200,
:headers => headers,
:host => "#{bucket_name}.#{@host}",
:idempotent => true,
@ -70,7 +70,8 @@ module Fog
'Key' => object_name,
'Last-Modified' => Fog::Time.now.to_date_header,
'Content-Length' => options['Content-Length'] || data[:headers]['Content-Length'],
'StorageClass' => options['x-amz-storage-class'] || 'STANDARD'
'StorageClass' => options['x-amz-storage-class'] || 'STANDARD',
'VersionId' => bucket[:versioning] == 'Enabled' ? Fog::Mock.random_base64(32) : 'null'
}
for key, value in options
@ -80,13 +81,28 @@ module Fog
end
end
bucket[:objects][object_name] = object
if bucket[:versioning]
bucket[:objects][object_name] ||= []
# When versioning is suspended, putting an object will create a new 'null' version if the latest version
# is a value other than 'null', otherwise it will replace the latest version.
if bucket[:versioning] == 'Suspended' && bucket[:objects][object_name].last['VersionId'] == 'null'
bucket[:objects][object_name].pop
end
bucket[:objects][object_name] << object
else
bucket[:objects][object_name] = [object]
end
response.headers = {
'Content-Length' => object['Content-Length'],
'Content-Type' => object['Content-Type'],
'ETag' => object['ETag'],
'Last-Modified' => object['Last-Modified']
'Content-Length' => object['Content-Length'],
'Content-Type' => object['Content-Type'],
'ETag' => object['ETag'],
'Last-Modified' => object['Last-Modified'],
}
response.headers['x-amz-version-id'] = object['VersionId'] if object['VersionId'] != 'null'
else
response.status = 404
raise(Excon::Errors.status_error({:expects => 200}, response))

View File

@ -0,0 +1,53 @@
Fog.mock!
Shindo.tests("Storage[:aws] | directory", [:aws]) do
directory_attributes = {
:key => 'fogdirectorytests'
}
model_tests(Fog::Storage[:aws].directories, directory_attributes, Fog.mocking?) do
tests("#versioning=") do
tests("#versioning=(true)").succeeds do
@instance.versioning = true
end
tests("#versioning=(true) sets versioning to 'Enabled'").returns('Enabled') do
@instance.versioning = true
@instance.connection.get_bucket_versioning(@instance.key).body['VersioningConfiguration']['Status']
end
tests("#versioning=(false)").succeeds do
@instance.versioning = false
end
tests("#versioning=(false) sets versioning to 'Suspended'").returns('Suspended') do
@instance.versioning = false
@instance.connection.get_bucket_versioning(@instance.key).body['VersioningConfiguration']['Status']
end
end
end
model_tests(Fog::Storage[:aws].directories, directory_attributes, Fog.mocking?) do
tests("#versioning?") do
tests("#versioning? false if not enabled").returns(false) do
@instance.versioning?
end
tests("#versioning? true if enabled").returns(true) do
@instance.connection.put_bucket_versioning(@instance.key, 'Enabled')
@instance.versioning?
end
tests("#versioning? false if suspended").returns(false) do
@instance.connection.put_bucket_versioning(@instance.key, 'Suspended')
@instance.versioning?
end
end
end
end

View File

@ -0,0 +1,63 @@
Fog.mock!
Shindo.tests("Storage[:aws] | file", [:aws]) do
file_attributes = {
:key => 'fog_file_tests',
:body => lorem_file,
:public => true
}
directory_attributes = {
:key => 'fogfilestests'
}
@directory = Fog::Storage[:aws].directories.create(directory_attributes)
model_tests(@directory.files, file_attributes, Fog.mocking?) do
tests("#version") do
tests("#version should be null if versioning isn't enabled").returns(nil) do
@instance.version
end
end
end
@directory.versioning = true
model_tests(@directory.files, file_attributes, Fog.mocking?) do
tests("#version") do
tests("#version should not be null if versioning is enabled").returns(false) do
@instance.version == nil
end
end
@directory.files.create(:key => @instance.key)
@instance.destroy
tests("#versions") do
tests('#versions.size includes versions (including DeleteMarkers) for all keys').returns(3) do
@instance.versions.size
end
tests('#versions are all for the correct key').returns(true) do
@instance.versions.all? { |v| v.key == @instance.key }
end
end
tests("#destroy") do
tests("#destroy a specific version should delete the version, not create a DeleteMarker").returns(2) do
@instance.destroy('versionId' => @instance.version)
@instance.versions.all.size
end
end
end
@directory.versions.each(&:destroy)
@directory.destroy
end

View File

@ -0,0 +1,58 @@
Fog.mock!
Shindo.tests("Storage[:aws] | files", [:aws]) do
file_attributes = {
:key => 'fog_file_tests',
:body => lorem_file,
:public => true
}
directory_attributes = {
:key => 'fogfilestests'
}
@directory = Fog::Storage[:aws].directories.create(directory_attributes)
@directory.versioning = true
model_tests(@directory.files, file_attributes, Fog.mocking?) do
v1 = @instance.version
v2 = @directory.connection.put_object(@directory.key, @instance.key, 'version 2 content').headers['x-amz-version-id']
v3 = @directory.connection.delete_object(@directory.key, @instance.key).headers['x-amz-version-id']
v4 = @directory.connection.put_object(@directory.key, @instance.key, 'version 3 content').headers['x-amz-version-id']
tests("#get") do
tests("#get without version fetches the latest version").returns(v4) do
@directory.files.get(@instance.key).version
end
tests("#get with version fetches that exact version").returns(v2) do
@directory.files.get(@instance.key, 'versionId' => v2).version
end
tests("#get with a deleted version returns nil").returns(nil) do
@directory.files.get(@instance.key, 'versionId' => v3)
end
end
tests("#head") do
tests("#head without version fetches the latest version").returns(v4) do
@directory.files.head(@instance.key).version
end
tests("#head with version fetches that exact version").returns(v2) do
@directory.files.head(@instance.key, 'versionId' => v2).version
end
tests("#head with a deleted version returns nil").returns(nil) do
@directory.files.head(@instance.key, 'versionId' => v3)
end
end
end
@directory.versions.each(&:destroy)
@directory.destroy
end

View File

@ -0,0 +1,54 @@
Fog.mock!
Shindo.tests("Storage[:aws] | version", [:aws]) do
file_attributes = {
:key => 'fog_file_tests',
:body => lorem_file,
:public => true
}
directory_attributes = {
:key => 'fogfilestests'
}
@directory = Fog::Storage[:aws].directories.create(directory_attributes)
@directory.versioning = true
model_tests(@directory.files, file_attributes, Fog.mocking?) do
@version_instance = @instance.versions.first
@directory.connection.put_object(@directory.key, @instance.key, 'second version content')
tests("#file") do
tests("#file should return the object associated with the version").returns(@version_instance.version) do
@version_instance.file.version
end
end
tests("#delete_marker") do
tests("#delete_marker should be false if the version isn't a DeleteMarker'").returns(false) do
@version_instance.delete_marker
end
tests("#delete_marker should be true if the version isn't a DeleteMarker'").returns(true) do
@instance.destroy
@instance.versions.all.last.delete_marker
end
end
tests("#destroy") do
tests("#destroy removes the specific version").returns(false) do
@version_instance.destroy
@instance.versions.all.collect(&:version).include?(@version_instance.version)
end
end
end
@directory.versions.each(&:destroy)
@directory.destroy
end

View File

@ -0,0 +1,51 @@
Fog.mock!
Shindo.tests("Storage[:aws] | versions", [:aws]) do
file_attributes = {
:key => 'fog_file_tests',
:body => lorem_file,
:public => true
}
directory_attributes = {
:key => 'fogfilestests'
}
model_tests(Fog::Storage[:aws].directories, directory_attributes, Fog.mocking?) do
@instance.versioning = true
versions = []
versions << @instance.connection.put_object(@instance.key, 'one', 'abcde').headers['x-amz-version-id']
versions << @instance.connection.put_object(@instance.key, 'one', '32423').headers['x-amz-version-id']
versions << @instance.connection.delete_object(@instance.key, 'one').headers['x-amz-version-id']
versions << @instance.connection.put_object(@instance.key, 'two', 'aoeu').headers['x-amz-version-id']
tests('#versions') do
tests('#versions.size includes versions (including DeleteMarkers) for all keys').returns(4) do
@instance.versions.size
end
tests('#versions returns the correct versions').returns(versions) do
@instance.versions.collect(&:version)
end
end
tests("#all") do
tests("#all for a directory returns all versions, regardless of key").returns(versions) do
@instance.versions.all.collect(&:version)
end
tests("#all for file returns only versions for that file").returns(1) do
@instance.files.get('two').versions.collect(&:version).size
end
tests("#all for file returns only versions for that file").returns(versions.last) do
@instance.files.get('two').versions.collect(&:version).first
end
end
@instance.versions.each(&:destroy)
end
end

View File

@ -0,0 +1,172 @@
def clear_bucket
Fog::Storage[:aws].get_bucket_object_versions(@aws_bucket_name).body['Versions'].each do |version|
object = version[version.keys.first]
Fog::Storage[:aws].delete_object(@aws_bucket_name, object['Key'], 'versionId' => object['VersionId'])
end
end
def create_versioned_bucket
@aws_bucket_name = 'fogbuckettests-' + Fog::Mock.random_hex(16)
Fog::Storage[:aws].put_bucket(@aws_bucket_name)
Fog::Storage[:aws].put_bucket_versioning(@aws_bucket_name, 'Enabled')
end
def delete_bucket
Fog::Storage[:aws].get_bucket_object_versions(@aws_bucket_name).body['Versions'].each do |version|
object = version[version.keys.first]
Fog::Storage[:aws].delete_object(@aws_bucket_name, object['Key'], 'versionId' => object['VersionId'])
end
Fog::Storage[:aws].delete_bucket(@aws_bucket_name)
end
Shindo.tests('Fog::Storage[:aws] | versioning', [:aws]) do
tests('success') do
tests("#put_bucket_versioning") do
@aws_bucket_name = 'fogbuckettests-' + Fog::Mock.random_hex(16)
Fog::Storage[:aws].put_bucket(@aws_bucket_name)
tests("#put_bucket_versioning('#{@aws_bucket_name}', 'Enabled')").succeeds do
Fog::Storage[:aws].put_bucket_versioning(@aws_bucket_name, 'Enabled')
end
tests("#put_bucket_versioning('#{@aws_bucket_name}', 'Suspended')").succeeds do
Fog::Storage[:aws].put_bucket_versioning(@aws_bucket_name, 'Suspended')
end
delete_bucket
end
tests("#get_bucket_versioning('#{@aws_bucket_name}')") do
@aws_bucket_name = 'fogbuckettests-' + Fog::Mock.random_hex(16)
Fog::Storage[:aws].put_bucket(@aws_bucket_name)
tests("#get_bucket_versioning('#{@aws_bucket_name}') without versioning").returns({}) do
Fog::Storage[:aws].get_bucket_versioning(@aws_bucket_name).body['VersioningConfiguration']
end
tests("#get_bucket_versioning('#{@aws_bucket_name}') with versioning enabled").returns('Enabled') do
Fog::Storage[:aws].put_bucket_versioning(@aws_bucket_name, 'Enabled')
Fog::Storage[:aws].get_bucket_versioning(@aws_bucket_name).body['VersioningConfiguration']['Status']
end
tests("#get_bucket_versioning('#{@aws_bucket_name}') with versioning suspended").returns('Suspended') do
Fog::Storage[:aws].put_bucket_versioning(@aws_bucket_name, 'Suspended')
Fog::Storage[:aws].get_bucket_versioning(@aws_bucket_name).body['VersioningConfiguration']['Status']
end
delete_bucket
end
tests("#get_bucket_object_versions('#{@aws_bucket_name}')") do
create_versioned_bucket
before do
@versions = Fog::Storage[:aws].get_bucket_object_versions(@aws_bucket_name)
end
v1 = Fog::Storage[:aws].directories.get(@aws_bucket_name).files.create(:body => 'a', :key => 'file')
v2 = Fog::Storage[:aws].directories.get(@aws_bucket_name).files.create(:body => 'ab', :key => v1.key)
v3 = Fog::Storage[:aws].directories.get(@aws_bucket_name).files.create(:body => 'abc', :key => v1.key)
v4 = Fog::Storage[:aws].directories.get(@aws_bucket_name).files.create(:body => 'abcd', :key => v1.key)
tests("versions").returns([v4.version, v3.version, v2.version, v1.version]) do
@versions.body['Versions'].collect {|v| v['Version']['VersionId']}
end
tests("version sizes").returns([4, 3, 2, 1]) do
@versions.body['Versions'].collect {|v| v['Version']['Size']}
end
tests("latest version").returns(v4.version) do
latest = @versions.body['Versions'].find {|v| v['Version']['IsLatest']}
latest['Version']['VersionId']
end
end
tests("get_object('#{@aws_bucket_name}', 'file')") do
clear_bucket
v1 = Fog::Storage[:aws].directories.get(@aws_bucket_name).files.create(:body => 'a', :key => 'file')
v2 = Fog::Storage[:aws].directories.get(@aws_bucket_name).files.create(:body => 'ab', :key => v1.key)
tests("get_object('#{@aws_bucket_name}', '#{v2.key}') returns the latest version").returns(v2.version) do
res = Fog::Storage[:aws].get_object(@aws_bucket_name, v2.key)
res.headers['x-amz-version-id']
end
tests("get_object('#{@aws_bucket_name}', '#{v1.key}', 'versionId' => '#{v1.version}') returns the specified version").returns(v1.version) do
res = Fog::Storage[:aws].get_object(@aws_bucket_name, v1.key, 'versionId' => v1.version)
res.headers['x-amz-version-id']
end
v2.destroy
tests("get_object('#{@aws_bucket_name}', '#{v2.key}') raises exception if delete marker is latest version").raises(Excon::Errors::NotFound) do
Fog::Storage[:aws].get_object(@aws_bucket_name, v2.key)
end
end
tests("delete_object('#{@aws_bucket_name}', 'file')") do
clear_bucket
file = Fog::Storage[:aws].directories.get(@aws_bucket_name).files.create(:body => 'a', :key => 'file')
tests("deleting an object just stores a delete marker").returns(true) do
file.destroy
versions = Fog::Storage[:aws].get_bucket_object_versions(@aws_bucket_name)
versions.body['Versions'].first.has_key?('DeleteMarker')
end
tests("there are two versions: the original and the delete marker").returns(2) do
versions = Fog::Storage[:aws].get_bucket_object_versions(@aws_bucket_name)
versions.body['Versions'].size
end
tests("deleting the delete marker makes the object available again").returns(file.version) do
versions = Fog::Storage[:aws].get_bucket_object_versions(@aws_bucket_name)
delete_marker = versions.body['Versions'].find { |v| v.has_key?('DeleteMarker') }
Fog::Storage[:aws].delete_object(@aws_bucket_name, file.key, 'versionId' => delete_marker['DeleteMarker']['VersionId'])
res = Fog::Storage[:aws].get_object(@aws_bucket_name, file.key)
res.headers['x-amz-version-id']
end
end
delete_bucket
end
tests('failure') do
create_versioned_bucket
tests("#put_bucket_versioning('#{@aws_bucket_name}', 'bad_value')").raises(Excon::Errors::BadRequest) do
Fog::Storage[:aws].put_bucket_versioning(@aws_bucket_name, 'bad_value')
end
tests("#put_bucket_versioning('fognonbucket', 'Enabled')").raises(Excon::Errors::NotFound) do
Fog::Storage[:aws].put_bucket_versioning('fognonbucket', 'Enabled')
end
tests("#get_bucket_versioning('fognonbucket')").raises(Excon::Errors::NotFound) do
Fog::Storage[:aws].get_bucket_versioning('fognonbucket')
end
tests("#get_bucket_object_versions('fognonbucket')").raises(Excon::Errors::NotFound) do
Fog::Storage[:aws].get_bucket_object_versions('fognonbucket')
end
file = Fog::Storage[:aws].directories.get(@aws_bucket_name).files.create(:body => 'y', :key => 'x')
tests("#get_object('#{@aws_bucket_name}', '#{file.key}', 'versionId' => 'bad_version'").raises(Excon::Errors::BadRequest) do
Fog::Storage[:aws].get_object(@aws_bucket_name, file.key, 'versionId' => '-1')
end
tests("#delete_object('#{@aws_bucket_name}', '#{file.key}', 'versionId' => 'bad_version'").raises(Excon::Errors::BadRequest) do
Fog::Storage[:aws].delete_object(@aws_bucket_name, file.key, 'versionId' => '-1')
end
end
# don't keep the bucket around
delete_bucket
end