36 |
37 |
Readme
38 |
Getting started
39 | <%= erb_data[:readme] %>
40 |
41 |
42 |
--------------------------------------------------------------------------------
/support/faster-xml-simple/test/regression_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/test_helper'
2 |
3 | class RegressionTest < FasterXSTest
4 | def test_content_nil_regressions
5 | expected = {"asdf"=>{"jklsemicolon"=>{}}}
6 | assert_equal expected, FasterXmlSimple.xml_in("
")
7 | assert_equal expected, FasterXmlSimple.xml_in("
", 'forcearray'=>['asdf'])
8 | end
9 |
10 | def test_s3_regression
11 | str = File.read("test/fixtures/test-7.xml")
12 | assert_nil FasterXmlSimple.xml_in(str)["AccessControlPolicy"]["AccessControlList"]["__content__"]
13 | end
14 |
15 | def test_xml_simple_transparency
16 | assert_equal XmlSimple.xml_in("
"), FasterXmlSimple.xml_in("
")
17 | end
18 |
19 | def test_suppress_empty_variations
20 | str = "
"
21 |
22 | assert_equal Hash.new, FasterXmlSimple.xml_in(str)["asdf"]["fdsa"]
23 | assert_nil FasterXmlSimple.xml_in(str, 'suppressempty'=>nil)["asdf"]["fdsa"]
24 | assert_equal '', FasterXmlSimple.xml_in(str, 'suppressempty'=>'')["asdf"]["fdsa"]
25 | assert !FasterXmlSimple.xml_in(str, 'suppressempty'=>true)["asdf"].has_key?("fdsa")
26 | end
27 |
28 | def test_empty_string_doesnt_crash
29 | assert_raise(XML::Parser::ParseError) do
30 | silence_stderr do
31 | FasterXmlSimple.xml_in('')
32 | end
33 | end
34 | end
35 |
36 | def test_keeproot_false
37 | str = "
1"
38 | expected = {"fdsa"=>"1"}
39 | assert_equal expected, FasterXmlSimple.xml_in(str, 'keeproot'=>false)
40 | end
41 |
42 | def test_keeproot_false_with_force_content
43 | str = "
1"
44 | expected = {"fdsa"=>{"__content__"=>"1"}}
45 | assert_equal expected, FasterXmlSimple.xml_in(str, 'keeproot'=>false, 'forcecontent'=>true)
46 | end
47 | end
--------------------------------------------------------------------------------
/support/faster-xml-simple/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'rake'
3 | require 'rake/testtask'
4 | require 'rake/rdoctask'
5 | require 'rake/packagetask'
6 | require 'rake/gempackagetask'
7 | require 'lib/faster_xml_simple'
8 |
9 | task :default => :test
10 |
11 | Rake::TestTask.new do |test|
12 | test.pattern = 'test/*_test.rb'
13 | test.verbose = true
14 | end
15 |
16 |
17 | Rake::RDocTask.new do |rdoc|
18 | rdoc.rdoc_dir = 'doc'
19 | rdoc.title = "FasterXmlSimple, a libxml based replacement for XmlSimple"
20 | rdoc.options << '--line-numbers' << '--inline-source'
21 | rdoc.rdoc_files.include('README')
22 | rdoc.rdoc_files.include('COPYING')
23 | rdoc.rdoc_files.include('lib/**/*.rb')
24 | end
25 |
26 | namespace :dist do
27 |
28 | spec = Gem::Specification.new do |s|
29 | s.name = 'faster_xml_simple'
30 | s.version = Gem::Version.new(FasterXmlSimple::Version)
31 | s.summary = "A libxml based replacement for XmlSimple"
32 | s.description = s.summary
33 | s.email = 'michael@koziarski.com'
34 | s.author = 'Michael Koziarski'
35 | s.has_rdoc = true
36 | s.extra_rdoc_files = %w(README COPYING)
37 | s.homepage = 'http://fasterxs.rubyforge.org'
38 | s.rubyforge_project = 'fasterxs'
39 | s.files = FileList['Rakefile', 'lib/**/*.rb']
40 | s.test_files = Dir['test/**/*']
41 |
42 | s.add_dependency 'libxml-ruby', '>= 0.3.8.4'
43 | s.rdoc_options = ['--title', "",
44 | '--main', 'README',
45 | '--line-numbers', '--inline-source']
46 | end
47 | Rake::GemPackageTask.new(spec) do |pkg|
48 | pkg.need_tar_gz = true
49 | pkg.package_files.include('{lib,test}/**/*')
50 | pkg.package_files.include('README')
51 | pkg.package_files.include('COPYING')
52 | pkg.package_files.include('Rakefile')
53 | end
54 | end
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 | 0.3.0
2 |
3 | [ ] Alias make alias for establish_connection! that is non-bang
4 |
5 | [ ] Pass filter criteria like :max_keys onto methods like logs_for and logs which return logs.
6 | [ ] Add high level support to custom logging information as documented in the "Adding Custom Information..." here http://docs.amazonwebservices.com/AmazonS3/2006-03-01/LogFormat.html
7 |
8 | [ ] Bucket.delete(:force => true) needs to fetch all objects in the bucket until there are no more, taking into account the max-keys limit of 1000 objects at a time and it needs to do so in a very efficient manner so it can handle very large buckets (using :prefix and :marker)
9 | [ ] Ability to set content_type on S3Object that has not been stored yet
10 | [ ] Allow symbol and abbreviated version of logging options ('target_prefix' => :prefix, 'target_bucket' => :bucket)
11 | [ ] Allow symbol options for grant's constructor ('permission' => :permission)
12 | [ ] Reconsider save method to Policies returned by Bucket and S3Object's acl instance method so you can do some_object.acl.save after modifying it rather than some_object.acl(some_object.acl)
13 |
14 | [X] S3Object.copy and S3Object.move should preserve the acl
15 | [X] Consider opening up Net::HTTPGenericRequest to replace hardcoded chunk_size to something greater than 1k (maybe 500k since the files are presumed to be quite large)
16 | [X] Add S3Object.exists?
17 | [X] See about replacing XmlSimple with libxml if it's installed since XmlSimple can be rather slow (due to wrapping REXML)
18 | [X] Ability to build up the README from internal docs so documentation for various classes and the README can feed from a single source
19 | [X] Bittorrent documentation
20 | [X] Document logging methods
21 | [X] Bittorrent
22 | [X] ACL documentation
23 | [X] Log management ([de]activation & retrieval)
24 | [X] Remote ACL tests
25 | [X] ACL requesting and parsing
26 | [X] ACL updating for already stored objects which merges with existing ACL
27 |
--------------------------------------------------------------------------------
/lib/aws/s3/service.rb:
--------------------------------------------------------------------------------
1 | module AWS
2 | module S3
3 | # The service lets you find out general information about your account, like what buckets you have.
4 | #
5 | # Service.buckets
6 | # # => []
7 | class Service < Base
8 | @@response = nil #:nodoc:
9 |
10 | class << self
11 | # List all your buckets.
12 | #
13 | # Service.buckets
14 | # # => []
15 | #
16 | # For performance reasons, the bucket list will be cached. If you want avoid all caching, pass the
:reload
17 | # as an argument:
18 | #
19 | # Service.buckets(:reload)
20 | def buckets
21 | response = get('/')
22 | if response.empty?
23 | []
24 | else
25 | response.buckets.map {|attributes| Bucket.new(attributes)}
26 | end
27 | end
28 | memoized :buckets
29 |
30 | # Sometimes methods that make requests to the S3 servers return some object, like a Bucket or an S3Object.
31 | # Other times they return just
true. Other times they raise an exception that you may want to rescue. Despite all these
32 | # possible outcomes, every method that makes a request stores its response object for you in Service.response. You can always
33 | # get to the last request's response via Service.response.
34 | #
35 | # objects = Bucket.objects('jukebox')
36 | # Service.response.success?
37 | # # => true
38 | #
39 | # This is also useful when an error exception is raised in the console which you weren't expecting. You can
40 | # root around in the response to get more details of what might have gone wrong.
41 | def response
42 | @@response
43 | end
44 |
45 | def response=(response) #:nodoc:
46 | @@response = response
47 | end
48 | end
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/aws/s3/bittorrent.rb:
--------------------------------------------------------------------------------
1 | module AWS
2 | module S3
3 | # Objects on S3 can be distributed via the BitTorrent file sharing protocol.
4 | #
5 | # You can get a torrent file for an object by calling
torrent_for:
6 | #
7 | # S3Object.torrent_for 'kiss.jpg', 'marcel'
8 | #
9 | # Or just call the
torrent method if you already have the object:
10 | #
11 | # song = S3Object.find 'kiss.jpg', 'marcel'
12 | # song.torrent
13 | #
14 | # Calling
grant_torrent_access_to on a object will allow anyone to anonymously
15 | # fetch the torrent file for that object:
16 | #
17 | # S3Object.grant_torrent_access_to 'kiss.jpg', 'marcel'
18 | #
19 | # Anonymous requests to
20 | #
21 | # http://s3.amazonaws.com/marcel/kiss.jpg?torrent
22 | #
23 | # will serve up the torrent file for that object.
24 | module BitTorrent
25 | def self.included(klass) #:nodoc:
26 | klass.extend ClassMethods
27 | end
28 |
29 | # Adds methods to S3Object for accessing the torrent of a given object.
30 | module ClassMethods
31 | # Returns the torrent file for the object with the given
key.
32 | def torrent_for(key, bucket = nil)
33 | get(path!(bucket, key) << '?torrent').body
34 | end
35 | alias_method :torrent, :torrent_for
36 |
37 | # Grants access to the object with the given
key to be accessible as a torrent.
38 | def grant_torrent_access_to(key, bucket = nil)
39 | policy = acl(key, bucket)
40 | return true if policy.grants.include?(:public_read)
41 | policy.grants << ACL::Grant.grant(:public_read)
42 | acl(key, bucket, policy)
43 | end
44 | alias_method :grant_torrent_access, :grant_torrent_access_to
45 | end
46 |
47 | # Returns the torrent file for the object.
48 | def torrent
49 | self.class.torrent_for(key, bucket.name)
50 | end
51 |
52 | # Grants torrent access publicly to anyone who requests it on this object.
53 | def grant_torrent_access
54 | self.class.grant_torrent_access_to(key, bucket.name)
55 | end
56 | end
57 | end
58 | end
--------------------------------------------------------------------------------
/INSTALL:
--------------------------------------------------------------------------------
1 | == Rubygems
2 |
3 | The easiest way to install aws/s3 is with Rubygems:
4 |
5 | % sudo gem i aws-s3 -ry
6 |
7 | == Directly from svn
8 |
9 | % svn co svn://rubyforge.org/var/svn/amazon/s3/trunk aws
10 |
11 | == As a Rails plugin
12 |
13 | If you want to use aws/s3 with a Rails application, you can export the repository
14 | into your plugins directory and then check it in:
15 |
16 | % cd my-rails-application/vendor/plugins
17 | % svn export svn://rubyforge.org/var/svn/amazon/s3/trunk aws
18 | % svn add aws
19 |
20 | Or you could pull it down with an svn:externals:
21 |
22 | % cd my-rails-application/vendor/plugins
23 | % svn propedit svn:externals .
24 |
25 | Then add the following line, save and exit:
26 |
27 | aws svn://rubyforge.org/var/svn/amazon/s3/trunk
28 |
29 | If you go the svn route, be sure that you have all the dependencies installed. The list of dependencies follow.
30 |
31 | == Dependencies
32 |
33 | AWS::S3 requires Ruby 1.8.4 or greater.
34 |
35 | It also has the following dependencies:
36 |
37 | sudo gem i xml-simple -ry
38 | sudo gem i builder -ry
39 | sudo gem i mime-types -ry
40 |
41 | === XML parsing (xml-simple)
42 |
43 | AWS::S3 depends on XmlSimple (http://xml-simple.rubyforge.org/). When installing aws/s3 with
44 | Rubygems, this dependency will be taken care of for you. Otherwise, installation instructions are listed on the xml-simple
45 | site.
46 |
47 | If your system has the Ruby libxml bindings installed (http://libxml.rubyforge.org/) they will be used instead of REXML (which is what XmlSimple uses). For those concerned with speed and efficiency, it would behoove you to install libxml (instructions here: http://libxml.rubyforge.org/install.html) as it is considerably faster and less expensive than REXML.
48 |
49 | === XML generation (builder)
50 |
51 | AWS::S3 also depends on the Builder library (http://builder.rubyforge.org/ and http://rubyforge.org/projects/builder/). This will also automatically be installed for you when using Rubygems.
52 |
53 | === Content type inference (mime-types)
54 |
55 | AWS::S3 depends on the MIME::Types library (http://mime-types.rubyforge.org/) to infer the content type of an object that does not explicitly specify it. This library will automatically be installed for you when using Rubygems.
--------------------------------------------------------------------------------
/test/parsing_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/test_helper'
2 |
3 | class TypecastingTest < Test::Unit::TestCase
4 | # Make it easier to call methods in tests
5 | Parsing::Typecasting.public_instance_methods.each do |method|
6 | Parsing::Typecasting.send(:module_function, method)
7 | end
8 |
9 | def test_array_with_one_element_that_is_a_hash
10 | value = [{'Available' => 'true'}]
11 | assert_equal [{'available' => true}], Parsing::Typecasting.typecast(value)
12 | end
13 |
14 | def test_hash_with_one_key_whose_value_is_an_array
15 | value = {
16 | 'Bucket' =>
17 | [
18 | {'Available' => 'true'}
19 | ]
20 | }
21 |
22 | expected = {
23 | 'bucket' =>
24 | [
25 | {'available' => true}
26 | ]
27 | }
28 | assert_equal expected, Parsing::Typecasting.typecast(value)
29 | end
30 |
31 | end
32 |
33 | class XmlParserTest < Test::Unit::TestCase
34 | def test_bucket_is_always_forced_to_be_an_array_unless_empty
35 | one_bucket = Parsing::XmlParser.new(Fixtures::Buckets.bucket_list_with_one_bucket)
36 | more_than_one = Parsing::XmlParser.new(Fixtures::Buckets.bucket_list_with_more_than_one_bucket)
37 |
38 | [one_bucket, more_than_one].each do |bucket_list|
39 | assert_kind_of Array, bucket_list['buckets']['bucket']
40 | end
41 |
42 | no_buckets = Parsing::XmlParser.new(Fixtures::Buckets.empty_bucket_list)
43 | assert no_buckets.has_key?('buckets')
44 | assert_nil no_buckets['buckets']
45 | end
46 |
47 | def test_bucket_contents_are_forced_to_be_an_array_unless_empty
48 | one_key = Parsing::XmlParser.new(Fixtures::Buckets.bucket_with_one_key)
49 | more_than_one = Parsing::XmlParser.new(Fixtures::Buckets.bucket_with_more_than_one_key)
50 | [one_key, more_than_one].each do |bucket_with_contents|
51 | assert_kind_of Array, bucket_with_contents['contents']
52 | end
53 |
54 | no_keys = Parsing::XmlParser.new(Fixtures::Buckets.empty_bucket)
55 | assert !no_keys.has_key?('contents')
56 | end
57 |
58 | def test_policy_grants_are_always_an_array
59 | policy = Parsing::XmlParser.new(Fixtures::Policies.policy_with_one_grant)
60 | assert_kind_of Array, policy['access_control_list']['grant']
61 | end
62 |
63 | def test_empty_xml_response_is_not_parsed
64 | assert_equal({}, Parsing::XmlParser.new(''))
65 | end
66 | end
--------------------------------------------------------------------------------
/lib/aws/s3.rb:
--------------------------------------------------------------------------------
1 | require 'cgi'
2 | require 'uri'
3 | require 'openssl'
4 | require 'digest/sha1'
5 | require 'net/https'
6 | require 'time'
7 | require 'date'
8 | require 'open-uri'
9 |
10 | $:.unshift(File.dirname(__FILE__))
11 | require 's3/extensions'
12 | require_library_or_gem 'builder' unless defined? Builder
13 | require_library_or_gem 'mime/types', 'mime-types' unless defined? MIME::Types
14 |
15 | require 's3/base'
16 | require 's3/version'
17 | require 's3/parsing'
18 | require 's3/acl'
19 | require 's3/logging'
20 | require 's3/bittorrent'
21 | require 's3/service'
22 | require 's3/owner'
23 | require 's3/bucket'
24 | require 's3/object'
25 | require 's3/error'
26 | require 's3/exceptions'
27 | require 's3/connection'
28 | require 's3/authentication'
29 | require 's3/response'
30 |
31 | module AWS
32 | module S3
33 | UNSAFE_URI = /[^-_.!~*'()a-zA-Z\d;\/?:@&=$,\[\]]/n
34 |
35 | def self.escape_uri(path)
36 | URI.escape(path.to_s, UNSAFE_URI)
37 | end
38 |
39 | def self.escape_uri_component(path)
40 | escaped = escape_uri(path)
41 | escaped.gsub!(/=/, '%3D')
42 | escaped.gsub!(/&/, '%26')
43 | escaped.gsub!(/;/, '%3B')
44 | escaped
45 | end
46 |
47 | Base.class_eval do
48 | include AWS::S3::Connection::Management
49 | end
50 |
51 | Bucket.class_eval do
52 | include AWS::S3::Logging::Management
53 | include AWS::S3::ACL::Bucket
54 | end
55 |
56 | S3Object.class_eval do
57 | include AWS::S3::ACL::S3Object
58 | include AWS::S3::BitTorrent
59 | end
60 | end
61 | end
62 |
63 |
64 | require_library_or_gem 'xmlsimple', 'xml-simple' unless defined? XmlSimple
65 | # If libxml is installed, we use the FasterXmlSimple library, that provides most of the functionality of XmlSimple
66 | # except it uses the xml/libxml library for xml parsing (rather than REXML). If libxml isn't installed, we just fall back on
67 | # XmlSimple.
68 | AWS::S3::Parsing.parser =
69 | begin
70 | require_library_or_gem 'xml/libxml'
71 | # Older version of libxml aren't stable (bus error when requesting attributes that don't exist) so we
72 | # have to use a version greater than '0.3.8.2'.
73 | raise LoadError unless XML::Parser::VERSION > '0.3.8.2'
74 | $:.push(File.join(File.dirname(__FILE__), '..', '..', 'support', 'faster-xml-simple', 'lib'))
75 | require_library_or_gem 'faster_xml_simple'
76 | FasterXmlSimple
77 | rescue LoadError
78 | XmlSimple
79 | end
80 |
--------------------------------------------------------------------------------
/test/remote/logging_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/test_helper'
2 |
3 | class RemoteLoggingTest < Test::Unit::TestCase
4 | def setup
5 | establish_real_connection
6 | end
7 |
8 | def teardown
9 | disconnect!
10 | end
11 |
12 | def test_logging
13 | Bucket.create(TEST_BUCKET) # Clear out any custom grants
14 |
15 | # Confirm that logging is not enabled on the test bucket
16 |
17 | assert !Bucket.logging_enabled_for?(TEST_BUCKET)
18 | assert !Bucket.find(TEST_BUCKET).logging_enabled?
19 |
20 | assert_equal [], Bucket.logs_for(TEST_BUCKET)
21 |
22 | # Confirm the current bucket doesn't have logging grants
23 |
24 | policy = Bucket.acl(TEST_BUCKET)
25 | assert !policy.grants.include?(:logging_read_acp)
26 | assert !policy.grants.include?(:logging_write)
27 |
28 | # Confirm that we can enable logging
29 |
30 | assert_nothing_raised do
31 | Bucket.enable_logging_for TEST_BUCKET
32 | end
33 |
34 | # Confirm enabling logging worked
35 |
36 | assert Service.response.success?
37 |
38 | assert Bucket.logging_enabled_for?(TEST_BUCKET)
39 | assert Bucket.find(TEST_BUCKET).logging_enabled?
40 |
41 | # Confirm the appropriate grants were added
42 |
43 | policy = Bucket.acl(TEST_BUCKET)
44 | assert policy.grants.include?(:logging_read_acp)
45 | assert policy.grants.include?(:logging_write)
46 |
47 | # Confirm logging status used defaults
48 |
49 | logging_status = Bucket.logging_status_for TEST_BUCKET
50 | assert_equal TEST_BUCKET, logging_status.target_bucket
51 | assert_equal 'log-', logging_status.target_prefix
52 |
53 | # Confirm we can update the logging status
54 |
55 | logging_status.target_prefix = 'access-log-'
56 |
57 | assert_nothing_raised do
58 | Bucket.logging_status_for TEST_BUCKET, logging_status
59 | end
60 |
61 | assert Service.response.success?
62 |
63 | logging_status = Bucket.logging_status_for TEST_BUCKET
64 | assert_equal 'access-log-', logging_status.target_prefix
65 |
66 | # Confirm we can make a request for the bucket's logs
67 |
68 | assert_nothing_raised do
69 | Bucket.logs_for TEST_BUCKET
70 | end
71 |
72 | # Confirm we can disable logging
73 |
74 | assert_nothing_raised do
75 | Bucket.disable_logging_for(TEST_BUCKET)
76 | end
77 |
78 | assert Service.response.success?
79 |
80 | assert !Bucket.logging_enabled_for?(TEST_BUCKET)
81 | end
82 | end
--------------------------------------------------------------------------------
/test/error_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/test_helper'
2 |
3 | class ErrorTest < Test::Unit::TestCase
4 | def setup
5 | @container = AWS::S3
6 | @error = Error.new(Parsing::XmlParser.new(Fixtures::Errors.access_denied))
7 | @container.send(:remove_const, :NotImplemented) if @container.const_defined?(:NotImplemented)
8 | end
9 |
10 | def test_error_class_is_automatically_generated
11 | assert !@container.const_defined?('NotImplemented')
12 | error = Error.new(Parsing::XmlParser.new(Fixtures::Errors.not_implemented))
13 | assert @container.const_defined?('NotImplemented')
14 | end
15 |
16 | def test_error_contains_attributes
17 | assert_equal 'Access Denied', @error.message
18 | end
19 |
20 | def test_error_is_raisable_as_exception
21 | assert_raises(@container::AccessDenied) do
22 | @error.raise
23 | end
24 | end
25 |
26 | def test_error_message_is_passed_along_to_exception
27 | @error.raise
28 | rescue @container::AccessDenied => e
29 | assert_equal 'Access Denied', e.message
30 | end
31 |
32 | def test_response_is_passed_along_to_exception
33 | response = Error::Response.new(FakeResponse.new(:code => 409, :body => Fixtures::Errors.access_denied))
34 | response.error.raise
35 | rescue @container::ResponseError => e
36 | assert e.response
37 | assert_kind_of Error::Response, e.response
38 | assert_equal response.error, e.response.error
39 | end
40 |
41 | def test_exception_class_clash
42 | assert !@container.const_defined?(:NotImplemented)
43 | # Create a class that does not inherit from exception that has the same name as the class
44 | # the Error instance is about to attempt to find or create
45 | @container.const_set(:NotImplemented, Class.new)
46 | assert @container.const_defined?(:NotImplemented)
47 |
48 | assert_raises(ExceptionClassClash) do
49 | Error.new(Parsing::XmlParser.new(Fixtures::Errors.not_implemented))
50 | end
51 | end
52 |
53 | def test_error_response_handles_attributes_with_no_value
54 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Errors.error_with_no_message, :code => 500})
55 |
56 | begin
57 | Bucket.create('foo', 'invalid-argument' => 'bad juju')
58 | rescue ResponseError => error
59 | end
60 |
61 | assert_nothing_raised do
62 | error.response.error.message
63 | end
64 | assert_nil error.response.error.message
65 |
66 | assert_raises(NoMethodError) do
67 | error.response.error.non_existant_method
68 | end
69 | end
70 | end
--------------------------------------------------------------------------------
/test/response_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/test_helper'
2 | class BaseResponseTest < Test::Unit::TestCase
3 | def setup
4 | @headers = {'content-type' => 'text/plain', 'date' => Time.now}
5 | @response = FakeResponse.new()
6 | @base_response = Base::Response.new(@response)
7 | end
8 |
9 | def test_status_predicates
10 | response = Proc.new {|code| Base::Response.new(FakeResponse.new(:code => code))}
11 | assert response[200].success?
12 | assert response[300].redirect?
13 | assert response[400].client_error?
14 | assert response[500].server_error?
15 | end
16 |
17 | def test_headers_passed_along_from_original_response
18 | assert_equal @response.headers, @base_response.headers
19 | assert_equal @response['date'], @base_response['date']
20 | original_headers, new_headers = {}, {}
21 | @response.headers.each {|k,v| original_headers[k] = v}
22 | @base_response.each {|k,v| new_headers[k] = v}
23 | assert_equal original_headers, new_headers
24 | end
25 | end
26 |
27 | class ErrorResponseTest < Test::Unit::TestCase
28 | def test_error_responses_are_always_in_error
29 | assert Error::Response.new(FakeResponse.new).error?
30 | assert Error::Response.new(FakeResponse.new(:code => 200)).error?
31 | assert Error::Response.new(FakeResponse.new(:headers => {'content-type' => 'text/plain'})).error?
32 | end
33 | end
34 |
35 | class S3ObjectResponseTest < Test::Unit::TestCase
36 | def test_etag_extracted
37 | mock_connection_for(S3Object, :returns => {:headers => {"etag" => %("acbd18db4cc2f85cedef654fccc4a4d8")}}).once
38 | object_response = S3Object.create('name_does_not_matter', 'data does not matter', 'bucket does not matter')
39 | assert_equal "acbd18db4cc2f85cedef654fccc4a4d8", object_response.etag
40 | end
41 | end
42 |
43 | class ResponseClassFinderTest < Test::Unit::TestCase
44 | class CampfireBucket < Bucket
45 | end
46 |
47 | class BabyBase < Base
48 | end
49 |
50 | def test_on_base
51 | assert_equal Base::Response, FindResponseClass.for(Base)
52 | assert_equal Base::Response, FindResponseClass.for(AWS::S3::Base)
53 |
54 | end
55 |
56 | def test_on_subclass_with_corresponding_response_class
57 | assert_equal Bucket::Response, FindResponseClass.for(Bucket)
58 | assert_equal Bucket::Response, FindResponseClass.for(AWS::S3::Bucket)
59 | end
60 |
61 | def test_on_subclass_with_intermediary_parent_that_has_corresponding_response_class
62 | assert_equal Bucket::Response, FindResponseClass.for(CampfireBucket)
63 | end
64 |
65 | def test_on_subclass_with_no_corresponding_response_class_and_no_intermediary_parent
66 | assert_equal Base::Response, FindResponseClass.for(BabyBase)
67 | end
68 | end
--------------------------------------------------------------------------------
/lib/aws/s3/error.rb:
--------------------------------------------------------------------------------
1 | module AWS
2 | module S3
3 | # 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
4 | # specific to the error. All exception that are raised as a result of a request returning an error response inherit from the
5 | # ResponseError exception. So should you choose to rescue any such exception, you can simple rescue ResponseError.
6 | #
7 | # 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
8 | # errors listed at http://docs.amazonwebservices.com/AmazonS3/2006-03-01/ErrorCodeList.html):
9 | #
10 | # begin
11 | # Bucket.delete('jukebox')
12 | # rescue ResponseError => error
13 | # # ...
14 | # end
15 | #
16 | # Once you've captured the exception, you can extract the error message from S3, as well as the full error response, which includes
17 | # things like the HTTP response code:
18 | #
19 | # error
20 | # # => #
21 | # error.message
22 | # # => "The bucket you tried to delete is not empty"
23 | # error.response.code
24 | # # => 409
25 | #
26 | # You could use this information to redisplay the error in a way you see fit, or just to log the error and continue on.
27 | class Error
28 | #:stopdoc:
29 | attr_accessor :response
30 | def initialize(error, response = nil)
31 | @error = error
32 | @response = response
33 | @container = AWS::S3
34 | find_or_create_exception!
35 | end
36 |
37 | def raise
38 | Kernel.raise exception.new(message, response)
39 | end
40 |
41 | private
42 | attr_reader :error, :exception, :container
43 |
44 | def find_or_create_exception!
45 | @exception = container.const_defined?(code) ? find_exception : create_exception
46 | end
47 |
48 | def find_exception
49 | exception_class = container.const_get(code)
50 | Kernel.raise ExceptionClassClash.new(exception_class) unless exception_class.ancestors.include?(ResponseError)
51 | exception_class
52 | end
53 |
54 | def create_exception
55 | container.const_set(code, Class.new(ResponseError))
56 | end
57 |
58 | def method_missing(method, *args, &block)
59 | # We actually want nil if the attribute is nil. So we use has_key? rather than [] + ||.
60 | if error.has_key?(method.to_s)
61 | error[method.to_s]
62 | else
63 | super
64 | end
65 | end
66 | end
67 | end
68 | end
69 | #:startdoc:
--------------------------------------------------------------------------------
/test/fixtures.rb:
--------------------------------------------------------------------------------
1 | require 'yaml'
2 |
3 | module AWS
4 | module S3
5 | # When this file is loaded, for each fixture file, a module is created within the Fixtures module
6 | # with the same name as the fixture file. For each fixture in that fixture file, a singleton method is
7 | # added to the module with the name of the given fixture, returning the value of the fixture.
8 | #
9 | # For example:
10 | #
11 | # A fixture in buckets.yml named empty_bucket_list with value hi!
12 | # would be made available like so:
13 | #
14 | # Fixtures::Buckets.empty_bucket_list
15 | # => "hi!"
16 | #
17 | # Alternatively you can treat the fixture module like a hash
18 | #
19 | # Fixtures::Buckets[:empty_bucket_list]
20 | # => "hi!"
21 | #
22 | # You can find out all available fixtures by calling
23 | #
24 | # Fixtures.fixtures
25 | # => ["Buckets"]
26 | #
27 | # And all the fixtures contained in a given fixture by calling
28 | #
29 | # Fixtures::Buckets.fixtures
30 | # => ["bucket_list_with_more_than_one_bucket", "bucket_list_with_one_bucket", "empty_bucket_list"]
31 | module Fixtures
32 | class << self
33 | def create_fixtures
34 | files.each do |file|
35 | create_fixture_for(file)
36 | end
37 | end
38 |
39 | def create_fixture_for(file)
40 | fixtures = YAML.load_file(path(file))
41 | fixture_module = Module.new
42 |
43 | fixtures.each do |name, value|
44 | fixture_module.module_eval(<<-EVAL, __FILE__, __LINE__)
45 | def #{name}
46 | #{value.inspect}
47 | end
48 | module_function :#{name}
49 | EVAL
50 | end
51 |
52 | fixture_module.module_eval(<<-EVAL, __FILE__, __LINE__)
53 | module_function
54 |
55 | def fixtures
56 | #{fixtures.keys.sort.inspect}
57 | end
58 |
59 | def [](name)
60 | send(name) if fixtures.include?(name.to_s)
61 | end
62 | EVAL
63 |
64 | const_set(module_name(file), fixture_module)
65 | end
66 |
67 | def fixtures
68 | constants.sort
69 | end
70 |
71 | private
72 |
73 | def files
74 | Dir.glob(File.dirname(__FILE__) + '/fixtures/*.yml').map {|fixture| File.basename(fixture)}
75 | end
76 |
77 | def module_name(file_name)
78 | File.basename(file_name, '.*').capitalize
79 | end
80 |
81 | def path(file_name)
82 | File.join(File.dirname(__FILE__), 'fixtures', file_name)
83 | end
84 | end
85 |
86 | create_fixtures
87 | end
88 | end
89 | end
--------------------------------------------------------------------------------
/test/logging_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/test_helper'
2 |
3 | class LoggingStatusReadingTest < Test::Unit::TestCase
4 |
5 | def setup
6 | @disabled = logging_status(:logging_disabled)
7 | @enabled = logging_status(:logging_enabled)
8 | @new_status = Logging::Status.new('target_bucket' => 'foo', 'target_prefix' => 'access-log-')
9 | end
10 |
11 | def test_logging_enabled?
12 | assert !@disabled.logging_enabled?
13 | assert !@new_status.logging_enabled?
14 | assert @enabled.logging_enabled?
15 | end
16 |
17 | def test_passing_in_prefix_and_bucket
18 | assert_equal 'foo', @new_status.target_bucket
19 | assert_equal 'access-log-', @new_status.target_prefix
20 | assert !@new_status.logging_enabled?
21 | end
22 |
23 | private
24 | def logging_status(fixture)
25 | Logging::Status.new(Parsing::XmlParser.new(Fixtures::Logging[fixture.to_s]))
26 | end
27 | end
28 |
29 | class LoggingStatusWritingTest < LoggingStatusReadingTest
30 | def setup
31 | super
32 | @disabled = Logging::Status.new(Parsing::XmlParser.new(@disabled.to_xml))
33 | @enabled = Logging::Status.new(Parsing::XmlParser.new(@enabled.to_xml))
34 | end
35 | end
36 |
37 | class LogTest < Test::Unit::TestCase
38 | def test_value_converted_to_log_lines
39 | log_object = S3Object.new
40 | log_object.value = Fixtures::Logs.simple_log.join
41 | log = Logging::Log.new(log_object)
42 | assert_nothing_raised do
43 | log.lines
44 | end
45 |
46 | assert_equal 2, log.lines.size
47 | assert_kind_of Logging::Log::Line, log.lines.first
48 | assert_equal 'marcel', log.lines.first.bucket
49 | end
50 | end
51 |
52 | class LogLineTest < Test::Unit::TestCase
53 | def setup
54 | @line = Logging::Log::Line.new(Fixtures::Loglines.bucket_get)
55 | end
56 |
57 | def test_field_accessors
58 | expected_results = {
59 | :owner => Owner.new('id' => 'bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1'),
60 | :bucket => 'marcel',
61 | :time => Time.parse('Nov 14 2006 06:36:48 +0000'),
62 | :remote_ip => '67.165.183.125',
63 | :request_id => '8B5297D428A05432',
64 | :requestor => Owner.new('id' => 'bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1'),
65 | :operation => 'REST.GET.BUCKET',
66 | :key => nil,
67 | :request_uri => 'GET /marcel HTTP/1.1',
68 | :http_status => 200,
69 | :error_code => nil,
70 | :bytes_sent => 4534,
71 | :object_size => nil,
72 | :total_time => 398,
73 | :turn_around_time => 395,
74 | :referrer => nil,
75 | :user_agent => nil
76 | }
77 |
78 | expected_results.each do |field, expected|
79 | assert_equal expected, @line.send(field)
80 | end
81 |
82 | assert_equal expected_results, @line.attributes
83 | end
84 |
85 | def test_user_agent
86 | line = Logging::Log::Line.new(Fixtures::Loglines.browser_get)
87 | assert_equal 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0', line.user_agent
88 | end
89 | end
--------------------------------------------------------------------------------
/lib/aws/s3/parsing.rb:
--------------------------------------------------------------------------------
1 | #:stopdoc:
2 | module AWS
3 | module S3
4 | module Parsing
5 | class << self
6 | def parser=(parsing_library)
7 | XmlParser.parsing_library = parsing_library
8 | end
9 |
10 | def parser
11 | XmlParser.parsing_library
12 | end
13 | end
14 |
15 | module Typecasting
16 | def typecast(object)
17 | case object
18 | when Hash
19 | typecast_hash(object)
20 | when Array
21 | object.map {|element| typecast(element)}
22 | when String
23 | CoercibleString.coerce(object)
24 | else
25 | object
26 | end
27 | end
28 |
29 | def typecast_hash(hash)
30 | if content = hash['__content__']
31 | typecast(content)
32 | else
33 | keys = hash.keys.map {|key| key.underscore}
34 | values = hash.values.map {|value| typecast(value)}
35 | keys.inject({}) do |new_hash, key|
36 | new_hash[key] = values.slice!(0)
37 | new_hash
38 | end
39 | end
40 | end
41 | end
42 |
43 | class XmlParser < Hash
44 | include Typecasting
45 |
46 | class << self
47 | attr_accessor :parsing_library
48 | end
49 |
50 | attr_reader :body, :xml_in, :root
51 |
52 | def initialize(body)
53 | @body = body
54 | unless body.strip.empty?
55 | parse
56 | set_root
57 | typecast_xml_in
58 | end
59 | end
60 |
61 | private
62 |
63 | def parse
64 | @xml_in = self.class.parsing_library.xml_in(body, parsing_options)
65 | end
66 |
67 | def parsing_options
68 | {
69 | # Includes the enclosing tag as the top level key
70 | 'keeproot' => true,
71 | # Makes tag value available via the '__content__' key
72 | 'contentkey' => '__content__',
73 | # Always parse tags into a hash, even when there are no attributes
74 | # (unless there is also no value, in which case it is nil)
75 | 'forcecontent' => true,
76 | # If a tag is empty, makes its content nil
77 | 'suppressempty' => nil,
78 | # Force nested elements to be put into an array, even if there is only one of them
79 | 'forcearray' => ['Contents', 'Bucket', 'Grant']
80 | }
81 | end
82 |
83 | def set_root
84 | @root = @xml_in.keys.first.underscore
85 | end
86 |
87 | def typecast_xml_in
88 | typecast_xml = {}
89 | @xml_in.dup.each do |key, value| # Some typecasting is destructive so we dup
90 | typecast_xml[key.underscore] = typecast(value)
91 | end
92 | # An empty body will try to update with a string so only update if the result is a hash
93 | update(typecast_xml[root]) if typecast_xml[root].is_a?(Hash)
94 | end
95 | end
96 | end
97 | end
98 | end
99 | #:startdoc:
--------------------------------------------------------------------------------
/test/bucket_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/test_helper'
2 |
3 | class BucketTest < Test::Unit::TestCase
4 | def test_bucket_name_validation
5 | valid_names = %w(123 joe step-one step_two step3 step_4 step-5 step.six)
6 | invalid_names = ['12', 'jo', 'kevin spacey', 'larry@wall', '', 'a' * 256]
7 | validate_name = Proc.new {|name| Bucket.send(:validate_name!, name)}
8 | valid_names.each do |valid_name|
9 | assert_nothing_raised { validate_name[valid_name] }
10 | end
11 |
12 | invalid_names.each do |invalid_name|
13 | assert_raises(InvalidBucketName) { validate_name[invalid_name] }
14 | end
15 | end
16 |
17 | def test_empty_bucket
18 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.empty_bucket, :code => 200})
19 | bucket = Bucket.find('marcel_molina')
20 | assert bucket.empty?
21 | end
22 |
23 | def test_bucket_with_one_file
24 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.bucket_with_one_key, :code => 200})
25 | bucket = Bucket.find('marcel_molina')
26 | assert !bucket.empty?
27 | assert_equal 1, bucket.size
28 | assert_equal %w(tongue_overload.jpg), bucket.objects.map {|object| object.key}
29 | assert bucket['tongue_overload.jpg']
30 | end
31 |
32 | def test_bucket_with_more_than_one_file
33 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.bucket_with_more_than_one_key, :code => 200})
34 | bucket = Bucket.find('marcel_molina')
35 | assert !bucket.empty?
36 | assert_equal 2, bucket.size
37 | assert_equal %w(beluga_baby.jpg tongue_overload.jpg), bucket.objects.map {|object| object.key}.sort
38 | assert bucket['tongue_overload.jpg']
39 | end
40 |
41 | def test_bucket_path
42 | assert_equal '/bucket_name?max-keys=2', Bucket.send(:path, 'bucket_name', :max_keys => 2)
43 | assert_equal '/bucket_name', Bucket.send(:path, 'bucket_name', {})
44 | end
45 |
46 | def test_should_not_be_truncated
47 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.bucket_with_more_than_one_key, :code => 200})
48 | bucket = Bucket.find('marcel_molina')
49 | assert !bucket.is_truncated
50 | end
51 |
52 | def test_should_be_truncated
53 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.truncated_bucket_with_more_than_one_key, :code => 200})
54 | bucket = Bucket.find('marcel_molina')
55 | assert bucket.is_truncated
56 | end
57 |
58 | def test_bucket_name_should_have_leading_slash_prepended_only_once_when_forcing_a_delete
59 | # References bug: http://rubyforge.org/tracker/index.php?func=detail&aid=19158&group_id=2409&atid=9356
60 | bucket_name = 'foo'
61 | expected_bucket_path = "/#{bucket_name}"
62 |
63 | mock_bucket = flexmock('Mock bucket') do |mock|
64 | mock.should_receive(:delete_all).once
65 | end
66 | mock_response = flexmock('Mock delete response') do |mock|
67 | mock.should_receive(:success?).once
68 | end
69 |
70 | flexmock(Bucket).should_receive(:find).with(bucket_name).once.and_return(mock_bucket)
71 | flexmock(Base).should_receive(:delete).with(expected_bucket_path).once.and_return(mock_response)
72 | Bucket.delete(bucket_name, :force => true)
73 | end
74 | end
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | require 'test/unit'
2 | $:.unshift File.dirname(__FILE__) + '/../lib'
3 | require 'aws/s3'
4 | require File.dirname(__FILE__) + '/mocks/fake_response'
5 | require File.dirname(__FILE__) + '/fixtures'
6 | begin
7 | require_library_or_gem 'ruby-debug'
8 | rescue LoadError
9 | end
10 | require_library_or_gem 'flexmock'
11 | require_library_or_gem 'flexmock/test_unit'
12 |
13 |
14 | # Data copied from http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAuthentication.html
15 | module AmazonDocExampleData
16 | module Example1
17 | module_function
18 |
19 | def request
20 | request = Net::HTTP::Put.new('/quotes/nelson')
21 | request['Content-Md5'] = 'c8fdb181845a4ca6b8fec737b3581d76'
22 | request['Content-Type'] = 'text/html'
23 | request['Date'] = 'Thu, 17 Nov 2005 18:49:58 GMT'
24 | request['X-Amz-Meta-Author'] = 'foo@bar.com'
25 | request['X-Amz-Magic'] = 'abracadabra'
26 | request
27 | end
28 |
29 | def canonical_string
30 | "PUT\nc8fdb181845a4ca6b8fec737b3581d76\ntext/html\nThu, 17 Nov 2005 18:49:58 GMT\nx-amz-magic:abracadabra\nx-amz-meta-author:foo@bar.com\n/quotes/nelson"
31 | end
32 |
33 | def access_key_id
34 | '44CF9590006BF252F707'
35 | end
36 |
37 | def secret_access_key
38 | 'OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV'
39 | end
40 |
41 | def signature
42 | 'jZNOcbfWmD/A/f3hSvVzXZjM2HU='
43 | end
44 |
45 | def authorization_header
46 | 'AWS 44CF9590006BF252F707:jZNOcbfWmD/A/f3hSvVzXZjM2HU='
47 | end
48 | end
49 |
50 | module Example3
51 | module_function
52 |
53 | def request
54 | request = Net::HTTP::Get.new('/quotes/nelson')
55 | request['Date'] = date
56 | request
57 | end
58 |
59 | def date
60 | 'Thu Mar 9 01:24:20 CST 2006'
61 | end
62 |
63 | def access_key_id
64 | Example1.access_key_id
65 | end
66 |
67 | def secret_access_key
68 | Example1.secret_access_key
69 | end
70 |
71 | def expires
72 | 1141889120
73 | end
74 |
75 | def query_string
76 | 'AWSAccessKeyId=44CF9590006BF252F707&Expires=1141889120&Signature=vjbyPxybdZaNmGa%2ByT272YEAiv4%3D'
77 | end
78 |
79 | def canonical_string
80 | "GET\n\n\n1141889120\n/quotes/nelson"
81 | end
82 |
83 | end
84 | end
85 |
86 | class Test::Unit::TestCase
87 | include AWS::S3
88 |
89 | def sample_proxy_settings
90 | {:host => 'http://google.com', :port => 8080, :user => 'marcel', :password => 'secret'}
91 | end
92 |
93 | def mock_connection_for(klass, options = {})
94 | data = options[:returns]
95 | return_values = case data
96 | when Hash
97 | FakeResponse.new(data)
98 | when Array
99 | data.map {|hash| FakeResponse.new(hash)}
100 | else
101 | abort "Response data for mock connection must be a Hash or an Array. Was #{data.inspect}."
102 | end
103 |
104 | connection = flexmock('Mock connection') do |mock|
105 | mock.should_receive(:request).and_return(*return_values).at_least.once
106 | end
107 |
108 | flexmock(klass).should_receive(:connection).and_return(connection)
109 | end
110 | end
--------------------------------------------------------------------------------
/test/remote/acl_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/test_helper'
2 |
3 | class RemoteACLTest < Test::Unit::TestCase
4 |
5 | def setup
6 | establish_real_connection
7 | end
8 |
9 | def teardown
10 | disconnect!
11 | end
12 |
13 | def test_acl
14 | Bucket.create(TEST_BUCKET) # Wipe out the existing bucket's ACL
15 |
16 | bucket_policy = Bucket.acl(TEST_BUCKET)
17 | assert_equal 1, bucket_policy.grants.size
18 | assert !bucket_policy.grants.include?(:public_read_acp)
19 |
20 | bucket_policy.grants << ACL::Grant.grant(:public_read_acp)
21 |
22 | assert_nothing_raised do
23 | Bucket.acl(TEST_BUCKET, bucket_policy)
24 | end
25 |
26 | bucket = Bucket.find(TEST_BUCKET)
27 | assert bucket.acl.grants.include?(:public_read_acp)
28 |
29 | bucket.acl.grants.pop # Get rid of the newly added grant
30 |
31 | assert !bucket.acl.grants.include?(:public_read_acp)
32 | bucket.acl(bucket.acl) # Update its acl
33 | assert Service.response.success?
34 |
35 | bucket_policy = Bucket.acl(TEST_BUCKET)
36 | assert_equal 1, bucket_policy.grants.size
37 | assert !bucket_policy.grants.include?(:public_read_acp)
38 |
39 | S3Object.store('testing-acls', 'the test data', TEST_BUCKET, :content_type => 'text/plain')
40 | acl = S3Object.acl('testing-acls', TEST_BUCKET)
41 |
42 | # Confirm object has the default policy
43 |
44 | assert !acl.grants.empty?
45 | assert_equal 1, acl.grants.size
46 | grant = acl.grants.first
47 |
48 | assert_equal 'FULL_CONTROL', grant.permission
49 |
50 | grantee = grant.grantee
51 |
52 | assert acl.owner.id
53 | assert acl.owner.display_name
54 | assert grantee.id
55 | assert grantee.display_name
56 |
57 | assert_equal acl.owner.id, grantee.id
58 | assert_equal acl.owner.display_name, grantee.display_name
59 |
60 | assert_equal Owner.current, acl.owner
61 |
62 |
63 | # Manually add read access to an Amazon customer by email address
64 |
65 | new_grant = ACL::Grant.new
66 | new_grant.permission = 'READ'
67 | new_grant_grantee = ACL::Grantee.new
68 | new_grant_grantee.email_address = [['amazon', 'marcelmolina'].join('@'), 'com'].join('.')
69 | new_grant.grantee = new_grant_grantee
70 | acl.grants << new_grant
71 |
72 | assert_nothing_raised do
73 | S3Object.acl('testing-acls', TEST_BUCKET, acl)
74 | end
75 |
76 | # Confirm the acl was updated successfully
77 |
78 | assert Service.response.success?
79 |
80 | acl = S3Object.acl('testing-acls', TEST_BUCKET)
81 | assert !acl.grants.empty?
82 | assert_equal 2, acl.grants.size
83 | new_grant = acl.grants.last
84 | assert_equal 'READ', new_grant.permission
85 |
86 | # Confirm instance method has same result
87 |
88 | assert_equal acl.grants, S3Object.find('testing-acls', TEST_BUCKET).acl.grants
89 |
90 | # Get rid of the grant we just added
91 |
92 | acl.grants.pop
93 |
94 | # Confirm acl class method sees that the bucket option is being used to put a new acl
95 |
96 | assert_nothing_raised do
97 | TestS3Object.acl('testing-acls', acl)
98 | end
99 |
100 | assert Service.response.success?
101 |
102 | acl = TestS3Object.acl('testing-acls')
103 |
104 | # Confirm added grant was removed from the policy
105 |
106 | assert !acl.grants.empty?
107 | assert_equal 1, acl.grants.size
108 | grant = acl.grants.first
109 | assert_equal 'FULL_CONTROL', grant.permission
110 |
111 | assert_nothing_raised do
112 | S3Object.delete('testing-acls', TEST_BUCKET)
113 | end
114 |
115 | assert Service.response.success?
116 | end
117 | end
--------------------------------------------------------------------------------
/test/remote/bucket_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/test_helper'
2 |
3 | class RemoteBucketTest < Test::Unit::TestCase
4 |
5 | def setup
6 | establish_real_connection
7 | assert Bucket.find(TEST_BUCKET).delete_all
8 | end
9 |
10 | def teardown
11 | disconnect!
12 | end
13 |
14 | def test_bucket
15 | # Fetch the testing bucket
16 |
17 | bucket = nil
18 | assert_nothing_raised do
19 | bucket = Bucket.find(TEST_BUCKET)
20 | end
21 |
22 | assert bucket
23 |
24 | # Confirm we can fetch the bucket implicitly
25 |
26 | bucket = nil
27 | assert_nothing_raised do
28 | bucket = TestBucket.find
29 | end
30 |
31 | assert bucket
32 |
33 | # Confirm the bucket has the right name
34 |
35 | assert_equal TEST_BUCKET, bucket.name
36 |
37 | assert bucket.empty?
38 | assert_equal 0, bucket.size
39 |
40 | # Add some files to the bucket
41 |
42 | assert_nothing_raised do
43 | %w(a m z).each do |file_name|
44 | S3Object.create(file_name, file_name, bucket.name, :content_type => 'text/plain')
45 | end
46 | end
47 |
48 | # Confirm that we can reload the objects
49 |
50 | assert_nothing_raised do
51 | bucket.objects(:reload)
52 | end
53 |
54 | assert !bucket.empty?
55 | assert_equal 3, bucket.size
56 |
57 | assert_nothing_raised do
58 | bucket.objects(:marker => 'm')
59 | end
60 |
61 | assert_equal 1, bucket.size
62 | assert bucket['z']
63 |
64 | assert_equal 1, Bucket.find(TEST_BUCKET, :max_keys => 1).size
65 |
66 | assert_nothing_raised do
67 | bucket.objects(:reload)
68 | end
69 |
70 | assert_equal 3, bucket.size
71 |
72 | # Ensure the reloaded buckets have been repatriated
73 |
74 | assert_equal bucket, bucket.objects.first.bucket
75 |
76 | # Confirm that we can delete one of the objects and it will be removed
77 |
78 | object_to_be_deleted = bucket.objects.last
79 | assert_nothing_raised do
80 | object_to_be_deleted.delete
81 | end
82 |
83 | assert !bucket.objects.include?(object_to_be_deleted)
84 |
85 | # Confirm that we can add an object
86 |
87 | object = bucket.new_object(:value => 'hello')
88 |
89 | assert_raises(NoKeySpecified) do
90 | object.store
91 | end
92 |
93 | object.key = 'abc'
94 | assert_nothing_raised do
95 | object.store
96 | end
97 |
98 | assert bucket.objects.include?(object)
99 |
100 | # Confirm that the object is still there after reloading its objects
101 |
102 | assert_nothing_raised do
103 | bucket.objects(:reload)
104 | end
105 | assert bucket.objects.include?(object)
106 |
107 | # Check that TestBucket has the same objects fetched implicitly
108 |
109 | assert_equal bucket.objects, TestBucket.objects
110 |
111 | # Empty out bucket
112 |
113 | assert_nothing_raised do
114 | bucket.delete_all
115 | end
116 |
117 | assert bucket.empty?
118 |
119 | bucket = nil
120 | assert_nothing_raised do
121 | bucket = Bucket.find(TEST_BUCKET)
122 | end
123 |
124 | assert bucket.empty?
125 | end
126 |
127 | def test_bucket_name_is_switched_with_options_when_bucket_is_implicit_and_options_are_passed
128 | Object.const_set(:ImplicitlyNamedBucket, Class.new(Bucket))
129 | ImplicitlyNamedBucket.current_bucket = TEST_BUCKET
130 | assert ImplicitlyNamedBucket.objects.empty?
131 |
132 | %w(a b c).each {|key| S3Object.store(key, 'value does not matter', TEST_BUCKET)}
133 |
134 | assert_equal 3, ImplicitlyNamedBucket.objects.size
135 |
136 | objects = nil
137 | assert_nothing_raised do
138 | objects = ImplicitlyNamedBucket.objects(:max_keys => 1)
139 | end
140 |
141 | assert objects
142 | assert_equal 1, objects.size
143 | ensure
144 | %w(a b c).each {|key| S3Object.delete(key, TEST_BUCKET)}
145 | end
146 | end
--------------------------------------------------------------------------------
/test/base_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/test_helper'
2 |
3 | class BaseTest < Test::Unit::TestCase
4 | def test_connection_established
5 | assert_raises(NoConnectionEstablished) do
6 | Base.connection
7 | end
8 |
9 | Base.establish_connection!(:access_key_id => '123', :secret_access_key => 'abc')
10 | assert_kind_of Connection, Base.connection
11 |
12 | instance = Base.new
13 | assert_equal instance.send(:connection), Base.connection
14 | assert_equal instance.send(:http), Base.connection.http
15 | end
16 |
17 | def test_respond_with
18 | assert_equal Base::Response, Base.send(:response_class)
19 | Base.send(:respond_with, Bucket::Response) do
20 | assert_equal Bucket::Response, Base.send(:response_class)
21 | end
22 | assert_equal Base::Response, Base.send(:response_class)
23 | end
24 |
25 | def test_request_tries_again_when_encountering_an_internal_error
26 | mock_connection_for(Bucket, :returns => [
27 | # First request is an internal error
28 | {:body => Fixtures::Errors.internal_error, :code => 500, :error => true},
29 | # Second request is a success
30 | {:body => Fixtures::Buckets.empty_bucket, :code => 200}
31 | ])
32 | bucket = nil # Block scope hack
33 | assert_nothing_raised do
34 | bucket = Bucket.find('marcel')
35 | end
36 | # Don't call objects 'cause we don't want to make another request
37 | assert bucket.object_cache.empty?
38 | end
39 |
40 | def test_request_tries_up_to_three_times
41 | mock_connection_for(Bucket, :returns => [
42 | # First request is an internal error
43 | {:body => Fixtures::Errors.internal_error, :code => 500, :error => true},
44 | # Second request is also an internal error
45 | {:body => Fixtures::Errors.internal_error, :code => 500, :error => true},
46 | # Ditto third
47 | {:body => Fixtures::Errors.internal_error, :code => 500, :error => true},
48 | # Fourth works
49 | {:body => Fixtures::Buckets.empty_bucket, :code => 200}
50 | ])
51 | bucket = nil # Block scope hack
52 | assert_nothing_raised do
53 | bucket = Bucket.find('marcel')
54 | end
55 | # Don't call objects 'cause we don't want to make another request
56 | assert bucket.object_cache.empty?
57 | end
58 |
59 | def test_request_tries_again_three_times_and_gives_up
60 | mock_connection_for(Bucket, :returns => [
61 | # First request is an internal error
62 | {:body => Fixtures::Errors.internal_error, :code => 500, :error => true},
63 | # Second request is also an internal error
64 | {:body => Fixtures::Errors.internal_error, :code => 500, :error => true},
65 | # Ditto third
66 | {:body => Fixtures::Errors.internal_error, :code => 500, :error => true},
67 | # Ditto fourth
68 | {:body => Fixtures::Errors.internal_error, :code => 500, :error => true},
69 | ])
70 | assert_raises(InternalError) do
71 | Bucket.find('marcel')
72 | end
73 | end
74 | end
75 |
76 | class MultiConnectionsTest < Test::Unit::TestCase
77 | class ClassToTestSettingCurrentBucket < Base
78 | set_current_bucket_to 'foo'
79 | end
80 |
81 | def setup
82 | Base.send(:connections).clear
83 | end
84 |
85 | def test_default_connection_options_are_used_for_subsequent_connections
86 | assert !Base.connected?
87 |
88 | assert_raises(MissingAccessKey) do
89 | Base.establish_connection!
90 | end
91 |
92 | assert !Base.connected?
93 |
94 | assert_raises(NoConnectionEstablished) do
95 | Base.connection
96 | end
97 |
98 | assert_nothing_raised do
99 | Base.establish_connection!(:access_key_id => '123', :secret_access_key => 'abc')
100 | end
101 |
102 | assert Base.connected?
103 |
104 | assert_nothing_raised do
105 | Base.connection
106 | end
107 |
108 | # All subclasses are currently using the default connection
109 | assert_equal Base.connection, Bucket.connection
110 |
111 | # No need to pass in the required options. The default connection will supply them
112 | assert_nothing_raised do
113 | Bucket.establish_connection!(:server => 'foo.s3.amazonaws.com')
114 | end
115 |
116 | assert Base.connection != Bucket.connection
117 | assert_equal '123', Bucket.connection.access_key_id
118 | assert_equal 'foo', Bucket.connection.subdomain
119 | end
120 |
121 | def test_current_bucket
122 | Base.establish_connection!(:access_key_id => '123', :secret_access_key => 'abc')
123 | assert_raises(CurrentBucketNotSpecified) do
124 | Base.current_bucket
125 | end
126 |
127 | S3Object.establish_connection!(:server => 'foo-bucket.s3.amazonaws.com')
128 | assert_nothing_raised do
129 | assert_equal 'foo-bucket', S3Object.current_bucket
130 | end
131 | end
132 |
133 | def test_setting_the_current_bucket
134 | assert_equal 'foo', ClassToTestSettingCurrentBucket.current_bucket
135 | end
136 | end
137 |
--------------------------------------------------------------------------------
/test/fixtures/buckets.yml:
--------------------------------------------------------------------------------
1 | empty_bucket_list: >
2 |
3 |
4 | ab00c3106e091f8fe23154c85678cda66628adb330bc00f02cf4a1c36d76bc48
5 | amazon
6 |
7 |
8 |
9 |
10 |
11 | bucket_list_with_one_bucket: >
12 |
13 |
14 | ab00c3106e091f8fe23154c85678cda66628adb330bc00f02cf4a1c36d76bc48
15 | amazon
16 |
17 |
18 |
19 | marcel_molina
20 | 2006-10-04T15:58:38.000Z
21 |
22 |
23 |
24 |
25 |
26 | bucket_list_with_more_than_one_bucket: >
27 |
28 |
29 | ab00c3106e091f8fe23154c85678cda66628adb330bc00f02cf4a1c36d76bc48
30 | amazon
31 |
32 |
33 |
34 | marcel_molina
35 | 2006-10-04T15:58:38.000Z
36 |
37 |
38 | marcel_molina_jr
39 | 2006-10-04T16:01:30.000Z
40 |
41 |
42 |
43 |
44 | empty_bucket: >
45 |
46 | marcel_molina
47 |
48 |
49 | 1000
50 | false
51 |
52 |
53 | bucket_with_one_key: >
54 |
55 | marcel_molina
56 |
57 |
58 | 1000
59 | false
60 |
61 | tongue_overload.jpg
62 | 2006-10-05T02:42:22.000Z
63 | "f21f7c4e8ea6e34b268887b07d6da745"
64 | 60673
65 |
66 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1
67 | mmolina@onramp.net
68 |
69 | STANDARD
70 |
71 |
72 |
73 | bucket_with_more_than_one_key: >
74 |
75 | marcel_molina
76 |
77 |
78 | 1000
79 | false
80 |
81 | beluga_baby.jpg
82 | 2006-10-05T02:55:10.000Z
83 | "b2453d4a39a7387674a8c505112a2f0b"
84 | 35807
85 |
86 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1
87 | mmolina@onramp.net
88 |
89 | STANDARD
90 |
91 |
92 | tongue_overload.jpg
93 | 2006-10-05T02:42:22.000Z
94 | "f21f7c4e8ea6e34b268887b07d6da745"
95 | 60673
96 |
97 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1
98 | mmolina@onramp.net
99 |
100 | STANDARD
101 |
102 |
103 |
104 | truncated_bucket_with_more_than_one_key: >
105 |
106 | marcel_molina
107 |
108 |
109 | 2
110 | true
111 |
112 | beluga_baby.jpg
113 | 2006-10-05T02:55:10.000Z
114 | "b2453d4a39a7387674a8c505112a2f0b"
115 | 35807
116 |
117 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1
118 | mmolina@onramp.net
119 |
120 | STANDARD
121 |
122 |
123 | tongue_overload.jpg
124 | 2006-10-05T02:42:22.000Z
125 | "f21f7c4e8ea6e34b268887b07d6da745"
126 | 60673
127 |
128 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1
129 | mmolina@onramp.net
130 |
131 | STANDARD
132 |
133 |
134 |
--------------------------------------------------------------------------------
/test/authentication_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/test_helper'
2 |
3 | class HeaderAuthenticationTest < Test::Unit::TestCase
4 | def test_encoded_canonical
5 | signature = Authentication::Signature.new(request, key_id, secret)
6 | assert_equal AmazonDocExampleData::Example1.canonical_string, signature.send(:canonical_string)
7 | assert_equal AmazonDocExampleData::Example1.signature, signature.send(:encoded_canonical)
8 | end
9 |
10 | def test_authorization_header
11 | header = Authentication::Header.new(request, key_id, secret)
12 | assert_equal AmazonDocExampleData::Example1.canonical_string, header.send(:canonical_string)
13 | assert_equal AmazonDocExampleData::Example1.authorization_header, header
14 | end
15 |
16 | private
17 | def request; AmazonDocExampleData::Example1.request end
18 | def key_id ; AmazonDocExampleData::Example1.access_key_id end
19 | def secret ; AmazonDocExampleData::Example1.secret_access_key end
20 | end
21 |
22 | class QueryStringAuthenticationTest < Test::Unit::TestCase
23 | def test_query_string
24 | query_string = Authentication::QueryString.new(request, key_id, secret, :expires_in => 60)
25 | assert_equal AmazonDocExampleData::Example3.canonical_string, query_string.send(:canonical_string)
26 | assert_equal AmazonDocExampleData::Example3.query_string, query_string
27 | end
28 |
29 | def test_query_string_with_explicit_expiry
30 | query_string = Authentication::QueryString.new(request, key_id, secret, :expires => expires)
31 | assert_equal expires, query_string.send(:canonical_string).instance_variable_get(:@options)[:expires]
32 | assert_equal AmazonDocExampleData::Example3.query_string, query_string
33 | end
34 |
35 | def test_expires_in_is_coerced_to_being_an_integer_in_case_it_is_a_special_integer_proxy
36 | # References bug: http://rubyforge.org/tracker/index.php?func=detail&aid=17458&group_id=2409&atid=9356
37 | integer_proxy = Class.new do
38 | attr_reader :integer
39 | def initialize(integer)
40 | @integer = integer
41 | end
42 |
43 | def to_int
44 | integer
45 | end
46 | end
47 |
48 | actual_integer = 25
49 | query_string = Authentication::QueryString.new(request, key_id, secret, :expires_in => integer_proxy.new(actual_integer))
50 | assert_equal actual_integer, query_string.send(:expires_in)
51 | end
52 |
53 | private
54 | def request; AmazonDocExampleData::Example3.request end
55 | def key_id ; AmazonDocExampleData::Example3.access_key_id end
56 | def secret ; AmazonDocExampleData::Example3.secret_access_key end
57 | def expires; AmazonDocExampleData::Example3.expires end
58 | end
59 |
60 | class CanonicalStringTest < Test::Unit::TestCase
61 | def setup
62 | @request = Net::HTTP::Post.new('/test')
63 | @canonical_string = Authentication::CanonicalString.new(@request)
64 | end
65 |
66 | def test_path_does_not_include_query_string
67 | request = Net::HTTP::Get.new('/test/query/string?foo=bar&baz=quux')
68 | assert_equal '/test/query/string', Authentication::CanonicalString.new(request).send(:path)
69 |
70 | # Make sure things still work when there is *no* query string
71 | request = Net::HTTP::Get.new('/')
72 | assert_equal '/', Authentication::CanonicalString.new(request).send(:path)
73 | request = Net::HTTP::Get.new('/foo/bar')
74 | assert_equal '/foo/bar', Authentication::CanonicalString.new(request).send(:path)
75 | end
76 |
77 | def test_path_includes_significant_query_strings
78 | significant_query_strings = [
79 | ['/test/query/string?acl', '/test/query/string?acl'],
80 | ['/test/query/string?acl&foo=bar', '/test/query/string?acl'],
81 | ['/test/query/string?foo=bar&acl', '/test/query/string?acl'],
82 | ['/test/query/string?acl=foo', '/test/query/string?acl'],
83 | ['/test/query/string?torrent=foo', '/test/query/string?torrent'],
84 | ['/test/query/string?logging=foo', '/test/query/string?logging'],
85 | ['/test/query/string?bar=baz&acl=foo', '/test/query/string?acl'],
86 | ['/test/query/string?acl&response-content-disposition=1', '/test/query/string?acl&response-content-disposition=1']
87 | ]
88 |
89 | significant_query_strings.each do |uncleaned_path, expected_cleaned_path|
90 | assert_equal expected_cleaned_path, Authentication::CanonicalString.new(Net::HTTP::Get.new(uncleaned_path)).send(:path)
91 | end
92 | end
93 |
94 | def test_default_headers_set
95 | Authentication::CanonicalString.default_headers.each do |header|
96 | assert @canonical_string.headers.include?(header)
97 | end
98 | end
99 |
100 | def test_interesting_headers_are_copied_over
101 | an_interesting_header = 'content-md5'
102 | string_without_interesting_header = Authentication::CanonicalString.new(@request)
103 | assert string_without_interesting_header.headers[an_interesting_header].empty?
104 |
105 | # Add an interesting header
106 | @request[an_interesting_header] = 'foo'
107 | string_with_interesting_header = Authentication::CanonicalString.new(@request)
108 | assert_equal 'foo', string_with_interesting_header.headers[an_interesting_header]
109 | end
110 |
111 | def test_canonical_string
112 | request = AmazonDocExampleData::Example1.request
113 | assert_equal AmazonDocExampleData::Example1.canonical_string, Authentication::CanonicalString.new(request)
114 | end
115 | end
116 |
--------------------------------------------------------------------------------
/lib/aws/s3/exceptions.rb:
--------------------------------------------------------------------------------
1 | module AWS
2 | module S3
3 |
4 | # Abstract super class of all AWS::S3 exceptions
5 | class S3Exception < StandardError
6 | end
7 |
8 | # All responses with a code between 300 and 599 that contain an body are wrapped in an
9 | # ErrorResponse which contains an Error object. This Error class generates a custom exception with the name
10 | # of the xml Error and its message. All such runtime generated exception classes descend from ResponseError
11 | # and contain the ErrorResponse object so that all code that makes a request can rescue ResponseError and get
12 | # access to the ErrorResponse.
13 | class ResponseError < S3Exception
14 | attr_reader :response
15 | def initialize(message, response)
16 | @response = response
17 | super(message)
18 | end
19 | end
20 |
21 | #:stopdoc:
22 |
23 | # Most ResponseError's are created just time on a need to have basis, but we explicitly define the
24 | # InternalError exception because we want to explicitly rescue InternalError in some cases.
25 | class InternalError < ResponseError
26 | end
27 |
28 | class NoSuchKey < ResponseError
29 | end
30 |
31 | class RequestTimeout < ResponseError
32 | end
33 |
34 | # Abstract super class for all invalid options.
35 | class InvalidOption < S3Exception
36 | end
37 |
38 | # Raised if an invalid value is passed to the :access option when creating a Bucket or an S3Object.
39 | class InvalidAccessControlLevel < InvalidOption
40 | def initialize(valid_levels, access_level)
41 | super("Valid access control levels are #{valid_levels.inspect}. You specified `#{access_level}'.")
42 | end
43 | end
44 |
45 | # Raised if either the access key id or secret access key arguments are missing when establishing a connection.
46 | class MissingAccessKey < InvalidOption
47 | def initialize(missing_keys)
48 | key_list = missing_keys.map {|key| key.to_s}.join(' and the ')
49 | super("You did not provide both required access keys. Please provide the #{key_list}.")
50 | end
51 | end
52 |
53 | # Raised if a request is attempted before any connections have been established.
54 | class NoConnectionEstablished < S3Exception
55 | end
56 |
57 | # Raised if an unrecognized option is passed when establishing a connection.
58 | class InvalidConnectionOption < InvalidOption
59 | def initialize(invalid_options)
60 | message = "The following connection options are invalid: #{invalid_options.join(', ')}. " +
61 | "The valid connection options are: #{Connection::Options::VALID_OPTIONS.join(', ')}."
62 | super(message)
63 | end
64 | end
65 |
66 | # Raised if an invalid bucket name is passed when creating a new Bucket.
67 | class InvalidBucketName < S3Exception
68 | def initialize(invalid_name)
69 | message = "`#{invalid_name}' is not a valid bucket name. " +
70 | "Bucket names must be between 3 and 255 bytes and " +
71 | "can contain letters, numbers, dashes and underscores."
72 | super(message)
73 | end
74 | end
75 |
76 | # Raised if an invalid key name is passed when creating an S3Object.
77 | class InvalidKeyName < S3Exception
78 | def initialize(invalid_name)
79 | message = "`#{invalid_name}' is not a valid key name. " +
80 | "Key names must be no more than 1024 bytes long."
81 | super(message)
82 | end
83 | end
84 |
85 | # Raised if an invalid value is assigned to an S3Object's specific metadata name.
86 | class InvalidMetadataValue < S3Exception
87 | def initialize(invalid_names)
88 | message = "The following metadata names have invalid values: #{invalid_names.join(', ')}. " +
89 | "Metadata can not be larger than 2kilobytes."
90 | super(message)
91 | end
92 | end
93 |
94 | # Raised if the current bucket can not be inferred when not explicitly specifying the target bucket in the calling
95 | # method's arguments.
96 | class CurrentBucketNotSpecified < S3Exception
97 | def initialize(address)
98 | message = "No bucket name can be inferred from your current connection's address (`#{address}')"
99 | super(message)
100 | end
101 | end
102 |
103 | # Raised when an orphaned S3Object belonging to no bucket tries to access its (non-existant) bucket.
104 | class NoBucketSpecified < S3Exception
105 | def initialize
106 | super('The current object must have its bucket set')
107 | end
108 | end
109 |
110 | # Raised if an attempt is made to save an S3Object that does not have a key set.
111 | class NoKeySpecified < S3Exception
112 | def initialize
113 | super('The current object must have its key set')
114 | end
115 | end
116 |
117 | # Raised if you try to save a deleted object.
118 | class DeletedObject < S3Exception
119 | def initialize
120 | super('You can not save a deleted object')
121 | end
122 | end
123 |
124 | class ExceptionClassClash < S3Exception #:nodoc:
125 | def initialize(klass)
126 | message = "The exception class you tried to create (`#{klass}') exists and is not an exception"
127 | super(message)
128 | end
129 | end
130 |
131 | #:startdoc:
132 | end
133 | end
--------------------------------------------------------------------------------
/lib/aws/s3/response.rb:
--------------------------------------------------------------------------------
1 | #:stopdoc:
2 | module AWS
3 | module S3
4 | class Base
5 | class Response < String
6 | attr_reader :response, :body, :parsed
7 | def initialize(response)
8 | @response = response
9 | @body = response.body.to_s
10 | super(body)
11 | end
12 |
13 | def headers
14 | headers = {}
15 | response.each do |header, value|
16 | headers[header] = value
17 | end
18 | headers
19 | end
20 | memoized :headers
21 |
22 | def [](header)
23 | headers[header]
24 | end
25 |
26 | def each(&block)
27 | headers.each(&block)
28 | end
29 |
30 | def code
31 | response.code.to_i
32 | end
33 |
34 | {:success => 200..299, :redirect => 300..399,
35 | :client_error => 400..499, :server_error => 500..599}.each do |result, code_range|
36 | class_eval(<<-EVAL, __FILE__, __LINE__)
37 | def #{result}?
38 | return false unless response
39 | (#{code_range}).include? code
40 | end
41 | EVAL
42 | end
43 |
44 | def error?
45 | !success? && response['content-type'] == 'application/xml' && parsed.root == 'error'
46 | end
47 |
48 | def error
49 | Error.new(parsed, self)
50 | end
51 | memoized :error
52 |
53 | def parsed
54 | # XmlSimple is picky about what kind of object it parses, so we pass in body rather than self
55 | Parsing::XmlParser.new(body)
56 | end
57 | memoized :parsed
58 |
59 | def inspect
60 | "#<%s:0x%s %s %s>" % [self.class, object_id, response.code, response.message]
61 | end
62 | end
63 | end
64 |
65 | class Bucket
66 | class Response < Base::Response
67 | def bucket
68 | parsed
69 | end
70 | end
71 | end
72 |
73 | class S3Object
74 | class Response < Base::Response
75 | def etag
76 | headers['etag'][1...-1]
77 | end
78 | end
79 | end
80 |
81 | class Service
82 | class Response < Base::Response
83 | def empty?
84 | parsed['buckets'].nil?
85 | end
86 |
87 | def buckets
88 | parsed['buckets']['bucket'] || []
89 | end
90 | end
91 | end
92 |
93 | module ACL
94 | class Policy
95 | class Response < Base::Response
96 | alias_method :policy, :parsed
97 | end
98 | end
99 | end
100 |
101 | # Requests whose response code is between 300 and 599 and contain an in their body
102 | # are wrapped in an Error::Response. This Error::Response contains an Error object which raises an exception
103 | # that corresponds to the error in the response body. The exception object contains the ErrorResponse, so
104 | # in all cases where a request happens, you can rescue ResponseError and have access to the ErrorResponse and
105 | # its Error object which contains information about the ResponseError.
106 | #
107 | # begin
108 | # Bucket.create(..)
109 | # rescue ResponseError => exception
110 | # exception.response
111 | # # =>
112 | # exception.response.error
113 | # # =>
114 | # end
115 | class Error
116 | class Response < Base::Response
117 | def error?
118 | true
119 | end
120 |
121 | def inspect
122 | "#<%s:0x%s %s %s: '%s'>" % [self.class.name, object_id, response.code, error.code, error.message]
123 | end
124 | end
125 | end
126 |
127 | # Guess response class name from current class name. If the guessed response class doesn't exist
128 | # do the same thing to the current class's parent class, up the inheritance heirarchy until either
129 | # a response class is found or until we get to the top of the heirarchy in which case we just use
130 | # the the Base response class.
131 | #
132 | # Important: This implemantation assumes that the Base class has a corresponding Base::Response.
133 | class FindResponseClass #:nodoc:
134 | class << self
135 | def for(start)
136 | new(start).find
137 | end
138 | end
139 |
140 | def initialize(start)
141 | @container = AWS::S3
142 | @current_class = start
143 | end
144 |
145 | def find
146 | self.current_class = current_class.superclass until response_class_found?
147 | target.const_get(class_to_find)
148 | end
149 |
150 | private
151 | attr_reader :container
152 | attr_accessor :current_class
153 |
154 | def target
155 | container.const_get(current_name)
156 | end
157 |
158 | def target?
159 | container.const_defined?(current_name)
160 | end
161 |
162 | def response_class_found?
163 | target? && target.const_defined?(class_to_find)
164 | end
165 |
166 | def class_to_find
167 | :Response
168 | end
169 |
170 | def current_name
171 | truncate(current_class)
172 | end
173 |
174 | def truncate(klass)
175 | klass.name[/[^:]+$/]
176 | end
177 | end
178 | end
179 | end
180 | #:startdoc:
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | head:
2 |
3 | 0.6.2:
4 |
5 | - Apparently need to use custom __method__ in Ruby versions right up to 1.8.7.
6 |
7 | 0.6.1:
8 |
9 | - Use custom __method__ in Ruby versions *prior* to 1.8.7 not *up to* 1.8.7.
10 |
11 | - Rename Kernel#memoize to Kernel#expirable_memoize so that it doesn't conflict with memoize method in ActiveSupport which has an entirely different API and semantics. Reported by [Florian Dütsc (mail@florian-duetsch.de)].
12 |
13 | 0.6.0:
14 |
15 | - Full 1.9 compatibility (all tests passing against 1.9 & 1.8.6). Thanks to [David (dvdplm@gmail.com), Cyril David (cyx.ucron@gmail.com)]
16 |
17 | 0.5.1:
18 |
19 | - For now just redefine __method__ to take arguments so we don't break 1.8.7 use today
20 |
21 | 0.5.0:
22 |
23 | - Bug #17458 fixed. Normalize the :expires_in option to always be an integer even if the actual object passed in is a proxy to an integer, such as is the case with 2.hours from ActiveSupport which is actually an instance of ActiveSupport::Duration. Reported by [Steve Kickert steve@riverocktech.com]
24 |
25 | - Bug #19158 fixed. Don't prepend leading slash onto bucket name when deleting a bucket with the :force => true option.
26 |
27 | - Bug #17628 fixed. Don't ignore :use_ssl => false in url_for when the connection is established over ssl. Reported by [Tom Fixed (tkat11)]
28 |
29 | - Bug #13052 fixed. Appease some proxies by always including a Content-Length with all requests. Reported by [James Murty (jmurty)]
30 |
31 | - Bug #13756 fixed. Attributes that are false should not raise NoMethodError in Base#method_missing. Fixed by [Scott Patten]
32 |
33 | - Bug #19189 fixed. No longer reference Date::ABBR_MONTHS constant which was removed in Ruby 1.8.6. Reported by [Khurram Virani (kvirani)]
34 |
35 | - Bug #20487 fixed. If a request fails and is retried, only escape the request path the first time. Reported by [anonymous]
36 |
37 | - Replace ad-hoc S3Object.copy method with newly support built in API call.
38 |
39 | - Do not make connections persistent by default. This "feature" causes far more broken pipes than it is worth. Use with caution.
40 |
41 | 0.4.0:
42 |
43 | - Various adjustments to connection handling to try to mitigate exceptions raised from deep within Net::HTTP.
44 |
45 | - Don't coerce numbers that start with a zero because the zero will be lost. If a bucket, for example, has a name like '0815', all operation it will fail. Closes ticket #10089 [reported anonymously]"
46 |
47 | - Add ability to connect through a proxy using the :proxy option when establishing a connection. Suggested by [Simon Horne ]
48 |
49 | - Add :authenticated option to url_for. When passing false, don't generate signature parameters for query string.
50 |
51 | - Make url_for accept custom port settings. [Rich Olson]
52 |
53 | 0.3.0:
54 |
55 | - Ensure content type is eventually set to account for changes made to Net::HTTP in Ruby version 1.8.5. Reported by [David Hanson, Stephen Caudill, Tom Mornini ]
56 |
57 | - Add :persistent option to connections which keeps a persistent connection rather than creating a new one per request, defaulting to true. Based on a patch by [Metalhead ]
58 |
59 | - If we are retrying a request after rescuing one of the retry exceptions, rewind the body if its an IO stream so it starts at the beginning. [Jamis Buck]
60 |
61 | - Ensure that all paths being submitted to S3 are valid utf8. If they are not, we remove the extended characters. Ample help from [Jamis Buck]
62 |
63 | - Wrap logs in Log objects which exposes each line as a Log::Line that has accessors by name for each field.
64 |
65 | - Various performance optimizations for the extensions code. [Roman LE NEGRATE ]
66 |
67 | - Make S3Object.copy more efficient by streaming in both directions in parallel.
68 |
69 | - Open up Net:HTTPGenericRequest to make the chunk size 1 megabyte, up from 1 kilobyte.
70 |
71 | - Add S3Object.exists?
72 |
73 | 0.2.1:
74 |
75 | - When the bucket name argument (for e.g. Bucket.objects) is being used as the option hash, reassign it to the options variable and set the bucket to nil so bucket inference + options works.
76 |
77 | - Don't call CGI.escape on query string parameters in Hash#to_query_string since all paths get passed through URI.escape right before the request is made. Paths were getting double escaped. Bug spotted by [David Hanson]
78 |
79 | - Make s3sh exec irb.bat if on Windows. Bug spotted by [N. Sathish Kumar ]
80 |
81 | - Avoid class_variable_(get|set) since it was only recently added to Ruby. Spotted by [N. Sathish Kumar ]
82 |
83 | - Raise NoSuchKey if S3Object.about requests a key that does not exist.
84 |
85 | - If the response body is an empty string, don't try to parse it as xml.
86 |
87 | - Don't reject every body type save for IO and String at the door when making a request. Suggested by [Alex MacCaw ]
88 |
89 | - Allow dots in bucket names. [Jesse Newland]
90 |
91 | 0.2.0:
92 |
93 | - Infer content type for an object when calling S3Object.store without explicitly passing in the :content_type option.
94 |
95 | 0.1.2:
96 |
97 | - Scrap (overly) fancy generator based version of CoercibleString with a much simpler and clearer case statement. Continuations are really slow and the specific use of the generator was leaking memory. Bug spotted by [Remco van't Veer]
98 |
99 | 0.1.1:
100 |
101 | - Don't add the underscore method to String if it is already defined (like, for example, from ActiveSupport). Bug spotted by [Matt White ]
102 |
103 | 0.1.0:
104 |
105 | - Initial public release
106 |
--------------------------------------------------------------------------------
/support/faster-xml-simple/lib/faster_xml_simple.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2006 Michael Koziarski
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of
5 | # this software and associated documentation files (the "Software"), to deal in the
6 | # Software without restriction, including without limitation the rights to use,
7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8 | # Software, and to permit persons to whom the Software is furnished to do so,
9 | # subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included in all
12 | # copies or substantial portions of the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
18 | # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
19 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 |
21 | require 'rubygems'
22 | require 'xml/libxml'
23 |
24 | class FasterXmlSimple
25 | Version = '0.5.0'
26 | class << self
27 | # Take an string containing XML, and returns a hash representing that
28 | # XML document. For example:
29 | #
30 | # FasterXmlSimple.xml_in("1")
31 | # {"root"=>{"something"=>{"__content__"=>"1"}}}
32 | #
33 | # Faster XML Simple is designed to be a drop in replacement for the xml_in
34 | # functionality of http://xml-simple.rubyforge.org
35 | #
36 | # The following options are supported:
37 | #
38 | # * contentkey: The key to use for the content of text elements,
39 | # defaults to '\_\_content__'
40 | # * forcearray: The list of elements which should always be returned
41 | # as arrays. Under normal circumstances single element arrays are inlined.
42 | # * suppressempty: The value to return for empty elements, pass +true+
43 | # to remove empty elements entirely.
44 | # * keeproot: By default the hash returned has a single key with the
45 | # name of the root element. If the name of the root element isn't
46 | # interesting to you, pass +false+.
47 | # * forcecontent: By default a text element with no attributes, will
48 | # be collapsed to just a string instead of a hash with a single key.
49 | # Pass +true+ to prevent this.
50 | #
51 | #
52 | def xml_in(string, options={})
53 | new(string, options).out
54 | end
55 | end
56 |
57 | def initialize(string, options) #:nodoc:
58 | @doc = parse(string)
59 | @options = default_options.merge options
60 | end
61 |
62 | def out #:nodoc:
63 | if @options['keeproot']
64 | {@doc.root.name => collapse(@doc.root)}
65 | else
66 | collapse(@doc.root)
67 | end
68 | end
69 |
70 | private
71 | def default_options
72 | {'contentkey' => '__content__', 'forcearray' => [], 'keeproot'=>true}
73 | end
74 |
75 | def collapse(element)
76 | result = hash_of_attributes(element)
77 | if text_node? element
78 | text = collapse_text(element)
79 | result[content_key] = text if text =~ /\S/
80 | elsif element.children?
81 | element.inject(result) do |hash, child|
82 | unless child.text?
83 | child_result = collapse(child)
84 | (hash[child.name] ||= []) << child_result
85 | end
86 | hash
87 | end
88 | end
89 | if result.empty?
90 | return empty_element
91 | end
92 | # Compact them to ensure it complies with the user's requests
93 | inline_single_element_arrays(result)
94 | remove_empty_elements(result) if suppress_empty?
95 | if content_only?(result) && !force_content?
96 | result[content_key]
97 | else
98 | result
99 | end
100 | end
101 |
102 | def content_only?(result)
103 | result.keys == [content_key]
104 | end
105 |
106 | def content_key
107 | @options['contentkey']
108 | end
109 |
110 | def force_array?(key_name)
111 | Array(@options['forcearray']).include?(key_name)
112 | end
113 |
114 | def inline_single_element_arrays(result)
115 | result.each do |key, value|
116 | if value.size == 1 && value.is_a?(Array) && !force_array?(key)
117 | result[key] = value.first
118 | end
119 | end
120 | end
121 |
122 | def remove_empty_elements(result)
123 | result.each do |key, value|
124 | if value == empty_element
125 | result.delete key
126 | end
127 | end
128 | end
129 |
130 | def suppress_empty?
131 | @options['suppressempty'] == true
132 | end
133 |
134 | def empty_element
135 | if !@options.has_key? 'suppressempty'
136 | {}
137 | else
138 | @options['suppressempty']
139 | end
140 | end
141 |
142 | # removes the content if it's nothing but blanks, prevents
143 | # the hash being polluted with lots of content like "\n\t\t\t"
144 | def suppress_empty_content(result)
145 | result.delete content_key if result[content_key] !~ /\S/
146 | end
147 |
148 | def force_content?
149 | @options['forcecontent']
150 | end
151 |
152 | # a text node is one with 1 or more child nodes which are
153 | # text nodes, and no non-text children, there's no sensible
154 | # way to support nodes which are text and markup like:
155 | # Something Bold
156 | def text_node?(element)
157 | !element.text? && element.all? {|c| c.text?}
158 | end
159 |
160 | # takes a text node, and collapses it into a string
161 | def collapse_text(element)
162 | element.map {|c| c.content } * ''
163 | end
164 |
165 | def hash_of_attributes(element)
166 | result = {}
167 | element.each_attr do |attribute|
168 | name = attribute.name
169 | name = [attribute.ns, attribute.name].join(':') if attribute.ns?
170 | result[name] = attribute.value
171 | end
172 | result
173 | end
174 |
175 | def parse(string)
176 | if string == ''
177 | string = ' '
178 | end
179 | XML::Parser.string(string).parse
180 | end
181 | end
182 |
183 | class XmlSimple # :nodoc:
184 | def self.xml_in(*args)
185 | FasterXmlSimple.xml_in *args
186 | end
187 | end
--------------------------------------------------------------------------------
/support/rdoc/code_info.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'rake'
3 | require 'rdoc/rdoc'
4 |
5 | module RDoc
6 | class CodeInfo
7 | class << self
8 | def parse(wildcard_pattern = nil)
9 | @info_for_corpus = parse_files(wildcard_pattern)
10 | end
11 |
12 | def for(constant)
13 | new(constant).info
14 | end
15 |
16 | def info_for_corpus
17 | raise RuntimeError, "You must first generate a corpus to search by using RDoc::CodeInfo.parse" unless @info_for_corpus
18 | @info_for_corpus
19 | end
20 |
21 | def parsed_files
22 | info_for_corpus.map {|info| info.file_absolute_name}
23 | end
24 |
25 | def files_to_parse
26 | @files_to_parse ||= Rake::FileList.new
27 | end
28 |
29 | private
30 | def parse_files(pattern)
31 | files = pattern ? Rake::FileList[pattern] : files_to_parse
32 | options = Options.instance
33 | options.parse(files << '-q', RDoc::GENERATORS)
34 | rdoc.send(:parse_files, options)
35 | end
36 |
37 | def rdoc
38 | TopLevel.reset
39 | rdoc = RDoc.new
40 | stats = Stats.new
41 | # We don't want any output so we'll override the print method
42 | stats.instance_eval { def print; nil end }
43 | rdoc.instance_variable_set(:@stats, stats)
44 | rdoc
45 | end
46 | end
47 |
48 | attr_reader :info
49 | def initialize(location)
50 | @location = CodeLocation.new(location)
51 | find_constant
52 | find_method if @location.has_method?
53 | end
54 |
55 | private
56 | attr_reader :location
57 | attr_writer :info
58 | def find_constant
59 | parts = location.namespace_parts
60 | self.class.info_for_corpus.each do |file_info|
61 | @info = parts.inject(file_info) do |result, const_part|
62 | (result.find_module_named(const_part) || result.find_class_named(const_part)) || break
63 | end
64 | return if info
65 | end
66 | end
67 |
68 | def find_method
69 | return unless info
70 | self.info = info.method_list.detect do |method_info|
71 | next unless method_info.name == location.method_name
72 | if location.class_method?
73 | method_info.singleton
74 | elsif location.instance_method?
75 | !method_info.singleton
76 | else
77 | true
78 | end
79 | end
80 | end
81 | end
82 |
83 | class CodeLocation
84 | attr_reader :location
85 |
86 | def initialize(location)
87 | @location = location
88 | end
89 |
90 | def parts
91 | location.split(/::|\.|#/)
92 | end
93 |
94 | def namespace_parts
95 | has_method? ? parts[0...-1] : parts
96 | end
97 |
98 | def has_method?
99 | ('a'..'z').include?(parts.last[0, 1])
100 | end
101 |
102 | def instance_method?
103 | !location['#'].nil?
104 | end
105 |
106 | def class_method?
107 | has_method? && !location[/#|\./]
108 | end
109 |
110 | def method_name
111 | parts.last if has_method?
112 | end
113 | end
114 | end
115 |
116 | if __FILE__ == $0
117 | require 'test/unit'
118 | class CodeInfoTest < Test::Unit::TestCase
119 | def setup
120 | RDoc::CodeInfo.parse(__FILE__)
121 | end
122 |
123 | def test_constant_lookup
124 | assert RDoc::CodeInfo.for('RDoc')
125 |
126 | info = RDoc::CodeInfo.for('RDoc::CodeInfo')
127 | assert_equal 'CodeInfo', info.name
128 | end
129 |
130 | def test_method_lookup
131 | {'RDoc::CodeInfo.parse' => true,
132 | 'RDoc::CodeInfo::parse' => true,
133 | 'RDoc::CodeInfo#parse' => false,
134 | 'RDoc::CodeInfo.find_method' => true,
135 | 'RDoc::CodeInfo::find_method' => false,
136 | 'RDoc::CodeInfo#find_method' => true,
137 | 'RDoc::CodeInfo#no_such_method' => false,
138 | 'RDoc::NoSuchConst#foo' => false}.each do |location, result_of_lookup|
139 | assert_equal result_of_lookup, !RDoc::CodeInfo.for(location).nil?
140 | end
141 | end
142 | end
143 |
144 | class CodeLocationTest < Test::Unit::TestCase
145 | def test_parts
146 | {'Foo' => %w(Foo),
147 | 'Foo::Bar' => %w(Foo Bar),
148 | 'Foo::Bar#baz' => %w(Foo Bar baz),
149 | 'Foo::Bar.baz' => %w(Foo Bar baz),
150 | 'Foo::Bar::baz' => %w(Foo Bar baz),
151 | 'Foo::Bar::Baz' => %w(Foo Bar Baz)}.each do |location, parts|
152 | assert_equal parts, RDoc::CodeLocation.new(location).parts
153 | end
154 | end
155 |
156 | def test_namespace_parts
157 | {'Foo' => %w(Foo),
158 | 'Foo::Bar' => %w(Foo Bar),
159 | 'Foo::Bar#baz' => %w(Foo Bar),
160 | 'Foo::Bar.baz' => %w(Foo Bar),
161 | 'Foo::Bar::baz' => %w(Foo Bar),
162 | 'Foo::Bar::Baz' => %w(Foo Bar Baz)}.each do |location, namespace_parts|
163 | assert_equal namespace_parts, RDoc::CodeLocation.new(location).namespace_parts
164 | end
165 | end
166 |
167 | def test_has_method?
168 | {'Foo' => false,
169 | 'Foo::Bar' => false,
170 | 'Foo::Bar#baz' => true,
171 | 'Foo::Bar.baz' => true,
172 | 'Foo::Bar::baz' => true,
173 | 'Foo::Bar::Baz' => false}.each do |location, has_method_result|
174 | assert_equal has_method_result, RDoc::CodeLocation.new(location).has_method?
175 | end
176 | end
177 |
178 | def test_instance_method?
179 | {'Foo' => false,
180 | 'Foo::Bar' => false,
181 | 'Foo::Bar#baz' => true,
182 | 'Foo::Bar.baz' => false,
183 | 'Foo::Bar::baz' => false,
184 | 'Foo::Bar::Baz' => false}.each do |location, is_instance_method|
185 | assert_equal is_instance_method, RDoc::CodeLocation.new(location).instance_method?
186 | end
187 | end
188 |
189 | def test_class_method?
190 | {'Foo' => false,
191 | 'Foo::Bar' => false,
192 | 'Foo::Bar#baz' => false,
193 | 'Foo::Bar.baz' => false,
194 | 'Foo::Bar::baz' => true,
195 | 'Foo::Bar::Baz' => false}.each do |location, is_class_method|
196 | assert_equal is_class_method, RDoc::CodeLocation.new(location).class_method?
197 | end
198 | end
199 |
200 | def test_method_name
201 | {'Foo' => nil,
202 | 'Foo::Bar' => nil,
203 | 'Foo::Bar#baz' => 'baz',
204 | 'Foo::Bar.baz' => 'baz',
205 | 'Foo::Bar::baz' => 'baz',
206 | 'Foo::Bar::Baz' => nil}.each do |location, method_name|
207 | assert_equal method_name, RDoc::CodeLocation.new(location).method_name
208 | end
209 | end
210 | end
211 | end
--------------------------------------------------------------------------------
/test/acl_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/test_helper'
2 |
3 | class PolicyReadingTest < Test::Unit::TestCase
4 |
5 | def setup
6 | @policy = prepare_policy
7 | end
8 |
9 | def test_policy_owner
10 | assert_kind_of Owner, @policy.owner
11 | assert_equal 'bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1', @policy.owner.id
12 | assert_equal 'mmolina@onramp.net', @policy.owner.display_name
13 | end
14 |
15 | def test_grants
16 | assert @policy.grants
17 | assert !@policy.grants.empty?
18 | grant = @policy.grants.first
19 | assert_kind_of ACL::Grant, grant
20 | assert_equal 'FULL_CONTROL', grant.permission
21 | end
22 |
23 | def test_grants_have_grantee
24 | grant = @policy.grants.first
25 | assert grantee = grant.grantee
26 | assert_kind_of ACL::Grantee, grantee
27 | assert_equal 'bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1', grantee.id
28 | assert_equal 'mmolina@onramp.net', grantee.display_name
29 | assert_equal 'CanonicalUser', grantee.type
30 | end
31 |
32 | def test_grantee_always_responds_to_email_address
33 | assert_nothing_raised do
34 | @policy.grants.first.grantee.email_address
35 | end
36 | end
37 |
38 | private
39 | def prepare_policy
40 | ACL::Policy.new(parsed_policy)
41 | end
42 |
43 | def parsed_policy
44 | Parsing::XmlParser.new Fixtures::Policies.policy_with_one_grant
45 | end
46 | end
47 |
48 | class PolicyWritingTest < PolicyReadingTest
49 |
50 | def setup
51 | policy = prepare_policy
52 | # Dump the policy to xml and retranslate it back from the xml then run all the tests in the xml reading
53 | # test. This round tripping indirectly asserts that the original xml document is the same as the to_xml
54 | # dump.
55 | @policy = ACL::Policy.new(Parsing::XmlParser.new(policy.to_xml))
56 | end
57 |
58 | end
59 |
60 | class PolicyTest < Test::Unit::TestCase
61 | def test_building_policy_by_hand
62 | policy = grant = grantee = nil
63 | assert_nothing_raised do
64 | policy = ACL::Policy.new
65 | grant = ACL::Grant.new
66 | grantee = ACL::Grantee.new
67 | grantee.email_address = 'marcel@vernix.org'
68 | grant.permission = 'READ_ACP'
69 | grant.grantee = grantee
70 | policy.grants << grant
71 | policy.owner = Owner.new('id' => '123456789', 'display_name' => 'noradio')
72 | end
73 |
74 | assert_nothing_raised do
75 | policy.to_xml
76 | end
77 |
78 | assert !policy.grants.empty?
79 | assert_equal 1, policy.grants.size
80 | assert_equal 'READ_ACP', policy.grants.first.permission
81 | end
82 |
83 | def test_include?
84 | policy = ACL::Policy.new(Parsing::XmlParser.new(Fixtures::Policies.policy_with_one_grant))
85 | assert !policy.grants.include?(:public_read)
86 | policy.grants << ACL::Grant.grant(:public_read)
87 | assert policy.grants.include?(:public_read)
88 |
89 | assert policy.grants.include?(ACL::Grant.grant(:public_read))
90 | [false, 1, '1'].each do |non_grant|
91 | assert !policy.grants.include?(non_grant)
92 | end
93 | end
94 |
95 | def test_delete
96 | policy = ACL::Policy.new(Parsing::XmlParser.new(Fixtures::Policies.policy_with_one_grant))
97 | policy.grants << ACL::Grant.grant(:public_read)
98 | assert policy.grants.include?(:public_read)
99 | assert policy.grants.delete(:public_read)
100 | assert !policy.grants.include?(:public_read)
101 | [false, 1, '1'].each do |non_grant|
102 | assert_nil policy.grants.delete(non_grant)
103 | end
104 | end
105 |
106 | def test_grant_list_comparison
107 | policy = ACL::Policy.new
108 | policy2 = ACL::Policy.new
109 |
110 | grant_names = [:public_read, :public_read_acp, :authenticated_write]
111 | grant_names.each {|grant_name| policy.grants << ACL::Grant.grant(grant_name)}
112 | grant_names.reverse_each {|grant_name| policy2.grants << ACL::Grant.grant(grant_name)}
113 |
114 | assert_equal policy.grants, policy2.grants
115 | end
116 | end
117 |
118 | class GrantTest < Test::Unit::TestCase
119 | def test_permission_must_be_valid
120 | grant = ACL::Grant.new
121 | assert_nothing_raised do
122 | grant.permission = 'READ_ACP'
123 | end
124 |
125 | assert_raises(InvalidAccessControlLevel) do
126 | grant.permission = 'not a valid permission'
127 | end
128 | end
129 |
130 | def test_stock_grants
131 | assert_raises(ArgumentError) do
132 | ACL::Grant.grant :this_is_not_a_stock_grant
133 | end
134 |
135 | grant = nil
136 | assert_nothing_raised do
137 | grant = ACL::Grant.grant(:public_read)
138 | end
139 |
140 | assert grant
141 | assert_kind_of ACL::Grant, grant
142 | assert_equal 'READ', grant.permission
143 | assert grant.grantee
144 | assert_kind_of ACL::Grantee, grant.grantee
145 | assert_equal 'AllUsers', grant.grantee.group
146 | end
147 | end
148 |
149 | class GranteeTest < Test::Unit::TestCase
150 | def test_type_inference
151 | grantee = ACL::Grantee.new
152 |
153 | assert_nothing_raised do
154 | grantee.type
155 | end
156 |
157 | assert_nil grantee.type
158 | grantee.group = 'AllUsers'
159 | assert_equal 'AllUsers', grantee.group
160 | assert_equal 'Group', grantee.type
161 | grantee.email_address = 'marcel@vernix.org'
162 | assert_equal 'AmazonCustomerByEmail', grantee.type
163 | grantee.display_name = 'noradio'
164 | assert_equal 'AmazonCustomerByEmail', grantee.type
165 | grantee.id = '123456789'
166 | assert_equal 'CanonicalUser', grantee.type
167 | end
168 |
169 | def test_type_is_extracted_if_present
170 | grantee = ACL::Grantee.new('xsi:type' => 'CanonicalUser')
171 | assert_equal 'CanonicalUser', grantee.type
172 | end
173 |
174 | def test_type_representation
175 | grantee = ACL::Grantee.new('uri' => 'http://acs.amazonaws.com/groups/global/AllUsers')
176 |
177 | assert_equal 'AllUsers Group', grantee.type_representation
178 | grantee.group = 'AuthenticatedUsers'
179 | assert_equal 'AuthenticatedUsers Group', grantee.type_representation
180 | grantee.email_address = 'marcel@vernix.org'
181 | assert_equal 'marcel@vernix.org', grantee.type_representation
182 | grantee.display_name = 'noradio'
183 | grantee.id = '123456789'
184 | assert_equal 'noradio', grantee.type_representation
185 | end
186 | end
187 |
188 | class ACLOptionProcessorTest < Test::Unit::TestCase
189 | def test_empty_options
190 | options = {}
191 | assert_nothing_raised do
192 | process! options
193 | end
194 | assert_equal({}, options)
195 | end
196 |
197 | def test_invalid_access_level
198 | options = {:access => :foo}
199 | assert_raises(InvalidAccessControlLevel) do
200 | process! options
201 | end
202 | end
203 |
204 | def test_valid_access_level_is_normalized
205 | valid_access_levels = [
206 | {:access => :private},
207 | {'access' => 'private'},
208 | {:access => 'private'},
209 | {'access' => :private},
210 | {'x-amz-acl' => 'private'},
211 | {:x_amz_acl => :private},
212 | {:x_amz_acl => 'private'},
213 | {'x_amz_acl' => :private}
214 | ]
215 |
216 | valid_access_levels.each do |options|
217 | assert_nothing_raised do
218 | process! options
219 | end
220 | assert_equal 'private', acl(options)
221 | end
222 |
223 | valid_hyphenated_access_levels = [
224 | {:access => :public_read},
225 | {'access' => 'public_read'},
226 | {'access' => 'public-read'},
227 | {:access => 'public_read'},
228 | {:access => 'public-read'},
229 | {'access' => :public_read},
230 |
231 | {'x-amz-acl' => 'public_read'},
232 | {:x_amz_acl => :public_read},
233 | {:x_amz_acl => 'public_read'},
234 | {:x_amz_acl => 'public-read'},
235 | {'x_amz_acl' => :public_read}
236 | ]
237 |
238 | valid_hyphenated_access_levels.each do |options|
239 | assert_nothing_raised do
240 | process! options
241 | end
242 | assert_equal 'public-read', acl(options)
243 | end
244 | end
245 |
246 | private
247 | def process!(options)
248 | ACL::OptionProcessor.process!(options)
249 | end
250 |
251 | def acl(options)
252 | options['x-amz-acl']
253 | end
254 | end
255 |
--------------------------------------------------------------------------------
/test/connection_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/test_helper'
2 |
3 | class ConnectionTest < Test::Unit::TestCase
4 | attr_reader :keys
5 | def setup
6 | @keys = {:access_key_id => '123', :secret_access_key => 'abc'}.freeze
7 | end
8 |
9 | def test_creating_a_connection
10 | connection = Connection.new(keys)
11 | assert_kind_of Net::HTTP, connection.http
12 | end
13 |
14 | def test_use_ssl_option_is_set_in_connection
15 | connection = Connection.new(keys.merge(:use_ssl => true))
16 | assert connection.http.use_ssl?
17 | end
18 |
19 | def test_use_ssl_option_defaults_to_false_in_connection
20 | connection = Connection.new(keys)
21 | assert !connection.http.use_ssl?
22 | end
23 |
24 | def test_use_ssl_option_is_set_to_false_in_connection
25 | connection = Connection.new(keys.merge(:use_ssl => false))
26 | assert !connection.http.use_ssl?
27 | end
28 |
29 | def test_setting_port_to_443_implies_use_ssl
30 | connection = Connection.new(keys.merge(:port => 443))
31 | assert connection.http.use_ssl?
32 | end
33 |
34 | def test_protocol
35 | connection = Connection.new(keys)
36 | assert_equal 'http://', connection.protocol
37 | connection = Connection.new(keys.merge(:use_ssl => true))
38 | assert_equal 'https://', connection.protocol
39 | end
40 |
41 | def test_url_for_honors_use_ssl_option_if_it_is_false_even_if_connection_has_use_ssl_option_set
42 | # References bug: http://rubyforge.org/tracker/index.php?func=detail&aid=17628&group_id=2409&atid=9356
43 | connection = Connection.new(keys.merge(:use_ssl => true))
44 | assert_match %r(^http://), connection.url_for('/pathdoesnotmatter', :authenticated => false, :use_ssl => false)
45 | end
46 |
47 | def test_connection_is_not_persistent_by_default
48 | connection = Connection.new(keys)
49 | assert !connection.persistent?
50 |
51 | connection = Connection.new(keys.merge(:persistent => true))
52 | assert connection.persistent?
53 | end
54 |
55 | def test_server_and_port_are_passed_onto_connection
56 | connection = Connection.new(keys)
57 | options = connection.instance_variable_get('@options')
58 | assert_equal connection.http.address, options[:server]
59 | assert_equal connection.http.port, options[:port]
60 | end
61 |
62 | def test_not_including_required_access_keys_raises
63 | assert_raises(MissingAccessKey) do
64 | Connection.new
65 | end
66 |
67 | assert_raises(MissingAccessKey) do
68 | Connection.new(:access_key_id => '123')
69 | end
70 |
71 | assert_nothing_raised do
72 | Connection.new(keys)
73 | end
74 | end
75 |
76 | def test_access_keys_extracted
77 | connection = Connection.new(keys)
78 | assert_equal '123', connection.access_key_id
79 | assert_equal 'abc', connection.secret_access_key
80 | end
81 |
82 | def test_request_method_class_lookup
83 | connection = Connection.new(keys)
84 | expectations = {
85 | :get => Net::HTTP::Get, :post => Net::HTTP::Post,
86 | :put => Net::HTTP::Put, :delete => Net::HTTP::Delete,
87 | :head => Net::HTTP::Head
88 | }
89 |
90 | expectations.each do |verb, klass|
91 | assert_equal klass, connection.send(:request_method, verb)
92 | end
93 | end
94 |
95 | def test_url_for_uses_default_protocol_server_and_port
96 | connection = Connection.new(:access_key_id => '123', :secret_access_key => 'abc', :port => 80)
97 | assert_match %r(^http://s3\.amazonaws\.com/foo\?), connection.url_for('/foo')
98 |
99 | connection = Connection.new(:access_key_id => '123', :secret_access_key => 'abc', :use_ssl => true, :port => 443)
100 | assert_match %r(^https://s3\.amazonaws\.com/foo\?), connection.url_for('/foo')
101 | end
102 |
103 | def test_url_for_remembers_custom_protocol_server_and_port
104 | connection = Connection.new(:access_key_id => '123', :secret_access_key => 'abc', :server => 'example.org', :port => 555, :use_ssl => true)
105 | assert_match %r(^https://example\.org:555/foo\?), connection.url_for('/foo')
106 | end
107 |
108 | def test_url_for_with_and_without_authenticated_urls
109 | connection = Connection.new(:access_key_id => '123', :secret_access_key => 'abc', :server => 'example.org')
110 | authenticated = lambda {|url| url['?AWSAccessKeyId']}
111 | assert authenticated[connection.url_for('/foo')]
112 | assert authenticated[connection.url_for('/foo', :authenticated => true)]
113 | assert !authenticated[connection.url_for('/foo', :authenticated => false)]
114 | end
115 |
116 | def test_url_for_with_canonical_query_params
117 | connection = Connection.new(:access_key_id => '123', :secret_access_key => 'abc', :server => 'example.org')
118 | dispositioned = lambda {|url| url['?response-content-disposition=a']}
119 | assert dispositioned[connection.url_for("/foo?response-content-disposition=a")]
120 | end
121 |
122 | def test_connecting_through_a_proxy
123 | connection = nil
124 | assert_nothing_raised do
125 | connection = Connection.new(keys.merge(:proxy => sample_proxy_settings))
126 | end
127 | assert connection.http.proxy?
128 | end
129 |
130 | def test_request_only_escapes_the_path_the_first_time_it_runs_and_not_subsequent_times
131 | connection = Connection.new(@keys)
132 | unescaped_path = 'path with spaces'
133 | escaped_path = 'path%20with%20spaces'
134 |
135 | flexmock(Connection).should_receive(:prepare_path).with(unescaped_path).once.and_return(escaped_path).ordered
136 | flexmock(connection.http).should_receive(:request).and_raise(Errno::EPIPE).ordered
137 | flexmock(connection.http).should_receive(:request).ordered
138 | connection.request :put, unescaped_path
139 | end
140 |
141 | def test_if_request_has_no_body_then_the_content_length_is_set_to_zero
142 | # References bug: http://rubyforge.org/tracker/index.php?func=detail&aid=13052&group_id=2409&atid=9356
143 | connection = Connection.new(@keys)
144 | flexmock(Net::HTTP::Put).new_instances.should_receive(:content_length=).once.with(0).ordered
145 | flexmock(connection.http).should_receive(:request).once.ordered
146 | connection.request :put, 'path does not matter'
147 | end
148 | end
149 |
150 | class ConnectionOptionsTest < Test::Unit::TestCase
151 |
152 | def setup
153 | @options = generate_options(:server => 'example.org', :port => 555)
154 | @default_options = generate_options
155 | end
156 |
157 | def test_server_extracted
158 | assert_key_transfered(:server, 'example.org', @options)
159 | end
160 |
161 | def test_port_extracted
162 | assert_key_transfered(:port, 555, @options)
163 | end
164 |
165 | def test_server_defaults_to_default_host
166 | assert_equal DEFAULT_HOST, @default_options[:server]
167 | end
168 |
169 | def test_port_defaults_to_80_if_use_ssl_is_false
170 | assert_equal 80, @default_options[:port]
171 | end
172 |
173 | def test_port_is_set_to_443_if_use_ssl_is_true
174 | options = generate_options(:use_ssl => true)
175 | assert_equal 443, options[:port]
176 | end
177 |
178 | def test_explicit_port_trumps_use_ssl
179 | options = generate_options(:port => 555, :use_ssl => true)
180 | assert_equal 555, options[:port]
181 | end
182 |
183 | def test_invalid_options_raise
184 | assert_raises(InvalidConnectionOption) do
185 | generate_options(:host => 'campfire.s3.amazonaws.com')
186 | end
187 | end
188 |
189 | def test_not_specifying_all_required_proxy_settings_raises
190 | assert_raises(ArgumentError) do
191 | generate_options(:proxy => {})
192 | end
193 | end
194 |
195 | def test_not_specifying_proxy_option_at_all_does_not_raise
196 | assert_nothing_raised do
197 | generate_options
198 | end
199 | end
200 |
201 | def test_specifying_all_required_proxy_settings
202 | assert_nothing_raised do
203 | generate_options(:proxy => sample_proxy_settings)
204 | end
205 | end
206 |
207 | def test_only_host_setting_is_required
208 | assert_nothing_raised do
209 | generate_options(:proxy => {:host => 'http://google.com'})
210 | end
211 | end
212 |
213 | def test_proxy_settings_are_extracted
214 | options = generate_options(:proxy => sample_proxy_settings)
215 | assert_equal sample_proxy_settings.values.map {|value| value.to_s}.sort, options.proxy_settings.map {|value| value.to_s}.sort
216 | end
217 |
218 | def test_recognizing_that_the_settings_want_to_connect_through_a_proxy
219 | options = generate_options(:proxy => sample_proxy_settings)
220 | assert options.connecting_through_proxy?
221 | end
222 |
223 | private
224 | def assert_key_transfered(key, value, options)
225 | assert_equal value, options[key]
226 | end
227 |
228 | def generate_options(options = {})
229 | Connection::Options.new(options)
230 | end
231 | end
232 |
--------------------------------------------------------------------------------
/test/object_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/test_helper'
2 |
3 | class ObjectTest < Test::Unit::TestCase
4 | def setup
5 | bucket = Bucket.new(Parsing::XmlParser.new(Fixtures::Buckets.bucket_with_one_key))
6 | @object = bucket.objects.first
7 | end
8 |
9 | def test_header_settings_reader_and_writer
10 | headers = {'content-type' => 'text/plain'}
11 | mock_connection_for(S3Object, :returns => {:headers => headers})
12 |
13 | assert_nothing_raised do
14 | @object.content_type
15 | end
16 |
17 | assert_equal 'text/plain', @object.content_type
18 |
19 | assert_nothing_raised do
20 | @object.content_type = 'image/jpg'
21 | end
22 |
23 | assert_equal 'image/jpg', @object.content_type
24 |
25 | assert_raises(NoMethodError) do
26 | @object.non_existant_header_setting
27 | end
28 | end
29 |
30 | def test_key_name_validation
31 | assert_raises(InvalidKeyName) do
32 | S3Object.create(nil, '', 'marcel')
33 | end
34 |
35 | assert_raises(InvalidKeyName) do
36 | huge_name = 'a' * 1500
37 | S3Object.create(huge_name, '', 'marcel')
38 | end
39 | end
40 |
41 | def test_content_type_inference
42 | [
43 | ['foo.jpg', {}, 'image/jpeg'],
44 | ['foo.txt', {}, 'text/plain'],
45 | ['foo', {}, nil],
46 | ['foo.asdf', {}, nil],
47 | ['foo.jpg', {:content_type => nil}, nil],
48 | ['foo', {:content_type => 'image/jpg'}, 'image/jpg'],
49 | ['foo.jpg', {:content_type => 'image/png'}, 'image/png'],
50 | ['foo.asdf', {:content_type => 'image/jpg'}, 'image/jpg']
51 | ].each do |key, options, content_type|
52 | S3Object.send(:infer_content_type!, key, options)
53 | assert_equal content_type, options[:content_type]
54 | end
55 | end
56 |
57 | def test_object_has_owner
58 | assert_kind_of Owner, @object.owner
59 | end
60 |
61 | def test_url_is_authenticated
62 | conn = Connection.new :access_key_id => '123', :secret_access_key => 'abc'
63 |
64 | begin
65 | AWS::S3::Base.connections['AWS::S3::Base'] = conn
66 | authenticated = lambda {|url| url['?AWSAccessKeyId']}
67 | assert authenticated[@object.url]
68 | ensure
69 | AWS::S3::Base.connections.clear
70 | end
71 | end
72 |
73 | def test_url_with_custom_query
74 | conn = Connection.new :access_key_id => '123', :secret_access_key => 'abc'
75 |
76 | begin
77 | AWS::S3::Base.connections['AWS::S3::Base'] = conn
78 | assert_match 'response-content-disposition=attachment%3B%20filename%3Dfoo.txt',
79 | @object.url(:query => {
80 | 'response-content-disposition' => 'attachment; filename=foo.txt'})
81 | ensure
82 | AWS::S3::Base.connections.clear
83 | end
84 | end
85 |
86 | def test_owner_attributes_are_accessible
87 | owner = @object.owner
88 | assert owner.id
89 | assert owner.display_name
90 | assert_equal 'bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1', owner.id
91 | assert_equal 'mmolina@onramp.net', owner.display_name
92 | end
93 |
94 | def test_only_valid_attributes_accessible
95 | assert_raises(NoMethodError) do
96 | @object.owner.foo
97 | end
98 | end
99 |
100 | def test_fetching_object_value_generates_value_object
101 | mock_connection_for(S3Object, :returns => {:body => 'hello!'})
102 | value = S3Object.value('foo', 'bar')
103 | assert_kind_of S3Object::Value, value
104 | assert_equal 'hello!', value
105 | end
106 |
107 | def test_fetching_file_by_name_raises_when_heuristic_fails
108 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.bucket_with_one_key})
109 | assert_raises(NoSuchKey) do
110 | S3Object.find('not_tongue_overload.jpg', 'marcel_molina')
111 | end
112 |
113 | object = nil # Block scoping
114 | assert_nothing_raised do
115 | object = S3Object.find('tongue_overload.jpg', 'marcel_molina')
116 | end
117 | assert_kind_of S3Object, object
118 | assert_equal 'tongue_overload.jpg', object.key
119 | end
120 |
121 | def test_about
122 | headers = {'content-size' => '12345', 'date' => Time.now.httpdate, 'content-type' => 'application/xml'}
123 | mock_connection_for(S3Object, :returns => [
124 | {:headers => headers},
125 | {:code => 404}
126 | ]
127 | )
128 | about = S3Object.about('foo', 'bar')
129 | assert_kind_of S3Object::About, about
130 | assert_equal headers, about
131 |
132 | assert_raises(NoSuchKey) do
133 | S3Object.about('foo', 'bar')
134 | end
135 | end
136 |
137 | def test_can_tell_that_an_s3object_does_not_exist
138 | mock_connection_for(S3Object, :returns => {:code => 404})
139 | assert_equal false, S3Object.exists?('foo', 'bar')
140 | end
141 |
142 | def test_can_tell_that_an_s3object_exists
143 | mock_connection_for(S3Object, :returns => {:code => 200})
144 | assert_equal true, S3Object.exists?('foo', 'bar')
145 | end
146 |
147 | def test_s3object_equality
148 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.bucket_with_more_than_one_key})
149 | file1, file2 = Bucket.objects('does not matter')
150 | assert file1 == file1
151 | assert file2 == file2
152 | assert !(file1 == file2) # /!\ Parens required /!\
153 | end
154 |
155 | def test_inspect
156 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.bucket_with_one_key})
157 | object = S3Object.find('tongue_overload.jpg', 'bucket does not matter')
158 | assert object.path
159 | assert_nothing_raised { object.inspect }
160 | assert object.inspect[object.path]
161 | end
162 |
163 | def test_etag
164 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.bucket_with_one_key})
165 | file = S3Object.find('tongue_overload.jpg', 'bucket does not matter')
166 | assert file.etag
167 | assert_equal 'f21f7c4e8ea6e34b268887b07d6da745', file.etag
168 | end
169 |
170 | def test_fetching_information_about_an_object_that_does_not_exist_raises_no_such_key
171 | mock_connection_for(S3Object, :returns => {:body => '', :code => 404})
172 | assert_raises(NoSuchKey) do
173 | S3Object.about('asdfasdfasdfas-this-does-not-exist', 'bucket does not matter')
174 | end
175 | end
176 | def test_copy_options_are_used
177 | options = {'x-amz-storage-class' => 'REDUCED_REDUNDANCY'}
178 | resp = FakeResponse.new
179 |
180 | connection = flexmock('Mock connection') do |mock|
181 | mock.should_receive(:request).
182 | # The storage-class key must be passed to connection.request(:put, ...)
183 | with(:put, '/some-bucket/new', hsh(options), any, any).
184 | and_return(resp)
185 | end
186 | flexmock(S3Object).should_receive(:connection).and_return(connection)
187 |
188 | result = S3Object.copy('old', 'new', 'some-bucket', options)
189 | assert_equal resp.code, result.code
190 | end
191 | end
192 |
193 | class MetadataTest < Test::Unit::TestCase
194 | def setup
195 | @metadata = S3Object::Metadata.new(Fixtures::Headers.headers_including_one_piece_of_metadata)
196 | end
197 |
198 | def test_only_metadata_is_extracted
199 | assert @metadata.to_headers.size == 1
200 | assert @metadata.to_headers['x-amz-meta-test']
201 | assert_equal 'foo', @metadata.to_headers['x-amz-meta-test']
202 | end
203 |
204 | def test_setting_new_metadata_normalizes_name
205 | @metadata[:bar] = 'baz'
206 | assert @metadata.to_headers.include?('x-amz-meta-bar')
207 | @metadata['baz'] = 'quux'
208 | assert @metadata.to_headers.include?('x-amz-meta-baz')
209 | @metadata['x-amz-meta-quux'] = 'whatever'
210 | assert @metadata.to_headers.include?('x-amz-meta-quux')
211 | end
212 |
213 | def test_clobbering_existing_header
214 | @metadata[:bar] = 'baz'
215 | assert_equal 'baz', @metadata.to_headers['x-amz-meta-bar']
216 | @metadata[:bar] = 'quux'
217 | assert_equal 'quux', @metadata.to_headers['x-amz-meta-bar']
218 | @metadata['bar'] = 'foo'
219 | assert_equal 'foo', @metadata.to_headers['x-amz-meta-bar']
220 | @metadata['x-amz-meta-bar'] = 'bar'
221 | assert_equal 'bar', @metadata.to_headers['x-amz-meta-bar']
222 | end
223 |
224 | def test_invalid_metadata
225 | @metadata[:invalid_header] = ' ' * (S3Object::Metadata::SIZE_LIMIT + 1)
226 | assert_raises InvalidMetadataValue do
227 | @metadata.to_headers
228 | end
229 | end
230 | end
231 |
232 | class ValueTest < Test::Unit::TestCase
233 | def setup
234 | @response = FakeResponse.new(:body => 'hello there')
235 | @value = S3Object::Value.new(@response)
236 | end
237 |
238 | def test_value_is_set_to_response_body
239 | assert_equal @response.body, @value
240 | end
241 |
242 | def test_response_is_accessible_from_value_object
243 | assert_equal @response, @value.response
244 | end
245 | end
246 |
--------------------------------------------------------------------------------
/test/extensions_test.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | require File.dirname(__FILE__) + '/test_helper'
3 |
4 | class HashExtensionsTest < Test::Unit::TestCase
5 | def test_to_query_string
6 | # Because hashes aren't ordered, I'm mostly testing against hashes with just one key
7 | symbol_keys = {:one => 1}
8 | string_keys = {'one' => 1}
9 | expected = '?one=1'
10 | [symbol_keys, string_keys].each do |hash|
11 | assert_equal expected, hash.to_query_string
12 | end
13 | end
14 |
15 | def test_empty_hash_returns_no_query_string
16 | assert_equal '', {}.to_query_string
17 | end
18 |
19 | def test_include_question_mark
20 | hash = {:one => 1}
21 | assert_equal '?one=1', hash.to_query_string
22 | assert_equal 'one=1', hash.to_query_string(false)
23 | end
24 |
25 | def test_elements_joined_by_ampersand
26 | hash = {:one => 1, :two => 2}
27 | qs = hash.to_query_string
28 | assert qs['one=1&two=2'] || qs['two=2&one=1']
29 | end
30 |
31 | def test_escape_values
32 | hash = {:one => '5+ 1=6&'}
33 | assert_equal '?one=5%2B%201%3D6%26', hash.to_query_string
34 | end
35 |
36 | def test_normalized_options
37 | expectations = [
38 | [{:foo_bar => 1}, {'foo-bar' => '1'}],
39 | [{'foo_bar' => 1}, {'foo-bar' => '1'}],
40 | [{'foo-bar' => 1}, {'foo-bar' => '1'}],
41 | [{}, {}]
42 | ]
43 |
44 | expectations.each do |(before, after)|
45 | assert_equal after, before.to_normalized_options
46 | end
47 | end
48 | end
49 |
50 | class StringExtensionsTest < Test::Unit::TestCase
51 | def test_previous
52 | expectations = {'abc' => 'abb', '123' => '122', '1' => '0'}
53 | expectations.each do |before, after|
54 | assert_equal after, before.previous
55 | end
56 | end
57 |
58 | def test_to_header
59 | transformations = {
60 | 'foo' => 'foo',
61 | :foo => 'foo',
62 | 'foo-bar' => 'foo-bar',
63 | 'foo_bar' => 'foo-bar',
64 | :foo_bar => 'foo-bar',
65 | 'Foo-Bar' => 'foo-bar',
66 | 'Foo_Bar' => 'foo-bar'
67 | }
68 |
69 | transformations.each do |before, after|
70 | assert_equal after, before.to_header
71 | end
72 | end
73 |
74 | def test_valid_utf8?
75 | assert !"318597/620065/GTL_75\24300_A600_A610.zip".valid_utf8?
76 | assert "318597/620065/GTL_75£00_A600_A610.zip".valid_utf8?
77 | end
78 |
79 | def test_remove_extended
80 | assert "318597/620065/GTL_75\24300_A600_A610.zip".remove_extended.valid_utf8?
81 | assert "318597/620065/GTL_75£00_A600_A610.zip".remove_extended.valid_utf8?
82 | end
83 |
84 | def test_tap
85 | assert_equal("http://google.com/foo/", "http://google.com".tap {|url| url << "/foo/" })
86 | end
87 |
88 | end
89 |
90 | class CoercibleStringTest < Test::Unit::TestCase
91 |
92 | def test_coerce
93 | coercions = [
94 | ['1', 1],
95 | ['false', false],
96 | ['true', true],
97 | ['2006-10-29T23:14:47.000Z', Time.parse('2006-10-29T23:14:47.000Z')],
98 | ['Hello!', 'Hello!'],
99 | ['false23', 'false23'],
100 | ['03 1-2-3-Apple-Tree.mp3', '03 1-2-3-Apple-Tree.mp3'],
101 | ['0815', '0815'] # This number isn't coerced because the leading zero would be lost
102 | ]
103 |
104 | coercions.each do |before, after|
105 | assert_nothing_raised do
106 | assert_equal after, CoercibleString.coerce(before)
107 | end
108 | end
109 | end
110 | end
111 |
112 | class KerneltExtensionsTest < Test::Unit::TestCase
113 | class Foo
114 | def foo
115 | __method__
116 | end
117 |
118 | def bar
119 | foo
120 | end
121 |
122 | def baz
123 | bar
124 | end
125 | end
126 |
127 | class Bar
128 | def foo
129 | calling_method
130 | end
131 |
132 | def bar
133 | calling_method
134 | end
135 |
136 | def calling_method
137 | __method__(1)
138 | end
139 | end
140 |
141 | def test___method___works_regardless_of_nesting
142 | f = Foo.new
143 | [:foo, :bar, :baz].each do |method|
144 | assert_equal 'foo', f.send(method)
145 | end
146 | end
147 |
148 | def test___method___depth
149 | b = Bar.new
150 | assert_equal 'foo', b.foo
151 | assert_equal 'bar', b.bar
152 | end
153 | end if RUBY_VERSION <= '1.8.7'
154 |
155 | class ModuleExtensionsTest < Test::Unit::TestCase
156 | class Foo
157 | def foo(reload = false)
158 | expirable_memoize(reload) do
159 | Time.now
160 | end
161 | end
162 |
163 | def bar(reload = false)
164 | expirable_memoize(reload, :baz) do
165 | Time.now
166 | end
167 | end
168 |
169 | def quux
170 | Time.now
171 | end
172 | memoized :quux
173 | end
174 |
175 | def setup
176 | @instance = Foo.new
177 | end
178 |
179 | def test_memoize
180 | assert !instance_variables_of(@instance).include?('@foo')
181 | cached_result = @instance.foo
182 | assert_equal cached_result, @instance.foo
183 | assert instance_variables_of(@instance).include?('@foo')
184 | assert_equal cached_result, @instance.send(:instance_variable_get, :@foo)
185 | assert_not_equal cached_result, new_cache = @instance.foo(:reload)
186 | assert_equal new_cache, @instance.foo
187 | assert_equal new_cache, @instance.send(:instance_variable_get, :@foo)
188 | end
189 |
190 | def test_customizing_memoize_storage
191 | assert !instance_variables_of(@instance).include?('@bar')
192 | assert !instance_variables_of(@instance).include?('@baz')
193 | cached_result = @instance.bar
194 | assert !instance_variables_of(@instance).include?('@bar')
195 | assert instance_variables_of(@instance).include?('@baz')
196 | assert_equal cached_result, @instance.bar
197 | assert_equal cached_result, @instance.send(:instance_variable_get, :@baz)
198 | assert_nil @instance.send(:instance_variable_get, :@bar)
199 | end
200 |
201 | def test_memoized
202 | assert !instance_variables_of(@instance).include?('@quux')
203 | cached_result = @instance.quux
204 | assert_equal cached_result, @instance.quux
205 | assert instance_variables_of(@instance).include?('@quux')
206 | assert_equal cached_result, @instance.send(:instance_variable_get, :@quux)
207 | assert_not_equal cached_result, new_cache = @instance.quux(:reload)
208 | assert_equal new_cache, @instance.quux
209 | assert_equal new_cache, @instance.send(:instance_variable_get, :@quux)
210 | end
211 |
212 | def test_constant_setting
213 | some_module = Module.new
214 | assert !some_module.const_defined?(:FOO)
215 | assert_nothing_raised do
216 | some_module.constant :FOO, 'bar'
217 | end
218 |
219 | assert some_module.const_defined?(:FOO)
220 | assert_nothing_raised do
221 | some_module::FOO
222 | some_module.foo
223 | end
224 | assert_equal 'bar', some_module::FOO
225 | assert_equal 'bar', some_module.foo
226 |
227 | assert_nothing_raised do
228 | some_module.constant :FOO, 'baz'
229 | end
230 |
231 | assert_equal 'bar', some_module::FOO
232 | assert_equal 'bar', some_module.foo
233 | end
234 |
235 | private
236 | # For 1.9 compatibility
237 | def instance_variables_of(object)
238 | object.instance_variables.map do |instance_variable|
239 | instance_variable.to_s
240 | end
241 | end
242 |
243 | end
244 |
245 | class AttributeProxyTest < Test::Unit::TestCase
246 | class BlindProxyUsingDefaultAttributesHash
247 | include SelectiveAttributeProxy
248 | proxy_to :exlusively => false
249 | end
250 |
251 | class BlindProxyUsingCustomAttributeHash
252 | include SelectiveAttributeProxy
253 | proxy_to :settings
254 | end
255 |
256 | class ProxyUsingPassedInAttributeHash
257 | include SelectiveAttributeProxy
258 |
259 | def initialize(attributes = {})
260 | @attributes = attributes
261 | end
262 | end
263 |
264 | class RestrictedProxy
265 | include SelectiveAttributeProxy
266 |
267 | private
268 | def proxiable_attribute?(name)
269 | %w(foo bar baz).include?(name)
270 | end
271 | end
272 |
273 | class NonExclusiveProxy
274 | include SelectiveAttributeProxy
275 | proxy_to :settings, :exclusively => false
276 | end
277 |
278 | def test_using_all_defaults
279 | b = BlindProxyUsingDefaultAttributesHash.new
280 | assert_nothing_raised do
281 | b.foo = 'bar'
282 | end
283 |
284 | assert_nothing_raised do
285 | b.foo
286 | end
287 |
288 | assert_equal 'bar', b.foo
289 | end
290 |
291 | def test_storage_is_autovivified
292 | b = BlindProxyUsingDefaultAttributesHash.new
293 | assert_nothing_raised do
294 | b.send(:attributes)['foo'] = 'bar'
295 | end
296 |
297 | assert_nothing_raised do
298 | b.foo
299 | end
300 |
301 | assert_equal 'bar', b.foo
302 | end
303 |
304 | def test_limiting_which_attributes_are_proxiable
305 | r = RestrictedProxy.new
306 | assert_nothing_raised do
307 | r.foo = 'bar'
308 | end
309 |
310 | assert_nothing_raised do
311 | r.foo
312 | end
313 |
314 | assert_equal 'bar', r.foo
315 |
316 | assert_raises(NoMethodError) do
317 | r.quux = 'foo'
318 | end
319 |
320 | assert_raises(NoMethodError) do
321 | r.quux
322 | end
323 | end
324 |
325 | def test_proxying_is_exclusive_by_default
326 | p = ProxyUsingPassedInAttributeHash.new('foo' => 'bar')
327 | assert_nothing_raised do
328 | p.foo
329 | p.foo = 'baz'
330 | end
331 |
332 | assert_equal 'baz', p.foo
333 |
334 | assert_raises(NoMethodError) do
335 | p.quux
336 | end
337 | end
338 |
339 | def test_setting_the_proxy_as_non_exclusive
340 | n = NonExclusiveProxy.new
341 | assert_nothing_raised do
342 | n.foo = 'baz'
343 | end
344 |
345 | assert_nothing_raised do
346 | n.foo
347 | end
348 |
349 | assert_equal 'baz', n.foo
350 | end
351 | end
352 |
--------------------------------------------------------------------------------
/lib/aws/s3/extensions.rb:
--------------------------------------------------------------------------------
1 | #encoding: BINARY
2 | #:stopdoc:
3 |
4 | class Hash
5 | def to_query_string(include_question_mark = true)
6 | query_string = ''
7 | unless empty?
8 | query_string << '?' if include_question_mark
9 | query_string << inject([]) do |params, (key, value)|
10 | params << "#{key}=#{AWS::S3.escape_uri_component(value)}"
11 | end.join('&')
12 | end
13 | query_string
14 | end
15 |
16 | def to_normalized_options
17 | # Convert all option names to downcased strings, and replace underscores with hyphens
18 | inject({}) do |normalized_options, (name, value)|
19 | normalized_options[name.to_header] = value.to_s
20 | normalized_options
21 | end
22 | end
23 |
24 | def to_normalized_options!
25 | replace(to_normalized_options)
26 | end
27 | end
28 |
29 | class String
30 | if RUBY_VERSION <= '1.9'
31 | def previous!
32 | self[-1] -= 1
33 | self
34 | end
35 | else
36 | def previous!
37 | self[-1] = (self[-1].ord - 1).chr
38 | self
39 | end
40 | end
41 |
42 | def tap
43 | yield(self)
44 | self
45 | end unless ''.respond_to?(:tap)
46 |
47 | def previous
48 | dup.previous!
49 | end
50 |
51 | def to_header
52 | downcase.tr('_', '-')
53 | end
54 |
55 | # ActiveSupport adds an underscore method to String so let's just use that one if
56 | # we find that the method is already defined
57 | def underscore
58 | gsub(/::/, '/').
59 | gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
60 | gsub(/([a-z\d])([A-Z])/,'\1_\2').
61 | tr("-", "_").downcase
62 | end unless public_method_defined? :underscore
63 |
64 | if RUBY_VERSION >= '1.9'
65 | def valid_utf8?
66 | dup.force_encoding('UTF-8').valid_encoding?
67 | end
68 | else
69 | def valid_utf8?
70 | scan(Regexp.new('[^\x00-\xa0]', nil, 'u')) { |s| s.unpack('U') }
71 | true
72 | rescue ArgumentError
73 | false
74 | end
75 | end
76 |
77 | # All paths in in S3 have to be valid unicode so this takes care of
78 | # cleaning up any strings that aren't valid utf-8 according to String#valid_utf8?
79 | if RUBY_VERSION >= '1.9'
80 | def remove_extended!
81 | sanitized_string = ''
82 | each_byte do |byte|
83 | character = byte.chr
84 | sanitized_string << character if character.ascii_only?
85 | end
86 | sanitized_string
87 | end
88 | else
89 | def remove_extended!
90 | gsub!(/[\x80-\xFF]/) { "%02X" % $&[0] }
91 | end
92 | end
93 |
94 | def remove_extended
95 | dup.remove_extended!
96 | end
97 | end
98 |
99 | class CoercibleString < String
100 | class << self
101 | def coerce(string)
102 | new(string).coerce
103 | end
104 | end
105 |
106 | def coerce
107 | case self
108 | when 'true'; true
109 | when 'false'; false
110 | # Don't coerce numbers that start with zero
111 | when /^[1-9]+\d*$/; Integer(self)
112 | when datetime_format; Time.parse(self)
113 | else
114 | self
115 | end
116 | end
117 |
118 | private
119 | # Lame hack since Date._parse is so accepting. S3 dates are of the form: '2006-10-29T23:14:47.000Z'
120 | # so unless the string looks like that, don't even try, otherwise it might convert an object's
121 | # key from something like '03 1-2-3-Apple-Tree.mp3' to Sat Feb 03 00:00:00 CST 2001.
122 | def datetime_format
123 | /^\d{4}-\d{2}-\d{2}\w\d{2}:\d{2}:\d{2}/
124 | end
125 | end
126 |
127 | class Symbol
128 | def to_header
129 | to_s.to_header
130 | end
131 | end
132 |
133 | module Kernel
134 | def __method__(depth = 0)
135 | caller[depth][/`([^']+)'/, 1]
136 | end if RUBY_VERSION <= '1.8.7'
137 |
138 | def __called_from__
139 | caller[1][/`([^']+)'/, 1]
140 | end if RUBY_VERSION > '1.8.7'
141 |
142 | def expirable_memoize(reload = false, storage = nil)
143 | current_method = RUBY_VERSION > '1.8.7' ? __called_from__ : __method__(1)
144 | storage = "@#{storage || current_method}"
145 | if reload
146 | instance_variable_set(storage, nil)
147 | else
148 | if cache = instance_variable_get(storage)
149 | return cache
150 | end
151 | end
152 | instance_variable_set(storage, yield)
153 | end
154 |
155 | def require_library_or_gem(library, gem_name = nil)
156 | if RUBY_VERSION >= '1.9'
157 | gem(gem_name || library, '>=0')
158 | end
159 | require library
160 | rescue LoadError => library_not_installed
161 | begin
162 | require 'rubygems'
163 | require library
164 | rescue LoadError
165 | raise library_not_installed
166 | end
167 | end
168 | end
169 |
170 | class Object
171 | def returning(value)
172 | yield(value)
173 | value
174 | end
175 | end
176 |
177 | class Module
178 | def memoized(method_name)
179 | original_method = "unmemoized_#{method_name}_#{Time.now.to_i}"
180 | alias_method original_method, method_name
181 | module_eval(<<-EVAL, __FILE__, __LINE__)
182 | def #{method_name}(reload = false, *args, &block)
183 | expirable_memoize(reload) do
184 | send(:#{original_method}, *args, &block)
185 | end
186 | end
187 | EVAL
188 | end
189 |
190 | def constant(name, value)
191 | unless const_defined?(name)
192 | const_set(name, value)
193 | module_eval(<<-EVAL, __FILE__, __LINE__)
194 | def self.#{name.to_s.downcase}
195 | #{name.to_s}
196 | end
197 | EVAL
198 | end
199 | end
200 | end
201 |
202 | class Class # :nodoc:
203 | def cattr_reader(*syms)
204 | syms.flatten.each do |sym|
205 | class_eval(<<-EOS, __FILE__, __LINE__)
206 | unless defined? @@#{sym}
207 | @@#{sym} = nil
208 | end
209 |
210 | def self.#{sym}
211 | @@#{sym}
212 | end
213 |
214 | def #{sym}
215 | @@#{sym}
216 | end
217 | EOS
218 | end
219 | end
220 |
221 | def cattr_writer(*syms)
222 | syms.flatten.each do |sym|
223 | class_eval(<<-EOS, __FILE__, __LINE__)
224 | unless defined? @@#{sym}
225 | @@#{sym} = nil
226 | end
227 |
228 | def self.#{sym}=(obj)
229 | @@#{sym} = obj
230 | end
231 |
232 | def #{sym}=(obj)
233 | @@#{sym} = obj
234 | end
235 | EOS
236 | end
237 | end
238 |
239 | def cattr_accessor(*syms)
240 | cattr_reader(*syms)
241 | cattr_writer(*syms)
242 | end
243 | end if Class.instance_methods.grep(/^cattr_(?:reader|writer|accessor)$/).empty?
244 |
245 | module SelectiveAttributeProxy
246 | def self.included(klass)
247 | klass.extend(ClassMethods)
248 | klass.class_eval(<<-EVAL, __FILE__, __LINE__)
249 | cattr_accessor :attribute_proxy
250 | cattr_accessor :attribute_proxy_options
251 |
252 | # Default name for attribute storage
253 | self.attribute_proxy = :attributes
254 | self.attribute_proxy_options = {:exclusively => true}
255 |
256 | private
257 | # By default proxy all attributes
258 | def proxiable_attribute?(name)
259 | return true unless self.class.attribute_proxy_options[:exclusively]
260 | send(self.class.attribute_proxy).has_key?(name)
261 | end
262 |
263 | def method_missing(method, *args, &block)
264 | # Autovivify attribute storage
265 | if method == self.class.attribute_proxy
266 | ivar = "@\#{method}"
267 | instance_variable_set(ivar, {}) unless instance_variable_get(ivar).is_a?(Hash)
268 | instance_variable_get(ivar)
269 | # Delegate to attribute storage
270 | elsif method.to_s =~ /^(\\w+)(=?)$/ && proxiable_attribute?($1)
271 | attributes_hash_name = self.class.attribute_proxy
272 | $2.empty? ? send(attributes_hash_name)[$1] : send(attributes_hash_name)[$1] = args.first
273 | else
274 | super
275 | end
276 | end
277 | EVAL
278 | end
279 |
280 | module ClassMethods
281 | def proxy_to(attribute_name, options = {})
282 | if attribute_name.is_a?(Hash)
283 | options = attribute_name
284 | else
285 | self.attribute_proxy = attribute_name
286 | end
287 | self.attribute_proxy_options = options
288 | end
289 | end
290 | end
291 |
292 | # When streaming data up, Net::HTTPGenericRequest hard codes a chunk size of 1k. For large files this
293 | # is an unfortunately low chunk size, so here we make it use a much larger default size and move it into a method
294 | # 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
295 | # than I've already had to...).
296 | module Net
297 | class HTTPGenericRequest
298 | def send_request_with_body_stream(sock, ver, path, f)
299 | raise ArgumentError, "Content-Length not given and Transfer-Encoding is not `chunked'" unless content_length() or chunked?
300 | unless content_type()
301 | warn 'net/http: warning: Content-Type did not set; using application/x-www-form-urlencoded' if $VERBOSE
302 | set_content_type 'application/x-www-form-urlencoded'
303 | end
304 | write_header sock, ver, path
305 | if chunked?
306 | while s = f.read(chunk_size)
307 | sock.write(sprintf("%x\r\n", s.length) << s << "\r\n")
308 | end
309 | sock.write "0\r\n\r\n"
310 | else
311 | while s = f.read(chunk_size)
312 | sock.write s
313 | end
314 | end
315 | end
316 |
317 | def chunk_size
318 | 1048576 # 1 megabyte
319 | end
320 | end
321 |
322 | # Net::HTTP before 1.8.4 doesn't have the use_ssl? method or the Delete request type
323 | class HTTP
324 | def use_ssl?
325 | @use_ssl
326 | end unless public_method_defined? :use_ssl?
327 |
328 | class Delete < HTTPRequest
329 | METHOD = 'DELETE'
330 | REQUEST_HAS_BODY = false
331 | RESPONSE_HAS_BODY = true
332 | end unless const_defined? :Delete
333 | end
334 | end
335 |
336 | class XmlGenerator < String #:nodoc:
337 | attr_reader :xml
338 | def initialize
339 | @xml = Builder::XmlMarkup.new(:indent => 2, :target => self)
340 | super()
341 | build
342 | end
343 | end
344 | #:startdoc:
345 |
--------------------------------------------------------------------------------
/lib/aws/s3/base.rb:
--------------------------------------------------------------------------------
1 | module AWS #:nodoc:
2 | # AWS::S3 is a Ruby library for Amazon's Simple Storage Service's REST API (http://aws.amazon.com/s3).
3 | # Full documentation of the currently supported API can be found at http://docs.amazonwebservices.com/AmazonS3/2006-03-01.
4 | #
5 | # == Getting started
6 | #
7 | # To get started you need to require 'aws/s3':
8 | #
9 | # % irb -rubygems
10 | # irb(main):001:0> require 'aws/s3'
11 | # # => true
12 | #
13 | # 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.
14 | #
15 | # % s3sh
16 | # >> Version
17 | #
18 | # Before you can do anything, you must establish a connection using Base.establish_connection!. A basic connection would look something like this:
19 | #
20 | # AWS::S3::Base.establish_connection!(
21 | # :access_key_id => 'abc',
22 | # :secret_access_key => '123'
23 | # )
24 | #
25 | # The minimum connection options that you must specify are your access key id and your secret access key.
26 | #
27 | # (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.)
28 | #
29 | # 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:
30 | #
31 | # % cat .amazon_keys
32 | # export AMAZON_ACCESS_KEY_ID='abcdefghijklmnop'
33 | # export AMAZON_SECRET_ACCESS_KEY='1234567891012345'
34 | #
35 | # Then load it in your shell's rc file.
36 | #
37 | # % cat .zshrc
38 | # if [[ -f "$HOME/.amazon_keys" ]]; then
39 | # source "$HOME/.amazon_keys";
40 | # fi
41 | #
42 | # See more connection details at AWS::S3::Connection::Management::ClassMethods.
43 | module S3
44 | constant :DEFAULT_HOST, 's3.amazonaws.com'
45 |
46 | # AWS::S3::Base is the abstract super class of all classes who make requests against S3, such as the built in
47 | # Service, Bucket and S3Object classes. It provides methods for making requests, inferring or setting response classes,
48 | # processing request options, and accessing attributes from S3's response data.
49 | #
50 | # Establishing a connection with the Base class is the entry point to using the library:
51 | #
52 | # AWS::S3::Base.establish_connection!(:access_key_id => '...', :secret_access_key => '...')
53 | #
54 | # The :access_key_id and :secret_access_key are the two required connection options. More
55 | # details can be found in the docs for Connection::Management::ClassMethods.
56 | #
57 | # Extensive examples can be found in the README[link:files/README.html].
58 | class Base
59 | class << self
60 | # Wraps the current connection's request method and picks the appropriate response class to wrap the response in.
61 | # If the response is an error, it will raise that error as an exception. All such exceptions can be caught by rescuing
62 | # their superclass, the ResponseError exception class.
63 | #
64 | # It is unlikely that you would call this method directly. Subclasses of Base have convenience methods for each http request verb
65 | # that wrap calls to request.
66 | def request(verb, path, options = {}, body = nil, attempts = 0, &block)
67 | Service.response = nil
68 | process_options!(options, verb)
69 | response = response_class.new(connection.request(verb, path, options, body, attempts, &block))
70 | Service.response = response
71 |
72 | Error::Response.new(response.response).error.raise if response.error?
73 | response
74 | # Once in a while, a request to S3 returns an internal error. A glitch in the matrix I presume. Since these
75 | # errors are few and far between the request method will rescue InternalErrors the first three times they encouter them
76 | # and will retry the request again. Most of the time the second attempt will work.
77 | rescue InternalError, RequestTimeout
78 | if attempts == 3
79 | raise
80 | else
81 | attempts += 1
82 | retry
83 | end
84 | end
85 |
86 | [:get, :post, :put, :delete, :head].each do |verb|
87 | class_eval(<<-EVAL, __FILE__, __LINE__)
88 | def #{verb}(path, headers = {}, body = nil, &block)
89 | request(:#{verb}, path, headers, body, &block)
90 | end
91 | EVAL
92 | end
93 |
94 | # Called when a method which requires a bucket name is called without that bucket name specified. It will try to
95 | # infer the current bucket by looking for it as the subdomain of the current connection's address. If no subdomain
96 | # is found, CurrentBucketNotSpecified will be raised.
97 | #
98 | # MusicBucket.establish_connection! :server => 'jukeboxzero.s3.amazonaws.com'
99 | # MusicBucket.connection.server
100 | # => 'jukeboxzero.s3.amazonaws.com'
101 | # MusicBucket.current_bucket
102 | # => 'jukeboxzero'
103 | #
104 | # Rather than infering the current bucket from the subdomain, the current class' bucket can be explicitly set with
105 | # set_current_bucket_to.
106 | def current_bucket
107 | connection.subdomain or raise CurrentBucketNotSpecified.new(connection.http.address)
108 | end
109 |
110 | # If you plan on always using a specific bucket for certain files, you can skip always having to specify the bucket by creating
111 | # a subclass of Bucket or S3Object and telling it what bucket to use:
112 | #
113 | # class JukeBoxSong < AWS::S3::S3Object
114 | # set_current_bucket_to 'jukebox'
115 | # end
116 | #
117 | # For all methods that take a bucket name as an argument, the current bucket will be used if the bucket name argument is omitted.
118 | #
119 | # other_song = 'baby-please-come-home.mp3'
120 | # JukeBoxSong.store(other_song, open(other_song))
121 | #
122 | # This time we didn't have to explicitly pass in the bucket name, as the JukeBoxSong class knows that it will
123 | # always use the 'jukebox' bucket.
124 | #
125 | # "Astute readers", as they say, may have noticed that we used the third parameter to pass in the content type,
126 | # rather than the fourth parameter as we had the last time we created an object. If the bucket can be inferred, or
127 | # is explicitly set, as we've done in the JukeBoxSong class, then the third argument can be used to pass in
128 | # options.
129 | #
130 | # Now all operations that would have required a bucket name no longer do.
131 | #
132 | # other_song = JukeBoxSong.find('baby-please-come-home.mp3')
133 | def set_current_bucket_to(name)
134 | raise ArgumentError, "`#{__method__}' must be called on a subclass of #{self.name}" if self == AWS::S3::Base
135 | instance_eval(<<-EVAL)
136 | def current_bucket
137 | '#{name}'
138 | end
139 | EVAL
140 | end
141 | alias_method :current_bucket=, :set_current_bucket_to
142 |
143 | private
144 |
145 | def response_class
146 | FindResponseClass.for(self)
147 | end
148 |
149 | def process_options!(options, verb)
150 | options.replace(RequestOptions.process(options, verb))
151 | end
152 |
153 | # Using the conventions layed out in the response_class works for more than 80% of the time.
154 | # There are a few edge cases though where we want a given class to wrap its responses in different
155 | # response classes depending on which method is being called.
156 | def respond_with(klass)
157 | eval(<<-EVAL, binding, __FILE__, __LINE__)
158 | def new_response_class
159 | #{klass}
160 | end
161 |
162 | class << self
163 | alias_method :old_response_class, :response_class
164 | alias_method :response_class, :new_response_class
165 | end
166 | EVAL
167 |
168 | yield
169 | ensure
170 | # Restore the original version
171 | eval(<<-EVAL, binding, __FILE__, __LINE__)
172 | class << self
173 | alias_method :response_class, :old_response_class
174 | end
175 | EVAL
176 | end
177 |
178 | def bucket_name(name)
179 | name || current_bucket
180 | end
181 |
182 | class RequestOptions < Hash #:nodoc:
183 | attr_reader :options, :verb
184 |
185 | class << self
186 | def process(*args, &block)
187 | new(*args, &block).process!
188 | end
189 | end
190 |
191 | def initialize(options, verb = :get)
192 | @options = options.to_normalized_options
193 | @verb = verb
194 | super()
195 | end
196 |
197 | def process!
198 | set_access_controls! if verb == :put
199 | replace(options)
200 | end
201 |
202 | private
203 | def set_access_controls!
204 | ACL::OptionProcessor.process!(options)
205 | end
206 | end
207 | end
208 |
209 | def initialize(attributes = {}) #:nodoc:
210 | @attributes = attributes
211 | end
212 |
213 | private
214 | attr_reader :attributes
215 |
216 | def connection
217 | self.class.connection
218 | end
219 |
220 | def http
221 | connection.http
222 | end
223 |
224 | def request(*args, &block)
225 | self.class.request(*args, &block)
226 | end
227 |
228 | def method_missing(method, *args, &block)
229 | case
230 | when attributes.has_key?(method.to_s)
231 | attributes[method.to_s]
232 | when attributes.has_key?(method)
233 | attributes[method]
234 | else
235 | super
236 | end
237 | end
238 | end
239 | end
240 | end
241 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'rake'
3 | require 'rake/testtask'
4 | require 'rake/rdoctask'
5 | require 'rake/packagetask'
6 | require 'rake/gempackagetask'
7 |
8 | require File.dirname(__FILE__) + '/lib/aws/s3'
9 |
10 | def library_root
11 | File.dirname(__FILE__)
12 | end
13 |
14 | task :default => :test
15 |
16 | Rake::TestTask.new do |test|
17 | test.pattern = 'test/*_test.rb'
18 | test.verbose = true
19 | end
20 |
21 | namespace :doc do
22 | Rake::RDocTask.new do |rdoc|
23 | rdoc.rdoc_dir = 'doc'
24 | rdoc.title = "AWS::S3 -- Support for Amazon S3's REST api"
25 | rdoc.options << '--line-numbers' << '--inline-source'
26 | rdoc.rdoc_files.include('README')
27 | rdoc.rdoc_files.include('COPYING')
28 | rdoc.rdoc_files.include('INSTALL')
29 | rdoc.rdoc_files.include('lib/**/*.rb')
30 | end
31 |
32 | task :rdoc => 'doc:readme'
33 |
34 | task :refresh => :rerdoc do
35 | system 'open doc/index.html'
36 | end
37 |
38 | task :readme do
39 | require 'support/rdoc/code_info'
40 | RDoc::CodeInfo.parse('lib/**/*.rb')
41 |
42 | strip_comments = lambda {|comment| comment.gsub(/^# ?/, '')}
43 | docs_for = lambda do |location|
44 | info = RDoc::CodeInfo.for(location)
45 | raise RuntimeError, "Couldn't find documentation for `#{location}'" unless info
46 | strip_comments[info.comment]
47 | end
48 |
49 | open('README', 'w') do |file|
50 | file.write ERB.new(IO.read('README.erb')).result(binding)
51 | end
52 | end
53 |
54 | task :deploy => :rerdoc do
55 | sh %(scp -r doc marcel@rubyforge.org:/var/www/gforge-projects/amazon/)
56 | end
57 | end
58 |
59 | namespace :dist do
60 | spec = Gem::Specification.new do |s|
61 | s.name = 'aws-s3'
62 | s.version = Gem::Version.new(AWS::S3::Version)
63 | s.summary = "Client library for Amazon's Simple Storage Service's REST API"
64 | s.description = s.summary
65 | s.email = 'marcel@vernix.org'
66 | s.author = 'Marcel Molina Jr.'
67 | s.has_rdoc = true
68 | s.extra_rdoc_files = %w(README COPYING INSTALL)
69 | s.homepage = 'http://amazon.rubyforge.org'
70 | s.rubyforge_project = 'amazon'
71 | s.files = FileList['Rakefile', 'lib/**/*.rb', 'bin/*', 'support/**/*.rb']
72 | s.executables << 's3sh'
73 | s.test_files = Dir['test/**/*']
74 |
75 | s.add_dependency 'xml-simple'
76 | s.add_dependency 'builder'
77 | s.add_dependency 'mime-types'
78 | s.rdoc_options = ['--title', "AWS::S3 -- Support for Amazon S3's REST api",
79 | '--main', 'README',
80 | '--line-numbers', '--inline-source']
81 | s.license = "MIT"
82 | end
83 |
84 | # Regenerate README before packaging
85 | task :package => 'doc:readme'
86 | Rake::GemPackageTask.new(spec) do |pkg|
87 | pkg.need_tar_gz = true
88 | pkg.package_files.include('{lib,script,test,support}/**/*')
89 | pkg.package_files.include('README')
90 | pkg.package_files.include('COPYING')
91 | pkg.package_files.include('INSTALL')
92 | pkg.package_files.include('Rakefile')
93 | end
94 |
95 | desc 'Install with gems'
96 | task :install => :repackage do
97 | sh "sudo gem i pkg/#{spec.name}-#{spec.version}.gem"
98 | end
99 |
100 | desc 'Uninstall gem'
101 | task :uninstall do
102 | sh "sudo gem uninstall #{spec.name} -x"
103 | end
104 |
105 | desc 'Reinstall gem'
106 | task :reinstall => [:uninstall, :install]
107 |
108 | task :confirm_release do
109 | print "Releasing version #{spec.version}. Are you sure you want to proceed? [Yn] "
110 | abort if STDIN.getc == ?n
111 | end
112 |
113 | desc 'Tag release'
114 | task :tag do
115 | sh %(git tag -a '#{spec.version}-release' -m 'Tagging #{spec.version} release')
116 | sh 'git push --tags'
117 | end
118 |
119 | desc 'Update changelog to include a release marker'
120 | task :add_release_marker_to_changelog do
121 | changelog = IO.read('CHANGELOG')
122 | changelog.sub!(/^head:/, "#{spec.version}:")
123 |
124 | open('CHANGELOG', 'w') do |file|
125 | file.write "head:\n\n#{changelog}"
126 | end
127 | end
128 |
129 | task :commit_changelog do
130 | sh %(git commit CHANGELOG -m "Bump changelog version marker for release")
131 | sh 'git push'
132 | end
133 |
134 | package_name = lambda {|specification| File.join('pkg', "#{specification.name}-#{specification.version}")}
135 |
136 | desc 'Push a release to rubyforge'
137 | task :release => [:confirm_release, :clean, :add_release_marker_to_changelog, :package, :commit_changelog, :tag] do
138 | require 'rubyforge'
139 | package = package_name[spec]
140 |
141 | rubyforge = RubyForge.new.configure
142 | rubyforge.login
143 |
144 | user_config = rubyforge.userconfig
145 | user_config['release_changes'] = YAML.load_file('CHANGELOG')[spec.version.to_s].join("\n")
146 |
147 | version_already_released = lambda do
148 | releases = rubyforge.autoconfig['release_ids']
149 | releases.has_key?(spec.name) && releases[spec.name][spec.version.to_s]
150 | end
151 |
152 | abort("Release #{spec.version} already exists!") if version_already_released.call
153 |
154 | begin
155 | rubyforge.add_release(spec.rubyforge_project, spec.name, spec.version.to_s, "#{package}.tar.gz", "#{package}.gem")
156 | puts "Version #{spec.version} released!"
157 | rescue Exception => exception
158 | puts 'Release failed!'
159 | raise
160 | end
161 | end
162 |
163 | desc 'Upload a beta gem'
164 | task :push_beta_gem => [:clobber_package, :package] do
165 | beta_gem = package_name[spec]
166 | sh %(scp #{beta_gem}.gem marcel@rubyforge.org:/var/www/gforge-projects/amazon/beta)
167 | end
168 |
169 | task :spec do
170 | puts spec.to_ruby
171 | end
172 | end
173 |
174 | desc 'Check code to test ratio'
175 | task :stats do
176 | library_files = FileList["#{library_root}/lib/**/*.rb"]
177 | test_files = FileList["#{library_root}/test/**/*_test.rb"]
178 | count_code_lines = Proc.new do |lines|
179 | lines.inject(0) do |code_lines, line|
180 | next code_lines if [/^\s*$/, /^\s*#/].any? {|non_code_line| non_code_line === line}
181 | code_lines + 1
182 | end
183 | end
184 |
185 | count_code_lines_for_files = Proc.new do |files|
186 | files.inject(0) {|code_lines, file| code_lines + count_code_lines[IO.read(file)]}
187 | end
188 |
189 | library_code_lines = count_code_lines_for_files[library_files]
190 | test_code_lines = count_code_lines_for_files[test_files]
191 | ratio = Proc.new { sprintf('%.2f', test_code_lines.to_f / library_code_lines)}
192 |
193 | puts "Code LOC: #{library_code_lines} Test LOC: #{test_code_lines} Code to Test Ratio: 1:#{ratio.call}"
194 | end
195 |
196 | namespace :test do
197 | find_file = lambda do |name|
198 | file_name = lambda {|path| File.join(path, "#{name}.rb")}
199 | root = $:.detect do |path|
200 | File.exist?(file_name[path])
201 | end
202 | file_name[root] if root
203 | end
204 |
205 | TEST_LOADER = find_file['rake/rake_test_loader']
206 | multiruby = lambda do |glob|
207 | system 'multiruby', TEST_LOADER, *Dir.glob(glob)
208 | end
209 |
210 | desc 'Check test coverage'
211 | task :coverage do
212 | system("rcov -x Library -x support --sort coverage #{File.join(library_root, 'test/*_test.rb')}")
213 | show_test_coverage_results
214 | end
215 |
216 | Rake::TestTask.new(:remote) do |test|
217 | test.pattern = 'test/remote/*_test.rb'
218 | test.verbose = true
219 | end
220 |
221 | Rake::TestTask.new(:all) do |test|
222 | test.pattern = 'test/**/*_test.rb'
223 | test.verbose = true
224 | end
225 |
226 | desc 'Check test coverage of full stack remote tests'
227 | task :full_coverage do
228 | system("rcov -x Library -x support --sort coverage #{File.join(library_root, 'test/remote/*_test.rb')} #{File.join(library_root, 'test/*_test.rb')}")
229 | show_test_coverage_results
230 | end
231 |
232 | desc 'Run local tests against multiple versions of Ruby'
233 | task :version_audit do
234 | multiruby['test/*_test.rb']
235 | end
236 |
237 | namespace :version_audit do
238 | desc 'Run remote tests against multiple versions of Ruby'
239 | task :remote do
240 | multiruby['test/remote/*_test.rb']
241 | end
242 |
243 | desc 'Run all tests against multiple versions of Ruby'
244 | task :all do
245 | multiruby['test/**/*_test.rb']
246 | end
247 | end
248 |
249 | def show_test_coverage_results
250 | system("open #{File.join(library_root, 'coverage/index.html')}") if PLATFORM['darwin']
251 | end
252 |
253 | desc 'Remove coverage products'
254 | task :clobber_coverage do
255 | rm_r 'coverage' rescue nil
256 | end
257 | end
258 |
259 | namespace :todo do
260 | class << TODOS = IO.read(File.join(library_root, 'TODO'))
261 | def items
262 | split("\n").grep(/^\[\s|X\]/)
263 | end
264 |
265 | def completed
266 | find_items_matching(/^\[X\]/)
267 | end
268 |
269 | def uncompleted
270 | find_items_matching(/^\[\s\]/)
271 | end
272 |
273 | def find_items_matching(regexp)
274 | items.grep(regexp).instance_eval do
275 | def display
276 | puts map {|item| "* #{item.sub(/^\[[^\]]\]\s/, '')}"}
277 | end
278 | self
279 | end
280 | end
281 | end
282 |
283 | desc 'Completed todo items'
284 | task :completed do
285 | TODOS.completed.display
286 | end
287 |
288 | desc 'Incomplete todo items'
289 | task :uncompleted do
290 | TODOS.uncompleted.display
291 | end
292 | end if File.exists?(File.join(library_root, 'TODO'))
293 |
294 | namespace :site do
295 | require 'erb'
296 | require 'rdoc/markup/simple_markup'
297 | require 'rdoc/markup/simple_markup/to_html'
298 |
299 | readme = lambda { IO.read('README')[/^== Getting started\n(.*)/m, 1] }
300 |
301 | readme_to_html = lambda do
302 | handler = SM::ToHtml.new
303 | handler.instance_eval do
304 | require 'syntax'
305 | require 'syntax/convertors/html'
306 | def accept_verbatim(am, fragment)
307 | syntax = Syntax::Convertors::HTML.for_syntax('ruby')
308 | @res << %(#{syntax.convert(fragment.txt, true)}
)
309 | end
310 | end
311 | SM::SimpleMarkup.new.convert(readme.call, handler)
312 | end
313 |
314 | desc 'Regenerate the public website page'
315 | task :build => 'doc:readme' do
316 | open('site/public/index.html', 'w') do |file|
317 | erb_data = {}
318 | erb_data[:readme] = readme_to_html.call
319 | file.write ERB.new(IO.read('site/index.erb')).result(binding)
320 | end
321 | end
322 |
323 | task :refresh => :build do
324 | system 'open site/public/index.html'
325 | end
326 |
327 | desc 'Update the live website'
328 | task :deploy => :build do
329 | site_files = FileList['site/public/*']
330 | site_files.delete_if {|file| File.directory?(file)}
331 | sh %(scp #{site_files.join ' '} marcel@rubyforge.org:/var/www/gforge-projects/amazon/)
332 | end
333 | end
334 |
335 | task :clean => ['dist:clobber_package', 'doc:clobber_rdoc', 'test:clobber_coverage']
336 |
--------------------------------------------------------------------------------
/lib/aws/s3/authentication.rb:
--------------------------------------------------------------------------------
1 | module AWS
2 | module S3
3 | # All authentication is taken care of for you by the AWS::S3 library. None the less, some details of the two types
4 | # of authentication and when they are used may be of interest to some.
5 | #
6 | # === Header based authentication
7 | #
8 | # Header based authentication is achieved by setting a special Authorization header whose value
9 | # is formatted like so:
10 | #
11 | # "AWS #{access_key_id}:#{encoded_canonical}"
12 | #
13 | # The access_key_id is the public key that is assigned by Amazon for a given account which you use when
14 | # establishing your initial connection. The encoded_canonical is computed according to rules layed out
15 | # by Amazon which we will describe presently.
16 | #
17 | # ==== Generating the encoded canonical string
18 | #
19 | # The "canonical string", generated by the CanonicalString class, is computed by collecting the current request method,
20 | # a set of significant headers of the current request, and the current request path into a string.
21 | # That canonical string is then encrypted with the secret_access_key assigned by Amazon. The resulting encrypted canonical
22 | # string is then base 64 encoded.
23 | #
24 | # === Query string based authentication
25 | #
26 | # When accessing a restricted object from the browser, you can authenticate via the query string, by setting the following parameters:
27 | #
28 | # "AWSAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}"
29 | #
30 | # The QueryString class is responsible for generating the appropriate parameters for authentication via the
31 | # query string.
32 | #
33 | # The access_key_id and encoded_canonical are the same as described in the Header based authentication section.
34 | # The expires value dictates for how long the current url is valid (by default, it will expire in 5 minutes). Expiration can be specified
35 | # either by an absolute time (expressed in seconds since the epoch), or in relative time (in number of seconds from now).
36 | # Details of how to customize the expiration of the url are provided in the documentation for the QueryString class.
37 | #
38 | # All requests made by this library use header authentication. When a query string authenticated url is needed,
39 | # the S3Object#url method will include the appropriate query string parameters.
40 | #
41 | # === Full authentication specification
42 | #
43 | # The full specification of the authentication protocol can be found at
44 | # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAuthentication.html
45 | class Authentication
46 | constant :AMAZON_HEADER_PREFIX, 'x-amz-'
47 |
48 | # Signature is the abstract super class for the Header and QueryString authentication methods. It does the job
49 | # of computing the canonical_string using the CanonicalString class as well as encoding the canonical string. The subclasses
50 | # parameterize these computations and arrange them in a string form appropriate to how they are used, in one case a http request
51 | # header value, and in the other case key/value query string parameter pairs.
52 | class Signature < String #:nodoc:
53 | attr_reader :request, :access_key_id, :secret_access_key, :options
54 |
55 | def initialize(request, access_key_id, secret_access_key, options = {})
56 | super()
57 | @request, @access_key_id, @secret_access_key = request, access_key_id, secret_access_key
58 | @options = options
59 | end
60 |
61 | private
62 |
63 | def canonical_string
64 | options = {}
65 | options[:expires] = expires if expires?
66 | CanonicalString.new(request, options)
67 | end
68 | memoized :canonical_string
69 |
70 | def encoded_canonical
71 | klass = OpenSSL::Digest.respond_to?(:new) ? OpenSSL::Digest : OpenSSL::Digest::Digest
72 | digest = klass.new('sha1')
73 | b64_hmac = [OpenSSL::HMAC.digest(digest, secret_access_key, canonical_string)].pack("m").strip
74 | url_encode? ? CGI.escape(b64_hmac) : b64_hmac
75 | end
76 |
77 | def url_encode?
78 | !@options[:url_encode].nil?
79 | end
80 |
81 | def expires?
82 | is_a? QueryString
83 | end
84 |
85 | def date
86 | request['date'].to_s.strip.empty? ? Time.now : Time.parse(request['date'])
87 | end
88 | end
89 |
90 | # Provides header authentication by computing the value of the Authorization header. More details about the
91 | # various authentication schemes can be found in the docs for its containing module, Authentication.
92 | class Header < Signature #:nodoc:
93 | def initialize(*args)
94 | super
95 | self << "AWS #{access_key_id}:#{encoded_canonical}"
96 | end
97 | end
98 |
99 | # Provides query string authentication by computing the three authorization parameters: AWSAccessKeyId, Expires and Signature.
100 | # More details about the various authentication schemes can be found in the docs for its containing module, Authentication.
101 | class QueryString < Signature #:nodoc:
102 | constant :DEFAULT_EXPIRY, 300 # 5 minutes
103 | def initialize(*args)
104 | super
105 | options[:url_encode] = true
106 | self << build
107 | end
108 |
109 | private
110 |
111 | # Will return one of three values, in the following order of precedence:
112 | #
113 | # 1) Seconds since the epoch explicitly passed in the +:expires+ option
114 | # 2) The current time in seconds since the epoch plus the number of seconds passed in
115 | # the +:expires_in+ option
116 | # 3) The current time in seconds since the epoch plus the default number of seconds (60 seconds)
117 | def expires
118 | return options[:expires] if options[:expires]
119 | date.to_i + expires_in
120 | end
121 |
122 | def expires_in
123 | options.has_key?(:expires_in) ? Integer(options[:expires_in]) : DEFAULT_EXPIRY
124 | end
125 |
126 | def build
127 | "AWSAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}"
128 | end
129 | end
130 |
131 | # The CanonicalString is used to generate an encrypted signature, signed with your secrect access key. It is composed of
132 | # data related to the given request for which it provides authentication. This data includes the request method, request headers,
133 | # and the request path. Both Header and QueryString use it to generate their signature.
134 | class CanonicalString < String #:nodoc:
135 | class << self
136 | def default_headers
137 | %w(content-type content-md5)
138 | end
139 |
140 | def interesting_headers
141 | ['content-md5', 'content-type', 'date', amazon_header_prefix]
142 | end
143 |
144 | def amazon_header_prefix
145 | /^#{AMAZON_HEADER_PREFIX}/io
146 | end
147 |
148 | def query_parameters
149 | %w(acl location logging notification partNumber policy
150 | requestPayment torrent uploadId uploads versionId
151 | versioning versions delete lifecycle tagging cors
152 | response-content-type response-content-language
153 | response-expires response-cache-control
154 | response-content-disposition response-content-encoding)
155 | end
156 |
157 | def query_parameters_for_signature(params)
158 | params.select {|k, v| query_parameters.include?(k)}
159 | end
160 |
161 | def resource_parameters
162 | Set.new %w(acl logging torrent)
163 | end
164 |
165 | memoized :default_headers
166 | memoized :interesting_headers
167 | memoized :query_parameters
168 | memoized :resource_parameters
169 | end
170 |
171 | attr_reader :request, :headers
172 |
173 | def initialize(request, options = {})
174 | super()
175 | @request = request
176 | @headers = {}
177 | @options = options
178 | # "For non-authenticated or anonymous requests. A NotImplemented error result code will be returned if
179 | # an authenticated (signed) request specifies a Host: header other than 's3.amazonaws.com'"
180 | # (from http://docs.amazonwebservices.com/AmazonS3/2006-03-01/VirtualHosting.html)
181 | request['Host'] = DEFAULT_HOST
182 | build
183 | end
184 |
185 | private
186 | def build
187 | self << "#{request.method}\n"
188 | ensure_date_is_valid
189 |
190 | initialize_headers
191 | set_expiry!
192 |
193 | headers.sort_by {|k, _| k}.each do |key, value|
194 | value = value.to_s.strip
195 | self << (key =~ self.class.amazon_header_prefix ? "#{key}:#{value}" : value)
196 | self << "\n"
197 | end
198 | self << path
199 | end
200 |
201 | def initialize_headers
202 | identify_interesting_headers
203 | set_default_headers
204 | end
205 |
206 | def set_expiry!
207 | self.headers['date'] = @options[:expires] if @options[:expires]
208 | end
209 |
210 | def ensure_date_is_valid
211 | request['Date'] ||= Time.now.httpdate
212 | end
213 |
214 | def identify_interesting_headers
215 | request.each do |key, value|
216 | key = key.downcase # Can't modify frozen string so no bang
217 | if self.class.interesting_headers.any? {|header| header === key}
218 | self.headers[key] = value.to_s.strip
219 | end
220 | end
221 | end
222 |
223 | def set_default_headers
224 | self.class.default_headers.each do |header|
225 | self.headers[header] ||= ''
226 | end
227 | end
228 |
229 | def path
230 | [only_path, extract_significant_parameter].compact.join('?')
231 | end
232 |
233 | def extract_significant_parameter
234 | query = URI.parse(request.path).query
235 | return nil if query.nil?
236 | params = CGI.parse(query) #this automatically unescapes query params
237 | params = self.class.query_parameters_for_signature(params).to_a
238 | return nil if params.empty?
239 | params.sort! { |(x_key, _), (y_key, _)| x_key <=> y_key }
240 | params.map! do |(key, value)|
241 | if value.nil? || resource_parameter?(key)
242 | key
243 | else
244 | value = value.join if value.respond_to?(:join)
245 | "#{key}=#{value}"
246 | end
247 | end.join("&")
248 | end
249 |
250 | def resource_parameter?(key)
251 | self.class.resource_parameters.include? key
252 | end
253 |
254 | def only_path
255 | request.path[/^[^?]*/]
256 | end
257 | end
258 | end
259 | end
260 | end
261 |
--------------------------------------------------------------------------------
/test/remote/object_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/test_helper'
2 |
3 | class RemoteS3ObjectTest < Test::Unit::TestCase
4 | def setup
5 | establish_real_connection
6 | end
7 |
8 | def teardown
9 | disconnect!
10 | end
11 |
12 | def test_object
13 | key = 'testing_s3objects'
14 | value = 'testing'
15 | content_type = 'text/plain'
16 | unauthenticated_url = ['http:/', Base.connection.http.address, TEST_BUCKET, key].join('/')
17 |
18 | # Create an object
19 |
20 | response = nil
21 | assert_nothing_raised do
22 | response = S3Object.create(key, value, TEST_BUCKET, :access => :public_read, :content_type => content_type)
23 | end
24 |
25 | # Check response
26 |
27 | assert response.success?
28 |
29 | # Extract the object's etag
30 |
31 | etag = nil
32 | assert_nothing_raised do
33 | etag = response.etag
34 | end
35 |
36 | assert etag
37 |
38 | # Confirm we can't create an object unless the bucket is set
39 |
40 | assert_raises(NoBucketSpecified) do
41 | object = S3Object.new
42 | object.key = 'hello'
43 | object.store
44 | end
45 |
46 | # Fetch newly created object to show it was actually created
47 |
48 | object = nil
49 | assert_nothing_raised do
50 | object = S3Object.find(key, TEST_BUCKET)
51 | end
52 |
53 | assert object
54 |
55 | # Confirm it has the right etag
56 |
57 | assert_equal etag, object.etag
58 |
59 | # Check if its owner is properly set
60 |
61 | assert_nothing_raised do
62 | object.owner.display_name
63 | end
64 |
65 | # Confirm we can get the object's key
66 |
67 | assert_equal key, object.key
68 |
69 | # Confirm its value was properly set
70 |
71 | assert_equal value, object.value
72 | assert_equal value, S3Object.value(key, TEST_BUCKET)
73 | streamed_value = ''
74 | assert_nothing_raised do
75 | S3Object.stream(key, TEST_BUCKET) do |segment|
76 | streamed_value << segment
77 | end
78 | end
79 |
80 | assert_equal value, streamed_value
81 |
82 | # Change its value
83 |
84 | new_value = ""
85 | assert_nothing_raised do
86 | object.value = new_value
87 | end
88 | assert_equal new_value, object.value
89 |
90 | # Confirm content type was properly set
91 |
92 | assert_equal content_type, object.content_type
93 |
94 | # Change its content type
95 |
96 | new_content_type = 'text/javascript'
97 | assert_nothing_raised do
98 | object.content_type = new_content_type
99 | end
100 |
101 | assert_equal new_content_type, object.content_type
102 |
103 | # Test that it is publicly readable
104 |
105 | response = fetch_object_at(unauthenticated_url)
106 | assert (200..299).include?(response.code.to_i)
107 |
108 | # Confirm that it has no meta data
109 |
110 | assert object.metadata.empty?
111 |
112 | # Set some meta data
113 |
114 | metadata_key = :secret_sauce
115 | metadata_value = "it's a secret"
116 | object.metadata[metadata_key] = metadata_value
117 |
118 | # Persist all changes
119 |
120 | assert_nothing_raised do
121 | object.store
122 | end
123 |
124 | # Refetch the object
125 |
126 | key = object.key
127 | object = nil
128 | assert_nothing_raised do
129 | object = S3Object.find(key, TEST_BUCKET)
130 | end
131 |
132 | # Confirm all changes were persisted
133 |
134 | assert object
135 | assert_equal key, object.key
136 |
137 | assert_equal new_content_type, object.content_type
138 |
139 | assert_equal new_value, object.value
140 | assert_equal new_value, object.value(:reload)
141 |
142 | assert !object.metadata.empty?
143 | assert_equal metadata_value, object.metadata[metadata_key]
144 |
145 | # Change acl
146 |
147 | assert_nothing_raised do
148 | S3Object.create(object.key, object.value, TEST_BUCKET, :access => :private, :content_type => object.content_type)
149 | end
150 |
151 | # Confirm object is no longer publicly readable
152 |
153 | response = fetch_object_at(unauthenticated_url)
154 | assert (400..499).include?(response.code.to_i)
155 |
156 | # Confirm object is accessible from its authenticated url
157 |
158 | response = fetch_object_at(object.url)
159 | assert (200..299).include?(response.code.to_i)
160 |
161 | # Copy the object
162 |
163 | assert_nothing_raised do
164 | object.copy('testing_s3objects-copy')
165 | end
166 |
167 | # Confirm the object is identical
168 |
169 | copy = nil
170 | assert_nothing_raised do
171 | copy = S3Object.find('testing_s3objects-copy', TEST_BUCKET)
172 | end
173 |
174 | assert copy
175 |
176 | assert_equal object.value, copy.value
177 | assert_equal object.content_type, copy.content_type
178 |
179 | # Delete object
180 |
181 | assert_nothing_raised do
182 | object.delete
183 | end
184 |
185 | # Confirm we can rename objects
186 |
187 | renamed_to = copy.key + '-renamed'
188 | renamed_value = copy.value
189 | assert_nothing_raised do
190 | S3Object.rename(copy.key, renamed_to, TEST_BUCKET)
191 | end
192 |
193 | # Confirm renamed copy exists
194 |
195 | renamed = nil
196 | assert_nothing_raised do
197 | renamed = S3Object.find(renamed_to, TEST_BUCKET)
198 | end
199 |
200 | assert renamed
201 | assert_equal renamed_value, renamed.value
202 |
203 | # Confirm copy is deleted
204 |
205 | assert_raises(NoSuchKey) do
206 | S3Object.find(copy.key, TEST_BUCKET)
207 | end
208 |
209 | # Confirm that you can not store an object once it is deleted
210 |
211 | assert_raises(DeletedObject) do
212 | object.store
213 | end
214 |
215 | assert_raises(NoSuchKey) do
216 | S3Object.find(key, TEST_BUCKET)
217 | end
218 |
219 | # Confirm we can pass in an IO stream and have the uploading sent in chunks
220 |
221 | response = nil
222 | test_file_key = File.basename(TEST_FILE)
223 | assert_nothing_raised do
224 | response = S3Object.store(test_file_key, open(TEST_FILE), TEST_BUCKET)
225 | end
226 | assert response.success?
227 |
228 | assert_equal File.size(TEST_FILE), Integer(S3Object.about(test_file_key, TEST_BUCKET)['content-length'])
229 |
230 | result = nil
231 | assert_nothing_raised do
232 | result = S3Object.delete(test_file_key, TEST_BUCKET)
233 | end
234 |
235 | assert result
236 | end
237 |
238 | def test_content_type_inference
239 | # Confirm appropriate content type is inferred when not specified
240 |
241 | content_type_objects = {'foo.jpg' => 'image/jpeg', 'no-extension-specified' => 'binary/octet-stream', 'foo.txt' => 'text/plain'}
242 | content_type_objects.each_key do |key|
243 | S3Object.store(key, 'fake data', TEST_BUCKET) # No content type explicitly set
244 | end
245 |
246 | content_type_objects.each do |key, content_type|
247 | assert_equal content_type, S3Object.about(key, TEST_BUCKET)['content-type']
248 | end
249 |
250 | # Confirm we can update the content type
251 |
252 | assert_nothing_raised do
253 | object = S3Object.find('no-extension-specified', TEST_BUCKET)
254 | object.content_type = 'application/pdf'
255 | object.store
256 | end
257 |
258 | assert_equal 'application/pdf', S3Object.about('no-extension-specified', TEST_BUCKET)['content-type']
259 |
260 | ensure
261 | # Get rid of objects we just created
262 | content_type_objects.each_key {|key| S3Object.delete(key, TEST_BUCKET) }
263 | end
264 |
265 | def test_body_can_be_more_than_just_string_or_io
266 | require 'stringio'
267 | key = 'testing-body-as-string-io'
268 | io = StringIO.new('hello there')
269 | S3Object.store(key, io, TEST_BUCKET)
270 | assert_equal 'hello there', S3Object.value(key, TEST_BUCKET)
271 | ensure
272 | S3Object.delete(key, TEST_BUCKET)
273 | end
274 |
275 | def test_fetching_information_about_an_object_that_does_not_exist_raises_no_such_key
276 | assert_raises(NoSuchKey) do
277 | S3Object.about('asdfasdfasdfas-this-does-not-exist', TEST_BUCKET)
278 | end
279 | end
280 |
281 | # Regression test for http://developer.amazonwebservices.com/connect/thread.jspa?messageID=49152&tstart=0#49152
282 | def test_finding_an_object_with_slashes_in_its_name_does_not_escape_the_slash
283 | S3Object.store('rails/1', 'value does not matter', TEST_BUCKET)
284 | S3Object.store('rails/1.html', 'value does not matter', TEST_BUCKET)
285 |
286 | object = nil
287 | assert_nothing_raised do
288 | object = S3Object.find('rails/1.html', TEST_BUCKET)
289 | end
290 |
291 | assert_equal 'rails/1.html', object.key
292 | ensure
293 | %w(rails/1 rails/1.html).each {|key| S3Object.delete(key, TEST_BUCKET)}
294 | end
295 |
296 | def test_finding_an_object_with_spaces_in_its_name
297 | assert_nothing_raised do
298 | S3Object.store('name with spaces', 'value does not matter', TEST_BUCKET)
299 | end
300 |
301 | object = nil
302 | assert_nothing_raised do
303 | object = S3Object.find('name with spaces', TEST_BUCKET)
304 | end
305 |
306 | assert object
307 | assert_equal 'name with spaces', object.key
308 |
309 | # Confirm authenticated url is generated correctly despite space in file name
310 |
311 | response = fetch_object_at(object.url)
312 | assert (200..299).include?(response.code.to_i)
313 |
314 | ensure
315 | S3Object.delete('name with spaces', TEST_BUCKET)
316 | end
317 |
318 | def test_copying_an_object_should_copy_over_its_acl_also_if_requested
319 | key = 'copied-objects-inherit-acl'
320 | copy_key = key + '2'
321 | S3Object.store(key, 'value does not matter', TEST_BUCKET)
322 | original_object = S3Object.find(key, TEST_BUCKET)
323 | original_object.acl.grants << ACL::Grant.grant(:public_read)
324 | original_object.acl.grants << ACL::Grant.grant(:public_read_acp)
325 |
326 | S3Object.acl(key, TEST_BUCKET, original_object.acl)
327 |
328 | acl = S3Object.acl(key, TEST_BUCKET)
329 | assert_equal 3, acl.grants.size
330 |
331 | S3Object.copy(key, copy_key, TEST_BUCKET, :copy_acl => true)
332 | copied_object = S3Object.find(copy_key, TEST_BUCKET)
333 | assert_equal acl.grants, copied_object.acl.grants
334 | ensure
335 | S3Object.delete(key, TEST_BUCKET)
336 | S3Object.delete(copy_key, TEST_BUCKET)
337 | end
338 |
339 | def test_handling_a_path_that_is_not_valid_utf8
340 | key = "318597/620065/GTL_75\24300_A600_A610.zip"
341 | assert_nothing_raised do
342 | S3Object.store(key, 'value does not matter', TEST_BUCKET)
343 | end
344 |
345 | object = nil
346 | assert_nothing_raised do
347 | object = S3Object.find(key, TEST_BUCKET)
348 | end
349 |
350 | assert object
351 |
352 | url = nil
353 | assert_nothing_raised do
354 | url = S3Object.url_for(key, TEST_BUCKET)
355 | end
356 |
357 | assert url
358 |
359 | assert_equal object.value, fetch_object_at(url).body
360 | ensure
361 | assert_nothing_raised do
362 | S3Object.delete(key, TEST_BUCKET)
363 | end
364 | end
365 |
366 | private
367 | def fetch_object_at(url)
368 | Net::HTTP.get_response(URI.parse(url))
369 | end
370 |
371 | end
--------------------------------------------------------------------------------