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 | # Othertimes 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
--------------------------------------------------------------------------------
/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 | AWS::S3::Base.class_eval do
32 | include AWS::S3::Connection::Management
33 | end
34 |
35 | AWS::S3::Bucket.class_eval do
36 | include AWS::S3::Logging::Management
37 | include AWS::S3::ACL::Bucket
38 | end
39 |
40 | AWS::S3::S3Object.class_eval do
41 | include AWS::S3::ACL::S3Object
42 | include AWS::S3::BitTorrent
43 | end
44 |
45 | require_library_or_gem 'xmlsimple', 'xml-simple' unless defined? XmlSimple
46 | # If libxml is installed, we use the FasterXmlSimple library, that provides most of the functionality of XmlSimple
47 | # except it uses the xml/libxml library for xml parsing (rather than REXML). If libxml isn't installed, we just fall back on
48 | # XmlSimple.
49 | AWS::S3::Parsing.parser =
50 | begin
51 | require_library_or_gem 'xml/libxml'
52 | # Older version of libxml aren't stable (bus error when requesting attributes that don't exist) so we
53 | # have to use a version greater than '0.3.8.2'.
54 | raise LoadError unless XML::Parser::VERSION > '0.3.8.2'
55 | $:.push(File.join(File.dirname(__FILE__), '..', '..', 'support', 'faster-xml-simple', 'lib'))
56 | require_library_or_gem 'faster_xml_simple'
57 | FasterXmlSimple
58 | rescue LoadError
59 | XmlSimple
60 | end
61 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 = 'marcel@vernix.org'
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 | ]
87 |
88 | significant_query_strings.each do |uncleaned_path, expected_cleaned_path|
89 | assert_equal expected_cleaned_path, Authentication::CanonicalString.new(Net::HTTP::Get.new(uncleaned_path)).send(:path)
90 | end
91 | end
92 |
93 | def test_default_headers_set
94 | Authentication::CanonicalString.default_headers.each do |header|
95 | assert @canonical_string.headers.include?(header)
96 | end
97 | end
98 |
99 | def test_interesting_headers_are_copied_over
100 | an_interesting_header = 'content-md5'
101 | string_without_interesting_header = Authentication::CanonicalString.new(@request)
102 | assert string_without_interesting_header.headers[an_interesting_header].empty?
103 |
104 | # Add an interesting header
105 | @request[an_interesting_header] = 'foo'
106 | string_with_interesting_header = Authentication::CanonicalString.new(@request)
107 | assert_equal 'foo', string_with_interesting_header.headers[an_interesting_header]
108 | end
109 |
110 | def test_canonical_string
111 | request = AmazonDocExampleData::Example1.request
112 | assert_equal AmazonDocExampleData::Example1.canonical_string, Authentication::CanonicalString.new(request)
113 | end
114 | end
--------------------------------------------------------------------------------
/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/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_owner_attributes_are_accessible
62 | owner = @object.owner
63 | assert owner.id
64 | assert owner.display_name
65 | assert_equal 'bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1', owner.id
66 | assert_equal 'mmolina@onramp.net', owner.display_name
67 | end
68 |
69 | def test_only_valid_attributes_accessible
70 | assert_raises(NoMethodError) do
71 | @object.owner.foo
72 | end
73 | end
74 |
75 | def test_fetching_object_value_generates_value_object
76 | mock_connection_for(S3Object, :returns => {:body => 'hello!'})
77 | value = S3Object.value('foo', 'bar')
78 | assert_kind_of S3Object::Value, value
79 | assert_equal 'hello!', value
80 | end
81 |
82 | def test_fetching_file_by_name_raises_when_heuristic_fails
83 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.bucket_with_one_key})
84 | assert_raises(NoSuchKey) do
85 | S3Object.find('not_tongue_overload.jpg', 'marcel_molina')
86 | end
87 |
88 | object = nil # Block scoping
89 | assert_nothing_raised do
90 | object = S3Object.find('tongue_overload.jpg', 'marcel_molina')
91 | end
92 | assert_kind_of S3Object, object
93 | assert_equal 'tongue_overload.jpg', object.key
94 | end
95 |
96 | def test_about
97 | headers = {'content-size' => '12345', 'date' => Time.now.httpdate, 'content-type' => 'application/xml'}
98 | mock_connection_for(S3Object, :returns => [
99 | {:headers => headers},
100 | {:code => 404}
101 | ]
102 | )
103 | about = S3Object.about('foo', 'bar')
104 | assert_kind_of S3Object::About, about
105 | assert_equal headers, about
106 |
107 | assert_raises(NoSuchKey) do
108 | S3Object.about('foo', 'bar')
109 | end
110 | end
111 |
112 | def test_can_tell_that_an_s3object_does_not_exist
113 | mock_connection_for(S3Object, :returns => {:code => 404})
114 | assert_equal false, S3Object.exists?('foo', 'bar')
115 | end
116 |
117 | def test_can_tell_that_an_s3object_exists
118 | mock_connection_for(S3Object, :returns => {:code => 200})
119 | assert_equal true, S3Object.exists?('foo', 'bar')
120 | end
121 |
122 | def test_s3object_equality
123 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.bucket_with_more_than_one_key})
124 | file1, file2 = Bucket.objects('does not matter')
125 | assert file1 == file1
126 | assert file2 == file2
127 | assert !(file1 == file2) # /!\ Parens required /!\
128 | end
129 |
130 | def test_inspect
131 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.bucket_with_one_key})
132 | object = S3Object.find('tongue_overload.jpg', 'bucket does not matter')
133 | assert object.path
134 | assert_nothing_raised { object.inspect }
135 | assert object.inspect[object.path]
136 | end
137 |
138 | def test_etag
139 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.bucket_with_one_key})
140 | file = S3Object.find('tongue_overload.jpg', 'bucket does not matter')
141 | assert file.etag
142 | assert_equal 'f21f7c4e8ea6e34b268887b07d6da745', file.etag
143 | end
144 |
145 | def test_fetching_information_about_an_object_that_does_not_exist_raises_no_such_key
146 | mock_connection_for(S3Object, :returns => {:body => '', :code => 404})
147 | assert_raises(NoSuchKey) do
148 | S3Object.about('asdfasdfasdfas-this-does-not-exist', 'bucket does not matter')
149 | end
150 | end
151 | def test_copy_options_are_used
152 | options = {'x-amz-storage-class' => 'REDUCED_REDUNDANCY'}
153 | resp = FakeResponse.new
154 |
155 | connection = flexmock('Mock connection') do |mock|
156 | mock.should_receive(:request).
157 | # The storage-class key must be passed to connection.request(:put, ...)
158 | with(:put, '/some-bucket/new', hsh(options), any, any).
159 | and_return(resp)
160 | end
161 | flexmock(S3Object).should_receive(:connection).and_return(connection)
162 |
163 | result = S3Object.copy('old', 'new', 'some-bucket', options)
164 | assert_equal resp.code, result.code
165 | end
166 | end
167 |
168 | class MetadataTest < Test::Unit::TestCase
169 | def setup
170 | @metadata = S3Object::Metadata.new(Fixtures::Headers.headers_including_one_piece_of_metadata)
171 | end
172 |
173 | def test_only_metadata_is_extracted
174 | assert @metadata.to_headers.size == 1
175 | assert @metadata.to_headers['x-amz-meta-test']
176 | assert_equal 'foo', @metadata.to_headers['x-amz-meta-test']
177 | end
178 |
179 | def test_setting_new_metadata_normalizes_name
180 | @metadata[:bar] = 'baz'
181 | assert @metadata.to_headers.include?('x-amz-meta-bar')
182 | @metadata['baz'] = 'quux'
183 | assert @metadata.to_headers.include?('x-amz-meta-baz')
184 | @metadata['x-amz-meta-quux'] = 'whatever'
185 | assert @metadata.to_headers.include?('x-amz-meta-quux')
186 | end
187 |
188 | def test_clobbering_existing_header
189 | @metadata[:bar] = 'baz'
190 | assert_equal 'baz', @metadata.to_headers['x-amz-meta-bar']
191 | @metadata[:bar] = 'quux'
192 | assert_equal 'quux', @metadata.to_headers['x-amz-meta-bar']
193 | @metadata['bar'] = 'foo'
194 | assert_equal 'foo', @metadata.to_headers['x-amz-meta-bar']
195 | @metadata['x-amz-meta-bar'] = 'bar'
196 | assert_equal 'bar', @metadata.to_headers['x-amz-meta-bar']
197 | end
198 |
199 | def test_invalid_metadata
200 | @metadata[:invalid_header] = ' ' * (S3Object::Metadata::SIZE_LIMIT + 1)
201 | assert_raises InvalidMetadataValue do
202 | @metadata.to_headers
203 | end
204 | end
205 | end
206 |
207 | class ValueTest < Test::Unit::TestCase
208 | def setup
209 | @response = FakeResponse.new(:body => 'hello there')
210 | @value = S3Object::Value.new(@response)
211 | end
212 |
213 | def test_value_is_set_to_response_body
214 | assert_equal @response.body, @value
215 | end
216 |
217 | def test_response_is_accessible_from_value_object
218 | assert_equal @response, @value.response
219 | end
220 | end
--------------------------------------------------------------------------------
/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_setting_port_to_443_implies_use_ssl
20 | connection = Connection.new(keys.merge(:port => 443))
21 | assert connection.http.use_ssl?
22 | end
23 |
24 | def test_protocol
25 | connection = Connection.new(keys)
26 | assert_equal 'http://', connection.protocol
27 | connection = Connection.new(keys.merge(:use_ssl => true))
28 | assert_equal 'https://', connection.protocol
29 | end
30 |
31 | def test_url_for_honors_use_ssl_option_if_it_is_false_even_if_connection_has_use_ssl_option_set
32 | # References bug: http://rubyforge.org/tracker/index.php?func=detail&aid=17628&group_id=2409&atid=9356
33 | connection = Connection.new(keys.merge(:use_ssl => true))
34 | assert_match %r(^http://), connection.url_for('/pathdoesnotmatter', :authenticated => false, :use_ssl => false)
35 | end
36 |
37 | def test_connection_is_not_persistent_by_default
38 | connection = Connection.new(keys)
39 | assert !connection.persistent?
40 |
41 | connection = Connection.new(keys.merge(:persistent => true))
42 | assert connection.persistent?
43 | end
44 |
45 | def test_server_and_port_are_passed_onto_connection
46 | connection = Connection.new(keys)
47 | options = connection.instance_variable_get('@options')
48 | assert_equal connection.http.address, options[:server]
49 | assert_equal connection.http.port, options[:port]
50 | end
51 |
52 | def test_not_including_required_access_keys_raises
53 | assert_raises(MissingAccessKey) do
54 | Connection.new
55 | end
56 |
57 | assert_raises(MissingAccessKey) do
58 | Connection.new(:access_key_id => '123')
59 | end
60 |
61 | assert_nothing_raised do
62 | Connection.new(keys)
63 | end
64 | end
65 |
66 | def test_access_keys_extracted
67 | connection = Connection.new(keys)
68 | assert_equal '123', connection.access_key_id
69 | assert_equal 'abc', connection.secret_access_key
70 | end
71 |
72 | def test_request_method_class_lookup
73 | connection = Connection.new(keys)
74 | expectations = {
75 | :get => Net::HTTP::Get, :post => Net::HTTP::Post,
76 | :put => Net::HTTP::Put, :delete => Net::HTTP::Delete,
77 | :head => Net::HTTP::Head
78 | }
79 |
80 | expectations.each do |verb, klass|
81 | assert_equal klass, connection.send(:request_method, verb)
82 | end
83 | end
84 |
85 | def test_url_for_uses_default_protocol_server_and_port
86 | connection = Connection.new(:access_key_id => '123', :secret_access_key => 'abc', :port => 80)
87 | assert_match %r(^http://s3\.amazonaws\.com/foo\?), connection.url_for('/foo')
88 |
89 | connection = Connection.new(:access_key_id => '123', :secret_access_key => 'abc', :use_ssl => true, :port => 443)
90 | assert_match %r(^https://s3\.amazonaws\.com/foo\?), connection.url_for('/foo')
91 | end
92 |
93 | def test_url_for_remembers_custom_protocol_server_and_port
94 | connection = Connection.new(:access_key_id => '123', :secret_access_key => 'abc', :server => 'example.org', :port => 555, :use_ssl => true)
95 | assert_match %r(^https://example\.org:555/foo\?), connection.url_for('/foo')
96 | end
97 |
98 | def test_url_for_with_and_without_authenticated_urls
99 | connection = Connection.new(:access_key_id => '123', :secret_access_key => 'abc', :server => 'example.org')
100 | authenticated = lambda {|url| url['?AWSAccessKeyId']}
101 | assert authenticated[connection.url_for('/foo')]
102 | assert authenticated[connection.url_for('/foo', :authenticated => true)]
103 | assert !authenticated[connection.url_for('/foo', :authenticated => false)]
104 | end
105 |
106 | def test_connecting_through_a_proxy
107 | connection = nil
108 | assert_nothing_raised do
109 | connection = Connection.new(keys.merge(:proxy => sample_proxy_settings))
110 | end
111 | assert connection.http.proxy?
112 | end
113 |
114 | def test_request_only_escapes_the_path_the_first_time_it_runs_and_not_subsequent_times
115 | connection = Connection.new(@keys)
116 | unescaped_path = 'path with spaces'
117 | escaped_path = 'path%20with%20spaces'
118 |
119 | flexmock(Connection).should_receive(:prepare_path).with(unescaped_path).once.and_return(escaped_path).ordered
120 | flexmock(connection.http).should_receive(:request).and_raise(Errno::EPIPE).ordered
121 | flexmock(connection.http).should_receive(:request).ordered
122 | connection.request :put, unescaped_path
123 | end
124 |
125 | def test_if_request_has_no_body_then_the_content_length_is_set_to_zero
126 | # References bug: http://rubyforge.org/tracker/index.php?func=detail&aid=13052&group_id=2409&atid=9356
127 | connection = Connection.new(@keys)
128 | flexmock(Net::HTTP::Put).new_instances.should_receive(:content_length=).once.with(0).ordered
129 | flexmock(connection.http).should_receive(:request).once.ordered
130 | connection.request :put, 'path does not matter'
131 | end
132 | end
133 |
134 | class ConnectionOptionsTest < Test::Unit::TestCase
135 |
136 | def setup
137 | @options = generate_options(:server => 'example.org', :port => 555)
138 | @default_options = generate_options
139 | end
140 |
141 | def test_server_extracted
142 | assert_key_transfered(:server, 'example.org', @options)
143 | end
144 |
145 | def test_port_extracted
146 | assert_key_transfered(:port, 555, @options)
147 | end
148 |
149 | def test_server_defaults_to_default_host
150 | assert_equal DEFAULT_HOST, @default_options[:server]
151 | end
152 |
153 | def test_port_defaults_to_80_if_use_ssl_is_false
154 | assert_equal 80, @default_options[:port]
155 | end
156 |
157 | def test_port_is_set_to_443_if_use_ssl_is_true
158 | options = generate_options(:use_ssl => true)
159 | assert_equal 443, options[:port]
160 | end
161 |
162 | def test_explicit_port_trumps_use_ssl
163 | options = generate_options(:port => 555, :use_ssl => true)
164 | assert_equal 555, options[:port]
165 | end
166 |
167 | def test_invalid_options_raise
168 | assert_raises(InvalidConnectionOption) do
169 | generate_options(:host => 'campfire.s3.amazonaws.com')
170 | end
171 | end
172 |
173 | def test_not_specifying_all_required_proxy_settings_raises
174 | assert_raises(ArgumentError) do
175 | generate_options(:proxy => {})
176 | end
177 | end
178 |
179 | def test_not_specifying_proxy_option_at_all_does_not_raise
180 | assert_nothing_raised do
181 | generate_options
182 | end
183 | end
184 |
185 | def test_specifying_all_required_proxy_settings
186 | assert_nothing_raised do
187 | generate_options(:proxy => sample_proxy_settings)
188 | end
189 | end
190 |
191 | def test_only_host_setting_is_required
192 | assert_nothing_raised do
193 | generate_options(:proxy => {:host => 'http://google.com'})
194 | end
195 | end
196 |
197 | def test_proxy_settings_are_extracted
198 | options = generate_options(:proxy => sample_proxy_settings)
199 | assert_equal sample_proxy_settings.values.map {|value| value.to_s}.sort, options.proxy_settings.map {|value| value.to_s}.sort
200 | end
201 |
202 | def test_recognizing_that_the_settings_want_to_connect_through_a_proxy
203 | options = generate_options(:proxy => sample_proxy_settings)
204 | assert options.connecting_through_proxy?
205 | end
206 |
207 | private
208 | def assert_key_transfered(key, value, options)
209 | assert_equal value, options[key]
210 | end
211 |
212 | def generate_options(options = {})
213 | Connection::Options.new(options)
214 | end
215 | end
216 |
--------------------------------------------------------------------------------
/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/extensions_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/test_helper'
2 |
3 | class HashExtensionsTest < Test::Unit::TestCase
4 | def test_to_query_string
5 | # Because hashes aren't ordered, I'm mostly testing against hashes with just one key
6 | symbol_keys = {:one => 1}
7 | string_keys = {'one' => 1}
8 | expected = '?one=1'
9 | [symbol_keys, string_keys].each do |hash|
10 | assert_equal expected, hash.to_query_string
11 | end
12 | end
13 |
14 | def test_empty_hash_returns_no_query_string
15 | assert_equal '', {}.to_query_string
16 | end
17 |
18 | def test_include_question_mark
19 | hash = {:one => 1}
20 | assert_equal '?one=1', hash.to_query_string
21 | assert_equal 'one=1', hash.to_query_string(false)
22 | end
23 |
24 | def test_elements_joined_by_ampersand
25 | hash = {:one => 1, :two => 2}
26 | qs = hash.to_query_string
27 | assert qs['one=1&two=2'] || qs['two=2&one=1']
28 | end
29 |
30 | def test_normalized_options
31 | expectations = [
32 | [{:foo_bar => 1}, {'foo-bar' => '1'}],
33 | [{'foo_bar' => 1}, {'foo-bar' => '1'}],
34 | [{'foo-bar' => 1}, {'foo-bar' => '1'}],
35 | [{}, {}]
36 | ]
37 |
38 | expectations.each do |(before, after)|
39 | assert_equal after, before.to_normalized_options
40 | end
41 | end
42 | end
43 |
44 | class StringExtensionsTest < Test::Unit::TestCase
45 | def test_previous
46 | expectations = {'abc' => 'abb', '123' => '122', '1' => '0'}
47 | expectations.each do |before, after|
48 | assert_equal after, before.previous
49 | end
50 | end
51 |
52 | def test_to_header
53 | transformations = {
54 | 'foo' => 'foo',
55 | :foo => 'foo',
56 | 'foo-bar' => 'foo-bar',
57 | 'foo_bar' => 'foo-bar',
58 | :foo_bar => 'foo-bar',
59 | 'Foo-Bar' => 'foo-bar',
60 | 'Foo_Bar' => 'foo-bar'
61 | }
62 |
63 | transformations.each do |before, after|
64 | assert_equal after, before.to_header
65 | end
66 | end
67 |
68 | def test_valid_utf8?
69 | assert !"318597/620065/GTL_75\24300_A600_A610.zip".valid_utf8?
70 | assert "318597/620065/GTL_75£00_A600_A610.zip".valid_utf8?
71 | end
72 |
73 | def test_remove_extended
74 | assert "318597/620065/GTL_75\24300_A600_A610.zip".remove_extended.valid_utf8?
75 | assert "318597/620065/GTL_75£00_A600_A610.zip".remove_extended.valid_utf8?
76 | end
77 | end
78 |
79 | class CoercibleStringTest < Test::Unit::TestCase
80 |
81 | def test_coerce
82 | coercions = [
83 | ['1', 1],
84 | ['false', false],
85 | ['true', true],
86 | ['2006-10-29T23:14:47.000Z', Time.parse('2006-10-29T23:14:47.000Z')],
87 | ['Hello!', 'Hello!'],
88 | ['false23', 'false23'],
89 | ['03 1-2-3-Apple-Tree.mp3', '03 1-2-3-Apple-Tree.mp3'],
90 | ['0815', '0815'] # This number isn't coerced because the leading zero would be lost
91 | ]
92 |
93 | coercions.each do |before, after|
94 | assert_nothing_raised do
95 | assert_equal after, CoercibleString.coerce(before)
96 | end
97 | end
98 | end
99 | end
100 |
101 | class ModuleExtensionsTest < Test::Unit::TestCase
102 | class Foo
103 | def foo(reload = false)
104 | expirable_memoize(reload) do
105 | Time.now
106 | end
107 | end
108 |
109 | def bar(reload = false)
110 | expirable_memoize(reload, :baz) do
111 | Time.now
112 | end
113 | end
114 |
115 | def quux
116 | Time.now
117 | end
118 | memoized :quux
119 | end
120 |
121 | def setup
122 | @instance = Foo.new
123 | end
124 |
125 | def test_memoize
126 | assert !instance_variables_of(@instance).include?('@foo')
127 | cached_result = @instance.foo
128 | assert_equal cached_result, @instance.foo
129 | assert instance_variables_of(@instance).include?('@foo')
130 | assert_equal cached_result, @instance.send(:instance_variable_get, :@foo)
131 | assert_not_equal cached_result, new_cache = @instance.foo(:reload)
132 | assert_equal new_cache, @instance.foo
133 | assert_equal new_cache, @instance.send(:instance_variable_get, :@foo)
134 | end
135 |
136 | def test_customizing_memoize_storage
137 | assert !instance_variables_of(@instance).include?('@bar')
138 | assert !instance_variables_of(@instance).include?('@baz')
139 | cached_result = @instance.bar
140 | assert !instance_variables_of(@instance).include?('@bar')
141 | assert instance_variables_of(@instance).include?('@baz')
142 | assert_equal cached_result, @instance.bar
143 | assert_equal cached_result, @instance.send(:instance_variable_get, :@baz)
144 | assert_nil @instance.send(:instance_variable_get, :@bar)
145 | end
146 |
147 | def test_memoized
148 | assert !instance_variables_of(@instance).include?('@quux')
149 | cached_result = @instance.quux
150 | assert_equal cached_result, @instance.quux
151 | assert instance_variables_of(@instance).include?('@quux')
152 | assert_equal cached_result, @instance.send(:instance_variable_get, :@quux)
153 | assert_not_equal cached_result, new_cache = @instance.quux(:reload)
154 | assert_equal new_cache, @instance.quux
155 | assert_equal new_cache, @instance.send(:instance_variable_get, :@quux)
156 | end
157 |
158 | def test_constant_setting
159 | some_module = Module.new
160 | assert !some_module.const_defined?(:FOO)
161 | assert_nothing_raised do
162 | some_module.constant :FOO, 'bar'
163 | end
164 |
165 | assert some_module.const_defined?(:FOO)
166 | assert_nothing_raised do
167 | some_module::FOO
168 | some_module.foo
169 | end
170 | assert_equal 'bar', some_module::FOO
171 | assert_equal 'bar', some_module.foo
172 |
173 | assert_nothing_raised do
174 | some_module.constant :FOO, 'baz'
175 | end
176 |
177 | assert_equal 'bar', some_module::FOO
178 | assert_equal 'bar', some_module.foo
179 | end
180 |
181 | private
182 | # For 1.9 compatibility
183 | def instance_variables_of(object)
184 | object.instance_variables.map do |instance_variable|
185 | instance_variable.to_s
186 | end
187 | end
188 |
189 | end
190 |
191 | class AttributeProxyTest < Test::Unit::TestCase
192 | class BlindProxyUsingDefaultAttributesHash
193 | include SelectiveAttributeProxy
194 | proxy_to :exlusively => false
195 | end
196 |
197 | class BlindProxyUsingCustomAttributeHash
198 | include SelectiveAttributeProxy
199 | proxy_to :settings
200 | end
201 |
202 | class ProxyUsingPassedInAttributeHash
203 | include SelectiveAttributeProxy
204 |
205 | def initialize(attributes = {})
206 | @attributes = attributes
207 | end
208 | end
209 |
210 | class RestrictedProxy
211 | include SelectiveAttributeProxy
212 |
213 | private
214 | def proxiable_attribute?(name)
215 | %w(foo bar baz).include?(name)
216 | end
217 | end
218 |
219 | class NonExclusiveProxy
220 | include SelectiveAttributeProxy
221 | proxy_to :settings, :exclusively => false
222 | end
223 |
224 | def test_using_all_defaults
225 | b = BlindProxyUsingDefaultAttributesHash.new
226 | assert_nothing_raised do
227 | b.foo = 'bar'
228 | end
229 |
230 | assert_nothing_raised do
231 | b.foo
232 | end
233 |
234 | assert_equal 'bar', b.foo
235 | end
236 |
237 | def test_storage_is_autovivified
238 | b = BlindProxyUsingDefaultAttributesHash.new
239 | assert_nothing_raised do
240 | b.send(:attributes)['foo'] = 'bar'
241 | end
242 |
243 | assert_nothing_raised do
244 | b.foo
245 | end
246 |
247 | assert_equal 'bar', b.foo
248 | end
249 |
250 | def test_limiting_which_attributes_are_proxiable
251 | r = RestrictedProxy.new
252 | assert_nothing_raised do
253 | r.foo = 'bar'
254 | end
255 |
256 | assert_nothing_raised do
257 | r.foo
258 | end
259 |
260 | assert_equal 'bar', r.foo
261 |
262 | assert_raises(NoMethodError) do
263 | r.quux = 'foo'
264 | end
265 |
266 | assert_raises(NoMethodError) do
267 | r.quux
268 | end
269 | end
270 |
271 | def test_proxying_is_exclusive_by_default
272 | p = ProxyUsingPassedInAttributeHash.new('foo' => 'bar')
273 | assert_nothing_raised do
274 | p.foo
275 | p.foo = 'baz'
276 | end
277 |
278 | assert_equal 'baz', p.foo
279 |
280 | assert_raises(NoMethodError) do
281 | p.quux
282 | end
283 | end
284 |
285 | def test_setting_the_proxy_as_non_exclusive
286 | n = NonExclusiveProxy.new
287 | assert_nothing_raised do
288 | n.foo = 'baz'
289 | end
290 |
291 | assert_nothing_raised do
292 | n.foo
293 | end
294 |
295 | assert_equal 'baz', n.foo
296 | end
297 | end
298 |
--------------------------------------------------------------------------------
/lib/aws/s3/extensions.rb:
--------------------------------------------------------------------------------
1 | #:stopdoc:
2 |
3 | class Hash
4 | def to_query_string(include_question_mark = true)
5 | query_string = ''
6 | unless empty?
7 | query_string << '?' if include_question_mark
8 | query_string << inject([]) do |params, (key, value)|
9 | params << "#{key}=#{value}"
10 | end.join('&')
11 | end
12 | query_string
13 | end
14 |
15 | def to_normalized_options
16 | # Convert all option names to downcased strings, and replace underscores with hyphens
17 | inject({}) do |normalized_options, (name, value)|
18 | normalized_options[name.to_header] = value.to_s
19 | normalized_options
20 | end
21 | end
22 |
23 | def to_normalized_options!
24 | replace(to_normalized_options)
25 | end
26 | end
27 |
28 | class String
29 | def previous!
30 | self[-1] = (self[-1].ord - 1).chr
31 | self
32 | end
33 |
34 | def previous
35 | dup.previous!
36 | end
37 |
38 | def to_header
39 | downcase.tr('_', '-')
40 | end
41 |
42 | # ActiveSupport adds an underscore method to String so let's just use that one if
43 | # we find that the method is already defined
44 | def underscore
45 | gsub(/::/, '/').
46 | gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
47 | gsub(/([a-z\d])([A-Z])/,'\1_\2').
48 | tr("-", "_").downcase
49 | end unless public_method_defined? :underscore
50 |
51 | def valid_utf8?
52 | dup.force_encoding('UTF-8').valid_encoding?
53 | end
54 |
55 | # All paths in in S3 have to be valid unicode so this takes care of
56 | # cleaning up any strings that aren't valid utf-8 according to String#valid_utf8?
57 | def remove_extended!
58 | sanitized_string = ''
59 | each_byte do |byte|
60 | character = byte.chr
61 | sanitized_string << character if character.ascii_only?
62 | end
63 | sanitized_string
64 | end
65 |
66 | def remove_extended
67 | dup.remove_extended!
68 | end
69 | end
70 |
71 | class CoercibleString < String
72 | class << self
73 | def coerce(string)
74 | new(string).coerce
75 | end
76 | end
77 |
78 | def coerce
79 | case self
80 | when 'true'; true
81 | when 'false'; false
82 | # Don't coerce numbers that start with zero
83 | when /^[1-9]+\d*$/; Integer(self)
84 | when datetime_format; Time.parse(self)
85 | else
86 | self
87 | end
88 | end
89 |
90 | private
91 | # Lame hack since Date._parse is so accepting. S3 dates are of the form: '2006-10-29T23:14:47.000Z'
92 | # so unless the string looks like that, don't even try, otherwise it might convert an object's
93 | # key from something like '03 1-2-3-Apple-Tree.mp3' to Sat Feb 03 00:00:00 CST 2001.
94 | def datetime_format
95 | /^\d{4}-\d{2}-\d{2}\w\d{2}:\d{2}:\d{2}/
96 | end
97 | end
98 |
99 | class Symbol
100 | def to_header
101 | to_s.to_header
102 | end
103 | end
104 |
105 | module Kernel
106 | def __called_from__
107 | caller[1][/`([^']+)'/, 1]
108 | end
109 |
110 | def expirable_memoize(reload = false, storage = nil)
111 | current_method = __called_from__
112 | storage = "@#{storage || current_method}"
113 | if reload
114 | instance_variable_set(storage, nil)
115 | else
116 | if cache = instance_variable_get(storage)
117 | return cache
118 | end
119 | end
120 | instance_variable_set(storage, yield)
121 | end
122 |
123 | def require_library_or_gem(library, gem_name = nil)
124 | gem(gem_name || library, '>=0')
125 | require library
126 | rescue LoadError => library_not_installed
127 | begin
128 | require 'rubygems'
129 | require library
130 | rescue LoadError
131 | raise library_not_installed
132 | end
133 | end
134 | end
135 |
136 | class Object
137 | def returning(value)
138 | yield(value)
139 | value
140 | end
141 | end
142 |
143 | class Module
144 | def memoized(method_name)
145 | original_method = "unmemoized_#{method_name}_#{Time.now.to_i}"
146 | alias_method original_method, method_name
147 | module_eval(<<-EVAL, __FILE__, __LINE__)
148 | def #{method_name}(reload = false, *args, &block)
149 | expirable_memoize(reload) do
150 | send(:#{original_method}, *args, &block)
151 | end
152 | end
153 | EVAL
154 | end
155 |
156 | def constant(name, value)
157 | unless const_defined?(name)
158 | const_set(name, value)
159 | module_eval(<<-EVAL, __FILE__, __LINE__)
160 | def self.#{name.to_s.downcase}
161 | #{name.to_s}
162 | end
163 | EVAL
164 | end
165 | end
166 |
167 | # Transforms MarcelBucket into
168 | #
169 | # class MarcelBucket < AWS::S3::Bucket
170 | # set_current_bucket_to 'marcel'
171 | # end
172 | def const_missing_from_s3_library(sym)
173 | if sym.to_s =~ /^(\w+)(Bucket|S3Object)$/
174 | const = const_set(sym, Class.new(AWS::S3.const_get($2)))
175 | const.current_bucket = $1.underscore
176 | const
177 | else
178 | const_missing_not_from_s3_library(sym)
179 | end
180 | end
181 | alias_method :const_missing_not_from_s3_library, :const_missing
182 | alias_method :const_missing, :const_missing_from_s3_library
183 | end
184 |
185 |
186 | class Class # :nodoc:
187 | def cattr_reader(*syms)
188 | syms.flatten.each do |sym|
189 | class_eval(<<-EOS, __FILE__, __LINE__)
190 | unless defined? @@#{sym}
191 | @@#{sym} = nil
192 | end
193 |
194 | def self.#{sym}
195 | @@#{sym}
196 | end
197 |
198 | def #{sym}
199 | @@#{sym}
200 | end
201 | EOS
202 | end
203 | end
204 |
205 | def cattr_writer(*syms)
206 | syms.flatten.each do |sym|
207 | class_eval(<<-EOS, __FILE__, __LINE__)
208 | unless defined? @@#{sym}
209 | @@#{sym} = nil
210 | end
211 |
212 | def self.#{sym}=(obj)
213 | @@#{sym} = obj
214 | end
215 |
216 | def #{sym}=(obj)
217 | @@#{sym} = obj
218 | end
219 | EOS
220 | end
221 | end
222 |
223 | def cattr_accessor(*syms)
224 | cattr_reader(*syms)
225 | cattr_writer(*syms)
226 | end
227 | end if Class.instance_methods(false).grep(/^cattr_(?:reader|writer|accessor)$/).empty?
228 |
229 | module SelectiveAttributeProxy
230 | def self.included(klass)
231 | klass.extend(ClassMethods)
232 | klass.class_eval(<<-EVAL, __FILE__, __LINE__)
233 | cattr_accessor :attribute_proxy
234 | cattr_accessor :attribute_proxy_options
235 |
236 | # Default name for attribute storage
237 | self.attribute_proxy = :attributes
238 | self.attribute_proxy_options = {:exclusively => true}
239 |
240 | private
241 | # By default proxy all attributes
242 | def proxiable_attribute?(name)
243 | return true unless self.class.attribute_proxy_options[:exclusively]
244 | send(self.class.attribute_proxy).has_key?(name)
245 | end
246 |
247 | def method_missing(method, *args, &block)
248 | # Autovivify attribute storage
249 | if method == self.class.attribute_proxy
250 | ivar = "@\#{method}"
251 | instance_variable_set(ivar, {}) unless instance_variable_get(ivar).is_a?(Hash)
252 | instance_variable_get(ivar)
253 | # Delegate to attribute storage
254 | elsif method.to_s =~ /^(\\w+)(=?)$/ && proxiable_attribute?($1)
255 | attributes_hash_name = self.class.attribute_proxy
256 | $2.empty? ? send(attributes_hash_name)[$1] : send(attributes_hash_name)[$1] = args.first
257 | else
258 | super
259 | end
260 | end
261 | EVAL
262 | end
263 |
264 | module ClassMethods
265 | def proxy_to(attribute_name, options = {})
266 | if attribute_name.is_a?(Hash)
267 | options = attribute_name
268 | else
269 | self.attribute_proxy = attribute_name
270 | end
271 | self.attribute_proxy_options = options
272 | end
273 | end
274 | end
275 |
276 | # When streaming data up, Net::HTTPGenericRequest hard codes a chunk size of 1k. For large files this
277 | # is an unfortunately low chunk size, so here we make it use a much larger default size and move it into a method
278 | # 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
279 | # than I've already had to...).
280 | module Net
281 | class HTTPGenericRequest
282 | def send_request_with_body_stream(sock, ver, path, f)
283 | raise ArgumentError, "Content-Length not given and Transfer-Encoding is not `chunked'" unless content_length() or chunked?
284 | unless content_type()
285 | warn 'net/http: warning: Content-Type did not set; using application/x-www-form-urlencoded' if $VERBOSE
286 | set_content_type 'application/x-www-form-urlencoded'
287 | end
288 | write_header sock, ver, path
289 | if chunked?
290 | while s = f.read(chunk_size)
291 | sock.write(sprintf("%x\r\n", s.length) << s << "\r\n")
292 | end
293 | sock.write "0\r\n\r\n"
294 | else
295 | while s = f.read(chunk_size)
296 | sock.write s
297 | end
298 | end
299 | end
300 |
301 | def chunk_size
302 | 1048576 # 1 megabyte
303 | end
304 | end
305 |
306 | # Net::HTTP before 1.8.4 doesn't have the use_ssl? method or the Delete request type
307 | class HTTP
308 | def use_ssl?
309 | @use_ssl
310 | end unless public_method_defined? :use_ssl?
311 |
312 | class Delete < HTTPRequest
313 | METHOD = 'DELETE'
314 | REQUEST_HAS_BODY = false
315 | RESPONSE_HAS_BODY = true
316 | end unless const_defined? :Delete
317 | end
318 | end
319 |
320 | class XmlGenerator < String #:nodoc:
321 | attr_reader :xml
322 | def initialize
323 | @xml = Builder::XmlMarkup.new(:indent => 2, :target => self)
324 | super()
325 | build
326 | end
327 | end
328 | #:startdoc:
329 |
--------------------------------------------------------------------------------
/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 | digest = OpenSSL::Digest::Digest.new('sha1')
72 | b64_hmac = [OpenSSL::HMAC.digest(digest, secret_access_key, canonical_string)].pack("m").strip
73 | url_encode? ? CGI.escape(b64_hmac) : b64_hmac
74 | end
75 |
76 | def url_encode?
77 | !@options[:url_encode].nil?
78 | end
79 |
80 | def expires?
81 | is_a? QueryString
82 | end
83 |
84 | def date
85 | request['date'].to_s.strip.empty? ? Time.now : Time.parse(request['date'])
86 | end
87 | end
88 |
89 | # Provides header authentication by computing the value of the Authorization header. More details about the
90 | # various authentication schemes can be found in the docs for its containing module, Authentication.
91 | class Header < Signature #:nodoc:
92 | def initialize(*args)
93 | super
94 | self << "AWS #{access_key_id}:#{encoded_canonical}"
95 | end
96 | end
97 |
98 | # Provides query string authentication by computing the three authorization parameters: AWSAccessKeyId, Expires and Signature.
99 | # More details about the various authentication schemes can be found in the docs for its containing module, Authentication.
100 | class QueryString < Signature #:nodoc:
101 | constant :DEFAULT_EXPIRY, 300 # 5 minutes
102 | def initialize(*args)
103 | super
104 | options[:url_encode] = true
105 | self << build
106 | end
107 |
108 | private
109 |
110 | # Will return one of three values, in the following order of precedence:
111 | #
112 | # 1) Seconds since the epoch explicitly passed in the +:expires+ option
113 | # 2) The current time in seconds since the epoch plus the number of seconds passed in
114 | # the +:expires_in+ option
115 | # 3) The current time in seconds since the epoch plus the default number of seconds (60 seconds)
116 | def expires
117 | return options[:expires] if options[:expires]
118 | date.to_i + expires_in
119 | end
120 |
121 | def expires_in
122 | options.has_key?(:expires_in) ? Integer(options[:expires_in]) : DEFAULT_EXPIRY
123 | end
124 |
125 | # Keep in alphabetical order
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 | end
148 |
149 | attr_reader :request, :headers
150 |
151 | def initialize(request, options = {})
152 | super()
153 | @request = request
154 | @headers = {}
155 | @options = options
156 | # "For non-authenticated or anonymous requests. A NotImplemented error result code will be returned if
157 | # an authenticated (signed) request specifies a Host: header other than 's3.amazonaws.com'"
158 | # (from http://docs.amazonwebservices.com/AmazonS3/2006-03-01/VirtualHosting.html)
159 | request['Host'] = DEFAULT_HOST
160 | build
161 | end
162 |
163 | private
164 | def build
165 | self << "#{request.method}\n"
166 | ensure_date_is_valid
167 |
168 | initialize_headers
169 | set_expiry!
170 |
171 | headers.sort_by {|k, _| k}.each do |key, value|
172 | value = value.to_s.strip
173 | self << (key =~ self.class.amazon_header_prefix ? "#{key}:#{value}" : value)
174 | self << "\n"
175 | end
176 | self << path
177 | end
178 |
179 | def initialize_headers
180 | identify_interesting_headers
181 | set_default_headers
182 | end
183 |
184 | def set_expiry!
185 | self.headers['date'] = @options[:expires] if @options[:expires]
186 | end
187 |
188 | def ensure_date_is_valid
189 | request['Date'] ||= Time.now.httpdate
190 | end
191 |
192 | def identify_interesting_headers
193 | request.each do |key, value|
194 | key = key.downcase # Can't modify frozen string so no bang
195 | if self.class.interesting_headers.any? {|header| header === key}
196 | self.headers[key] = value.to_s.strip
197 | end
198 | end
199 | end
200 |
201 | def set_default_headers
202 | self.class.default_headers.each do |header|
203 | self.headers[header] ||= ''
204 | end
205 | end
206 |
207 | def path
208 | [only_path, extract_significant_parameter].compact.join('?')
209 | end
210 |
211 | def extract_significant_parameter
212 | request.path[/[&?](acl|torrent|logging)(?:&|=|$)/, 1]
213 | end
214 |
215 | def only_path
216 | request.path[/^[^?]*/]
217 | end
218 | end
219 | end
220 | end
221 | end
--------------------------------------------------------------------------------
/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 | end
82 |
83 | # Regenerate README before packaging
84 | task :package => 'doc:readme'
85 | Rake::GemPackageTask.new(spec) do |pkg|
86 | pkg.need_tar_gz = true
87 | pkg.package_files.include('{lib,script,test,support}/**/*')
88 | pkg.package_files.include('README')
89 | pkg.package_files.include('COPYING')
90 | pkg.package_files.include('INSTALL')
91 | pkg.package_files.include('Rakefile')
92 | end
93 |
94 | desc 'Install with gems'
95 | task :install => :repackage do
96 | sh "sudo gem i pkg/#{spec.name}-#{spec.version}.gem"
97 | end
98 |
99 | desc 'Uninstall gem'
100 | task :uninstall do
101 | sh "sudo gem uninstall #{spec.name} -x"
102 | end
103 |
104 | desc 'Reinstall gem'
105 | task :reinstall => [:uninstall, :install]
106 |
107 | task :confirm_release do
108 | print "Releasing version #{spec.version}. Are you sure you want to proceed? [Yn] "
109 | abort if STDIN.getc == ?n
110 | end
111 |
112 | desc 'Tag release'
113 | task :tag do
114 | sh %(git tag -a '#{spec.version}-release' -m 'Tagging #{spec.version} release')
115 | sh 'git push --tags'
116 | end
117 |
118 | desc 'Update changelog to include a release marker'
119 | task :add_release_marker_to_changelog do
120 | changelog = IO.read('CHANGELOG')
121 | changelog.sub!(/^head:/, "#{spec.version}:")
122 |
123 | open('CHANGELOG', 'w') do |file|
124 | file.write "head:\n\n#{changelog}"
125 | end
126 | end
127 |
128 | task :commit_changelog do
129 | sh %(git commit CHANGELOG -m "Bump changelog version marker for release")
130 | sh 'git push'
131 | end
132 |
133 | package_name = lambda {|specification| File.join('pkg', "#{specification.name}-#{specification.version}")}
134 |
135 | desc 'Push a release to rubyforge'
136 | task :release => [:confirm_release, :clean, :add_release_marker_to_changelog, :package, :commit_changelog, :tag] do
137 | require 'rubyforge'
138 | package = package_name[spec]
139 |
140 | rubyforge = RubyForge.new.configure
141 | rubyforge.login
142 |
143 | user_config = rubyforge.userconfig
144 | user_config['release_changes'] = YAML.load_file('CHANGELOG')[spec.version.to_s].join("\n")
145 |
146 | version_already_released = lambda do
147 | releases = rubyforge.autoconfig['release_ids']
148 | releases.has_key?(spec.name) && releases[spec.name][spec.version.to_s]
149 | end
150 |
151 | abort("Release #{spec.version} already exists!") if version_already_released.call
152 |
153 | begin
154 | rubyforge.add_release(spec.rubyforge_project, spec.name, spec.version.to_s, "#{package}.tar.gz", "#{package}.gem")
155 | puts "Version #{spec.version} released!"
156 | rescue Exception => exception
157 | puts 'Release failed!'
158 | raise
159 | end
160 | end
161 |
162 | desc 'Upload a beta gem'
163 | task :push_beta_gem => [:clobber_package, :package] do
164 | beta_gem = package_name[spec]
165 | sh %(scp #{beta_gem}.gem marcel@rubyforge.org:/var/www/gforge-projects/amazon/beta)
166 | end
167 |
168 | task :spec do
169 | puts spec.to_ruby
170 | end
171 | end
172 |
173 | desc 'Check code to test ratio'
174 | task :stats do
175 | library_files = FileList["#{library_root}/lib/**/*.rb"]
176 | test_files = FileList["#{library_root}/test/**/*_test.rb"]
177 | count_code_lines = Proc.new do |lines|
178 | lines.inject(0) do |code_lines, line|
179 | next code_lines if [/^\s*$/, /^\s*#/].any? {|non_code_line| non_code_line === line}
180 | code_lines + 1
181 | end
182 | end
183 |
184 | count_code_lines_for_files = Proc.new do |files|
185 | files.inject(0) {|code_lines, file| code_lines + count_code_lines[IO.read(file)]}
186 | end
187 |
188 | library_code_lines = count_code_lines_for_files[library_files]
189 | test_code_lines = count_code_lines_for_files[test_files]
190 | ratio = Proc.new { sprintf('%.2f', test_code_lines.to_f / library_code_lines)}
191 |
192 | puts "Code LOC: #{library_code_lines} Test LOC: #{test_code_lines} Code to Test Ratio: 1:#{ratio.call}"
193 | end
194 |
195 | namespace :test do
196 | find_file = lambda do |name|
197 | file_name = lambda {|path| File.join(path, "#{name}.rb")}
198 | root = $:.detect do |path|
199 | File.exist?(file_name[path])
200 | end
201 | file_name[root] if root
202 | end
203 |
204 | TEST_LOADER = find_file['rake/rake_test_loader']
205 | multiruby = lambda do |glob|
206 | system 'multiruby', TEST_LOADER, *Dir.glob(glob)
207 | end
208 |
209 | desc 'Check test coverage'
210 | task :coverage do
211 | system("rcov -x Library -x support --sort coverage #{File.join(library_root, 'test/*_test.rb')}")
212 | show_test_coverage_results
213 | end
214 |
215 | Rake::TestTask.new(:remote) do |test|
216 | test.pattern = 'test/remote/*_test.rb'
217 | test.verbose = true
218 | end
219 |
220 | Rake::TestTask.new(:all) do |test|
221 | test.pattern = 'test/**/*_test.rb'
222 | test.verbose = true
223 | end
224 |
225 | desc 'Check test coverage of full stack remote tests'
226 | task :full_coverage do
227 | system("rcov -x Library -x support --sort coverage #{File.join(library_root, 'test/remote/*_test.rb')} #{File.join(library_root, 'test/*_test.rb')}")
228 | show_test_coverage_results
229 | end
230 |
231 | desc 'Run local tests against multiple versions of Ruby'
232 | task :version_audit do
233 | multiruby['test/*_test.rb']
234 | end
235 |
236 | namespace :version_audit do
237 | desc 'Run remote tests against multiple versions of Ruby'
238 | task :remote do
239 | multiruby['test/remote/*_test.rb']
240 | end
241 |
242 | desc 'Run all tests against multiple versions of Ruby'
243 | task :all do
244 | multiruby['test/**/*_test.rb']
245 | end
246 | end
247 |
248 | def show_test_coverage_results
249 | system("open #{File.join(library_root, 'coverage/index.html')}") if PLATFORM['darwin']
250 | end
251 |
252 | desc 'Remove coverage products'
253 | task :clobber_coverage do
254 | rm_r 'coverage' rescue nil
255 | end
256 | end
257 |
258 | namespace :todo do
259 | class << TODOS = IO.read(File.join(library_root, 'TODO'))
260 | def items
261 | split("\n").grep(/^\[\s|X\]/)
262 | end
263 |
264 | def completed
265 | find_items_matching(/^\[X\]/)
266 | end
267 |
268 | def uncompleted
269 | find_items_matching(/^\[\s\]/)
270 | end
271 |
272 | def find_items_matching(regexp)
273 | items.grep(regexp).instance_eval do
274 | def display
275 | puts map {|item| "* #{item.sub(/^\[[^\]]\]\s/, '')}"}
276 | end
277 | self
278 | end
279 | end
280 | end
281 |
282 | desc 'Completed todo items'
283 | task :completed do
284 | TODOS.completed.display
285 | end
286 |
287 | desc 'Incomplete todo items'
288 | task :uncompleted do
289 | TODOS.uncompleted.display
290 | end
291 | end if File.exists?(File.join(library_root, 'TODO'))
292 |
293 | namespace :site do
294 | require 'erb'
295 | require 'rdoc/markup/simple_markup'
296 | require 'rdoc/markup/simple_markup/to_html'
297 |
298 | readme = lambda { IO.read('README')[/^== Getting started\n(.*)/m, 1] }
299 |
300 | readme_to_html = lambda do
301 | handler = SM::ToHtml.new
302 | handler.instance_eval do
303 | require 'syntax'
304 | require 'syntax/convertors/html'
305 | def accept_verbatim(am, fragment)
306 | syntax = Syntax::Convertors::HTML.for_syntax('ruby')
307 | @res << %(#{syntax.convert(fragment.txt, true)}
)
308 | end
309 | end
310 | SM::SimpleMarkup.new.convert(readme.call, handler)
311 | end
312 |
313 | desc 'Regenerate the public website page'
314 | task :build => 'doc:readme' do
315 | open('site/public/index.html', 'w') do |file|
316 | erb_data = {}
317 | erb_data[:readme] = readme_to_html.call
318 | file.write ERB.new(IO.read('site/index.erb')).result(binding)
319 | end
320 | end
321 |
322 | task :refresh => :build do
323 | system 'open site/public/index.html'
324 | end
325 |
326 | desc 'Update the live website'
327 | task :deploy => :build do
328 | site_files = FileList['site/public/*']
329 | site_files.delete_if {|file| File.directory?(file)}
330 | sh %(scp #{site_files.join ' '} marcel@rubyforge.org:/var/www/gforge-projects/amazon/)
331 | end
332 | end
333 |
334 | task :clean => ['dist:clobber_package', 'doc:clobber_rdoc', 'test:clobber_coverage']
335 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------