From maccman at gmail.com Mon Jan 1 10:24:44 2007 From: maccman at gmail.com (Alex MacCaw) Date: Mon, 1 Jan 2007 15:24:44 +0000 Subject: [s3-dev] SSL_write:: bad start line Message-ID: <14cc92570701010724o19bc7c36td87b47c281f85aeb@mail.gmail.com> Seems I've found a bug... I left a backgroundrb script running for a couple of hours (with the library running in it). After a lengthy period, the first time I tried to upload a file it gave the error "SSL_write:: bad start line". After this, when I tried again, it gave the error: "SSL_write:: bad write retry" I'll try and see what happens without using SSL and without :persistent turned on. -- http://www.eribium.org | http://juggernaut.rubyforge.org -------------- next part -------------- An HTML attachment was scrubbed... URL: http://rubyforge.org/pipermail/amazon-s3-dev/attachments/20070101/12591501/attachment.html From jhosteny at gmail.com Tue Jan 2 10:21:14 2007 From: jhosteny at gmail.com (Joe Hosteny) Date: Tue, 2 Jan 2007 10:21:14 -0500 Subject: [s3-dev] A bit off topic In-Reply-To: <20061229062302.GI82301@comox.textdrive.com> References: <2f0ee70f0612281739l5f2df0c6t46b0c3c7d27c7ca1@mail.gmail.com> <20061229062302.GI82301@comox.textdrive.com> Message-ID: <2f0ee70f0701020721v12bd314fof21dada264dca812@mail.gmail.com> On 12/29/06, Marcel Molina Jr. wrote: > On Thu, Dec 28, 2006 at 08:39:53PM -0500, beechflyer74 wrote: > > Thanks for the library. It's been very useful for me already. > > Hey thanks, my pleasure. > > > I was wondering if you intend on releasing a library for EC2 as well? > > If this is in the works, I'd be willing to contribute. > > Cool. Yeah, I've been planning on it, as are others on this list I've gathered. One of > the first steps as I see it is extracting reusable bits of the S3 > code into a set of classes that all AWS libraries could use. For example, the > authentication code, the request code and the connection management code. > This would then become a gem dependency for all AWS libs that I work on (or > anyone else for that matter if they are interested). Sounds good. I was thinking the same, so I have been spending time closely reading the code and the S3 API documentation. After that, I was going to sketch out how I thought the EC2 stuff could fit into the picture. > I'm a bit fatigued from working on the S3 library to jump right into EC2 > immediately (after the "holidays" I think I might be all set) but the clock > is a ticking and there's code to be written. Several people have also > expressed interest. > > The 'amazon' project on rubyforge is intended to be an AWS umbrella. I'm also > planning on writing a lib for SQS (which is way simpler than EC2). > > I'll give a holler on this list if I get working on anything. I encourage you > to do the same. > Will do. > marcel > -- > Marcel Molina Jr. > _______________________________________________ > amazon-s3-dev mailing list > amazon-s3-dev at rubyforge.org > http://rubyforge.org/mailman/listinfo/amazon-s3-dev > From metalhead at metalhead.ws Fri Jan 5 10:49:00 2007 From: metalhead at metalhead.ws (Metalhead) Date: Fri, 5 Jan 2007 16:49:00 +0100 Subject: [s3-dev] adding metadata before upload In-Reply-To: <14cc92570612311114g88e8468we941052e4cbf834e@mail.gmail.com> References: <14cc92570612311114g88e8468we941052e4cbf834e@mail.gmail.com> Message-ID: <20070105164900.3079519e@huginn.asgard.yggdrasill> > If I create a new object, I can seem to set metadata or content_type. For > example: > aws = AWS::S3:: Bucket.find(asset.bucket.s3_name).new_object > aws.key = asset.s3_name > aws.value = asset.file_data > aws.content_type = asset.content_type # Doesn't work > aws.metadata[:encrypted] = asset.encrypted # Neither does this > aws.store > I get an argument error, "wrong number of arguments (0 for 1)". > I would recommend allowing people to add metadata in the store class method > (this is what I tried to do originally). That's because metadata for new objects is handled incorrectly, i.e. not stored. More specifically, the error message you see is caused by the call to About.new in object.rb:502, which has been declared to require an argument for the initializer in object.rb:315. Lars -- Tired? Try a scroll of charging on yourself. -------------- next part -------------- A non-text attachment was scrubbed... Name: signature.asc Type: application/pgp-signature Size: 198 bytes Desc: not available Url : http://rubyforge.org/pipermail/amazon-s3-dev/attachments/20070105/26330b16/attachment.bin From jhosteny at gmail.com Fri Jan 5 17:31:28 2007 From: jhosteny at gmail.com (Joe Hosteny) Date: Fri, 5 Jan 2007 17:31:28 -0500 Subject: [s3-dev] EC2 preliminary work Message-ID: <2f0ee70f0701051431i7d7dd125ufd51191610eb87c7@mail.gmail.com> Marcel, Here was a first cut at re-factoring some code from AWS::S3 into the AWS module so that EC2 stuff could be added. Go easy on me - I was trying to learn the code as I went, and I think I made a bit of a mess. But it may be useful to you to see some of this. Feel free to use all or none of it as you see fit. Mu understanding is that the Amazon folks will be providing a REST interface for EC2. I wanted to make sure it kept an eye open to that since that would probably be the preferred implementation. Also, as you're probably aware, the query string authentication is different for EC2. It's not hard though; the canonicalized string calculation is slightly different. This passes all the regressions (though some tests have been modified), and is a patch against the repo at revision 158. Also, if you have some ideas as to how you would ultimately like this to look, let me know. I'd be happy to redo this with your input. Regards, Joe -------------- next part -------------- Index: test/test_helper.rb =================================================================== --- test/test_helper.rb (revision 158) +++ test/test_helper.rb (working copy) @@ -78,5 +78,6 @@ end class Test::Unit::TestCase + include AWS include AWS::S3 -end \ No newline at end of file +end Index: test/error_test.rb =================================================================== --- test/error_test.rb (revision 158) +++ test/error_test.rb (working copy) @@ -3,7 +3,7 @@ class ErrorTest < Test::Unit::TestCase def setup @container = AWS::S3 - @error = Error.new(Parsing::XmlParser.new(Fixtures::Errors.access_denied)) + @error = Error.new(Parsing::XmlParser.new(Fixtures::Errors.access_denied), @container) end def teardown @@ -12,7 +12,7 @@ def test_error_class_is_automatically_generated assert !@container.const_defined?('NotImplemented') - error = Error.new(Parsing::XmlParser.new(Fixtures::Errors.not_implemented)) + error = Error.new(Parsing::XmlParser.new(Fixtures::Errors.not_implemented), @container) assert @container.const_defined?('NotImplemented') end @@ -35,7 +35,7 @@ def test_response_is_passed_along_to_exception response = Error::Response.new(FakeResponse.new(:code => 409, :body => Fixtures::Errors.access_denied)) response.error.raise - rescue @container::ResponseError => e + rescue AWS::ResponseError => e assert e.response assert_kind_of Error::Response, e.response assert_equal response.error, e.response.error @@ -48,8 +48,8 @@ @container.const_set(:NotImplemented, Class.new) assert @container.const_defined?(:NotImplemented) - assert_raises(ExceptionClassClash) do - Error.new(Parsing::XmlParser.new(Fixtures::Errors.not_implemented)) + assert_raises(AWS::ExceptionClassClash) do + Error.new(Parsing::XmlParser.new(Fixtures::Errors.not_implemented), @container) end end Index: test/response_test.rb =================================================================== --- test/response_test.rb (revision 158) +++ test/response_test.rb (working copy) @@ -50,21 +50,21 @@ end def test_on_base - assert_equal Base::Response, FindResponseClass.for(Base) - assert_equal Base::Response, FindResponseClass.for(AWS::S3::Base) + assert_equal Base::Response, FindResponseClass.for(Base, AWS::S3) + assert_equal S3Base::Response, FindResponseClass.for(AWS::S3::S3Base, AWS::S3) end def test_on_subclass_with_corresponding_response_class - assert_equal Bucket::Response, FindResponseClass.for(Bucket) - assert_equal Bucket::Response, FindResponseClass.for(AWS::S3::Bucket) + assert_equal Bucket::Response, FindResponseClass.for(Bucket, AWS::S3) + assert_equal Bucket::Response, FindResponseClass.for(AWS::S3::Bucket, AWS::S3) end def test_on_subclass_with_intermediary_parent_that_has_corresponding_response_class - assert_equal Bucket::Response, FindResponseClass.for(CampfireBucket) + assert_equal Bucket::Response, FindResponseClass.for(CampfireBucket, AWS::S3) end def test_on_subclass_with_no_corresponding_response_class_and_no_intermediary_parent - assert_equal Base::Response, FindResponseClass.for(BabyBase) + assert_equal Base::Response, FindResponseClass.for(BabyBase, AWS::S3) end -end \ No newline at end of file +end Index: test/base_test.rb =================================================================== --- test/base_test.rb (revision 158) +++ test/base_test.rb (working copy) @@ -16,10 +16,11 @@ def test_respond_with assert_equal Base::Response, Base.send(:response_class) - Base.send(:respond_with, Bucket::Response) do - assert_equal Bucket::Response, Base.send(:response_class) + assert_equal S3Base::Response, S3Base.send(:response_class) + S3Base.send(:respond_with, Bucket::Response) do + assert_equal Bucket::Response, S3Base.send(:response_class) end - assert_equal Base::Response, Base.send(:response_class) + assert_equal S3Base::Response, S3Base.send(:response_class) end def test_request_tries_again_when_encountering_an_internal_error @@ -80,7 +81,7 @@ end class MultiConnectionsTest < Test::Unit::TestCase - class ClassToTestSettingCurrentBucket < Base + class ClassToTestSettingCurrentBucket < S3Base set_current_bucket_to 'foo' end @@ -93,7 +94,7 @@ assert !Base.connected? assert_raises(MissingAccessKey) do - Base.establish_connection! + S3Base.establish_connection! end assert !Base.connected? @@ -103,7 +104,7 @@ end assert_nothing_raised do - Base.establish_connection!(:access_key_id => '123', :secret_access_key => 'abc') + S3Base.establish_connection!(:access_key_id => '123', :secret_access_key => 'abc') end assert Base.connected? @@ -113,22 +114,22 @@ end # All subclasses are currently using the default connection - assert Base.connection == Bucket.connection + assert S3Base.connection == Bucket.connection # No need to pass in the required options. The default connection will supply them assert_nothing_raised do Bucket.establish_connection!(:server => 'foo.s3.amazonaws.com') end - assert Base.connection != Bucket.connection + assert S3Base.connection != Bucket.connection assert_equal '123', Bucket.connection.access_key_id assert_equal 'foo', Bucket.connection.subdomain end def test_current_bucket - Base.establish_connection!(:access_key_id => '123', :secret_access_key => 'abc') + S3Base.establish_connection!(:access_key_id => '123', :secret_access_key => 'abc') assert_raises(CurrentBucketNotSpecified) do - Base.current_bucket + S3Base.current_bucket end S3Object.establish_connection!(:server => 'foo-bucket.s3.amazonaws.com') Index: test/mocks/base.rb =================================================================== --- test/mocks/base.rb (revision 158) +++ test/mocks/base.rb (working copy) @@ -1,89 +1,91 @@ require_library_or_gem 'flexmock' module AWS - module S3 - class FakeResponse < String - attr_reader :code, :body, :headers - def initialize(options = {}) - @code = options.delete(:code) || 200 - @body = options.delete(:body) || '' - @headers = {'content-type' => 'application/xml'}.merge(options.delete(:headers) || {}) - super(@body) - end + class FakeResponse < String + attr_reader :code, :body, :headers + def initialize(options = {}) + @code = options.delete(:code) || 200 + @body = options.delete(:body) || '' + @headers = {'content-type' => 'application/xml'}.merge(options.delete(:headers) || {}) + super(@body) + end - # For ErrorResponse - def response - self + # For ErrorResponse + def response + self + end + + def [](header) + headers[header] + end + + def each(&block) + headers.each(&block) + end + alias_method :each_header, :each + end + + class Base + class << self + @@responses = [] + @@in_test_mode = false + @@catch_all_response = nil + + def in_test_mode=(boolean) + @@in_test_mode = boolean end - def [](header) - headers[header] + def responses + @@responses end - def each(&block) - headers.each(&block) + def catch_all_response + @@catch_all_response end - alias_method :each_header, :each - end - - class Base - class << self - @@responses = [] - @@in_test_mode = false - @@catch_all_response = nil - - def in_test_mode=(boolean) - @@in_test_mode = boolean - end - - def responses - @@responses - end - - def catch_all_response - @@catch_all_response - end - def reset! - responses.clear - end - - def request_returns(response_data) - responses.concat [response_data].flatten.map {|data| FakeResponse.new(data)} - end - - def request_always_returns(response_data, &block) - in_test_mode do - @@catch_all_response = FakeResponse.new(response_data) - yield - @@catch_all_response = nil - end - end - - def in_test_mode(&block) - self.in_test_mode = true + def reset! + responses.clear + end + + def request_returns(response_data) + responses.concat [response_data].flatten.map {|data| FakeResponse.new(data)} + end + + def request_always_returns(response_data, &block) + in_test_mode do + @@catch_all_response = FakeResponse.new(response_data) yield - ensure - self.in_test_mode = false + @@catch_all_response = nil end - - alias_method :old_connection, :connection - def connection - if @@in_test_mode - @mock_connection ||= - begin - mock_connection = FlexMock.new - mock_connection.mock_handle(:request) do - raise 'No responses left' unless response = catch_all_response || responses.shift - response - end - mock_connection + end + + def in_test_mode(&block) + self.in_test_mode = true + yield + ensure + self.in_test_mode = false + end + + alias_method :old_connection, :connection + def connection + if @@in_test_mode + @mock_connection ||= + begin + mock_connection = FlexMock.new + mock_connection.mock_handle(:request) do + raise 'No responses left' unless response = catch_all_response || responses.shift + response end - else - old_connection - end + mock_connection + end + else + old_connection end end end end + module S3 + class Base < AWS::Base + end + end end \ No newline at end of file Index: lib/aws/response.rb =================================================================== --- lib/aws/response.rb (revision 0) +++ lib/aws/response.rb (revision 0) @@ -0,0 +1,157 @@ +#:stopdoc: +module AWS + class Base + class Response < String + attr_reader :response, :body, :parsed + def initialize(response) + @response = response + @body = response.body.to_s + super(body) + end + + def container + AWS + end + + def headers + headers = {} + response.each do |header, value| + headers[header] = value + end + headers + end + memoized :headers + + def [](header) + headers[header] + end + + def each(&block) + headers.each(&block) + end + + def code + response.code.to_i + end + + {:success => 200..299, :redirect => 300..399, + :client_error => 400..499, :server_error => 500..599}.each do |result, code_range| + class_eval(<<-EVAL, __FILE__, __LINE__) + def #{result}? + return false unless response + (#{code_range}).include? code + end + EVAL + end + + def error? + !success? && response['content-type'] == 'application/xml' && parsed.root == 'error' + end + + def error + Error.new(parsed, container, self) + end + memoized :error + + def parsed + # XmlSimple is picky about what kind of object it parses, so we pass in body rather than self + Parsing::XmlParser.new(body) + end + memoized :parsed + + def inspect + "#<%s:0x%s %s %s>" % [self.class, object_id, response.code, response.message] + end + end + end + + # Requests whose response code is between 300 and 599 and contain an in their body + # are wrapped in an Error::Response. This Error::Response contains an Error object which raises an exception + # that corresponds to the error in the response body. The exception object contains the ErrorResponse, so + # in all cases where a request happens, you can rescue ResponseError and have access to the ErrorResponse and + # its Error object which contains information about the ResponseError. + # + # begin + # Bucket.create(..) + # rescue ResponseError => exception + # exception.response + # # => + # exception.response.error + # # => + # end + class Error + class Response < Base::Response + def error? + true + end + + def inspect + "#<%s:0x%s %s %s: '%s'>" % [self.class.name, object_id, response.code, error.code, error.message] + end + end + end + + # Guess response class name from current class name. If the guessed response class doesn't exist + # do the same thing to the current class's parent class, up the inheritance heirarchy until either + # a response class is found or until we get to the top of the heirarchy in which case we just use + # the the Base response class. + # + # Important: This implemantation assumes that the Base class has a corresponding Base::Response. + class FindResponseClass #:nodoc: + class << self + def for(start, container) + new(start, container).find + end + end + + def initialize(start, container) + @container = container + @current_class = start + @start_class = start + end + + def find + loop do + break if response_class_found? + @current_class = @current_class.superclass + if @current_class.nil? + @current_class = @start_class + @container.to_s =~ /(.*?)::[^:]+$/ + path = $1.split("::") + @container = path.inject(Module.const_get(path.shift.to_s)) { |cont, mod| cont.const_get(mod.to_s) } + next + end + end + target.const_get(class_to_find) + end + + private + attr_reader :container + attr_accessor :current_class + + def target + container.const_get(current_name) + end + + def target? + container.const_defined?(current_name) + end + + def response_class_found? + target? && target.const_defined?(class_to_find) + end + + def class_to_find + :Response + end + + def current_name + truncate(current_class) + end + + def truncate(klass) + klass.name[/[^:]+$/] + end + end +end +#:startdoc: \ No newline at end of file Index: lib/aws/authentication.rb =================================================================== --- lib/aws/authentication.rb (revision 158) +++ lib/aws/authentication.rb (working copy) @@ -1,218 +1,91 @@ module AWS - module S3 - # All authentication is taken care of for you by the AWS::S3 library. None the less, some details of the two types - # of authentication and when they are used may be of interest to some. - # - # === Header based authentication - # - # Header based authentication is achieved by setting a special Authorization header whose value - # is formatted like so: - # - # "AWS #{access_key_id}:#{encoded_canonical}" - # - # The access_key_id is the public key that is assigned by Amazon for a given account which you use when - # establishing your initial connection. The encoded_canonical is computed according to rules layed out - # by Amazon which we will describe presently. - # - # ==== Generating the encoded canonical string - # - # The "canonical string", generated by the CanonicalString class, is computed by collecting the current request method, - # a set of significant headers of the current request, and the current request path into a string. - # That canonical string is then encrypted with the secret_access_key assigned by Amazon. The resulting encrypted canonical - # string is then base 64 encoded. - # - # === Query string based authentication - # - # When accessing a restricted object from the browser, you can authenticate via the query string, by setting the following parameters: - # - # "AWSAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}" - # - # The QueryString class is responsible for generating the appropriate parameters for authentication via the - # query string. - # - # The access_key_id and encoded_canonical are the same as described in the Header based authentication section. - # The expires value dictates for how long the current url is valid (by default, it will expire in 5 minutes). Expiration can be specified - # either by an absolute time (expressed in seconds since the epoch), or in relative time (in number of seconds from now). - # Details of how to customize the expiration of the url are provided in the documentation for the QueryString class. - # - # All requests made by this library use header authentication. When a query string authenticated url is needed, - # the S3Object#url method will include the appropriate query string parameters. - # - # === Full authentication specification - # - # The full specification of the authentication protocol can be found at - # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAuthentication.html - class Authentication - constant :AMAZON_HEADER_PREFIX, 'x-amz-' - - # Signature is the abstract super class for the Header and QueryString authentication methods. It does the job - # of computing the canonical_string using the CanonicalString class as well as encoding the canonical string. The subclasses - # parameterize these computations and arrange them in a string form appropriate to how they are used, in one case a http request - # header value, and in the other case key/value query string parameter pairs. - class Signature < String #:nodoc: - attr_reader :request, :access_key_id, :secret_access_key + # All authentication is taken care of for you by the AWS::S3 library. None the less, some details of the two types + # of authentication and when they are used may be of interest to some. + # + # === Header based authentication + # + # Header based authentication is achieved by setting a special Authorization header whose value + # is formatted like so: + # + # "AWS #{access_key_id}:#{encoded_canonical}" + # + # The access_key_id is the public key that is assigned by Amazon for a given account which you use when + # establishing your initial connection. The encoded_canonical is computed according to rules layed out + # by Amazon which we will describe presently. + # + # ==== Generating the encoded canonical string + # + # The "canonical string", generated by the CanonicalString class, is computed by collecting the current request method, + # a set of significant headers of the current request, and the current request path into a string. + # That canonical string is then encrypted with the secret_access_key assigned by Amazon. The resulting encrypted canonical + # string is then base 64 encoded. + # + # === Query string based authentication + # + # When accessing a restricted object from the browser, you can authenticate via the query string, by setting the following parameters: + # + # "AWSAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}" + # + # The QueryString class is responsible for generating the appropriate parameters for authentication via the + # query string. + # + # The access_key_id and encoded_canonical are the same as described in the Header based authentication section. + # The expires value dictates for how long the current url is valid (by default, it will expire in 5 minutes). Expiration can be specified + # either by an absolute time (expressed in seconds since the epoch), or in relative time (in number of seconds from now). + # Details of how to customize the expiration of the url are provided in the documentation for the QueryString class. + # + # All requests made by this library use header authentication. When a query string authenticated url is needed, + # the S3Object#url method will include the appropriate query string parameters. + # + # === Full authentication specification + # + # The full specification of the authentication protocol can be found at + # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAuthentication.html + module Authentication + constant :AMAZON_HEADER_PREFIX, 'x-amz-' + + def self.included(base) + constants.each do |c| + base.const_set(c, const_get(c)) + end + end + + # Signature is the abstract super class for the Header and QueryString authentication methods. It does the job + # of computing the canonical_string using the CanonicalString class as well as encoding the canonical string. The subclasses + # parameterize these computations and arrange them in a string form appropriate to how they are used, in one case a http request + # header value, and in the other case key/value query string parameter pairs. + class BaseSignature < String #:nodoc: + attr_reader :request, :access_key_id, :secret_access_key + + def initialize(request, access_key_id, secret_access_key, options = {}) + super() + @request, @access_key_id, @secret_access_key = request, access_key_id, secret_access_key + @options = options + end + + private - def initialize(request, access_key_id, secret_access_key, options = {}) - super() - @request, @access_key_id, @secret_access_key = request, access_key_id, secret_access_key - @options = options + def canonical_string + raise NotImplementedError end - private - - def canonical_string - options = {} - options[:expires] = expires if expires? - CanonicalString.new(request, options) - end - memoized :canonical_string - - def encoded_canonical - digest = OpenSSL::Digest::Digest.new('sha1') - b64_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, secret_access_key, canonical_string)).strip - url_encode? ? CGI.escape(b64_hmac) : b64_hmac - end - - def url_encode? - !@options[:url_encode].nil? - end - - def expires? - is_a? QueryString - end - - def date - request['date'].to_s.strip.empty? ? Time.now : Time.parse(request['date']) - end - end - - # Provides header authentication by computing the value of the Authorization header. More details about the - # various authentication schemes can be found in the docs for its containing module, Authentication. - class Header < Signature #:nodoc: - def initialize(*args) - super - self << "AWS #{access_key_id}:#{encoded_canonical}" + def encoded_canonical + digest = OpenSSL::Digest::Digest.new('sha1') + b64_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, secret_access_key, canonical_string)).strip + url_encode? ? CGI.escape(b64_hmac) : b64_hmac end - end - - # Provides query string authentication by computing the three authorization parameters: AWSAccessKeyId, Expires and Signature. - # More details about the various authentication schemes can be found in the docs for its containing module, Authentication. - class QueryString < Signature #:nodoc: - constant :DEFAULT_EXPIRY, 300 # 5 minutes - def initialize(*args) - super - @options[:url_encode] = true - self << build + def url_encode? + !@options[:url_encode].nil? end - private - - # Will return one of three values, in the following order of precedence: - # - # 1) Seconds since the epoch explicitly passed in the +:expires+ option - # 2) The current time in seconds since the epoch plus the number of seconds passed in - # the +:expires_in+ option - # 3) The current time in seconds since the epoch plus the default number of seconds (60 seconds) - def expires - return @options[:expires] if @options[:expires] - date.to_i + (@options[:expires_in] || DEFAULT_EXPIRY) - end - - # Keep in alphabetical order - def build - "AWSAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}" - end - end - - # The CanonicalString is used to generate an encrypted signature, signed with your secrect access key. It is composed of - # data related to the given request for which it provides authentication. This data includes the request method, request headers, - # and the request path. Both Header and QueryString use it to generate their signature. - class CanonicalString < String #:nodoc: - class << self - def default_headers - %w(content-type content-md5) - end - - def interesting_headers - ['content-md5', 'content-type', 'date', amazon_header_prefix] - end - - def amazon_header_prefix - /^#{AMAZON_HEADER_PREFIX}/io - end + def expires? + false end - attr_reader :request, :headers - - def initialize(request, options = {}) - super() - @request = request - @headers = {} - @options = 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 - build + def date + request['date'].to_s.strip.empty? ? Time.now : Time.parse(request['date']) end - - private - def build - self << "#{request.method}\n" - ensure_date_is_valid - - initialize_headers - set_expiry! - - headers.sort_by {|k, _| k}.each do |key, value| - value = value.to_s.strip - self << (key =~ self.class.amazon_header_prefix ? "#{key}:#{value}" : value) - self << "\n" - end - self << path - end - - def initialize_headers - identify_interesting_headers - set_default_headers - end - - def set_expiry! - self.headers['date'] = @options[:expires] if @options[:expires] - end - - def ensure_date_is_valid - request['Date'] ||= Time.now.httpdate - end - - def identify_interesting_headers - request.each do |key, value| - key = key.downcase # Can't modify frozen string so no bang - if self.class.interesting_headers.any? {|header| header === key} - self.headers[key] = value.to_s.strip - end - end - end - - def set_default_headers - self.class.default_headers.each do |header| - self.headers[header] ||= '' - end - end - - def path - [only_path, extract_significant_parameter].compact.join('?') - end - - def extract_significant_parameter - request.path[/[&?](acl|torrent|logging)(?:&|=|$)/, 1] - end - - def only_path - request.path[/^[^?]*/] - end - end end end end \ No newline at end of file Index: lib/aws/extensions.rb =================================================================== --- lib/aws/extensions.rb (revision 158) +++ lib/aws/extensions.rb (working copy) @@ -213,6 +213,49 @@ end end if Class.instance_methods(false).grep(/^cattr_(?:reader|writer|accessor)$/).empty? +class Module # :nodoc: + def mattr_reader(*syms) + syms.flatten.each do |sym| + module_eval(<<-EOS, __FILE__, __LINE__) + unless defined? @@#{sym} + @@#{sym} = nil + end + + def self.#{sym} + @@#{sym} + end + + def #{sym} + @@#{sym} + end + EOS + end + end + + def mattr_writer(*syms) + syms.flatten.each do |sym| + module_eval(<<-EOS, __FILE__, __LINE__) + unless defined? @@#{sym} + @@#{sym} = nil + end + + def self.#{sym}=(obj) + @@#{sym} = obj + end + + def #{sym}=(obj) + @@#{sym} = obj + end + EOS + end + end + + def mattr_accessor(*syms) + mattr_reader(*syms) + mattr_writer(*syms) + end +end if Module.instance_methods(false).grep(/^mattr_(?:reader|writer|accessor)$/).empty? + module SelectiveAttributeProxy def self.included(klass) klass.extend(ClassMethods) Index: lib/aws/connection.rb =================================================================== --- lib/aws/connection.rb (revision 158) +++ lib/aws/connection.rb (working copy) @@ -1,259 +1,259 @@ module AWS - module S3 - class Connection #:nodoc: - class << self - def connect(options = {}) - new(options) - end + constant :DEFAULT_HOST, 's3.amazonaws.com' + class Connection #:nodoc: + class << self + def connect(options = {}) + new(options) + end + + def prepare_path(path) + path = path.remove_extended unless path.utf8? + URI.escape(path) + end + end + + attr_reader :access_key_id, :secret_access_key, :http, :options + + # Creates a new connection. Connections make the actual requests to S3, though these requests are usually + # called from subclasses of Base. + # + # For details on establishing connections, check the Connection::Management::ClassMethods. + def initialize(options = {}) + @options = Options.new(options) + connect + end - def prepare_path(path) - path = path.remove_extended unless path.utf8? - URI.escape(path) + def request(verb, path, headers = {}, body = nil, attempts = 0, &block) + body.rewind if body.respond_to?(:rewind) unless attempts.zero? + + requester = Proc.new do + path = self.class.prepare_path(path) + request = request_method(verb).new(path, headers) + ensure_content_type!(request) + add_user_agent!(request) + authenticate!(request) + if body + if body.respond_to?(:read) + request.body_stream = body + request.content_length = body.respond_to?(:lstat) ? body.lstat.size : body.size + else + request.body = body + end end + http.request(request, &block) end - attr_reader :access_key_id, :secret_access_key, :http, :options + if persistent? + http.start unless http.started? + requester.call + else + http.start(&requester) + end + rescue Errno::EPIPE, Timeout::Error, Errno::EPIPE, Errno::EINVAL + @http = create_connection + attempts == 3 ? raise : (attempts += 1; retry) + end + + def url_for(path, options = {}) + path = self.class.prepare_path(path) + request = request_method(:get).new(path, {}) + query_string = query_string_authentication(request, options) + "#{protocol(options)}#{http.address}#{path}?#{query_string}" + end + + def subdomain + http.address[/^([^.]+).#{DEFAULT_HOST}$/, 1] + end + + def persistent? + options[:persistent] + end + + def protocol(options = {}) + (options[:use_ssl] || http.use_ssl?) ? 'https://' : 'http://' + end + + private + def extract_keys! + missing_keys = [] + extract_key = Proc.new {|key| options[key] || (missing_keys.push(key); nil)} + @access_key_id = extract_key[:access_key_id] + @secret_access_key = extract_key[:secret_access_key] + raise MissingAccessKey.new(missing_keys) unless missing_keys.empty? + end - # Creates a new connection. Connections make the actual requests to S3, though these requests are usually - # called from subclasses of Base. - # - # For details on establishing connections, check the Connection::Management::ClassMethods. - def initialize(options = {}) - @options = Options.new(options) - connect + def create_connection + http = Net::HTTP.new(options[:server], options[:port]) + http.use_ssl = !options[:use_ssl].nil? || options[:port] == 443 + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + http end - - def request(verb, path, headers = {}, body = nil, attempts = 0, &block) - body.rewind if body.respond_to?(:rewind) unless attempts.zero? - - requester = Proc.new do - path = self.class.prepare_path(path) - request = request_method(verb).new(path, headers) - ensure_content_type!(request) - add_user_agent!(request) - authenticate!(request) - if body - if body.respond_to?(:read) - request.body_stream = body - request.content_length = body.respond_to?(:lstat) ? body.lstat.size : body.size - else - request.body = body - end - end - http.request(request, &block) - end - - if persistent? - http.start unless http.started? - requester.call - else - http.start(&requester) - end - rescue Errno::EPIPE, Timeout::Error, Errno::EPIPE, Errno::EINVAL + + def connect + extract_keys! @http = create_connection - attempts == 3 ? raise : (attempts += 1; retry) end - def url_for(path, options = {}) - path = self.class.prepare_path(path) - request = request_method(:get).new(path, {}) - query_string = query_string_authentication(request, options) - "#{protocol(options)}#{http.address}#{path}?#{query_string}" + def ensure_content_type!(request) + request['Content-Type'] ||= 'binary/octet-stream' end - def subdomain - http.address[/^([^.]+).#{DEFAULT_HOST}$/, 1] + # Just do Header authentication for now + def authenticate!(request) + request['Authorization'] = Authentication::Header.new(request, access_key_id, secret_access_key) end - def persistent? - options[:persistent] + def add_user_agent!(request) + request['User-Agent'] ||= "AWS::S3/#{Version}" end - def protocol(options = {}) - (options[:use_ssl] || http.use_ssl?) ? 'https://' : 'http://' + def query_string_authentication(request, options = {}) + Authentication::QueryString.new(request, access_key_id, secret_access_key, options) end + + def request_method(verb) + Net::HTTP.const_get(verb.to_s.capitalize) + end - private - def extract_keys! - missing_keys = [] - extract_key = Proc.new {|key| options[key] || (missing_keys.push(key); nil)} - @access_key_id = extract_key[:access_key_id] - @secret_access_key = extract_key[:secret_access_key] - raise MissingAccessKey.new(missing_keys) unless missing_keys.empty? + def method_missing(method, *args, &block) + options[method] || super + end + + module Management #:nodoc: + def self.included(base) + base.cattr_accessor :connections + base.connections = {} + base.extend ClassMethods + end + + # Manage the creation and destruction of connections for AWS::S3::Base and its subclasses. Connections are + # created with establish_connection!. + module ClassMethods + # Creates a new connection with which to make requests to the S3 servers for the calling class. + # + # AWS::S3::Base.establish_connection!(:access_key_id => '...', :secret_access_key => '...') + # + # You can set connections for every subclass of AWS::S3::Base. Once the initial connection is made on + # Base, all subsequent connections will inherit whatever values you don't specify explictly. This allows you to + # customize details of the connection, such as what server the requests are made to, by just specifying one + # option. + # + # AWS::S3::Bucket.established_connection!(:use_ssl => true) + # + # The Bucket connection would inherit the :access_key_id and the :secret_access_key from + # Base's connection. Unlike the Base connection, all Bucket requests would be made over SSL. + # + # == Required arguments + # + # * :access_key_id - The access key id for your S3 account. Provided by Amazon. + # * :secret_access_key - The secret access key for your S3 account. Provided by Amazon. + # + # If any of these required arguments is missing, a MissingAccessKey exception will be raised. + # + # == Optional arguments + # + # * :server - The server to make requests to. You can use this to specify your bucket in the subdomain, + # or your own domain's cname if you are using virtual hosted buckets. Defaults to s3.amazonaws.com. + # * :port - The port to the requests should be made on. Defaults to 80 or 443 if the :use_ssl + # argument is set. + # * :use_ssl - Whether requests should be made over SSL. If set to true, the :port argument + # will be implicitly set to 443, unless specified otherwise. Defaults to false. + # * :persistent - Whether to use a persistent connection to the server. Having this on provides around a two fold + # performance increase but for long running processes some firewalls may find the long lived connection suspicious and close the connection. + # If you run into connection errors, try setting :persistent to false. Defaults to true. + def establish_connection!(options = {}) + # After you've already established the default connection, just specify + # the difference for subsequent connections + options = default_connection.options.merge(options) if connected? + connections[connection_name] = Connection.connect(options) end - def create_connection - http = Net::HTTP.new(options[:server], options[:port]) - http.use_ssl = !options[:use_ssl].nil? || options[:port] == 443 - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - http + # Returns the connection for the current class, or Base's default connection if the current class does not + # have its own connection. + # + # If not connection has been established yet, NoConnectionEstablished will be raised. + def connection + if connected? + connections[connection_name] || default_connection + else + raise NoConnectionEstablished + end end - def connect - extract_keys! - @http = create_connection + # Returns true if a connection has been made yet. + def connected? + !connections.empty? end - def ensure_content_type!(request) - request['Content-Type'] ||= 'binary/octet-stream' + # Removes the connection for the current class. If there is no connection for the current class, the default + # connection will be removed. + def disconnect(name = connection_name) + name = default_connection unless connections.has_key?(name) + connection = connections[name] + connection.http.finish if connection.persistent? + connections.delete(name) end - # Just do Header authentication for now - def authenticate!(request) - request['Authorization'] = Authentication::Header.new(request, access_key_id, secret_access_key) + # Clears *all* connections, from all classes, with prejudice. + def disconnect! + connections.each_key {|connection| disconnect(connection)} end - - def add_user_agent!(request) - request['User-Agent'] ||= "AWS::S3/#{Version}" - end - - def query_string_authentication(request, options = {}) - Authentication::QueryString.new(request, access_key_id, secret_access_key, options) - end - def request_method(verb) - Net::HTTP.const_get(verb.to_s.capitalize) + private + def connection_name + name + end + + def default_connection_name + # TODO: fix for EC2 + 'AWS::S3::S3Base' + end + + def default_connection + connections[default_connection_name] + end + end + end + + class Options < Hash #:nodoc: + class << self + def valid_options + [:access_key_id, :secret_access_key, :server, :port, :use_ssl, :persistent] end - - def method_missing(method, *args, &block) - options[method] || super + end + + attr_reader :options + def initialize(options = {}) + super() + @options = options + validate! + extract_persistent! + extract_server! + extract_port! + extract_remainder! + end + + private + def extract_persistent! + self[:persistent] = options.has_key?(:persitent) ? options[:persitent] : true end - module Management #:nodoc: - def self.included(base) - base.cattr_accessor :connections - base.connections = {} - base.extend ClassMethods + def extract_server! + self[:server] = options.delete(:server) || DEFAULT_HOST end - - # Manage the creation and destruction of connections for AWS::S3::Base and its subclasses. Connections are - # created with establish_connection!. - module ClassMethods - # Creates a new connection with which to make requests to the S3 servers for the calling class. - # - # AWS::S3::Base.establish_connection!(:access_key_id => '...', :secret_access_key => '...') - # - # You can set connections for every subclass of AWS::S3::Base. Once the initial connection is made on - # Base, all subsequent connections will inherit whatever values you don't specify explictly. This allows you to - # customize details of the connection, such as what server the requests are made to, by just specifying one - # option. - # - # AWS::S3::Bucket.established_connection!(:use_ssl => true) - # - # The Bucket connection would inherit the :access_key_id and the :secret_access_key from - # Base's connection. Unlike the Base connection, all Bucket requests would be made over SSL. - # - # == Required arguments - # - # * :access_key_id - The access key id for your S3 account. Provided by Amazon. - # * :secret_access_key - The secret access key for your S3 account. Provided by Amazon. - # - # If any of these required arguments is missing, a MissingAccessKey exception will be raised. - # - # == Optional arguments - # - # * :server - The server to make requests to. You can use this to specify your bucket in the subdomain, - # or your own domain's cname if you are using virtual hosted buckets. Defaults to s3.amazonaws.com. - # * :port - The port to the requests should be made on. Defaults to 80 or 443 if the :use_ssl - # argument is set. - # * :use_ssl - Whether requests should be made over SSL. If set to true, the :port argument - # will be implicitly set to 443, unless specified otherwise. Defaults to false. - # * :persistent - Whether to use a persistent connection to the server. Having this on provides around a two fold - # performance increase but for long running processes some firewalls may find the long lived connection suspicious and close the connection. - # If you run into connection errors, try setting :persistent to false. Defaults to true. - def establish_connection!(options = {}) - # After you've already established the default connection, just specify - # the difference for subsequent connections - options = default_connection.options.merge(options) if connected? - connections[connection_name] = Connection.connect(options) - end - - # Returns the connection for the current class, or Base's default connection if the current class does not - # have its own connection. - # - # If not connection has been established yet, NoConnectionEstablished will be raised. - def connection - if connected? - connections[connection_name] || default_connection - else - raise NoConnectionEstablished - end - end - - # Returns true if a connection has been made yet. - def connected? - !connections.empty? - end - - # Removes the connection for the current class. If there is no connection for the current class, the default - # connection will be removed. - def disconnect(name = connection_name) - name = default_connection unless connections.has_key?(name) - connection = connections[name] - connection.http.finish if connection.persistent? - connections.delete(name) - end - - # Clears *all* connections, from all classes, with prejudice. - def disconnect! - connections.each_key {|connection| disconnect(connection)} - end - private - def connection_name - name - end - - def default_connection_name - 'AWS::S3::Base' - end - - def default_connection - connections[default_connection_name] - end + def extract_port! + self[:port] = options.delete(:port) || (options[:use_ssl] ? 443 : 80) end - end - class Options < Hash #:nodoc: - class << self - def valid_options - [:access_key_id, :secret_access_key, :server, :port, :use_ssl, :persistent] - end + def extract_remainder! + update(options) end - attr_reader :options - def initialize(options = {}) - super() - @options = options - validate! - extract_persistent! - extract_server! - extract_port! - extract_remainder! + def validate! + invalid_options = options.keys.select {|key| !self.class.valid_options.include?(key)} + raise InvalidConnectionOption.new(invalid_options) unless invalid_options.empty? end - - private - def extract_persistent! - self[:persistent] = options.has_key?(:persitent) ? options[:persitent] : true - end - - def extract_server! - self[:server] = options.delete(:server) || DEFAULT_HOST - end - - def extract_port! - self[:port] = options.delete(:port) || (options[:use_ssl] ? 443 : 80) - end - - def extract_remainder! - update(options) - end - - def validate! - invalid_options = options.keys.select {|key| !self.class.valid_options.include?(key)} - raise InvalidConnectionOption.new(invalid_options) unless invalid_options.empty? - end - end end end end \ No newline at end of file Index: lib/aws/exceptions.rb =================================================================== --- lib/aws/exceptions.rb (revision 0) +++ lib/aws/exceptions.rb (revision 0) @@ -0,0 +1,64 @@ +module AWS + + # Abstract super class of all AWS::S3 exceptions + class AWSException < StandardError + end + + # All responses with a code between 300 and 599 that contain an body are wrapped in an + # ErrorResponse which contains an Error object. This Error class generates a custom exception with the name + # of the xml Error and its message. All such runtime generated exception classes descend from ResponseError + # and contain the ErrorResponse object so that all code that makes a request can rescue ResponseError and get + # access to the ErrorResponse. + class ResponseError < AWSException + attr_reader :response + def initialize(message, response) + @response = response + super(message) + end + end + + #:stopdoc: + + # Most ResponseError's are created just time on a need to have basis, but we explicitly define the + # InternalError exception because we want to explicitly rescue InternalError in some cases. + class InternalError < ResponseError + end + + class NoSuchKey < ResponseError + end + + class RequestTimeout < ResponseError + end + + # Abstract super class for all invalid options. + class InvalidOption < AWSException + end + + # Raised if either the access key id or secret access key arguments are missing when establishing a connection. + class MissingAccessKey < InvalidOption + def initialize(missing_keys) + key_list = missing_keys.map {|key| key.to_s}.join(' and the ') + super("You did not provide both required access keys. Please provide the #{key_list}.") + end + end + + # Raised if a request is attempted before any connections have been established. + class NoConnectionEstablished < AWSException + end + + # Raised if an unrecognized option is passed when establishing a connection. + class InvalidConnectionOption < InvalidOption + def initialize(invalid_options) + message = "The following connection options are invalid: #{invalid_options.join(', ')}. " + + "The valid connection options are: #{Connection::Options.valid_options.join(', ')}." + super(message) + end + end + + class ExceptionClassClash < AWSException #:nodoc: + def initialize(klass) + message = "The exception class you tried to create (`#{klass}') exists and is not an exception" + super(message) + end + end +end Index: lib/aws/s3/bucket.rb =================================================================== --- lib/aws/s3/bucket.rb (revision 158) +++ lib/aws/s3/bucket.rb (working copy) @@ -58,7 +58,7 @@ # # Bucket.delete('photos', :force => true) # # => true - class Bucket < Base + class Bucket < S3Base class << self # Creates a bucket named name. # Index: lib/aws/s3/authentication.rb =================================================================== --- lib/aws/s3/authentication.rb (revision 158) +++ lib/aws/s3/authentication.rb (working copy) @@ -1,218 +0,0 @@ -module AWS - module S3 - # All authentication is taken care of for you by the AWS::S3 library. None the less, some details of the two types - # of authentication and when they are used may be of interest to some. - # - # === Header based authentication - # - # Header based authentication is achieved by setting a special Authorization header whose value - # is formatted like so: - # - # "AWS #{access_key_id}:#{encoded_canonical}" - # - # The access_key_id is the public key that is assigned by Amazon for a given account which you use when - # establishing your initial connection. The encoded_canonical is computed according to rules layed out - # by Amazon which we will describe presently. - # - # ==== Generating the encoded canonical string - # - # The "canonical string", generated by the CanonicalString class, is computed by collecting the current request method, - # a set of significant headers of the current request, and the current request path into a string. - # That canonical string is then encrypted with the secret_access_key assigned by Amazon. The resulting encrypted canonical - # string is then base 64 encoded. - # - # === Query string based authentication - # - # When accessing a restricted object from the browser, you can authenticate via the query string, by setting the following parameters: - # - # "AWSAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}" - # - # The QueryString class is responsible for generating the appropriate parameters for authentication via the - # query string. - # - # The access_key_id and encoded_canonical are the same as described in the Header based authentication section. - # The expires value dictates for how long the current url is valid (by default, it will expire in 5 minutes). Expiration can be specified - # either by an absolute time (expressed in seconds since the epoch), or in relative time (in number of seconds from now). - # Details of how to customize the expiration of the url are provided in the documentation for the QueryString class. - # - # All requests made by this library use header authentication. When a query string authenticated url is needed, - # the S3Object#url method will include the appropriate query string parameters. - # - # === Full authentication specification - # - # The full specification of the authentication protocol can be found at - # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAuthentication.html - class Authentication - constant :AMAZON_HEADER_PREFIX, 'x-amz-' - - # Signature is the abstract super class for the Header and QueryString authentication methods. It does the job - # of computing the canonical_string using the CanonicalString class as well as encoding the canonical string. The subclasses - # parameterize these computations and arrange them in a string form appropriate to how they are used, in one case a http request - # header value, and in the other case key/value query string parameter pairs. - class Signature < String #:nodoc: - attr_reader :request, :access_key_id, :secret_access_key - - def initialize(request, access_key_id, secret_access_key, options = {}) - super() - @request, @access_key_id, @secret_access_key = request, access_key_id, secret_access_key - @options = options - end - - private - - def canonical_string - options = {} - options[:expires] = expires if expires? - CanonicalString.new(request, options) - end - memoized :canonical_string - - def encoded_canonical - digest = OpenSSL::Digest::Digest.new('sha1') - b64_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, secret_access_key, canonical_string)).strip - url_encode? ? CGI.escape(b64_hmac) : b64_hmac - end - - def url_encode? - !@options[:url_encode].nil? - end - - def expires? - is_a? QueryString - end - - def date - request['date'].to_s.strip.empty? ? Time.now : Time.parse(request['date']) - end - end - - # Provides header authentication by computing the value of the Authorization header. More details about the - # various authentication schemes can be found in the docs for its containing module, Authentication. - class Header < Signature #:nodoc: - def initialize(*args) - super - self << "AWS #{access_key_id}:#{encoded_canonical}" - end - end - - # Provides query string authentication by computing the three authorization parameters: AWSAccessKeyId, Expires and Signature. - # More details about the various authentication schemes can be found in the docs for its containing module, Authentication. - class QueryString < Signature #:nodoc: - constant :DEFAULT_EXPIRY, 300 # 5 minutes - - def initialize(*args) - super - @options[:url_encode] = true - self << build - end - - private - - # Will return one of three values, in the following order of precedence: - # - # 1) Seconds since the epoch explicitly passed in the +:expires+ option - # 2) The current time in seconds since the epoch plus the number of seconds passed in - # the +:expires_in+ option - # 3) The current time in seconds since the epoch plus the default number of seconds (60 seconds) - def expires - return @options[:expires] if @options[:expires] - date.to_i + (@options[:expires_in] || DEFAULT_EXPIRY) - end - - # Keep in alphabetical order - def build - "AWSAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}" - end - end - - # The CanonicalString is used to generate an encrypted signature, signed with your secrect access key. It is composed of - # data related to the given request for which it provides authentication. This data includes the request method, request headers, - # and the request path. Both Header and QueryString use it to generate their signature. - class CanonicalString < String #:nodoc: - class << self - def default_headers - %w(content-type content-md5) - end - - def interesting_headers - ['content-md5', 'content-type', 'date', amazon_header_prefix] - end - - def amazon_header_prefix - /^#{AMAZON_HEADER_PREFIX}/io - end - end - - attr_reader :request, :headers - - def initialize(request, options = {}) - super() - @request = request - @headers = {} - @options = 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 - build - end - - private - def build - self << "#{request.method}\n" - ensure_date_is_valid - - initialize_headers - set_expiry! - - headers.sort_by {|k, _| k}.each do |key, value| - value = value.to_s.strip - self << (key =~ self.class.amazon_header_prefix ? "#{key}:#{value}" : value) - self << "\n" - end - self << path - end - - def initialize_headers - identify_interesting_headers - set_default_headers - end - - def set_expiry! - self.headers['date'] = @options[:expires] if @options[:expires] - end - - def ensure_date_is_valid - request['Date'] ||= Time.now.httpdate - end - - def identify_interesting_headers - request.each do |key, value| - key = key.downcase # Can't modify frozen string so no bang - if self.class.interesting_headers.any? {|header| header === key} - self.headers[key] = value.to_s.strip - end - end - end - - def set_default_headers - self.class.default_headers.each do |header| - self.headers[header] ||= '' - end - end - - def path - [only_path, extract_significant_parameter].compact.join('?') - end - - def extract_significant_parameter - request.path[/[&?](acl|torrent|logging)(?:&|=|$)/, 1] - end - - def only_path - request.path[/^[^?]*/] - end - end - end - end -end \ No newline at end of file Index: lib/aws/s3/exceptions.rb =================================================================== --- lib/aws/s3/exceptions.rb (revision 158) +++ lib/aws/s3/exceptions.rb (working copy) @@ -2,39 +2,9 @@ module S3 # Abstract super class of all AWS::S3 exceptions - class S3Exception < StandardError + class S3Exception < AWSException end - # All responses with a code between 300 and 599 that contain an body are wrapped in an - # ErrorResponse which contains an Error object. This Error class generates a custom exception with the name - # of the xml Error and its message. All such runtime generated exception classes descend from ResponseError - # and contain the ErrorResponse object so that all code that makes a request can rescue ResponseError and get - # access to the ErrorResponse. - class ResponseError < S3Exception - attr_reader :response - def initialize(message, response) - @response = response - super(message) - end - end - - #:stopdoc: - - # Most ResponseError's are created just time on a need to have basis, but we explicitly define the - # InternalError exception because we want to explicitly rescue InternalError in some cases. - class InternalError < ResponseError - end - - class NoSuchKey < ResponseError - end - - class RequestTimeout < ResponseError - end - - # Abstract super class for all invalid options. - class InvalidOption < S3Exception - end - # Raised if an invalid value is passed to the :access option when creating a Bucket or an S3Object. class InvalidAccessControlLevel < InvalidOption def initialize(valid_levels, access_level) @@ -42,27 +12,6 @@ end end - # Raised if either the access key id or secret access key arguments are missing when establishing a connection. - class MissingAccessKey < InvalidOption - def initialize(missing_keys) - key_list = missing_keys.map {|key| key.to_s}.join(' and the ') - super("You did not provide both required access keys. Please provide the #{key_list}.") - end - end - - # Raised if a request is attempted before any connections have been established. - class NoConnectionEstablished < S3Exception - end - - # Raised if an unrecognized option is passed when establishing a connection. - class InvalidConnectionOption < InvalidOption - def initialize(invalid_options) - message = "The following connection options are invalid: #{invalid_options.join(', ')}. " + - "The valid connection options are: #{Connection::Options.valid_options.join(', ')}." - super(message) - end - end - # Raised if an invalid bucket name is passed when creating a new Bucket. class InvalidBucketName < S3Exception def initialize(invalid_name) Index: lib/aws/s3/error.rb =================================================================== --- lib/aws/s3/error.rb (revision 158) +++ lib/aws/s3/error.rb (working copy) @@ -1,69 +0,0 @@ -module AWS - module S3 - # Anything you do that makes a request to S3 could result in an error. If it does, the AWS::S3 library will raise an exception - # specific to the error. All exception that are raised as a result of a request returning an error response inherit from the - # ResponseError exception. So should you choose to rescue any such exception, you can simple rescue ResponseError. - # - # Say you go to delete a bucket, but the bucket turns out to not be empty. This results in a BucketNotEmpty error (one of the many - # errors listed at http://docs.amazonwebservices.com/AmazonS3/2006-03-01/ErrorCodeList.html): - # - # begin - # Bucket.delete('jukebox') - # rescue ResponseError => error - # # ... - # end - # - # Once you've captured the exception, you can extract the error message from S3, as well as the full error response, which includes - # things like the HTTP response code: - # - # error - # # => # - # error.message - # # => "The bucket you tried to delete is not empty" - # error.response.code - # # => 409 - # - # You could use this information to redisplay the error in a way you see fit, or just to log the error and continue on. - class Error - #:stopdoc: - attr_accessor :response - def initialize(error, response = nil) - @error = error - @response = response - @container = AWS::S3 - find_or_create_exception! - end - - def raise - Kernel.raise exception.new(message, response) - end - - private - attr_reader :error, :exception, :container - - def find_or_create_exception! - @exception = container.const_defined?(code) ? find_exception : create_exception - end - - def find_exception - exception_class = container.const_get(code) - Kernel.raise ExceptionClassClash.new(exception_class) unless exception_class.ancestors.include?(ResponseError) - exception_class - end - - def create_exception - container.const_set(code, Class.new(ResponseError)) - end - - def method_missing(method, *args, &block) - # We actually want nil if the attribute is nil. So we use has_key? rather than [] + ||. - if error.has_key?(method.to_s) - error[method.to_s] - else - super - end - end - end - end -end -#:startdoc: \ No newline at end of file Index: lib/aws/s3/response.rb =================================================================== --- lib/aws/s3/response.rb (revision 158) +++ lib/aws/s3/response.rb (working copy) @@ -1,69 +1,16 @@ #:stopdoc: module AWS module S3 - class Base - class Response < String - attr_reader :response, :body, :parsed - def initialize(response) - @response = response - @body = response.body.to_s - super(body) + class S3Base + class Response < Base::Response + def container + AWS::S3 end - - def headers - headers = {} - response.each do |header, value| - headers[header] = value - end - headers - end - memoized :headers - - def [](header) - headers[header] - end - - def each(&block) - headers.each(&block) - end - - def code - response.code.to_i - end - - {:success => 200..299, :redirect => 300..399, - :client_error => 400..499, :server_error => 500..599}.each do |result, code_range| - class_eval(<<-EVAL, __FILE__, __LINE__) - def #{result}? - return false unless response - (#{code_range}).include? code - end - EVAL - end - - def error? - !success? && response['content-type'] == 'application/xml' && parsed.root == 'error' - end - - def error - Error.new(parsed, self) - end - memoized :error - - def parsed - # XmlSimple is picky about what kind of object it parses, so we pass in body rather than self - Parsing::XmlParser.new(body) - end - memoized :parsed - - def inspect - "#<%s:0x%s %s %s>" % [self.class, object_id, response.code, response.message] - end end end class Bucket - class Response < Base::Response + class Response < S3Base::Response def bucket parsed end @@ -71,7 +18,7 @@ end class S3Object - class Response < Base::Response + class Response < S3Base::Response def etag headers['etag'][1...-1] end @@ -79,7 +26,7 @@ end class Service - class Response < Base::Response + class Response < S3Base::Response def empty? parsed['buckets'].nil? end @@ -92,89 +39,11 @@ module ACL class Policy - class Response < Base::Response + class Response < S3Base::Response alias_method :policy, :parsed end end end - - # Requests whose response code is between 300 and 599 and contain an in their body - # are wrapped in an Error::Response. This Error::Response contains an Error object which raises an exception - # that corresponds to the error in the response body. The exception object contains the ErrorResponse, so - # in all cases where a request happens, you can rescue ResponseError and have access to the ErrorResponse and - # its Error object which contains information about the ResponseError. - # - # begin - # Bucket.create(..) - # rescue ResponseError => exception - # exception.response - # # => - # exception.response.error - # # => - # end - class Error - class Response < Base::Response - def error? - true - end - - def inspect - "#<%s:0x%s %s %s: '%s'>" % [self.class.name, object_id, response.code, error.code, error.message] - end - end - end - - # Guess response class name from current class name. If the guessed response class doesn't exist - # do the same thing to the current class's parent class, up the inheritance heirarchy until either - # a response class is found or until we get to the top of the heirarchy in which case we just use - # the the Base response class. - # - # Important: This implemantation assumes that the Base class has a corresponding Base::Response. - class FindResponseClass #:nodoc: - class << self - def for(start) - new(start).find - end - end - - def initialize(start) - @container = AWS::S3 - @current_class = start - end - - def find - self.current_class = current_class.superclass until response_class_found? - target.const_get(class_to_find) - end - - private - attr_reader :container - attr_accessor :current_class - - def target - container.const_get(current_name) - end - - def target? - container.const_defined?(current_name) - end - - def response_class_found? - target? && target.const_defined?(class_to_find) - end - - def class_to_find - :Response - end - - def current_name - truncate(current_class) - end - - def truncate(klass) - klass.name[/[^:]+$/] - end - end end end #:startdoc: \ No newline at end of file Index: lib/aws/s3/extensions.rb =================================================================== --- lib/aws/s3/extensions.rb (revision 158) +++ lib/aws/s3/extensions.rb (working copy) @@ -1,315 +0,0 @@ -#:stopdoc: - -class Hash - def to_query_string(include_question_mark = true) - query_string = '' - unless empty? - query_string << '?' if include_question_mark - query_string << inject([]) do |params, (key, value)| - params << "#{key}=#{value}" - end.join('&') - end - query_string - end - - def to_normalized_options - # Convert all option names to downcased strings, and replace underscores with hyphens - inject({}) do |normalized_options, (name, value)| - normalized_options[name.to_header] = value.to_s - normalized_options - end - end - - def to_normalized_options! - replace(to_normalized_options) - end -end - -class String - def previous! - self[-1] -= 1 - self - end - - def previous - dup.previous! - end - - def to_header - downcase.tr('_', '-') - end - - # ActiveSupport adds an underscore method to String so let's just use that one if - # we find that the method is already defined - def underscore - gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). - gsub(/([a-z\d])([A-Z])/,'\1_\2'). - downcase - end unless public_method_defined? :underscore - - def utf8? - scan(/[^\x00-\xa0]/u) { |s| s.unpack('U') } - true - rescue ArgumentError - false - end - - # All paths in in S3 have to be valid unicode so this takes care of - # cleaning up any strings that aren't valid utf-8 according to String#utf8? - def remove_extended! - gsub!(/[\x80-\xFF]/) { "%02X" % $&[0] } - end - - def remove_extended - dup.remove_extended! - end -end - -class CoercibleString < String - class << self - def coerce(string) - new(string).coerce - end - end - - def coerce - case self - when 'true': true - when 'false': false - when /^\d+$/: Integer(self) - when datetime_format: Time.parse(self) - else - self - end - end - - private - # Lame hack since Date._parse is so accepting. S3 dates are of the form: '2006-10-29T23:14:47.000Z' - # so unless the string looks like that, don't even try, otherwise it might convert an object's - # key from something like '03 1-2-3-Apple-Tree.mp3' to Sat Feb 03 00:00:00 CST 2001. - def datetime_format - /^\d{4}-\d{2}-\d{2}\w\d{2}:\d{2}:\d{2}/ - end -end - -class Symbol - def to_header - to_s.to_header - end -end - -module Kernel - def __method__(depth = 0) - caller[depth][/`([^']+)'/, 1] - end if RUBY_VERSION < '1.9' - - def memoize(reload = false, storage = nil) - storage = "@#{storage || __method__(1)}" - if reload - instance_variable_set(storage, nil) - else - if cache = instance_variable_get(storage) - return cache - end - end - instance_variable_set(storage, yield) - end - - def require_library_or_gem(library) - require library - rescue LoadError => library_not_installed - begin - require 'rubygems' - require library - rescue LoadError - raise library_not_installed - end - end -end - -class Module - def memoized(method_name) - original_method = "unmemoized_#{method_name}_#{Time.now.to_i}" - alias_method original_method, method_name - module_eval(<<-EVAL, __FILE__, __LINE__) - def #{method_name}(reload = false, *args, &block) - memoize(reload) do - send(:#{original_method}, *args, &block) - end - end - EVAL - end - - def constant(name, value) - unless const_defined?(name) - const_set(name, value) - module_eval(<<-EVAL, __FILE__, __LINE__) - def self.#{name.to_s.downcase} - #{name.to_s} - end - EVAL - end - end - - # Transforms MarcelBucket into - # - # class MarcelBucket < AWS::S3::Bucket - # set_current_bucket_to 'marcel' - # end - def const_missing_from_s3_library(sym) - if sym.to_s =~ /^(\w+)(Bucket|S3Object)$/ - const = const_set(sym, Class.new(AWS::S3.const_get($2))) - const.current_bucket = $1.underscore - const - else - const_missing_not_from_s3_library(sym) - end - end - alias_method :const_missing_not_from_s3_library, :const_missing - alias_method :const_missing, :const_missing_from_s3_library -end - - -class Class # :nodoc: - def cattr_reader(*syms) - syms.flatten.each do |sym| - class_eval(<<-EOS, __FILE__, __LINE__) - unless defined? @@#{sym} - @@#{sym} = nil - end - - def self.#{sym} - @@#{sym} - end - - def #{sym} - @@#{sym} - end - EOS - end - end - - def cattr_writer(*syms) - syms.flatten.each do |sym| - class_eval(<<-EOS, __FILE__, __LINE__) - unless defined? @@#{sym} - @@#{sym} = nil - end - - def self.#{sym}=(obj) - @@#{sym} = obj - end - - def #{sym}=(obj) - @@#{sym} = obj - end - EOS - end - end - - def cattr_accessor(*syms) - cattr_reader(*syms) - cattr_writer(*syms) - end -end if Class.instance_methods(false).grep(/^cattr_(?:reader|writer|accessor)$/).empty? - -module SelectiveAttributeProxy - def self.included(klass) - klass.extend(ClassMethods) - klass.class_eval(<<-EVAL, __FILE__, __LINE__) - cattr_accessor :attribute_proxy - cattr_accessor :attribute_proxy_options - - # Default name for attribute storage - self.attribute_proxy = :attributes - self.attribute_proxy_options = {:exclusively => true} - - private - # By default proxy all attributes - def proxiable_attribute?(name) - return true unless self.class.attribute_proxy_options[:exclusively] - send(self.class.attribute_proxy).has_key?(name) - end - - def method_missing(method, *args, &block) - # Autovivify attribute storage - if method == self.class.attribute_proxy - ivar = "@\#{method}" - instance_variable_set(ivar, {}) unless instance_variable_get(ivar).is_a?(Hash) - instance_variable_get(ivar) - # Delegate to attribute storage - elsif method.to_s =~ /^(\\w+)(=?)$/ && proxiable_attribute?($1) - attributes_hash_name = self.class.attribute_proxy - $2.empty? ? send(attributes_hash_name)[$1] : send(attributes_hash_name)[$1] = args.first - else - super - end - end - EVAL - end - - module ClassMethods - def proxy_to(attribute_name, options = {}) - if attribute_name.is_a?(Hash) - options = attribute_name - else - self.attribute_proxy = attribute_name - end - self.attribute_proxy_options = options - end - end -end - -# When streaming data up, Net::HTTPGenericRequest hard codes a chunk size of 1k. For large files this -# is an unfortunately low chunk size, so here we make it use a much larger default size and move it into a method -# so that the implementation of send_request_with_body_stream doesn't need to be changed to change the chunk size (at least not anymore -# than I've already had to...). -module Net - class HTTPGenericRequest - def send_request_with_body_stream(sock, ver, path, f) - raise ArgumentError, "Content-Length not given and Transfer-Encoding is not `chunked'" unless content_length() or chunked? - unless content_type() - warn 'net/http: warning: Content-Type did not set; using application/x-www-form-urlencoded' if $VERBOSE - set_content_type 'application/x-www-form-urlencoded' - end - write_header sock, ver, path - if chunked? - while s = f.read(chunk_size) - sock.write(sprintf("%x\r\n", s.length) << s << "\r\n") - end - sock.write "0\r\n\r\n" - else - while s = f.read(chunk_size) - sock.write s - end - end - end - - def chunk_size - 1048576 # 1 megabyte - end - end - - # Net::HTTP before 1.8.4 doesn't have the use_ssl? method or the Delete request type - class HTTP - def use_ssl? - @use_ssl - end unless public_method_defined? :use_ssl? - - class Delete < HTTPRequest - METHOD = 'DELETE' - REQUEST_HAS_BODY = false - RESPONSE_HAS_BODY = true - end unless const_defined? :Delete - end -end - -class XmlGenerator < String #:nodoc: - attr_reader :xml - def initialize - @xml = Builder::XmlMarkup.new(:indent => 2, :target => self) - super() - build - end -end -#:startdoc: Index: lib/aws/s3/connection.rb =================================================================== --- lib/aws/s3/connection.rb (revision 158) +++ lib/aws/s3/connection.rb (working copy) @@ -1,259 +0,0 @@ -module AWS - module S3 - class Connection #:nodoc: - class << self - def connect(options = {}) - new(options) - end - - def prepare_path(path) - path = path.remove_extended unless path.utf8? - URI.escape(path) - end - end - - attr_reader :access_key_id, :secret_access_key, :http, :options - - # Creates a new connection. Connections make the actual requests to S3, though these requests are usually - # called from subclasses of Base. - # - # For details on establishing connections, check the Connection::Management::ClassMethods. - def initialize(options = {}) - @options = Options.new(options) - connect - end - - def request(verb, path, headers = {}, body = nil, attempts = 0, &block) - body.rewind if body.respond_to?(:rewind) unless attempts.zero? - - requester = Proc.new do - path = self.class.prepare_path(path) - request = request_method(verb).new(path, headers) - ensure_content_type!(request) - add_user_agent!(request) - authenticate!(request) - if body - if body.respond_to?(:read) - request.body_stream = body - request.content_length = body.respond_to?(:lstat) ? body.lstat.size : body.size - else - request.body = body - end - end - http.request(request, &block) - end - - if persistent? - http.start unless http.started? - requester.call - else - http.start(&requester) - end - rescue Errno::EPIPE, Timeout::Error, Errno::EPIPE, Errno::EINVAL - @http = create_connection - attempts == 3 ? raise : (attempts += 1; retry) - end - - def url_for(path, options = {}) - path = self.class.prepare_path(path) - request = request_method(:get).new(path, {}) - query_string = query_string_authentication(request, options) - "#{protocol(options)}#{http.address}#{path}?#{query_string}" - end - - def subdomain - http.address[/^([^.]+).#{DEFAULT_HOST}$/, 1] - end - - def persistent? - options[:persistent] - end - - def protocol(options = {}) - (options[:use_ssl] || http.use_ssl?) ? 'https://' : 'http://' - end - - private - def extract_keys! - missing_keys = [] - extract_key = Proc.new {|key| options[key] || (missing_keys.push(key); nil)} - @access_key_id = extract_key[:access_key_id] - @secret_access_key = extract_key[:secret_access_key] - raise MissingAccessKey.new(missing_keys) unless missing_keys.empty? - end - - def create_connection - http = Net::HTTP.new(options[:server], options[:port]) - http.use_ssl = !options[:use_ssl].nil? || options[:port] == 443 - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - http - end - - def connect - extract_keys! - @http = create_connection - 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) - end - - def add_user_agent!(request) - request['User-Agent'] ||= "AWS::S3/#{Version}" - end - - def query_string_authentication(request, options = {}) - Authentication::QueryString.new(request, access_key_id, secret_access_key, options) - end - - def request_method(verb) - Net::HTTP.const_get(verb.to_s.capitalize) - end - - def method_missing(method, *args, &block) - options[method] || super - end - - module Management #:nodoc: - def self.included(base) - base.cattr_accessor :connections - base.connections = {} - base.extend ClassMethods - end - - # Manage the creation and destruction of connections for AWS::S3::Base and its subclasses. Connections are - # created with establish_connection!. - module ClassMethods - # Creates a new connection with which to make requests to the S3 servers for the calling class. - # - # AWS::S3::Base.establish_connection!(:access_key_id => '...', :secret_access_key => '...') - # - # You can set connections for every subclass of AWS::S3::Base. Once the initial connection is made on - # Base, all subsequent connections will inherit whatever values you don't specify explictly. This allows you to - # customize details of the connection, such as what server the requests are made to, by just specifying one - # option. - # - # AWS::S3::Bucket.established_connection!(:use_ssl => true) - # - # The Bucket connection would inherit the :access_key_id and the :secret_access_key from - # Base's connection. Unlike the Base connection, all Bucket requests would be made over SSL. - # - # == Required arguments - # - # * :access_key_id - The access key id for your S3 account. Provided by Amazon. - # * :secret_access_key - The secret access key for your S3 account. Provided by Amazon. - # - # If any of these required arguments is missing, a MissingAccessKey exception will be raised. - # - # == Optional arguments - # - # * :server - The server to make requests to. You can use this to specify your bucket in the subdomain, - # or your own domain's cname if you are using virtual hosted buckets. Defaults to s3.amazonaws.com. - # * :port - The port to the requests should be made on. Defaults to 80 or 443 if the :use_ssl - # argument is set. - # * :use_ssl - Whether requests should be made over SSL. If set to true, the :port argument - # will be implicitly set to 443, unless specified otherwise. Defaults to false. - # * :persistent - Whether to use a persistent connection to the server. Having this on provides around a two fold - # performance increase but for long running processes some firewalls may find the long lived connection suspicious and close the connection. - # If you run into connection errors, try setting :persistent to false. Defaults to true. - def establish_connection!(options = {}) - # After you've already established the default connection, just specify - # the difference for subsequent connections - options = default_connection.options.merge(options) if connected? - connections[connection_name] = Connection.connect(options) - end - - # Returns the connection for the current class, or Base's default connection if the current class does not - # have its own connection. - # - # If not connection has been established yet, NoConnectionEstablished will be raised. - def connection - if connected? - connections[connection_name] || default_connection - else - raise NoConnectionEstablished - end - end - - # Returns true if a connection has been made yet. - def connected? - !connections.empty? - end - - # Removes the connection for the current class. If there is no connection for the current class, the default - # connection will be removed. - def disconnect(name = connection_name) - name = default_connection unless connections.has_key?(name) - connection = connections[name] - connection.http.finish if connection.persistent? - connections.delete(name) - end - - # Clears *all* connections, from all classes, with prejudice. - def disconnect! - connections.each_key {|connection| disconnect(connection)} - end - - private - def connection_name - name - end - - def default_connection_name - 'AWS::S3::Base' - end - - def default_connection - connections[default_connection_name] - end - end - end - - class Options < Hash #:nodoc: - class << self - def valid_options - [:access_key_id, :secret_access_key, :server, :port, :use_ssl, :persistent] - end - end - - attr_reader :options - def initialize(options = {}) - super() - @options = options - validate! - extract_persistent! - extract_server! - extract_port! - extract_remainder! - end - - private - def extract_persistent! - self[:persistent] = options.has_key?(:persitent) ? options[:persitent] : true - end - - def extract_server! - self[:server] = options.delete(:server) || DEFAULT_HOST - end - - def extract_port! - self[:port] = options.delete(:port) || (options[:use_ssl] ? 443 : 80) - end - - def extract_remainder! - update(options) - end - - def validate! - invalid_options = options.keys.select {|key| !self.class.valid_options.include?(key)} - raise InvalidConnectionOption.new(invalid_options) unless invalid_options.empty? - end - end - end - end -end \ No newline at end of file Index: lib/aws/s3/service.rb =================================================================== --- lib/aws/s3/service.rb (revision 158) +++ lib/aws/s3/service.rb (working copy) @@ -4,7 +4,7 @@ # # Service.buckets # # => [] - class Service < Base + class Service < S3Base @@response = nil #:nodoc: class << self Index: lib/aws/s3/parsing.rb =================================================================== --- lib/aws/s3/parsing.rb (revision 158) +++ lib/aws/s3/parsing.rb (working copy) @@ -1,99 +0,0 @@ -#:stopdoc: -module AWS - module S3 - module Parsing - class << self - def parser=(parsing_library) - XmlParser.parsing_library = parsing_library - end - - def parser - XmlParser.parsing_library - end - end - - module Typecasting - def typecast(object) - case object - when Hash - typecast_hash(object) - when Array - object.map {|element| typecast(element)} - when String - CoercibleString.coerce(object) - else - object - end - end - - def typecast_hash(hash) - if content = hash['__content__'] - typecast(content) - else - keys = hash.keys.map {|key| key.underscore} - values = hash.values.map {|value| typecast(value)} - keys.inject({}) do |new_hash, key| - new_hash[key] = values.slice!(0) - new_hash - end - end - end - end - - class XmlParser < Hash - include Typecasting - - class << self - attr_accessor :parsing_library - end - - attr_reader :body, :xml_in, :root - - def initialize(body) - @body = body - unless body.strip.empty? - parse - set_root - typecast_xml_in - end - end - - private - - def parse - @xml_in = self.class.parsing_library.xml_in(body, parsing_options) - end - - def parsing_options - { - # Includes the enclosing tag as the top level key - 'keeproot' => true, - # Makes tag value available via the '__content__' key - 'contentkey' => '__content__', - # Always parse tags into a hash, even when there are no attributes - # (unless there is also no value, in which case it is nil) - 'forcecontent' => true, - # If a tag is empty, makes its content nil - 'suppressempty' => nil, - # Force nested elements to be put into an array, even if there is only one of them - 'forcearray' => ['Contents', 'Bucket', 'Grant'] - } - end - - def set_root - @root = @xml_in.keys.first.underscore - end - - def typecast_xml_in - typecast_xml = {} - @xml_in.dup.each do |key, value| # Some typecasting is destructive so we dup - typecast_xml[key.underscore] = typecast(value) - end - # An empty body will try to update with a string so only update if the result is a hash - update(typecast_xml[root]) if typecast_xml[root].is_a?(Hash) - end - end - end - end -end -#:startdoc: \ No newline at end of file Index: lib/aws/s3/object.rb =================================================================== --- lib/aws/s3/object.rb (revision 158) +++ lib/aws/s3/object.rb (working copy) @@ -113,7 +113,7 @@ # pp song.metada # {"x-amz-meta-released" => 2006, # "x-amz-meta-album" => "A River Ain't Too Much To Love"} - class S3Object < Base + class S3Object < S3Base class << self # Returns the value of the object with key in the specified bucket. # Index: lib/aws/s3/base.rb =================================================================== --- lib/aws/s3/base.rb (revision 158) +++ lib/aws/s3/base.rb (working copy) @@ -41,7 +41,7 @@ # # See more connection details at AWS::S3::Connection::Management::ClassMethods. module S3 - constant :DEFAULT_HOST, 's3.amazonaws.com' +# constant :DEFAULT_HOST, 's3.amazonaws.com' # AWS::S3::Base is the abstract super class of all classes who make requests against S3, such as the built in # Service, Bucket and S3Object classes. It provides methods for making requests, inferring or setting response classes, @@ -55,37 +55,8 @@ # details can be found in the docs for Connection::Management::ClassMethods. # # Extensive examples can be found in the README[link:files/README.html]. - class Base - class << self - # Wraps the current connection's request method and picks the appropriate response class to wrap the response in. - # If the response is an error, it will raise that error as an exception. All such exceptions can be caught by rescuing - # their superclass, the ResponseError exception class. - # - # 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) - Service.response = nil - process_options!(options, verb) - response = response_class.new(connection.request(verb, path, options, body, attempts, &block)) - Service.response = response - - Error::Response.new(response.response).error.raise if response.error? - response - # Once in a while, a request to S3 returns an internal error. A glitch in the matrix I presume. Since these - # errors are few and far between the request method will rescue InternalErrors the first three times they encouter them - # and will retry the request again. Most of the time the second attempt will work. - rescue *retry_exceptions - attempts == 3 ? raise : (attempts += 1; retry) - end - - [: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) - end - EVAL - end - + class S3Base < AWS::Base + class << self # Called when a method which requires a bucket name is called without that bucket name specified. It will try to # infer the current bucket by looking for it as the subdomain of the current connection's address. If no subdomain # is found, CurrentBucketNotSpecified will be raised. @@ -126,7 +97,7 @@ # # other_song = JukeBoxSong.find('baby-please-come-home.mp3') def set_current_bucket_to(name) - raise ArgumentError, "`#{__method__}' must be called on a subclass of #{self.name}" if self == AWS::S3::Base + raise ArgumentError, "`#{__method__}' must be called on a subclass of #{self.name}" if self == AWS::S3::S3Base instance_eval(<<-EVAL) def current_bucket '#{name}' @@ -136,48 +107,26 @@ alias_method :current_bucket=, :set_current_bucket_to private - + + def do_request(verb, path, options = {}, body = nil, attempts = 0, &block) + Service.response = nil + response = response_class.new(connection.request(verb, path, options, body, attempts, &block)) + Service.response = response + response + end + def response_class - FindResponseClass.for(self) + FindResponseClass.for(self, AWS::S3) end - + def process_options!(options, verb) options.replace(RequestOptions.process(options, verb)) end - - # Using the conventions layed out in the response_class works for more than 80% of the time. - # There are a few edge cases though where we want a given class to wrap its responses in different - # response classes depending on which method is being called. - def respond_with(klass) - eval(<<-EVAL, binding, __FILE__, __LINE__) - def new_response_class - #{klass} - end - class << self - alias_method :old_response_class, :response_class - alias_method :response_class, :new_response_class - end - EVAL - - yield - ensure - # Restore the original version - eval(<<-EVAL, binding, __FILE__, __LINE__) - class << self - alias_method :response_class, :old_response_class - end - EVAL - end - def bucket_name(name) name || current_bucket end - def retry_exceptions - [InternalError, RequestTimeout] - end - class RequestOptions < Hash #:nodoc: attr_reader :options, :verb @@ -204,29 +153,6 @@ end end end - - def initialize(attributes = {}) #:nodoc: - @attributes = attributes - end - - private - attr_reader :attributes - - def connection - self.class.connection - end - - def http - connection.http - end - - def request(*args, &block) - self.class.request(*args, &block) - end - - def method_missing(method, *args, &block) - attributes[method.to_s] || attributes[method] || super - end end end end Index: lib/aws/parsing.rb =================================================================== --- lib/aws/parsing.rb (revision 158) +++ lib/aws/parsing.rb (working copy) @@ -1,98 +1,96 @@ #:stopdoc: module AWS - module S3 - module Parsing - class << self - def parser=(parsing_library) - XmlParser.parsing_library = parsing_library + module Parsing + class << self + def parser=(parsing_library) + XmlParser.parsing_library = parsing_library + end + + def parser + XmlParser.parsing_library + end + end + + module Typecasting + def typecast(object) + case object + when Hash + typecast_hash(object) + when Array + object.map {|element| typecast(element)} + when String + CoercibleString.coerce(object) + else + object end - - def parser - XmlParser.parsing_library - end end - module Typecasting - def typecast(object) - case object - when Hash - typecast_hash(object) - when Array - object.map {|element| typecast(element)} - when String - CoercibleString.coerce(object) - else - object + def typecast_hash(hash) + if content = hash['__content__'] + typecast(content) + else + keys = hash.keys.map {|key| key.underscore} + values = hash.values.map {|value| typecast(value)} + keys.inject({}) do |new_hash, key| + new_hash[key] = values.slice!(0) + new_hash end end - - def typecast_hash(hash) - if content = hash['__content__'] - typecast(content) - else - keys = hash.keys.map {|key| key.underscore} - values = hash.values.map {|value| typecast(value)} - keys.inject({}) do |new_hash, key| - new_hash[key] = values.slice!(0) - new_hash - end - end - end end + end + + class XmlParser < Hash + include Typecasting - class XmlParser < Hash - include Typecasting + class << self + attr_accessor :parsing_library + end + + attr_reader :body, :xml_in, :root + + def initialize(body) + @body = body + unless body.strip.empty? + parse + set_root + typecast_xml_in + end + end + + private + + def parse + @xml_in = self.class.parsing_library.xml_in(body, parsing_options) + end - class << self - attr_accessor :parsing_library + def parsing_options + { + # Includes the enclosing tag as the top level key + 'keeproot' => true, + # Makes tag value available via the '__content__' key + 'contentkey' => '__content__', + # Always parse tags into a hash, even when there are no attributes + # (unless there is also no value, in which case it is nil) + 'forcecontent' => true, + # If a tag is empty, makes its content nil + 'suppressempty' => nil, + # Force nested elements to be put into an array, even if there is only one of them + 'forcearray' => ['Contents', 'Bucket', 'Grant'] + } end - attr_reader :body, :xml_in, :root + def set_root + @root = @xml_in.keys.first.underscore + end - def initialize(body) - @body = body - unless body.strip.empty? - parse - set_root - typecast_xml_in + def typecast_xml_in + typecast_xml = {} + @xml_in.dup.each do |key, value| # Some typecasting is destructive so we dup + typecast_xml[key.underscore] = typecast(value) end + # An empty body will try to update with a string so only update if the result is a hash + update(typecast_xml[root]) if typecast_xml[root].is_a?(Hash) end - - private - - def parse - @xml_in = self.class.parsing_library.xml_in(body, parsing_options) - end - - def parsing_options - { - # Includes the enclosing tag as the top level key - 'keeproot' => true, - # Makes tag value available via the '__content__' key - 'contentkey' => '__content__', - # Always parse tags into a hash, even when there are no attributes - # (unless there is also no value, in which case it is nil) - 'forcecontent' => true, - # If a tag is empty, makes its content nil - 'suppressempty' => nil, - # Force nested elements to be put into an array, even if there is only one of them - 'forcearray' => ['Contents', 'Bucket', 'Grant'] - } - end - - def set_root - @root = @xml_in.keys.first.underscore - end - - def typecast_xml_in - typecast_xml = {} - @xml_in.dup.each do |key, value| # Some typecasting is destructive so we dup - typecast_xml[key.underscore] = typecast(value) - end - # An empty body will try to update with a string so only update if the result is a hash - update(typecast_xml[root]) if typecast_xml[root].is_a?(Hash) - end - end end end end Index: lib/aws/s3.rb =================================================================== --- lib/aws/s3.rb (revision 158) +++ lib/aws/s3.rb (working copy) @@ -9,13 +9,14 @@ require 'open-uri' $:.unshift(File.dirname(__FILE__)) -require 's3/extensions' +require 'extensions' require_library_or_gem 'builder' unless defined? Builder require_library_or_gem 'mime/types' unless defined? MIME::Types +require 'base' require 's3/base' require 's3/version' -require 's3/parsing' +require 'parsing' require 's3/acl' require 's3/logging' require 's3/bittorrent' @@ -23,14 +24,17 @@ require 's3/owner' require 's3/bucket' require 's3/object' -require 's3/error' +require 'error' +require 'exceptions' require 's3/exceptions' -require 's3/connection' +require 'connection' +require 'authentication' require 's3/authentication' +require 'response' require 's3/response' -AWS::S3::Base.class_eval do - include AWS::S3::Connection::Management +AWS::Base.class_eval do + include AWS::Connection::Management end AWS::S3::Bucket.class_eval do @@ -47,7 +51,7 @@ # If libxml is installed, we use the FasterXmlSimple library, that provides most of the functionality of XmlSimple # except it uses the xml/libxml library for xml parsing (rather than REXML). If libxml isn't installed, we just fall back on # XmlSimple. -AWS::S3::Parsing.parser = +AWS::Parsing.parser = begin require_library_or_gem 'xml/libxml' # Older version of libxml aren't stable (bus error when requesting attributes that don't exist) so we Index: lib/aws/error.rb =================================================================== --- lib/aws/error.rb (revision 158) +++ lib/aws/error.rb (working copy) @@ -1,69 +1,67 @@ module AWS - module S3 - # Anything you do that makes a request to S3 could result in an error. If it does, the AWS::S3 library will raise an exception - # specific to the error. All exception that are raised as a result of a request returning an error response inherit from the - # ResponseError exception. So should you choose to rescue any such exception, you can simple rescue ResponseError. - # - # Say you go to delete a bucket, but the bucket turns out to not be empty. This results in a BucketNotEmpty error (one of the many - # errors listed at http://docs.amazonwebservices.com/AmazonS3/2006-03-01/ErrorCodeList.html): - # - # begin - # Bucket.delete('jukebox') - # rescue ResponseError => error - # # ... - # end - # - # Once you've captured the exception, you can extract the error message from S3, as well as the full error response, which includes - # things like the HTTP response code: - # - # error - # # => # - # error.message - # # => "The bucket you tried to delete is not empty" - # error.response.code - # # => 409 - # - # You could use this information to redisplay the error in a way you see fit, or just to log the error and continue on. - class Error - #:stopdoc: - attr_accessor :response - def initialize(error, response = nil) - @error = error - @response = response - @container = AWS::S3 - find_or_create_exception! + # Anything you do that makes a request to S3 could result in an error. If it does, the AWS::S3 library will raise an exception + # specific to the error. All exception that are raised as a result of a request returning an error response inherit from the + # ResponseError exception. So should you choose to rescue any such exception, you can simple rescue ResponseError. + # + # Say you go to delete a bucket, but the bucket turns out to not be empty. This results in a BucketNotEmpty error (one of the many + # errors listed at http://docs.amazonwebservices.com/AmazonS3/2006-03-01/ErrorCodeList.html): + # + # begin + # Bucket.delete('jukebox') + # rescue ResponseError => error + # # ... + # end + # + # Once you've captured the exception, you can extract the error message from S3, as well as the full error response, which includes + # things like the HTTP response code: + # + # error + # # => # + # error.message + # # => "The bucket you tried to delete is not empty" + # error.response.code + # # => 409 + # + # You could use this information to redisplay the error in a way you see fit, or just to log the error and continue on. + class Error + #:stopdoc: + attr_accessor :response + def initialize(error, container, response = nil) + @error = error + @response = response + @container = container + find_or_create_exception! + end + + def raise + Kernel.raise exception.new(message, response) + end + + private + attr_reader :error, :exception, :container + + def find_or_create_exception! + @exception = container.const_defined?(code) ? find_exception : create_exception end - def raise - Kernel.raise exception.new(message, response) + def find_exception + exception_class = container.const_get(code) + Kernel.raise ExceptionClassClash.new(exception_class) unless exception_class.ancestors.include?(ResponseError) + exception_class end - private - attr_reader :error, :exception, :container - - def find_or_create_exception! - @exception = container.const_defined?(code) ? find_exception : create_exception + def create_exception + container.const_set(code, Class.new(ResponseError)) + end + + def method_missing(method, *args, &block) + # We actually want nil if the attribute is nil. So we use has_key? rather than [] + ||. + if error.has_key?(method.to_s) + error[method.to_s] + else + super end - - def find_exception - exception_class = container.const_get(code) - Kernel.raise ExceptionClassClash.new(exception_class) unless exception_class.ancestors.include?(ResponseError) - exception_class - end - - def create_exception - container.const_set(code, Class.new(ResponseError)) - end - - def method_missing(method, *args, &block) - # We actually want nil if the attribute is nil. So we use has_key? rather than [] + ||. - if error.has_key?(method.to_s) - error[method.to_s] - else - super - end - end - end + end end end #:startdoc: \ No newline at end of file Index: lib/aws/base.rb =================================================================== --- lib/aws/base.rb (revision 0) +++ lib/aws/base.rb (revision 0) @@ -0,0 +1,151 @@ +module AWS #:nodoc: + # AWS::S3 is a Ruby library for Amazon's Simple Storage Service's REST API (http://aws.amazon.com/s3). + # Full documentation of the currently supported API can be found at http://docs.amazonwebservices.com/AmazonS3/2006-03-01. + # + # == Getting started + # + # To get started you need to require 'aws/s3': + # + # % irb -rubygems + # irb(main):001:0> require 'aws/s3' + # # => true + # + # The AWS::S3 library ships with an interactive shell called s3sh. From within it, you have access to all the operations the library exposes from the command line. + # + # % s3sh + # >> Version + # + # Before you can do anything, you must establish a connection using Base.establish_connection!. A basic connection would look something like this: + # + # AWS::S3::Base.establish_connection!( + # :access_key_id => 'abc', + # :secret_access_key => '123' + # ) + # + # The minimum connection options that you must specify are your access key id and your secret access key. + # + # (If you don't already have your access keys, all you need to sign up for the S3 service is an account at Amazon. You can sign up for S3 and get access keys by visiting http://aws.amazon.com/s3.) + # + # For convenience, if you set two special environment variables with the value of your access keys, the console will automatically create a default connection for you. For example: + # + # % cat .amazon_keys + # export AMAZON_ACCESS_KEY_ID='abcdefghijklmnop' + # export AMAZON_SECRET_ACCESS_KEY='1234567891012345' + # + # Then load it in your shell's rc file. + # + # % cat .zshrc + # if [[ -f "$HOME/.amazon_keys" ]]; then + # source "$HOME/.amazon_keys"; + # fi + # + # See more connection details at AWS::S3::Connection::Management::ClassMethods. + + # AWS::S3::Base is the abstract super class of all classes who make requests against S3, such as the built in + # Service, Bucket and S3Object classes. It provides methods for making requests, inferring or setting response classes, + # processing request options, and accessing attributes from S3's response data. + # + # Establishing a connection with the Base class is the entry point to using the library: + # + # AWS::S3::Base.establish_connection!(:access_key_id => '...', :secret_access_key => '...') + # + # The :access_key_id and :secret_access_key are the two required connection options. More + # details can be found in the docs for Connection::Management::ClassMethods. + # + # Extensive examples can be found in the README[link:files/README.html]. + class Base + class << self + # Wraps the current connection's request method and picks the appropriate response class to wrap the response in. + # If the response is an error, it will raise that error as an exception. All such exceptions can be caught by rescuing + # their superclass, the ResponseError exception class. + # + # 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) + process_options!(options, verb) + response = do_request(verb, path, options, body, attempts, &block) + Error::Response.new(response.response).error.raise if response.error? + response + # Once in a while, a request to S3 returns an internal error. A glitch in the matrix I presume. Since these + # errors are few and far between the request method will rescue InternalErrors the first three times they encouter them + # and will retry the request again. Most of the time the second attempt will work. + rescue *retry_exceptions + attempts == 3 ? raise : (attempts += 1; retry) + end + + [: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) + end + EVAL + end + + private + + def do_request(verb, path, options = {}, body = nil, attempts = 0, &block) + response_class.new(connection.request(verb, path, options, body, attempts, &block)) + end + + def response_class + FindResponseClass.for(self, AWS) + end + + def process_options!(options, verb) + raise NotImplementedError + end + + # Using the conventions layed out in the response_class works for more than 80% of the time. + # There are a few edge cases though where we want a given class to wrap its responses in different + # response classes depending on which method is being called. + def respond_with(klass) + eval(<<-EVAL, binding, __FILE__, __LINE__) + def new_response_class + #{klass} + end + + class << self + alias_method :old_response_class, :response_class + alias_method :response_class, :new_response_class + end + EVAL + + yield + ensure + # Restore the original version + eval(<<-EVAL, binding, __FILE__, __LINE__) + class << self + alias_method :response_class, :old_response_class + end + EVAL + end + + def retry_exceptions + [InternalError, RequestTimeout] + end + end + + def initialize(attributes = {}) #:nodoc: + @attributes = attributes + end + + private + attr_reader :attributes + + def connection + self.class.connection + end + + def http + connection.http + end + + def request(*args, &block) + self.class.request(*args, &block) + end + + def method_missing(method, *args, &block) + attributes[method.to_s] || attributes[method] || super + end + end +end From marcel at vernix.org Fri Jan 5 17:59:36 2007 From: marcel at vernix.org (Marcel Molina Jr.) Date: Fri, 5 Jan 2007 22:59:36 +0000 Subject: [s3-dev] EC2 preliminary work In-Reply-To: <2f0ee70f0701051431i7d7dd125ufd51191610eb87c7@mail.gmail.com> References: <2f0ee70f0701051431i7d7dd125ufd51191610eb87c7@mail.gmail.com> Message-ID: <20070105225936.GO88548@comox.textdrive.com> On Fri, Jan 05, 2007 at 05:31:28PM -0500, Joe Hosteny wrote: > Here was a first cut at re-factoring some code from AWS::S3 into the > AWS module so that EC2 stuff could be added. Go easy on me - I was > trying to learn the code as I went, and I think I made a bit of a > mess. But it may be useful to you to see some of this. Feel free to > use all or none of it as you see fit. Awesome. Thanks for diving into that Joe. I'm going to slice a chunk of time out of my weekend and try to process all the bugs and fixes that you guys emailed in over the holidays. The plan is to try to get a bug fix release out beginning of next week. marcel -- Marcel Molina Jr. From jhosteny at gmail.com Mon Jan 8 20:47:41 2007 From: jhosteny at gmail.com (Joe Hosteny) Date: Mon, 8 Jan 2007 20:47:41 -0500 Subject: [s3-dev] EC2 preliminary work In-Reply-To: <20070105225936.GO88548@comox.textdrive.com> References: <2f0ee70f0701051431i7d7dd125ufd51191610eb87c7@mail.gmail.com> <20070105225936.GO88548@comox.textdrive.com> Message-ID: <2f0ee70f0701081747w2765e893jeffeb90c7ef77561@mail.gmail.com> On 1/5/07, Marcel Molina Jr. wrote: > On Fri, Jan 05, 2007 at 05:31:28PM -0500, Joe Hosteny wrote: > > Here was a first cut at re-factoring some code from AWS::S3 into the > > AWS module so that EC2 stuff could be added. Go easy on me - I was > > trying to learn the code as I went, and I think I made a bit of a > > mess. But it may be useful to you to see some of this. Feel free to > > use all or none of it as you see fit. > > Awesome. Thanks for diving into that Joe. I'm going to slice a chunk of time > out of my weekend and try to process all the bugs and fixes that you guys > emailed in over the holidays. I've attached another patch. This one fixes a number of problems with the first, and adds some code to do the EC2 authentication (see the AWS::EC2::Base.query_for method). For example, try EC2::Base.connection.query_for({"Action"=>"DescribeImages"}) from the shell (now implemented in the file awssh). The authentication doesn't seem to work right now, and I'm not sure why. I'll look into it later. Again, feel free to use any portion of this. The authentication stuff can still be refactored quite a bit to simplify. > > The plan is to try to get a bug fix release out beginning of next week. > > marcel > -- > Marcel Molina Jr. > _______________________________________________ > amazon-s3-dev mailing list > amazon-s3-dev at rubyforge.org > http://rubyforge.org/mailman/listinfo/amazon-s3-dev > -------------- next part -------------- Index: test/test_helper.rb =================================================================== --- test/test_helper.rb (revision 158) +++ test/test_helper.rb (working copy) @@ -1,6 +1,7 @@ require 'test/unit' $:.unshift File.dirname(__FILE__) + '/../lib' -require 'aws/s3' +#require 'aws/s3' +require 'aws' require File.dirname(__FILE__) + '/mocks/base' require File.dirname(__FILE__) + '/fixtures' require_library_or_gem 'breakpoint' @@ -78,5 +79,6 @@ end class Test::Unit::TestCase + include AWS include AWS::S3 -end \ No newline at end of file +end Index: test/error_test.rb =================================================================== --- test/error_test.rb (revision 158) +++ test/error_test.rb (working copy) @@ -3,7 +3,7 @@ class ErrorTest < Test::Unit::TestCase def setup @container = AWS::S3 - @error = Error.new(Parsing::XmlParser.new(Fixtures::Errors.access_denied)) + @error = Error.new(Parsing::XmlParser.new(Fixtures::Errors.access_denied), @container) end def teardown @@ -12,7 +12,7 @@ def test_error_class_is_automatically_generated assert !@container.const_defined?('NotImplemented') - error = Error.new(Parsing::XmlParser.new(Fixtures::Errors.not_implemented)) + error = Error.new(Parsing::XmlParser.new(Fixtures::Errors.not_implemented), @container) assert @container.const_defined?('NotImplemented') end @@ -35,7 +35,7 @@ def test_response_is_passed_along_to_exception response = Error::Response.new(FakeResponse.new(:code => 409, :body => Fixtures::Errors.access_denied)) response.error.raise - rescue @container::ResponseError => e + rescue AWS::ResponseError => e assert e.response assert_kind_of Error::Response, e.response assert_equal response.error, e.response.error @@ -48,8 +48,8 @@ @container.const_set(:NotImplemented, Class.new) assert @container.const_defined?(:NotImplemented) - assert_raises(ExceptionClassClash) do - Error.new(Parsing::XmlParser.new(Fixtures::Errors.not_implemented)) + assert_raises(AWS::ExceptionClassClash) do + Error.new(Parsing::XmlParser.new(Fixtures::Errors.not_implemented), @container) end end Index: test/response_test.rb =================================================================== --- test/response_test.rb (revision 158) +++ test/response_test.rb (working copy) @@ -50,21 +50,21 @@ end def test_on_base - assert_equal Base::Response, FindResponseClass.for(Base) - assert_equal Base::Response, FindResponseClass.for(AWS::S3::Base) + assert_equal Base::Response, FindResponseClass.for(Base, AWS::S3) + assert_equal S3::Base::Response, FindResponseClass.for(AWS::S3::Base, AWS::S3) end def test_on_subclass_with_corresponding_response_class - assert_equal Bucket::Response, FindResponseClass.for(Bucket) - assert_equal Bucket::Response, FindResponseClass.for(AWS::S3::Bucket) + assert_equal Bucket::Response, FindResponseClass.for(Bucket, AWS::S3) + assert_equal Bucket::Response, FindResponseClass.for(AWS::S3::Bucket, AWS::S3) end def test_on_subclass_with_intermediary_parent_that_has_corresponding_response_class - assert_equal Bucket::Response, FindResponseClass.for(CampfireBucket) + assert_equal Bucket::Response, FindResponseClass.for(CampfireBucket, AWS::S3) end def test_on_subclass_with_no_corresponding_response_class_and_no_intermediary_parent - assert_equal Base::Response, FindResponseClass.for(BabyBase) + assert_equal Base::Response, FindResponseClass.for(BabyBase, AWS::S3) end -end \ No newline at end of file +end Index: test/connection_test.rb =================================================================== --- test/connection_test.rb (revision 158) +++ test/connection_test.rb (working copy) @@ -123,6 +123,7 @@ end def generate_options(options = {}) + options = options.merge(:default_host => DEFAULT_HOST) Connection::Options.new(options) end end \ No newline at end of file Index: test/base_test.rb =================================================================== --- test/base_test.rb (revision 158) +++ test/base_test.rb (working copy) @@ -6,20 +6,21 @@ Base.connection end - Base.establish_connection!(:access_key_id => '123', :secret_access_key => 'abc') - assert_kind_of Connection, Base.connection + S3::Base.establish_connection!(:access_key_id => '123', :secret_access_key => 'abc') + assert_kind_of Connection, S3::Base.connection - instance = Base.new - assert_equal instance.send(:connection), Base.connection - assert_equal instance.send(:http), Base.connection.http + instance = S3::Base.new + assert_equal instance.send(:connection), S3::Base.connection + assert_equal instance.send(:http), S3::Base.connection.http end def test_respond_with assert_equal Base::Response, Base.send(:response_class) - Base.send(:respond_with, Bucket::Response) do - assert_equal Bucket::Response, Base.send(:response_class) + assert_equal S3::Base::Response, S3::Base.send(:response_class) + S3::Base.send(:respond_with, Bucket::Response) do + assert_equal Bucket::Response, S3::Base.send(:response_class) end - assert_equal Base::Response, Base.send(:response_class) + assert_equal S3::Base::Response, S3::Base.send(:response_class) end def test_request_tries_again_when_encountering_an_internal_error @@ -80,7 +81,7 @@ end class MultiConnectionsTest < Test::Unit::TestCase - class ClassToTestSettingCurrentBucket < Base + class ClassToTestSettingCurrentBucket < S3::Base set_current_bucket_to 'foo' end @@ -93,7 +94,7 @@ assert !Base.connected? assert_raises(MissingAccessKey) do - Base.establish_connection! + S3::Base.establish_connection! end assert !Base.connected? @@ -103,32 +104,32 @@ end assert_nothing_raised do - Base.establish_connection!(:access_key_id => '123', :secret_access_key => 'abc') + S3::Base.establish_connection!(:access_key_id => '123', :secret_access_key => 'abc') end assert Base.connected? assert_nothing_raised do - Base.connection + S3::Base.connection end # All subclasses are currently using the default connection - assert Base.connection == Bucket.connection + assert S3::Base.connection == Bucket.connection # No need to pass in the required options. The default connection will supply them assert_nothing_raised do Bucket.establish_connection!(:server => 'foo.s3.amazonaws.com') end - assert Base.connection != Bucket.connection + assert S3::Base.connection != Bucket.connection assert_equal '123', Bucket.connection.access_key_id assert_equal 'foo', Bucket.connection.subdomain end def test_current_bucket - Base.establish_connection!(:access_key_id => '123', :secret_access_key => 'abc') + S3::Base.establish_connection!(:access_key_id => '123', :secret_access_key => 'abc') assert_raises(CurrentBucketNotSpecified) do - Base.current_bucket + S3::Base.current_bucket end S3Object.establish_connection!(:server => 'foo-bucket.s3.amazonaws.com') Index: test/mocks/base.rb =================================================================== --- test/mocks/base.rb (revision 158) +++ test/mocks/base.rb (working copy) @@ -1,72 +1,76 @@ require_library_or_gem 'flexmock' module AWS - module S3 - class FakeResponse < String - attr_reader :code, :body, :headers - def initialize(options = {}) - @code = options.delete(:code) || 200 - @body = options.delete(:body) || '' - @headers = {'content-type' => 'application/xml'}.merge(options.delete(:headers) || {}) - super(@body) + class FakeResponse < String + attr_reader :code, :body, :headers + def initialize(options = {}) + @code = options.delete(:code) || 200 + @body = options.delete(:body) || '' + @headers = {'content-type' => 'application/xml'}.merge(options.delete(:headers) || {}) + super(@body) + end + + # For ErrorResponse + def response + self + end + + def [](header) + headers[header] + end + + def each(&block) + headers.each(&block) + end + alias_method :each_header, :each + end + + class Base + class << self + @@responses = [] + @@in_test_mode = false + @@catch_all_response = nil + + def in_test_mode=(boolean) + @@in_test_mode = boolean end + + def responses + @@responses + end + + def catch_all_response + @@catch_all_response + end - # For ErrorResponse - def response - self + def reset! + responses.clear end - def [](header) - headers[header] + def request_returns(response_data) + responses.concat [response_data].flatten.map {|data| FakeResponse.new(data)} end - def each(&block) - headers.each(&block) + def request_always_returns(response_data, &block) + in_test_mode do + @@catch_all_response = FakeResponse.new(response_data) + yield + @@catch_all_response = nil + end end - alias_method :each_header, :each + + def in_test_mode(&block) + self.in_test_mode = true + yield + ensure + self.in_test_mode = false + end end + end - class Base + module S3 + class Base < AWS::Base class << self - @@responses = [] - @@in_test_mode = false - @@catch_all_response = nil - - def in_test_mode=(boolean) - @@in_test_mode = boolean - end - - def responses - @@responses - end - - def catch_all_response - @@catch_all_response - end - - def reset! - responses.clear - end - - def request_returns(response_data) - responses.concat [response_data].flatten.map {|data| FakeResponse.new(data)} - end - - def request_always_returns(response_data, &block) - in_test_mode do - @@catch_all_response = FakeResponse.new(response_data) - yield - @@catch_all_response = nil - end - end - - def in_test_mode(&block) - self.in_test_mode = true - yield - ensure - self.in_test_mode = false - end - alias_method :old_connection, :connection def connection if @@in_test_mode Index: Rakefile =================================================================== --- Rakefile (revision 158) +++ Rakefile (working copy) @@ -5,7 +5,7 @@ require 'rake/packagetask' require 'rake/gempackagetask' -require File.dirname(__FILE__) + '/lib/aws/s3' +require File.dirname(__FILE__) + '/lib/aws' def library_root File.dirname(__FILE__) Index: lib/aws/response.rb =================================================================== --- lib/aws/response.rb (revision 0) +++ lib/aws/response.rb (revision 0) @@ -0,0 +1,157 @@ +#:stopdoc: +module AWS + class Base + class Response < String + attr_reader :response, :body, :parsed + def initialize(response) + @response = response + @body = response.body.to_s + super(body) + end + + def container + AWS + end + + def headers + headers = {} + response.each do |header, value| + headers[header] = value + end + headers + end + memoized :headers + + def [](header) + headers[header] + end + + def each(&block) + headers.each(&block) + end + + def code + response.code.to_i + end + + {:success => 200..299, :redirect => 300..399, + :client_error => 400..499, :server_error => 500..599}.each do |result, code_range| + class_eval(<<-EVAL, __FILE__, __LINE__) + def #{result}? + return false unless response + (#{code_range}).include? code + end + EVAL + end + + def error? + !success? && response['content-type'] == 'application/xml' && parsed.root == 'error' + end + + def error + Error.new(parsed, container, self) + end + memoized :error + + def parsed + # XmlSimple is picky about what kind of object it parses, so we pass in body rather than self + Parsing::XmlParser.new(body) + end + memoized :parsed + + def inspect + "#<%s:0x%s %s %s>" % [self.class, object_id, response.code, response.message] + end + end + end + + # Requests whose response code is between 300 and 599 and contain an in their body + # are wrapped in an Error::Response. This Error::Response contains an Error object which raises an exception + # that corresponds to the error in the response body. The exception object contains the ErrorResponse, so + # in all cases where a request happens, you can rescue ResponseError and have access to the ErrorResponse and + # its Error object which contains information about the ResponseError. + # + # begin + # Bucket.create(..) + # rescue ResponseError => exception + # exception.response + # # => + # exception.response.error + # # => + # end + class Error + class Response < Base::Response + def error? + true + end + + def inspect + "#<%s:0x%s %s %s: '%s'>" % [self.class.name, object_id, response.code, error.code, error.message] + end + end + end + + # Guess response class name from current class name. If the guessed response class doesn't exist + # do the same thing to the current class's parent class, up the inheritance heirarchy until either + # a response class is found or until we get to the top of the heirarchy in which case we just use + # the the Base response class. + # + # Important: This implemantation assumes that the Base class has a corresponding Base::Response. + class FindResponseClass #:nodoc: + class << self + def for(start, container) + new(start, container).find + end + end + + def initialize(start, container) + @container = container + @current_class = start + @start_class = start + end + + def find + loop do + break if response_class_found? + @current_class = @current_class.superclass + if @current_class.nil? + @current_class = @start_class + path = @container.to_s.split("::") + path.pop + @container = path.join("::").to_const + next + end + end + target.const_get(class_to_find) + end + + private + attr_reader :container + attr_accessor :current_class + + def target + container.const_get(current_name) + end + + def target? + container.const_defined?(current_name) + end + + def response_class_found? + target? && target.const_defined?(class_to_find) + end + + def class_to_find + :Response + end + + def current_name + truncate(current_class) + end + + def truncate(klass) + klass.name[/[^:]+$/] + end + end +end +#:startdoc: \ No newline at end of file Index: lib/aws/authentication.rb =================================================================== --- lib/aws/authentication.rb (revision 158) +++ lib/aws/authentication.rb (working copy) @@ -1,218 +1,87 @@ module AWS - module S3 - # All authentication is taken care of for you by the AWS::S3 library. None the less, some details of the two types - # of authentication and when they are used may be of interest to some. - # - # === Header based authentication - # - # Header based authentication is achieved by setting a special Authorization header whose value - # is formatted like so: - # - # "AWS #{access_key_id}:#{encoded_canonical}" - # - # The access_key_id is the public key that is assigned by Amazon for a given account which you use when - # establishing your initial connection. The encoded_canonical is computed according to rules layed out - # by Amazon which we will describe presently. - # - # ==== Generating the encoded canonical string - # - # The "canonical string", generated by the CanonicalString class, is computed by collecting the current request method, - # a set of significant headers of the current request, and the current request path into a string. - # That canonical string is then encrypted with the secret_access_key assigned by Amazon. The resulting encrypted canonical - # string is then base 64 encoded. - # - # === Query string based authentication - # - # When accessing a restricted object from the browser, you can authenticate via the query string, by setting the following parameters: - # - # "AWSAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}" - # - # The QueryString class is responsible for generating the appropriate parameters for authentication via the - # query string. - # - # The access_key_id and encoded_canonical are the same as described in the Header based authentication section. - # The expires value dictates for how long the current url is valid (by default, it will expire in 5 minutes). Expiration can be specified - # either by an absolute time (expressed in seconds since the epoch), or in relative time (in number of seconds from now). - # Details of how to customize the expiration of the url are provided in the documentation for the QueryString class. - # - # All requests made by this library use header authentication. When a query string authenticated url is needed, - # the S3Object#url method will include the appropriate query string parameters. - # - # === Full authentication specification - # - # The full specification of the authentication protocol can be found at - # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAuthentication.html - class Authentication - constant :AMAZON_HEADER_PREFIX, 'x-amz-' - - # Signature is the abstract super class for the Header and QueryString authentication methods. It does the job - # of computing the canonical_string using the CanonicalString class as well as encoding the canonical string. The subclasses - # parameterize these computations and arrange them in a string form appropriate to how they are used, in one case a http request - # header value, and in the other case key/value query string parameter pairs. - class Signature < String #:nodoc: - attr_reader :request, :access_key_id, :secret_access_key - - def initialize(request, access_key_id, secret_access_key, options = {}) - super() - @request, @access_key_id, @secret_access_key = request, access_key_id, secret_access_key - @options = options - end - - private - - def canonical_string - options = {} - options[:expires] = expires if expires? - CanonicalString.new(request, options) - end - memoized :canonical_string - - def encoded_canonical - digest = OpenSSL::Digest::Digest.new('sha1') - b64_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, secret_access_key, canonical_string)).strip - url_encode? ? CGI.escape(b64_hmac) : b64_hmac - end - - def url_encode? - !@options[:url_encode].nil? - end - - def expires? - is_a? QueryString - end - - def date - request['date'].to_s.strip.empty? ? Time.now : Time.parse(request['date']) - end + # All authentication is taken care of for you by the AWS::S3 library. None the less, some details of the two types + # of authentication and when they are used may be of interest to some. + # + # === Header based authentication + # + # Header based authentication is achieved by setting a special Authorization header whose value + # is formatted like so: + # + # "AWS #{access_key_id}:#{encoded_canonical}" + # + # The access_key_id is the public key that is assigned by Amazon for a given account which you use when + # establishing your initial connection. The encoded_canonical is computed according to rules layed out + # by Amazon which we will describe presently. + # + # ==== Generating the encoded canonical string + # + # The "canonical string", generated by the CanonicalString class, is computed by collecting the current request method, + # a set of significant headers of the current request, and the current request path into a string. + # That canonical string is then encrypted with the secret_access_key assigned by Amazon. The resulting encrypted canonical + # string is then base 64 encoded. + # + # === Query string based authentication + # + # When accessing a restricted object from the browser, you can authenticate via the query string, by setting the following parameters: + # + # "AWSAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}" + # + # The QueryString class is responsible for generating the appropriate parameters for authentication via the + # query string. + # + # The access_key_id and encoded_canonical are the same as described in the Header based authentication section. + # The expires value dictates for how long the current url is valid (by default, it will expire in 5 minutes). Expiration can be specified + # either by an absolute time (expressed in seconds since the epoch), or in relative time (in number of seconds from now). + # Details of how to customize the expiration of the url are provided in the documentation for the QueryString class. + # + # All requests made by this library use header authentication. When a query string authenticated url is needed, + # the S3Object#url method will include the appropriate query string parameters. + # + # === Full authentication specification + # + # The full specification of the authentication protocol can be found at + # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAuthentication.html + module Authentication + constant :AMAZON_HEADER_PREFIX, 'x-amz-' + + def self.included(base) + constants.each do |c| + base.const_set(c, const_get(c)) end - - # Provides header authentication by computing the value of the Authorization header. More details about the - # various authentication schemes can be found in the docs for its containing module, Authentication. - class Header < Signature #:nodoc: - def initialize(*args) - super - self << "AWS #{access_key_id}:#{encoded_canonical}" - end + end + + # Signature is the abstract super class for the Header and QueryString authentication methods. It does the job + # of computing the canonical_string using the CanonicalString class as well as encoding the canonical string. The subclasses + # parameterize these computations and arrange them in a string form appropriate to how they are used, in one case a http request + # header value, and in the other case key/value query string parameter pairs. + class BaseSignature < String #:nodoc: + attr_reader :request, :access_key_id, :secret_access_key + + def initialize(request, access_key_id, secret_access_key, options = {}) + super() + @request, @access_key_id, @secret_access_key = request, access_key_id, secret_access_key + @options = options end - - # Provides query string authentication by computing the three authorization parameters: AWSAccessKeyId, Expires and Signature. - # More details about the various authentication schemes can be found in the docs for its containing module, Authentication. - class QueryString < Signature #:nodoc: - constant :DEFAULT_EXPIRY, 300 # 5 minutes - - def initialize(*args) - super - @options[:url_encode] = true - self << build + + private + + def encoded_canonical + digest = OpenSSL::Digest::Digest.new('sha1') + b64_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, secret_access_key, canonical_string)).strip + url_encode? ? CGI.escape(b64_hmac) : b64_hmac end - private - - # Will return one of three values, in the following order of precedence: - # - # 1) Seconds since the epoch explicitly passed in the +:expires+ option - # 2) The current time in seconds since the epoch plus the number of seconds passed in - # the +:expires_in+ option - # 3) The current time in seconds since the epoch plus the default number of seconds (60 seconds) - def expires - return @options[:expires] if @options[:expires] - date.to_i + (@options[:expires_in] || DEFAULT_EXPIRY) - end - - # Keep in alphabetical order - def build - "AWSAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}" - end - end - - # The CanonicalString is used to generate an encrypted signature, signed with your secrect access key. It is composed of - # data related to the given request for which it provides authentication. This data includes the request method, request headers, - # and the request path. Both Header and QueryString use it to generate their signature. - class CanonicalString < String #:nodoc: - class << self - def default_headers - %w(content-type content-md5) - end - - def interesting_headers - ['content-md5', 'content-type', 'date', amazon_header_prefix] - end - - def amazon_header_prefix - /^#{AMAZON_HEADER_PREFIX}/io - end + def url_encode? + !@options[:url_encode].nil? end - attr_reader :request, :headers - - def initialize(request, options = {}) - super() - @request = request - @headers = {} - @options = 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 - build + def expires? + false end - - private - def build - self << "#{request.method}\n" - ensure_date_is_valid - - initialize_headers - set_expiry! - headers.sort_by {|k, _| k}.each do |key, value| - value = value.to_s.strip - self << (key =~ self.class.amazon_header_prefix ? "#{key}:#{value}" : value) - self << "\n" - end - self << path - end - - def initialize_headers - identify_interesting_headers - set_default_headers - end - - def set_expiry! - self.headers['date'] = @options[:expires] if @options[:expires] - end - - def ensure_date_is_valid - request['Date'] ||= Time.now.httpdate - end - - def identify_interesting_headers - request.each do |key, value| - key = key.downcase # Can't modify frozen string so no bang - if self.class.interesting_headers.any? {|header| header === key} - self.headers[key] = value.to_s.strip - end - end - end - - def set_default_headers - self.class.default_headers.each do |header| - self.headers[header] ||= '' - end - end - - def path - [only_path, extract_significant_parameter].compact.join('?') - end - - def extract_significant_parameter - request.path[/[&?](acl|torrent|logging)(?:&|=|$)/, 1] - end - - def only_path - request.path[/^[^?]*/] - end - end + def date + request['date'].to_s.strip.empty? ? Time.now : Time.parse(request['date']) + end end end end \ No newline at end of file Index: lib/aws/extensions.rb =================================================================== --- lib/aws/extensions.rb (revision 158) +++ lib/aws/extensions.rb (working copy) @@ -39,6 +39,11 @@ downcase.tr('_', '-') end + def to_const + s = split("::") + s.inject(Module.const_get(s.shift.to_s)) { |cont, mod| cont.const_get(mod.to_s) } + end + # ActiveSupport adds an underscore method to String so let's just use that one if # we find that the method is already defined def underscore @@ -213,6 +218,49 @@ end end if Class.instance_methods(false).grep(/^cattr_(?:reader|writer|accessor)$/).empty? +class Module # :nodoc: + def mattr_reader(*syms) + syms.flatten.each do |sym| + module_eval(<<-EOS, __FILE__, __LINE__) + unless defined? @@#{sym} + @@#{sym} = nil + end + + def self.#{sym} + @@#{sym} + end + + def #{sym} + @@#{sym} + end + EOS + end + end + + def mattr_writer(*syms) + syms.flatten.each do |sym| + module_eval(<<-EOS, __FILE__, __LINE__) + unless defined? @@#{sym} + @@#{sym} = nil + end + + def self.#{sym}=(obj) + @@#{sym} = obj + end + + def #{sym}=(obj) + @@#{sym} = obj + end + EOS + end + end + + def mattr_accessor(*syms) + mattr_reader(*syms) + mattr_writer(*syms) + end +end if Module.instance_methods(false).grep(/^mattr_(?:reader|writer|accessor)$/).empty? + module SelectiveAttributeProxy def self.included(klass) klass.extend(ClassMethods) Index: lib/aws/connection.rb =================================================================== --- lib/aws/connection.rb (revision 158) +++ lib/aws/connection.rb (working copy) @@ -1,259 +1,278 @@ module AWS - module S3 - class Connection #:nodoc: - class << self - def connect(options = {}) - new(options) - end + class Connection #:nodoc: + class << self + def connect(options = {}) + new(options) + end + + def prepare_path(path) + path = path.remove_extended unless path.utf8? + URI.escape(path) + end + end + + attr_reader :access_key_id, :secret_access_key, :http, :container, :options + + # Creates a new connection. Connections make the actual requests to S3, though these requests are usually + # called from subclasses of Base. + # + # For details on establishing connections, check the Connection::Management::ClassMethods. + def initialize(options = {}) + @options = Options.new(options) + @container = @options[:container] || AWS + connect + end - def prepare_path(path) - path = path.remove_extended unless path.utf8? - URI.escape(path) + def request(verb, path, headers = {}, body = nil, attempts = 0, &block) + body.rewind if body.respond_to?(:rewind) unless attempts.zero? + + requester = Proc.new do + path = self.class.prepare_path(path) + request = request_method(verb).new(path, headers) + ensure_content_type!(request) + add_user_agent!(request) + authenticate!(request) + if body + if body.respond_to?(:read) + request.body_stream = body + request.content_length = body.respond_to?(:lstat) ? body.lstat.size : body.size + else + request.body = body + end end + http.request(request, &block) end - attr_reader :access_key_id, :secret_access_key, :http, :options + if persistent? + http.start unless http.started? + requester.call + else + http.start(&requester) + end + rescue Errno::EPIPE, Timeout::Error, Errno::EPIPE, Errno::EINVAL + @http = create_connection + attempts == 3 ? raise : (attempts += 1; retry) + end + + def url_for(path, options = {}) + path = self.class.prepare_path(path) + request = request_method(:get).new(path, {}) + query_string = query_string_authentication(request, options) + "#{protocol(options)}#{http.address}#{path}?#{query_string}" + end + + def query_for(query) + qs = query.to_a.collect! { |p| "#{p[0]}=#{p[1]}" }.join("&") + path = "#{path}/?#{qs}" if qs != "" + path = self.class.prepare_path(path) + request = request_method(:get).new(path, {}) + path = URI.parse(request.path).path + query_string = query_string_authentication(request, options) + "#{protocol(options)}#{http.address}#{path}?#{query_string}" + end + + def subdomain + http.address[/^([^.]+).#{options[:default_host]}$/, 1] + end + + def persistent? + options[:persistent] + end + + def protocol(options = {}) + (options[:use_ssl] || http.use_ssl?) ? 'https://' : 'http://' + end + + private + def extract_keys! + missing_keys = [] + extract_key = Proc.new {|key| options[key] || (missing_keys.push(key); nil)} + @access_key_id = extract_key[:access_key_id] + @secret_access_key = extract_key[:secret_access_key] + raise MissingAccessKey.new(missing_keys) unless missing_keys.empty? + end - # Creates a new connection. Connections make the actual requests to S3, though these requests are usually - # called from subclasses of Base. - # - # For details on establishing connections, check the Connection::Management::ClassMethods. - def initialize(options = {}) - @options = Options.new(options) - connect + def create_connection + http = Net::HTTP.new(options[:server], options[:port]) + http.use_ssl = !options[:use_ssl].nil? || options[:port] == 443 + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + http end - - def request(verb, path, headers = {}, body = nil, attempts = 0, &block) - body.rewind if body.respond_to?(:rewind) unless attempts.zero? - - requester = Proc.new do - path = self.class.prepare_path(path) - request = request_method(verb).new(path, headers) - ensure_content_type!(request) - add_user_agent!(request) - authenticate!(request) - if body - if body.respond_to?(:read) - request.body_stream = body - request.content_length = body.respond_to?(:lstat) ? body.lstat.size : body.size - else - request.body = body - end - end - http.request(request, &block) - end - - if persistent? - http.start unless http.started? - requester.call - else - http.start(&requester) - end - rescue Errno::EPIPE, Timeout::Error, Errno::EPIPE, Errno::EINVAL + + def connect + extract_keys! @http = create_connection - attempts == 3 ? raise : (attempts += 1; retry) end - def url_for(path, options = {}) - path = self.class.prepare_path(path) - request = request_method(:get).new(path, {}) - query_string = query_string_authentication(request, options) - "#{protocol(options)}#{http.address}#{path}?#{query_string}" + def ensure_content_type!(request) + request['Content-Type'] ||= 'binary/octet-stream' end - def subdomain - http.address[/^([^.]+).#{DEFAULT_HOST}$/, 1] + # Just do Header authentication for now + def authenticate!(request) + request['Authorization'] = "#{container}::Authentication::Header".to_const.new(request, access_key_id, secret_access_key) end - def persistent? - options[:persistent] + def add_user_agent!(request) + version = container.const_get(:Version) + request['User-Agent'] ||= "#{container.to_s}/#{version}" end - def protocol(options = {}) - (options[:use_ssl] || http.use_ssl?) ? 'https://' : 'http://' + def query_string_authentication(request, options = {}) + "#{container}::Authentication::QueryString".to_const.new(request, access_key_id, secret_access_key, options) end + + def request_method(verb) + Net::HTTP.const_get(verb.to_s.capitalize) + end - private - def extract_keys! - missing_keys = [] - extract_key = Proc.new {|key| options[key] || (missing_keys.push(key); nil)} - @access_key_id = extract_key[:access_key_id] - @secret_access_key = extract_key[:secret_access_key] - raise MissingAccessKey.new(missing_keys) unless missing_keys.empty? + def method_missing(method, *args, &block) + options[method] || super + end + + module Management #:nodoc: + def self.included(base) + base.cattr_accessor :connections + base.connections = {} + base.instance_eval(<<-EVAL, __FILE__, __LINE__) + def container + scope = /(.+)::Base/.match(#{base}.to_s)[1] + scope.to_const + end + def default_connection_name + #{base}.to_s + end + def default_host + container.send(:default_host) + end + EVAL + base.extend ClassMethods + end + + # Manage the creation and destruction of connections for AWS::S3::Base and its subclasses. Connections are + # created with establish_connection!. + module ClassMethods + # Creates a new connection with which to make requests to the S3 servers for the calling class. + # + # AWS::S3::Base.establish_connection!(:access_key_id => '...', :secret_access_key => '...') + # + # You can set connections for every subclass of AWS::S3::Base. Once the initial connection is made on + # Base, all subsequent connections will inherit whatever values you don't specify explictly. This allows you to + # customize details of the connection, such as what server the requests are made to, by just specifying one + # option. + # + # AWS::S3::Bucket.established_connection!(:use_ssl => true) + # + # The Bucket connection would inherit the :access_key_id and the :secret_access_key from + # Base's connection. Unlike the Base connection, all Bucket requests would be made over SSL. + # + # == Required arguments + # + # * :access_key_id - The access key id for your S3 account. Provided by Amazon. + # * :secret_access_key - The secret access key for your S3 account. Provided by Amazon. + # + # If any of these required arguments is missing, a MissingAccessKey exception will be raised. + # + # == Optional arguments + # + # * :server - The server to make requests to. You can use this to specify your bucket in the subdomain, + # or your own domain's cname if you are using virtual hosted buckets. Defaults to s3.amazonaws.com. + # * :port - The port to the requests should be made on. Defaults to 80 or 443 if the :use_ssl + # argument is set. + # * :use_ssl - Whether requests should be made over SSL. If set to true, the :port argument + # will be implicitly set to 443, unless specified otherwise. Defaults to false. + # * :persistent - Whether to use a persistent connection to the server. Having this on provides around a two fold + # performance increase but for long running processes some firewalls may find the long lived connection suspicious and close the connection. + # If you run into connection errors, try setting :persistent to false. Defaults to true. + def establish_connection!(options = {}) + # After you've already established the default connection, just specify + # the difference for subsequent connections + options = options.merge(:default_host => default_host, :container => container) + options = default_connection.options.merge(options) if connected? + connections[connection_name] = Connection.connect(options) end - def create_connection - http = Net::HTTP.new(options[:server], options[:port]) - http.use_ssl = !options[:use_ssl].nil? || options[:port] == 443 - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - http + # Returns the connection for the current class, or Base's default connection if the current class does not + # have its own connection. + # + # If not connection has been established yet, NoConnectionEstablished will be raised. + def connection + if connected? + connections[connection_name] || default_connection + else + raise NoConnectionEstablished + end end - def connect - extract_keys! - @http = create_connection + # Returns true if a connection has been made yet. + def connected? + !connections.empty? end - def ensure_content_type!(request) - request['Content-Type'] ||= 'binary/octet-stream' + # Removes the connection for the current class. If there is no connection for the current class, the default + # connection will be removed. + def disconnect(name = connection_name) + name = default_connection unless connections.has_key?(name) + connection = connections[name] + connection.http.finish if connection.persistent? + connections.delete(name) end - # Just do Header authentication for now - def authenticate!(request) - request['Authorization'] = Authentication::Header.new(request, access_key_id, secret_access_key) + # Clears *all* connections, from all classes, with prejudice. + def disconnect! + connections.each_key {|connection| disconnect(connection)} end - - def add_user_agent!(request) - request['User-Agent'] ||= "AWS::S3/#{Version}" - end - - def query_string_authentication(request, options = {}) - Authentication::QueryString.new(request, access_key_id, secret_access_key, options) - end - def request_method(verb) - Net::HTTP.const_get(verb.to_s.capitalize) + private + def connection_name + name + end + + def default_connection + connections[default_connection_name] + end + end + end + + class Options < Hash #:nodoc: + class << self + def valid_options + [:access_key_id, :secret_access_key, :server, :default_host, :container, :port, :use_ssl, :persistent] end - - def method_missing(method, *args, &block) - options[method] || super + end + + attr_reader :options + def initialize(options = {}) + super() + @options = options + validate! + extract_persistent! + extract_server! + extract_port! + extract_remainder! + end + + private + def extract_persistent! + self[:persistent] = options.has_key?(:persitent) ? options[:persitent] : true end - module Management #:nodoc: - def self.included(base) - base.cattr_accessor :connections - base.connections = {} - base.extend ClassMethods + def extract_server! + self[:server] = options.delete(:server) || options[:default_host] end - - # Manage the creation and destruction of connections for AWS::S3::Base and its subclasses. Connections are - # created with establish_connection!. - module ClassMethods - # Creates a new connection with which to make requests to the S3 servers for the calling class. - # - # AWS::S3::Base.establish_connection!(:access_key_id => '...', :secret_access_key => '...') - # - # You can set connections for every subclass of AWS::S3::Base. Once the initial connection is made on - # Base, all subsequent connections will inherit whatever values you don't specify explictly. This allows you to - # customize details of the connection, such as what server the requests are made to, by just specifying one - # option. - # - # AWS::S3::Bucket.established_connection!(:use_ssl => true) - # - # The Bucket connection would inherit the :access_key_id and the :secret_access_key from - # Base's connection. Unlike the Base connection, all Bucket requests would be made over SSL. - # - # == Required arguments - # - # * :access_key_id - The access key id for your S3 account. Provided by Amazon. - # * :secret_access_key - The secret access key for your S3 account. Provided by Amazon. - # - # If any of these required arguments is missing, a MissingAccessKey exception will be raised. - # - # == Optional arguments - # - # * :server - The server to make requests to. You can use this to specify your bucket in the subdomain, - # or your own domain's cname if you are using virtual hosted buckets. Defaults to s3.amazonaws.com. - # * :port - The port to the requests should be made on. Defaults to 80 or 443 if the :use_ssl - # argument is set. - # * :use_ssl - Whether requests should be made over SSL. If set to true, the :port argument - # will be implicitly set to 443, unless specified otherwise. Defaults to false. - # * :persistent - Whether to use a persistent connection to the server. Having this on provides around a two fold - # performance increase but for long running processes some firewalls may find the long lived connection suspicious and close the connection. - # If you run into connection errors, try setting :persistent to false. Defaults to true. - def establish_connection!(options = {}) - # After you've already established the default connection, just specify - # the difference for subsequent connections - options = default_connection.options.merge(options) if connected? - connections[connection_name] = Connection.connect(options) - end - - # Returns the connection for the current class, or Base's default connection if the current class does not - # have its own connection. - # - # If not connection has been established yet, NoConnectionEstablished will be raised. - def connection - if connected? - connections[connection_name] || default_connection - else - raise NoConnectionEstablished - end - end - - # Returns true if a connection has been made yet. - def connected? - !connections.empty? - end - - # Removes the connection for the current class. If there is no connection for the current class, the default - # connection will be removed. - def disconnect(name = connection_name) - name = default_connection unless connections.has_key?(name) - connection = connections[name] - connection.http.finish if connection.persistent? - connections.delete(name) - end - - # Clears *all* connections, from all classes, with prejudice. - def disconnect! - connections.each_key {|connection| disconnect(connection)} - end - private - def connection_name - name - end - - def default_connection_name - 'AWS::S3::Base' - end - - def default_connection - connections[default_connection_name] - end + def extract_port! + self[:port] = options.delete(:port) || (options[:use_ssl] ? 443 : 80) end - end - class Options < Hash #:nodoc: - class << self - def valid_options - [:access_key_id, :secret_access_key, :server, :port, :use_ssl, :persistent] - end + def extract_remainder! + update(options) end - attr_reader :options - def initialize(options = {}) - super() - @options = options - validate! - extract_persistent! - extract_server! - extract_port! - extract_remainder! + def validate! + invalid_options = options.keys.select {|key| !self.class.valid_options.include?(key)} + raise InvalidConnectionOption.new(invalid_options) unless invalid_options.empty? end - - private - def extract_persistent! - self[:persistent] = options.has_key?(:persitent) ? options[:persitent] : true - end - - def extract_server! - self[:server] = options.delete(:server) || DEFAULT_HOST - end - - def extract_port! - self[:port] = options.delete(:port) || (options[:use_ssl] ? 443 : 80) - end - - def extract_remainder! - update(options) - end - - def validate! - invalid_options = options.keys.select {|key| !self.class.valid_options.include?(key)} - raise InvalidConnectionOption.new(invalid_options) unless invalid_options.empty? - end - end end end end \ No newline at end of file Index: lib/aws/exceptions.rb =================================================================== --- lib/aws/exceptions.rb (revision 0) +++ lib/aws/exceptions.rb (revision 0) @@ -0,0 +1,64 @@ +module AWS + + # Abstract super class of all AWS::S3 exceptions + class AWSException < StandardError + end + + # All responses with a code between 300 and 599 that contain an body are wrapped in an + # ErrorResponse which contains an Error object. This Error class generates a custom exception with the name + # of the xml Error and its message. All such runtime generated exception classes descend from ResponseError + # and contain the ErrorResponse object so that all code that makes a request can rescue ResponseError and get + # access to the ErrorResponse. + class ResponseError < AWSException + attr_reader :response + def initialize(message, response) + @response = response + super(message) + end + end + + #:stopdoc: + + # Most ResponseError's are created just time on a need to have basis, but we explicitly define the + # InternalError exception because we want to explicitly rescue InternalError in some cases. + class InternalError < ResponseError + end + + class NoSuchKey < ResponseError + end + + class RequestTimeout < ResponseError + end + + # Abstract super class for all invalid options. + class InvalidOption < AWSException + end + + # Raised if either the access key id or secret access key arguments are missing when establishing a connection. + class MissingAccessKey < InvalidOption + def initialize(missing_keys) + key_list = missing_keys.map {|key| key.to_s}.join(' and the ') + super("You did not provide both required access keys. Please provide the #{key_list}.") + end + end + + # Raised if a request is attempted before any connections have been established. + class NoConnectionEstablished < AWSException + end + + # Raised if an unrecognized option is passed when establishing a connection. + class InvalidConnectionOption < InvalidOption + def initialize(invalid_options) + message = "The following connection options are invalid: #{invalid_options.join(', ')}. " + + "The valid connection options are: #{Connection::Options.valid_options.join(', ')}." + super(message) + end + end + + class ExceptionClassClash < AWSException #:nodoc: + def initialize(klass) + message = "The exception class you tried to create (`#{klass}') exists and is not an exception" + super(message) + end + end +end Index: lib/aws/s3/authentication.rb =================================================================== --- lib/aws/s3/authentication.rb (revision 158) +++ lib/aws/s3/authentication.rb (working copy) @@ -1,218 +0,0 @@ -module AWS - module S3 - # All authentication is taken care of for you by the AWS::S3 library. None the less, some details of the two types - # of authentication and when they are used may be of interest to some. - # - # === Header based authentication - # - # Header based authentication is achieved by setting a special Authorization header whose value - # is formatted like so: - # - # "AWS #{access_key_id}:#{encoded_canonical}" - # - # The access_key_id is the public key that is assigned by Amazon for a given account which you use when - # establishing your initial connection. The encoded_canonical is computed according to rules layed out - # by Amazon which we will describe presently. - # - # ==== Generating the encoded canonical string - # - # The "canonical string", generated by the CanonicalString class, is computed by collecting the current request method, - # a set of significant headers of the current request, and the current request path into a string. - # That canonical string is then encrypted with the secret_access_key assigned by Amazon. The resulting encrypted canonical - # string is then base 64 encoded. - # - # === Query string based authentication - # - # When accessing a restricted object from the browser, you can authenticate via the query string, by setting the following parameters: - # - # "AWSAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}" - # - # The QueryString class is responsible for generating the appropriate parameters for authentication via the - # query string. - # - # The access_key_id and encoded_canonical are the same as described in the Header based authentication section. - # The expires value dictates for how long the current url is valid (by default, it will expire in 5 minutes). Expiration can be specified - # either by an absolute time (expressed in seconds since the epoch), or in relative time (in number of seconds from now). - # Details of how to customize the expiration of the url are provided in the documentation for the QueryString class. - # - # All requests made by this library use header authentication. When a query string authenticated url is needed, - # the S3Object#url method will include the appropriate query string parameters. - # - # === Full authentication specification - # - # The full specification of the authentication protocol can be found at - # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAuthentication.html - class Authentication - constant :AMAZON_HEADER_PREFIX, 'x-amz-' - - # Signature is the abstract super class for the Header and QueryString authentication methods. It does the job - # of computing the canonical_string using the CanonicalString class as well as encoding the canonical string. The subclasses - # parameterize these computations and arrange them in a string form appropriate to how they are used, in one case a http request - # header value, and in the other case key/value query string parameter pairs. - class Signature < String #:nodoc: - attr_reader :request, :access_key_id, :secret_access_key - - def initialize(request, access_key_id, secret_access_key, options = {}) - super() - @request, @access_key_id, @secret_access_key = request, access_key_id, secret_access_key - @options = options - end - - private - - def canonical_string - options = {} - options[:expires] = expires if expires? - CanonicalString.new(request, options) - end - memoized :canonical_string - - def encoded_canonical - digest = OpenSSL::Digest::Digest.new('sha1') - b64_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, secret_access_key, canonical_string)).strip - url_encode? ? CGI.escape(b64_hmac) : b64_hmac - end - - def url_encode? - !@options[:url_encode].nil? - end - - def expires? - is_a? QueryString - end - - def date - request['date'].to_s.strip.empty? ? Time.now : Time.parse(request['date']) - end - end - - # Provides header authentication by computing the value of the Authorization header. More details about the - # various authentication schemes can be found in the docs for its containing module, Authentication. - class Header < Signature #:nodoc: - def initialize(*args) - super - self << "AWS #{access_key_id}:#{encoded_canonical}" - end - end - - # Provides query string authentication by computing the three authorization parameters: AWSAccessKeyId, Expires and Signature. - # More details about the various authentication schemes can be found in the docs for its containing module, Authentication. - class QueryString < Signature #:nodoc: - constant :DEFAULT_EXPIRY, 300 # 5 minutes - - def initialize(*args) - super - @options[:url_encode] = true - self << build - end - - private - - # Will return one of three values, in the following order of precedence: - # - # 1) Seconds since the epoch explicitly passed in the +:expires+ option - # 2) The current time in seconds since the epoch plus the number of seconds passed in - # the +:expires_in+ option - # 3) The current time in seconds since the epoch plus the default number of seconds (60 seconds) - def expires - return @options[:expires] if @options[:expires] - date.to_i + (@options[:expires_in] || DEFAULT_EXPIRY) - end - - # Keep in alphabetical order - def build - "AWSAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}" - end - end - - # The CanonicalString is used to generate an encrypted signature, signed with your secrect access key. It is composed of - # data related to the given request for which it provides authentication. This data includes the request method, request headers, - # and the request path. Both Header and QueryString use it to generate their signature. - class CanonicalString < String #:nodoc: - class << self - def default_headers - %w(content-type content-md5) - end - - def interesting_headers - ['content-md5', 'content-type', 'date', amazon_header_prefix] - end - - def amazon_header_prefix - /^#{AMAZON_HEADER_PREFIX}/io - end - end - - attr_reader :request, :headers - - def initialize(request, options = {}) - super() - @request = request - @headers = {} - @options = 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 - build - end - - private - def build - self << "#{request.method}\n" - ensure_date_is_valid - - initialize_headers - set_expiry! - - headers.sort_by {|k, _| k}.each do |key, value| - value = value.to_s.strip - self << (key =~ self.class.amazon_header_prefix ? "#{key}:#{value}" : value) - self << "\n" - end - self << path - end - - def initialize_headers - identify_interesting_headers - set_default_headers - end - - def set_expiry! - self.headers['date'] = @options[:expires] if @options[:expires] - end - - def ensure_date_is_valid - request['Date'] ||= Time.now.httpdate - end - - def identify_interesting_headers - request.each do |key, value| - key = key.downcase # Can't modify frozen string so no bang - if self.class.interesting_headers.any? {|header| header === key} - self.headers[key] = value.to_s.strip - end - end - end - - def set_default_headers - self.class.default_headers.each do |header| - self.headers[header] ||= '' - end - end - - def path - [only_path, extract_significant_parameter].compact.join('?') - end - - def extract_significant_parameter - request.path[/[&?](acl|torrent|logging)(?:&|=|$)/, 1] - end - - def only_path - request.path[/^[^?]*/] - end - end - end - end -end \ No newline at end of file Index: lib/aws/s3/exceptions.rb =================================================================== --- lib/aws/s3/exceptions.rb (revision 158) +++ lib/aws/s3/exceptions.rb (working copy) @@ -2,39 +2,9 @@ module S3 # Abstract super class of all AWS::S3 exceptions - class S3Exception < StandardError + class S3Exception < AWSException end - # All responses with a code between 300 and 599 that contain an body are wrapped in an - # ErrorResponse which contains an Error object. This Error class generates a custom exception with the name - # of the xml Error and its message. All such runtime generated exception classes descend from ResponseError - # and contain the ErrorResponse object so that all code that makes a request can rescue ResponseError and get - # access to the ErrorResponse. - class ResponseError < S3Exception - attr_reader :response - def initialize(message, response) - @response = response - super(message) - end - end - - #:stopdoc: - - # Most ResponseError's are created just time on a need to have basis, but we explicitly define the - # InternalError exception because we want to explicitly rescue InternalError in some cases. - class InternalError < ResponseError - end - - class NoSuchKey < ResponseError - end - - class RequestTimeout < ResponseError - end - - # Abstract super class for all invalid options. - class InvalidOption < S3Exception - end - # Raised if an invalid value is passed to the :access option when creating a Bucket or an S3Object. class InvalidAccessControlLevel < InvalidOption def initialize(valid_levels, access_level) @@ -42,27 +12,6 @@ end end - # Raised if either the access key id or secret access key arguments are missing when establishing a connection. - class MissingAccessKey < InvalidOption - def initialize(missing_keys) - key_list = missing_keys.map {|key| key.to_s}.join(' and the ') - super("You did not provide both required access keys. Please provide the #{key_list}.") - end - end - - # Raised if a request is attempted before any connections have been established. - class NoConnectionEstablished < S3Exception - end - - # Raised if an unrecognized option is passed when establishing a connection. - class InvalidConnectionOption < InvalidOption - def initialize(invalid_options) - message = "The following connection options are invalid: #{invalid_options.join(', ')}. " + - "The valid connection options are: #{Connection::Options.valid_options.join(', ')}." - super(message) - end - end - # Raised if an invalid bucket name is passed when creating a new Bucket. class InvalidBucketName < S3Exception def initialize(invalid_name) @@ -121,13 +70,6 @@ end end - class ExceptionClassClash < S3Exception #:nodoc: - def initialize(klass) - message = "The exception class you tried to create (`#{klass}') exists and is not an exception" - super(message) - end - end - #:startdoc: end end \ No newline at end of file Index: lib/aws/s3/error.rb =================================================================== --- lib/aws/s3/error.rb (revision 158) +++ lib/aws/s3/error.rb (working copy) @@ -1,69 +0,0 @@ -module AWS - module S3 - # Anything you do that makes a request to S3 could result in an error. If it does, the AWS::S3 library will raise an exception - # specific to the error. All exception that are raised as a result of a request returning an error response inherit from the - # ResponseError exception. So should you choose to rescue any such exception, you can simple rescue ResponseError. - # - # Say you go to delete a bucket, but the bucket turns out to not be empty. This results in a BucketNotEmpty error (one of the many - # errors listed at http://docs.amazonwebservices.com/AmazonS3/2006-03-01/ErrorCodeList.html): - # - # begin - # Bucket.delete('jukebox') - # rescue ResponseError => error - # # ... - # end - # - # Once you've captured the exception, you can extract the error message from S3, as well as the full error response, which includes - # things like the HTTP response code: - # - # error - # # => # - # error.message - # # => "The bucket you tried to delete is not empty" - # error.response.code - # # => 409 - # - # You could use this information to redisplay the error in a way you see fit, or just to log the error and continue on. - class Error - #:stopdoc: - attr_accessor :response - def initialize(error, response = nil) - @error = error - @response = response - @container = AWS::S3 - find_or_create_exception! - end - - def raise - Kernel.raise exception.new(message, response) - end - - private - attr_reader :error, :exception, :container - - def find_or_create_exception! - @exception = container.const_defined?(code) ? find_exception : create_exception - end - - def find_exception - exception_class = container.const_get(code) - Kernel.raise ExceptionClassClash.new(exception_class) unless exception_class.ancestors.include?(ResponseError) - exception_class - end - - def create_exception - container.const_set(code, Class.new(ResponseError)) - end - - def method_missing(method, *args, &block) - # We actually want nil if the attribute is nil. So we use has_key? rather than [] + ||. - if error.has_key?(method.to_s) - error[method.to_s] - else - super - end - end - end - end -end -#:startdoc: \ No newline at end of file Index: lib/aws/s3/response.rb =================================================================== --- lib/aws/s3/response.rb (revision 158) +++ lib/aws/s3/response.rb (working copy) @@ -2,63 +2,10 @@ module AWS module S3 class Base - class Response < String - attr_reader :response, :body, :parsed - def initialize(response) - @response = response - @body = response.body.to_s - super(body) + class Response < Base::Response + def container + AWS::S3 end - - def headers - headers = {} - response.each do |header, value| - headers[header] = value - end - headers - end - memoized :headers - - def [](header) - headers[header] - end - - def each(&block) - headers.each(&block) - end - - def code - response.code.to_i - end - - {:success => 200..299, :redirect => 300..399, - :client_error => 400..499, :server_error => 500..599}.each do |result, code_range| - class_eval(<<-EVAL, __FILE__, __LINE__) - def #{result}? - return false unless response - (#{code_range}).include? code - end - EVAL - end - - def error? - !success? && response['content-type'] == 'application/xml' && parsed.root == 'error' - end - - def error - Error.new(parsed, self) - end - memoized :error - - def parsed - # XmlSimple is picky about what kind of object it parses, so we pass in body rather than self - Parsing::XmlParser.new(body) - end - memoized :parsed - - def inspect - "#<%s:0x%s %s %s>" % [self.class, object_id, response.code, response.message] - end end end @@ -97,84 +44,6 @@ end end end - - # Requests whose response code is between 300 and 599 and contain an in their body - # are wrapped in an Error::Response. This Error::Response contains an Error object which raises an exception - # that corresponds to the error in the response body. The exception object contains the ErrorResponse, so - # in all cases where a request happens, you can rescue ResponseError and have access to the ErrorResponse and - # its Error object which contains information about the ResponseError. - # - # begin - # Bucket.create(..) - # rescue ResponseError => exception - # exception.response - # # => - # exception.response.error - # # => - # end - class Error - class Response < Base::Response - def error? - true - end - - def inspect - "#<%s:0x%s %s %s: '%s'>" % [self.class.name, object_id, response.code, error.code, error.message] - end - end - end - - # Guess response class name from current class name. If the guessed response class doesn't exist - # do the same thing to the current class's parent class, up the inheritance heirarchy until either - # a response class is found or until we get to the top of the heirarchy in which case we just use - # the the Base response class. - # - # Important: This implemantation assumes that the Base class has a corresponding Base::Response. - class FindResponseClass #:nodoc: - class << self - def for(start) - new(start).find - end - end - - def initialize(start) - @container = AWS::S3 - @current_class = start - end - - def find - self.current_class = current_class.superclass until response_class_found? - target.const_get(class_to_find) - end - - private - attr_reader :container - attr_accessor :current_class - - def target - container.const_get(current_name) - end - - def target? - container.const_defined?(current_name) - end - - def response_class_found? - target? && target.const_defined?(class_to_find) - end - - def class_to_find - :Response - end - - def current_name - truncate(current_class) - end - - def truncate(klass) - klass.name[/[^:]+$/] - end - end end end #:startdoc: \ No newline at end of file Index: lib/aws/s3/extensions.rb =================================================================== --- lib/aws/s3/extensions.rb (revision 158) +++ lib/aws/s3/extensions.rb (working copy) @@ -1,315 +0,0 @@ -#:stopdoc: - -class Hash - def to_query_string(include_question_mark = true) - query_string = '' - unless empty? - query_string << '?' if include_question_mark - query_string << inject([]) do |params, (key, value)| - params << "#{key}=#{value}" - end.join('&') - end - query_string - end - - def to_normalized_options - # Convert all option names to downcased strings, and replace underscores with hyphens - inject({}) do |normalized_options, (name, value)| - normalized_options[name.to_header] = value.to_s - normalized_options - end - end - - def to_normalized_options! - replace(to_normalized_options) - end -end - -class String - def previous! - self[-1] -= 1 - self - end - - def previous - dup.previous! - end - - def to_header - downcase.tr('_', '-') - end - - # ActiveSupport adds an underscore method to String so let's just use that one if - # we find that the method is already defined - def underscore - gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). - gsub(/([a-z\d])([A-Z])/,'\1_\2'). - downcase - end unless public_method_defined? :underscore - - def utf8? - scan(/[^\x00-\xa0]/u) { |s| s.unpack('U') } - true - rescue ArgumentError - false - end - - # All paths in in S3 have to be valid unicode so this takes care of - # cleaning up any strings that aren't valid utf-8 according to String#utf8? - def remove_extended! - gsub!(/[\x80-\xFF]/) { "%02X" % $&[0] } - end - - def remove_extended - dup.remove_extended! - end -end - -class CoercibleString < String - class << self - def coerce(string) - new(string).coerce - end - end - - def coerce - case self - when 'true': true - when 'false': false - when /^\d+$/: Integer(self) - when datetime_format: Time.parse(self) - else - self - end - end - - private - # Lame hack since Date._parse is so accepting. S3 dates are of the form: '2006-10-29T23:14:47.000Z' - # so unless the string looks like that, don't even try, otherwise it might convert an object's - # key from something like '03 1-2-3-Apple-Tree.mp3' to Sat Feb 03 00:00:00 CST 2001. - def datetime_format - /^\d{4}-\d{2}-\d{2}\w\d{2}:\d{2}:\d{2}/ - end -end - -class Symbol - def to_header - to_s.to_header - end -end - -module Kernel - def __method__(depth = 0) - caller[depth][/`([^']+)'/, 1] - end if RUBY_VERSION < '1.9' - - def memoize(reload = false, storage = nil) - storage = "@#{storage || __method__(1)}" - if reload - instance_variable_set(storage, nil) - else - if cache = instance_variable_get(storage) - return cache - end - end - instance_variable_set(storage, yield) - end - - def require_library_or_gem(library) - require library - rescue LoadError => library_not_installed - begin - require 'rubygems' - require library - rescue LoadError - raise library_not_installed - end - end -end - -class Module - def memoized(method_name) - original_method = "unmemoized_#{method_name}_#{Time.now.to_i}" - alias_method original_method, method_name - module_eval(<<-EVAL, __FILE__, __LINE__) - def #{method_name}(reload = false, *args, &block) - memoize(reload) do - send(:#{original_method}, *args, &block) - end - end - EVAL - end - - def constant(name, value) - unless const_defined?(name) - const_set(name, value) - module_eval(<<-EVAL, __FILE__, __LINE__) - def self.#{name.to_s.downcase} - #{name.to_s} - end - EVAL - end - end - - # Transforms MarcelBucket into - # - # class MarcelBucket < AWS::S3::Bucket - # set_current_bucket_to 'marcel' - # end - def const_missing_from_s3_library(sym) - if sym.to_s =~ /^(\w+)(Bucket|S3Object)$/ - const = const_set(sym, Class.new(AWS::S3.const_get($2))) - const.current_bucket = $1.underscore - const - else - const_missing_not_from_s3_library(sym) - end - end - alias_method :const_missing_not_from_s3_library, :const_missing - alias_method :const_missing, :const_missing_from_s3_library -end - - -class Class # :nodoc: - def cattr_reader(*syms) - syms.flatten.each do |sym| - class_eval(<<-EOS, __FILE__, __LINE__) - unless defined? @@#{sym} - @@#{sym} = nil - end - - def self.#{sym} - @@#{sym} - end - - def #{sym} - @@#{sym} - end - EOS - end - end - - def cattr_writer(*syms) - syms.flatten.each do |sym| - class_eval(<<-EOS, __FILE__, __LINE__) - unless defined? @@#{sym} - @@#{sym} = nil - end - - def self.#{sym}=(obj) - @@#{sym} = obj - end - - def #{sym}=(obj) - @@#{sym} = obj - end - EOS - end - end - - def cattr_accessor(*syms) - cattr_reader(*syms) - cattr_writer(*syms) - end -end if Class.instance_methods(false).grep(/^cattr_(?:reader|writer|accessor)$/).empty? - -module SelectiveAttributeProxy - def self.included(klass) - klass.extend(ClassMethods) - klass.class_eval(<<-EVAL, __FILE__, __LINE__) - cattr_accessor :attribute_proxy - cattr_accessor :attribute_proxy_options - - # Default name for attribute storage - self.attribute_proxy = :attributes - self.attribute_proxy_options = {:exclusively => true} - - private - # By default proxy all attributes - def proxiable_attribute?(name) - return true unless self.class.attribute_proxy_options[:exclusively] - send(self.class.attribute_proxy).has_key?(name) - end - - def method_missing(method, *args, &block) - # Autovivify attribute storage - if method == self.class.attribute_proxy - ivar = "@\#{method}" - instance_variable_set(ivar, {}) unless instance_variable_get(ivar).is_a?(Hash) - instance_variable_get(ivar) - # Delegate to attribute storage - elsif method.to_s =~ /^(\\w+)(=?)$/ && proxiable_attribute?($1) - attributes_hash_name = self.class.attribute_proxy - $2.empty? ? send(attributes_hash_name)[$1] : send(attributes_hash_name)[$1] = args.first - else - super - end - end - EVAL - end - - module ClassMethods - def proxy_to(attribute_name, options = {}) - if attribute_name.is_a?(Hash) - options = attribute_name - else - self.attribute_proxy = attribute_name - end - self.attribute_proxy_options = options - end - end -end - -# When streaming data up, Net::HTTPGenericRequest hard codes a chunk size of 1k. For large files this -# is an unfortunately low chunk size, so here we make it use a much larger default size and move it into a method -# so that the implementation of send_request_with_body_stream doesn't need to be changed to change the chunk size (at least not anymore -# than I've already had to...). -module Net - class HTTPGenericRequest - def send_request_with_body_stream(sock, ver, path, f) - raise ArgumentError, "Content-Length not given and Transfer-Encoding is not `chunked'" unless content_length() or chunked? - unless content_type() - warn 'net/http: warning: Content-Type did not set; using application/x-www-form-urlencoded' if $VERBOSE - set_content_type 'application/x-www-form-urlencoded' - end - write_header sock, ver, path - if chunked? - while s = f.read(chunk_size) - sock.write(sprintf("%x\r\n", s.length) << s << "\r\n") - end - sock.write "0\r\n\r\n" - else - while s = f.read(chunk_size) - sock.write s - end - end - end - - def chunk_size - 1048576 # 1 megabyte - end - end - - # Net::HTTP before 1.8.4 doesn't have the use_ssl? method or the Delete request type - class HTTP - def use_ssl? - @use_ssl - end unless public_method_defined? :use_ssl? - - class Delete < HTTPRequest - METHOD = 'DELETE' - REQUEST_HAS_BODY = false - RESPONSE_HAS_BODY = true - end unless const_defined? :Delete - end -end - -class XmlGenerator < String #:nodoc: - attr_reader :xml - def initialize - @xml = Builder::XmlMarkup.new(:indent => 2, :target => self) - super() - build - end -end -#:startdoc: Index: lib/aws/s3/connection.rb =================================================================== --- lib/aws/s3/connection.rb (revision 158) +++ lib/aws/s3/connection.rb (working copy) @@ -1,259 +0,0 @@ -module AWS - module S3 - class Connection #:nodoc: - class << self - def connect(options = {}) - new(options) - end - - def prepare_path(path) - path = path.remove_extended unless path.utf8? - URI.escape(path) - end - end - - attr_reader :access_key_id, :secret_access_key, :http, :options - - # Creates a new connection. Connections make the actual requests to S3, though these requests are usually - # called from subclasses of Base. - # - # For details on establishing connections, check the Connection::Management::ClassMethods. - def initialize(options = {}) - @options = Options.new(options) - connect - end - - def request(verb, path, headers = {}, body = nil, attempts = 0, &block) - body.rewind if body.respond_to?(:rewind) unless attempts.zero? - - requester = Proc.new do - path = self.class.prepare_path(path) - request = request_method(verb).new(path, headers) - ensure_content_type!(request) - add_user_agent!(request) - authenticate!(request) - if body - if body.respond_to?(:read) - request.body_stream = body - request.content_length = body.respond_to?(:lstat) ? body.lstat.size : body.size - else - request.body = body - end - end - http.request(request, &block) - end - - if persistent? - http.start unless http.started? - requester.call - else - http.start(&requester) - end - rescue Errno::EPIPE, Timeout::Error, Errno::EPIPE, Errno::EINVAL - @http = create_connection - attempts == 3 ? raise : (attempts += 1; retry) - end - - def url_for(path, options = {}) - path = self.class.prepare_path(path) - request = request_method(:get).new(path, {}) - query_string = query_string_authentication(request, options) - "#{protocol(options)}#{http.address}#{path}?#{query_string}" - end - - def subdomain - http.address[/^([^.]+).#{DEFAULT_HOST}$/, 1] - end - - def persistent? - options[:persistent] - end - - def protocol(options = {}) - (options[:use_ssl] || http.use_ssl?) ? 'https://' : 'http://' - end - - private - def extract_keys! - missing_keys = [] - extract_key = Proc.new {|key| options[key] || (missing_keys.push(key); nil)} - @access_key_id = extract_key[:access_key_id] - @secret_access_key = extract_key[:secret_access_key] - raise MissingAccessKey.new(missing_keys) unless missing_keys.empty? - end - - def create_connection - http = Net::HTTP.new(options[:server], options[:port]) - http.use_ssl = !options[:use_ssl].nil? || options[:port] == 443 - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - http - end - - def connect - extract_keys! - @http = create_connection - 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) - end - - def add_user_agent!(request) - request['User-Agent'] ||= "AWS::S3/#{Version}" - end - - def query_string_authentication(request, options = {}) - Authentication::QueryString.new(request, access_key_id, secret_access_key, options) - end - - def request_method(verb) - Net::HTTP.const_get(verb.to_s.capitalize) - end - - def method_missing(method, *args, &block) - options[method] || super - end - - module Management #:nodoc: - def self.included(base) - base.cattr_accessor :connections - base.connections = {} - base.extend ClassMethods - end - - # Manage the creation and destruction of connections for AWS::S3::Base and its subclasses. Connections are - # created with establish_connection!. - module ClassMethods - # Creates a new connection with which to make requests to the S3 servers for the calling class. - # - # AWS::S3::Base.establish_connection!(:access_key_id => '...', :secret_access_key => '...') - # - # You can set connections for every subclass of AWS::S3::Base. Once the initial connection is made on - # Base, all subsequent connections will inherit whatever values you don't specify explictly. This allows you to - # customize details of the connection, such as what server the requests are made to, by just specifying one - # option. - # - # AWS::S3::Bucket.established_connection!(:use_ssl => true) - # - # The Bucket connection would inherit the :access_key_id and the :secret_access_key from - # Base's connection. Unlike the Base connection, all Bucket requests would be made over SSL. - # - # == Required arguments - # - # * :access_key_id - The access key id for your S3 account. Provided by Amazon. - # * :secret_access_key - The secret access key for your S3 account. Provided by Amazon. - # - # If any of these required arguments is missing, a MissingAccessKey exception will be raised. - # - # == Optional arguments - # - # * :server - The server to make requests to. You can use this to specify your bucket in the subdomain, - # or your own domain's cname if you are using virtual hosted buckets. Defaults to s3.amazonaws.com. - # * :port - The port to the requests should be made on. Defaults to 80 or 443 if the :use_ssl - # argument is set. - # * :use_ssl - Whether requests should be made over SSL. If set to true, the :port argument - # will be implicitly set to 443, unless specified otherwise. Defaults to false. - # * :persistent - Whether to use a persistent connection to the server. Having this on provides around a two fold - # performance increase but for long running processes some firewalls may find the long lived connection suspicious and close the connection. - # If you run into connection errors, try setting :persistent to false. Defaults to true. - def establish_connection!(options = {}) - # After you've already established the default connection, just specify - # the difference for subsequent connections - options = default_connection.options.merge(options) if connected? - connections[connection_name] = Connection.connect(options) - end - - # Returns the connection for the current class, or Base's default connection if the current class does not - # have its own connection. - # - # If not connection has been established yet, NoConnectionEstablished will be raised. - def connection - if connected? - connections[connection_name] || default_connection - else - raise NoConnectionEstablished - end - end - - # Returns true if a connection has been made yet. - def connected? - !connections.empty? - end - - # Removes the connection for the current class. If there is no connection for the current class, the default - # connection will be removed. - def disconnect(name = connection_name) - name = default_connection unless connections.has_key?(name) - connection = connections[name] - connection.http.finish if connection.persistent? - connections.delete(name) - end - - # Clears *all* connections, from all classes, with prejudice. - def disconnect! - connections.each_key {|connection| disconnect(connection)} - end - - private - def connection_name - name - end - - def default_connection_name - 'AWS::S3::Base' - end - - def default_connection - connections[default_connection_name] - end - end - end - - class Options < Hash #:nodoc: - class << self - def valid_options - [:access_key_id, :secret_access_key, :server, :port, :use_ssl, :persistent] - end - end - - attr_reader :options - def initialize(options = {}) - super() - @options = options - validate! - extract_persistent! - extract_server! - extract_port! - extract_remainder! - end - - private - def extract_persistent! - self[:persistent] = options.has_key?(:persitent) ? options[:persitent] : true - end - - def extract_server! - self[:server] = options.delete(:server) || DEFAULT_HOST - end - - def extract_port! - self[:port] = options.delete(:port) || (options[:use_ssl] ? 443 : 80) - end - - def extract_remainder! - update(options) - end - - def validate! - invalid_options = options.keys.select {|key| !self.class.valid_options.include?(key)} - raise InvalidConnectionOption.new(invalid_options) unless invalid_options.empty? - end - end - end - end -end \ No newline at end of file Index: lib/aws/s3/parsing.rb =================================================================== --- lib/aws/s3/parsing.rb (revision 158) +++ lib/aws/s3/parsing.rb (working copy) @@ -1,99 +0,0 @@ -#:stopdoc: -module AWS - module S3 - module Parsing - class << self - def parser=(parsing_library) - XmlParser.parsing_library = parsing_library - end - - def parser - XmlParser.parsing_library - end - end - - module Typecasting - def typecast(object) - case object - when Hash - typecast_hash(object) - when Array - object.map {|element| typecast(element)} - when String - CoercibleString.coerce(object) - else - object - end - end - - def typecast_hash(hash) - if content = hash['__content__'] - typecast(content) - else - keys = hash.keys.map {|key| key.underscore} - values = hash.values.map {|value| typecast(value)} - keys.inject({}) do |new_hash, key| - new_hash[key] = values.slice!(0) - new_hash - end - end - end - end - - class XmlParser < Hash - include Typecasting - - class << self - attr_accessor :parsing_library - end - - attr_reader :body, :xml_in, :root - - def initialize(body) - @body = body - unless body.strip.empty? - parse - set_root - typecast_xml_in - end - end - - private - - def parse - @xml_in = self.class.parsing_library.xml_in(body, parsing_options) - end - - def parsing_options - { - # Includes the enclosing tag as the top level key - 'keeproot' => true, - # Makes tag value available via the '__content__' key - 'contentkey' => '__content__', - # Always parse tags into a hash, even when there are no attributes - # (unless there is also no value, in which case it is nil) - 'forcecontent' => true, - # If a tag is empty, makes its content nil - 'suppressempty' => nil, - # Force nested elements to be put into an array, even if there is only one of them - 'forcearray' => ['Contents', 'Bucket', 'Grant'] - } - end - - def set_root - @root = @xml_in.keys.first.underscore - end - - def typecast_xml_in - typecast_xml = {} - @xml_in.dup.each do |key, value| # Some typecasting is destructive so we dup - typecast_xml[key.underscore] = typecast(value) - end - # An empty body will try to update with a string so only update if the result is a hash - update(typecast_xml[root]) if typecast_xml[root].is_a?(Hash) - end - end - end - end -end -#:startdoc: \ No newline at end of file Index: lib/aws/s3/base.rb =================================================================== --- lib/aws/s3/base.rb (revision 158) +++ lib/aws/s3/base.rb (working copy) @@ -55,37 +55,8 @@ # details can be found in the docs for Connection::Management::ClassMethods. # # Extensive examples can be found in the README[link:files/README.html]. - class Base + class Base < Base class << self - # Wraps the current connection's request method and picks the appropriate response class to wrap the response in. - # If the response is an error, it will raise that error as an exception. All such exceptions can be caught by rescuing - # their superclass, the ResponseError exception class. - # - # 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) - Service.response = nil - process_options!(options, verb) - response = response_class.new(connection.request(verb, path, options, body, attempts, &block)) - Service.response = response - - Error::Response.new(response.response).error.raise if response.error? - response - # Once in a while, a request to S3 returns an internal error. A glitch in the matrix I presume. Since these - # errors are few and far between the request method will rescue InternalErrors the first three times they encouter them - # and will retry the request again. Most of the time the second attempt will work. - rescue *retry_exceptions - attempts == 3 ? raise : (attempts += 1; retry) - end - - [: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) - end - EVAL - end - # Called when a method which requires a bucket name is called without that bucket name specified. It will try to # infer the current bucket by looking for it as the subdomain of the current connection's address. If no subdomain # is found, CurrentBucketNotSpecified will be raised. @@ -136,48 +107,26 @@ alias_method :current_bucket=, :set_current_bucket_to private - + + def do_request(verb, path, options = {}, body = nil, attempts = 0, &block) + Service.response = nil + response = response_class.new(connection.request(verb, path, options, body, attempts, &block)) + Service.response = response + response + end + def response_class - FindResponseClass.for(self) + FindResponseClass.for(self, AWS::S3) end - + def process_options!(options, verb) options.replace(RequestOptions.process(options, verb)) end - - # Using the conventions layed out in the response_class works for more than 80% of the time. - # There are a few edge cases though where we want a given class to wrap its responses in different - # response classes depending on which method is being called. - def respond_with(klass) - eval(<<-EVAL, binding, __FILE__, __LINE__) - def new_response_class - #{klass} - end - class << self - alias_method :old_response_class, :response_class - alias_method :response_class, :new_response_class - end - EVAL - - yield - ensure - # Restore the original version - eval(<<-EVAL, binding, __FILE__, __LINE__) - class << self - alias_method :response_class, :old_response_class - end - EVAL - end - def bucket_name(name) name || current_bucket end - def retry_exceptions - [InternalError, RequestTimeout] - end - class RequestOptions < Hash #:nodoc: attr_reader :options, :verb @@ -204,29 +153,6 @@ end end end - - def initialize(attributes = {}) #:nodoc: - @attributes = attributes - end - - private - attr_reader :attributes - - def connection - self.class.connection - end - - def http - connection.http - end - - def request(*args, &block) - self.class.request(*args, &block) - end - - def method_missing(method, *args, &block) - attributes[method.to_s] || attributes[method] || super - end end end end Index: lib/aws/parsing.rb =================================================================== --- lib/aws/parsing.rb (revision 158) +++ lib/aws/parsing.rb (working copy) @@ -1,98 +1,96 @@ #:stopdoc: module AWS - module S3 - module Parsing - class << self - def parser=(parsing_library) - XmlParser.parsing_library = parsing_library + module Parsing + class << self + def parser=(parsing_library) + XmlParser.parsing_library = parsing_library + end + + def parser + XmlParser.parsing_library + end + end + + module Typecasting + def typecast(object) + case object + when Hash + typecast_hash(object) + when Array + object.map {|element| typecast(element)} + when String + CoercibleString.coerce(object) + else + object end - - def parser - XmlParser.parsing_library - end end - module Typecasting - def typecast(object) - case object - when Hash - typecast_hash(object) - when Array - object.map {|element| typecast(element)} - when String - CoercibleString.coerce(object) - else - object + def typecast_hash(hash) + if content = hash['__content__'] + typecast(content) + else + keys = hash.keys.map {|key| key.underscore} + values = hash.values.map {|value| typecast(value)} + keys.inject({}) do |new_hash, key| + new_hash[key] = values.slice!(0) + new_hash end end - - def typecast_hash(hash) - if content = hash['__content__'] - typecast(content) - else - keys = hash.keys.map {|key| key.underscore} - values = hash.values.map {|value| typecast(value)} - keys.inject({}) do |new_hash, key| - new_hash[key] = values.slice!(0) - new_hash - end - end - end end + end + + class XmlParser < Hash + include Typecasting - class XmlParser < Hash - include Typecasting + class << self + attr_accessor :parsing_library + end + + attr_reader :body, :xml_in, :root + + def initialize(body) + @body = body + unless body.strip.empty? + parse + set_root + typecast_xml_in + end + end + + private + + def parse + @xml_in = self.class.parsing_library.xml_in(body, parsing_options) + end - class << self - attr_accessor :parsing_library + def parsing_options + { + # Includes the enclosing tag as the top level key + 'keeproot' => true, + # Makes tag value available via the '__content__' key + 'contentkey' => '__content__', + # Always parse tags into a hash, even when there are no attributes + # (unless there is also no value, in which case it is nil) + 'forcecontent' => true, + # If a tag is empty, makes its content nil + 'suppressempty' => nil, + # Force nested elements to be put into an array, even if there is only one of them + 'forcearray' => ['Contents', 'Bucket', 'Grant'] + } end - attr_reader :body, :xml_in, :root + def set_root + @root = @xml_in.keys.first.underscore + end - def initialize(body) - @body = body - unless body.strip.empty? - parse - set_root - typecast_xml_in + def typecast_xml_in + typecast_xml = {} + @xml_in.dup.each do |key, value| # Some typecasting is destructive so we dup + typecast_xml[key.underscore] = typecast(value) end + # An empty body will try to update with a string so only update if the result is a hash + update(typecast_xml[root]) if typecast_xml[root].is_a?(Hash) end - - private - - def parse - @xml_in = self.class.parsing_library.xml_in(body, parsing_options) - end - - def parsing_options - { - # Includes the enclosing tag as the top level key - 'keeproot' => true, - # Makes tag value available via the '__content__' key - 'contentkey' => '__content__', - # Always parse tags into a hash, even when there are no attributes - # (unless there is also no value, in which case it is nil) - 'forcecontent' => true, - # If a tag is empty, makes its content nil - 'suppressempty' => nil, - # Force nested elements to be put into an array, even if there is only one of them - 'forcearray' => ['Contents', 'Bucket', 'Grant'] - } - end - - def set_root - @root = @xml_in.keys.first.underscore - end - - def typecast_xml_in - typecast_xml = {} - @xml_in.dup.each do |key, value| # Some typecasting is destructive so we dup - typecast_xml[key.underscore] = typecast(value) - end - # An empty body will try to update with a string so only update if the result is a hash - update(typecast_xml[root]) if typecast_xml[root].is_a?(Hash) - end - end end end end Index: lib/aws/s3.rb =================================================================== --- lib/aws/s3.rb (revision 158) +++ lib/aws/s3.rb (working copy) @@ -1,21 +1,7 @@ -require 'base64' -require 'cgi' -require 'uri' -require 'openssl' -require 'digest/sha1' -require 'net/https' -require 'time' -require 'date' -require 'open-uri' - $:.unshift(File.dirname(__FILE__)) -require 's3/extensions' -require_library_or_gem 'builder' unless defined? Builder -require_library_or_gem 'mime/types' unless defined? MIME::Types require 's3/base' require 's3/version' -require 's3/parsing' require 's3/acl' require 's3/logging' require 's3/bittorrent' @@ -23,14 +9,12 @@ require 's3/owner' require 's3/bucket' require 's3/object' -require 's3/error' require 's3/exceptions' -require 's3/connection' require 's3/authentication' require 's3/response' AWS::S3::Base.class_eval do - include AWS::S3::Connection::Management + include AWS::Connection::Management end AWS::S3::Bucket.class_eval do @@ -42,20 +26,3 @@ include AWS::S3::ACL::S3Object include AWS::S3::BitTorrent end - -require_library_or_gem 'xmlsimple' unless defined? XmlSimple -# If libxml is installed, we use the FasterXmlSimple library, that provides most of the functionality of XmlSimple -# except it uses the xml/libxml library for xml parsing (rather than REXML). If libxml isn't installed, we just fall back on -# XmlSimple. -AWS::S3::Parsing.parser = - begin - require_library_or_gem 'xml/libxml' - # Older version of libxml aren't stable (bus error when requesting attributes that don't exist) so we - # have to use a version greater than '0.3.8.2'. - raise LoadError unless XML::Parser::VERSION > '0.3.8.2' - $:.push(File.join(File.dirname(__FILE__), '..', '..', 'support', 'faster-xml-simple', 'lib')) - require_library_or_gem 'faster_xml_simple' - FasterXmlSimple - rescue LoadError - XmlSimple - end Index: lib/aws/ec2/response.rb =================================================================== --- lib/aws/ec2/response.rb (revision 0) +++ lib/aws/ec2/response.rb (revision 0) @@ -0,0 +1,13 @@ +#:stopdoc: +module AWS + module EC2 + class Base + class Response < Base::Response + def container + AWS::EC2 + end + end + end + end +end +#:startdoc: \ No newline at end of file Index: lib/aws/ec2/authentication.rb =================================================================== --- lib/aws/ec2/authentication.rb (revision 0) +++ lib/aws/ec2/authentication.rb (revision 0) @@ -0,0 +1,157 @@ +module AWS + module EC2 + module Authentication + include AWS::Authentication + + constant :API_VERSION, '2006-10-01' + + class Signature < AWS::Authentication::BaseSignature + def canonical_string + options = {} + options[:expires] = expires if expires? + CanonicalString.new(request, access_key_id, options) + end + memoized :canonical_string + end + + # Provides query string authentication by computing the three authorization parameters: AWSAccessKeyId, Expires and Signature. + # More details about the various authentication schemes can be found in the docs for its containing module, Authentication. + class QueryString < Signature #:nodoc: + constant :DEFAULT_EXPIRY, 300 # 5 minutes + + def initialize(*args) + super + @options[:url_encode] = true + ensure_date_is_valid + self << build + end + + private + + # Will return one of three values, in the following order of precedence: + # + # 1) Seconds since the epoch explicitly passed in the +:expires+ option + # 2) The current time in seconds since the epoch plus the number of seconds passed in + # the +:expires_in+ option + # 3) The current time in seconds since the epoch plus the default number of seconds (60 seconds) + def expires + return @options[:expires] if @options[:expires] + date.to_i + (@options[:expires_in] || DEFAULT_EXPIRY) + end + + def expires? + true + end + + def ensure_date_is_valid + request['date'] ||= Time.now.httpdate + end + + # Keep in alphabetical order + def build + require 'uri' + uri = URI.parse(@request.path) + params = Hash[*uri.query.split("&").map { |e| e.split("=") }.map { |e| [e[0], e[1]] }.flatten] + params.merge!({ + "SignatureVersion" => "1", + "AWSAccessKeyId" => access_key_id, + "Version" => API_VERSION, + "Timestamp" => Time.parse(request['date']).gmtime.iso8601, +# "Expires" => Time.at(expires).gmtime.iso8601, + "Signature" => encoded_canonical + }).keys.sort_by { |k| k.downcase }.sort.inject([]) do |s, k| + if params[k] + s << "#{CGI::escape(k)}=#{CGI::escape(params[k])}" + else + s << "#{CGI::escape(k)}" + end + end.join("&") + end + end + + # The CanonicalString is used to generate an encrypted signature, signed with your secrect access key. It is composed of + # data related to the given request for which it provides authentication. This data includes the request method, request headers, + # and the request path. Both Header and QueryString use it to generate their signature. + class CanonicalString < String #:nodoc: + class << self + def default_headers + %w(content-type content-md5) + end + + def interesting_headers + ['content-md5', 'content-type', 'date', amazon_header_prefix] + end + + def amazon_header_prefix + /^#{AMAZON_HEADER_PREFIX}/io + end + end + + attr_reader :request, :headers, :access_key_id + + def initialize(request, access_key_id, options = {}) + super() + @request = request + @headers = {} + @options = options + @access_key_id = access_key_id + build + end + + private + def build + ensure_date_is_valid + uri = URI.parse(@request.path) + params = Hash[*uri.query.split("&").map { |e| e.split("=") }.map { |e| [e[0], e[1]] }.flatten] + self << params.merge!({ + "SignatureVersion" => "1", + "AWSAccessKeyId" => access_key_id, + "Version" => API_VERSION, + "Timestamp" => Time.parse(request['date']).gmtime.iso8601, +# "Expires" => Time.at(@options[:expires]).gmtime.iso8601, + }).keys.sort_by { |k| k.downcase }.inject("") { |s, k| s += "#{k}#{params[k]}" } + end + + def initialize_headers + identify_interesting_headers + set_default_headers + end + + def set_expiry! + self.headers['date'] = @options[:expires] if @options[:expires] + end + + def ensure_date_is_valid + request['date'] ||= Time.now.httpdate + end + + def identify_interesting_headers + request.each do |key, value| + key = key.downcase # Can't modify frozen string so no bang + if self.class.interesting_headers.any? {|header| header === key} + self.headers[key] = value.to_s.strip + end + end + end + + def set_default_headers + self.class.default_headers.each do |header| + self.headers[header] ||= '' + end + end + + def path + [only_path, extract_significant_parameter].compact.join('?') + end + + def extract_significant_parameter + request.path[/[&?](acl|torrent|logging)(?:&|=|$)/, 1] + end + + def only_path + request.path[/^[^?]*/] + end + end + end + end +end \ No newline at end of file Index: lib/aws/ec2/exceptions.rb =================================================================== --- lib/aws/ec2/exceptions.rb (revision 0) +++ lib/aws/ec2/exceptions.rb (revision 0) @@ -0,0 +1,7 @@ +module AWS + module EC2 + # Abstract super class of all AWS::EC2 exceptions + class EC2Exception < AWSException + end + end +end \ No newline at end of file Index: lib/aws/ec2/version.rb =================================================================== --- lib/aws/ec2/version.rb (revision 0) +++ lib/aws/ec2/version.rb (revision 0) @@ -0,0 +1,12 @@ +module AWS + module EC2 + module VERSION #:nodoc: + MAJOR = '0' + MINOR = '1' + TINY = '0' + BETA = Time.now.to_i.to_s + end + + Version = [VERSION::MAJOR, VERSION::MINOR, VERSION::TINY, VERSION::BETA].compact * '.' + end +end \ No newline at end of file Index: lib/aws/ec2/base.rb =================================================================== --- lib/aws/ec2/base.rb (revision 0) +++ lib/aws/ec2/base.rb (revision 0) @@ -0,0 +1,76 @@ +module AWS #:nodoc: + # AWS::S3 is a Ruby library for Amazon's Simple Storage Service's REST API (http://aws.amazon.com/s3). + # Full documentation of the currently supported API can be found at http://docs.amazonwebservices.com/AmazonS3/2006-03-01. + # + # == Getting started + # + # To get started you need to require 'aws/s3': + # + # % irb -rubygems + # irb(main):001:0> require 'aws/s3' + # # => true + # + # The AWS::S3 library ships with an interactive shell called s3sh. From within it, you have access to all the operations the library exposes from the command line. + # + # % s3sh + # >> Version + # + # Before you can do anything, you must establish a connection using Base.establish_connection!. A basic connection would look something like this: + # + # AWS::S3::Base.establish_connection!( + # :access_key_id => 'abc', + # :secret_access_key => '123' + # ) + # + # The minimum connection options that you must specify are your access key id and your secret access key. + # + # (If you don't already have your access keys, all you need to sign up for the S3 service is an account at Amazon. You can sign up for S3 and get access keys by visiting http://aws.amazon.com/s3.) + # + # For convenience, if you set two special environment variables with the value of your access keys, the console will automatically create a default connection for you. For example: + # + # % cat .amazon_keys + # export AMAZON_ACCESS_KEY_ID='abcdefghijklmnop' + # export AMAZON_SECRET_ACCESS_KEY='1234567891012345' + # + # Then load it in your shell's rc file. + # + # % cat .zshrc + # if [[ -f "$HOME/.amazon_keys" ]]; then + # source "$HOME/.amazon_keys"; + # fi + # + # See more connection details at AWS::S3::Connection::Management::ClassMethods. + module EC2 + constant :DEFAULT_HOST, 'ec2.amazonaws.com' + + # AWS::S3::Base is the abstract super class of all classes who make requests against S3, such as the built in + # Service, Bucket and S3Object classes. It provides methods for making requests, inferring or setting response classes, + # processing request options, and accessing attributes from S3's response data. + # + # Establishing a connection with the Base class is the entry point to using the library: + # + # AWS::S3::Base.establish_connection!(:access_key_id => '...', :secret_access_key => '...') + # + # The :access_key_id and :secret_access_key are the two required connection options. More + # details can be found in the docs for Connection::Management::ClassMethods. + # + # Extensive examples can be found in the README[link:files/README.html]. + class Base < Base + class << self + private + + def do_request(verb, path, options = {}, body = nil, attempts = 0, &block) + response = response_class.new(connection.request(verb, path, options, body, attempts, &block)) + response + end + + def response_class + FindResponseClass.for(self, AWS::EC2) + end + + def process_options!(options, verb) + end + end + end + end +end Index: lib/aws/error.rb =================================================================== --- lib/aws/error.rb (revision 158) +++ lib/aws/error.rb (working copy) @@ -1,69 +1,67 @@ module AWS - module S3 - # Anything you do that makes a request to S3 could result in an error. If it does, the AWS::S3 library will raise an exception - # specific to the error. All exception that are raised as a result of a request returning an error response inherit from the - # ResponseError exception. So should you choose to rescue any such exception, you can simple rescue ResponseError. - # - # Say you go to delete a bucket, but the bucket turns out to not be empty. This results in a BucketNotEmpty error (one of the many - # errors listed at http://docs.amazonwebservices.com/AmazonS3/2006-03-01/ErrorCodeList.html): - # - # begin - # Bucket.delete('jukebox') - # rescue ResponseError => error - # # ... - # end - # - # Once you've captured the exception, you can extract the error message from S3, as well as the full error response, which includes - # things like the HTTP response code: - # - # error - # # => # - # error.message - # # => "The bucket you tried to delete is not empty" - # error.response.code - # # => 409 - # - # You could use this information to redisplay the error in a way you see fit, or just to log the error and continue on. - class Error - #:stopdoc: - attr_accessor :response - def initialize(error, response = nil) - @error = error - @response = response - @container = AWS::S3 - find_or_create_exception! + # Anything you do that makes a request to S3 could result in an error. If it does, the AWS::S3 library will raise an exception + # specific to the error. All exception that are raised as a result of a request returning an error response inherit from the + # ResponseError exception. So should you choose to rescue any such exception, you can simple rescue ResponseError. + # + # Say you go to delete a bucket, but the bucket turns out to not be empty. This results in a BucketNotEmpty error (one of the many + # errors listed at http://docs.amazonwebservices.com/AmazonS3/2006-03-01/ErrorCodeList.html): + # + # begin + # Bucket.delete('jukebox') + # rescue ResponseError => error + # # ... + # end + # + # Once you've captured the exception, you can extract the error message from S3, as well as the full error response, which includes + # things like the HTTP response code: + # + # error + # # => # + # error.message + # # => "The bucket you tried to delete is not empty" + # error.response.code + # # => 409 + # + # You could use this information to redisplay the error in a way you see fit, or just to log the error and continue on. + class Error + #:stopdoc: + attr_accessor :response + def initialize(error, container, response = nil) + @error = error + @response = response + @container = container + find_or_create_exception! + end + + def raise + Kernel.raise exception.new(message, response) + end + + private + attr_reader :error, :exception, :container + + def find_or_create_exception! + @exception = container.const_defined?(code) ? find_exception : create_exception end - def raise - Kernel.raise exception.new(message, response) + def find_exception + exception_class = container.const_get(code) + Kernel.raise ExceptionClassClash.new(exception_class) unless exception_class.ancestors.include?(ResponseError) + exception_class end - private - attr_reader :error, :exception, :container - - def find_or_create_exception! - @exception = container.const_defined?(code) ? find_exception : create_exception + def create_exception + container.const_set(code, Class.new(ResponseError)) + end + + def method_missing(method, *args, &block) + # We actually want nil if the attribute is nil. So we use has_key? rather than [] + ||. + if error.has_key?(method.to_s) + error[method.to_s] + else + super end - - def find_exception - exception_class = container.const_get(code) - Kernel.raise ExceptionClassClash.new(exception_class) unless exception_class.ancestors.include?(ResponseError) - exception_class - end - - def create_exception - container.const_set(code, Class.new(ResponseError)) - end - - def method_missing(method, *args, &block) - # We actually want nil if the attribute is nil. So we use has_key? rather than [] + ||. - if error.has_key?(method.to_s) - error[method.to_s] - else - super - end - end - end + end end end #:startdoc: \ No newline at end of file Index: lib/aws/ec2.rb =================================================================== --- lib/aws/ec2.rb (revision 0) +++ lib/aws/ec2.rb (revision 0) @@ -0,0 +1,9 @@ +require 'ec2/base' +require 'ec2/version' +require 'ec2/exceptions' +require 'ec2/authentication' +require 'ec2/response' + +AWS::EC2::Base.class_eval do + include AWS::Connection::Management +end Index: lib/aws/base.rb =================================================================== --- lib/aws/base.rb (revision 0) +++ lib/aws/base.rb (revision 0) @@ -0,0 +1,147 @@ +module AWS #:nodoc: + # AWS::S3 is a Ruby library for Amazon's Simple Storage Service's REST API (http://aws.amazon.com/s3). + # Full documentation of the currently supported API can be found at http://docs.amazonwebservices.com/AmazonS3/2006-03-01. + # + # == Getting started + # + # To get started you need to require 'aws/s3': + # + # % irb -rubygems + # irb(main):001:0> require 'aws/s3' + # # => true + # + # The AWS::S3 library ships with an interactive shell called s3sh. From within it, you have access to all the operations the library exposes from the command line. + # + # % s3sh + # >> Version + # + # Before you can do anything, you must establish a connection using Base.establish_connection!. A basic connection would look something like this: + # + # AWS::S3::Base.establish_connection!( + # :access_key_id => 'abc', + # :secret_access_key => '123' + # ) + # + # The minimum connection options that you must specify are your access key id and your secret access key. + # + # (If you don't already have your access keys, all you need to sign up for the S3 service is an account at Amazon. You can sign up for S3 and get access keys by visiting http://aws.amazon.com/s3.) + # + # For convenience, if you set two special environment variables with the value of your access keys, the console will automatically create a default connection for you. For example: + # + # % cat .amazon_keys + # export AMAZON_ACCESS_KEY_ID='abcdefghijklmnop' + # export AMAZON_SECRET_ACCESS_KEY='1234567891012345' + # + # Then load it in your shell's rc file. + # + # % cat .zshrc + # if [[ -f "$HOME/.amazon_keys" ]]; then + # source "$HOME/.amazon_keys"; + # fi + # + # See more connection details at AWS::S3::Connection::Management::ClassMethods. + + # AWS::S3::Base is the abstract super class of all classes who make requests against S3, such as the built in + # Service, Bucket and S3Object classes. It provides methods for making requests, inferring or setting response classes, + # processing request options, and accessing attributes from S3's response data. + # + # Establishing a connection with the Base class is the entry point to using the library: + # + # AWS::S3::Base.establish_connection!(:access_key_id => '...', :secret_access_key => '...') + # + # The :access_key_id and :secret_access_key are the two required connection options. More + # details can be found in the docs for Connection::Management::ClassMethods. + # + # Extensive examples can be found in the README[link:files/README.html]. + class Base + class << self + # Wraps the current connection's request method and picks the appropriate response class to wrap the response in. + # If the response is an error, it will raise that error as an exception. All such exceptions can be caught by rescuing + # their superclass, the ResponseError exception class. + # + # 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) + process_options!(options, verb) + response = do_request(verb, path, options, body, attempts, &block) + Error::Response.new(response.response).error.raise if response.error? + response + # Once in a while, a request to S3 returns an internal error. A glitch in the matrix I presume. Since these + # errors are few and far between the request method will rescue InternalErrors the first three times they encouter them + # and will retry the request again. Most of the time the second attempt will work. + rescue *retry_exceptions + attempts == 3 ? raise : (attempts += 1; retry) + end + + [: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) + end + EVAL + end + + private + + def do_request(verb, path, options = {}, body = nil, attempts = 0, &block) + response_class.new(connection.request(verb, path, options, body, attempts, &block)) + end + + def response_class + FindResponseClass.for(self, AWS) + end + + # Using the conventions layed out in the response_class works for more than 80% of the time. + # There are a few edge cases though where we want a given class to wrap its responses in different + # response classes depending on which method is being called. + def respond_with(klass) + eval(<<-EVAL, binding, __FILE__, __LINE__) + def new_response_class + #{klass} + end + + class << self + alias_method :old_response_class, :response_class + alias_method :response_class, :new_response_class + end + EVAL + + yield + ensure + # Restore the original version + eval(<<-EVAL, binding, __FILE__, __LINE__) + class << self + alias_method :response_class, :old_response_class + end + EVAL + end + + def retry_exceptions + [InternalError, RequestTimeout] + end + end + + def initialize(attributes = {}) #:nodoc: + @attributes = attributes + end + + private + attr_reader :attributes + + def connection + self.class.connection + end + + def http + connection.http + end + + def request(*args, &block) + self.class.request(*args, &block) + end + + def method_missing(method, *args, &block) + attributes[method.to_s] || attributes[method] || super + end + end +end Index: lib/aws.rb =================================================================== --- lib/aws.rb (revision 0) +++ lib/aws.rb (revision 0) @@ -0,0 +1,42 @@ +require 'base64' +require 'cgi' +require 'uri' +require 'openssl' +require 'digest/sha1' +require 'net/https' +require 'time' +require 'date' +require 'open-uri' + +$:.unshift(File.dirname(__FILE__)) +require 'aws/extensions' +require_library_or_gem 'builder' unless defined? Builder +require_library_or_gem 'mime/types' unless defined? MIME::Types + +require 'aws/base' +require 'aws/parsing' +require 'aws/error' +require 'aws/exceptions' +require 'aws/connection' +require 'aws/authentication' +require 'aws/response' + +require 'aws/s3' +require 'aws/ec2' + +require_library_or_gem 'xmlsimple' unless defined? XmlSimple +# If libxml is installed, we use the FasterXmlSimple library, that provides most of the functionality of XmlSimple +# except it uses the xml/libxml library for xml parsing (rather than REXML). If libxml isn't installed, we just fall back on +# XmlSimple. +AWS::Parsing.parser = + begin + require_library_or_gem 'xml/libxml' + # Older version of libxml aren't stable (bus error when requesting attributes that don't exist) so we + # have to use a version greater than '0.3.8.2'. + raise LoadError unless XML::Parser::VERSION > '0.3.8.2' + $:.push(File.join(File.dirname(__FILE__), '..', '..', 'support', 'faster-xml-simple', 'lib')) + require_library_or_gem 'faster_xml_simple' + FasterXmlSimple + rescue LoadError + XmlSimple + end Index: bin/s3sh =================================================================== --- bin/s3sh (revision 158) +++ bin/s3sh (working copy) @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -s3_lib = File.dirname(__FILE__) + '/../lib/aws/s3' -setup = File.dirname(__FILE__) + '/setup' -irb_name = RUBY_PLATFORM =~ /mswin32/ ? 'irb.bat' : 'irb' - -exec "#{irb_name} -r #{s3_lib} -r #{setup} --simple-prompt" \ No newline at end of file Index: bin/setup.rb =================================================================== --- bin/setup.rb (revision 158) +++ bin/setup.rb (working copy) @@ -4,7 +4,11 @@ :access_key_id => ENV['AMAZON_ACCESS_KEY_ID'], :secret_access_key => ENV['AMAZON_SECRET_ACCESS_KEY'] ) + AWS::EC2::Base.establish_connection!( + :access_key_id => ENV['AMAZON_ACCESS_KEY_ID'], + :secret_access_key => ENV['AMAZON_SECRET_ACCESS_KEY'] + ) end require File.dirname(__FILE__) + '/../test/fixtures' -include AWS::S3 \ No newline at end of file +include AWS \ No newline at end of file Index: bin/awssh =================================================================== --- bin/awssh (revision 158) +++ bin/awssh (working copy) @@ -1,6 +1,6 @@ #!/usr/bin/env ruby -s3_lib = File.dirname(__FILE__) + '/../lib/aws/s3' +aws_lib = File.dirname(__FILE__) + '/../lib/aws' setup = File.dirname(__FILE__) + '/setup' irb_name = RUBY_PLATFORM =~ /mswin32/ ? 'irb.bat' : 'irb' -exec "#{irb_name} -r #{s3_lib} -r #{setup} --simple-prompt" \ No newline at end of file +exec "#{irb_name} -r #{aws_lib} -r #{setup} --simple-prompt" \ No newline at end of file From jhosteny at gmail.com Mon Jan 8 22:19:10 2007 From: jhosteny at gmail.com (Joe Hosteny) Date: Mon, 8 Jan 2007 22:19:10 -0500 Subject: [s3-dev] EC2 preliminary work In-Reply-To: <2f0ee70f0701081747w2765e893jeffeb90c7ef77561@mail.gmail.com> References: <2f0ee70f0701051431i7d7dd125ufd51191610eb87c7@mail.gmail.com> <20070105225936.GO88548@comox.textdrive.com> <2f0ee70f0701081747w2765e893jeffeb90c7ef77561@mail.gmail.com> Message-ID: <2f0ee70f0701081919p2e5624c5k429da64da9fdab4e@mail.gmail.com> On 1/8/07, Joe Hosteny wrote: > On 1/5/07, Marcel Molina Jr. wrote: > > On Fri, Jan 05, 2007 at 05:31:28PM -0500, Joe Hosteny wrote: > > > Here was a first cut at re-factoring some code from AWS::S3 into the > > > AWS module so that EC2 stuff could be added. Go easy on me - I was > > > trying to learn the code as I went, and I think I made a bit of a > > > mess. But it may be useful to you to see some of this. Feel free to > > > use all or none of it as you see fit. > > > > Awesome. Thanks for diving into that Joe. I'm going to slice a chunk of time > > out of my weekend and try to process all the bugs and fixes that you guys > > emailed in over the holidays. > > I've attached another patch. This one fixes a number of problems with > the first, and adds some code to do the EC2 authentication (see the > AWS::EC2::Base.query_for method). For example, try > EC2::Base.connection.query_for({"Action"=>"DescribeImages"}) from the > shell (now implemented in the file awssh). > > The authentication doesn't seem to work right now, and I'm not sure > why. I'll look into it later. Ah, the canonicalized string was being escaped in the build of the query string. When that's fixed, the DescribeImages call works. I should have also mentioned that the shell creates connection to S3 and EC2 by default. Also, the AWs module is included, not AWS::S3 and AWS::EC2. So, you have to do something like: S3::Service.buckets instead of Service.buckets > > Again, feel free to use any portion of this. The authentication stuff > can still be refactored quite a bit to simplify. > > > > > The plan is to try to get a bug fix release out beginning of next week. > > > > marcel > > -- > > Marcel Molina Jr. > > _______________________________________________ > > amazon-s3-dev mailing list > > amazon-s3-dev at rubyforge.org > > http://rubyforge.org/mailman/listinfo/amazon-s3-dev > > > > > From jhosteny at gmail.com Tue Jan 9 20:07:14 2007 From: jhosteny at gmail.com (Joe Hosteny) Date: Tue, 9 Jan 2007 20:07:14 -0500 Subject: [s3-dev] EC2 preliminary work In-Reply-To: <2f0ee70f0701081919p2e5624c5k429da64da9fdab4e@mail.gmail.com> References: <2f0ee70f0701051431i7d7dd125ufd51191610eb87c7@mail.gmail.com> <20070105225936.GO88548@comox.textdrive.com> <2f0ee70f0701081747w2765e893jeffeb90c7ef77561@mail.gmail.com> <2f0ee70f0701081919p2e5624c5k429da64da9fdab4e@mail.gmail.com> Message-ID: <2f0ee70f0701091707v5ba7cec2n1f1616221f777f46@mail.gmail.com> Okay, here's one more. This removes a lot of the duplicated authentication code. Once again, all regressions pass. On 1/8/07, Joe Hosteny wrote: > On 1/8/07, Joe Hosteny wrote: > > On 1/5/07, Marcel Molina Jr. wrote: > > > On Fri, Jan 05, 2007 at 05:31:28PM -0500, Joe Hosteny wrote: > > > > Here was a first cut at re-factoring some code from AWS::S3 into the > > > > AWS module so that EC2 stuff could be added. Go easy on me - I was > > > > trying to learn the code as I went, and I think I made a bit of a > > > > mess. But it may be useful to you to see some of this. Feel free to > > > > use all or none of it as you see fit. > > > > > > Awesome. Thanks for diving into that Joe. I'm going to slice a chunk of time > > > out of my weekend and try to process all the bugs and fixes that you guys > > > emailed in over the holidays. > > > > I've attached another patch. This one fixes a number of problems with > > the first, and adds some code to do the EC2 authentication (see the > > AWS::EC2::Base.query_for method). For example, try > > EC2::Base.connection.query_for({"Action"=>"DescribeImages"}) from the > > shell (now implemented in the file awssh). > > > > The authentication doesn't seem to work right now, and I'm not sure > > why. I'll look into it later. > > Ah, the canonicalized string was being escaped in the build of the > query string. When that's fixed, the DescribeImages call works. > > I should have also mentioned that the shell creates connection to S3 > and EC2 by default. Also, the AWs module is included, not AWS::S3 and > AWS::EC2. So, you have to do something like: > > S3::Service.buckets > > instead of > > Service.buckets > > > > > Again, feel free to use any portion of this. The authentication stuff > > can still be refactored quite a bit to simplify. > > > > > > > > The plan is to try to get a bug fix release out beginning of next week. > > > > > > marcel > > > -- > > > Marcel Molina Jr. > > > _______________________________________________ > > > amazon-s3-dev mailing list > > > amazon-s3-dev at rubyforge.org > > > http://rubyforge.org/mailman/listinfo/amazon-s3-dev > > > > > > > > > > -------------- next part -------------- An embedded and charset-unspecified text was scrubbed... Name: patch.txt Url: http://rubyforge.org/pipermail/amazon-s3-dev/attachments/20070109/842925f6/attachment-0001.txt From marcel at vernix.org Tue Jan 9 21:02:42 2007 From: marcel at vernix.org (Marcel Molina Jr.) Date: Wed, 10 Jan 2007 02:02:42 +0000 Subject: [s3-dev] EC2 preliminary work In-Reply-To: <2f0ee70f0701091707v5ba7cec2n1f1616221f777f46@mail.gmail.com> References: <2f0ee70f0701051431i7d7dd125ufd51191610eb87c7@mail.gmail.com> <20070105225936.GO88548@comox.textdrive.com> <2f0ee70f0701081747w2765e893jeffeb90c7ef77561@mail.gmail.com> <2f0ee70f0701081919p2e5624c5k429da64da9fdab4e@mail.gmail.com> <2f0ee70f0701091707v5ba7cec2n1f1616221f777f46@mail.gmail.com> Message-ID: <20070110020242.GB88548@comox.textdrive.com> On Tue, Jan 09, 2007 at 08:07:14PM -0500, Joe Hosteny wrote: > Okay, here's one more. This removes a lot of the duplicated > authentication code. Once again, all regressions pass. Neat. Thanks for hammering away at this. I just got accepted into EC2 so I can actually work on this stuff soon :) marcel -- Marcel Molina Jr. From jhosteny at gmail.com Wed Jan 10 11:25:11 2007 From: jhosteny at gmail.com (Joe Hosteny) Date: Wed, 10 Jan 2007 11:25:11 -0500 Subject: [s3-dev] EC2 preliminary work In-Reply-To: <20070110020242.GB88548@comox.textdrive.com> References: <2f0ee70f0701051431i7d7dd125ufd51191610eb87c7@mail.gmail.com> <20070105225936.GO88548@comox.textdrive.com> <2f0ee70f0701081747w2765e893jeffeb90c7ef77561@mail.gmail.com> <2f0ee70f0701081919p2e5624c5k429da64da9fdab4e@mail.gmail.com> <2f0ee70f0701091707v5ba7cec2n1f1616221f777f46@mail.gmail.com> <20070110020242.GB88548@comox.textdrive.com> Message-ID: <2f0ee70f0701100825x74b6d325r60400ed3471733d1@mail.gmail.com> On 1/9/07, Marcel Molina Jr. wrote: > On Tue, Jan 09, 2007 at 08:07:14PM -0500, Joe Hosteny wrote: > > Okay, here's one more. This removes a lot of the duplicated > > authentication code. Once again, all regressions pass. > > Neat. Thanks for hammering away at this. I just got accepted into EC2 so I > can actually work on this stuff soon :) > No problem. I think I know where some of your objections to modifications may be. Feel free to contact me offline, too, if you'd like to discuss any of them. Also, I queried the Amazon folks about the possible release date for a REST interface for EC2. I haven't heard back from them yet. > marcel > -- > Marcel Molina Jr. > _______________________________________________ > amazon-s3-dev mailing list > amazon-s3-dev at rubyforge.org > http://rubyforge.org/mailman/listinfo/amazon-s3-dev > From kookster at gmail.com Wed Jan 10 14:29:34 2007 From: kookster at gmail.com (Andrew Kuklewicz) Date: Wed, 10 Jan 2007 14:29:34 -0500 Subject: [s3-dev] Can't set metadata on create - workaround Message-ID: <19ba6d7f0701101129h319eeebcg5c19079be09239ca@mail.gmail.com> Same problem as others, can't set metadata on initial store of an S3Object. I put in Lars' changes, still not fixed (at least for me). I am continuing to try and figure this out, and patch it, but not making much headway yet. I agree with Lars that it has something to do with the About object - on initial store, the request method is using a Hash instead of an About for creating the http request, though I do see the headers initially set in it, it is not properly processed to create an About object - perhaps that has something to do with it, I'll know when someone fixes it I guess :) Ended up with a work around - now I initially store the object with just a 1 char text string body, then find it, and update it's metadata and set the value attribute to the input stream of my file. Oh ugly ugly, but it works, and takes almost no longer than a single create as the initial create is such a small amount of data. Reminds me of the bad old days of inserting into a table to get a unique id, then putting in all the values... Anyway, here is some sample code for this unlovely hack: AWS::S3::S3Object.store('my-file-name', 'placeholder body text', 'my-bucket', :content_type=>'text/plain') my_file = AWS::S3::S3Object.find 'my-file-name', 'my-bucket' my_file.metadata['meta-data-type']='meta-data-value' my_file.content_type='audio/mpeg' my_file.value=open('/path/to/my/file') my_file.store I am awash in the ugliness, but it's a work around, so be nice. -Andrew -------------- next part -------------- An HTML attachment was scrubbed... URL: http://rubyforge.org/pipermail/amazon-s3-dev/attachments/20070110/023e34d4/attachment.html From metalhead at metalhead.ws Wed Jan 10 14:38:09 2007 From: metalhead at metalhead.ws (Metalhead) Date: Wed, 10 Jan 2007 20:38:09 +0100 Subject: [s3-dev] Can't set metadata on create - workaround In-Reply-To: <19ba6d7f0701101129h319eeebcg5c19079be09239ca@mail.gmail.com> References: <19ba6d7f0701101129h319eeebcg5c19079be09239ca@mail.gmail.com> Message-ID: <20070110203809.795d40b0@huginn.asgard.yggdrasill> > Same problem as others, can't set metadata on initial store of an S3Object. > I put in Lars' changes, still not fixed (at least for me). I am continuing > to try and figure this out, and patch it, but not making much headway yet. > I agree with Lars that it has something to do with the About object - on > initial store, the request method is using a Hash instead of an About for > creating the http request, though I do see the headers initially set in it, > it is not properly processed to create an About object - perhaps that has > something to do with it, I'll know when someone fixes it I guess :) I'm curious, what didn't work for you with the fix I proposed? I never ran into any problems with it. It's not a real fix though, I remember imagining at least one situation where it would cause havoc. Lars -- They say that if you step on a crack you could break your mother's back. -------------- next part -------------- A non-text attachment was scrubbed... Name: signature.asc Type: application/pgp-signature Size: 198 bytes Desc: not available Url : http://rubyforge.org/pipermail/amazon-s3-dev/attachments/20070110/81eb244f/attachment.bin From kookster at gmail.com Wed Jan 10 16:24:42 2007 From: kookster at gmail.com (Andrew Kuklewicz) Date: Wed, 10 Jan 2007 16:24:42 -0500 Subject: [s3-dev] Fwd: Can't set metadata on create - workaround In-Reply-To: <19ba6d7f0701101235t24501900j30d22ad2a7cb96bd@mail.gmail.com> References: <19ba6d7f0701101129h319eeebcg5c19079be09239ca@mail.gmail.com> <20070110203809.795d40b0@huginn.asgard.yggdrasill> <19ba6d7f0701101235t24501900j30d22ad2a7cb96bd@mail.gmail.com> Message-ID: <19ba6d7f0701101324x7fa5c2bcuc1d8d6f2d029f778@mail.gmail.com> Hey Lars, I'll patch and try again to get you better info. It caused no error, the metadata was just empty after the initial store. Might be how I was trying to use it - I'll work on it tonight and send back a test case for you. -Andrew On 1/10/07, Metalhead wrote: > > Same problem as others, can't set metadata on initial store of an > S3Object. > > I put in Lars' changes, still not fixed (at least for me). I am > continuing > > to try and figure this out, and patch it, but not making much headway > yet. > > I agree with Lars that it has something to do with the About object - on > > initial store, the request method is using a Hash instead of an About > for > > creating the http request, though I do see the headers initially set in > it, > > it is not properly processed to create an About object - perhaps that > has > > something to do with it, I'll know when someone fixes it I guess :) > > I'm curious, what didn't work for you with the fix I proposed? I never ran > into > any problems with it. It's not a real fix though, I remember imagining at > least > one situation where it would cause havoc. > > Lars > > > -- > They say that if you step on a crack you could break your mother's back. > > > _______________________________________________ > amazon-s3-dev mailing list > amazon-s3-dev at rubyforge.org > http://rubyforge.org/mailman/listinfo/amazon-s3-dev > > > > -- Andrew Kuklewicz -- Andrew Kuklewicz -------------- next part -------------- An HTML attachment was scrubbed... URL: http://rubyforge.org/pipermail/amazon-s3-dev/attachments/20070110/e4a148ba/attachment.html From kookster at gmail.com Thu Jan 11 00:26:52 2007 From: kookster at gmail.com (Andrew Kuklewicz) Date: Thu, 11 Jan 2007 00:26:52 -0500 Subject: [s3-dev] Can't set metadata on create - workaround In-Reply-To: <20070110203809.795d40b0@huginn.asgard.yggdrasill> References: <19ba6d7f0701101129h319eeebcg5c19079be09239ca@mail.gmail.com> <20070110203809.795d40b0@huginn.asgard.yggdrasill> Message-ID: <19ba6d7f0701102126u17f759bbna38b85b1aed9d659@mail.gmail.com> Ok - I take it back, the fix works, it was a small difference in how store is called as a class method that threw me. After applying the patch Lars send in, I find the following both work, and are equivalent. I was using the first method, and trying to set a metadata header like 'foo', but you have to include the prefix yourself using this mechanism, so a 'foo' header will not be saved nor throw an error, but 'x-amz-meta-foo' will be saved. 1) Using the store class method: AWS::S3::S3Object.store('myfile-81805', open('/data/production/amazon/myfolder/myfile-81805'), 'my-s3-bucket', :content_type=>'audio/mpeg', 'x-amz-meta-my-file-name'=>'my.mp3') 2) Using the Bucket new_object method, and then store, you don't have to include the metadata prefix: my_bucket = AWS::S3::Bucket.find('my-s3-bucket') s3o = my_bucket.new_object s3o.key = 'myfile-81805' s3o.value = open('/data/production/amazon/myfolder/myfile-81805') s3o.content_type = 'audio/mpeg' s3o.metadata['my-file-name'] = 'myfile-81805' s3o.store So thanks to Lars! -A On 1/10/07, Metalhead wrote: > > > Same problem as others, can't set metadata on initial store of an > S3Object. > > I put in Lars' changes, still not fixed (at least for me). I am > continuing > > to try and figure this out, and patch it, but not making much headway > yet. > > I agree with Lars that it has something to do with the About object - on > > initial store, the request method is using a Hash instead of an About > for > > creating the http request, though I do see the headers initially set in > it, > > it is not properly processed to create an About object - perhaps that > has > > something to do with it, I'll know when someone fixes it I guess :) > > I'm curious, what didn't work for you with the fix I proposed? I never ran > into > any problems with it. It's not a real fix though, I remember imagining at > least > one situation where it would cause havoc. > > Lars > > > -- > They say that if you step on a crack you could break your mother's back. > > > _______________________________________________ > amazon-s3-dev mailing list > amazon-s3-dev at rubyforge.org > http://rubyforge.org/mailman/listinfo/amazon-s3-dev > > > > -- Andrew Kuklewicz -------------- next part -------------- An HTML attachment was scrubbed... URL: http://rubyforge.org/pipermail/amazon-s3-dev/attachments/20070111/99eae6ef/attachment.html From marcus at marcusr.org.uk Thu Jan 11 04:31:09 2007 From: marcus at marcusr.org.uk (Marcus Roberts) Date: Thu, 11 Jan 2007 09:31:09 +0000 Subject: [s3-dev] Some observations Message-ID: <6D6BB3F2-8969-4300-A716-35DBCB4B8D67@marcusr.org.uk> I thought I'd pass on some short observations I've made in using the S3 library over the last couple of days. I'm using S3 like a file system, so directory listings have been a central area. Some or all of this may be obvious from the documentation, but I thought it might be useful info for someone else just starting out. Firstly, getting *most* of the metadata on each file requires a request be made to the S3 server. In my first implementation of a directory listing, I was accessing object.about[] for each file in the directory, which became painful as the list grew longer. However, all I needed was filename, size and last modified. You can get all of these for "free" using object.key, object.size and object.last_modified. If you ask for metadata for a file, a HEAD request is sent. If you're behind a firewall that filters on content-type, your network connection will be broken when you ask for the metadata of a file that's blocked, because the content-type header is sent, and that's enough to trigger the firewall. So I couldn't build a directory listing when the listing contained blocked files, even though I wasn't downloading the file content. This problem went away when I used the built-in metadata, but it might be something to be aware of in the future. And finally, the firewall I deployed behind was a Watchguard, which was set up to send all HTTP traffic via an outbound HTTP Proxy. This was set to remove all "unknown" headers, so requests with the x-amz- meta- headers were failing because these were stripped out. Switching to SSL solves this problem of course :) It's all working fine now, and the S3 library has made interfacing with AWS simple! Marcus From christopher.bailey at adobe.com Fri Jan 12 16:33:10 2007 From: christopher.bailey at adobe.com (Christopher Bailey) Date: Fri, 12 Jan 2007 13:33:10 -0800 Subject: [s3-dev] Problems with store hanging Message-ID: We?ve been having some problems with calls to S3Object#store hanging. I don?t seem to get exceptions, the call doesn?t seem to return, and eventually Mongrel simply times out. The files we?re storing are not big (<1MB). A typical call looks like: AWS::S3::S3Object.store(key, data, {'mod-time' => mod_time.getutc.to_s, :content_type => content_type}) These files do not exist on S3 (i.e. No S3 object with that key yet exists) at the time. These ?hangs? are sporadic, so I haven?t been able to do a TCP/IP trace/packet dump on one yet. I?m wondering if anyone else has seen this, or if folks have an idea how I might further debug it. I should note that ?data? above, is typically an open file, i.e. Something like ?open(some_file)?. I did not the thread about metadata causing some problems, but it didn?t seem to be the same problem, in that in our case, the file never gets uploaded at all, and the #store call just hangs. Anyway, any further help is appreciated. System details: we see this on both MacOS X (ruby 1.8.4), and Fedora Core 6 (ruby 1.8.5). Using AWS gem version 0.3.0, edge Rails 5690. __ Christopher Bailey Senior Computer Scientist Web Services and Digital Imaging Development Adobe Systems Incorporated mailto: chbailey at adobe.com -------------- next part -------------- An HTML attachment was scrubbed... URL: http://rubyforge.org/pipermail/amazon-s3-dev/attachments/20070112/3aaac1c7/attachment.html From kookster at gmail.com Fri Jan 12 19:17:13 2007 From: kookster at gmail.com (Andrew Kuklewicz) Date: Fri, 12 Jan 2007 19:17:13 -0500 Subject: [s3-dev] Problems with store hanging In-Reply-To: References: Message-ID: <19ba6d7f0701121617x527eb1b0j6231b1fc88fd6e0e@mail.gmail.com> Hmm. I've had some sporadic problems where after a long time (probably a few minutes) a large streamed file, >2 mbs, would throw a broken pipe kind of error, but never where it just hung completely - how long did you wait (generally)? -Andrew On 1/12/07, Christopher Bailey wrote: > > We've been having some problems with calls to S3Object#store hanging. I > don't seem to get exceptions, the call doesn't seem to return, and > eventually Mongrel simply times out. The files we're storing are not big > (<1MB). A typical call looks like: > > AWS::S3::S3Object.store(key, data, {'mod-time' => mod_time.getutc.to_s, > :content_type => content_type}) > > These files do not exist on S3 (i.e. No S3 object with that key yet > exists) at the time. These "hangs" are sporadic, so I haven't been able to > do a TCP/IP trace/packet dump on one yet. I'm wondering if anyone else has > seen this, or if folks have an idea how I might further debug it. > > I should note that "data" above, is typically an open file, i.e. Something > like "open(some_file)". I did not the thread about metadata causing some > problems, but it didn't seem to be the same problem, in that in our case, > the file never gets uploaded at all, and the #store call just hangs. > Anyway, any further help is appreciated. > > System details: we see this on both MacOS X (ruby 1.8.4), and Fedora Core > 6 (ruby 1.8.5). Using AWS gem version 0.3.0, edge Rails 5690. > > __ > Christopher Bailey > Senior Computer Scientist > Web Services and Digital Imaging Development > Adobe Systems Incorporated > mailto: chbailey at adobe.com > > _______________________________________________ > amazon-s3-dev mailing list > amazon-s3-dev at rubyforge.org > http://rubyforge.org/mailman/listinfo/amazon-s3-dev > > > -- Andrew Kuklewicz -------------- next part -------------- An HTML attachment was scrubbed... URL: http://rubyforge.org/pipermail/amazon-s3-dev/attachments/20070112/56a5a726/attachment.html From christopher.bailey at adobe.com Fri Jan 12 19:54:49 2007 From: christopher.bailey at adobe.com (Christopher Bailey) Date: Fri, 12 Jan 2007 16:54:49 -0800 Subject: [s3-dev] Problems with store hanging In-Reply-To: <19ba6d7f0701121617x527eb1b0j6231b1fc88fd6e0e@mail.gmail.com> Message-ID: WE can wait forever. And, we?re talking about files that are say 150k. On one person?s machine, we do see broken pipe messages, but not on others. Also, in doing some further looking, we eliminated the use of streaming from a file. And further, we also tried making the connection NOT persistent, which initially looked promising, but then we got the failures again. I will have to go put some further debugging into the AWS::S3 code and see. We previously used Amazon?s library (with a patch or two of our own, but nothing significant), and never had a problem, so I do suspect it?s the AWS::S3 code. Does any of this sound familiar to the folks who were seeing the metadata issues? That?s my next tact is to try that workaround that was mentioned in that thread. On 1/12/07 4:17 PM, "Andrew Kuklewicz" wrote: > Hmm. > I've had some sporadic problems where after a long time (probably a few > minutes) a large streamed file, >2 mbs, would throw a broken pipe kind of > error, but never where it just hung completely - how long did you wait > (generally)? > > -Andrew > > > > On 1/12/07, Christopher Bailey wrote: >> We've been having some problems with calls to S3Object#store hanging. I >> don't seem to get exceptions, the call doesn't seem to return, and eventually >> Mongrel simply times out. The files we're storing are not big (<1MB). A >> typical call looks like: >> >> AWS::S3::S3Object.store(key, data, {'mod-time' => mod_time.getutc.to_s, >> :content_type => content_type}) >> >> These files do not exist on S3 (i.e. No S3 object with that key yet exists) >> at the time. These "hangs" are sporadic, so I haven't been able to do a >> TCP/IP trace/packet dump on one yet. I'm wondering if anyone else has seen >> this, or if folks have an idea how I might further debug it. >> >> I should note that "data" above, is typically an open file, i.e. Something >> like "open(some_file)". I did not the thread about metadata causing some >> problems, but it didn't seem to be the same problem, in that in our case, the >> file never gets uploaded at all, and the #store call just hangs. Anyway, any >> further help is appreciated. >> >> System details: we see this on both MacOS X (ruby 1.8.4), and Fedora Core 6 >> (ruby 1.8.5). Using AWS gem version 0.3.0, edge Rails 5690. >> >> __ >> Christopher Bailey >> Senior Computer Scientist >> Web Services and Digital Imaging Development >> Adobe Systems Incorporated >> mailto: chbailey at adobe.com >> >> _______________________________________________ >> amazon-s3-dev mailing list >> amazon-s3-dev at rubyforge.org >> http://rubyforge.org/mailman/listinfo/amazon-s3-dev >> >> > > __ Christopher Bailey Senior Computer Scientist Web Services and Digital Imaging Development Adobe Systems Incorporated mailto: chbailey at adobe.com phone: 530.888.5705 -------------- next part -------------- An HTML attachment was scrubbed... URL: http://rubyforge.org/pipermail/amazon-s3-dev/attachments/20070112/a81e0ac2/attachment.html From metalhead at metalhead.ws Sat Jan 13 06:04:23 2007 From: metalhead at metalhead.ws (Metalhead) Date: Sat, 13 Jan 2007 12:04:23 +0100 Subject: [s3-dev] Problems with store hanging In-Reply-To: References: <19ba6d7f0701121617x527eb1b0j6231b1fc88fd6e0e@mail.gmail.com> Message-ID: <20070113120423.5fb773c0@huginn.asgard.yggdrasill> > Does any of this sound familiar to the folks who were seeing the metadata > issues? That?s my next tact is to try that workaround that was mentioned in > that thread. Hmm, I'd be rather surprised if this was related. The metadata bug only affects metadata not being part of the object (and hence not being stored), but not the transfer or even store method itself at all. Also this only really affects code where an object is created, then modified, stored, etc. You're not creating objects since you're using the static method to store stuff. Have you tried going the long way by creating the object first, setting its metadata and then calling store on it? Lars -- They say that a fountain looks nothing like a regularly erupting geyser. -------------- next part -------------- A non-text attachment was scrubbed... Name: signature.asc Type: application/pgp-signature Size: 198 bytes Desc: not available Url : http://rubyforge.org/pipermail/amazon-s3-dev/attachments/20070113/94879a2e/attachment.bin From emmett.shear at gmail.com Fri Jan 26 21:19:53 2007 From: emmett.shear at gmail.com (Emmett Shear) Date: Fri, 26 Jan 2007 18:19:53 -0800 Subject: [s3-dev] Thread safety of requests? Message-ID: <860712640701261819j3854122ai6a92af966733d3b0@mail.gmail.com> I have a webserver which loads and restreams data off S3; it works great when I have only one connection at a time, but when I have multiple incoming connections it fails with an exception: Thu Jan 25 16:33:56 PST 2007: ERROR: wrong status line: "N\276\377\202\227.... Which looks like it's coming from the middle of the binary file I'm streaming. Having looked through the aws/s3 source code, it looks like all requests share a single connection; maybe that's what's causing this? Is aws/s3 thread safe? How can I solve this problem? Thanks, Emmett From metalhead at metalhead.ws Sat Jan 27 06:35:30 2007 From: metalhead at metalhead.ws (Metalhead) Date: Sat, 27 Jan 2007 12:35:30 +0100 Subject: [s3-dev] Thread safety of requests? In-Reply-To: <860712640701261819j3854122ai6a92af966733d3b0@mail.gmail.com> References: <860712640701261819j3854122ai6a92af966733d3b0@mail.gmail.com> Message-ID: <20070127123530.174428d4@huginn.asgard.yggdrasill> > I have a webserver which loads and restreams data off S3; it works > great when I have only one connection at a time, but when I have > multiple incoming connections it fails with an exception: > > Thu Jan 25 16:33:56 PST 2007: ERROR: wrong status line: "N\276\377\202\227.... > > Which looks like it's coming from the middle of the binary file I'm > streaming. Having looked through the aws/s3 source code, it looks like > all requests share a single connection; maybe that's what's causing > this? Is aws/s3 thread safe? How can I solve this problem? I've been looking into that before; you're right, all requests share a single connection. I'm not quite sure how that would've caused the error you're seeing. Have you tried turning persistent connections off? As far as I understand it, there's not really a way to have multiple connections with the current version, as it uses class names to identify connections. That part would've to be completely rewritten to allow connection pooling. Lars -- Affairs with nymphs are often very expensive. -------------- next part -------------- A non-text attachment was scrubbed... Name: signature.asc Type: application/pgp-signature Size: 197 bytes Desc: not available Url : http://rubyforge.org/pipermail/amazon-s3-dev/attachments/20070127/a5830209/attachment-0001.bin