aws-s3 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/COPYING +19 -0
  2. data/INSTALL +35 -0
  3. data/README +529 -0
  4. data/Rakefile +284 -0
  5. data/bin/s3sh +4 -0
  6. data/bin/setup.rb +10 -0
  7. data/lib/aws/s3.rb +64 -0
  8. data/lib/aws/s3/acl.rb +631 -0
  9. data/lib/aws/s3/authentication.rb +218 -0
  10. data/lib/aws/s3/base.rb +232 -0
  11. data/lib/aws/s3/bittorrent.rb +58 -0
  12. data/lib/aws/s3/bucket.rb +323 -0
  13. data/lib/aws/s3/connection.rb +212 -0
  14. data/lib/aws/s3/error.rb +69 -0
  15. data/lib/aws/s3/exceptions.rb +130 -0
  16. data/lib/aws/s3/extensions.rb +186 -0
  17. data/lib/aws/s3/logging.rb +163 -0
  18. data/lib/aws/s3/object.rb +565 -0
  19. data/lib/aws/s3/owner.rb +44 -0
  20. data/lib/aws/s3/parsing.rb +138 -0
  21. data/lib/aws/s3/response.rb +180 -0
  22. data/lib/aws/s3/service.rb +43 -0
  23. data/lib/aws/s3/version.rb +12 -0
  24. data/support/faster-xml-simple/lib/faster_xml_simple.rb +115 -0
  25. data/support/faster-xml-simple/test/regression_test.rb +16 -0
  26. data/support/faster-xml-simple/test/xml_simple_comparison_test.rb +22 -0
  27. data/support/rdoc/code_info.rb +211 -0
  28. data/test/acl_test.rb +243 -0
  29. data/test/authentication_test.rb +96 -0
  30. data/test/base_test.rb +143 -0
  31. data/test/bucket_test.rb +48 -0
  32. data/test/connection_test.rb +120 -0
  33. data/test/error_test.rb +75 -0
  34. data/test/extensions_test.rb +282 -0
  35. data/test/fixtures.rb +89 -0
  36. data/test/fixtures/buckets.yml +102 -0
  37. data/test/fixtures/errors.yml +34 -0
  38. data/test/fixtures/headers.yml +3 -0
  39. data/test/fixtures/logging.yml +15 -0
  40. data/test/fixtures/policies.yml +16 -0
  41. data/test/logging_test.rb +36 -0
  42. data/test/mocks/base.rb +89 -0
  43. data/test/object_test.rb +177 -0
  44. data/test/parsing_test.rb +82 -0
  45. data/test/remote/acl_test.rb +117 -0
  46. data/test/remote/bittorrent_test.rb +45 -0
  47. data/test/remote/bucket_test.rb +127 -0
  48. data/test/remote/logging_test.rb +82 -0
  49. data/test/remote/object_test.rb +267 -0
  50. data/test/remote/test_file.data +0 -0
  51. data/test/remote/test_helper.rb +30 -0
  52. data/test/response_test.rb +70 -0
  53. data/test/service_test.rb +26 -0
  54. data/test/test_helper.rb +82 -0
  55. metadata +125 -0
