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