aws-s3 0.2.1 → 0.3.0
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.
- 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
|