Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/aws/s3/acl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions lib/aws/s3/authentication.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions lib/aws/s3/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions lib/aws/s3/bucket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 <tt>name</tt>.
Expand All @@ -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 <tt>name</tt>.
Expand Down Expand Up @@ -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

Expand Down
44 changes: 31 additions & 13 deletions lib/aws/s3/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
40 changes: 29 additions & 11 deletions lib/aws/s3/object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ class << self
# === Other options
# * <tt>:range</tt> - 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)
Expand Down Expand Up @@ -181,14 +181,26 @@ def find(key, bucket = nil)
# Makes a copy of the object with <tt>key</tt> to <tt>copy_key</tt>, preserving the ACL of the existing object if the <tt>:copy_acl</tt> 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 <tt>key</tt> to <tt>copy_key</tt>, preserving the ACL of the existing object if the <tt>:copy_acl</tt> 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 <tt>from</tt> to have key in <tt>to</tt>.
def rename(from, to, bucket = nil, options = {})
copy(from, to, bucket, options)
Expand All @@ -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
Expand All @@ -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 <tt>data</tt> argument can be a string or an I/O stream.
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
20 changes: 20 additions & 0 deletions test/remote/object_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down