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/