kerryb-right_aws 1.7.3

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.
@@ -0,0 +1,1056 @@
1
+ #
2
+ # Copyright (c) 2007-2008 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #
23
+
24
+ module RightAws
25
+
26
+ # = RightAws::S3 -- RightScale's Amazon S3 interface
27
+ # The RightAws::S3 class provides a complete interface to Amazon's Simple
28
+ # Storage Service.
29
+ # For explanations of the semantics
30
+ # of each call, please refer to Amazon's documentation at
31
+ # http://developer.amazonwebservices.com/connect/kbcategory.jspa?categoryID=48
32
+ #
33
+ # See examples below for the bucket and buckets methods.
34
+ #
35
+ # Error handling: all operations raise an RightAws::AwsError in case
36
+ # of problems. Note that transient errors are automatically retried.
37
+ #
38
+ # It is a good way to use domain naming style getting a name for the buckets.
39
+ # See http://docs.amazonwebservices.com/AmazonS3/2006-03-01/UsingBucket.html
40
+ # about the naming convention for the buckets. This case they can be accessed using a virtual domains.
41
+ #
42
+ # Let assume you have 3 buckets: 'awesome-bucket', 'awesome_bucket' and 'AWEsomE-bucket'.
43
+ # The first ones objects can be accessed as: http:// awesome-bucket.s3.amazonaws.com/key/object
44
+ #
45
+ # But the rest have to be accessed as:
46
+ # http:// s3.amazonaws.com/awesome_bucket/key/object and http:// s3.amazonaws.com/AWEsomE-bucket/key/object
47
+ #
48
+ # See: http://docs.amazonwebservices.com/AmazonS3/2006-03-01/VirtualHosting.html for better explanation.
49
+ #
50
+ class S3
51
+ attr_reader :interface
52
+
53
+ # Create a new handle to an S3 account. All handles share the same per process or per thread
54
+ # HTTP connection to Amazon S3. Each handle is for a specific account.
55
+ # The +params+ are passed through as-is to RightAws::S3Interface.new
56
+ def initialize(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
57
+ @interface = S3Interface.new(aws_access_key_id, aws_secret_access_key, params)
58
+ end
59
+
60
+ # Retrieve a list of buckets.
61
+ # Returns an array of RightAws::S3::Bucket instances.
62
+ # # Create handle to S3 account
63
+ # s3 = RightAws::S3.new(aws_access_key_id, aws_secret_access_key)
64
+ # my_buckets_names = s3.buckets.map{|b| b.name}
65
+ # puts "Buckets on S3: #{my_bucket_names.join(', ')}"
66
+ def buckets
67
+ @interface.list_all_my_buckets.map! do |entry|
68
+ owner = Owner.new(entry[:owner_id], entry[:owner_display_name])
69
+ Bucket.new(self, entry[:name], entry[:creation_date], owner)
70
+ end
71
+ end
72
+
73
+ # Retrieve an individual bucket.
74
+ # If the bucket does not exist and +create+ is set, a new bucket
75
+ # is created on S3. Launching this method with +create+=+true+ may
76
+ # affect on the bucket's ACL if the bucket already exists.
77
+ # Returns a RightAws::S3::Bucket instance or +nil+ if the bucket does not exist
78
+ # and +create+ is not set.
79
+ #
80
+ # s3 = RightAws::S3.new(aws_access_key_id, aws_secret_access_key)
81
+ # bucket1 = s3.bucket('my_awesome_bucket_1')
82
+ # bucket1.keys #=> exception here if the bucket does not exists
83
+ # ...
84
+ # bucket2 = s3.bucket('my_awesome_bucket_2', true)
85
+ # bucket2.keys #=> list of keys
86
+ # # create a bucket at the European location with public read access
87
+ # bucket3 = s3.bucket('my-awesome-bucket-3', true, 'public-read', :location => :eu)
88
+ #
89
+ # see http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html
90
+ # (section: Canned Access Policies)
91
+ #
92
+ def bucket(name, create=false, perms=nil, headers={})
93
+ headers['x-amz-acl'] = perms if perms
94
+ @interface.create_bucket(name, headers) if create
95
+ buckets.each { |bucket| return bucket if bucket.name == name }
96
+ nil
97
+ end
98
+
99
+
100
+ class Bucket
101
+ attr_reader :s3, :name, :owner, :creation_date
102
+
103
+ # Create a Bucket instance.
104
+ # If the bucket does not exist and +create+ is set, a new bucket
105
+ # is created on S3. Launching this method with +create+=+true+ may
106
+ # affect on the bucket's ACL if the bucket already exists.
107
+ # Returns Bucket instance or +nil+ if the bucket does not exist
108
+ # and +create+ is not set.
109
+ #
110
+ # s3 = RightAws::S3.new(aws_access_key_id, aws_secret_access_key)
111
+ # ...
112
+ # bucket1 = RightAws::S3::Bucket.create(s3, 'my_awesome_bucket_1')
113
+ # bucket1.keys #=> exception here if the bucket does not exists
114
+ # ...
115
+ # bucket2 = RightAws::S3::Bucket.create(s3, 'my_awesome_bucket_2', true)
116
+ # bucket2.keys #=> list of keys
117
+ # # create a bucket at the European location with public read access
118
+ # bucket3 = RightAws::S3::Bucket.create(s3,'my-awesome-bucket-3', true, 'public-read', :location => :eu)
119
+ #
120
+ # see http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html
121
+ # (section: Canned Access Policies)
122
+ #
123
+ def self.create(s3, name, create=false, perms=nil, headers={})
124
+ s3.bucket(name, create, perms, headers)
125
+ end
126
+
127
+
128
+ # Create a bucket instance. In normal use this method should
129
+ # not be called directly.
130
+ # Use RightAws::S3::Bucket.create or RightAws::S3.bucket instead.
131
+ def initialize(s3, name, creation_date=nil, owner=nil)
132
+ @s3 = s3
133
+ @name = name
134
+ @owner = owner
135
+ @creation_date = creation_date
136
+ if @creation_date && !@creation_date.is_a?(Time)
137
+ @creation_date = Time.parse(@creation_date)
138
+ end
139
+ end
140
+
141
+ # Return bucket name as a String.
142
+ #
143
+ # bucket = RightAws::S3.bucket('my_awesome_bucket')
144
+ # puts bucket #=> 'my_awesome_bucket'
145
+ #
146
+ def to_s
147
+ @name.to_s
148
+ end
149
+ alias_method :full_name, :to_s
150
+
151
+ # Return a public link to bucket.
152
+ #
153
+ # bucket.public_link #=> 'https://s3.amazonaws.com:443/my_awesome_bucket'
154
+ #
155
+ def public_link
156
+ params = @s3.interface.params
157
+ "#{params[:protocol]}://#{params[:server]}:#{params[:port]}/#{full_name}"
158
+ end
159
+
160
+ # Returns the bucket location
161
+ def location
162
+ @location ||= @s3.interface.bucket_location(@name)
163
+ end
164
+
165
+ # Retrieve a group of keys from Amazon.
166
+ # +options+ is a hash: { 'prefix'=>'', 'marker'=>'', 'max-keys'=>5, 'delimiter'=>'' }).
167
+ # Retrieves meta-headers information if +head+ it +true+.
168
+ # Returns an array of Key instances.
169
+ #
170
+ # bucket.keys #=> # returns all keys from bucket
171
+ # bucket.keys('prefix' => 'logs') #=> # returns all keys that starts with 'logs'
172
+ #
173
+ def keys(options={}, head=false)
174
+ keys_and_service(options, head)[0]
175
+ end
176
+
177
+ # Same as +keys+ method but return an array of [keys, service_data].
178
+ # where +service_data+ is a hash with additional output information.
179
+ #
180
+ # keys, service = bucket.keys_and_service({'max-keys'=> 2, 'prefix' => 'logs'})
181
+ # p keys #=> # 2 keys array
182
+ # p service #=> {"max-keys"=>"2", "prefix"=>"logs", "name"=>"my_awesome_bucket", "marker"=>"", "is_truncated"=>true}
183
+ #
184
+ def keys_and_service(options={}, head=false)
185
+ opt = {}; options.each{ |key, value| opt[key.to_s] = value }
186
+ service_data = {}
187
+ thislist = {}
188
+ list = []
189
+ @s3.interface.incrementally_list_bucket(@name, opt) do |thislist|
190
+ thislist[:contents].each do |entry|
191
+ owner = Owner.new(entry[:owner_id], entry[:owner_display_name])
192
+ key = Key.new(self, entry[:key], nil, {}, {}, entry[:last_modified], entry[:e_tag], entry[:size], entry[:storage_class], owner)
193
+ key.head if head
194
+ list << key
195
+ end
196
+ end
197
+ thislist.each_key do |key|
198
+ service_data[key] = thislist[key] unless (key == :contents || key == :common_prefixes)
199
+ end
200
+ [list, service_data]
201
+ end
202
+
203
+ # Retrieve key information from Amazon.
204
+ # The +key_name+ is a +String+ or Key instance.
205
+ # Retrieves meta-header information if +head+ is +true+.
206
+ # Returns new Key instance.
207
+ #
208
+ # key = bucket.key('logs/today/1.log', true) #=> #<RightAws::S3::Key:0xb7b1e240 ... >
209
+ # # is the same as:
210
+ # key = RightAws::S3::Key.create(bucket, 'logs/today/1.log')
211
+ # key.head
212
+ #
213
+ def key(key_name, head=false)
214
+ raise 'Key name can not be empty.' if key_name.blank?
215
+ key_instance = nil
216
+ # if this key exists - find it ....
217
+ keys({'prefix'=>key_name}, head).each do |key|
218
+ if key.name == key_name.to_s
219
+ key_instance = key
220
+ break
221
+ end
222
+ end
223
+ # .... else this key is unknown
224
+ unless key_instance
225
+ key_instance = Key.create(self, key_name.to_s)
226
+ end
227
+ key_instance
228
+ end
229
+
230
+ # Store object data.
231
+ # The +key+ is a +String+ or Key instance.
232
+ # Returns +true+.
233
+ #
234
+ # bucket.put('logs/today/1.log', 'Olala!') #=> true
235
+ #
236
+ def put(key, data=nil, meta_headers={}, perms=nil, headers={})
237
+ key = Key.create(self, key.to_s, data, meta_headers) unless key.is_a?(Key)
238
+ key.put(data, perms, headers)
239
+ end
240
+
241
+ # Retrieve object data from Amazon.
242
+ # The +key+ is a +String+ or Key.
243
+ # Returns Key instance.
244
+ #
245
+ # key = bucket.get('logs/today/1.log') #=>
246
+ # puts key.data #=> 'sasfasfasdf'
247
+ #
248
+ def get(key, headers={})
249
+ key = Key.create(self, key.to_s) unless key.is_a?(Key)
250
+ key.get(headers)
251
+ end
252
+
253
+ # Rename object. Returns RightAws::S3::Key instance.
254
+ #
255
+ # new_key = bucket.rename_key('logs/today/1.log','logs/today/2.log') #=> #<RightAws::S3::Key:0xb7b1e240 ... >
256
+ # puts key.name #=> 'logs/today/2.log'
257
+ # key.exists? #=> true
258
+ #
259
+ def rename_key(old_key_or_name, new_name)
260
+ old_key_or_name = Key.create(self, old_key_or_name.to_s) unless old_key_or_name.is_a?(Key)
261
+ old_key_or_name.rename(new_name)
262
+ old_key_or_name
263
+ end
264
+
265
+ # Create an object copy. Returns a destination RightAws::S3::Key instance.
266
+ #
267
+ # new_key = bucket.copy_key('logs/today/1.log','logs/today/2.log') #=> #<RightAws::S3::Key:0xb7b1e240 ... >
268
+ # puts key.name #=> 'logs/today/2.log'
269
+ # key.exists? #=> true
270
+ #
271
+ def copy_key(old_key_or_name, new_key_or_name)
272
+ old_key_or_name = Key.create(self, old_key_or_name.to_s) unless old_key_or_name.is_a?(Key)
273
+ old_key_or_name.copy(new_key_or_name)
274
+ end
275
+
276
+ # Move an object to other location. Returns a destination RightAws::S3::Key instance.
277
+ #
278
+ # new_key = bucket.copy_key('logs/today/1.log','logs/today/2.log') #=> #<RightAws::S3::Key:0xb7b1e240 ... >
279
+ # puts key.name #=> 'logs/today/2.log'
280
+ # key.exists? #=> true
281
+ #
282
+ def move_key(old_key_or_name, new_key_or_name)
283
+ old_key_or_name = Key.create(self, old_key_or_name.to_s) unless old_key_or_name.is_a?(Key)
284
+ old_key_or_name.move(new_key_or_name)
285
+ end
286
+
287
+ # Remove all keys from a bucket.
288
+ # Returns +true+.
289
+ #
290
+ # bucket.clear #=> true
291
+ #
292
+ def clear
293
+ @s3.interface.clear_bucket(@name)
294
+ end
295
+
296
+ # Delete all keys where the 'folder_key' can be interpreted
297
+ # as a 'folder' name.
298
+ # Returns an array of string keys that have been deleted.
299
+ #
300
+ # bucket.keys.map{|key| key.name}.join(', ') #=> 'test, test/2/34, test/3, test1, test1/logs'
301
+ # bucket.delete_folder('test') #=> ['test','test/2/34','test/3']
302
+ #
303
+ def delete_folder(folder, separator='/')
304
+ @s3.interface.delete_folder(@name, folder, separator)
305
+ end
306
+
307
+ # Delete a bucket. Bucket must be empty.
308
+ # If +force+ is set, clears and deletes the bucket.
309
+ # Returns +true+.
310
+ #
311
+ # bucket.delete(true) #=> true
312
+ #
313
+ def delete(force=false)
314
+ force ? @s3.interface.force_delete_bucket(@name) : @s3.interface.delete_bucket(@name)
315
+ end
316
+
317
+ # Return a list of grantees.
318
+ #
319
+ def grantees
320
+ Grantee::grantees(self)
321
+ end
322
+
323
+ end
324
+
325
+
326
+ class Key
327
+ attr_reader :bucket, :name, :last_modified, :e_tag, :size, :storage_class, :owner
328
+ attr_accessor :headers, :meta_headers
329
+ attr_writer :data
330
+
331
+ # Separate Amazon meta headers from other headers
332
+ def self.split_meta(headers) #:nodoc:
333
+ hash = headers.dup
334
+ meta = {}
335
+ hash.each do |key, value|
336
+ if key[/^#{S3Interface::AMAZON_METADATA_PREFIX}/]
337
+ meta[key.gsub(S3Interface::AMAZON_METADATA_PREFIX,'')] = value
338
+ hash.delete(key)
339
+ end
340
+ end
341
+ [hash, meta]
342
+ end
343
+
344
+ def self.add_meta_prefix(meta_headers, prefix=S3Interface::AMAZON_METADATA_PREFIX)
345
+ meta = {}
346
+ meta_headers.each do |meta_header, value|
347
+ if meta_header[/#{prefix}/]
348
+ meta[meta_header] = value
349
+ else
350
+ meta["#{S3Interface::AMAZON_METADATA_PREFIX}#{meta_header}"] = value
351
+ end
352
+ end
353
+ meta
354
+ end
355
+
356
+
357
+ # Create a new Key instance, but do not create the actual key.
358
+ # The +name+ is a +String+.
359
+ # Returns a new Key instance.
360
+ #
361
+ # key = RightAws::S3::Key.create(bucket, 'logs/today/1.log') #=> #<RightAws::S3::Key:0xb7b1e240 ... >
362
+ # key.exists? #=> true | false
363
+ # key.put('Woohoo!') #=> true
364
+ # key.exists? #=> true
365
+ #
366
+ def self.create(bucket, name, data=nil, meta_headers={})
367
+ new(bucket, name, data, {}, meta_headers)
368
+ end
369
+
370
+ # Create a new Key instance, but do not create the actual key.
371
+ # In normal use this method should not be called directly.
372
+ # Use RightAws::S3::Key.create or bucket.key() instead.
373
+ #
374
+ def initialize(bucket, name, data=nil, headers={}, meta_headers={},
375
+ last_modified=nil, e_tag=nil, size=nil, storage_class=nil, owner=nil)
376
+ raise 'Bucket must be a Bucket instance.' unless bucket.is_a?(Bucket)
377
+ @bucket = bucket
378
+ @name = name
379
+ @data = data
380
+ @e_tag = e_tag
381
+ @size = size.to_i
382
+ @storage_class = storage_class
383
+ @owner = owner
384
+ @last_modified = last_modified
385
+ if @last_modified && !@last_modified.is_a?(Time)
386
+ @last_modified = Time.parse(@last_modified)
387
+ end
388
+ @headers, @meta_headers = self.class.split_meta(headers)
389
+ @meta_headers.merge!(meta_headers)
390
+ end
391
+
392
+ # Return key name as a String.
393
+ #
394
+ # key = RightAws::S3::Key.create(bucket, 'logs/today/1.log') #=> #<RightAws::S3::Key:0xb7b1e240 ... >
395
+ # puts key #=> 'logs/today/1.log'
396
+ #
397
+ def to_s
398
+ @name.to_s
399
+ end
400
+
401
+ # Return the full S3 path to this key (bucket/key).
402
+ #
403
+ # key.full_name #=> 'my_awesome_bucket/cool_key'
404
+ #
405
+ def full_name(separator='/')
406
+ "#{@bucket.to_s}#{separator}#{@name}"
407
+ end
408
+
409
+ # Return a public link to a key.
410
+ #
411
+ # key.public_link #=> 'https://s3.amazonaws.com:443/my_awesome_bucket/cool_key'
412
+ #
413
+ def public_link
414
+ params = @bucket.s3.interface.params
415
+ "#{params[:protocol]}://#{params[:server]}:#{params[:port]}/#{full_name('/')}"
416
+ end
417
+
418
+ # Return Key data. Retrieve this data from Amazon if it is the first time call.
419
+ # TODO TRB 6/19/07 What does the above mean? Clarify.
420
+ #
421
+ def data
422
+ get if !@data and exists?
423
+ @data
424
+ end
425
+
426
+ # Retrieve object data and attributes from Amazon.
427
+ # Returns a +String+.
428
+ #
429
+ def get(headers={})
430
+ response = @bucket.s3.interface.get(@bucket.name, @name, headers)
431
+ @data = response[:object]
432
+ @headers, @meta_headers = self.class.split_meta(response[:headers])
433
+ refresh(false)
434
+ @data
435
+ end
436
+
437
+ # Store object data on S3.
438
+ # Parameter +data+ is a +String+ or S3Object instance.
439
+ # Returns +true+.
440
+ #
441
+ # key = RightAws::S3::Key.create(bucket, 'logs/today/1.log')
442
+ # key.data = 'Qwerty'
443
+ # key.put #=> true
444
+ # ...
445
+ # key.put('Olala!') #=> true
446
+ #
447
+ def put(data=nil, perms=nil, headers={})
448
+ headers['x-amz-acl'] = perms if perms
449
+ @data = data || @data
450
+ meta = self.class.add_meta_prefix(@meta_headers)
451
+ @bucket.s3.interface.put(@bucket.name, @name, @data, meta.merge(headers))
452
+ end
453
+
454
+ # Rename an object. Returns new object name.
455
+ #
456
+ # key = RightAws::S3::Key.create(bucket, 'logs/today/1.log') #=> #<RightAws::S3::Key:0xb7b1e240 ... >
457
+ # key.rename('logs/today/2.log') #=> 'logs/today/2.log'
458
+ # puts key.name #=> 'logs/today/2.log'
459
+ # key.exists? #=> true
460
+ #
461
+ def rename(new_name)
462
+ @bucket.s3.interface.rename(@bucket.name, @name, new_name)
463
+ @name = new_name
464
+ end
465
+
466
+ # Create an object copy. Returns a destination RightAws::S3::Key instance.
467
+ #
468
+ # # Key instance as destination
469
+ # key1 = RightAws::S3::Key.create(bucket, 'logs/today/1.log') #=> #<RightAws::S3::Key:0xb7b1e240 ... >
470
+ # key2 = RightAws::S3::Key.create(bucket, 'logs/today/2.log') #=> #<RightAws::S3::Key:0xb7b5e240 ... >
471
+ # key1.put('Olala!') #=> true
472
+ # key1.copy(key2) #=> #<RightAws::S3::Key:0xb7b5e240 ... >
473
+ # key1.exists? #=> true
474
+ # key2.exists? #=> true
475
+ # puts key2.data #=> 'Olala!'
476
+ #
477
+ # # String as destination
478
+ # key = RightAws::S3::Key.create(bucket, 'logs/today/777.log') #=> #<RightAws::S3::Key:0xb7b1e240 ... >
479
+ # key.put('Olala!') #=> true
480
+ # new_key = key.copy('logs/today/888.log') #=> #<RightAws::S3::Key:0xb7b5e240 ... >
481
+ # key.exists? #=> true
482
+ # new_key.exists? #=> true
483
+ #
484
+ def copy(new_key_or_name)
485
+ new_key_or_name = Key.create(@bucket, new_key_or_name.to_s) unless new_key_or_name.is_a?(Key)
486
+ @bucket.s3.interface.copy(@bucket.name, @name, new_key_or_name.bucket.name, new_key_or_name.name)
487
+ new_key_or_name
488
+ end
489
+
490
+ # Move an object to other location. Returns a destination RightAws::S3::Key instance.
491
+ #
492
+ # # Key instance as destination
493
+ # key1 = RightAws::S3::Key.create(bucket, 'logs/today/1.log') #=> #<RightAws::S3::Key:0xb7b1e240 ... >
494
+ # key2 = RightAws::S3::Key.create(bucket, 'logs/today/2.log') #=> #<RightAws::S3::Key:0xb7b5e240 ... >
495
+ # key1.put('Olala!') #=> true
496
+ # key1.move(key2) #=> #<RightAws::S3::Key:0xb7b5e240 ... >
497
+ # key1.exists? #=> false
498
+ # key2.exists? #=> true
499
+ # puts key2.data #=> 'Olala!'
500
+ #
501
+ # # String as destination
502
+ # key = RightAws::S3::Key.create(bucket, 'logs/today/777.log') #=> #<RightAws::S3::Key:0xb7b1e240 ... >
503
+ # key.put('Olala!') #=> true
504
+ # new_key = key.move('logs/today/888.log') #=> #<RightAws::S3::Key:0xb7b5e240 ... >
505
+ # key.exists? #=> false
506
+ # new_key.exists? #=> true
507
+ #
508
+ def move(new_key_or_name)
509
+ new_key_or_name = Key.create(@bucket, new_key_or_name.to_s) unless new_key_or_name.is_a?(Key)
510
+ @bucket.s3.interface.move(@bucket.name, @name, new_key_or_name.bucket.name, new_key_or_name.name)
511
+ new_key_or_name
512
+ end
513
+
514
+ # Retrieve key info from bucket and update attributes.
515
+ # Refresh meta-headers (by calling +head+ method) if +head+ is set.
516
+ # Returns +true+ if the key exists in bucket and +false+ otherwise.
517
+ #
518
+ # key = RightAws::S3::Key.create(bucket, 'logs/today/1.log')
519
+ # key.e_tag #=> nil
520
+ # key.meta_headers #=> {}
521
+ # key.refresh #=> true
522
+ # key.e_tag #=> '12345678901234567890bf11094484b6'
523
+ # key.meta_headers #=> {"family"=>"qwerty", "name"=>"asdfg"}
524
+ #
525
+ def refresh(head=true)
526
+ new_key = @bucket.key(self)
527
+ @last_modified = new_key.last_modified
528
+ @e_tag = new_key.e_tag
529
+ @size = new_key.size
530
+ @storage_class = new_key.storage_class
531
+ @owner = new_key.owner
532
+ if @last_modified
533
+ self.head
534
+ true
535
+ else
536
+ @headers = @meta_headers = {}
537
+ false
538
+ end
539
+ end
540
+
541
+ # Updates headers and meta-headers from S3.
542
+ # Returns +true+.
543
+ #
544
+ # key.meta_headers #=> {"family"=>"qwerty"}
545
+ # key.head #=> true
546
+ # key.meta_headers #=> {"family"=>"qwerty", "name"=>"asdfg"}
547
+ #
548
+ def head
549
+ @headers, @meta_headers = self.class.split_meta(@bucket.s3.interface.head(@bucket, @name))
550
+ true
551
+ end
552
+
553
+ # Reload meta-headers only. Returns meta-headers hash.
554
+ #
555
+ # key.reload_meta #=> {"family"=>"qwerty", "name"=>"asdfg"}
556
+ #
557
+ def reload_meta
558
+ @meta_headers = self.class.split_meta(@bucket.s3.interface.head(@bucket, @name)).last
559
+ end
560
+
561
+ # Replace meta-headers by new hash at S3. Returns new meta-headers hash.
562
+ #
563
+ # key.reload_meta #=> {"family"=>"qwerty", "name"=>"asdfg"}
564
+ # key.save_meta #=> {"family"=>"oops", "race" => "troll"}
565
+ # key.reload_meta #=> {"family"=>"oops", "race" => "troll"}
566
+ #
567
+ def save_meta(meta_headers)
568
+ meta = self.class.add_meta_prefix(meta_headers)
569
+ @bucket.s3.interface.copy(@bucket.name, @name, @bucket.name, @name, :replace, meta)
570
+ @meta_headers = self.class.split_meta(meta)[1]
571
+ end
572
+
573
+ # Check for existence of the key in the given bucket.
574
+ # Returns +true+ or +false+.
575
+ #
576
+ # key = RightAws::S3::Key.create(bucket,'logs/today/1.log')
577
+ # key.exists? #=> false
578
+ # key.put('Woohoo!') #=> true
579
+ # key.exists? #=> true
580
+ #
581
+ def exists?
582
+ @bucket.key(self).last_modified ? true : false
583
+ end
584
+
585
+ # Remove key from bucket.
586
+ # Returns +true+.
587
+ #
588
+ # key.delete #=> true
589
+ #
590
+ def delete
591
+ raise 'Key name must be specified.' if @name.blank?
592
+ @bucket.s3.interface.delete(@bucket, @name)
593
+ end
594
+
595
+ # Return a list of grantees.
596
+ #
597
+ def grantees
598
+ Grantee::grantees(self)
599
+ end
600
+
601
+ end
602
+
603
+
604
+ class Owner
605
+ attr_reader :id, :name
606
+
607
+ def initialize(id, name)
608
+ @id = id
609
+ @name = name
610
+ end
611
+
612
+ # Return Owner name as a +String+.
613
+ def to_s
614
+ @name
615
+ end
616
+ end
617
+
618
+
619
+ # There are 2 ways to set permissions for a bucket or key (called a +thing+ below):
620
+ #
621
+ # 1 . Use +perms+ param to set 'Canned Access Policies' when calling the <tt>bucket.create</tt>,
622
+ # <tt>bucket.put</tt> and <tt>key.put</tt> methods.
623
+ # The +perms+ param can take these values: 'private', 'public-read', 'public-read-write' and
624
+ # 'authenticated-read'.
625
+ # (see http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html).
626
+ #
627
+ # bucket = s3.bucket('bucket_for_kd_test_13', true, 'public-read')
628
+ # key.put('Woohoo!','public-read-write' )
629
+ #
630
+ # 2 . Use Grantee instances (the permission is a +String+ or an +Array+ of: 'READ', 'WRITE',
631
+ # 'READ_ACP', 'WRITE_ACP', 'FULL_CONTROL'):
632
+ #
633
+ # bucket = s3.bucket('my_awesome_bucket', true)
634
+ # grantee1 = RightAws::S3::Grantee.new(bucket, 'a123b...223c', FULL_CONTROL, :apply)
635
+ # grantee2 = RightAws::S3::Grantee.new(bucket, 'xy3v3...5fhp', [READ, WRITE], :apply)
636
+ #
637
+ # There is only one way to get and to remove permission (via Grantee instances):
638
+ #
639
+ # grantees = bucket.grantees # a list of Grantees that have any access for this bucket
640
+ # grantee1 = RightAws::S3::Grantee.new(bucket, 'a123b...223c')
641
+ # grantee1.perms #=> returns a list of perms for this grantee to that bucket
642
+ # ...
643
+ # grantee1.drop # remove all perms for this grantee
644
+ # grantee2.revoke('WRITE') # revoke write access only
645
+ #
646
+ class Grantee
647
+ # A bucket or a key the grantee has an access to.
648
+ attr_reader :thing
649
+ # Grantee Amazon id.
650
+ attr_reader :id
651
+ # Grantee display name.
652
+ attr_reader :name
653
+ # Array of permissions.
654
+ attr_accessor :perms
655
+
656
+ # Retrieve Owner information and a list of Grantee instances that have
657
+ # a access to this thing (bucket or key).
658
+ #
659
+ # bucket = s3.bucket('my_awesome_bucket', true, 'public-read')
660
+ # ...
661
+ # RightAws::S3::Grantee.owner_and_grantees(bucket) #=> [owner, grantees]
662
+ #
663
+ def self.owner_and_grantees(thing)
664
+ if thing.is_a?(Bucket)
665
+ bucket, key = thing, ''
666
+ else
667
+ bucket, key = thing.bucket, thing
668
+ end
669
+ hash = bucket.s3.interface.get_acl_parse(bucket.to_s, key.to_s)
670
+ owner = Owner.new(hash[:owner][:id], hash[:owner][:display_name])
671
+
672
+ grantees = []
673
+ hash[:grantees].each do |id, params|
674
+ grantees << new(thing, id, params[:permissions], nil, params[:display_name])
675
+ end
676
+ [owner, grantees]
677
+ end
678
+
679
+ # Retrieves a list of Grantees instances that have an access to this thing(bucket or key).
680
+ #
681
+ # bucket = s3.bucket('my_awesome_bucket', true, 'public-read')
682
+ # ...
683
+ # RightAws::S3::Grantee.grantees(bucket) #=> grantees
684
+ #
685
+ def self.grantees(thing)
686
+ owner_and_grantees(thing)[1]
687
+ end
688
+
689
+ def self.put_acl(thing, owner, grantees) #:nodoc:
690
+ if thing.is_a?(Bucket)
691
+ bucket, key = thing, ''
692
+ else
693
+ bucket, key = thing.bucket, thing
694
+ end
695
+ body = "<AccessControlPolicy>" +
696
+ "<Owner>" +
697
+ "<ID>#{owner.id}</ID>" +
698
+ "<DisplayName>#{owner.name}</DisplayName>" +
699
+ "</Owner>" +
700
+ "<AccessControlList>" +
701
+ grantees.map{|grantee| grantee.to_xml}.join +
702
+ "</AccessControlList>" +
703
+ "</AccessControlPolicy>"
704
+ bucket.s3.interface.put_acl(bucket.to_s, key.to_s, body)
705
+ end
706
+
707
+ # Create a new Grantee instance.
708
+ # Grantee +id+ must exist on S3. If +action+ == :refresh, then retrieve
709
+ # permissions from S3 and update @perms. If +action+ == :apply, then apply
710
+ # perms to +thing+ at S3. If +action+ == :apply_and_refresh then it performs.
711
+ # both the actions. This is used for the new grantees that had no perms to
712
+ # this thing before. The default action is :refresh.
713
+ #
714
+ # bucket = s3.bucket('my_awesome_bucket', true, 'public-read')
715
+ # grantee1 = RightAws::S3::Grantee.new(bucket, 'a123b...223c', FULL_CONTROL)
716
+ # ...
717
+ # grantee2 = RightAws::S3::Grantee.new(bucket, 'abcde...asdf', [FULL_CONTROL, READ], :apply)
718
+ # grantee3 = RightAws::S3::Grantee.new(bucket, 'aaaaa...aaaa', 'READ', :apply_and_refresh)
719
+ #
720
+ def initialize(thing, id, perms=[], action=:refresh, name=nil)
721
+ @thing = thing
722
+ @id = id
723
+ @name = name
724
+ @perms = perms.to_a
725
+ case action
726
+ when :apply: apply
727
+ when :refresh: refresh
728
+ when :apply_and_refresh: apply; refresh
729
+ end
730
+ end
731
+
732
+ # Return +true+ if the grantee has any permissions to the thing.
733
+ def exists?
734
+ self.class.grantees(@thing).each do |grantee|
735
+ return true if @id == grantee.id
736
+ end
737
+ false
738
+ end
739
+
740
+ # Return Grantee type (+String+): "Group" or "CanonicalUser".
741
+ def type
742
+ @id[/^http:/] ? "Group" : "CanonicalUser"
743
+ end
744
+
745
+ # Return a name or an id.
746
+ def to_s
747
+ @name || @id
748
+ end
749
+
750
+ # Add permissions for grantee.
751
+ # Permissions: 'READ', 'WRITE', 'READ_ACP', 'WRITE_ACP', 'FULL_CONTROL'.
752
+ # See http://docs.amazonwebservices.com/AmazonS3/2006-03-01/UsingPermissions.html .
753
+ # Returns +true+.
754
+ #
755
+ # grantee.grant('FULL_CONTROL') #=> true
756
+ # grantee.grant('FULL_CONTROL','WRITE','READ') #=> true
757
+ # grantee.grant(['WRITE_ACP','READ','READ_ACP']) #=> true
758
+ #
759
+ def grant(*permissions)
760
+ permissions.flatten!
761
+ old_perms = @perms.dup
762
+ @perms += permissions
763
+ @perms.uniq!
764
+ return true if @perms == old_perms
765
+ apply
766
+ end
767
+
768
+ # Revoke permissions for grantee.
769
+ # Permissions: 'READ', 'WRITE', 'READ_ACP', 'WRITE_ACP', 'FULL_CONTROL'
770
+ # See http://docs.amazonwebservices.com/AmazonS3/2006-03-01/UsingPermissions.html .
771
+ # Default value is 'FULL_CONTROL'.
772
+ # Returns +true+.
773
+ #
774
+ # grantee.revoke('READ') #=> true
775
+ # grantee.revoke('FULL_CONTROL','WRITE') #=> true
776
+ # grantee.revoke(['READ_ACP','WRITE_ACP']) #=> true
777
+ #
778
+ def revoke(*permissions)
779
+ permissions.flatten!
780
+ old_perms = @perms.dup
781
+ @perms -= permissions
782
+ @perms.uniq!
783
+ return true if @perms == old_perms
784
+ apply
785
+ end
786
+
787
+ # Revoke all permissions for this grantee.
788
+ # Returns +true+.
789
+ #
790
+ # grantee.drop #=> true
791
+ #
792
+ def drop
793
+ @perms = []
794
+ apply
795
+ end
796
+
797
+ # Refresh grantee perms for its +thing+.
798
+ # Returns +true+ if the grantee has perms for this +thing+ or
799
+ # +false+ otherwise, and updates @perms value as a side-effect.
800
+ #
801
+ # grantee.grant('FULL_CONTROL') #=> true
802
+ # grantee.refresh #=> true
803
+ # grantee.drop #=> true
804
+ # grantee.refresh #=> false
805
+ #
806
+ def refresh
807
+ @perms = []
808
+ self.class.grantees(@thing).each do |grantee|
809
+ if @id == grantee.id
810
+ @name = grantee.name
811
+ @perms = grantee.perms
812
+ return true
813
+ end
814
+ end
815
+ false
816
+ end
817
+
818
+ # Apply current grantee @perms to +thing+. This method is called internally by the +grant+
819
+ # and +revoke+ methods. In normal use this method should not
820
+ # be called directly.
821
+ #
822
+ # grantee.perms = ['FULL_CONTROL']
823
+ # grantee.apply #=> true
824
+ #
825
+ def apply
826
+ @perms.uniq!
827
+ owner, grantees = self.class.owner_and_grantees(@thing)
828
+ # walk through all the grantees and replace the data for the current one and ...
829
+ grantees.map! { |grantee| grantee.id == @id ? self : grantee }
830
+ # ... if this grantee is not known - add this bad boy to a list
831
+ grantees << self unless grantees.include?(self)
832
+ # set permissions
833
+ self.class.put_acl(@thing, owner, grantees)
834
+ end
835
+
836
+ def to_xml # :nodoc:
837
+ id_str = @id[/^http/] ? "<URI>#{@id}</URI>" : "<ID>#{@id}</ID>"
838
+ grants = ''
839
+ @perms.each do |perm|
840
+ grants << "<Grant>" +
841
+ "<Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " +
842
+ "xsi:type=\"#{type}\">#{id_str}</Grantee>" +
843
+ "<Permission>#{perm}</Permission>" +
844
+ "</Grant>"
845
+ end
846
+ grants
847
+ end
848
+
849
+ end
850
+
851
+ end
852
+
853
+ # RightAws::S3Generator and RightAws::S3Generator::Bucket methods:
854
+ #
855
+ # s3g = RightAws::S3Generator.new('1...2', 'nx...Y6') #=> #<RightAws::S3Generator:0xb7b5cc94>
856
+ #
857
+ # # List all buckets(method 'GET'):
858
+ # buckets_list = s3g.buckets #=> 'https://s3.amazonaws.com:443/?Signature=Y...D&Expires=1180941864&AWSAccessKeyId=1...2'
859
+ # # Create bucket link (method 'PUT'):
860
+ # bucket = s3g.bucket('my_awesome_bucket') #=> #<RightAws::S3Generator::Bucket:0xb7bcbda8>
861
+ # link_to_create = bucket.create_link(1.hour) #=> https://s3.amazonaws.com:443/my_awesome_bucket?Signature=4...D&Expires=1180942132&AWSAccessKeyId=1...2
862
+ # # ... or:
863
+ # bucket = RightAws::S3Generator::Bucket.create(s3g, 'my_awesome_bucket') #=> #<RightAws::S3Generator::Bucket:0xb7bcbda8>
864
+ # link_to_create = bucket.create_link(1.hour) #=> https://s3.amazonaws.com:443/my_awesome_bucket?Signature=4...D&Expires=1180942132&AWSAccessKeyId=1...2
865
+ # # ... or:
866
+ # bucket = RightAws::S3Generator::Bucket.new(s3g, 'my_awesome_bucket') #=> #<RightAws::S3Generator::Bucket:0xb7bcbda8>
867
+ # link_to_create = bucket.create_link(1.hour) #=> https://s3.amazonaws.com:443/my_awesome_bucket?Signature=4...D&Expires=1180942132&AWSAccessKeyId=1...2
868
+ # # List bucket(method 'GET'):
869
+ # bucket.keys(1.day) #=> https://s3.amazonaws.com:443/my_awesome_bucket?Signature=i...D&Expires=1180942620&AWSAccessKeyId=1...2
870
+ # # Create/put key (method 'PUT'):
871
+ # bucket.put('my_cool_key') #=> https://s3.amazonaws.com:443/my_awesome_bucket/my_cool_key?Signature=q...D&Expires=1180943094&AWSAccessKeyId=1...2
872
+ # # Get key data (method 'GET'):
873
+ # bucket.get('logs/today/1.log', 1.hour) #=> https://s3.amazonaws.com:443/my_awesome_bucket/my_cool_key?Signature=h...M%3D&Expires=1180820032&AWSAccessKeyId=1...2
874
+ # # Delete bucket (method 'DELETE'):
875
+ # bucket.delete(2.hour) #=> https://s3.amazonaws.com:443/my_awesome_bucket/logs%2Ftoday%2F1.log?Signature=4...D&Expires=1180820032&AWSAccessKeyId=1...2
876
+ #
877
+ # RightAws::S3Generator::Key methods:
878
+ #
879
+ # # Create Key instance:
880
+ # key = RightAws::S3Generator::Key.new(bicket, 'my_cool_key') #=> #<RightAws::S3Generator::Key:0xb7b7394c>
881
+ # # Put key data (method 'PUT'):
882
+ # key.put #=> https://s3.amazonaws.com:443/my_awesome_bucket/my_cool_key?Signature=2...D&Expires=1180943302&AWSAccessKeyId=1...2
883
+ # # Get key data (method 'GET'):
884
+ # key.get #=> https://s3.amazonaws.com:443/my_awesome_bucket/my_cool_key?Signature=a...D&Expires=1180820032&AWSAccessKeyId=1...2
885
+ # # Head key (method 'HEAD'):
886
+ # key.head #=> https://s3.amazonaws.com:443/my_awesome_bucket/my_cool_key?Signature=b...D&Expires=1180820032&AWSAccessKeyId=1...2
887
+ # # Delete key (method 'DELETE'):
888
+ # key.delete #=> https://s3.amazonaws.com:443/my_awesome_bucket/my_cool_key?Signature=x...D&Expires=1180820032&AWSAccessKeyId=1...2
889
+ #
890
+ class S3Generator
891
+ attr_reader :interface
892
+
893
+ def initialize(aws_access_key_id, aws_secret_access_key, params={})
894
+ @interface = S3Interface.new(aws_access_key_id, aws_secret_access_key, params)
895
+ end
896
+
897
+ # Generate link to list all buckets
898
+ #
899
+ # s3.buckets(1.hour)
900
+ #
901
+ def buckets(expires=nil, headers={})
902
+ @interface.list_all_my_buckets_link(expires, headers)
903
+ end
904
+
905
+ # Create new S3LinkBucket instance and generate link to create it at S3.
906
+ #
907
+ # bucket= s3.bucket('my_owesome_bucket')
908
+ #
909
+ def bucket(name, expires=nil, headers={})
910
+ Bucket.create(self, name.to_s)
911
+ end
912
+
913
+ class Bucket
914
+ attr_reader :s3, :name
915
+
916
+ def to_s
917
+ @name
918
+ end
919
+ alias_method :full_name, :to_s
920
+
921
+ # Return a public link to bucket.
922
+ #
923
+ # bucket.public_link #=> 'https://s3.amazonaws.com:443/my_awesome_bucket'
924
+ #
925
+ def public_link
926
+ params = @s3.interface.params
927
+ "#{params[:protocol]}://#{params[:server]}:#{params[:port]}/#{full_name}"
928
+ end
929
+
930
+ # Create new S3LinkBucket instance and generate creation link for it.
931
+ def self.create(s3, name, expires=nil, headers={})
932
+ new(s3, name.to_s)
933
+ end
934
+
935
+ # Create new S3LinkBucket instance.
936
+ def initialize(s3, name)
937
+ @s3, @name = s3, name.to_s
938
+ end
939
+
940
+ # Return a link to create this bucket.
941
+ #
942
+ def create_link(expires=nil, headers={})
943
+ @s3.interface.create_bucket_link(@name, expires, headers)
944
+ end
945
+
946
+ # Generate link to list keys.
947
+ #
948
+ # bucket.keys
949
+ # bucket.keys('prefix'=>'logs')
950
+ #
951
+ def keys(options=nil, expires=nil, headers={})
952
+ @s3.interface.list_bucket_link(@name, options, expires, headers)
953
+ end
954
+
955
+ # Return a S3Generator::Key instance.
956
+ #
957
+ # bucket.key('my_cool_key').get #=> https://s3.amazonaws.com:443/my_awesome_bucket/my_cool_key?Signature=B...D&Expires=1180820032&AWSAccessKeyId=1...2
958
+ # bucket.key('my_cool_key').delete #=> https://s3.amazonaws.com:443/my_awesome_bucket/my_cool_key?Signature=B...D&Expires=1180820098&AWSAccessKeyId=1...2
959
+ #
960
+ def key(name)
961
+ Key.new(self, name)
962
+ end
963
+
964
+ # Generates link to PUT key data.
965
+ #
966
+ # puts bucket.put('logs/today/1.log', 2.hour)
967
+ #
968
+ def put(key, meta_headers={}, expires=nil, headers={})
969
+ meta = RightAws::S3::Key.add_meta_prefix(meta_headers)
970
+ @s3.interface.put_link(@name, key.to_s, nil, expires, meta.merge(headers))
971
+ end
972
+
973
+ # Generate link to GET key data.
974
+ #
975
+ # bucket.get('logs/today/1.log', 1.hour)
976
+ #
977
+ def get(key, expires=nil, headers={})
978
+ @s3.interface.get_link(@name, key.to_s, expires, headers)
979
+ end
980
+
981
+ # Generate link to delete bucket.
982
+ #
983
+ # bucket.delete(2.hour)
984
+ #
985
+ def delete(expires=nil, headers={})
986
+ @s3.interface.delete_bucket_link(@name, expires, headers)
987
+ end
988
+ end
989
+
990
+
991
+ class Key
992
+ attr_reader :bucket, :name
993
+
994
+ def to_s
995
+ @name
996
+ end
997
+
998
+ # Return a full S# name (bucket/key).
999
+ #
1000
+ # key.full_name #=> 'my_awesome_bucket/cool_key'
1001
+ #
1002
+ def full_name(separator='/')
1003
+ "#{@bucket.to_s}#{separator}#{@name}"
1004
+ end
1005
+
1006
+ # Return a public link to key.
1007
+ #
1008
+ # key.public_link #=> 'https://s3.amazonaws.com:443/my_awesome_bucket/cool_key'
1009
+ #
1010
+ def public_link
1011
+ params = @bucket.s3.interface.params
1012
+ "#{params[:protocol]}://#{params[:server]}:#{params[:port]}/#{full_name('/')}"
1013
+ end
1014
+
1015
+ def initialize(bucket, name, meta_headers={})
1016
+ @bucket = bucket
1017
+ @name = name.to_s
1018
+ @meta_headers = meta_headers
1019
+ raise 'Key name can not be empty.' if @name.blank?
1020
+ end
1021
+
1022
+ # Generate link to PUT key data.
1023
+ #
1024
+ # puts bucket.put('logs/today/1.log', '123', 2.hour) #=> https://s3.amazonaws.com:443/my_awesome_bucket/logs%2Ftoday%2F1.log?Signature=B...D&Expires=1180820032&AWSAccessKeyId=1...2
1025
+ #
1026
+ def put(expires=nil, headers={})
1027
+ @bucket.put(@name.to_s, @meta_headers, expires, headers)
1028
+ end
1029
+
1030
+ # Generate link to GET key data.
1031
+ #
1032
+ # bucket.get('logs/today/1.log', 1.hour) #=> https://s3.amazonaws.com:443/my_awesome_bucket/logs%2Ftoday%2F1.log?Signature=h...M%3D&Expires=1180820032&AWSAccessKeyId=1...2
1033
+ #
1034
+ def get(expires=nil, headers={})
1035
+ @bucket.s3.interface.get_link(@bucket.to_s, @name, expires, headers)
1036
+ end
1037
+
1038
+ # Generate link to delete key.
1039
+ #
1040
+ # bucket.delete(2.hour) #=> https://s3.amazonaws.com:443/my_awesome_bucket/logs%2Ftoday%2F1.log?Signature=4...D&Expires=1180820032&AWSAccessKeyId=1...2
1041
+ #
1042
+ def delete(expires=nil, headers={})
1043
+ @bucket.s3.interface.delete_link(@bucket.to_s, @name, expires, headers)
1044
+ end
1045
+
1046
+ # Generate link to head key.
1047
+ #
1048
+ # bucket.head(2.hour) #=> https://s3.amazonaws.com:443/my_awesome_bucket/logs%2Ftoday%2F1.log?Signature=4...D&Expires=1180820032&AWSAccessKeyId=1...2
1049
+ #
1050
+ def head(expires=nil, headers={})
1051
+ @bucket.s3.interface.head_link(@bucket.to_s, @name, expires, headers)
1052
+ end
1053
+ end
1054
+ end
1055
+
1056
+ end