@@ -0,0 +1,218 @@
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 <tt>Authorization</tt> header whose value
9
+ # is formatted like so:
10
+ #
11
+ # "AWS #{access_key_id}:#{encoded_canonical}"
12
+ #
13
+ # The <tt>access_key_id</tt> is the public key that is assigned by Amazon for a given account which you use when
14
+ # establishing your initial connection. The <tt>encoded_canonical</tt> 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 <tt>secret_access_key</tt> 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 <tt>access_key_id</tt> and <tt>encoded_canonical</tt> are the same as described in the Header based authentication section.
34
+ # The <tt>expires</tt> 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
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 = Base64.encode64(OpenSSL::HMAC.digest(digest, secret_access_key, canonical_string)).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
+
103
+ def initialize(*args)
104
+ super
105
+ @options[:url_encode] = true
106
+ self << build
107
+ end
108
+
109
+ private
110
+
111
+ # Will return one of three values, in the following order of precedence:
112
+ #
113
+ # 1) Seconds since the epoch explicitly passed in the +:expires+ option
114
+ # 2) The current time in seconds since the epoch plus the number of seconds passed in
115
+ # the +:expires_in+ option
116
+ # 3) The current time in seconds since the epoch plus the default number of seconds (60 seconds)
117
+ def expires
118
+ return @options[:expires] if @options[:expires]
119
+ date.to_i + (@options[:expires_in] || DEFAULT_EXPIRY)
120
+ end
121
+
122
+ # Keep in alphabetical order
123
+ def build
124
+ "AWSAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}"
125
+ end
126
+ end
127
+
128
+ # The CanonicalString is used to generate an encrypted signature, signed with your secrect access key. It is composed of
129
+ # data related to the given request for which it provides authentication. This data includes the request method, request headers,
130
+ # and the request path. Both Header and QueryString use it to generate their signature.
131
+ class CanonicalString < String #:nodoc:
132
+ class << self
133
+ def default_headers
134
+ %w(content-type content-md5)
135
+ end
136
+
137
+ def interesting_headers
138
+ ['content-md5', 'content-type', 'date', amazon_header_prefix]
139
+ end
140
+
141
+ def amazon_header_prefix
142
+ /^#{AMAZON_HEADER_PREFIX}/io
143
+ end
144
+ end
145
+
146
+ attr_reader :request, :headers
147
+
148
+ def initialize(request, options = {})
149
+ super()
150
+ @request = request
151
+ @headers = {}
152
+ @options = options
153
+ # "For non-authenticated or anonymous requests. A NotImplemented error result code will be returned if
154
+ # an authenticated (signed) request specifies a Host: header other than 's3.amazonaws.com'"
155
+ # (from http://docs.amazonwebservices.com/AmazonS3/2006-03-01/VirtualHosting.html)
156
+ request['Host'] = DEFAULT_HOST
157
+ build
158
+ end
159
+
160
+ private
161
+ def build
162
+ self << "#{request.method}\n"
163
+ ensure_date_is_valid
164
+
165
+ initialize_headers
166
+ set_expiry!
167
+
168
+ headers.sort_by {|k, _| k}.each do |key, value|
169
+ value = value.to_s.strip
170
+ self << (key =~ self.class.amazon_header_prefix ? "#{key}:#{value}" : value)
171
+ self << "\n"
172
+ end
173
+ self << path
174
+ end
175
+
176
+ def initialize_headers
177
+ identify_interesting_headers
178
+ set_default_headers
179
+ end
180
+
181
+ def set_expiry!
182
+ self.headers['date'] = @options[:expires] if @options[:expires]
183
+ end
184
+
185
+ def ensure_date_is_valid
186
+ request['Date'] ||= Time.now.httpdate
187
+ end
188
+
189
+ def identify_interesting_headers
190
+ request.each do |key, value|
191
+ key = key.downcase # Can't modify frozen string so no bang
192
+ if self.class.interesting_headers.any? {|header| header === key}
193
+ self.headers[key] = value.to_s.strip
194
+ end
195
+ end
196
+ end
197
+
198
+ def set_default_headers
199
+ self.class.default_headers.each do |header|
200
+ self.headers[header] ||= ''
201
+ end
202
+ end
203
+
204
+ def path
205
+ [only_path, extract_significant_parameter].compact.join('?')
206
+ end
207
+
208
+ def extract_significant_parameter
209
+ request.path[/[&?](acl|torrent|logging)(?:&|=|$)/, 1]
210
+ end
211
+
212
+ def only_path
213
+ request.path[/^[^?]*/]
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,232 @@
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 <tt>s3sh</tt>. 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 <tt>:access_key_id</tt> and <tt>:secret_access_key</tt> 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, URI.escape(path), options, body, &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, Timeout::Error
78
+ attempts == 3 ? raise : (attempts += 1; retry)
79
+ end
80
+
81
+ [:get, :post, :put, :delete, :head].each do |verb|
82
+ class_eval(<<-EVAL, __FILE__, __LINE__)
83
+ def #{verb}(path, headers = {}, body = nil, &block)
84
+ request(:#{verb}, path, headers, body, &block)
85
+ end
86
+ EVAL
87
+ end
88
+
89
+ # Called when a method which requires a bucket name is called without that bucket name specified. It will try to
90
+ # infer the current bucket by looking for it as the subdomain of the current connection's address. If no subdomain
91
+ # is found, CurrentBucketNotSpecified will be raised.
92
+ #
93
+ # MusicBucket.establish_connection! :server => 'jukeboxzero.s3.amazonaws.com'
94
+ # MusicBucket.connection.server
95
+ # => 'jukeboxzero.s3.amazonaws.com'
96
+ # MusicBucket.current_bucket
97
+ # => 'jukeboxzero'
98
+ #
99
+ # Rather than infering the current bucket from the subdomain, the current class' bucket can be explicitly set with
100
+ # set_current_bucket_to.
101
+ def current_bucket
102
+ connection.subdomain or raise CurrentBucketNotSpecified.new(connection.http.address)
103
+ end
104
+
105
+ # If you plan on always using a specific bucket for certain files, you can skip always having to specify the bucket by creating
106
+ # a subclass of Bucket or S3Object and telling it what bucket to use:
107
+ #
108
+ # class JukeBoxSong < AWS::S3::S3Object
109
+ # set_current_bucket_to 'jukebox'
110
+ # end
111
+ #
112
+ # For all methods that take a bucket name as an argument, the current bucket will be used if the bucket name argument is omitted.
113
+ #
114
+ # other_song = '/Users/marcel/baby-please-come-home.mp3'
115
+ # JukeBoxSong.store(
116
+ # File.basename(other_song),
117
+ # File.open(other_song),
118
+ # :content_type => 'audio/mpeg'
119
+ # )
120
+ #
121
+ # This time we didn't have to explicitly pass in the bucket name, as the JukeBoxSong class knows that it will
122
+ # always use the 'jukebox' bucket.
123
+ #
124
+ # "Astute readers", as they say, may have noticed that we used the third parameter to pass in the content type,
125
+ # rather than the fourth parameter as we had the last time we created an object. If the bucket can be infered, or
126
+ # is explicitly set, as we've done in the JukeBoxSong class, then the third argument can be used to pass in
127
+ # options.
128
+ #
129
+ # Now all operations that would have required a bucket name no longer do.
130
+ #
131
+ # other_song = JukeBoxSong.find('baby-please-come-home.mp3')
132
+ def set_current_bucket_to(name)
133
+ raise ArgumentError, "`#{__method__}' must be called on a subclass of #{self.name}" if self == AWS::S3::Base
134
+ instance_eval(<<-EVAL)
135
+ def current_bucket
136
+ '#{name}'
137
+ end
138
+ EVAL
139
+ end
140
+ alias_method :current_bucket=, :set_current_bucket_to
141
+
142
+ private
143
+
144
+ def response_class
145
+ FindResponseClass.for(self)
146
+ end
147
+
148
+ def process_options!(options, verb)
149
+ options.replace(RequestOptions.process(options, verb))
150
+ end
151
+
152
+ # Using the conventions layed out in the <tt>response_class</tt> works for more than 80% of the time.
153
+ # There are a few edge cases though where we want a given class to wrap its responses in different
154
+ # response classes depending on which method is being called.
155
+ def respond_with(klass)
156
+ eval(<<-EVAL, binding, __FILE__, __LINE__)
157
+ def new_response_class
158
+ #{klass}
159
+ end
160
+
161
+ class << self
162
+ alias_method :old_response_class, :response_class
163
+ alias_method :response_class, :new_response_class
164
+ end
165
+ EVAL
166
+
167
+ yield
168
+ ensure
169
+ # Restore the original version
170
+ eval(<<-EVAL, binding, __FILE__, __LINE__)
171
+ class << self
172
+ alias_method :response_class, :old_response_class
173
+ end
174
+ EVAL
175
+ end
176
+
177
+ def bucket_name(name)
178
+ name || current_bucket
179
+ end
180
+
181
+ class RequestOptions < Hash #:nodoc:
182
+ attr_reader :options, :verb
183
+
184
+ class << self
185
+ def process(*args, &block)
186
+ new(*args, &block).process!
187
+ end
188
+ end
189
+
190
+ def initialize(options, verb = :get)
191
+ @options = options.to_normalized_options
192
+ @verb = verb
193
+ super()
194
+ end
195
+
196
+ def process!
197
+ set_access_controls! if verb == :put
198
+ replace(options)
199
+ end
200
+
201
+ private
202
+ def set_access_controls!
203
+ ACL::OptionProcessor.process!(options)
204
+ end
205
+ end
206
+ end
207
+
208
+ def initialize(attributes = {}) #:nodoc:
209
+ @attributes = attributes
210
+ end
211
+
212
+ private
213
+ attr_reader :attributes
214
+
215
+ def connection
216
+ self.class.connection
217
+ end
218
+
219
+ def http
220
+ connection.http
221
+ end
222
+
223
+ def request(*args, &block)
224
+ self.class.request(*args, &block)
225
+ end
226
+
227
+ def method_missing(method, *args, &block)
228
+ attributes[method.to_s] || attributes[method] || super
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,58 @@
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 <tt>torrent_for</tt>:
6
+ #
7
+ # S3Object.torrent_for 'kiss.jpg', 'marcel'
8
+ #
9
+ # Or just call the <tt>torrent</tt> method if you already have the object:
10
+ #
11
+ # song = S3Object.find 'kiss.jpg', 'marcel'
12
+ # song.torrent
13
+ #
14
+ # Calling <tt>grant_torrent_access_to</tt> 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 <tt>key</tt>.
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 <tt>key</tt> 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