croaker-aws-s3 0.5.2.20090127001

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. data/COPYING +19 -0
  2. data/INSTALL +55 -0
  3. data/README +545 -0
  4. data/Rakefile +334 -0
  5. data/bin/s3sh +6 -0
  6. data/bin/setup.rb +10 -0
  7. data/lib/aws/s3/acl.rb +636 -0
  8. data/lib/aws/s3/authentication.rb +222 -0
  9. data/lib/aws/s3/base.rb +257 -0
  10. data/lib/aws/s3/bittorrent.rb +58 -0
  11. data/lib/aws/s3/bucket.rb +329 -0
  12. data/lib/aws/s3/connection.rb +349 -0
  13. data/lib/aws/s3/error.rb +69 -0
  14. data/lib/aws/s3/exceptions.rb +133 -0
  15. data/lib/aws/s3/extensions.rb +324 -0
  16. data/lib/aws/s3/logging.rb +311 -0
  17. data/lib/aws/s3/object.rb +613 -0
  18. data/lib/aws/s3/owner.rb +44 -0
  19. data/lib/aws/s3/parsing.rb +99 -0
  20. data/lib/aws/s3/response.rb +180 -0
  21. data/lib/aws/s3/service.rb +51 -0
  22. data/lib/aws/s3/version.rb +12 -0
  23. data/lib/aws/s3.rb +61 -0
  24. data/support/faster-xml-simple/lib/faster_xml_simple.rb +187 -0
  25. data/support/faster-xml-simple/test/regression_test.rb +47 -0
  26. data/support/faster-xml-simple/test/test_helper.rb +17 -0
  27. data/support/faster-xml-simple/test/xml_simple_comparison_test.rb +46 -0
  28. data/support/rdoc/code_info.rb +211 -0
  29. data/test/acl_test.rb +254 -0
  30. data/test/authentication_test.rb +114 -0
  31. data/test/base_test.rb +136 -0
  32. data/test/bucket_test.rb +74 -0
  33. data/test/connection_test.rb +217 -0
  34. data/test/error_test.rb +70 -0
  35. data/test/extensions_test.rb +331 -0
  36. data/test/fixtures/buckets.yml +133 -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/loglines.yml +5 -0
  41. data/test/fixtures/logs.yml +7 -0
  42. data/test/fixtures/policies.yml +16 -0
  43. data/test/fixtures.rb +89 -0
  44. data/test/logging_test.rb +89 -0
  45. data/test/mocks/fake_response.rb +26 -0
  46. data/test/object_test.rb +205 -0
  47. data/test/parsing_test.rb +66 -0
  48. data/test/remote/acl_test.rb +117 -0
  49. data/test/remote/bittorrent_test.rb +45 -0
  50. data/test/remote/bucket_test.rb +146 -0
  51. data/test/remote/logging_test.rb +82 -0
  52. data/test/remote/object_test.rb +371 -0
  53. data/test/remote/test_helper.rb +30 -0
  54. data/test/response_test.rb +68 -0
  55. data/test/service_test.rb +23 -0
  56. data/test/test_helper.rb +111 -0
  57. metadata +143 -0
