diff --git a/lib/aws/s3/acl.rb b/lib/aws/s3/acl.rb
index b780aaf..5695e2b 100644
--- a/lib/aws/s3/acl.rb
+++ b/lib/aws/s3/acl.rb
@@ -562,7 +562,7 @@ def acl(name, bucket = nil, policy = nil)
path = path!(bucket, name) << '?acl'
respond_with ACL::Policy::Response do
- policy ? put(path, {}, policy.to_xml) : ACL::Policy.new(get(path).policy)
+ policy ? put(path, {}, policy.to_xml) : ACL::Policy.new(get(bucket, path).policy)
end
end
end
diff --git a/lib/aws/s3/authentication.rb b/lib/aws/s3/authentication.rb
index 0efbc7a..39e4771 100644
--- a/lib/aws/s3/authentication.rb
+++ b/lib/aws/s3/authentication.rb
@@ -61,7 +61,6 @@ def initialize(request, access_key_id, secret_access_key, options = {})
private
def canonical_string
- options = {}
options[:expires] = expires if expires?
CanonicalString.new(request, options)
end
@@ -156,7 +155,7 @@ def initialize(request, options = {})
# "For non-authenticated or anonymous requests. A NotImplemented error result code will be returned if
# an authenticated (signed) request specifies a Host: header other than 's3.amazonaws.com'"
# (from http://docs.amazonwebservices.com/AmazonS3/2006-03-01/VirtualHosting.html)
- request['Host'] = DEFAULT_HOST
+ request['Host'] ||= DEFAULT_HOST
build
end
@@ -173,7 +172,7 @@ def build
self << (key =~ self.class.amazon_header_prefix ? "#{key}:#{value}" : value)
self << "\n"
end
- self << path
+ self << "/#{@options[:bucket]}#{path}"
end
def initialize_headers
diff --git a/lib/aws/s3/base.rb b/lib/aws/s3/base.rb
index 63abafa..4ca3b7c 100644
--- a/lib/aws/s3/base.rb
+++ b/lib/aws/s3/base.rb
@@ -63,10 +63,10 @@ class << self
#
# It is unlikely that you would call this method directly. Subclasses of Base have convenience methods for each http request verb
# that wrap calls to request.
- def request(verb, path, options = {}, body = nil, attempts = 0, &block)
+ def request(verb, bucket, path, options = {}, body = nil, attempts = 0, &block)
Service.response = nil
process_options!(options, verb)
- response = response_class.new(connection.request(verb, path, options, body, attempts, &block))
+ response = response_class.new(connection.request(verb, bucket, path, options, body, attempts, &block))
Service.response = response
Error::Response.new(response.response).error.raise if response.error?
@@ -85,8 +85,8 @@ def request(verb, path, options = {}, body = nil, attempts = 0, &block)
[:get, :post, :put, :delete, :head].each do |verb|
class_eval(<<-EVAL, __FILE__, __LINE__)
- def #{verb}(path, headers = {}, body = nil, &block)
- request(:#{verb}, path, headers, body, &block)
+ def #{verb}(bucket, path, headers = {}, body = nil, &block)
+ request(:#{verb}, bucket, path, headers, body, &block)
end
EVAL
end
diff --git a/lib/aws/s3/bucket.rb b/lib/aws/s3/bucket.rb
index 84d89d3..209eb56 100644
--- a/lib/aws/s3/bucket.rb
+++ b/lib/aws/s3/bucket.rb
@@ -76,7 +76,7 @@ class << self
# in the section called 'Setting access levels'.
def create(name, options = {})
validate_name!(name)
- put("/#{name}", options).success?
+ put(nil, "/#{name}", options).success?
end
# Fetches the bucket named name.
@@ -99,7 +99,7 @@ def create(name, options = {})
# There are several options which allow you to limit which objects are retrieved. The list of object filtering options
# are listed in the documentation for Bucket.objects.
def find(name = nil, options = {})
- new(get(path(name, options)).bucket)
+ new(get(bucket_name(name), path(name, options)).bucket)
end
# Return just the objects in the bucket named name.
@@ -178,7 +178,7 @@ def path(name, options = {})
options = name
name = nil
end
- "/#{bucket_name(name)}#{RequestOptions.process(options).to_query_string}"
+ "/#{RequestOptions.process(options).to_query_string}"
end
end
diff --git a/lib/aws/s3/connection.rb b/lib/aws/s3/connection.rb
index 1b91127..85e6081 100644
--- a/lib/aws/s3/connection.rb
+++ b/lib/aws/s3/connection.rb
@@ -23,15 +23,17 @@ def initialize(options = {})
connect
end
- def request(verb, path, headers = {}, body = nil, attempts = 0, &block)
+ def request(verb, bucket, path, headers = {}, body = nil, attempts = 0, request_options = {}, &block)
body.rewind if body.respond_to?(:rewind) unless attempts.zero?
requester = Proc.new do
path = self.class.prepare_path(path) if attempts.zero? # Only escape the path once
request = request_method(verb).new(path, headers)
+ ensure_same_bucket!(bucket)
+ ensure_header_hostname!(request)
ensure_content_type!(request)
add_user_agent!(request)
- authenticate!(request)
+ authenticate!(request, bucket)
if body
if body.respond_to?(:read)
request.body_stream = body
@@ -56,14 +58,16 @@ def request(verb, path, headers = {}, body = nil, attempts = 0, &block)
attempts == 3 ? raise : (attempts += 1; retry)
end
- def url_for(path, options = {})
- authenticate = options.delete(:authenticated)
+ def url_for(bucket, path, options = {})
+ authenticate = options.delete(:authenticated)
# Default to true unless explicitly false
- authenticate = true if authenticate.nil?
- path = self.class.prepare_path(path)
- request = request_method(:get).new(path, {})
- query_string = query_string_authentication(request, options)
- returning "#{protocol(options)}#{http.address}#{port_string}#{path}" do |url|
+ authenticate = true if authenticate.nil?
+ path = self.class.prepare_path(path)
+ request = request_method(:get).new(path, {})
+ options[:bucket] = bucket
+ query_string = query_string_authentication(request, options)
+ host = (subdomain == bucket ? http.address : "#{bucket}.#{DEFAULT_HOST}")
+ returning "#{protocol(options)}#{host}#{port_string}#{path}" do |url|
url << "?#{query_string}" if authenticate
end
end
@@ -121,13 +125,27 @@ def port_string
http.port == default_port ? '' : ":#{http.port}"
end
+ def ensure_header_hostname!(request)
+ request['Host'] = options[:server]
+ end
+
+ # Check if given bucket is the one defined uppon initialization.
+ # If not, close open connection and update bucket name
+ def ensure_same_bucket!(bucket)
+ if options[:bucket] != bucket
+ http.finish if persistent? and http and http.started?
+ options[:bucket] = bucket
+ options[:server] = bucket ? "#{options[:bucket]}.#{DEFAULT_HOST}" : DEFAULT_HOST
+ end
+ end
+
def ensure_content_type!(request)
request['Content-Type'] ||= 'binary/octet-stream'
end
# Just do Header authentication for now
- def authenticate!(request)
- request['Authorization'] = Authentication::Header.new(request, access_key_id, secret_access_key)
+ def authenticate!(request, bucket)
+ request['Authorization'] = Authentication::Header.new(request, access_key_id, secret_access_key, :bucket => bucket)
end
def add_user_agent!(request)
@@ -249,12 +267,12 @@ def default_connection
end
class Options < Hash #:nodoc:
- VALID_OPTIONS = [:access_key_id, :secret_access_key, :server, :port, :use_ssl, :persistent, :proxy].freeze
+ VALID_OPTIONS = [:access_key_id, :secret_access_key, :server, :port, :use_ssl, :persistent, :proxy, :bucket].freeze
def initialize(options = {})
super()
validate(options)
- replace(:server => DEFAULT_HOST, :port => (options[:use_ssl] ? 443 : 80))
+ replace(:server => (options[:bucket] ? "#{options[:bucket]}.#{DEFAULT_HOST}" : DEFAULT_HOST), :port => (options[:use_ssl] ? 443 : 80))
merge!(options)
end
diff --git a/lib/aws/s3/object.rb b/lib/aws/s3/object.rb
index bcdf9e1..81b917d 100644
--- a/lib/aws/s3/object.rb
+++ b/lib/aws/s3/object.rb
@@ -131,7 +131,7 @@ class << self
# === Other options
# * :range - Return only the bytes of the object in the specified range.
def value(key, bucket = nil, options = {}, &block)
- Value.new(get(path!(bucket, key, options), options, &block))
+ Value.new(get(bucket_name(bucket), path!(key, options), options, &block))
end
def stream(key, bucket = nil, options = {}, &block)
@@ -181,14 +181,26 @@ def find(key, bucket = nil)
# Makes a copy of the object with key to copy_key, preserving the ACL of the existing object if the :copy_acl option is true (default false).
def copy(key, copy_key, bucket = nil, options = {})
bucket = bucket_name(bucket)
- source_key = path!(bucket, key)
- default_options = {'x-amz-copy-source' => source_key}
- target_key = path!(bucket, copy_key)
- returning put(target_key, default_options) do
+ source_key = path_with_bucket!(bucket, key)
+ default_options = {'x-amz-copy-source' => URI.escape(source_key)}
+ target_key = path!(copy_key)
+ returning put(bucket, target_key, default_options) do
acl(copy_key, bucket, acl(key, bucket)) if options[:copy_acl]
end
end
+ # Makes a copy of the object with key to copy_key, preserving the ACL of the existing object if the :copy_acl option is true (default false).
+ def copy_to_bucket(key, copy_key, source_bucket, destination_bucket, options = {})
+ source_bucket = bucket_name(source_bucket)
+ destination_bucket = bucket_name(destination_bucket)
+ source_key = path_with_bucket!(source_bucket, key)
+ default_options = {'x-amz-copy-source' => URI.escape(source_key)}
+ target_key = path!(copy_key)
+ returning put(destination_bucket, target_key, default_options.merge(options)) do
+ acl(copy_key, destination_bucket, acl(key, source_bucket)) if options[:copy_acl]
+ end
+ end
+
# Rename the object with key from to have key in to.
def rename(from, to, bucket = nil, options = {})
copy(from, to, bucket, options)
@@ -200,7 +212,7 @@ def rename(from, to, bucket = nil, options = {})
#
# If the specified key does not exist, NoSuchKey is raised.
def about(key, bucket = nil, options = {})
- response = head(path!(bucket, key, options), options)
+ response = head(bucket_name(bucket), path!(key, options), options)
raise NoSuchKey.new("No such key `#{key}'", bucket) if response.code == 404
About.new(response.headers)
end
@@ -220,7 +232,7 @@ def exists?(key, bucket = nil)
def delete(key, bucket = nil, options = {})
# A bit confusing. Calling super actually makes an HTTP DELETE request. The delete method is
# defined in the Base class. It happens to have the same name.
- super(path!(bucket, key, options), options).success?
+ super(bucket_name(bucket), path!(key, options), options).success?
end
# When storing an object on the S3 servers using S3Object.store, the data argument can be a string or an I/O stream.
@@ -235,10 +247,12 @@ def delete(key, bucket = nil, options = {})
def store(key, data, bucket = nil, options = {})
validate_key!(key)
# Must build path before infering content type in case bucket is being used for options
- path = path!(bucket, key, options)
+ path = path!(key, options)
infer_content_type!(key, options)
- put(path, options, data) # Don't call .success? on response. We want to get the etag.
+ # Don't call .success? on response. We want to get the etag.
+ put(bucket_name(bucket), path, options, data)
+
end
alias_method :create, :store
alias_method :save, :store
@@ -288,10 +302,14 @@ def store(key, data, bucket = nil, options = {})
# :authenticated => false)
# # => http://s3.amazonaws.com/marcel/beluga_baby.jpg
def url_for(name, bucket = nil, options = {})
- connection.url_for(path!(bucket, name, options), options) # Do not normalize options
+ connection.url_for(bucket_name(bucket), path!(name, options), options) # Do not normalize options
+ end
+
+ def path!(name, options = {}) #:nodoc:
+ "/#{name}"
end
- def path!(bucket, name, options = {}) #:nodoc:
+ def path_with_bucket!(bucket, name, options = {})
# We're using the second argument for options
if bucket.is_a?(Hash)
options.replace(bucket)
diff --git a/test/remote/object_test.rb b/test/remote/object_test.rb
index c63076c..f19bb29 100644
--- a/test/remote/object_test.rb
+++ b/test/remote/object_test.rb
@@ -176,6 +176,26 @@ def test_object
assert_equal object.value, copy.value
assert_equal object.content_type, copy.content_type
+ # Test copy to an filename with an accent
+ copy_to_accent = nil
+ assert_nothing_raised do
+ object.copy('testing_s3objects-copy-to-accent-é')
+ copy_to_accent = S3Object.find('testing_s3objects-copy-to-accent-é', TEST_BUCKET)
+ assert copy_to_accent
+ assert_equal copy_to_accent.value, object.value
+ assert_equal copy_to_accent.content_type, object.content_type
+ end
+
+ # Test copy from an filename with an accent
+ assert_nothing_raised do
+ object_with_accent = S3Object.find('testing_s3objects-copy-to-accent-é')
+ object_with_accent.copy('testing_s3objects-copy-from-accent')
+ copy_from_accent = S3Object.find('testing_s3objects-copy-from-accent', TEST_BUCKET)
+ assert copy_from_accent
+ assert_equal copy_from_accent.value, object_with_accent.value
+ assert_equal copy_from_accent.content_type, object_with_accent.content_type
+ end
+
# Delete object
assert_nothing_raised do