aws-s3 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/INSTALL CHANGED
@@ -30,6 +30,10 @@ If you go the svn route, be sure that you have all the dependencies installed. T
30
30
 
31
31
  == Dependencies
32
32
 
33
+ AWS::S3 requires Ruby 1.8.4 or greater.
34
+
35
+ It also has the following dependencies:
36
+
33
37
  sudo gem i xml-simple -ry
34
38
  sudo gem i builder -ry
35
39
  sudo gem i mime-types -ry
data/README CHANGED
@@ -93,11 +93,7 @@ be found in the documentation for Bucket.find.
93
93
  To add an object to a bucket you specify the name of the object, its value, and the bucket to put it in.
94
94
 
95
95
  file = 'black-flowers.mp3'
96
- S3Object.store(
97
- file,
98
- File.open(file),
99
- 'jukebox'
100
- )
96
+ S3Object.store(file, open(file), 'jukebox')
101
97
 
102
98
  You'll see your file has been added to it:
103
99
 
@@ -129,11 +125,7 @@ bucket.
129
125
 
130
126
  You can store an object on S3 by specifying a key, its data and the name of the bucket you want to put it in:
131
127
 
132
- S3Object.store(
133
- 'headshot.jpg',
134
- File.open('headshot.jpg'),
135
- 'photos'
136
- )
128
+ S3Object.store('me.jpg', open('headshot.jpg'), 'photos')
137
129
 
138
130
  The content type of the object will be inferred by its extension. If the appropriate content type can not be inferred, S3 defaults
139
131
  to <tt>binary/octect-stream</tt>.
@@ -143,7 +135,7 @@ If you want to override this, you can explicitly indicate what content type the
143
135
  file = 'black-flowers.m4a'
144
136
  S3Object.store(
145
137
  file,
146
- File.open(file),
138
+ open(file),
147
139
  'jukebox',
148
140
  :content_type => 'audio/mp4a-latm'
149
141
  )
@@ -165,7 +157,7 @@ You can fetch just the object's data directly:
165
157
 
166
158
  Or stream it by passing a block to <tt>stream</tt>:
167
159
 
168
- File.open('song.mp3', 'w') do |file|
160
+ open('song.mp3', 'w') do |file|
169
161
  S3Object.stream('song.mp3', 'jukebox') do |chunk|
170
162
  file.write chunk
171
163
  end
@@ -179,6 +171,9 @@ tell the object to reload its <tt>value</tt>:
179
171
 
180
172
  Other functionality includes:
181
173
 
174
+ # Check if an object exists?
175
+ S3Object.exists? 'headshot.jpg', 'photos'
176
+
182
177
  # Copying an object
183
178
  S3Object.copy 'headshot.jpg', 'headshot2.jpg', 'photos'
184
179
 
@@ -247,14 +242,10 @@ If <tt>data</tt> is an I/O stream it will be read in segments and written to the
247
242
  may be desirable for very large files so they are not read into memory all at once.
248
243
 
249
244
  # Non streamed upload
250
- S3Object.store('simple-text-file.txt',
251
- 'hello world!',
252
- 'marcel')
245
+ S3Object.store('greeting.txt', 'hello world!', 'marcel')
253
246
 
254
247
  # Streamed upload
255
- S3Object.store('roots.mpeg',
256
- File.open('roots.mpeg'),
257
- 'marcel')
248
+ S3Object.store('roots.mpeg', open('roots.mpeg'), 'marcel')
258
249
 
259
250
 
260
251
  == Setting the current bucket
@@ -269,11 +260,8 @@ a subclass of Bucket or S3Object and telling it what bucket to use:
269
260
 
270
261
  For all methods that take a bucket name as an argument, the current bucket will be used if the bucket name argument is omitted.
271
262
 
272
- other_song = '/Users/marcel/baby-please-come-home.mp3'
273
- JukeBoxSong.store(
274
- File.basename(other_song),
275
- File.open(other_song)
276
- )
263
+ other_song = 'baby-please-come-home.mp3'
264
+ JukeBoxSong.store(other_song, open(other_song))
277
265
 