@@ -0,0 +1,329 @@
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(file, open(file), 'jukebox')
39
+ #
40
+ # You'll see your file has been added to it:
41
+ #
42
+ # music_bucket.objects
43
+ # # => [#<AWS::S3::S3Object '/jukebox/black-flowers.mp3'>]
44
+ #
45
+ # You can treat your bucket like a hash and access objects by name:
46
+ #
47
+ # jukebox['black-flowers.mp3']
48
+ # # => #<AWS::S3::S3Object '/jukebox/black-flowers.mp3'>
49
+ #
50
+ # In the event that you want to delete a bucket, you can use Bucket.delete.
51
+ #
52
+ # Bucket.delete('jukebox')
53
+ #
54
+ # Keep in mind, like unix directories, you can not delete a bucket unless it is empty. Trying to delete a bucket
55
+ # that contains objects will raise a BucketNotEmpty exception.
56
+ #
57
+ # Passing the :force => true option to delete will take care of deleting all the bucket's objects for you.
58
+ #
59
+ # Bucket.delete('photos', :force => true)
60
+ # # => true
61
+ class Bucket < Base
62
+ class << self
63
+ # Creates a bucket named <tt>name</tt>.
64
+ #
65
+ # Bucket.create('jukebox')
66
+ #
67
+ # Your bucket name must be unique across all of S3. If the name
68
+ # you request has already been taken, you will get a 409 Conflict response, and a BucketAlreadyExists exception
69
+ # will be raised.
70
+ #
71
+ # To create a bucket in a different location use option[:location]. The only available location is "eu" at the moment. If the
72
+ # option is ommitted the default location will be used.
73
+ #
74
+ # By default new buckets have their access level set to private. You can override this using the <tt>:access</tt> option.
75
+ #
76
+ # Bucket.create('internet_drop_box', :access => :public_read_write)
77
+ #
78
+ # The full list of access levels that you can set on Bucket and S3Object creation are listed in the README[link:files/README.html]
79
+ # in the section called 'Setting access levels'.
80
+ def create(name, options = {})
81
+ validate_name!(name)
82
+
83
+ # At the moment the only possible option is to create a bucket in the eu. if nothing is specified the default location will be used
84
+ if options[:location] && options[:location] == "eu"
85
+ body = "<CreateBucketConfiguration>
86
+ <LocationConstraint>EU</LocationConstraint>
87
+ </CreateBucketConfiguration>"
88
+ end
89
+ put("/#{name}", options, body).success?
90
+ end
91
+
92
+ # Fetches the bucket named <tt>name</tt>.
93
+ #
94
+ # Bucket.find('jukebox')
95
+ #
96
+ # If a default bucket is inferable from the current connection's subdomain, or if set explicitly with Base.set_current_bucket,
97
+ # it will be used if no bucket is specified.
98
+ #
99
+ # MusicBucket.current_bucket
100
+ # => 'jukebox'
101
+ # MusicBucket.find.name
102
+ # => 'jukebox'
103
+ #
104
+ # By default all objects contained in the bucket will be returned (sans their data) along with the bucket.
105
+ # You can access your objects using the Bucket#objects method.
106
+ #
107
+ # Bucket.find('jukebox').objects
108
+ #
109
+ # There are several options which allow you to limit which objects are retrieved. The list of object filtering options
110
+ # are listed in the documentation for Bucket.objects.
111
+ def find(name = nil, options = {})
112
+ new(get(path(name, options)).bucket)
113
+ end
114
+
115
+ # Return just the objects in the bucket named <tt>name</tt>.
116
+ #
117
+ # By default all objects of the named bucket will be returned. There are options, though, for filtering
118
+ # which objects are returned.
119
+ #
120
+ # === Object filtering options
121
+ #
122
+ # * <tt>:max_keys</tt> - The maximum number of keys you'd like to see in the response body.
123
+ # The server may return fewer than this many keys, but will not return more.
124
+ #
125
+ # Bucket.objects('jukebox').size
126
+ # # => 3
127
+ # Bucket.objects('jukebox', :max_keys => 1).size
128
+ # # => 1
129
+ #
130
+ # * <tt>:prefix</tt> - Restricts the response to only contain results that begin with the specified prefix.
131
+ #
132
+ # Bucket.objects('jukebox')
133
+ # # => [<AWS::S3::S3Object '/jazz/miles.mp3'>, <AWS::S3::S3Object '/jazz/dolphy.mp3'>, <AWS::S3::S3Object '/classical/malher.mp3'>]
134
+ # Bucket.objects('jukebox', :prefix => 'classical')
135
+ # # => [<AWS::S3::S3Object '/classical/malher.mp3'>]
136
+ #
137
+ # * <tt>:marker</tt> - Marker specifies where in the result set to resume listing. It restricts the response
138
+ # to only contain results that occur alphabetically _after_ the value of marker. To retrieve the next set of results,
139
+ # use the last key from the current page of results as the marker in your next request.
140
+ #
141
+ # # Skip 'mahler'
142
+ # Bucket.objects('jukebox', :marker => 'mb')
143
+ # # => [<AWS::S3::S3Object '/jazz/miles.mp3'>]
144
+ #
145
+ # === Examples
146
+ #
147
+ # # Return no more than 2 objects whose key's are listed alphabetically after the letter 'm'.
148
+ # Bucket.objects('jukebox', :marker => 'm', :max_keys => 2)
149
+ # # => [<AWS::S3::S3Object '/jazz/miles.mp3'>, <AWS::S3::S3Object '/classical/malher.mp3'>]
150
+ #
151
+ # # Return no more than 2 objects whose key's are listed alphabetically after the letter 'm' and have the 'jazz' prefix.
152
+ # Bucket.objects('jukebox', :marker => 'm', :max_keys => 2, :prefix => 'jazz')
153
+ # # => [<AWS::S3::S3Object '/jazz/miles.mp3'>]
154
+ def objects(name = nil, options = {})
155
+ find(name, options).object_cache
156
+ end
157
+
158
+ # Deletes the bucket named <tt>name</tt>.
159
+ #
160
+ # All objects in the bucket must be deleted before the bucket can be deleted. If the bucket is not empty,
161
+ # BucketNotEmpty will be raised.
162
+ #
163
+ # You can side step this issue by passing the :force => true option to delete which will take care of
164
+ # emptying the bucket before deleting it.
165
+ #
166
+ # Bucket.delete('photos', :force => true)
167
+ #
168
+ # Only the owner of a bucket can delete a bucket, regardless of the bucket's access control policy.
169
+ def delete(name = nil, options = {})
170
+ find(name).delete_all if options[:force]
171
+
172
+ name = path(name)
173
+ Base.delete(name).success?
174
+ end
175
+
176
+ # List all your buckets. This is a convenient wrapper around AWS::S3::Service.buckets.
177
+ def list(reload = false)
178
+ Service.buckets(reload)
179
+ end
180
+
181
+ private
182
+ def validate_name!(name)
183
+ raise InvalidBucketName.new(name) unless name =~ /^[-\w.]{3,255}$/
184
+ end
185
+
186
+ def path(name, options = {})
187
+ if name.is_a?(Hash)
188
+ options = name
189
+ name = nil
190
+ end
191
+ "/#{bucket_name(name)}#{RequestOptions.process(options).to_query_string}"
192
+ end
193
+ end
194
+
195
+ attr_reader :object_cache #:nodoc:
196
+
197
+ include Enumerable
198
+
199
+ def initialize(attributes = {}) #:nodoc:
200
+ super
201
+ @object_cache = []
202
+ build_contents!
203
+ end
204
+
205
+ # Fetches the object named <tt>object_key</tt>, or nil if the bucket does not contain an object with the
206
+ # specified key.
207
+ #
208
+ # bucket.objects
209
+ # => [#<AWS::S3::S3Object '/marcel_molina/beluga_baby.jpg'>,
210
+ # #<AWS::S3::S3Object '/marcel_molina/tongue_overload.jpg'>]
211
+ # bucket['beluga_baby.jpg']
212
+ # => #<AWS::S3::S3Object '/marcel_molina/beluga_baby.jpg'>
213
+ def [](object_key)
214
+ detect {|file| file.key == object_key.to_s}
215
+ end
216
+
217
+ # Initializes a new S3Object belonging to the current bucket.
218
+ #
219
+ # object = bucket.new_object
220
+ # object.value = data
221
+ # object.key = 'classical/mahler.mp3'
222
+ # object.store
223
+ # bucket.objects.include?(object)
224
+ # => true
225
+ def new_object(attributes = {})
226
+ object = S3Object.new(attributes)
227
+ register(object)
228
+ object
229
+ end
230
+
231
+ # List S3Object's of the bucket.
232
+ #
233
+ # Once fetched the objects will be cached. You can reload the objects by passing <tt>:reload</tt>.
234
+ #
235
+ # bucket.objects(:reload)
236
+ #
237
+ # You can also filter the objects using the same options listed in Bucket.objects.
238
+ #
239
+ # bucket.objects(:prefix => 'jazz')
240
+ #
241
+ # Using these filtering options will implictly reload the objects.
242
+ #
243
+ # To reclaim all the objects for the bucket you can pass in :reload again.
244
+ def objects(options = {})
245
+ if options.is_a?(Hash)
246
+ reload = !options.empty?
247
+ else
248
+ reload = options
249
+ options = {}
250
+ end
251
+
252
+ reload!(options) if reload || object_cache.empty?
253
+ object_cache
254
+ end
255
+
256
+ # Iterates over the objects in the bucket.
257
+ #
258
+ # bucket.each do |object|
259
+ # # Do something with the object ...
260
+ # end
261
+ def each(&block)
262
+ # Dup the collection since we might be destructively modifying the object_cache during the iteration.
263
+ objects.dup.each(&block)
264
+ end
265
+
266
+ # Returns true if there are no objects in the bucket.
267
+ def empty?
268
+ objects.empty?
269
+ end
270
+
271
+ # Returns the number of objects in the bucket.
272
+ def size
273
+ objects.size
274
+ end
275
+
276
+ # Deletes the bucket. See its class method counter part Bucket.delete for caveats about bucket deletion and how to ensure
277
+ # a bucket is deleted no matter what.
278
+ def delete(options = {})
279
+ self.class.delete(name, options)
280
+ end
281
+
282
+ # Delete all files in the bucket. Use with caution. Can not be undone.
283
+ def delete_all
284
+ each do |object|
285
+ object.delete
286
+ end
287
+ self
288
+ end
289
+ alias_method :clear, :delete_all
290
+
291
+ # Buckets observe their objects and have this method called when one of their objects
292
+ # is either stored or deleted.
293
+ def update(action, object) #:nodoc:
294
+ case action
295
+ when :stored then add object unless objects.include?(object)
296
+ when :deleted then object_cache.delete(object)
297
+ end
298
+ end
299
+
300
+ private
301
+ def build_contents!
302
+ return unless has_contents?
303
+ attributes.delete('contents').each do |content|
304
+ add new_object(content)
305
+ end
306
+ end
307
+
308
+ def has_contents?
309
+ attributes.has_key?('contents')
310
+ end
311
+
312
+ def add(object)
313
+ register(object)
314
+ object_cache << object
315
+ end
316
+
317
+ def register(object)
318
+ object.bucket = self
319
+ end
320
+
321
+ def reload!(options = {})
322
+ object_cache.clear
323
+ self.class.objects(name, options).each do |object|
324
+ add object
325
+ end
326
+ end
327
+ end
328
+ end
329
+ end
@@ -0,0 +1,349 @@
1
+ module AWS
2
+ module S3
3
+ class Connection #:nodoc:
4
+ class << self
5
+ def connect(options = {})
6
+ new(options)
7
+ end
8
+
9
+ # Added the ability to remove the bucket from the path.
10
+ def prepare_path(path)
11
+ path = path.remove_extended unless path.utf8?
12
+ URI.escape(path)
13
+ end
14
+ end
15
+
16
+ attr_reader :access_key_id, :secret_access_key, :http, :options
17
+
18
+ # Creates a new connection. Connections make the actual requests to S3, though these requests are usually
19
+ # called from subclasses of Base.
20
+ #
21
+ # For details on establishing connections, check the Connection::Management::ClassMethods.
22
+ def initialize(options = {})
23
+ @options = Options.new(options)
24
+ connect
25
+ end
26
+
27
+ def request(verb, path, headers = {}, body = nil, attempts = 0, &block)
28
+ body.rewind if body.respond_to?(:rewind) unless attempts.zero?
29
+
30
+ requester = Proc.new do
31
+ path = self.class.prepare_path(path) if attempts.zero? # Only escape the path once
32
+ request = request_method(verb).new(path, headers)
33
+ ensure_content_type!(request)
34
+ add_user_agent!(request)
35
+
36
+ authenticate!(request, { :bucket_address => self.subdomain })
37
+ if body
38
+ if body.respond_to?(:read)
39
+ request.body_stream = body
40
+ else
41
+ request.body = body
42
+ end
43
+ request.content_length = body.respond_to?(:lstat) ? body.stat.size : body.size
44
+ else
45
+ request.content_length = 0
46
+ end
47
+
48
+ http.request(request, &block)
49
+ end
50
+
51
+ if persistent?
52
+ http.start unless http.started?
53
+ requester.call
54
+ else
55
+ http.start(&requester)
56
+ end
57
+ rescue Errno::EPIPE, Timeout::Error, Errno::EINVAL, EOFError
58
+ @http = create_connection
59
+ attempts == 3 ? raise : (attempts += 1; retry)
60
+ end
61
+
62
+ def url_for(path, options = {})
63
+ authenticate = options.delete(:authenticated)
64
+ # Default to true unless explicitly false
65
+ authenticate = true if authenticate.nil?
66
+ if self.is_using_default_host?
67
+ bucket_name = AWS::S3.remove_or_extract_bucket_from_path(path, true)
68
+ address = "#{bucket_name}.#{DEFAULT_HOST}"
69
+ path = self.class.prepare_path(AWS::S3.remove_or_extract_bucket_from_path(path))
70
+ else
71
+ address = http.address
72
+ path = self.class.prepare_path(path)
73
+ end
74
+ request = request_method(:get).new(path, {})
75
+ query_string = query_string_authentication(request, options.merge({ :bucket_address => bucket_name }))
76
+ returning "#{protocol(options)}#{address}#{port_string}#{path}" do |url|
77
+ url << "?#{query_string}" if authenticate
78
+ end
79
+ end
80
+
81
+ def subdomain
82
+ http.address[/^([^.]+).#{DEFAULT_HOST}$/, 1]
83
+ end
84
+
85
+ def persistent?
86
+ options[:persistent]
87
+ end
88
+
89
+ def protocol(options = {})
90
+ # This always trumps http.use_ssl?
91
+ if options[:use_ssl] == false
92
+ 'http://'
93
+ elsif options[:use_ssl] || http.use_ssl?
94
+ 'https://'
95
+ else
96
+ 'http://'
97
+ end
98
+ end
99
+
100
+ #
101
+ def is_for_bucket?(bucket)
102
+ bucket == subdomain
103
+ end
104
+
105
+ def has_subdomain?
106
+ !subdomain.nil?
107
+ end
108
+
109
+ def is_using_default_host?
110
+ http.address == DEFAULT_HOST || self.has_subdomain?
111
+ end
112
+
113
+ private
114
+
115
+ def extract_keys!
116
+ missing_keys = []
117
+ extract_key = Proc.new {|key| options[key] || (missing_keys.push(key); nil)}
118
+ @access_key_id = extract_key[:access_key_id]
119
+ @secret_access_key = extract_key[:secret_access_key]
120
+
121
+ raise MissingAccessKey.new(missing_keys) unless missing_keys.empty?
122
+ end
123
+
124
+ def create_connection
125
+ http = http_class.new(options[:server], options[:port])
126
+ http.use_ssl = !options[:use_ssl].nil? || options[:port] == 443
127
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
128
+ http
129
+ end
130
+
131
+ def http_class
132
+ if options.connecting_through_proxy?
133
+ Net::HTTP::Proxy(*options.proxy_settings)
134
+ else
135
+ Net::HTTP
136
+ end
137
+ end
138
+
139
+ def connect
140
+ extract_keys!
141
+ @http = create_connection
142
+ end
143
+
144
+ def port_string
145
+ default_port = options[:use_ssl] ? 443 : 80
146
+ http.port == default_port ? '' : ":#{http.port}"
147
+ end
148
+
149
+ def ensure_content_type!(request)
150
+ request['Content-Type'] ||= 'binary/octet-stream'
151
+ end
152
+
153
+ # Just do Header authentication for now
154
+ def authenticate!(request, options={})
155
+ request['Authorization'] = Authentication::Header.new(request, access_key_id, secret_access_key, options)
156
+ end
157
+
158
+ def add_user_agent!(request)
159
+ request['User-Agent'] ||= "AWS::S3/#{Version}"
160
+ end
161
+
162
+ def query_string_authentication(request, options = {})
163
+ Authentication::QueryString.new(request, access_key_id, secret_access_key, options)
164
+ end
165
+
166
+ def request_method(verb)
167
+ Net::HTTP.const_get(verb.to_s.capitalize)
168
+ end
169
+
170
+ def method_missing(method, *args, &block)
171
+ options[method] || super
172
+ end
173
+
174
+ module Management #:nodoc:
175
+ def self.included(base)
176
+ base.cattr_accessor :connections
177
+ base.connections = {}
178
+ base.extend ClassMethods
179
+ end
180
+
181
+ # Manage the creation and destruction of connections for AWS::S3::Base and its subclasses. Connections are
182
+ # created with establish_connection!.
183
+ module ClassMethods
184
+ # Creates a new connection with which to make requests to the S3 servers for the calling class.
185
+ #
186
+ # AWS::S3::Base.establish_connection!(:access_key_id => '...', :secret_access_key => '...')
187
+ #
188
+ # You can set connections for every subclass of AWS::S3::Base. Once the initial connection is made on
189
+ # Base, all subsequent connections will inherit whatever values you don't specify explictly. This allows you to
190
+ # customize details of the connection, such as what server the requests are made to, by just specifying one
191
+ # option.
192
+ #
193
+ # AWS::S3::Bucket.established_connection!(:use_ssl => true)
194
+ #
195
+ # The Bucket connection would inherit the <tt>:access_key_id</tt> and the <tt>:secret_access_key</tt> from
196
+ # Base's connection. Unlike the Base connection, all Bucket requests would be made over SSL.
197
+ #
198
+ # == Required arguments
199
+ #
200
+ # * <tt>:access_key_id</tt> - The access key id for your S3 account. Provided by Amazon.
201
+ # * <tt>:secret_access_key</tt> - The secret access key for your S3 account. Provided by Amazon.
202
+ #
203
+ # If any of these required arguments is missing, a MissingAccessKey exception will be raised.
204
+ #
205
+ # == Optional arguments
206
+ #
207
+ # * <tt>:server</tt> - The server to make requests to. You can use this to specify your bucket in the subdomain,
208
+ # or your own domain's cname if you are using virtual hosted buckets. Defaults to <tt>s3.amazonaws.com</tt>.
209
+ # * <tt>:port</tt> - The port to the requests should be made on. Defaults to 80 or 443 if the <tt>:use_ssl</tt>
210
+ # argument is set.
211
+ # * <tt>:use_ssl</tt> - Whether requests should be made over SSL. If set to true, the <tt>:port</tt> argument
212
+ # will be implicitly set to 443, unless specified otherwise. Defaults to false.
213
+ # * <tt>:persistent</tt> - Whether to use a persistent connection to the server. Having this on provides around a two fold
214
+ # performance increase but for long running processes some firewalls may find the long lived connection suspicious and close the connection.
215
+ # If you run into connection errors, try setting <tt>:persistent</tt> to false. Defaults to false.
216
+ # * <tt>:proxy</tt> - If you need to connect through a proxy, you can specify your proxy settings by specifying a <tt>:host</tt>, <tt>:port</tt>, <tt>:user</tt>, and <tt>:password</tt>
217
+ # with the <tt>:proxy</tt> option.
218
+ # The <tt>:host</tt> setting is required if specifying a <tt>:proxy</tt>.
219
+ #
220
+ # AWS::S3::Bucket.established_connection!(:proxy => {
221
+ # :host => '...', :port => 8080, :user => 'marcel', :password => 'secret'
222
+ # })
223
+ def establish_connection!(options = {})
224
+ # After you've already established the default connection, just specify
225
+ # the difference for subsequent connections
226
+ options = default_connection.options.merge(options) if connected?
227
+ connections[connection_name] = Connection.connect(options)
228
+ end
229
+
230
+ # Returns the connection for the current class, or Base's default connection if the current class does not
231
+ # have its own connection.
232
+ #
233
+ # If not connection has been established yet, NoConnectionEstablished will be raised.
234
+ def connection
235
+ if connected?
236
+ connections[connection_name] || default_connection
237
+ else
238
+ raise NoConnectionEstablished
239
+ end
240
+ end
241
+
242
+ # Returns true if a connection has been made yet.
243
+ def connected?
244
+ !connections.empty?
245
+ end
246
+
247
+ # Removes the connection for the current class. If there is no connection for the current class, the default
248
+ # connection will be removed.
249
+ def disconnect(name = connection_name)
250
+ name = default_connection unless connections.has_key?(name)
251
+ connection = connections[name]
252
+ connection.http.finish if connection.persistent?
253
+ connections.delete(name)
254
+ end
255
+
256
+ # Clears *all* connections, from all classes, with prejudice.
257
+ def disconnect!
258
+ connections.each_key {|connection| disconnect(connection)}
259
+ end
260
+
261
+ private
262
+ def connection_name
263
+ name
264
+ end
265
+
266
+ def default_connection_name
267
+ 'AWS::S3::Base'
268
+ end
269
+
270
+ def default_connection
271
+ connections[default_connection_name]
272
+ end
273
+ end
274
+ end
275
+
276
+ class Options < Hash #:nodoc:
277
+ class << self
278
+ def valid_options
279
+ [:access_key_id, :secret_access_key, :server, :port, :use_ssl, :persistent, :proxy]
280
+ end
281
+ end
282
+
283
+ attr_reader :options
284
+ def initialize(options = {})
285
+ super()
286
+ @options = options
287
+ validate!
288
+ extract_proxy_settings!
289
+ extract_persistent!
290
+ extract_server!
291
+ extract_port!
292
+ extract_remainder!
293
+ end
294
+
295
+ def connecting_through_proxy?
296
+ !self[:proxy].nil?
297
+ end
298
+
299
+ def proxy_settings
300
+ proxy_setting_keys.map do |proxy_key|
301
+ self[:proxy][proxy_key]
302
+ end
303
+ end
304
+
305
+ private
306
+
307
+ def proxy_setting_keys
308
+ [:host, :port, :user, :password]
309
+ end
310
+
311
+ def missing_proxy_settings?
312
+ !self[:proxy].keys.include?(:host)
313
+ end
314
+
315
+ def extract_persistent!
316
+ self[:persistent] = options.has_key?(:persitent) ? options[:persitent] : false
317
+ end
318
+
319
+ def extract_proxy_settings!
320
+ self[:proxy] = options.delete(:proxy) if options.include?(:proxy)
321
+ validate_proxy_settings!
322
+ end
323
+
324
+ def extract_server!
325
+ self[:server] = options.delete(:server) || DEFAULT_HOST
326
+ end
327
+
328
+ def extract_port!
329
+ self[:port] = options.delete(:port) || (options[:use_ssl] ? 443 : 80)
330
+ end
331
+
332
+ def extract_remainder!
333
+ update(options)
334
+ end
335
+
336
+ def validate!
337
+ invalid_options = options.keys.select {|key| !self.class.valid_options.include?(key)}
338
+ raise InvalidConnectionOption.new(invalid_options) unless invalid_options.empty?
339
+ end
340
+
341
+ def validate_proxy_settings!
342
+ if connecting_through_proxy? && missing_proxy_settings?
343
+ raise ArgumentError, "Missing proxy settings. Must specify at least :host."
344
+ end
345
+ end
346
+ end
347
+ end
348
+ end
349
+ end