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,323 @@
1
+ module AWS
2
+ module S3
3
+ # Buckets are containers for objects (the files you store on S3). To create a new bucket you just specify its name.
4
+ #
5
+ # # Pick a unique name, or else you'll get an error
6
+ # # if the name is already taken.
7
+ # Bucket.create('jukebox')
8
+ #
9
+ # Bucket names must be unique across the entire S3 system, sort of like domain names across the internet. If you try
10
+ # to create a bucket with a name that is already taken, you will get an error.
11
+ #
12
+ # Assuming the name you chose isn't already taken, your new bucket will now appear in the bucket list:
13
+ #
14
+ # Service.buckets
15
+ # # => [#<AWS::S3::Bucket @attributes={"name"=>"jukebox"}>]
16
+ #
17
+ # Once you have succesfully created a bucket you can you can fetch it by name using Bucket.find.
18
+ #
19
+ # music_bucket = Bucket.find('jukebox')
20
+ #
21
+ # The bucket that is returned will contain a listing of all the objects in the bucket.
22
+ #
23
+ # music_bucket.objects.size
24
+ # # => 0
25
+ #
26
+ # If all you are interested in is the objects of the bucket, you can get to them directly using Bucket.objects.
27
+ #
28
+ # Bucket.objects('jukebox').size
29
+ # # => 0
30
+ #
31
+ # By default all objects will be returned, though there are several options you can use to limit what is returned, such as
32
+ # specifying that only objects whose name is after a certain place in the alphabet be returned, and etc. Details about these options can
33
+ # be found in the documentation for Bucket.find.
34
+ #
35
+ # To add an object to a bucket you specify the name of the object, its value, and the bucket to put it in.
36
+ #
37
+ # file = 'black-flowers.mp3'
38
+ # S3Object.store(
39
+ # file,
40
+ # File.open(file),
41
+ # 'jukebox',
42
+ # :content_type => 'audio/mpeg'
43
+ # )
44
+ #
45
+ # Though strictly not required, you should specify the content type of the file so that when you go to download it your browser will know how to handle the file.
46
+ #
47
+ # You'll see your file has been added to it:
48
+ #
49
+ # music_bucket.objects
50
+ # # => [#<AWS::S3::S3Object '/jukebox/black-flowers.mp3'>]
51
+ #
52
+ # You can treat your bucket like a hash and access objects by name:
53
+ #
54
+ # jukebox['black-flowers.mp3']
55
+ # # => #<AWS::S3::S3Object '/jukebox/black-flowers.mp3'>
56
+ #
57
+ # In the event that you want to delete a bucket, you can use Bucket.delete.
58
+ #
59
+ # Bucket.delete('jukebox')
60
+ #
61
+ # Keep in mind, like unix directories, you can not delete a bucket unless it is empty. Trying to delete a bucket
62
+ # that contains objects will raise a BucketNotEmpty exception.
63
+ #
64
+ # Passing the :force => true option to delete will take care of deleting all the bucket's objects for you.
65
+ #
66
+ # Bucket.delete('photos', :force => true)
67
+ # # => true
68
+ class Bucket < Base
69
+ class << self
70
+ # Creates a bucket named <tt>name</tt>.
71
+ #
72
+ # Bucket.create('jukebox')
73
+ #
74
+ # Your bucket name must be unique across all of S3. If the name
75
+ # you request has already been taken, you will get a 409 Conflict response, and a BucketAlreadyExists exception
76
+ # will be raised.
77
+ #
78
+ # By default new buckets have their access level set to private. You can override this using the <tt>:access</tt> option.
79
+ #
80
+ # Bucket.create('internet_drop_box', :access => :public_read_write)
81
+ #
82
+ # The full list of access levels that you can set on Bucket and S3Object creation are listed in the README[link:files/README.html]
83
+ # in the section called 'Setting access levels'.
84
+ def create(name, options = {})
85
+ validate_name!(name)
86
+ put("/#{name}", options).success?
87
+ end
88
+
89
+ # Fetches the bucket named <tt>name</tt>.
90
+ #
91
+ # Bucket.find('jukebox')
92
+ #
93
+ # If a default bucket is inferable from the current connection's subdomain, or if set explicitly with Base.set_current_bucket,
94
+ # it will be used if no bucket is specified.
95
+ #
96
+ # MusicBucket.current_bucket
97
+ # => 'jukebox'
98
+ # MusicBucket.find.name
99
+ # => 'jukebox'
100
+ #
101
+ # By default all objects contained in the bucket will be returned (sans their data) along with the bucket.
102
+ # You can access your objects using the Bucket#objects method.
103
+ #
104
+ # Bucket.find('jukebox').objects
105
+ #
106
+ # There are several options which allow you to limit which objects are retrieved. The list of object filtering options
107
+ # are listed in the documentation for Bucket.objects.
108
+ def find(name = nil, options = {})
109
+ new(get(path(name, options)).bucket)
110
+ end
111
+
112
+ # Return just the objects in the bucket named <tt>name</tt>.
113
+ #
114
+ # By default all objects of the named bucket will be returned. There are options, though, for filtering
115
+ # which objects are returned.
116
+ #
117
+ # === Object filtering options
118
+ #
119
+ # * <tt>:max_keys</tt> - The maximum number of keys you'd like to see in the response body.
120
+ # The server may return fewer than this many keys, but will not return more.
121
+ #
122
+ # Bucket.objects('jukebox').size
123
+ # # => 3
124
+ # Bucket.objects('jukebox', :max_keys => 1).size
125
+ # # => 1
126
+ #
127
+ # * <tt>:prefix</tt> - Restricts the response to only contain results that begin with the specified prefix.
128
+ #
129
+ # Bucket.objects('jukebox')
130
+ # # => [<AWS::S3::S3Object '/jazz/miles.mp3'>, <AWS::S3::S3Object '/jazz/dolphy.mp3'>, <AWS::S3::S3Object '/classical/malher.mp3'>]
131
+ # Bucket.objects('jukebox', :prefix => 'classical')
132
+ # # => [<AWS::S3::S3Object '/classical/malher.mp3'>]
133
+ #
134
+ # * <tt>:marker</tt> - Marker specifies where in the result set to resume listing. It restricts the response
135
+ # to only contain results that occur alphabetically _after_ the value of marker. To retrieve the next set of results,
136
+ # use the last key from the current page of results as the marker in your next request.
137
+ #
138
+ # # Skip 'mahler'
139
+ # Bucket.objects('jukebox', :marker => 'mb')
140
+ # # => [<AWS::S3::S3Object '/jazz/miles.mp3'>]
141
+ #
142
+ # === Examples
143
+ #
144
+ # # Return no more than 2 objects whose key's are listed alphabetically after the letter 'm'.
145
+ # Bucket.objects('jukebox', :marker => 'm', :max_keys => 2)
146
+ # # => [<AWS::S3::S3Object '/jazz/miles.mp3'>, <AWS::S3::S3Object '/classical/malher.mp3'>]
147
+ #
148
+ # # Return no more than 2 objects whose key's are listed alphabetically after the letter 'm' and have the 'jazz' prefix.
149
+ # Bucket.objects('jukebox', :marker => 'm', :max_keys => 2, :prefix => 'jazz')
150
+ # # => [<AWS::S3::S3Object '/jazz/miles.mp3'>]
151
+ def objects(name = nil, options = {})
152
+ find(name, options).object_cache
153
+ end
154
+
155
+ # Deletes the bucket named <tt>name</tt>.
156
+ #
157
+ # All objects in the bucket must be deleted before the bucket can be deleted. If the bucket is not empty,
158
+ # BucketNotEmpty will be raised.
159
+ #
160
+ # You can side step this issue by passing the :force => true option to delete which will take care of
161
+ # emptying the bucket before deleting it.
162
+ #
163
+ # Bucket.delete('photos', :force => true)
164
+ #
165
+ # Only the owner of a bucket can delete a bucket, regardless of the bucket's access control policy.
166
+ def delete(name = nil, options = {})
167
+ name = path(name)
168
+ find(name).delete_all if options[:force]
169
+ # A bit confusing. Calling super actually makes makes an HTTP DELETE request. The delete method is
170
+ # defined in the Base class. It happens to have the same name.
171
+ super(name).success?
172
+ end
173
+
174
+ # List all your buckets. This is a convenient wrapper around AWS::S3::Service.buckets.
175
+ def list(reload = false)
176
+ Service.buckets(reload)
177
+ end
178
+
179
+ private
180
+ def validate_name!(name)
181
+ raise InvalidBucketName.new(name) unless name =~ /^[-\w]{3,255}$/
182
+ end
183
+
184
+ def path(name, options = {})
185
+ "/#{bucket_name(name)}#{RequestOptions.process(options).to_query_string}"
186
+ end
187
+ end
188
+
189
+ attr_reader :object_cache #:nodoc:
190
+
191
+ include Enumerable
192
+
193
+ def initialize(attributes = {}) #:nodoc:
194
+ super
195
+ @object_cache = []
196
+ build_contents!
197
+ end
198
+
199
+ # Fetches the object named <tt>object_key</tt>, or nil if the bucket does not contain an object with the
200
+ # specified key.
201
+ #
202
+ # bucket.objects
203
+ # => [#<AWS::S3::S3Object '/marcel_molina/beluga_baby.jpg'>,
204
+ # #<AWS::S3::S3Object '/marcel_molina/tongue_overload.jpg'>]
205
+ # bucket['beluga_baby.jpg']
206
+ # => #<AWS::S3::S3Object '/marcel_molina/beluga_baby.jpg'>
207
+ def [](object_key)
208
+ detect {|file| file.key == object_key.to_s}
209
+ end
210
+
211
+ # Initializes a new S3Object belonging to the current bucket.
212
+ #
213
+ # object = bucket.new_object
214
+ # object.value = data
215
+ # object.key = 'classical/mahler.mp3'
216
+ # object.store
217
+ # bucket.objects.include?(object)
218
+ # => true
219
+ def new_object(attributes = {})
220
+ object = S3Object.new(attributes)
221
+ register(object)
222
+ object
223
+ end
224
+
225
+ # List S3Object's of the bucket.
226
+ #
227
+ # Once fetched the objects will be cached. You can reload the objects by passing <tt>:reload</tt>.
228
+ #
229
+ # bucket.objects(:reload)
230
+ #
231
+ # You can also filter the objects using the same options listed in Bucket.objects.
232
+ #
233
+ # bucket.objects(:prefix => 'jazz')
234
+ #
235
+ # Using these filtering options will implictly reload the objects.
236
+ #
237
+ # To reclaim all the objects for the bucket you can pass in :reload again.
238
+ def objects(options = {})
239
+ if options.is_a?(Hash)
240
+ reload = !options.empty?
241
+ else
242
+ reload = options
243
+ options = {}
244
+ end
245
+
246
+ reload!(options) if reload || object_cache.empty?
247
+ object_cache
248
+ end
249
+
250
+ # Iterates over the objects in the bucket.
251
+ #
252
+ # bucket.each do |object|
253
+ # # Do something with the object ...
254
+ # end
255
+ def each(&block)
256
+ # Dup the collection since we might be destructively modifying the object_cache during the iteration.
257
+ objects.dup.each(&block)
258
+ end
259
+
260
+ # Returns true if there are no objects in the bucket.
261
+ def empty?
262
+ objects.empty?
263
+ end
264
+
265
+ # Returns the number of objects in the bucket.
266
+ def size
267
+ objects.size
268
+ end
269
+
270
+ # Deletes the bucket. See its class method counter part Bucket.delete for caveats about bucket deletion and how to ensure
271
+ # a bucket is deleted no matter what.
272
+ def delete(options = {})
273
+ self.class.delete(name, options)
274
+ end
275
+
276
+ # Delete all files in the bucket. Use with caution. Can not be undone.
277
+ def delete_all
278
+ each do |object|
279
+ object.delete
280
+ end
281
+ self
282
+ end
283
+ alias_method :clear, :delete_all
284
+
285
+ # Buckets observer their objects and have this method called when one of their objects
286
+ # is either stored or deleted.
287
+ def update(action, object) #:nodoc:
288
+ case action
289
+ when :stored then add object unless objects.include?(object)
290
+ when :deleted then object_cache.delete(object)
291
+ end
292
+ end
293
+
294
+ private
295
+ def build_contents!
296
+ return unless has_contents?
297
+ attributes.delete('contents').each do |content|
298
+ add new_object(content)
299
+ end
300
+ end
301
+
302
+ def has_contents?
303
+ attributes.has_key?('contents')
304
+ end
305
+
306
+ def add(object)
307
+ register(object)
308
+ object_cache << object
309
+ end
310
+
311
+ def register(object)
312
+ object.bucket = self
313
+ end
314
+
315
+ def reload!(options = {})
316
+ object_cache.clear
317
+ self.class.objects(name, options).each do |object|
318
+ add object
319
+ end
320
+ end
321
+ end
322
+ end
323
+ end
@@ -0,0 +1,212 @@
1
+ module AWS
2
+ module S3
3
+ class Connection #:nodoc:
4
+ class << self
5
+ def connect(options = {})
6
+ new(options)
7
+ end
8
+ end
9
+
10
+ attr_reader :access_key_id, :secret_access_key, :http, :options
11
+
12
+ # Creates a new connection. Connections make the actual requests to S3, though these requests are usually
13
+ # called from subclasses of Base.
14
+ #
15
+ # For details on establishing connections, check the Connection::Management::ClassMethods.
16
+ def initialize(options = {})
17
+ @options = Options.new(options)
18
+ connect
19
+ end
20
+
21
+ def request(verb, path, headers = {}, body = nil, &block)
22
+ http.start do
23
+ request = request_method(verb).new(path, headers)
24
+ authenticate!(request)
25
+ case body
26
+ when String then request.body = body
27
+ when IO then
28
+ request.body_stream = body
29
+ request.content_length = body.lstat.size
30
+ end if body
31
+ http.request(request, &block)
32
+ end
33
+ end
34
+
35
+ def url_for(path, options = {})
36
+ path = URI.escape(path)
37
+ request = request_method(:get).new(path, {})
38
+ query_string = query_string_authentication(request, options)
39
+ "#{protocol(options)}#{http.address}#{path}?#{query_string}"
40
+ end
41
+
42
+ def subdomain
43
+ http.address[/^([^.]+).#{DEFAULT_HOST}$/, 1]
44
+ end
45
+
46
+ def protocol(options = {})
47
+ (options[:use_ssl] || http.use_ssl?) ? 'https://' : 'http://'
48
+ end
49
+
50
+ private
51
+ def extract_keys!
52
+ missing_keys = []
53
+ extract_key = Proc.new {|key| options[key] || (missing_keys.push(key); nil)}
54
+ @access_key_id = extract_key[:access_key_id]
55
+ @secret_access_key = extract_key[:secret_access_key]
56
+ raise MissingAccessKey.new(missing_keys) unless missing_keys.empty?
57
+ end
58
+
59
+ def connect
60
+ extract_keys!
61
+ @http = Net::HTTP.new(options[:server], options[:port])
62
+ @http.use_ssl = !options[:use_ssl].nil? || options[:port] == 443
63
+ @http
64
+ end
65
+
66
+ # Just do Header authentication for now
67
+ def authenticate!(request)
68
+ request['Authorization'] = Authentication::Header.new(request, access_key_id, secret_access_key)
69
+ end
70
+
71
+ def query_string_authentication(request, options = {})
72
+ Authentication::QueryString.new(request, access_key_id, secret_access_key, options)
73
+ end
74
+
75
+ def request_method(verb)
76
+ Net::HTTP.const_get(verb.to_s.capitalize)
77
+ end
78
+
79
+ def method_missing(method, *args, &block)
80
+ options[method] || super
81
+ end
82
+
83
+ module Management #:nodoc:
84
+ def self.included(base)
85
+ base.send(:class_variable_set, :@@connections, {})
86
+ base.extend ClassMethods
87
+ end
88
+
89
+ # Manage the creation and destruction of connections for AWS::S3::Base and its subclasses. Connections are
90
+ # created with establish_connection!.
91
+ module ClassMethods
92
+ # Creates a new connection with which to make requests to the S3 servers for the calling class.
93
+ #
94
+ # AWS::S3::Base.establish_connection!(:access_key_id => '...', :secret_access_key => '...')
95
+ #
96
+ # You can set connections for every subclass of AWS::S3::Base. Once the initial connection is made on
97
+ # Base, all subsequent connections will inherit whatever values you don't specify explictly. This allows you to
98
+ # customize details of the connection, such as what server the requests are made to, by just specifying one
99
+ # option.
100
+ #
101
+ # AWS::S3::Bucket.established_connection!(:use_ssl => true)
102
+ #
103
+ # The Bucket connection would inherit the <tt>:access_key_id</tt> and the <tt>:secret_access_key</tt> from
104
+ # Base's connection. Unlike the Base connection, all Bucket requests would be made over SSL.
105
+ #
106
+ # == Required arguments
107
+ #
108
+ # * <tt>:access_key_id</tt> - The access key id for your S3 account. Provided by Amazon.
109
+ # * <tt>:secret_access_key</tt> - The secret access key for your S3 account. Provided by Amazon.
110
+ #
111
+ # If any of these required arguments is missing, a MissingAccessKey exception will be raised.
112
+ #
113
+ # == Optional arguments
114
+ #
115
+ # * <tt>:server</tt> - The server to make requests to. You can use this to specify your bucket in the subdomain,
116
+ # or your own domain's cname if you are using virtual hosted buckets. Defaults to <tt>s3.amazonaws.com</tt>.
117
+ # * <tt>:port</tt> - The port to the requests should be made on. Defaults to 80 or 443 if the <tt>:use_ssl</tt>
118
+ # argument is set.
119
+ # * <tt>:use_ssl</tt> - Whether requests should be made over SSL. If set to true, the <tt>:port</tt> argument
120
+ # will be implicitly set to 443, unless specified otherwise. Defaults to false.
121
+ def establish_connection!(options = {})
122
+ # After you've already established the default connection, just specify
123
+ # the difference for subsequent connections
124
+ options = default_connection.options.merge(options) if connected?
125
+ connections[connection_name] = Connection.connect(options)
126
+ end
127
+
128
+ # Returns the connection for the current class, or Base's default connection if the current class does not
129
+ # have its own connection.
130
+ #
131
+ # If not connection has been established yet, NoConnectionEstablished will be raised.
132
+ def connection
133
+ if connected?
134
+ connections[connection_name] || default_connection
135
+ else
136
+ raise NoConnectionEstablished
137
+ end
138
+ end
139
+
140
+ # Returns true if a connection has been made yet.
141
+ def connected?
142
+ !connections.empty?
143
+ end
144
+
145
+ # Removes the connection for the current class. If there is no connection for the current class, the default
146
+ # connection will be removed.
147
+ def disconnect
148
+ connections.delete(connection_name) || connections.delete(default_connection)
149
+ end
150
+
151
+ # Clears *all* connections, from all classes, with prejudice.
152
+ def disconnect!
153
+ connections.clear
154
+ end
155
+
156
+ private
157
+ def connections
158
+ class_variable_get(:@@connections)
159
+ end
160
+
161
+ def connection_name
162
+ name
163
+ end
164
+
165
+ def default_connection_name
166
+ 'AWS::S3::Base'
167
+ end
168
+
169
+ def default_connection
170
+ connections[default_connection_name]
171
+ end
172
+ end
173
+ end
174
+
175
+ class Options < Hash #:nodoc:
176
+ class << self
177
+ def valid_options
178
+ [:access_key_id, :secret_access_key, :server, :port, :use_ssl]
179
+ end
180
+ end
181
+
182
+ attr_reader :options
183
+ def initialize(options = {})
184
+ super()
185
+ @options = options
186
+ validate!
187
+ extract_server!
188
+ extract_port!
189
+ extract_remainder!
190
+ end
191
+
192
+ private
193
+ def extract_server!
194
+ self[:server] = options.delete(:server) || DEFAULT_HOST
195
+ end
196
+
197
+ def extract_port!
198
+ self[:port] = options.delete(:port) || (options[:use_ssl] ? 443 : 80)
199
+ end
200
+
201
+ def extract_remainder!
202
+ update(options)
203
+ end
204
+
205
+ def validate!
206
+ invalid_options = options.keys.select {|key| !self.class.valid_options.include?(key)}
207
+ raise InvalidConnectionOption.new(invalid_options) unless invalid_options.empty?
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end