-
Notifications
You must be signed in to change notification settings - Fork 175
/
right_s3_interface.rb
990 lines (920 loc) · 39.8 KB
/
right_s3_interface.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
#
# Copyright (c) 2007-2008 RightScale Inc
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
module RightAws
class S3Interface < RightAwsBase
USE_100_CONTINUE_PUT_SIZE = 1_000_000
include RightAwsBaseInterface
DEFAULT_HOST = 's3.amazonaws.com'
DEFAULT_PORT = 443
DEFAULT_PROTOCOL = 'https'
REQUEST_TTL = 30
DEFAULT_EXPIRES_AFTER = 1 * 24 * 60 * 60 # One day's worth of seconds
ONE_YEAR_IN_SECONDS = 365 * 24 * 60 * 60
AMAZON_HEADER_PREFIX = 'x-amz-'
AMAZON_METADATA_PREFIX = 'x-amz-meta-'
@@bench = AwsBenchmarkingBlock.new
def self.bench_xml
@@bench.xml
end
def self.bench_s3
@@bench.service
end
# Creates new RightS3 instance.
#
# s3 = RightAws::S3Interface.new('1E3GDYEOGFJPIT7XXXXXX','hgTHt68JY07JKUY08ftHYtERkjgtfERn57XXXXXX', {:multi_thread => true, :logger => Logger.new('/tmp/x.log')}) #=> #<RightS3:0xb7b3c27c>
#
# Params is a hash:
#
# {:server => 's3.amazonaws.com' # Amazon service host: 's3.amazonaws.com'(default)
# :port => 443 # Amazon service port: 80 or 443(default)
# :protocol => 'https' # Amazon service protocol: 'http' or 'https'(default)
# :multi_thread => true|false # Multi-threaded (connection per each thread): true or false(default)
# :logger => Logger Object} # Logger instance: logs to STDOUT if omitted }
#
def initialize(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
init({ :name => 'S3',
:default_host => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).host : DEFAULT_HOST,
:default_port => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).port : DEFAULT_PORT,
:default_protocol => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).scheme : DEFAULT_PROTOCOL },
aws_access_key_id || ENV['AWS_ACCESS_KEY_ID'],
aws_secret_access_key || ENV['AWS_SECRET_ACCESS_KEY'],
params)
end
#-----------------------------------------------------------------
# Requests
#-----------------------------------------------------------------
# Produces canonical string for signing.
def canonical_string(method, path, headers={}, expires=nil) # :nodoc:
s3_headers = {}
headers.each do |key, value|
key = key.downcase
s3_headers[key] = value.to_s.strip if key[/^#{AMAZON_HEADER_PREFIX}|^content-md5$|^content-type$|^date$/o]
end
s3_headers['content-type'] ||= ''
s3_headers['content-md5'] ||= ''
s3_headers['date'] = '' if s3_headers.has_key? 'x-amz-date'
s3_headers['date'] = expires if expires
# prepare output string
out_string = "#{method}\n"
s3_headers.sort { |a, b| a[0] <=> b[0] }.each do |key, value|
out_string << (key[/^#{AMAZON_HEADER_PREFIX}/o] ? "#{key}:#{value}\n" : "#{value}\n")
end
# ignore everything after the question mark...
out_string << path.gsub(/\?.*$/, '')
# ...unless there is an acl or torrent parameter
out_string << '?acl' if path[/[&?]acl($|&|=)/]
out_string << '?torrent' if path[/[&?]torrent($|&|=)/]
out_string << '?location' if path[/[&?]location($|&|=)/]
# out_string << '?logging' if path[/[&?]logging($|&|=)/] # this one is beta, no support for now
out_string
end
def is_dns_bucket?(bucket_name)
bucket_name = bucket_name.to_s
return nil unless (3..63) === bucket_name.size
bucket_name.split('.').each do |component|
return nil unless component[/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/]
end
true
end
# Generates request hash for REST API.
# Assumes that headers[:url] is URL encoded (use CGI::escape)
def generate_rest_request(method, headers) # :nodoc:
# default server to use
server = @params[:server]
# fix path
path_to_sign = headers[:url]
path_to_sign = "/#{path_to_sign}" unless path_to_sign[/^\//]
# extract bucket name and check it's dns compartibility
path_to_sign[%r{^/([a-z0-9._-]*)(/[^?]*)?(\?.+)?}i]
bucket_name, key_path, params_list = $1, $2, $3
# select request model
if is_dns_bucket?(bucket_name)
# add backet to a server name
server = "#{bucket_name}.#{server}"
# remove bucket from the path
path = "#{key_path || '/'}#{params_list}"
# refactor the path (add '/' before params_list if the key is empty)
path_to_sign = "/#{bucket_name}#{path}"
else
path = path_to_sign
end
data = headers[:data]
# remove unset(==optional) and symbolyc keys
headers.each{ |key, value| headers.delete(key) if (value.nil? || key.is_a?(Symbol)) }
#
headers['content-type'] ||= ''
headers['date'] = Time.now.httpdate
# create request
request = "Net::HTTP::#{method.capitalize}".constantize.new(path)
request.body = data if data
# set request headers and meta headers
headers.each { |key, value| request[key.to_s] = value }
#generate auth strings
auth_string = canonical_string(request.method, path_to_sign, request.to_hash)
signature = AwsUtils::sign(@aws_secret_access_key, auth_string)
# set other headers
request['Authorization'] = "AWS #{@aws_access_key_id}:#{signature}"
# prepare output hash
{ :request => request,
:server => server,
:port => @params[:port],
:protocol => @params[:protocol] }
end
# Sends request to Amazon and parses the response.
# Raises AwsError if any banana happened.
def request_info(request, parser, &block) # :nodoc:
thread = @params[:multi_thread] ? Thread.current : Thread.main
thread[:s3_connection] ||= Rightscale::HttpConnection.new(:exception => RightAws::AwsError, :logger => @logger)
request_info_impl(thread[:s3_connection], @@bench, request, parser, &block)
end
# Returns an array of customer's buckets. Each item is a +hash+.
#
# s3.list_all_my_buckets #=>
# [{:owner_id => "00000000009314cc309ffe736daa2b264357476c7fea6efb2c3347ac3ab2792a",
# :owner_display_name => "root",
# :name => "bucket_name",
# :creation_date => "2007-04-19T18:47:43.000Z"}, ..., {...}]
#
def list_all_my_buckets(headers={})
req_hash = generate_rest_request('GET', headers.merge(:url=>''))
request_info(req_hash, S3ListAllMyBucketsParser.new(:logger => @logger))
rescue
on_exception
end
# Creates new bucket. Returns +true+ or an exception.
#
# # create a bucket at American server
# s3.create_bucket('my-awesome-bucket-us') #=> true
# # create a bucket at European server
# s3.create_bucket('my-awesome-bucket-eu', :location => :eu) #=> true
#
def create_bucket(bucket, headers={})
data = nil
unless headers[:location].blank?
data = "<CreateBucketConfiguration><LocationConstraint>#{headers[:location].to_s.upcase}</LocationConstraint></CreateBucketConfiguration>"
end
req_hash = generate_rest_request('PUT', headers.merge(:url=>bucket, :data => data))
request_info(req_hash, S3TrueParser.new)
rescue Exception => e
# if the bucket exists AWS returns an error for the location constraint interface. Drop it
e.is_a?(RightAws::AwsError) && e.message.include?('BucketAlreadyOwnedByYou') ? true : on_exception
end
# Retrieve bucket location
#
# s3.create_bucket('my-awesome-bucket-us') #=> true
# puts s3.bucket_location('my-awesome-bucket-us') #=> '' (Amazon's default value assumed)
#
# s3.create_bucket('my-awesome-bucket-eu', :location => :eu) #=> true
# puts s3.bucket_location('my-awesome-bucket-eu') #=> 'EU'
#
def bucket_location(bucket, headers={})
req_hash = generate_rest_request('GET', headers.merge(:url=>"#{bucket}?location"))
request_info(req_hash, S3BucketLocationParser.new)
rescue
on_exception
end
# Deletes new bucket. Bucket must be empty! Returns +true+ or an exception.
#
# s3.delete_bucket('my_awesome_bucket') #=> true
#
# See also: force_delete_bucket method
#
def delete_bucket(bucket, headers={})
req_hash = generate_rest_request('DELETE', headers.merge(:url=>bucket))
request_info(req_hash, S3TrueParser.new)
rescue
on_exception
end
# Returns an array of bucket's keys. Each array item (key data) is a +hash+.
#
# s3.list_bucket('my_awesome_bucket', { 'prefix'=>'t', 'marker'=>'', 'max-keys'=>5, delimiter=>'' }) #=>
# [{:key => "test1",
# :last_modified => "2007-05-18T07:00:59.000Z",
# :owner_id => "00000000009314cc309ffe736daa2b264357476c7fea6efb2c3347ac3ab2792a",
# :owner_display_name => "root",
# :e_tag => "000000000059075b964b07152d234b70",
# :storage_class => "STANDARD",
# :size => 3,
# :service=> {'is_truncated' => false,
# 'prefix' => "t",
# 'marker' => "",
# 'name' => "my_awesome_bucket",
# 'max-keys' => "5"}, ..., {...}]
#
def list_bucket(bucket, options={}, headers={})
bucket += '?'+options.map{|k, v| "#{k.to_s}=#{CGI::escape v.to_s}"}.join('&') unless options.blank?
req_hash = generate_rest_request('GET', headers.merge(:url=>bucket))
request_info(req_hash, S3ListBucketParser.new(:logger => @logger))
rescue
on_exception
end
# Incrementally list the contents of a bucket. Yields the following hash to a block:
# s3.incrementally_list_bucket('my_awesome_bucket', { 'prefix'=>'t', 'marker'=>'', 'max-keys'=>5, delimiter=>'' }) yields
# {
# :name => 'bucketname',
# :prefix => 'subfolder/',
# :marker => 'fileN.jpg',
# :max_keys => 234,
# :delimiter => '/',
# :is_truncated => true,
# :next_marker => 'fileX.jpg',
# :contents => [
# { :key => "file1",
# :last_modified => "2007-05-18T07:00:59.000Z",
# :e_tag => "000000000059075b964b07152d234b70",
# :size => 3,
# :storage_class => "STANDARD",
# :owner_id => "00000000009314cc309ffe736daa2b264357476c7fea6efb2c3347ac3ab2792a",
# :owner_display_name => "root"
# }, { :key, ...}, ... {:key, ...}
# ]
# :common_prefixes => [
# "prefix1",
# "prefix2",
# ...,
# "prefixN"
# ]
# }
def incrementally_list_bucket(bucket, options={}, headers={}, &block)
internal_options = options.symbolize_keys
begin
internal_bucket = bucket.dup
internal_bucket += '?'+internal_options.map{|k, v| "#{k.to_s}=#{CGI::escape v.to_s}"}.join('&') unless internal_options.blank?
req_hash = generate_rest_request('GET', headers.merge(:url=>internal_bucket))
response = request_info(req_hash, S3ImprovedListBucketParser.new(:logger => @logger))
there_are_more_keys = response[:is_truncated]
if(there_are_more_keys)
internal_options[:marker] = decide_marker(response)
total_results = response[:contents].length + response[:common_prefixes].length
internal_options[:'max-keys'] ? (internal_options[:'max-keys'] -= total_results) : nil
end
yield response
end while there_are_more_keys && under_max_keys(internal_options)
true
rescue
on_exception
end
private
def decide_marker(response)
return response[:next_marker].dup if response[:next_marker]
last_key = response[:contents].last[:key]
last_prefix = response[:common_prefixes].last
if(!last_key)
return nil if(!last_prefix)
last_prefix.dup
elsif(!last_prefix)
last_key.dup
else
last_key > last_prefix ? last_key.dup : last_prefix.dup
end
end
def under_max_keys(internal_options)
internal_options[:'max-keys'] ? internal_options[:'max-keys'] > 0 : true
end
public
# Saves object to Amazon. Returns +true+ or an exception.
# Any header starting with AMAZON_METADATA_PREFIX is considered
# user metadata. It will be stored with the object and returned
# when you retrieve the object. The total size of the HTTP
# request, not including the body, must be less than 4 KB.
#
# s3.put('my_awesome_bucket', 'log/current/1.log', 'Ola-la!', 'x-amz-meta-family'=>'Woho556!') #=> true
#
# This method is capable of 'streaming' uploads; that is, it can upload
# data from a file or other IO object without first reading all the data
# into memory. This is most useful for large PUTs - it is difficult to read
# a 2 GB file entirely into memory before sending it to S3.
# To stream an upload, pass an object that responds to 'read' (like the read
# method of IO) and to either 'lstat' or 'size'. For files, this means
# streaming is enabled by simply making the call:
#
# s3.put(bucket_name, 'S3keyname.forthisfile', File.open('localfilename.dat'))
#
# If the IO object you wish to stream from responds to the read method but
# doesn't implement lstat or size, you can extend the object dynamically
# to implement these methods, or define your own class which defines these
# methods. Be sure that your class returns 'nil' from read() after having
# read 'size' bytes. Otherwise S3 will drop the socket after
# 'Content-Length' bytes have been uploaded, and HttpConnection will
# interpret this as an error.
#
# This method now supports very large PUTs, where very large
# is > 2 GB.
#
# For Win32 users: Files and IO objects should be opened in binary mode. If
# a text mode IO object is passed to PUT, it will be converted to binary
# mode.
#
def put(bucket, key, data=nil, headers={})
# On Windows, if someone opens a file in text mode, we must reset it so
# to binary mode for streaming to work properly
if(data.respond_to?(:binmode))
data.binmode
end
if (data.respond_to?(:lstat) && data.lstat.size >= USE_100_CONTINUE_PUT_SIZE) ||
(data.respond_to?(:size) && data.size >= USE_100_CONTINUE_PUT_SIZE)
headers['expect'] = '100-continue'
end
req_hash = generate_rest_request('PUT', headers.merge(:url=>"#{bucket}/#{CGI::escape key}", :data=>data))
request_info(req_hash, S3TrueParser.new)
rescue
on_exception
end
# Retrieves object data from Amazon. Returns a +hash+ or an exception.
#
# s3.get('my_awesome_bucket', 'log/curent/1.log') #=>
#
# {:object => "Ola-la!",
# :headers => {"last-modified" => "Wed, 23 May 2007 09:08:04 GMT",
# "content-type" => "",
# "etag" => "\"000000000096f4ee74bc4596443ef2a4\"",
# "date" => "Wed, 23 May 2007 09:08:03 GMT",
# "x-amz-id-2" => "ZZZZZZZZZZZZZZZZZZZZ1HJXZoehfrS4QxcxTdNGldR7w/FVqblP50fU8cuIMLiu",
# "x-amz-meta-family" => "Woho556!",
# "x-amz-request-id" => "0000000C246D770C",
# "server" => "AmazonS3",
# "content-length" => "7"}}
#
# If a block is provided, yields incrementally to the block as
# the response is read. For large responses, this function is ideal as
# the response can be 'streamed'. The hash containing header fields is
# still returned.
# Example:
# foo = File.new('./chunder.txt', File::CREAT|File::RDWR)
# rhdr = s3.get('aws-test', 'Cent5V1_7_1.img.part.00') do |chunk|
# foo.write(chunk)
# end
# foo.close
#
def get(bucket, key, headers={}, &block)
req_hash = generate_rest_request('GET', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"))
request_info(req_hash, S3HttpResponseBodyParser.new, &block)
rescue
on_exception
end
# Retrieves object metadata. Returns a +hash+ of http_response_headers.
#
# s3.head('my_awesome_bucket', 'log/curent/1.log') #=>
# {"last-modified" => "Wed, 23 May 2007 09:08:04 GMT",
# "content-type" => "",
# "etag" => "\"000000000096f4ee74bc4596443ef2a4\"",
# "date" => "Wed, 23 May 2007 09:08:03 GMT",
# "x-amz-id-2" => "ZZZZZZZZZZZZZZZZZZZZ1HJXZoehfrS4QxcxTdNGldR7w/FVqblP50fU8cuIMLiu",
# "x-amz-meta-family" => "Woho556!",
# "x-amz-request-id" => "0000000C246D770C",
# "server" => "AmazonS3",
# "content-length" => "7"}
#
def head(bucket, key, headers={})
req_hash = generate_rest_request('HEAD', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"))
request_info(req_hash, S3HttpResponseHeadParser.new)
rescue
on_exception
end
# Deletes key. Returns +true+ or an exception.
#
# s3.delete('my_awesome_bucket', 'log/curent/1.log') #=> true
#
def delete(bucket, key='', headers={})
req_hash = generate_rest_request('DELETE', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"))
request_info(req_hash, S3TrueParser.new)
rescue
on_exception
end
# Copy an object.
# directive: :copy - copy meta-headers from source (default value)
# :replace - replace meta-headers by passed ones
#
# # copy a key with meta-headers
# s3.copy('b1', 'key1', 'b1', 'key1_copy') #=> {:e_tag=>"\"e8b...8d\"", :last_modified=>"2008-05-11T10:25:22.000Z"}
#
# # copy a key, overwrite meta-headers
# s3.copy('b1', 'key2', 'b1', 'key2_copy', :replace, 'x-amz-meta-family'=>'Woho555!') #=> {:e_tag=>"\"e8b...8d\"", :last_modified=>"2008-05-11T10:26:22.000Z"}
#
# see: http://docs.amazonwebservices.com/AmazonS3/2006-03-01/UsingCopyingObjects.html
# http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTObjectCOPY.html
#
def copy(src_bucket, src_key, dest_bucket, dest_key=nil, directive=:copy, headers={})
dest_key ||= src_key
headers['x-amz-metadata-directive'] = directive.to_s.upcase
headers['x-amz-copy-source'] = "#{src_bucket}/#{CGI::escape src_key}"
req_hash = generate_rest_request('PUT', headers.merge(:url=>"#{dest_bucket}/#{CGI::escape dest_key}"))
request_info(req_hash, S3CopyParser.new)
rescue
on_exception
end
# Move an object.
# directive: :copy - copy meta-headers from source (default value)
# :replace - replace meta-headers by passed ones
#
# # move bucket1/key1 to bucket1/key2
# s3.move('bucket1', 'key1', 'bucket1', 'key2') #=> {:e_tag=>"\"e8b...8d\"", :last_modified=>"2008-05-11T10:27:22.000Z"}
#
# # move bucket1/key1 to bucket2/key2 with new meta-headers assignment
# s3.copy('bucket1', 'key1', 'bucket2', 'key2', :replace, 'x-amz-meta-family'=>'Woho555!') #=> {:e_tag=>"\"e8b...8d\"", :last_modified=>"2008-05-11T10:28:22.000Z"}
#
def move(src_bucket, src_key, dest_bucket, dest_key=nil, directive=:copy, headers={})
copy_result = copy(src_bucket, src_key, dest_bucket, dest_key, directive, headers)
# delete an original key if it differs from a destination one
delete(src_bucket, src_key) unless src_bucket == dest_bucket && src_key == dest_key
copy_result
end
# Rename an object.
#
# # rename bucket1/key1 to bucket1/key2
# s3.rename('bucket1', 'key1', 'key2') #=> {:e_tag=>"\"e8b...8d\"", :last_modified=>"2008-05-11T10:29:22.000Z"}
#
def rename(src_bucket, src_key, dest_key, headers={})
move(src_bucket, src_key, src_bucket, dest_key, :copy, headers)
end
# Retieves the ACL (access control policy) for a bucket or object. Returns a hash of headers and xml doc with ACL data. See: http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html.
#
# s3.get_acl('my_awesome_bucket', 'log/curent/1.log') #=>
# {:headers => {"x-amz-id-2"=>"B3BdDMDUz+phFF2mGBH04E46ZD4Qb9HF5PoPHqDRWBv+NVGeA3TOQ3BkVvPBjgxX",
# "content-type"=>"application/xml;charset=ISO-8859-1",
# "date"=>"Wed, 23 May 2007 09:40:16 GMT",
# "x-amz-request-id"=>"B183FA7AB5FBB4DD",
# "server"=>"AmazonS3",
# "transfer-encoding"=>"chunked"},
# :object => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<AccessControlPolicy xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Owner>
# <ID>16144ab2929314cc309ffe736daa2b264357476c7fea6efb2c3347ac3ab2792a</ID><DisplayName>root</DisplayName></Owner>
# <AccessControlList><Grant><Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"CanonicalUser\"><ID>
# 16144ab2929314cc309ffe736daa2b264357476c7fea6efb2c3347ac3ab2792a</ID><DisplayName>root</DisplayName></Grantee>
# <Permission>FULL_CONTROL</Permission></Grant></AccessControlList></AccessControlPolicy>" }
#
def get_acl(bucket, key='', headers={})
key = key.blank? ? '' : "/#{CGI::escape key}"
req_hash = generate_rest_request('GET', headers.merge(:url=>"#{bucket}#{key}?acl"))
request_info(req_hash, S3HttpResponseBodyParser.new)
rescue
on_exception
end
# Retieves the ACL (access control policy) for a bucket or object.
# Returns a hash of {:owner, :grantees}
#
# s3.get_acl_parse('my_awesome_bucket', 'log/curent/1.log') #=>
#
# { :grantees=>
# { "16...2a"=>
# { :display_name=>"root",
# :permissions=>["FULL_CONTROL"],
# :attributes=>
# { "xsi:type"=>"CanonicalUser",
# "xmlns:xsi"=>"http://www.w3.org/2001/XMLSchema-instance"}},
# "http://acs.amazonaws.com/groups/global/AllUsers"=>
# { :display_name=>"AllUsers",
# :permissions=>["READ"],
# :attributes=>
# { "xsi:type"=>"Group",
# "xmlns:xsi"=>"http://www.w3.org/2001/XMLSchema-instance"}}},
# :owner=>
# { :id=>"16..2a",
# :display_name=>"root"}}
#
def get_acl_parse(bucket, key='', headers={})
key = key.blank? ? '' : "/#{CGI::escape key}"
req_hash = generate_rest_request('GET', headers.merge(:url=>"#{bucket}#{key}?acl"))
acl = request_info(req_hash, S3AclParser.new(:logger => @logger))
result = {}
result[:owner] = acl[:owner]
result[:grantees] = {}
acl[:grantees].each do |grantee|
key = grantee[:id] || grantee[:uri]
if result[:grantees].key?(key)
result[:grantees][key][:permissions] << grantee[:permissions]
else
result[:grantees][key] =
{ :display_name => grantee[:display_name] || grantee[:uri].to_s[/[^\/]*$/],
:permissions => grantee[:permissions].to_a,
:attributes => grantee[:attributes] }
end
end
result
rescue
on_exception
end
# Sets the ACL on a bucket or object.
def put_acl(bucket, key, acl_xml_doc, headers={})
key = key.blank? ? '' : "/#{CGI::escape key}"
req_hash = generate_rest_request('PUT', headers.merge(:url=>"#{bucket}#{key}?acl", :data=>acl_xml_doc))
request_info(req_hash, S3HttpResponseBodyParser.new)
rescue
on_exception
end
# Retieves the ACL (access control policy) for a bucket. Returns a hash of headers and xml doc with ACL data.
def get_bucket_acl(bucket, headers={})
return get_acl(bucket, '', headers)
rescue
on_exception
end
# Sets the ACL on a bucket only.
def put_bucket_acl(bucket, acl_xml_doc, headers={})
return put_acl(bucket, '', acl_xml_doc, headers)
rescue
on_exception
end
# Removes all keys from bucket. Returns +true+ or an exception.
#
# s3.clear_bucket('my_awesome_bucket') #=> true
#
def clear_bucket(bucket)
incrementally_list_bucket(bucket) do |results|
results[:contents].each { |key| delete(bucket, key[:key]) }
end
true
rescue
on_exception
end
# Deletes all keys in bucket then deletes bucket. Returns +true+ or an exception.
#
# s3.force_delete_bucket('my_awesome_bucket')
#
def force_delete_bucket(bucket)
clear_bucket(bucket)
delete_bucket(bucket)
rescue
on_exception
end
# Deletes all keys where the 'folder_key' may be assumed as 'folder' name. Returns an array of string keys that have been deleted.
#
# s3.list_bucket('my_awesome_bucket').map{|key_data| key_data[:key]} #=> ['test','test/2/34','test/3','test1','test1/logs']
# s3.delete_folder('my_awesome_bucket','test') #=> ['test','test/2/34','test/3']
#
def delete_folder(bucket, folder_key, separator='/')
folder_key.chomp!(separator)
allkeys = []
incrementally_list_bucket(bucket, { 'prefix' => folder_key }) do |results|
keys = results[:contents].map{ |s3_key| s3_key[:key][/^#{folder_key}($|#{separator}.*)/] ? s3_key[:key] : nil}.compact
keys.each{ |key| delete(bucket, key) }
allkeys << keys
end
allkeys
rescue
on_exception
end
# Retrieves object data only (headers are omitted). Returns +string+ or an exception.
#
# s3.get('my_awesome_bucket', 'log/curent/1.log') #=> 'Ola-la!'
#
def get_object(bucket, key, headers={})
get(bucket, key, headers)[:object]
rescue
on_exception
end
#-----------------------------------------------------------------
# Query API: Links
#-----------------------------------------------------------------
# Generates link for QUERY API
def generate_link(method, headers={}, expires=nil) #:nodoc:
# default server to use
server = @params[:server]
# fix path
path_to_sign = headers[:url]
path_to_sign = "/#{path_to_sign}" unless path_to_sign[/^\//]
# extract bucket name and check it's dns compartibility
path_to_sign[%r{^/([a-z0-9._-]*)(/[^?]*)?(\?.+)?}i]
bucket_name, key_path, params_list = $1, $2, $3
# select request model
if is_dns_bucket?(bucket_name)
# add backet to a server name
server = "#{bucket_name}.#{server}"
# remove bucket from the path
path = "#{key_path || '/'}#{params_list}"
# refactor the path (add '/' before params_list if the key is empty)
path_to_sign = "/#{bucket_name}#{path}"
else
path = path_to_sign
end
# expiration time
expires ||= DEFAULT_EXPIRES_AFTER
expires = Time.now.utc + expires if expires.is_a?(Fixnum) && (expires < ONE_YEAR_IN_SECONDS)
expires = expires.to_i
# remove unset(==optional) and symbolyc keys
headers.each{ |key, value| headers.delete(key) if (value.nil? || key.is_a?(Symbol)) }
#generate auth strings
auth_string = canonical_string(method, path_to_sign, headers, expires)
signature = CGI::escape(Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new("sha1"), @aws_secret_access_key, auth_string)).strip)
# path building
addon = "Signature=#{signature}&Expires=#{expires}&AWSAccessKeyId=#{@aws_access_key_id}"
path += path[/\?/] ? "&#{addon}" : "?#{addon}"
"#{@params[:protocol]}://#{server}:#{@params[:port]}#{path}"
rescue
on_exception
end
# Generates link for 'ListAllMyBuckets'.
#
# s3.list_all_my_buckets_link #=> url string
#
def list_all_my_buckets_link(expires=nil, headers={})
generate_link('GET', headers.merge(:url=>''), expires)
rescue
on_exception
end
# Generates link for 'CreateBucket'.
#
# s3.create_bucket_link('my_awesome_bucket') #=> url string
#
def create_bucket_link(bucket, expires=nil, headers={})
generate_link('PUT', headers.merge(:url=>bucket), expires)
rescue
on_exception
end
# Generates link for 'DeleteBucket'.
#
# s3.delete_bucket_link('my_awesome_bucket') #=> url string
#
def delete_bucket_link(bucket, expires=nil, headers={})
generate_link('DELETE', headers.merge(:url=>bucket), expires)
rescue
on_exception
end
# Generates link for 'ListBucket'.
#
# s3.list_bucket_link('my_awesome_bucket') #=> url string
#
def list_bucket_link(bucket, options=nil, expires=nil, headers={})
bucket += '?' + options.map{|k, v| "#{k.to_s}=#{CGI::escape v.to_s}"}.join('&') unless options.blank?
generate_link('GET', headers.merge(:url=>bucket), expires)
rescue
on_exception
end
# Generates link for 'PutObject'.
#
# s3.put_link('my_awesome_bucket',key, object) #=> url string
#
def put_link(bucket, key, data=nil, expires=nil, headers={})
generate_link('PUT', headers.merge(:url=>"#{bucket}/#{CGI::escape key}", :data=>data), expires)
rescue
on_exception
end
# Generates link for 'GetObject'.
#
# if a bucket comply with virtual hosting naming then retuns a link with the
# bucket as a part of host name:
#
# s3.get_link('my-awesome-bucket',key) #=> https://my-awesome-bucket.s3.amazonaws.com:443/asia%2Fcustomers?Signature=nh7...
#
# otherwise returns an old style link (the bucket is a part of path):
#
# s3.get_link('my_awesome_bucket',key) #=> https://s3.amazonaws.com:443/my_awesome_bucket/asia%2Fcustomers?Signature=QAO...
#
# see http://docs.amazonwebservices.com/AmazonS3/2006-03-01/VirtualHosting.html
def get_link(bucket, key, expires=nil, headers={})
generate_link('GET', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"), expires)
rescue
on_exception
end
# Generates link for 'HeadObject'.
#
# s3.head_link('my_awesome_bucket',key) #=> url string
#
def head_link(bucket, key, expires=nil, headers={})
generate_link('HEAD', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"), expires)
rescue
on_exception
end
# Generates link for 'DeleteObject'.
#
# s3.delete_link('my_awesome_bucket',key) #=> url string
#
def delete_link(bucket, key, expires=nil, headers={})
generate_link('DELETE', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"), expires)
rescue
on_exception
end
# Generates link for 'GetACL'.
#
# s3.get_acl_link('my_awesome_bucket',key) #=> url string
#
def get_acl_link(bucket, key='', headers={})
return generate_link('GET', headers.merge(:url=>"#{bucket}/#{CGI::escape key}?acl"))
rescue
on_exception
end
# Generates link for 'PutACL'.
#
# s3.put_acl_link('my_awesome_bucket',key) #=> url string
#
def put_acl_link(bucket, key='', headers={})
return generate_link('PUT', headers.merge(:url=>"#{bucket}/#{CGI::escape key}?acl"))
rescue
on_exception
end
# Generates link for 'GetBucketACL'.
#
# s3.get_acl_link('my_awesome_bucket',key) #=> url string
#
def get_bucket_acl_link(bucket, headers={})
return get_acl_link(bucket, '', headers)
rescue
on_exception
end
# Generates link for 'PutBucketACL'.
#
# s3.put_acl_link('my_awesome_bucket',key) #=> url string
#
def put_bucket_acl_link(bucket, acl_xml_doc, headers={})
return put_acl_link(bucket, '', acl_xml_doc, headers)
rescue
on_exception
end
#-----------------------------------------------------------------
# PARSERS:
#-----------------------------------------------------------------
class S3ListAllMyBucketsParser < RightAWSParser # :nodoc:
def reset
@result = []
@owner = {}
end
def tagstart(name, attributes)
@current_bucket = {} if name == 'Bucket'
end
def tagend(name)
case name
when 'ID' ; @owner[:owner_id] = @text
when 'DisplayName' ; @owner[:owner_display_name] = @text
when 'Name' ; @current_bucket[:name] = @text
when 'CreationDate'; @current_bucket[:creation_date] = @text
when 'Bucket' ; @result << @current_bucket.merge(@owner)
end
end
end
class S3ListBucketParser < RightAWSParser # :nodoc:
def reset
@result = []
@service = {}
@current_key = {}
end
def tagstart(name, attributes)
@current_key = {} if name == 'Contents'
end
def tagend(name)
case name
# service info
when 'Name' ; @service['name'] = @text
when 'Prefix' ; @service['prefix'] = @text
when 'Marker' ; @service['marker'] = @text
when 'MaxKeys' ; @service['max-keys'] = @text
when 'Delimiter' ; @service['delimiter'] = @text
when 'IsTruncated' ; @service['is_truncated'] = (@text =~ /false/ ? false : true)
# key data
when 'Key' ; @current_key[:key] = @text
when 'LastModified'; @current_key[:last_modified] = @text
when 'ETag' ; @current_key[:e_tag] = @text
when 'Size' ; @current_key[:size] = @text.to_i
when 'StorageClass'; @current_key[:storage_class] = @text
when 'ID' ; @current_key[:owner_id] = @text
when 'DisplayName' ; @current_key[:owner_display_name] = @text
when 'Contents' ; @current_key[:service] = @service; @result << @current_key
end
end
end
class S3ImprovedListBucketParser < RightAWSParser # :nodoc:
def reset
@result = {}
@result[:contents] = []
@result[:common_prefixes] = []
@contents = []
@current_key = {}
@common_prefixes = []
@in_common_prefixes = false
end
def tagstart(name, attributes)
@current_key = {} if name == 'Contents'
@in_common_prefixes = true if name == 'CommonPrefixes'
end
def tagend(name)
case name
# service info
when 'Name' ; @result[:name] = @text
# Amazon uses the same tag for the search prefix and for the entries
# in common prefix...so use our simple flag to see which element
# we are parsing
when 'Prefix' ; @in_common_prefixes ? @common_prefixes << @text : @result[:prefix] = @text
when 'Marker' ; @result[:marker] = @text
when 'MaxKeys' ; @result[:max_keys] = @text
when 'Delimiter' ; @result[:delimiter] = @text
when 'IsTruncated' ; @result[:is_truncated] = (@text =~ /false/ ? false : true)
when 'NextMarker' ; @result[:next_marker] = @text
# key data
when 'Key' ; @current_key[:key] = @text
when 'LastModified'; @current_key[:last_modified] = @text
when 'ETag' ; @current_key[:e_tag] = @text
when 'Size' ; @current_key[:size] = @text.to_i
when 'StorageClass'; @current_key[:storage_class] = @text
when 'ID' ; @current_key[:owner_id] = @text
when 'DisplayName' ; @current_key[:owner_display_name] = @text
when 'Contents' ; @result[:contents] << @current_key
# Common Prefix stuff
when 'CommonPrefixes' ; @result[:common_prefixes] = @common_prefixes; @in_common_prefixes = false
end
end
end
class S3BucketLocationParser < RightAWSParser # :nodoc:
def reset
@result = ''
end
def tagend(name)
@result = @text if name == 'LocationConstraint'
end
end
class S3AclParser < RightAWSParser # :nodoc:
def reset
@result = {:grantees=>[], :owner=>{}}
@current_grantee = {}
end
def tagstart(name, attributes)
@current_grantee = { :attributes => attributes } if name=='Grantee'
end
def tagend(name)
case name
# service info
when 'ID'
if @xmlpath == 'AccessControlPolicy/Owner'
@result[:owner][:id] = @text
else
@current_grantee[:id] = @text
end
when 'DisplayName'
if @xmlpath == 'AccessControlPolicy/Owner'
@result[:owner][:display_name] = @text
else
@current_grantee[:display_name] = @text
end
when 'URI'
@current_grantee[:uri] = @text
when 'Permission'
@current_grantee[:permissions] = @text
when 'Grant'
@result[:grantees] << @current_grantee
end
end
end
class S3CopyParser < RightAWSParser # :nodoc:
def reset
@result = {}
end
def tagend(name)
case name
when 'LastModified' : @result[:last_modified] = @text
when 'ETag' : @result[:e_tag] = @text
end
end
end
#-----------------------------------------------------------------
# PARSERS: Non XML
#-----------------------------------------------------------------
class S3HttpResponseParser # :nodoc:
attr_reader :result
def parse(response)
@result = response
end
def headers_to_string(headers)
result = {}
headers.each do |key, value|
value = value.to_s if value.is_a?(Array) && value.size<2
result[key] = value
end
result
end
end
class S3TrueParser < S3HttpResponseParser # :nodoc:
def parse(response)
@result = response.is_a?(Net::HTTPSuccess)
end
end
class S3HttpResponseBodyParser < S3HttpResponseParser # :nodoc:
def parse(response)
@result = {
:object => response.body,
:headers => headers_to_string(response.to_hash)
}
end
end
class S3HttpResponseHeadParser < S3HttpResponseParser # :nodoc:
def parse(response)
@result = headers_to_string(response.to_hash)
end
end
end
end