s3-sync 1.2.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,39 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ module S3sync
5
+ VERSION = '1.3.0'
6
+
7
+ require 's3sync/s3try'
8
+ require 's3sync/s3config'
9
+
10
+ # after other mods, so we don't overwrite yaml vals with defaults
11
+ include S3Config
12
+
13
+
14
+ def S3sync.s3cmdList(bucket, path, max=nil, delim=nil, marker=nil, headers={})
15
+ debug(max)
16
+ options = Hash.new
17
+ options['prefix'] = path # start at the right depth
18
+ options['max-keys'] = max ? max.to_s : 100
19
+ options['delimiter'] = delim if delim
20
+ options['marker'] = marker if marker
21
+ S3try(:list_bucket, bucket, options, headers)
22
+ end
23
+
24
+ # turn an array into a hash of pairs
25
+ def S3sync.hashPairs(ar)
26
+ ret = Hash.new
27
+ ar.each do |item|
28
+ name = (/^(.*?):/.match(item))[1]
29
+ item = (/^.*?:(.*)$/.match(item))[1]
30
+ ret[name] = item
31
+ end if ar
32
+ ret
33
+ end
34
+ end
35
+
36
+
37
+ def debug(str)
38
+ $stderr.puts str if $S3syncOptions['--debug']
39
+ end
@@ -0,0 +1,107 @@
1
+ # This software code is made available "AS IS" without warranties of any
2
+ # kind. You may copy, display, modify and redistribute the software
3
+ # code either by itself or as incorporated into your code; provided that
4
+ # you do not remove any proprietary notices. Your use of this software
5
+ # code is at your own risk and you waive any claim against the author
6
+ # with respect to your use of this software code.
7
+ # (c) 2007 s3sync.net
8
+ #
9
+
10
+ # The purpose of this file is to overlay the net/http library
11
+ # to add some functionality
12
+ # (without changing the file itself or requiring a specific version)
13
+ # It still isn't perfectly robust, i.e. if radical changes are made
14
+ # to the underlying lib this stuff will need updating.
15
+
16
+ require 'net/http'
17
+
18
+ module Net
19
+
20
+ $HTTPStreamingDebug = false
21
+
22
+ # Allow request body to be an IO stream
23
+ # Allow an IO stream argument to stream the response body out
24
+ class HTTP
25
+ alias _HTTPStreaming_request request
26
+
27
+ def request(req, body = nil, streamResponseBodyTo = nil, &block)
28
+ if not block_given? and streamResponseBodyTo and streamResponseBodyTo.respond_to?(:write)
29
+ $stderr.puts "Response using streaming" if $HTTPStreamingDebug
30
+ # this might be a retry, we should make sure the stream is at its beginning
31
+ streamResponseBodyTo.rewind if streamResponseBodyTo.respond_to?(:rewind) and streamResponseBodyTo != $stdout
32
+ block = proc do |res|
33
+ res.read_body do |chunk|
34
+ streamResponseBodyTo.write(chunk)
35
+ end
36
+ end
37
+ end
38
+ if body != nil && body.respond_to?(:read)
39
+ $stderr.puts "Request using streaming" if $HTTPStreamingDebug
40
+ # this might be a retry, we should make sure the stream is at its beginning
41
+ body.rewind if body.respond_to?(:rewind)
42
+ req.body_stream = body
43
+ return _HTTPStreaming_request(req, nil, &block)
44
+ else
45
+ return _HTTPStreaming_request(req, body, &block)
46
+ end
47
+ end
48
+ end
49
+
50
+ end #module
51
+
52
+ module S3sync
53
+ class ProgressStream < SimpleDelegator
54
+ def initialize(s, size=0)
55
+ @start = @last = Time.new
56
+ @total = size
57
+ @transferred = 0
58
+ @closed = false
59
+ @printed = false
60
+ @innerStream = s
61
+ super(@innerStream)
62
+ __setobj__(@innerStream)
63
+ end
64
+ # need to catch reads and writes so we can count what's being transferred
65
+ def read(i)
66
+ res = @innerStream.read(i)
67
+ @transferred += res.respond_to?(:length) ? res.length : 0
68
+ now = Time.new
69
+ if(now - @last > 1) # don't do this oftener than once per second
70
+ @printed = true
71
+ begin
72
+ $stdout.printf("\rProgress: %db %db/s %s ", @transferred, (@transferred/(now - @start)).floor,
73
+ @total > 0? (100 * @transferred/@total).floor.to_s + "%" : ""
74
+ )
75
+ rescue FloatDomainError
76
+ #wtf?
77
+ end
78
+ $stdout.flush
79
+ @last = now
80
+ end
81
+ res
82
+ end
83
+ def write(s)
84
+ @transferred += s.length
85
+ res = @innerStream.write(s)
86
+ now = Time.new
87
+ if(now -@last > 1) # don't do this oftener than once per second
88
+ @printed = true
89
+ $stdout.printf("\rProgress: %db %db/s %s ", @transferred, (@transferred/(now - @start)).floor,
90
+ @total > 0? (100 * @transferred/@total).floor.to_s + "%" : ""
91
+ )
92
+ $stdout.flush
93
+ @last = now
94
+ end
95
+ res
96
+ end
97
+ def rewind()
98
+ @transferred = 0
99
+ @innerStream.rewind if @innerStream.respond_to?(:rewind)
100
+ end
101
+ def close()
102
+ $stdout.printf("\n") if @printed and not @closed
103
+ @closed = true
104
+ @innerStream.close
105
+ end
106
+ end
107
+ end #module
@@ -0,0 +1,714 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This software code is made available "AS IS" without warranties of any
4
+ # kind. You may copy, display, modify and redistribute the software
5
+ # code either by itself or as incorporated into your code; provided that
6
+ # you do not remove any proprietary notices. Your use of this software
7
+ # code is at your own risk and you waive any claim against Amazon
8
+ # Digital Services, Inc. or its affiliates with respect to your use of
9
+ # this software code. (c) 2006 Amazon Digital Services, Inc. or its
10
+ # affiliates.
11
+
12
+ require 'base64'
13
+ require 'cgi'
14
+ require 'openssl'
15
+ require 'digest/sha1'
16
+ require 'net/https'
17
+ require 'rexml/document'
18
+ require 'time'
19
+
20
+ # this wasn't added until v 1.8.3
21
+ if (RUBY_VERSION < '1.8.3')
22
+ class Net::HTTP::Delete < Net::HTTPRequest
23
+ METHOD = 'DELETE'
24
+ REQUEST_HAS_BODY = false
25
+ RESPONSE_HAS_BODY = true
26
+ end
27
+ end
28
+
29
+ # this module has two big classes: AWSAuthConnection and
30
+ # QueryStringAuthGenerator. both use identical apis, but the first actually
31
+ # performs the operation, while the second simply outputs urls with the
32
+ # appropriate authentication query string parameters, which could be used
33
+ # in another tool (such as your web browser for GETs).
34
+ module S3
35
+ DEFAULT_HOST = 's3.amazonaws.com'
36
+ PORTS_BY_SECURITY = { true => 443, false => 80 }
37
+ METADATA_PREFIX = 'x-amz-meta-'
38
+ AMAZON_HEADER_PREFIX = 'x-amz-'
39
+
40
+ # builds the canonical string for signing.
41
+ def S3.canonical_string(method, bucket="", path="", path_args={}, headers={}, expires=nil)
42
+ interesting_headers = {}
43
+ headers.each do |key, value|
44
+ lk = key.downcase
45
+ if (lk == 'content-md5' or
46
+ lk == 'content-type' or
47
+ lk == 'date' or
48
+ lk =~ /^#{AMAZON_HEADER_PREFIX}/o)
49
+ interesting_headers[lk] = value.to_s.strip
50
+ end
51
+ end
52
+
53
+ # these fields get empty strings if they don't exist.
54
+ interesting_headers['content-type'] ||= ''
55
+ interesting_headers['content-md5'] ||= ''
56
+
57
+ # just in case someone used this. it's not necessary in this lib.
58
+ if interesting_headers.has_key? 'x-amz-date'
59
+ interesting_headers['date'] = ''
60
+ end
61
+
62
+ # if you're using expires for query string auth, then it trumps date
63
+ # (and x-amz-date)
64
+ if not expires.nil?
65
+ interesting_headers['date'] = expires
66
+ end
67
+
68
+ buf = "#{method}\n"
69
+ interesting_headers.sort { |a, b| a[0] <=> b[0] }.each do |key, value|
70
+ if key =~ /^#{AMAZON_HEADER_PREFIX}/o
71
+ buf << "#{key}:#{value}\n"
72
+ else
73
+ buf << "#{value}\n"
74
+ end
75
+ end
76
+
77
+ # build the path using the bucket and key
78
+ if not bucket.empty?
79
+ buf << "/#{bucket}"
80
+ end
81
+ # append the key (it might be empty string)
82
+ # append a slash regardless
83
+ buf << "/#{path}"
84
+
85
+ # if there is an acl, logging, or torrent parameter
86
+ # add them to the string
87
+ if path_args.has_key?('acl')
88
+ buf << '?acl'
89
+ elsif path_args.has_key?('torrent')
90
+ buf << '?torrent'
91
+ elsif path_args.has_key?('location')
92
+ buf << '?location'
93
+ elsif path_args.has_key?('logging')
94
+ buf << '?logging'
95
+ end
96
+
97
+ return buf
98
+ end
99
+
100
+ # encodes the given string with the aws_secret_access_key, by taking the
101
+ # hmac-sha1 sum, and then base64 encoding it. optionally, it will also
102
+ # url encode the result of that to protect the string if it's going to
103
+ # be used as a query string parameter.
104
+ def S3.encode(aws_secret_access_key, str, urlencode=false)
105
+ digest = OpenSSL::Digest::Digest.new('sha1')
106
+ b64_hmac =
107
+ Base64.encode64(
108
+ OpenSSL::HMAC.digest(digest, aws_secret_access_key, str)).strip
109
+
110
+ if urlencode
111
+ return CGI::escape(b64_hmac)
112
+ else
113
+ return b64_hmac
114
+ end
115
+ end
116
+
117
+ # build the path_argument string
118
+ def S3.path_args_hash_to_string(path_args={})
119
+ arg_string = ''
120
+ path_args.each { |k, v|
121
+ arg_string << k
122
+ if not v.nil?
123
+ arg_string << "=#{CGI::escape(v)}"
124
+ end
125
+ arg_string << '&'
126
+ }
127
+ return arg_string
128
+ end
129
+
130
+
131
+ # uses Net::HTTP to interface with S3. note that this interface should only
132
+ # be used for smaller objects, as it does not stream the data. if you were
133
+ # to download a 1gb file, it would require 1gb of memory. also, this class
134
+ # creates a new http connection each time. it would be greatly improved with
135
+ # some connection pooling.
136
+ class AWSAuthConnection
137
+ attr_accessor :calling_format
138
+
139
+ def initialize(aws_access_key_id, aws_secret_access_key, is_secure=true,
140
+ server=DEFAULT_HOST, port=PORTS_BY_SECURITY[is_secure],
141
+ calling_format=CallingFormat::REGULAR)
142
+ @aws_access_key_id = aws_access_key_id
143
+ @aws_secret_access_key = aws_secret_access_key
144
+ @server = server
145
+ @is_secure = is_secure
146
+ @calling_format = calling_format
147
+ @port = port
148
+ end
149
+
150
+ def create_bucket(bucket, headers={})
151
+ return Response.new(make_request('PUT', bucket, '', {}, headers))
152
+ end
153
+
154
+ # takes options :prefix, :marker, :max_keys, and :delimiter
155
+ def list_bucket(bucket, options={}, headers={})
156
+ path_args = {}
157
+ options.each { |k, v|
158
+ path_args[k] = v.to_s
159
+ }
160
+
161
+ return ListBucketResponse.new(make_request('GET', bucket, '', path_args, headers))
162
+ end
163
+
164
+ def delete_bucket(bucket, headers={})
165
+ return Response.new(make_request('DELETE', bucket, '', {}, headers))
166
+ end
167
+
168
+ def put(bucket, key, object=nil, headers={})
169
+ if object == nil
170
+ req = make_request('PUT', bucket, CGI::escape(key), {}, headers)
171
+ else
172
+ if not object.instance_of? S3Object
173
+ object = S3Object.new(object)
174
+ end
175
+ req = make_request('PUT', bucket, CGI::escape(key), {}, headers, object.data, object.metadata)
176
+ end
177
+
178
+ return Response.new(req)
179
+ end
180
+
181
+ def get(bucket, key, headers={})
182
+ return GetResponse.new(make_request('GET', bucket, CGI::escape(key), {}, headers))
183
+ end
184
+
185
+ def delete(bucket, key, headers={})
186
+ return Response.new(make_request('DELETE', bucket, CGI::escape(key), {}, headers))
187
+ end
188
+
189
+ def head(bucket, key, headers={})
190
+ return GetResponse.new(make_request('HEAD', bucket, CGI::escape(key), {}, headers))
191
+ end
192
+
193
+ def get_bucket_logging(bucket, headers={})
194
+ return GetResponse.new(make_request('GET', bucket, '', {'logging' => nil}, headers))
195
+ end
196
+
197
+ def put_bucket_logging(bucket, logging_xml_doc, headers={})
198
+ return Response.new(make_request('PUT', bucket, '', {'logging' => nil}, headers, logging_xml_doc))
199
+ end
200
+
201
+ def get_bucket_acl(bucket, headers={})
202
+ return get_acl(bucket, '', headers)
203
+ end
204
+
205
+ # returns an xml document representing the access control list.
206
+ # this could be parsed into an object.
207
+ def get_acl(bucket, key, headers={})
208
+ return GetResponse.new(make_request('GET', bucket, CGI::escape(key), {'acl' => nil}, headers))
209
+ end
210
+
211
+ def put_bucket_acl(bucket, acl_xml_doc, headers={})
212
+ return put_acl(bucket, '', acl_xml_doc, headers)
213
+ end
214
+
215
+ # sets the access control policy for the given resource. acl_xml_doc must
216
+ # be a string in the acl xml format.
217
+ def put_acl(bucket, key, acl_xml_doc, headers={})
218
+ return Response.new(
219
+ make_request('PUT', bucket, CGI::escape(key), {'acl' => nil}, headers, acl_xml_doc, {})
220
+ )
221
+ end
222
+
223
+ def list_all_my_buckets(headers={})
224
+ return ListAllMyBucketsResponse.new(make_request('GET', '', '', {}, headers))
225
+ end
226
+
227
+ private
228
+ def make_request(method, bucket='', key='', path_args={}, headers={}, data='', metadata={})
229
+
230
+ # build the domain based on the calling format
231
+ server = ''
232
+ if bucket.empty?
233
+ # for a bucketless request (i.e. list all buckets)
234
+ # revert to regular domain case since this operation
235
+ # does not make sense for vanity domains
236
+ server = @server
237
+ elsif @calling_format == CallingFormat::SUBDOMAIN
238
+ server = "#{bucket}.#{@server}"
239
+ elsif @calling_format == CallingFormat::VANITY
240
+ server = bucket
241
+ else
242
+ server = @server
243
+ end
244
+
245
+ # build the path based on the calling format
246
+ path = ''
247
+ if (not bucket.empty?) and (@calling_format == CallingFormat::REGULAR)
248
+ path << "/#{bucket}"
249
+ end
250
+ # add the slash after the bucket regardless
251
+ # the key will be appended if it is non-empty
252
+ path << "/#{key}"
253
+
254
+ # build the path_argument string
255
+ # add the ? in all cases since
256
+ # signature and credentials follow path args
257
+ path << '?'
258
+ path << S3.path_args_hash_to_string(path_args)
259
+
260
+ http = Net::HTTP.new(server, @port)
261
+ http.use_ssl = @is_secure
262
+ http.start do
263
+ req = method_to_request_class(method).new("#{path}")
264
+
265
+ set_headers(req, headers)
266
+ set_headers(req, metadata, METADATA_PREFIX)
267
+
268
+ set_aws_auth_header(req, @aws_access_key_id, @aws_secret_access_key, bucket, key, path_args)
269
+ if req.request_body_permitted?
270
+ return http.request(req, data)
271
+ else
272
+ return http.request(req)
273
+ end
274
+ end
275
+ end
276
+
277
+ def method_to_request_class(method)
278
+ case method
279
+ when 'GET'
280
+ return Net::HTTP::Get
281
+ when 'PUT'
282
+ return Net::HTTP::Put
283
+ when 'DELETE'
284
+ return Net::HTTP::Delete
285
+ when 'HEAD'
286
+ return Net::HTTP::Head
287
+ else
288
+ raise "Unsupported method #{method}"
289
+ end
290
+ end
291
+
292
+ # set the Authorization header using AWS signed header authentication
293
+ def set_aws_auth_header(request, aws_access_key_id, aws_secret_access_key, bucket='', key='', path_args={})
294
+ # we want to fix the date here if it's not already been done.
295
+ request['Date'] ||= Time.now.httpdate
296
+
297
+ # ruby will automatically add a random content-type on some verbs, so
298
+ # here we add a dummy one to 'supress' it. change this logic if having
299
+ # an empty content-type header becomes semantically meaningful for any
300
+ # other verb.
301
+ request['Content-Type'] ||= ''
302
+
303
+ canonical_string =
304
+ S3.canonical_string(request.method, bucket, key, path_args, request.to_hash, nil)
305
+ encoded_canonical = S3.encode(aws_secret_access_key, canonical_string)
306
+
307
+ request['Authorization'] = "AWS #{aws_access_key_id}:#{encoded_canonical}"
308
+ end
309
+
310
+ def set_headers(request, headers, prefix='')
311
+ headers.each do |key, value|
312
+ request[prefix + key] = value
313
+ end
314
+ end
315
+ end
316
+
317
+
318
+ # This interface mirrors the AWSAuthConnection class above, but instead
319
+ # of performing the operations, this class simply returns a url that can
320
+ # be used to perform the operation with the query string authentication
321
+ # parameters set.
322
+ class QueryStringAuthGenerator
323
+ attr_accessor :calling_format
324
+ attr_accessor :expires
325
+ attr_accessor :expires_in
326
+ attr_reader :server
327
+ attr_reader :port
328
+
329
+ # by default, expire in 1 minute
330
+ DEFAULT_EXPIRES_IN = 60
331
+
332
+ def initialize(aws_access_key_id, aws_secret_access_key, is_secure=true,
333
+ server=DEFAULT_HOST, port=PORTS_BY_SECURITY[is_secure],
334
+ format=CallingFormat::REGULAR)
335
+ @aws_access_key_id = aws_access_key_id
336
+ @aws_secret_access_key = aws_secret_access_key
337
+ @protocol = is_secure ? 'https' : 'http'
338
+ @server = server
339
+ @port = port
340
+ @calling_format = format
341
+ # by default expire
342
+ @expires_in = DEFAULT_EXPIRES_IN
343
+ end
344
+
345
+ # set the expires value to be a fixed time. the argument can
346
+ # be either a Time object or else seconds since epoch.
347
+ def expires=(value)
348
+ @expires = value
349
+ @expires_in = nil
350
+ end
351
+
352
+ # set the expires value to expire at some point in the future
353
+ # relative to when the url is generated. value is in seconds.
354
+ def expires_in=(value)
355
+ @expires_in = value
356
+ @expires = nil
357
+ end
358
+
359
+ def create_bucket(bucket, headers={})
360
+ return generate_url('PUT', bucket, '', {}, headers)
361
+ end
362
+
363
+ # takes options :prefix, :marker, :max_keys, and :delimiter
364
+ def list_bucket(bucket, options={}, headers={})
365
+ path_args = {}
366
+ options.each { |k, v|
367
+ path_args[k] = v.to_s
368
+ }
369
+ return generate_url('GET', bucket, '', path_args, headers)
370
+ end
371
+
372
+ def delete_bucket(bucket, headers={})
373
+ return generate_url('DELETE', bucket, '', {}, headers)
374
+ end
375
+
376
+ # don't really care what object data is. it's just for conformance with the
377
+ # other interface. If this doesn't work, check tcpdump to see if the client is
378
+ # putting a Content-Type header on the wire.
379
+ def put(bucket, key, object=nil, headers={})
380
+ object = S3Object.new(object) if not object.instance_of? S3Object
381
+ return generate_url('PUT', bucket, CGI::escape(key), {}, merge_meta(headers, object))
382
+ end
383
+
384
+ def get(bucket, key, headers={})
385
+ return generate_url('GET', bucket, CGI::escape(key), {}, headers)
386
+ end
387
+
388
+ def delete(bucket, key, headers={})
389
+ return generate_url('DELETE', bucket, CGI::escape(key), {}, headers)
390
+ end
391
+
392
+ def get_bucket_logging(bucket, headers={})
393
+ return generate_url('GET', bucket, '', {'logging' => nil}, headers)
394
+ end
395
+
396
+ def put_bucket_logging(bucket, logging_xml_doc, headers={})
397
+ return generate_url('PUT', bucket, '', {'logging' => nil}, headers)
398
+ end
399
+
400
+ def get_acl(bucket, key='', headers={})
401
+ return generate_url('GET', bucket, CGI::escape(key), {'acl' => nil}, headers)
402
+ end
403
+
404
+ def get_bucket_acl(bucket, headers={})
405
+ return get_acl(bucket, '', headers)
406
+ end
407
+
408
+ # don't really care what acl_xml_doc is.
409
+ # again, check the wire for Content-Type if this fails.
410
+ def put_acl(bucket, key, acl_xml_doc, headers={})
411
+ return generate_url('PUT', bucket, CGI::escape(key), {'acl' => nil}, headers)
412
+ end
413
+
414
+ def put_bucket_acl(bucket, acl_xml_doc, headers={})
415
+ return put_acl(bucket, '', acl_xml_doc, headers)
416
+ end
417
+
418
+ def list_all_my_buckets(headers={})
419
+ return generate_url('GET', '', '', {}, headers)
420
+ end
421
+
422
+
423
+ private
424
+ # generate a url with the appropriate query string authentication
425
+ # parameters set.
426
+ def generate_url(method, bucket="", key="", path_args={}, headers={})
427
+ expires = 0
428
+ if not @expires_in.nil?
429
+ expires = Time.now.to_i + @expires_in
430
+ elsif not @expires.nil?
431
+ expires = @expires
432
+ else
433
+ raise "invalid expires state"
434
+ end
435
+
436
+ canonical_string =
437
+ S3::canonical_string(method, bucket, key, path_args, headers, expires)
438
+ encoded_canonical =
439
+ S3::encode(@aws_secret_access_key, canonical_string)
440
+
441
+ url = CallingFormat.build_url_base(@protocol, @server, @port, bucket, @calling_format)
442
+
443
+ path_args["Signature"] = encoded_canonical.to_s
444
+ path_args["Expires"] = expires.to_s
445
+ path_args["AWSAccessKeyId"] = @aws_access_key_id.to_s
446
+ arg_string = S3.path_args_hash_to_string(path_args)
447
+
448
+ return "#{url}/#{key}?#{arg_string}"
449
+ end
450
+
451
+ def merge_meta(headers, object)
452
+ final_headers = headers.clone
453
+ if not object.nil? and not object.metadata.nil?
454
+ object.metadata.each do |k, v|
455
+ final_headers[METADATA_PREFIX + k] = v
456
+ end
457
+ end
458
+ return final_headers
459
+ end
460
+ end
461
+
462
+ class S3Object
463
+ attr_accessor :data
464
+ attr_accessor :metadata
465
+ def initialize(data, metadata={})
466
+ @data, @metadata = data, metadata
467
+ end
468
+ end
469
+
470
+ # class for storing calling format constants
471
+ module CallingFormat
472
+ REGULAR = 0 # http://s3.amazonaws.com/bucket/key
473
+ SUBDOMAIN = 1 # http://bucket.s3.amazonaws.com/key
474
+ VANITY = 2 # http://<vanity_domain>/key -- vanity_domain resolves to s3.amazonaws.com
475
+
476
+ # build the url based on the calling format, and bucket
477
+ def CallingFormat.build_url_base(protocol, server, port, bucket, format)
478
+ build_url_base = "#{protocol}://"
479
+ if bucket.empty?
480
+ build_url_base << "#{server}:#{port}"
481
+ elsif format == SUBDOMAIN
482
+ build_url_base << "#{bucket}.#{server}:#{port}"
483
+ elsif format == VANITY
484
+ build_url_base << "#{bucket}:#{port}"
485
+ else
486
+ build_url_base << "#{server}:#{port}/#{bucket}"
487
+ end
488
+ return build_url_base
489
+ end
490
+ end
491
+
492
+ class Owner
493
+ attr_accessor :id
494
+ attr_accessor :display_name
495
+ end
496
+
497
+ class ListEntry
498
+ attr_accessor :key
499
+ attr_accessor :last_modified
500
+ attr_accessor :etag
501
+ attr_accessor :size
502
+ attr_accessor :storage_class
503
+ attr_accessor :owner
504
+ end
505
+
506
+ class ListProperties
507
+ attr_accessor :name
508
+ attr_accessor :prefix
509
+ attr_accessor :marker
510
+ attr_accessor :max_keys
511
+ attr_accessor :delimiter
512
+ attr_accessor :is_truncated
513
+ attr_accessor :next_marker
514
+ end
515
+
516
+ class CommonPrefixEntry
517
+ attr_accessor :prefix
518
+ end
519
+
520
+ # Parses the list bucket output into a list of ListEntry objects, and
521
+ # a list of CommonPrefixEntry objects if applicable.
522
+ class ListBucketParser
523
+ attr_reader :properties
524
+ attr_reader :entries
525
+ attr_reader :common_prefixes
526
+
527
+ def initialize
528
+ reset
529
+ end
530
+
531
+ def tag_start(name, attributes)
532
+ if name == 'ListBucketResult'
533
+ @properties = ListProperties.new
534
+ elsif name == 'Contents'
535
+ @curr_entry = ListEntry.new
536
+ elsif name == 'Owner'
537
+ @curr_entry.owner = Owner.new
538
+ elsif name == 'CommonPrefixes'
539
+ @common_prefix_entry = CommonPrefixEntry.new
540
+ end
541
+ end
542
+
543
+ # we have one, add him to the entries list
544
+ def tag_end(name)
545
+ # this prefix is the one we echo back from the request
546
+ if name == 'Name'
547
+ @properties.name = @curr_text
548
+ elsif name == 'Prefix' and @is_echoed_prefix
549
+ @properties.prefix = @curr_text
550
+ @is_echoed_prefix = nil
551
+ elsif name == 'Marker'
552
+ @properties.marker = @curr_text
553
+ elsif name == 'MaxKeys'
554
+ @properties.max_keys = @curr_text.to_i
555
+ elsif name == 'Delimiter'
556
+ @properties.delimiter = @curr_text
557
+ elsif name == 'IsTruncated'
558
+ @properties.is_truncated = @curr_text == 'true'
559
+ elsif name == 'NextMarker'
560
+ @properties.next_marker = @curr_text
561
+ elsif name == 'Contents'
562
+ @entries << @curr_entry
563
+ elsif name == 'Key'
564
+ @curr_entry.key = @curr_text
565
+ elsif name == 'LastModified'
566
+ @curr_entry.last_modified = @curr_text
567
+ elsif name == 'ETag'
568
+ @curr_entry.etag = @curr_text
569
+ elsif name == 'Size'
570
+ @curr_entry.size = @curr_text.to_i
571
+ elsif name == 'StorageClass'
572
+ @curr_entry.storage_class = @curr_text
573
+ elsif name == 'ID'
574
+ @curr_entry.owner.id = @curr_text
575
+ elsif name == 'DisplayName'
576
+ @curr_entry.owner.display_name = @curr_text
577
+ elsif name == 'CommonPrefixes'
578
+ @common_prefixes << @common_prefix_entry
579
+ elsif name == 'Prefix'
580
+ # this is the common prefix for keys that match up to the delimiter
581
+ @common_prefix_entry.prefix = @curr_text
582
+ end
583
+ @curr_text = ''
584
+ end
585
+
586
+ def text(text)
587
+ @curr_text += text
588
+ end
589
+
590
+ def xmldecl(version, encoding, standalone)
591
+ # ignore
592
+ end
593
+
594
+ # get ready for another parse
595
+ def reset
596
+ @is_echoed_prefix = true;
597
+ @entries = []
598
+ @curr_entry = nil
599
+ @common_prefixes = []
600
+ @common_prefix_entry = nil
601
+ @curr_text = ''
602
+ end
603
+ end
604
+
605
+ class Bucket
606
+ attr_accessor :name
607
+ attr_accessor :creation_date
608
+ end
609
+
610
+ class ListAllMyBucketsParser
611
+ attr_reader :entries
612
+
613
+ def initialize
614
+ reset
615
+ end
616
+
617
+ def tag_start(name, attributes)
618
+ if name == 'Bucket'
619
+ @curr_bucket = Bucket.new
620
+ end
621
+ end
622
+
623
+ # we have one, add him to the entries list
624
+ def tag_end(name)
625
+ if name == 'Bucket'
626
+ @entries << @curr_bucket
627
+ elsif name == 'Name'
628
+ @curr_bucket.name = @curr_text
629
+ elsif name == 'CreationDate'
630
+ @curr_bucket.creation_date = @curr_text
631
+ end
632
+ @curr_text = ''
633
+ end
634
+
635
+ def text(text)
636
+ @curr_text += text
637
+ end
638
+
639
+ def xmldecl(version, encoding, standalone)
640
+ # ignore
641
+ end
642
+
643
+ # get ready for another parse
644
+ def reset
645
+ @entries = []
646
+ @owner = nil
647
+ @curr_bucket = nil
648
+ @curr_text = ''
649
+ end
650
+ end
651
+
652
+ class Response
653
+ attr_reader :http_response
654
+ def initialize(response)
655
+ @http_response = response
656
+ end
657
+ end
658
+
659
+ class GetResponse < Response
660
+ attr_reader :object
661
+ def initialize(response)
662
+ super(response)
663
+ metadata = get_aws_metadata(response)
664
+ data = response.body
665
+ @object = S3Object.new(data, metadata)
666
+ end
667
+
668
+ # parses the request headers and pulls out the s3 metadata into a hash
669
+ def get_aws_metadata(response)
670
+ metadata = {}
671
+ response.each do |key, value|
672
+ if key =~ /^#{METADATA_PREFIX}(.*)$/oi
673
+ metadata[$1] = value
674
+ else
675
+ metadata[key] = value
676
+ end
677
+ end
678
+ return metadata
679
+ end
680
+ end
681
+
682
+ class ListBucketResponse < Response
683
+ attr_reader :properties
684
+ attr_reader :entries
685
+ attr_reader :common_prefix_entries
686
+
687
+ def initialize(response)
688
+ super(response)
689
+ if response.is_a? Net::HTTPSuccess
690
+ parser = ListBucketParser.new
691
+ REXML::Document.parse_stream(response.body, parser)
692
+ @properties = parser.properties
693
+ @entries = parser.entries
694
+ @common_prefix_entries = parser.common_prefixes
695
+ else
696
+ @entries = []
697
+ end
698
+ end
699
+ end
700
+
701
+ class ListAllMyBucketsResponse < Response
702
+ attr_reader :entries
703
+ def initialize(response)
704
+ super(response)
705
+ if response.is_a? Net::HTTPSuccess
706
+ parser = ListAllMyBucketsParser.new
707
+ REXML::Document.parse_stream(response.body, parser)
708
+ @entries = parser.entries
709
+ else
710
+ @entries = []
711
+ end
712
+ end
713
+ end
714
+ end