├── .gitignore ├── test ├── remote │ ├── test_file.data │ ├── test_helper.rb │ ├── bittorrent_test.rb │ ├── logging_test.rb │ ├── acl_test.rb │ ├── bucket_test.rb │ └── object_test.rb ├── fixtures │ ├── headers.yml │ ├── logging.yml │ ├── loglines.yml │ ├── policies.yml │ ├── errors.yml │ ├── logs.yml │ └── buckets.yml ├── mocks │ └── fake_response.rb ├── service_test.rb ├── parsing_test.rb ├── error_test.rb ├── response_test.rb ├── fixtures.rb ├── logging_test.rb ├── bucket_test.rb ├── test_helper.rb ├── base_test.rb ├── authentication_test.rb ├── object_test.rb ├── connection_test.rb ├── acl_test.rb └── extensions_test.rb ├── site ├── public │ ├── images │ │ ├── favicon.ico │ │ └── box-and-gem.gif │ ├── ruby.css │ └── screen.css └── index.erb ├── support ├── faster-xml-simple │ ├── test │ │ ├── fixtures │ │ │ ├── test-1.xml │ │ │ ├── test-1.yml │ │ │ ├── test-1.rails.yml │ │ │ ├── test-3.xml │ │ │ ├── test-4.yml │ │ │ ├── test-4.rails.yml │ │ │ ├── test-3.yml │ │ │ ├── test-2.xml │ │ │ ├── test-3.rails.yml │ │ │ ├── test-2.yml │ │ │ ├── test-2.rails.yml │ │ │ ├── test-4.xml │ │ │ ├── test-5.yml │ │ │ ├── test-5.rails.yml │ │ │ ├── test-8.yml │ │ │ ├── test-8.xml │ │ │ ├── test-8.rails.yml │ │ │ ├── test-5.xml │ │ │ ├── test-7.yml │ │ │ ├── test-7.rails.yml │ │ │ ├── test-7.xml │ │ │ ├── test-6.xml │ │ │ ├── test-6.yml │ │ │ └── test-6.rails.yml │ │ ├── test_helper.rb │ │ ├── xml_simple_comparison_test.rb │ │ └── regression_test.rb │ ├── README │ ├── COPYING │ ├── Rakefile │ └── lib │ │ └── faster_xml_simple.rb └── rdoc │ └── code_info.rb ├── bin ├── s3sh └── setup.rb ├── lib └── aws │ ├── s3 │ ├── version.rb │ ├── owner.rb │ ├── service.rb │ ├── bittorrent.rb │ ├── error.rb │ ├── parsing.rb │ ├── exceptions.rb │ ├── response.rb │ ├── extensions.rb │ ├── authentication.rb │ └── base.rb │ └── s3.rb ├── COPYING ├── README.erb ├── TODO ├── INSTALL ├── CHANGELOG └── Rakefile /.gitignore: -------------------------------------------------------------------------------- 1 | .rake_tasks 2 | site/public/index.html 3 | pkg/ 4 | -------------------------------------------------------------------------------- /test/remote/test_file.data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloopletech/aws-s3/master/test/remote/test_file.data -------------------------------------------------------------------------------- /site/public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloopletech/aws-s3/master/site/public/images/favicon.ico -------------------------------------------------------------------------------- /test/fixtures/headers.yml: -------------------------------------------------------------------------------- 1 | headers_including_one_piece_of_metadata: 2 | x-amz-meta-test: foo 3 | content_type: text/plain -------------------------------------------------------------------------------- /site/public/images/box-and-gem.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloopletech/aws-s3/master/site/public/images/box-and-gem.gif -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-1.xml: -------------------------------------------------------------------------------- 1 | 2 | testing 3 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | something: 3 | something-else: 4 | - __content__: testing 5 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-1.rails.yml: -------------------------------------------------------------------------------- 1 | --- 2 | something: 3 | something-else: 4 | __content__: testing 5 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | testing 4 | 5 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-4.yml: -------------------------------------------------------------------------------- 1 | --- 2 | something: 3 | something-else: 4 | - another_child: 5 | attribute: "4" 6 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-4.rails.yml: -------------------------------------------------------------------------------- 1 | --- 2 | something: 3 | something-else: 4 | another_child: 5 | attribute: "4" 6 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-3.yml: -------------------------------------------------------------------------------- 1 | --- 2 | something: 3 | something-else: 4 | - __content__: "\n\ 5 | \t\ttesting\n\ 6 | \t" 7 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-2.xml: -------------------------------------------------------------------------------- 1 | 2 | testing 3 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-3.rails.yml: -------------------------------------------------------------------------------- 1 | --- 2 | something: 3 | something-else: 4 | __content__: "\n\ 5 | \t\ttesting\n\ 6 | \t" 7 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | something: 3 | something-else: 4 | - __content__: testing 5 | child_attribute: "15" 6 | root_attribute: "12" 7 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-2.rails.yml: -------------------------------------------------------------------------------- 1 | --- 2 | something: 3 | something-else: 4 | __content__: testing 5 | child_attribute: "15" 6 | root_attribute: "12" 7 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | testing 4 | 5 | testing 6 | 7 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-5.yml: -------------------------------------------------------------------------------- 1 | --- 2 | something: 3 | something-else: 4 | - __content__: testing 5 | - __content__: testing 6 | - __content__: testing 7 | - __content__: testing 8 | - __content__: testing 9 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-5.rails.yml: -------------------------------------------------------------------------------- 1 | --- 2 | something: 3 | something-else: 4 | - __content__: testing 5 | - __content__: testing 6 | - __content__: testing 7 | - __content__: testing 8 | - __content__: testing 9 | -------------------------------------------------------------------------------- /bin/s3sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | s3_lib = File.dirname(__FILE__) + '/../lib/aws/s3' 3 | setup = File.dirname(__FILE__) + '/setup' 4 | irb_name = RUBY_PLATFORM =~ /mswin32/ ? 'irb.bat' : 'irb' 5 | 6 | exec "#{irb_name} -r #{s3_lib} -r #{setup} --simple-prompt" -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-8.yml: -------------------------------------------------------------------------------- 1 | topic: 2 | title: 3 | id: 4 | type: integer 5 | approved: 6 | type: boolean 7 | parent-id: 8 | viewed-at: 9 | type: datetime 10 | written-on: 11 | type: date -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-8.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-8.rails.yml: -------------------------------------------------------------------------------- 1 | --- 2 | topic: 3 | parent-id: {} 4 | 5 | title: {} 6 | 7 | approved: 8 | type: boolean 9 | id: 10 | type: integer 11 | viewed-at: 12 | type: datetime 13 | written-on: 14 | type: date 15 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-5.xml: -------------------------------------------------------------------------------- 1 | 2 | testing 3 | testing 4 | testing 5 | testing 6 | testing 7 | -------------------------------------------------------------------------------- /lib/aws/s3/version.rb: -------------------------------------------------------------------------------- 1 | module AWS 2 | module S3 3 | module VERSION #:nodoc: 4 | MAJOR = '0' 5 | MINOR = '7' 6 | TINY = '0' 7 | BETA = Time.now.to_i.to_s 8 | end 9 | 10 | Version = [VERSION::MAJOR, VERSION::MINOR, VERSION::TINY, VERSION::BETA].compact * '.' 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /bin/setup.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | if ENV['AMAZON_ACCESS_KEY_ID'] && ENV['AMAZON_SECRET_ACCESS_KEY'] 3 | AWS::S3::Base.establish_connection!( 4 | :access_key_id => ENV['AMAZON_ACCESS_KEY_ID'], 5 | :secret_access_key => ENV['AMAZON_SECRET_ACCESS_KEY'] 6 | ) 7 | end 8 | 9 | require File.dirname(__FILE__) + '/../test/fixtures' 10 | include AWS::S3 -------------------------------------------------------------------------------- /support/faster-xml-simple/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'test/unit' 3 | require 'faster_xml_simple' 4 | 5 | class FasterXSTest < Test::Unit::TestCase 6 | def default_test 7 | end 8 | 9 | def silence_stderr 10 | str = STDERR.dup 11 | STDERR.reopen("/dev/null") 12 | STDERR.sync=true 13 | yield 14 | ensure 15 | STDERR.reopen(str) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /support/faster-xml-simple/README: -------------------------------------------------------------------------------- 1 | FasterXmlSimple 2 | 3 | FasterXS is intended to be a drop in replacement for the xml input functionality 4 | from XmlSimple. Instead of using rexml, it uses libxml and the associated ruby 5 | bindings. This reduces CPU utilisation and memory consumption considerably. 6 | 7 | Preliminary benchmarks show it between 3 and 10 times as fast, and using a 8 | fraction of the ram. -------------------------------------------------------------------------------- /test/fixtures/logging.yml: -------------------------------------------------------------------------------- 1 | logging_enabled: > 2 | 3 | 4 | mylogs 5 | access_log- 6 | 7 | 8 | 9 | logging_disabled: > 10 | 11 | 15 | -------------------------------------------------------------------------------- /test/fixtures/loglines.yml: -------------------------------------------------------------------------------- 1 | bucket_get: 2 | "bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 marcel [14/Nov/2006:06:36:48 +0000] 67.165.183.125 bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 8B5297D428A05432 REST.GET.BUCKET - \"GET /marcel HTTP/1.1\" 200 - 4534 - 398 395 \"-\" \"-\"\n" 3 | 4 | browser_get: 5 | "bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 marcel [25/Nov/2006:06:26:23 +0000] 67.165.183.125 65a011a29cdf8ec533ec3d1ccaae921c 41521D07CA012312 REST.GET.OBJECT kiss.jpg \"GET /marcel/kiss.jpg HTTP/1.1\" 200 - 67748 67748 259 104 \"-\" \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0\"\n" -------------------------------------------------------------------------------- /test/fixtures/policies.yml: -------------------------------------------------------------------------------- 1 | policy_with_one_grant: > 2 | 3 | 4 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 5 | mmolina@onramp.net 6 | 7 | 8 | 9 | 10 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 11 | mmolina@onramp.net 12 | 13 | FULL_CONTROL 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/mocks/fake_response.rb: -------------------------------------------------------------------------------- 1 | module AWS 2 | module S3 3 | class FakeResponse 4 | attr_reader :code, :body, :headers 5 | def initialize(options = {}) 6 | @code = options.delete(:code) || 200 7 | @body = options.delete(:body) || '' 8 | @headers = {'content-type' => 'application/xml'}.merge(options.delete(:headers) || {}) 9 | end 10 | 11 | # For ErrorResponse 12 | def response 13 | body 14 | end 15 | 16 | def [](header) 17 | headers[header] 18 | end 19 | 20 | def each(&block) 21 | headers.each(&block) 22 | end 23 | alias_method :each_header, :each 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-7.yml: -------------------------------------------------------------------------------- 1 | AccessControlPolicy: 2 | Owner: 3 | DisplayName: 4 | __content__: noradio 5 | ID: 6 | __content__: bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 7 | AccessControlList: 8 | Grant: 9 | - Permission: 10 | __content__: FULL_CONTROL 11 | Grantee: 12 | DisplayName: 13 | __content__: noradio 14 | ID: 15 | __content__: bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 16 | xsi:type: CanonicalUser 17 | - Permission: 18 | __content__: READ 19 | Grantee: 20 | URI: 21 | __content__: http://acs.amazonaws.com/groups/global/AllUsers 22 | xsi:type: Group 23 | -------------------------------------------------------------------------------- /site/public/ruby.css: -------------------------------------------------------------------------------- 1 | .ruby {border: 1px solid #333; } 2 | .ruby .normal {} 3 | .ruby .comment { color: #666; background-color: #eee; font-style: italic; } 4 | .ruby .keyword { color: #c96; font-weight: bold; } 5 | .ruby .method { color: #333; } 6 | .ruby .class { color: #333; } 7 | .ruby .module { color: #333; } 8 | .ruby .punct { color: #333; font-weight: bold; } 9 | .ruby .symbol { color: #333; } 10 | .ruby .string { color: #996; } 11 | .ruby .char { color: #999; } 12 | .ruby .ident { color: #333; } 13 | .ruby .constant { color: #69c; font-weight: bold;} 14 | .ruby .regex { color: #333; } 15 | .ruby .number { color: #333; } 16 | .ruby .attribute { color: #333; } 17 | .ruby .global { color: #333; } 18 | .ruby .expr { color: #333; } -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-7.rails.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AccessControlPolicy: 3 | AccessControlList: 4 | Grant: 5 | - Permission: 6 | __content__: FULL_CONTROL 7 | Grantee: 8 | DisplayName: 9 | __content__: noradio 10 | ID: 11 | __content__: bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 12 | xsi:type: CanonicalUser 13 | - Permission: 14 | __content__: READ 15 | Grantee: 16 | URI: 17 | __content__: http://acs.amazonaws.com/groups/global/AllUsers 18 | xsi:type: Group 19 | Owner: 20 | DisplayName: 21 | __content__: noradio 22 | ID: 23 | __content__: bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 24 | -------------------------------------------------------------------------------- /test/remote/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'uri' 3 | $:.unshift File.dirname(__FILE__) + '/../../lib' 4 | require 'aws/s3' 5 | begin 6 | require_library_or_gem 'breakpoint' 7 | rescue LoadError 8 | end 9 | 10 | TEST_BUCKET = 'aws-s3-tests' 11 | TEST_FILE = File.dirname(__FILE__) + '/test_file.data' 12 | 13 | class Test::Unit::TestCase 14 | include AWS::S3 15 | def establish_real_connection 16 | Base.establish_connection!( 17 | :access_key_id => ENV['AMAZON_ACCESS_KEY_ID'], 18 | :secret_access_key => ENV['AMAZON_SECRET_ACCESS_KEY'] 19 | ) 20 | end 21 | 22 | def disconnect! 23 | Base.disconnect 24 | end 25 | 26 | class TestBucket < Bucket 27 | set_current_bucket_to TEST_BUCKET 28 | end 29 | 30 | class TestS3Object < S3Object 31 | set_current_bucket_to TEST_BUCKET 32 | end 33 | end -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-7.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 5 | noradio 6 | 7 | 8 | 9 | 10 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 11 | noradio 12 | 13 | FULL_CONTROL 14 | 15 | 16 | 17 | http://acs.amazonaws.com/groups/global/AllUsers 18 | 19 | READ 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/service_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | 3 | class ServiceTest < Test::Unit::TestCase 4 | def test_bucket_list_with_empty_bucket_list 5 | mock_connection_for(Service, :returns => {:body => Fixtures::Buckets.empty_bucket_list, :code => 200}) 6 | list = Service.buckets(:reload) 7 | assert_equal [], list 8 | end 9 | 10 | def test_bucket_list_with_bucket_list_containing_one_bucket 11 | mock_connection_for(Service, :returns => {:body => Fixtures::Buckets.bucket_list_with_one_bucket, :code => 200}) 12 | list = Service.buckets(:reload) 13 | assert_equal 1, list.size 14 | assert_equal 'marcel_molina', list.first.name 15 | end 16 | 17 | def test_bucket_list_with_bucket_list_containing_more_than_one_bucket 18 | mock_connection_for(Service, :returns => {:body => Fixtures::Buckets.bucket_list_with_more_than_one_bucket, :code => 200}) 19 | list = Service.buckets(:reload) 20 | assert_equal 2, list.size 21 | assert_equal %w(marcel_molina marcel_molina_jr), list.map {|bucket| bucket.name}.sort 22 | end 23 | end -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-6.xml: -------------------------------------------------------------------------------- 1 | 2 | projectionist 3 | 4 | 5 | 1000 6 | false 7 | 8 | 1973-plymouth-satellite-sebring.jpg 9 | 2006-11-15T05:49:39.000Z 10 | "2ac1aa042e20ab7e1a9879b0df9f17b7" 11 | 186870 12 | 13 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 14 | noradio 15 | 16 | STANDARD 17 | 18 | 19 | 37-cluster.jpg 20 | 2006-11-15T05:51:20.000Z 21 | "4ead118ba91491f9c9697153264a1943" 22 | 43562 23 | 24 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 25 | noradio 26 | 27 | STANDARD 28 | 29 | -------------------------------------------------------------------------------- /support/faster-xml-simple/COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006 Michael Koziarski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in the 5 | Software without restriction, including without limitation the rights to use, 6 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 7 | Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN 17 | AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 18 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2006-2009 Marcel Molina Jr. 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 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-6.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ListBucketResult: 3 | Prefix: 4 | Name: 5 | __content__: projectionist 6 | MaxKeys: 7 | __content__: "1000" 8 | Contents: 9 | - StorageClass: 10 | __content__: STANDARD 11 | Owner: 12 | DisplayName: 13 | __content__: noradio 14 | ID: 15 | __content__: bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 16 | Size: 17 | __content__: "186870" 18 | ETag: 19 | __content__: "\"2ac1aa042e20ab7e1a9879b0df9f17b7\"" 20 | LastModified: 21 | __content__: "2006-11-15T05:49:39.000Z" 22 | Key: 23 | __content__: 1973-plymouth-satellite-sebring.jpg 24 | - StorageClass: 25 | __content__: STANDARD 26 | Owner: 27 | DisplayName: 28 | __content__: noradio 29 | ID: 30 | __content__: bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 31 | Size: 32 | __content__: "43562" 33 | ETag: 34 | __content__: "\"4ead118ba91491f9c9697153264a1943\"" 35 | LastModified: 36 | __content__: "2006-11-15T05:51:20.000Z" 37 | Key: 38 | __content__: 37-cluster.jpg 39 | Marker: 40 | IsTruncated: 41 | __content__: "false" 42 | -------------------------------------------------------------------------------- /test/fixtures/errors.yml: -------------------------------------------------------------------------------- 1 | not_implemented: > 2 | 3 | NotImplemented 4 | A header you provided implies functionality that is not implemented 5 | D1D13A09AC92427F 6 |
Host
7 | oNZgzTTmWiovwGGwHXAzz+1vRmAJVAplS9TF7B0cuOGfEwoi7DYSTa/1Qhv90CfW 8 |
9 | 10 | access_denied: > 11 | 12 | AccessDenied 13 | Access Denied 14 | F99F6D58B96C98E0 15 | XwCF7k3llrcEwtoHR7MusZ6ilCdF5DKDmwYpglvjKNjvwo24INCeXlEpo1M03Wxm 16 | 17 | 18 | internal_error: > 19 | 20 | InternalError 21 | Internal Error 22 | F99F6D223B96C98E0 23 | XwCF7k3llrcEwtoHR7MusZ6ilCdF5DKDmwYpglvjKNjvwo24INCeXlEpo1M03Wxm 24 | 25 | 26 | error_with_no_message: > 27 | 28 | InvalidArgument 29 | 30 | READ 31 | 74A377B1C0FA2BCF 32 | cP4rqsAEtHpN6Ckv08Hr3LXjLzx15/YgyoSqzs779vMR8MrAFSodxZp96wtuMQuI 33 | x-amz-acl 34 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-6.rails.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ListBucketResult: 3 | Prefix: {} 4 | 5 | Name: 6 | __content__: projectionist 7 | MaxKeys: 8 | __content__: "1000" 9 | Contents: 10 | - StorageClass: 11 | __content__: STANDARD 12 | Owner: 13 | DisplayName: 14 | __content__: noradio 15 | ID: 16 | __content__: bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 17 | Size: 18 | __content__: "186870" 19 | ETag: 20 | __content__: "\"2ac1aa042e20ab7e1a9879b0df9f17b7\"" 21 | LastModified: 22 | __content__: "2006-11-15T05:49:39.000Z" 23 | Key: 24 | __content__: 1973-plymouth-satellite-sebring.jpg 25 | - StorageClass: 26 | __content__: STANDARD 27 | Owner: 28 | DisplayName: 29 | __content__: noradio 30 | ID: 31 | __content__: bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 32 | Size: 33 | __content__: "43562" 34 | ETag: 35 | __content__: "\"4ead118ba91491f9c9697153264a1943\"" 36 | LastModified: 37 | __content__: "2006-11-15T05:51:20.000Z" 38 | Key: 39 | __content__: 37-cluster.jpg 40 | Marker: {} 41 | 42 | IsTruncated: 43 | __content__: "false" 44 | -------------------------------------------------------------------------------- /README.erb: -------------------------------------------------------------------------------- 1 | = AWS::S3 2 | 3 | <%= docs_for['AWS::S3'] %> 4 | 5 | == AWS::S3 Basics 6 | === The service, buckets and objects 7 | 8 | The three main concepts of S3 are the service, buckets and objects. 9 | 10 | ==== The service 11 | 12 | <%= docs_for['AWS::S3::Service'] %> 13 | 14 | ==== Buckets 15 | 16 | <%= docs_for['AWS::S3::Bucket'] %> 17 | 18 | ==== Objects 19 | 20 | <%= docs_for['AWS::S3::S3Object'] %> 21 | 22 | ==== Streaming uploads 23 | 24 | <%= docs_for['AWS::S3::S3Object::store'] %> 25 | 26 | == Setting the current bucket 27 | ==== Scoping operations to a specific bucket 28 | 29 | <%= docs_for['AWS::S3::Base.set_current_bucket_to'] %> 30 | 31 | == BitTorrent 32 | ==== Another way to download large files 33 | 34 | <%= docs_for['AWS::S3::BitTorrent'] %> 35 | 36 | == Access control 37 | ==== Using canned access control policies 38 | 39 | <%= docs_for['AWS::S3::ACL'] %> 40 | 41 | ==== Accessing private objects from a browser 42 | 43 | <%= docs_for['AWS::S3::S3Object.url_for'] %> 44 | 45 | == Logging 46 | ==== Tracking requests made on a bucket 47 | 48 | <%= docs_for['AWS::S3::Logging'] %> 49 | 50 | == Errors 51 | ==== When things go wrong 52 | 53 | <%= docs_for['AWS::S3::Error'] %> 54 | 55 | ==== Accessing the last request's response 56 | 57 | <%= docs_for['AWS::S3::Service.response'] %> 58 | -------------------------------------------------------------------------------- /test/fixtures/logs.yml: -------------------------------------------------------------------------------- 1 | simple_log: 2 | - "bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 marcel [14/Nov/2006:06:36:48 +0000] 67.165.183.125 bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 8B5297D428A05432 REST.GET.BUCKET - \"GET /marcel HTTP/1.1\" 200 - 4534 - 398 395 \"-\" \"-\"\n" 3 | - "bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 marcel [14/Nov/2006:06:38:58 +0000] 67.165.183.125 bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 8F6F3C4027849420 REST.GET.BUCKET - \"GET /marcel HTTP/1.1\" 200 - 4534 - 458 456 \"-\" \"-\"\n" 4 | 5 | requests_from_a_browser: 6 | - "bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 marcel [25/Nov/2006:06:26:23 +0000] 67.165.183.125 65a011a29cdf8ec533ec3d1ccaae921c 41521D07CA012312 REST.GET.OBJECT kiss.jpg \"GET /marcel/kiss.jpg HTTP/1.1\" 200 - 67748 67748 259 104 \"-\" \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0\"\n" 7 | - "bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 marcel [25/Nov/2006:06:26:27 +0000] 67.165.183.125 65a011a29cdf8ec533ec3d1ccaae921c 88629578AFDDD9B5 REST.GET.TORRENT kiss.jpg \"GET /marcel/kiss.jpg?torrent HTTP/1.1\" 200 - 215 - 379 - \"-\" \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0\"\n" -------------------------------------------------------------------------------- /test/remote/bittorrent_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | 3 | class RemoteBittorrentTest < Test::Unit::TestCase 4 | def setup 5 | establish_real_connection 6 | end 7 | 8 | def teardown 9 | disconnect! 10 | end 11 | 12 | def test_bittorrent 13 | bt_test_key = 'testing-bittorrent' 14 | S3Object.create(bt_test_key, 'foo', TEST_BUCKET) 15 | 16 | # Confirm we can fetch a bittorrent file for this object 17 | 18 | torrent_file = nil 19 | assert_nothing_raised do 20 | torrent_file = S3Object.torrent_for(bt_test_key, TEST_BUCKET) 21 | end 22 | assert torrent_file 23 | assert torrent_file['tracker'] 24 | 25 | # Make object accessible to the public via a torrent 26 | 27 | policy = S3Object.acl(bt_test_key, TEST_BUCKET) 28 | 29 | assert !policy.grants.include?(:public_read) 30 | 31 | assert_nothing_raised do 32 | S3Object.grant_torrent_access_to(bt_test_key, TEST_BUCKET) 33 | end 34 | 35 | policy = S3Object.acl(bt_test_key, TEST_BUCKET) 36 | 37 | assert policy.grants.include?(:public_read) 38 | 39 | # Confirm instance method wraps class method 40 | 41 | assert_equal torrent_file, S3Object.find(bt_test_key, TEST_BUCKET).torrent 42 | 43 | S3Object.delete(bt_test_key, TEST_BUCKET) 44 | end 45 | end -------------------------------------------------------------------------------- /lib/aws/s3/owner.rb: -------------------------------------------------------------------------------- 1 | module AWS 2 | module S3 3 | # Entities in S3 have an associated owner (the person who created them). The owner is a canonical representation of an 4 | # entity in the S3 system. It has an id and a display_name. 5 | # 6 | # These attributes can be used when specifying a ACL::Grantee for an ACL::Grant. 7 | # 8 | # You can retrieve the owner of the current account by calling Owner.current. 9 | class Owner 10 | undef_method :id if method_defined?(:id) # Get rid of Object#id 11 | include SelectiveAttributeProxy 12 | 13 | class << self 14 | # The owner of the current account. 15 | def current 16 | response = Service.get('/') 17 | new(response.parsed['owner']) if response.parsed['owner'] 18 | end 19 | memoized :current 20 | end 21 | 22 | def initialize(attributes = {}) #:nodoc: 23 | @attributes = attributes 24 | end 25 | 26 | def ==(other_owner) #:nodoc: 27 | hash == other_owner.hash 28 | end 29 | 30 | def hash #:nodoc 31 | [id, display_name].join.hash 32 | end 33 | 34 | private 35 | def proxiable_attribute?(name) 36 | valid_attributes.include?(name) 37 | end 38 | 39 | def valid_attributes 40 | %w(id display_name) 41 | end 42 | end 43 | end 44 | end -------------------------------------------------------------------------------- /support/faster-xml-simple/test/xml_simple_comparison_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | require 'yaml' 3 | 4 | class XmlSimpleComparisonTest < FasterXSTest 5 | 6 | # Define test methods 7 | 8 | Dir["test/fixtures/test-*.xml"].each do |file_name| 9 | xml_file_name = file_name 10 | method_name = File.basename(file_name, ".xml").gsub('-', '_') 11 | yml_file_name = file_name.gsub('xml', 'yml') 12 | rails_yml_file_name = file_name.gsub('xml', 'rails.yml') 13 | class_eval <<-EOV, __FILE__, __LINE__ 14 | def #{method_name} 15 | assert_equal YAML.load(File.read('#{yml_file_name}')), 16 | FasterXmlSimple.xml_in(File.read('#{xml_file_name}'), default_options ) 17 | end 18 | 19 | def #{method_name}_rails 20 | assert_equal YAML.load(File.read('#{rails_yml_file_name}')), 21 | FasterXmlSimple.xml_in(File.read('#{xml_file_name}'), rails_options) 22 | end 23 | EOV 24 | end 25 | 26 | def default_options 27 | { 28 | 'keeproot' => true, 29 | 'contentkey' => '__content__', 30 | 'forcecontent' => true, 31 | 'suppressempty' => nil, 32 | 'forcearray' => ['something-else'] 33 | } 34 | end 35 | 36 | def rails_options 37 | { 38 | 'forcearray' => false, 39 | 'forcecontent' => true, 40 | 'keeproot' => true, 41 | 'contentkey' => '__content__' 42 | } 43 | end 44 | 45 | 46 | end -------------------------------------------------------------------------------- /site/public/screen.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #fff; 3 | color: #333; 4 | font-family: verdana, sans-serif; 5 | font-size: 12px; 6 | margin: 0; 7 | } 8 | 9 | div.left_column { 10 | text-align: right; 11 | float: left; 12 | width: 185px; 13 | font-size: 14px; 14 | padding-right: 15px; 15 | } 16 | 17 | div.left_column img { 18 | margin-right: -15px; 19 | } 20 | 21 | 22 | pre { 23 | color: #555; 24 | margin-left: 2em; 25 | overflow: auto; 26 | width: 600px; 27 | } 28 | 29 | div.page_area { 30 | margin: 40px 0 120px 50px; 31 | } 32 | 33 | div.column { 34 | margin-left: 220px; 35 | padding-top: 3px; 36 | width: 450px; 37 | } 38 | 39 | a:link, 40 | a:visited { 41 | color: #039; 42 | } 43 | 44 | h1 { 45 | margin: 0 0 50px 0; 46 | font-size: 14px; 47 | } 48 | 49 | h2 { 50 | color: #bc0707; 51 | font-size: 24px; 52 | margin: 0; 53 | } 54 | 55 | h3 { 56 | font-size: 16px; 57 | line-height: 20px; 58 | margin: 10px 0 0 0; 59 | } 60 | 61 | 62 | div.header { 63 | font-size: 14px; 64 | } 65 | 66 | div.header p.links { 67 | color: #ccc; 68 | } 69 | 70 | div.header h4 { 71 | margin-bottom: 7px; 72 | font-size: 16px; 73 | } 74 | 75 | div.header pre { 76 | margin-top: 0; 77 | } 78 | 79 | 80 | div.readme { 81 | font-size: 12px; 82 | line-height: 140%; 83 | } 84 | 85 | div.readme h2 { 86 | font-size: 20px; 87 | margin: 2em 0 0 0; 88 | } 89 | 90 | div.readme h4 { 91 | font-size: 12px; 92 | color: #bc0707; 93 | color: #000; 94 | margin-top: 3em; 95 | } 96 | 97 | div.readme h4.first { 98 | margin-top: 1.5em; 99 | } 100 | -------------------------------------------------------------------------------- /site/index.erb: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | AWS::S3 - Ruby Library for Amazon Simple Storage Service (S3) 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 |
19 |
20 |
21 |

AWS::S3

22 |

A Ruby Library for Amazon's Simple
Storage Service's (S3) REST API.

23 | 29 |

Download with RubyGems

30 |
sudo gem i aws-s3
31 |

Clone from the master git repository

32 |
git clone git://github.com/marcel/aws-s3.git
33 |
34 | 35 |
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 --------------------------------------------------------------------------------