278
266
  This time we didn't have to explicitly pass in the bucket name, as the JukeBoxSong class knows that it will
279
267
  always use the 'jukebox' bucket.
@@ -477,12 +465,25 @@ Unless you specify otherwise, logs will be written to the bucket you want to log
477
465
 
478
466
  Now instead of logging right into the jukebox bucket, the logs will go into the bucket called jukebox-logs.
479
467
 
480
- Once logs have accumulated, you can access them using the <tt>log</tt> method:
468
+ Once logs have accumulated, you can access them using the <tt>logs</tt> method:
481
469
 
482
470
  pp Bucket.logs('jukebox')
483
- [#<AWS::S3::S3Object '/jukebox-logs/log-2006-11-14-07-15-24-2061C35880A310A1'>,
484
- #<AWS::S3::S3Object '/jukebox-logs/log-2006-11-14-08-15-27-D8EEF536EC09E6B3'>,
485
- #<AWS::S3::S3Object '/jukebox-logs/log-2006-11-14-08-15-29-355812B2B15BD789'>]
471
+ [#<AWS::S3::Logging::Log '/jukebox-logs/log-2006-11-14-07-15-24-2061C35880A310A1'>,
472
+ #<AWS::S3::Logging::Log '/jukebox-logs/log-2006-11-14-08-15-27-D8EEF536EC09E6B3'>,
473
+ #<AWS::S3::Logging::Log '/jukebox-logs/log-2006-11-14-08-15-29-355812B2B15BD789'>]
474
+
475
+ Each log has a <tt>lines</tt> method that gives you information about each request in that log. All the fields are available
476
+ as named methods. More information is available in Logging::Log::Line.
477
+
478
+ logs = Bucket.logs('jukebox')
479
+ log = logs.first
480
+ line = log.lines.first
481
+ line.operation
482
+ # => 'REST.GET.LOGGING_STATUS'
483
+ line.request_uri
484
+ # => 'GET /jukebox?logging HTTP/1.1'
485
+ line.remote_ip
486
+ # => "67.165.183.125"
486
487
 
487
488
  Disabling logging is just as simple as enabling it:
488
489
 
data/Rakefile CHANGED
@@ -46,7 +46,7 @@ namespace :doc do
46
46
  strip_comments[info.comment]
47
47
  end
48
48
 
49
- File.open('README', 'w') do |file|
49
+ open('README', 'w') do |file|
50
50
  file.write ERB.new(IO.read('README.erb')).result(binding)
51
51
  end
52
52
  end
@@ -120,7 +120,7 @@ namespace :dist do
120
120
  changelog = IO.read('CHANGELOG')
121
121
  changelog.sub!(/^trunk:/, "#{spec.version}:")
122
122
 
123
- File.open('CHANGELOG', 'w') do |file|
123
+ open('CHANGELOG', 'w') do |file|
124
124
  file.write "trunk:\n\n#{changelog}"
125
125
  end
126
126
  end
@@ -187,6 +187,19 @@ task :stats do
187
187
  end
188
188
 
189
189
  namespace :test do
190
+ find_file = lambda do |name|
191
+ file_name = lambda {|path| File.join(path, "#{name}.rb")}
192
+ root = $:.detect do |path|
193
+ File.exist?(file_name[path])
194
+ end
195
+ file_name[root] if root
196
+ end
197
+
198
+ TEST_LOADER = find_file['rake/rake_test_loader']
199
+ multiruby = lambda do |glob|
200
+ system 'multiruby', TEST_LOADER, *Dir.glob(glob)
201
+ end
202
+
190
203
  desc 'Check test coverage'
191
204
  task :coverage do
192
205
  system("rcov --sort coverage #{File.join(library_root, 'test/*_test.rb')}")
@@ -209,6 +222,23 @@ namespace :test do
209
222
  show_test_coverage_results
210
223
  end
211
224
 
225
+ desc 'Run local tests against multiple versions of Ruby'
226
+ task :version_audit do
227
+ multiruby['test/*_test.rb']
228
+ end
229
+
230
+ namespace :version_audit do
231
+ desc 'Run remote tests against multiple versions of Ruby'
232
+ task :remote do
233
+ multiruby['test/remote/*_test.rb']
234
+ end
235
+
236
+ desc 'Run all tests against multiple versions of Ruby'
237
+ task :all do
238
+ multiruby['test/**/*_test.rb']
239
+ end
240
+ end
241
+
212
242
  def show_test_coverage_results
213
243
  system("open #{File.join(library_root, 'coverage/index.html')}") if PLATFORM['darwin']
214
244
  end
@@ -260,7 +290,6 @@ namespace :site do
260
290
  require 'rdoc/markup/simple_markup/to_html'
261
291
 
262
292
  readme = lambda { IO.read('README')[/^== Getting started\n(.*)/m, 1] }
263
- ruby_code = /^\s*\n(\s+\S+.*?)\n\s*\n/m
264
293
 
265
294
  readme_to_html = lambda do
266
295
  handler = SM::ToHtml.new
@@ -277,7 +306,7 @@ namespace :site do
277
306
 
278
307
  desc 'Regenerate the public website page'
279
308
  task :build => 'doc:readme' do
280
- File.open('site/public/index.html', 'w') do |file|
309
+ open('site/public/index.html', 'w') do |file|
281
310
  erb_data = {}
282
311
  erb_data[:readme] = readme_to_html.call
283
312
  file.write ERB.new(IO.read('site/index.erb')).result(binding)
@@ -6,6 +6,7 @@ require 'digest/sha1'
6
6
  require 'net/https'
7
7
  require 'time'
8
8
  require 'date'
9
+ require 'open-uri'
9
10
 
10
11
  $:.unshift(File.dirname(__FILE__))
11
12
  require 's3/extensions'
@@ -131,7 +131,7 @@ module AWS
131
131
  def to_xml
132
132
  Builder.new(owner, grants).to_s
133
133
  end
134
-
134
+
135
135
  private
136
136
 
137
137
  def owner?
@@ -170,6 +170,11 @@ module AWS
170
170
  super
171
171
  end
172
172
  end
173
+
174
+ # Two grant lists are equal if they have identical grants both in terms of permission and grantee.
175
+ def ==(grants)
176
+ size == grants.size && all? {|grant| grants.include?(grant)}
177
+ end
173
178
  end
174
179
 
175
180
  class Builder < XmlGenerator #:nodoc:
@@ -313,7 +318,7 @@ module AWS
313
318
  end
314
319
 
315
320
  def inspect #:nodoc:
316
- "#<#{self.class}:##{object_id} #{self}>"
321
+ "#<%s:0x%s %s>" % [self.class, object_id, self]
317
322
  end
318
323
 
319
324
  def to_s #:nodoc:
@@ -440,7 +445,7 @@ module AWS
440
445
  end
441
446
 
442
447
  def inspect #:nodoc:
443
- "#<#{self.class}:##{object_id} #{type_representation || '(xsi not set yet)'}>"
448
+ "#<%s:0x%s %s>" [self.class, object_id, type_representation || '(type not set yet)']
444
449
  end
445
450
 
446
451
  private
@@ -66,7 +66,7 @@ module AWS #:nodoc:
66
66
  def request(verb, path, options = {}, body = nil, attempts = 0, &block)
67
67
  Service.response = nil
68
68
  process_options!(options, verb)
69
- response = response_class.new(connection.request(verb, URI.escape(path), options, body, &block))
69
+ response = response_class.new(connection.request(verb, path, options, body, &block))
70
70
  Service.response = response
71
71
 
72
72
  Error::Response.new(response.response).error.raise if response.error?
@@ -74,7 +74,8 @@ module AWS #:nodoc:
74
74
  # Once in a while, a request to S3 returns an internal error. A glitch in the matrix I presume. Since these
75
75
  # errors are few and far between the request method will rescue InternalErrors the first three times they encouter them
76
76
  # and will retry the request again. Most of the time the second attempt will work.
77
- rescue InternalError, Timeout::Error
77
+ rescue *retry_exceptions
78
+ body.rewind if body.respond_to?(:rewind)
78
79
  attempts == 3 ? raise : (attempts += 1; retry)
79
80
  end
80
81
 
@@ -111,11 +112,8 @@ module AWS #:nodoc:
111
112
  #
112
113
  # 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
  #
114
- # other_song = '/Users/marcel/baby-please-come-home.mp3'
115
- # JukeBoxSong.store(
116
- # File.basename(other_song),
117
- # File.open(other_song)
118
- # )
115
+ # other_song = 'baby-please-come-home.mp3'
116
+ # JukeBoxSong.store(other_song, open(other_song))
119
117
  #
120
118
  # This time we didn't have to explicitly pass in the bucket name, as the JukeBoxSong class knows that it will
121
119
  # always use the 'jukebox' bucket.
@@ -176,6 +174,10 @@ module AWS #:nodoc:
176
174
  def bucket_name(name)
177
175
  name || current_bucket
178
176
  end
177
+
178
+ def retry_exceptions
179
+ [InternalError, Timeout::Error, Errno::EPIPE, Errno::EINVAL, RequestTimeout]
180
+ end
179
181
 
180
182
  class RequestOptions < Hash #:nodoc:
181
183
  attr_reader :options, :verb
@@ -228,4 +230,4 @@ module AWS #:nodoc:
228
230
  end
229
231
  end
230
232
  end
231
- end
233
+ end
@@ -35,11 +35,7 @@ module AWS
35
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
36
  #
37
37
  # file = 'black-flowers.mp3'
38
- # S3Object.store(
39
- # file,
40
- # File.open(file),
41
- # 'jukebox'
42
- # )
38
+ # S3Object.store(file, open(file), 'jukebox')
43
39
  #
44
40
  # You'll see your file has been added to it:
45
41
  #
@@ -5,6 +5,11 @@ module AWS
5
5
  def connect(options = {})
6
6
  new(options)
7
7
  end
8
+
9
+ def prepare_path(path)
10
+ path = path.remove_extended unless path.utf8?
11
+ URI.escape(path)
12
+ end
8
13
  end
9
14
 
10
15
  attr_reader :access_key_id, :secret_access_key, :http, :options
@@ -19,8 +24,11 @@ module AWS
19
24
  end
20
25
 
21
26
  def request(verb, path, headers = {}, body = nil, &block)
22
- http.start do
27
+ requester = Proc.new do
28
+ path = self.class.prepare_path(path)
23
29
  request = request_method(verb).new(path, headers)
30
+ ensure_content_type!(request)
31
+ add_user_agent!(request)
24
32
  authenticate!(request)
25
33
  if body
26
34
  if body.respond_to?(:read)
@@ -32,10 +40,17 @@ module AWS
32
40
  end
33
41
  http.request(request, &block)
34
42
  end
43
+
44
+ if persistent?
45
+ http.start unless http.started?
46
+ requester.call
47
+ else
48
+ http.start(&requester)
49
+ end
35
50
  end
36
51
 
37
52
  def url_for(path, options = {})
38
- path = URI.escape(path)
53
+ path = self.class.prepare_path(path)
39
54
  request = request_method(:get).new(path, {})
40
55
  query_string = query_string_authentication(request, options)
41
56
  "#{protocol(options)}#{http.address}#{path}?#{query_string}"
@@ -45,11 +60,15 @@ module AWS
45
60
  http.address[/^([^.]+).#{DEFAULT_HOST}$/, 1]
46
61
  end
47
62
 
63
+ def persistent?
64
+ options[:persistent]
65
+ end
66
+
48
67
  def protocol(options = {})
49
68
  (options[:use_ssl] || http.use_ssl?) ? 'https://' : 'http://'
50
69
  end
51
70
 
52
- private
71
+ private
53
72
  def extract_keys!
54
73
  missing_keys = []
55
74
  extract_key = Proc.new {|key| options[key] || (missing_keys.push(key); nil)}
@@ -60,16 +79,25 @@ module AWS
60
79
 
61
80
  def connect
62
81
  extract_keys!
63
- @http = Net::HTTP.new(options[:server], options[:port])
64
- @http.use_ssl = !options[:use_ssl].nil? || options[:port] == 443
82
+ @http = Net::HTTP.new(options[:server], options[:port])
83
+ @http.use_ssl = !options[:use_ssl].nil? || options[:port] == 443
84
+ @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
65
85
  @http
66
86
  end
67
87
 
88
+ def ensure_content_type!(request)
89
+ request['Content-Type'] ||= 'binary/octet-stream'
90
+ end
91
+
68
92
  # Just do Header authentication for now
69
93
  def authenticate!(request)
70
94
  request['Authorization'] = Authentication::Header.new(request, access_key_id, secret_access_key)
71
95
  end
72
96
 
97
+ def add_user_agent!(request)
98
+ request['User-Agent'] ||= "AWS::S3/#{Version}"
99
+ end
100
+
73
101
  def query_string_authentication(request, options = {})
74
102
  Authentication::QueryString.new(request, access_key_id, secret_access_key, options)
75
103
  end
@@ -121,6 +149,9 @@ module AWS
121
149
  # argument is set.
122
150
  # * <tt>:use_ssl</tt> - Whether requests should be made over SSL. If set to true, the <tt>:port</tt> argument
123
151
  # will be implicitly set to 443, unless specified otherwise. Defaults to false.
152
+ # * <tt>:persistent</tt> - Whether to use a persistent connection to the server. Having this on provides around a two fold
153
+ # performance increase but for long running processes some firewalls may find the long lived connection suspicious and close the connection.
154
+ # If you run into connection errors, try setting <tt>:persistent</tt> to false. Defaults to true.
124
155
  def establish_connection!(options = {})
125
156
  # After you've already established the default connection, just specify
126
157
  # the difference for subsequent connections
@@ -147,13 +178,16 @@ module AWS
147
178
 
148
179
  # Removes the connection for the current class. If there is no connection for the current class, the default
149
180
  # connection will be removed.
150
- def disconnect
151
- connections.delete(connection_name) || connections.delete(default_connection)
181
+ def disconnect(name = connection_name)
182
+ name = default_connection unless connections.has_key?(name)
183
+ connection = connections[name]
184
+ connection.http.finish if connection.persistent?
185
+ connections.delete(name)
152
186
  end
153
187
 
154
188
  # Clears *all* connections, from all classes, with prejudice.
155
189
  def disconnect!
156
- connections.clear
190
+ connections.each_key {|connection| disconnect(connection)}
157
191
  end
158
192
 
159
193
  private
@@ -174,7 +208,7 @@ module AWS
174
208
  class Options < Hash #:nodoc:
175
209
  class << self
176
210
  def valid_options
177
- [:access_key_id, :secret_access_key, :server, :port, :use_ssl]
211
+ [:access_key_id, :secret_access_key, :server, :port, :use_ssl, :persistent]
178
212
  end
179
213
  end
180
214
 
@@ -183,12 +217,17 @@ module AWS
183
217
  super()
184
218
  @options = options
185
219
  validate!
220
+ extract_persistent!
186
221
  extract_server!
187
222
  extract_port!
188
223
  extract_remainder!
189
224
  end
190
225
 
191
226
  private
227
+ def extract_persistent!
228
+ self[:persistent] = options.has_key?(:persitent) ? options[:persitent] : true
229
+ end
230
+
192
231
  def extract_server!
193
232
  self[:server] = options.delete(:server) || DEFAULT_HOST
194
233
  end