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 +4 -0
- data/README +28 -27
- data/Rakefile +33 -4
- data/lib/aws/s3.rb +1 -0
- data/lib/aws/s3/acl.rb +8 -3
- data/lib/aws/s3/base.rb +10 -8
- data/lib/aws/s3/bucket.rb +1 -5
- data/lib/aws/s3/connection.rb +48 -9
- data/lib/aws/s3/exceptions.rb +3 -0
- data/lib/aws/s3/extensions.rb +99 -9
- data/lib/aws/s3/logging.rb +152 -9
- data/lib/aws/s3/object.rb +30 -22
- data/lib/aws/s3/parsing.rb +0 -29
- data/lib/aws/s3/response.rb +2 -2
- data/lib/aws/s3/version.rb +2 -2
- data/support/faster-xml-simple/lib/faster_xml_simple.rb +30 -11
- data/support/faster-xml-simple/test/regression_test.rb +11 -5
- data/support/faster-xml-simple/test/test_helper.rb +17 -0
- data/support/faster-xml-simple/test/xml_simple_comparison_test.rb +2 -3
- data/test/acl_test.rb +13 -2
- data/test/connection_test.rb +8 -0
- data/test/extensions_test.rb +57 -4
- data/test/fixtures/loglines.yml +5 -0
- data/test/fixtures/logs.yml +7 -0
- data/test/logging_test.rb +54 -1
- data/test/object_test.rb +15 -0
- data/test/parsing_test.rb +0 -20
- data/test/remote/object_test.rb +49 -1
- metadata +5 -2
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
|
-
|
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
|
-
|
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('
|
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 = '
|
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>
|
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::
|
484
|
-
#<AWS::S3::
|
485
|
-
#<AWS::S3::
|
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
|
-
|
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
|
-
|
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
|
-
|
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)
|
data/lib/aws/s3.rb
CHANGED
data/lib/aws/s3/acl.rb
CHANGED
@@ -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
|
-
"
|
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
|
-
"
|
448
|
+
"#<%s:0x%s %s>" [self.class, object_id, type_representation || '(type not set yet)']
|
444
449
|
end
|
445
450
|
|
446
451
|
private
|
data/lib/aws/s3/base.rb
CHANGED
@@ -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,
|
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
|
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 = '
|
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
|
data/lib/aws/s3/bucket.rb
CHANGED
@@ -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
|
#
|
data/lib/aws/s3/connection.rb
CHANGED
@@ -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
|
-
|
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 =
|
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
|
64
|
-
@http.use_ssl
|
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
|
-
|
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.
|
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
|