net-amazon-s3 0.1.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/LICENSE +25 -0
- data/lib/net/amazon/s3.rb +361 -0
- data/lib/net/amazon/s3/bucket.rb +141 -0
- data/lib/net/amazon/s3/errors.rb +47 -0
- data/lib/net/amazon/s3/object.rb +77 -0
- metadata +57 -0
data/LICENSE
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
Copyright (c) 2006 Ryan Grove <ryan@wonko.com>
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
* Redistributions of source code must retain the above copyright notice,
|
8
|
+
this list of conditions and the following disclaimer.
|
9
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
11
|
+
and/or other materials provided with the distribution.
|
12
|
+
* Neither the name of this project nor the names of its contributors may be
|
13
|
+
used to endorse or promote products derived from this software without
|
14
|
+
specific prior written permission.
|
15
|
+
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
17
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
18
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
19
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
20
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
21
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
22
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
23
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
24
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
25
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@@ -0,0 +1,361 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'digest/md5'
|
3
|
+
require 'net/https'
|
4
|
+
require 'net/amazon/s3/bucket'
|
5
|
+
require 'net/amazon/s3/errors'
|
6
|
+
require 'net/amazon/s3/object'
|
7
|
+
require 'openssl'
|
8
|
+
require 'rexml/document'
|
9
|
+
|
10
|
+
module Net; module Amazon
|
11
|
+
|
12
|
+
# = Net::Amazon::S3
|
13
|
+
#
|
14
|
+
# This library implements the Amazon S3 REST API (Rubyfied for your pleasure).
|
15
|
+
# Its usage is hopefully pretty straightforward. See below for examples.
|
16
|
+
#
|
17
|
+
# Author:: Ryan Grove (mailto:ryan@wonko.com)
|
18
|
+
# Version:: 0.1.0
|
19
|
+
# Copyright:: Copyright (c) 2006 Ryan Grove. All rights reserved.
|
20
|
+
# License:: New BSD License (http://opensource.org/licenses/bsd-license.php)
|
21
|
+
# Website:: http://wonko.com/software/net-amazon-s3
|
22
|
+
#
|
23
|
+
# == A Brief Overview of Amazon S3
|
24
|
+
#
|
25
|
+
# Amazon S3 stores arbitrary values (objects) identified by keys and organized
|
26
|
+
# into buckets. An S3 bucket is essentially a glorified Hash. Object values
|
27
|
+
# can be up to 5 GB in size, and objects can also have up to 2 KB of metadata
|
28
|
+
# associated with them.
|
29
|
+
#
|
30
|
+
# Bucket names share a global namespace and must be unique across all of
|
31
|
+
# S3, but object keys only have to be unique within the bucket in which they
|
32
|
+
# are stored.
|
33
|
+
#
|
34
|
+
# For more details, visit http://s3.amazonaws.com
|
35
|
+
#
|
36
|
+
# == Installation
|
37
|
+
#
|
38
|
+
# gem install net-amazon-s3
|
39
|
+
#
|
40
|
+
# == Examples
|
41
|
+
#
|
42
|
+
# === Create an instance of the S3 client
|
43
|
+
#
|
44
|
+
# require 'rubygems'
|
45
|
+
# require 'net/amazon/s3'
|
46
|
+
#
|
47
|
+
# access_key_id = 'DXM37ARQ25519H34E6W2'
|
48
|
+
# secret_access_key = '43HM88c+8kYr/UeFp+shjTnzFgisO5AZzpEO06FU'
|
49
|
+
#
|
50
|
+
# s3 = Net::Amazon::S3.new(access_key_id, secret_access_key)
|
51
|
+
#
|
52
|
+
# === Create a bucket and add an object to it
|
53
|
+
#
|
54
|
+
# foo = s3.create_bucket('foo')
|
55
|
+
# foo['bar'] = 'baz' # create object 'bar' and assign it the
|
56
|
+
# # value 'baz'
|
57
|
+
#
|
58
|
+
# === Upload a large file to the bucket
|
59
|
+
#
|
60
|
+
# File.open('mybigmovie.avi', 'rb') do |file|
|
61
|
+
# foo['mybigmovie.avi'] = file
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# === Download a large file from the bucket
|
65
|
+
#
|
66
|
+
# File.open('mybigmovie.avi', 'wb') do |file|
|
67
|
+
# foo['mybigmovie.avi'].value {|chunk| file.write(chunk) }
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# === Get a hash containing all objects in the bucket
|
71
|
+
#
|
72
|
+
# objects = foo.get_objects
|
73
|
+
#
|
74
|
+
# === Get all objects in the bucket whose keys begin with "my"
|
75
|
+
#
|
76
|
+
# my_objects = foo.get_objects('my')
|
77
|
+
#
|
78
|
+
# === Delete the bucket and everything in it
|
79
|
+
#
|
80
|
+
# s3.delete_bucket('foo', true)
|
81
|
+
#
|
82
|
+
# == TODO
|
83
|
+
#
|
84
|
+
# * Owner support
|
85
|
+
# * Object metadata support
|
86
|
+
# * ACLs
|
87
|
+
# * Logging configuration
|
88
|
+
#
|
89
|
+
class S3
|
90
|
+
include Enumerable
|
91
|
+
|
92
|
+
REST_ENDPOINT = 's3.amazonaws.com'
|
93
|
+
|
94
|
+
attr_accessor :access_key_id, :secret_access_key
|
95
|
+
attr_reader :options
|
96
|
+
|
97
|
+
# Creates and returns a new S3 client. The following options are available:
|
98
|
+
#
|
99
|
+
# [<tt>:enable_cache</tt>] Set to +true+ to enable intelligent caching of
|
100
|
+
# frequently-used requests. This can improve
|
101
|
+
# performance, but may result in staleness if other
|
102
|
+
# clients are simultaneously modifying the buckets
|
103
|
+
# and objects in this S3 account. Default is
|
104
|
+
# +true+.
|
105
|
+
# [<tt>:ssl</tt>] Set to +true+ to use SSL for all requests. No verification
|
106
|
+
# is performed on the server's certificate when SSL is used.
|
107
|
+
# Default is +true+.
|
108
|
+
def initialize(access_key_id, secret_access_key, options = {})
|
109
|
+
@access_key_id = access_key_id
|
110
|
+
@secret_access_key = secret_access_key
|
111
|
+
|
112
|
+
@options = {
|
113
|
+
:enable_cache => true,
|
114
|
+
:ssl => true
|
115
|
+
}
|
116
|
+
|
117
|
+
@options.merge!(options)
|
118
|
+
|
119
|
+
@cache = {}
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns +true+ if a bucket with the specified +bucket_name+ exists in this
|
123
|
+
# S3 account, +false+ otherwise.
|
124
|
+
def bucket_exist?(bucket_name)
|
125
|
+
return get_buckets.has_key?(bucket_name)
|
126
|
+
end
|
127
|
+
|
128
|
+
alias has_bucket? bucket_exist?
|
129
|
+
|
130
|
+
# Creates a new bucket with the specified +bucket_name+ and returns a
|
131
|
+
# Bucket object representing it.
|
132
|
+
def create_bucket(bucket_name)
|
133
|
+
error?(request_put("/#{bucket_name}"))
|
134
|
+
@cache.delete(:buckets)
|
135
|
+
return get_bucket(bucket_name)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Deletes the bucket with the specified +bucket_name+. If +recursive+ is
|
139
|
+
# +true+, all objects contained in the bucket will also be deleted. If
|
140
|
+
# +recursive+ is +false+ and the bucket is not empty, a
|
141
|
+
# S3Error::BucketNotEmpty error will be raised.
|
142
|
+
def delete_bucket(bucket_name, recursive = false)
|
143
|
+
unless bucket = get_bucket(bucket_name)
|
144
|
+
raise S3Error::NoSuchBucket, 'The specified bucket does not exist'
|
145
|
+
end
|
146
|
+
|
147
|
+
if recursive
|
148
|
+
bucket.each {|object| bucket.delete_object(object.name) }
|
149
|
+
end
|
150
|
+
|
151
|
+
@cache.delete(:buckets)
|
152
|
+
|
153
|
+
return true unless error?(request_delete("/#{bucket_name}"))
|
154
|
+
end
|
155
|
+
|
156
|
+
# Iterates through the list of buckets.
|
157
|
+
def each
|
158
|
+
get_buckets.each {|key, value| yield key, value }
|
159
|
+
end
|
160
|
+
|
161
|
+
# Raises the appropriate error if the specified Net::HTTPResponse object
|
162
|
+
# contains an Amazon S3 error; returns +false+ otherwise.
|
163
|
+
def error?(response)
|
164
|
+
return false if response.is_a?(Net::HTTPSuccess)
|
165
|
+
|
166
|
+
xml = REXML::Document.new(response.body)
|
167
|
+
|
168
|
+
unless xml.root.name == 'Error'
|
169
|
+
raise S3Error, "Unknown error: #{response.body}"
|
170
|
+
end
|
171
|
+
|
172
|
+
error_code = xml.root.elements['Code'].text
|
173
|
+
error_message = xml.root.elements['Message'].text
|
174
|
+
|
175
|
+
if S3Error.const_defined?(error_code)
|
176
|
+
raise S3Error.const_get(error_code), error_message
|
177
|
+
else
|
178
|
+
raise S3Error, "#{error_code}: #{error_message}"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Returns a Bucket object representing the specified bucket, or +nil+ if the
|
183
|
+
# bucket doesn't exist.
|
184
|
+
def get_bucket(bucket_name)
|
185
|
+
return nil unless bucket_exist?(bucket_name)
|
186
|
+
return @cache[:buckets][bucket_name]
|
187
|
+
end
|
188
|
+
|
189
|
+
alias [] get_bucket
|
190
|
+
|
191
|
+
# Gets a list of all buckets owned by this account. Returns a Hash of
|
192
|
+
# Bucket objects indexed by bucket name.
|
193
|
+
def get_buckets
|
194
|
+
if @options[:enable_cache] && !@cache[:buckets].nil?
|
195
|
+
return @cache[:buckets]
|
196
|
+
end
|
197
|
+
|
198
|
+
response = request_get('/')
|
199
|
+
error?(response)
|
200
|
+
|
201
|
+
xml = REXML::Document.new(response.body)
|
202
|
+
|
203
|
+
buckets = {}
|
204
|
+
|
205
|
+
xml.root.elements.each('Buckets/Bucket') do |element|
|
206
|
+
bucket_name = element.elements['Name'].text
|
207
|
+
creation_date = Time.parse(element.elements['CreationDate'].text)
|
208
|
+
|
209
|
+
buckets[bucket_name] = Bucket.new(self, bucket_name, creation_date)
|
210
|
+
end
|
211
|
+
|
212
|
+
return @cache[:buckets] = buckets
|
213
|
+
end
|
214
|
+
|
215
|
+
# Sends a properly-signed DELETE request to the specified S3 path and
|
216
|
+
# returns a Net::HTTPResponse object.
|
217
|
+
def request_delete(path, headers = {})
|
218
|
+
http = Net::HTTP.new(REST_ENDPOINT, @options[:ssl] ? 443 : 80)
|
219
|
+
|
220
|
+
http.use_ssl = @options[:ssl]
|
221
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
222
|
+
|
223
|
+
http.start do |http|
|
224
|
+
req = sign_request(Net::HTTP::Delete.new(path), nil, headers)
|
225
|
+
return http.request(req)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Sends a properly-signed GET request to the specified S3 path and returns
|
230
|
+
# a Net::HTTPResponse object.
|
231
|
+
#
|
232
|
+
# When called with a block, yields a Net::HTTPResponse object whose body has
|
233
|
+
# not been read; the caller can process it using
|
234
|
+
# Net::HTTPResponse.read_body.
|
235
|
+
def request_get(path, headers = {})
|
236
|
+
http = Net::HTTP.new(REST_ENDPOINT, @options[:ssl] ? 443 : 80)
|
237
|
+
|
238
|
+
http.use_ssl = @options[:ssl]
|
239
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
240
|
+
|
241
|
+
http.start do |http|
|
242
|
+
req = sign_request(Net::HTTP::Get.new(path), nil, headers)
|
243
|
+
|
244
|
+
if block_given?
|
245
|
+
http.request(req) {|response| yield response }
|
246
|
+
else
|
247
|
+
return http.request(req)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# Sends a properly-signed HEAD request to the specified S3 path and returns
|
253
|
+
# a Net::HTTPResponse object.
|
254
|
+
def request_head(path, headers = {})
|
255
|
+
http = Net::HTTP.new(REST_ENDPOINT, @options[:ssl] ? 443 : 80)
|
256
|
+
|
257
|
+
http.use_ssl = @options[:ssl]
|
258
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
259
|
+
|
260
|
+
http.start do |http|
|
261
|
+
req = sign_request(Net::HTTP::Head.new(path), nil, headers)
|
262
|
+
return http.request(req)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Sends a properly-signed PUT request to the specified S3 path and returns a
|
267
|
+
# Net::HTTPResponse object.
|
268
|
+
#
|
269
|
+
# If +content+ is an open IO stream, the body of the request will be read
|
270
|
+
# from the stream.
|
271
|
+
def request_put(path, content = nil, headers = {})
|
272
|
+
http = Net::HTTP.new(REST_ENDPOINT, @options[:ssl] ? 443 : 80)
|
273
|
+
|
274
|
+
http.use_ssl = @options[:ssl]
|
275
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
276
|
+
|
277
|
+
http.start do |http|
|
278
|
+
req = sign_request(Net::HTTP::Put.new(path), content, headers)
|
279
|
+
|
280
|
+
if content.is_a?(IO)
|
281
|
+
req.body_stream = content
|
282
|
+
else
|
283
|
+
req.body = content
|
284
|
+
end
|
285
|
+
|
286
|
+
response = http.request(req)
|
287
|
+
|
288
|
+
return response
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
private
|
293
|
+
|
294
|
+
# Adds an appropriately-signed +Authorization+ header to the
|
295
|
+
# Net::HTTPRequest +request+.
|
296
|
+
#
|
297
|
+
# If +content+ is an open IO stream, the body of the request will be read
|
298
|
+
# from the stream.
|
299
|
+
def sign_request(request, content = nil, headers = {})
|
300
|
+
unless request.is_a?(Net::HTTPRequest)
|
301
|
+
raise ArgumentError,
|
302
|
+
"Expected Net::HTTPRequest, not #{request.class}"
|
303
|
+
end
|
304
|
+
|
305
|
+
unless request.path =~ /^(\/.*?)(?:\?.*)?$/i
|
306
|
+
raise S3Error, "Invalid request path: #{request.path}"
|
307
|
+
end
|
308
|
+
|
309
|
+
path = $1
|
310
|
+
|
311
|
+
request['Host'] = REST_ENDPOINT
|
312
|
+
request['Date'] = Time.new.httpdate()
|
313
|
+
|
314
|
+
if content.nil?
|
315
|
+
request['Content-Length'] = 0
|
316
|
+
elsif content.is_a?(IO)
|
317
|
+
# Generate an MD5 hash of the stream's contents.
|
318
|
+
md5 = Digest::MD5.new
|
319
|
+
content.rewind
|
320
|
+
|
321
|
+
while buffer = content.read(65536) do
|
322
|
+
md5 << buffer
|
323
|
+
end
|
324
|
+
|
325
|
+
content.rewind
|
326
|
+
|
327
|
+
# Set headers.
|
328
|
+
request['Content-Type'] = 'binary/octet-stream'
|
329
|
+
request['Content-Length'] = content.stat.size
|
330
|
+
request['Content-MD5'] = Base64.encode64(md5.digest).strip
|
331
|
+
else
|
332
|
+
request['Content-Type'] = 'binary/octet-stream'
|
333
|
+
request['Content-Length'] = content.length
|
334
|
+
request['Content-MD5'] = Base64.encode64(Digest::MD5.digest(
|
335
|
+
content)).strip
|
336
|
+
end
|
337
|
+
|
338
|
+
headers.each {|key, value| request[key] = value }
|
339
|
+
|
340
|
+
hmac = OpenSSL::HMAC.new(@secret_access_key, OpenSSL::Digest::SHA1.new)
|
341
|
+
hmac << "#{request.method}\n#{request['Content-MD5']}\n".toutf8 +
|
342
|
+
"#{request['Content-Type']}\n#{request['Date']}\n".toutf8
|
343
|
+
|
344
|
+
request.to_hash.keys.sort.each do |key|
|
345
|
+
if key =~ /^x-amz-/i
|
346
|
+
hmac << "#{key.downcase.strip}:#{request[key].strip}\n".toutf8
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
hmac << path.toutf8
|
351
|
+
|
352
|
+
signature = Base64.encode64(hmac.digest).strip
|
353
|
+
|
354
|
+
request['Authorization'] = "AWS #{@access_key_id}:#{signature}"
|
355
|
+
|
356
|
+
return request
|
357
|
+
end
|
358
|
+
|
359
|
+
end
|
360
|
+
|
361
|
+
end; end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
require 'time'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module Net; module Amazon; class S3
|
6
|
+
|
7
|
+
# Represents an Amazon S3 bucket. This class should only be instantiated
|
8
|
+
# through one of the methods in the S3 class.
|
9
|
+
class Bucket
|
10
|
+
include Comparable
|
11
|
+
include Enumerable
|
12
|
+
|
13
|
+
attr_reader :name, :creation_date
|
14
|
+
|
15
|
+
# Creates and returns a new Bucket object. You should never create new
|
16
|
+
# Bucket objects directly. Instead, use one of the methods in the S3 class.
|
17
|
+
def initialize(s3, bucket_name, creation_date)
|
18
|
+
@s3 = s3
|
19
|
+
@name = bucket_name
|
20
|
+
@creation_date = creation_date
|
21
|
+
|
22
|
+
@cache = {}
|
23
|
+
end
|
24
|
+
|
25
|
+
# Compares two buckets by name.
|
26
|
+
def <=>(bucket)
|
27
|
+
return @name <=> bucket.name
|
28
|
+
end
|
29
|
+
|
30
|
+
# Creates and returns a new S3::Object with the specified +object_key+ and
|
31
|
+
# +value+. If this bucket already contains an object with the specified key,
|
32
|
+
# that object will be overwritten.
|
33
|
+
#
|
34
|
+
# If +value+ is an open IO stream, the value of the object will be read from
|
35
|
+
# the stream.
|
36
|
+
def create_object(object_key, value, metadata = {})
|
37
|
+
object_key_escaped = S3::Object.escape_key(object_key)
|
38
|
+
|
39
|
+
headers = {}
|
40
|
+
metadata.each {|key, value| headers["x-amz-meta-#{key}"] = value }
|
41
|
+
|
42
|
+
response = @s3.request_put("/#{@name}/#{object_key_escaped}", value,
|
43
|
+
headers)
|
44
|
+
@s3.error?(response)
|
45
|
+
|
46
|
+
@cache.delete(:objects)
|
47
|
+
|
48
|
+
return get_object(object_key)
|
49
|
+
end
|
50
|
+
|
51
|
+
alias []= create_object
|
52
|
+
|
53
|
+
# Deletes the specified object from this bucket.
|
54
|
+
def delete_object(object_key)
|
55
|
+
object_key_escaped = S3::Object.escape_key(object_key)
|
56
|
+
|
57
|
+
unless object = get_object(object_key)
|
58
|
+
raise S3Error::NoSuchKey, 'The specified key does not exist'
|
59
|
+
end
|
60
|
+
|
61
|
+
@cache.delete(:objects)
|
62
|
+
|
63
|
+
return true unless @s3.error?(@s3.request_delete(
|
64
|
+
"/#{@name}/#{object_key_escaped}"))
|
65
|
+
end
|
66
|
+
|
67
|
+
# Iterates through the list of objects.
|
68
|
+
def each
|
69
|
+
get_objects.each {|key, value| yield key, value }
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns a S3::Object representing the specified +object_key+, or +nil+ if
|
73
|
+
# the object doesn't exist in this bucket.
|
74
|
+
def get_object(object_key)
|
75
|
+
return get_objects(object_key)[object_key]
|
76
|
+
end
|
77
|
+
|
78
|
+
alias [] get_object
|
79
|
+
|
80
|
+
# Gets a list of all objects in this bucket whose keys begin with +prefix+.
|
81
|
+
# Returns a Hash of S3::Object objects indexed by object key.
|
82
|
+
def get_objects(prefix = '')
|
83
|
+
prefix = prefix.toutf8
|
84
|
+
|
85
|
+
if @s3.options[:enable_cache] && !@cache[:objects].nil? &&
|
86
|
+
!@cache[:objects][prefix].nil?
|
87
|
+
return @cache[:objects][prefix]
|
88
|
+
end
|
89
|
+
|
90
|
+
if @cache[:objects].nil?
|
91
|
+
@cache[:objects] = {}
|
92
|
+
end
|
93
|
+
|
94
|
+
objects = {}
|
95
|
+
request_uri = "/#{@name}?prefix=#{URI.escape(prefix)}"
|
96
|
+
is_truncated = true
|
97
|
+
|
98
|
+
# The request is made in a loop because the S3 API limits results to pages
|
99
|
+
# of 1,000 objects by default, so if there are more than 1,000 objects,
|
100
|
+
# we'll have to send more requests to get them all.
|
101
|
+
while is_truncated do
|
102
|
+
response = @s3.request_get(request_uri)
|
103
|
+
@s3.error?(response)
|
104
|
+
|
105
|
+
xml = REXML::Document.new(response.body)
|
106
|
+
|
107
|
+
if xml.root.elements['IsTruncated'].text == 'false'
|
108
|
+
is_truncated = false
|
109
|
+
else
|
110
|
+
request_uri = "/#{@name}?prefix=#{URI.escape(prefix)}&marker=" +
|
111
|
+
xml.root.elements.to_a('Contents').last.elements['Key'].text
|
112
|
+
end
|
113
|
+
|
114
|
+
next if xml.root.elements['Contents'].nil?
|
115
|
+
|
116
|
+
xml.root.elements.each('Contents') do |element|
|
117
|
+
object_key = element.elements['Key'].text
|
118
|
+
object_size = element.elements['Size'].text
|
119
|
+
object_etag = element.elements['ETag'].text
|
120
|
+
object_last_modified = Time.parse(
|
121
|
+
element.elements['LastModified'].text)
|
122
|
+
|
123
|
+
objects[object_key] = S3::Object.new(@s3, self, object_key,
|
124
|
+
object_size, object_etag, object_last_modified)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
return @cache[:objects][prefix] = objects
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns +true+ if an object with the specified +object_key+ exists in
|
132
|
+
# this bucket, +false+ otherwise.
|
133
|
+
def object_exist?(object_key)
|
134
|
+
return get_objects.has_key?(object_key)
|
135
|
+
end
|
136
|
+
|
137
|
+
alias has_object? object_exist?
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
end; end; end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Net; module Amazon; class S3; class S3Error < StandardError
|
2
|
+
|
3
|
+
class AccessDenied < S3Error; end
|
4
|
+
class AccountProblem < S3Error; end
|
5
|
+
class AllAccessDisabled < S3Error; end
|
6
|
+
class AmbiguousGrantByEmailAddress < S3Error; end
|
7
|
+
class OperationAborted < S3Error; end
|
8
|
+
class BadDigest < S3Error; end
|
9
|
+
class BucketAlreadyExists < S3Error; end
|
10
|
+
class BucketNotEmpty < S3Error; end
|
11
|
+
class CredentialsNotSupported < S3Error; end
|
12
|
+
class EntityTooLarge < S3Error; end
|
13
|
+
class IncompleteBody < S3Error; end
|
14
|
+
class InternalError < S3Error; end
|
15
|
+
class InvalidAccessKeyId < S3Error; end
|
16
|
+
class InvalidAddressingHeader < S3Error; end
|
17
|
+
class InvalidArgument < S3Error; end
|
18
|
+
class InvalidBucketName < S3Error; end
|
19
|
+
class InvalidDigest < S3Error; end
|
20
|
+
class InvalidRange < S3Error; end
|
21
|
+
class InvalidSecurity < S3Error; end
|
22
|
+
class InvalidStorageClass < S3Error; end
|
23
|
+
class InvalidTargetBucketForLogging < S3Error; end
|
24
|
+
class KeyTooLong < S3Error; end
|
25
|
+
class InvalidURI < S3Error; end
|
26
|
+
class MalformedACLError < S3Error; end
|
27
|
+
class MalformedXMLError < S3Error; end
|
28
|
+
class MaxMessageLengthExceeded < S3Error; end
|
29
|
+
class MetadataTooLarge < S3Error; end
|
30
|
+
class MethodNotAllowed < S3Error; end
|
31
|
+
class MissingContentLength < S3Error; end
|
32
|
+
class MissingSecurityHeader < S3Error; end
|
33
|
+
class NoLoggingStatusForKey < S3Error; end
|
34
|
+
class NoSuchBucket < S3Error; end
|
35
|
+
class NoSuchKey < S3Error; end
|
36
|
+
class NotImplemented < S3Error; end
|
37
|
+
class NotSignedUp < S3Error; end
|
38
|
+
class PreconditionFailed < S3Error; end
|
39
|
+
class RequestTimeout < S3Error; end
|
40
|
+
class RequestTimeTooSkewed < S3Error; end
|
41
|
+
class RequestTorrentOfBucketError < S3Error; end
|
42
|
+
class SignatureDoesNotMatch < S3Error; end
|
43
|
+
class TooManyBuckets < S3Error; end
|
44
|
+
class UnexpectedContent < S3Error; end
|
45
|
+
class UnresolvableGrantByEmailAddress < S3Error; end
|
46
|
+
|
47
|
+
end; end; end; end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Net; module Amazon; class S3
|
2
|
+
|
3
|
+
# Represents an Amazon S3 object. This class should only be instantiated
|
4
|
+
# through one of the methods in Bucket.
|
5
|
+
class Object
|
6
|
+
include Comparable
|
7
|
+
|
8
|
+
#--
|
9
|
+
# Public Class Methods
|
10
|
+
#++
|
11
|
+
|
12
|
+
# Escapes an object key for use in an S3 request path. This method should
|
13
|
+
# not be used to escape object keys for use in URL query parameters. Use
|
14
|
+
# URI.escape for that.
|
15
|
+
def self.escape_key(object_key)
|
16
|
+
return object_key.gsub(' ', '+').toutf8
|
17
|
+
end
|
18
|
+
|
19
|
+
#--
|
20
|
+
# Public Instance Methods
|
21
|
+
#++
|
22
|
+
|
23
|
+
attr_reader :name, :size, :etag, :last_modified
|
24
|
+
|
25
|
+
def initialize(s3, bucket, object_key, size, etag, last_modified)
|
26
|
+
@s3 = s3
|
27
|
+
@bucket = bucket
|
28
|
+
@key = object_key.toutf8
|
29
|
+
@size = size
|
30
|
+
@etag = etag
|
31
|
+
@last_modified = last_modified
|
32
|
+
|
33
|
+
@cache = {}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Compares two objects by key.
|
37
|
+
def <=>(object)
|
38
|
+
return @key <=> object.key
|
39
|
+
end
|
40
|
+
|
41
|
+
# Gets this object's value.
|
42
|
+
#
|
43
|
+
# When called with a block, yields the value in chunks as it is read in from
|
44
|
+
# the socket.
|
45
|
+
def value
|
46
|
+
key_escaped = Object.escape_key(@key)
|
47
|
+
|
48
|
+
if block_given?
|
49
|
+
@s3.request_get("/#{@bucket.name}/#{key_escaped}") do |response|
|
50
|
+
@s3.error?(response)
|
51
|
+
response.read_body {|chunk| yield chunk }
|
52
|
+
end
|
53
|
+
else
|
54
|
+
response = @s3.request_get("/#{@bucket.name}/#{key_escaped}")
|
55
|
+
@s3.error?(response)
|
56
|
+
|
57
|
+
return response.body
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Sets this object's value.
|
62
|
+
#
|
63
|
+
# If +new_value+ is an open IO stream, the value will be read from the
|
64
|
+
# stream.
|
65
|
+
def value=(new_value)
|
66
|
+
key_escaped = Object.escape_key(@key)
|
67
|
+
|
68
|
+
response = @s3.request_put("/#{@bucket.name}/" +
|
69
|
+
"#{key_escaped}", new_value)
|
70
|
+
@s3.error?(response)
|
71
|
+
|
72
|
+
return new_value
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end; end; end
|
metadata
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.0
|
3
|
+
specification_version: 1
|
4
|
+
name: net-amazon-s3
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.1.0
|
7
|
+
date: 2006-11-25 00:00:00 -08:00
|
8
|
+
summary: Amazon S3 library.
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: ryan@wonko.com
|
12
|
+
homepage: http://wonko.com/software/net-amazon-s3
|
13
|
+
rubyforge_project: net-amazon-s3
|
14
|
+
description:
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.8.4
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Ryan Grove
|
31
|
+
files:
|
32
|
+
- lib/net
|
33
|
+
- lib/net/amazon
|
34
|
+
- lib/net/amazon/s3
|
35
|
+
- lib/net/amazon/s3.rb
|
36
|
+
- lib/net/amazon/s3/bucket.rb
|
37
|
+
- lib/net/amazon/s3/errors.rb
|
38
|
+
- lib/net/amazon/s3/object.rb
|
39
|
+
- LICENSE
|
40
|
+
test_files: []
|
41
|
+
|
42
|
+
rdoc_options:
|
43
|
+
- --title
|
44
|
+
- Net::Amazon::S3 Documentation
|
45
|
+
- --main
|
46
|
+
- Net::Amazon::S3
|
47
|
+
- --line-numbers
|
48
|
+
extra_rdoc_files: []
|
49
|
+
|
50
|
+
executables: []
|
51
|
+
|
52
|
+
extensions: []
|
53
|
+
|
54
|
+
requirements: []
|
55
|
+
|
56
|
+
dependencies: []
|
57
|
+
|