staugaard-cloudmaster 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/VERSION.yml +4 -0
- data/bin/cloudmaster +45 -0
- data/lib/AWS/AWS.rb +3 -0
- data/lib/AWS/EC2.rb +14 -0
- data/lib/AWS/S3.rb +14 -0
- data/lib/AWS/SQS.rb +14 -0
- data/lib/AWS/SimpleDB.rb +14 -0
- data/lib/MockAWS/EC2.rb +119 -0
- data/lib/MockAWS/S3.rb +39 -0
- data/lib/MockAWS/SQS.rb +82 -0
- data/lib/MockAWS/SimpleDB.rb +46 -0
- data/lib/MockAWS/clock.rb +67 -0
- data/lib/OriginalAWS/AWS.rb +475 -0
- data/lib/OriginalAWS/EC2.rb +783 -0
- data/lib/OriginalAWS/S3.rb +559 -0
- data/lib/OriginalAWS/SQS.rb +159 -0
- data/lib/OriginalAWS/SimpleDB.rb +460 -0
- data/lib/RetryAWS/EC2.rb +88 -0
- data/lib/RetryAWS/S3.rb +77 -0
- data/lib/RetryAWS/SQS.rb +109 -0
- data/lib/RetryAWS/SimpleDB.rb +118 -0
- data/lib/SafeAWS/EC2.rb +63 -0
- data/lib/SafeAWS/S3.rb +56 -0
- data/lib/SafeAWS/SQS.rb +75 -0
- data/lib/SafeAWS/SimpleDB.rb +88 -0
- data/lib/aws_context.rb +165 -0
- data/lib/basic_configuration.rb +120 -0
- data/lib/clock.rb +10 -0
- data/lib/factory.rb +14 -0
- data/lib/file_logger.rb +36 -0
- data/lib/inifile.rb +148 -0
- data/lib/instance_logger.rb +25 -0
- data/lib/logger_factory.rb +38 -0
- data/lib/periodic.rb +29 -0
- data/lib/string_logger.rb +29 -0
- data/lib/sys_logger.rb +40 -0
- data/lib/user_data.rb +30 -0
- data/test/aws-config.ini +9 -0
- data/test/cloudmaster-tests.rb +329 -0
- data/test/configuration-test.rb +62 -0
- data/test/daytime-policy-tests.rb +47 -0
- data/test/enumerator-test.rb +47 -0
- data/test/fixed-policy-tests.rb +50 -0
- data/test/instance-pool-test.rb +359 -0
- data/test/instance-test.rb +98 -0
- data/test/job-policy-test.rb +95 -0
- data/test/manual-policy-tests.rb +63 -0
- data/test/named-queue-test.rb +90 -0
- data/test/resource-policy-tests.rb +126 -0
- data/test/suite +17 -0
- data/test/test-config.ini +47 -0
- metadata +111 -0
@@ -0,0 +1,559 @@
|
|
1
|
+
# Sample Ruby code for the O'Reilly book "Programming Amazon Web
|
2
|
+
# Services" by James Murty.
|
3
|
+
#
|
4
|
+
# This code was written for Ruby version 1.8.6 or greater.
|
5
|
+
#
|
6
|
+
# The S3 module implements the REST API of the Amazon Simple Storage Service.
|
7
|
+
require 'AWS'
|
8
|
+
require 'digest/md5'
|
9
|
+
|
10
|
+
class S3
|
11
|
+
include AWS # Include the AWS module as a mixin
|
12
|
+
|
13
|
+
S3_ENDPOINT = "s3.amazonaws.com"
|
14
|
+
XMLNS = 'http://s3.amazonaws.com/doc/2006-03-01/'
|
15
|
+
|
16
|
+
def valid_dns_name(bucket_name)
|
17
|
+
if bucket_name.size > 63 or bucket_name.size < 3
|
18
|
+
return false
|
19
|
+
end
|
20
|
+
|
21
|
+
return false unless bucket_name =~ /^[a-z0-9][a-z0-9.-]+$/
|
22
|
+
|
23
|
+
return false unless bucket_name =~ /[a-z]/ # Cannot be an IP address
|
24
|
+
|
25
|
+
bucket_name.split('.').each do |fragment|
|
26
|
+
return false if fragment =~ /^-/ or fragment =~ /-$/ or fragment =~ /^$/
|
27
|
+
end
|
28
|
+
|
29
|
+
return true
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def generate_s3_uri(bucket_name='', object_name='', params=[])
|
34
|
+
# Decide between the default and sub-domain host name formats
|
35
|
+
if valid_dns_name(bucket_name)
|
36
|
+
hostname = bucket_name + "." + S3_ENDPOINT
|
37
|
+
else
|
38
|
+
hostname = S3_ENDPOINT
|
39
|
+
end
|
40
|
+
|
41
|
+
# Build an initial secure or non-secure URI for the end point.
|
42
|
+
request_uri = (@secure_http ? "https://" : "http://") + hostname;
|
43
|
+
|
44
|
+
# Include the bucket name in the URI except for alternative hostnames
|
45
|
+
if hostname == S3_ENDPOINT
|
46
|
+
request_uri << '/' + URI.escape(bucket_name) if bucket_name != ''
|
47
|
+
end
|
48
|
+
|
49
|
+
# Add object name component to URI if present
|
50
|
+
request_uri << '/' + URI.escape(object_name) if object_name != ''
|
51
|
+
|
52
|
+
# Add request parameters to the URI. Each item in the params variable
|
53
|
+
# is a hash dictionary containing multiple keys.
|
54
|
+
query = ""
|
55
|
+
params.each do |hash|
|
56
|
+
hash.each do |name, value|
|
57
|
+
query << '&' if query.length > 0
|
58
|
+
|
59
|
+
if value.nil?
|
60
|
+
query << "#{name}"
|
61
|
+
else
|
62
|
+
query << "#{name}=#{CGI::escape(value.to_s)}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
request_uri << "?" + query if query.length > 0
|
67
|
+
|
68
|
+
return URI.parse(request_uri)
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
def get_owner_id
|
73
|
+
uri = generate_s3_uri()
|
74
|
+
response = do_rest('GET', uri)
|
75
|
+
buckets = []
|
76
|
+
|
77
|
+
xml_doc = REXML::Document.new(response.body)
|
78
|
+
xml_doc.elements['//Owner/ID'].text
|
79
|
+
end
|
80
|
+
|
81
|
+
def list_buckets
|
82
|
+
uri = generate_s3_uri()
|
83
|
+
response = do_rest('GET', uri)
|
84
|
+
buckets = []
|
85
|
+
|
86
|
+
xml_doc = REXML::Document.new(response.body)
|
87
|
+
|
88
|
+
xml_doc.elements.each('//Buckets/Bucket') do |bucket|
|
89
|
+
buckets << {
|
90
|
+
:name => bucket.elements['Name'].text,
|
91
|
+
:creation_date => bucket.elements['CreationDate'].text
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
return {
|
96
|
+
:owner_id => xml_doc.elements['//Owner/ID'].text,
|
97
|
+
:display_name => xml_doc.elements['//Owner/DisplayName'].text,
|
98
|
+
:buckets => buckets
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
def create_bucket(bucket_name, location=nil)
|
103
|
+
uri = generate_s3_uri(bucket_name)
|
104
|
+
|
105
|
+
if location
|
106
|
+
xml_doc = REXML::Document.new("<CreateBucketConfiguration/>")
|
107
|
+
xml_doc.root.add_attribute('xmlns', XMLNS)
|
108
|
+
xml_doc.root.add_element('LocationConstraint').text = location
|
109
|
+
do_rest('PUT', uri, xml_doc.to_s, {'Content-Type'=>'text/xml'})
|
110
|
+
else
|
111
|
+
do_rest('PUT', uri)
|
112
|
+
end
|
113
|
+
|
114
|
+
return true
|
115
|
+
end
|
116
|
+
|
117
|
+
def delete_bucket(bucket_name)
|
118
|
+
uri = generate_s3_uri(bucket_name)
|
119
|
+
do_rest('DELETE', uri)
|
120
|
+
return true
|
121
|
+
end
|
122
|
+
|
123
|
+
def get_bucket_location(bucket_name)
|
124
|
+
uri = generate_s3_uri(bucket_name, '', [:location=>nil])
|
125
|
+
response = do_rest('GET', uri)
|
126
|
+
|
127
|
+
xml_doc = REXML::Document.new(response.body)
|
128
|
+
return xml_doc.elements['LocationConstraint'].text
|
129
|
+
end
|
130
|
+
|
131
|
+
def list_objects(bucket_name, *params)
|
132
|
+
is_truncated = true
|
133
|
+
|
134
|
+
objects = []
|
135
|
+
prefixes = []
|
136
|
+
|
137
|
+
while is_truncated
|
138
|
+
uri = generate_s3_uri(bucket_name, '', params)
|
139
|
+
response = do_rest('GET', uri)
|
140
|
+
|
141
|
+
xml_doc = REXML::Document.new(response.body)
|
142
|
+
|
143
|
+
xml_doc.elements.each('//Contents') do |contents|
|
144
|
+
objects << {
|
145
|
+
:key => contents.elements['Key'].text,
|
146
|
+
:size => contents.elements['Size'].text,
|
147
|
+
:last_modified => contents.elements['LastModified'].text,
|
148
|
+
:etag => contents.elements['ETag'].text,
|
149
|
+
:owner_id => contents.elements['Owner/ID'].text,
|
150
|
+
:owner_name => contents.elements['Owner/DisplayName'].text
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
cps = xml_doc.elements.to_a('//CommonPrefixes')
|
155
|
+
if cps.length > 0
|
156
|
+
cps.each do |cp|
|
157
|
+
prefixes << cp.elements['Prefix'].text
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Determine whether listing is truncated
|
162
|
+
is_truncated = 'true' == xml_doc.elements['//IsTruncated'].text
|
163
|
+
|
164
|
+
# Remove any existing marker value
|
165
|
+
params.delete_if {|p| p[:marker]}
|
166
|
+
|
167
|
+
# Set the marker parameter to the NextMarker if possible,
|
168
|
+
# otherwise set it to the last key name in the listing
|
169
|
+
next_marker_elem = xml_doc.elements['//NextMarker']
|
170
|
+
last_key_elem = xml_doc.elements['//Contents/Key[last()]']
|
171
|
+
|
172
|
+
if next_marker_elem
|
173
|
+
params << {:marker => next_marker_elem.text}
|
174
|
+
elsif last_key_elem
|
175
|
+
params << {:marker => last_key_elem.text}
|
176
|
+
else
|
177
|
+
params << {:marker => ''}
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
|
182
|
+
return {
|
183
|
+
:bucket_name => bucket_name,
|
184
|
+
:objects => objects,
|
185
|
+
:prefixes => prefixes
|
186
|
+
}
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
def create_object(bucket_name, object_key, opts={})
|
191
|
+
# Initialize local variables for the provided option items
|
192
|
+
data = (opts[:data] ? opts[:data] : '')
|
193
|
+
headers = (opts[:headers] ? opts[:headers].clone : {})
|
194
|
+
metadata = (opts[:metadata] ? opts[:metadata].clone : {})
|
195
|
+
|
196
|
+
# The Content-Length header must always be set when data is uploaded.
|
197
|
+
headers['Content-Length'] =
|
198
|
+
(data.respond_to?(:stat) ? data.stat.size : data.size).to_s
|
199
|
+
|
200
|
+
# Calculate an md5 hash of the data for upload verification
|
201
|
+
if data.respond_to?(:stat)
|
202
|
+
# Generate MD5 digest from file data one chunk at a time
|
203
|
+
md5_digest = Digest::MD5.new
|
204
|
+
File.open(data.path, 'rb') do |io|
|
205
|
+
buffer = ''
|
206
|
+
md5_digest.update(buffer) while io.read(4096, buffer)
|
207
|
+
end
|
208
|
+
md5_hash = md5_digest.digest
|
209
|
+
else
|
210
|
+
md5_hash = Digest::MD5.digest(data)
|
211
|
+
end
|
212
|
+
headers['Content-MD5'] = encode_base64(md5_hash)
|
213
|
+
|
214
|
+
# Set the canned policy, may be: 'private', 'public-read',
|
215
|
+
# 'public-read-write', 'authenticated-read'
|
216
|
+
headers['x-amz-acl'] = opts[:policy] if opts[:policy]
|
217
|
+
|
218
|
+
# Set an explicit content type if none is provided, otherwise the
|
219
|
+
# ruby HTTP library will use its own default type
|
220
|
+
# 'application/x-www-form-urlencoded'
|
221
|
+
if not headers['Content-Type']
|
222
|
+
headers['Content-Type'] =
|
223
|
+
data.respond_to?(:to_str) ? 'text/plain' : 'application/octet-stream'
|
224
|
+
end
|
225
|
+
|
226
|
+
# Convert metadata items to headers using the
|
227
|
+
# S3 metadata header name prefix.
|
228
|
+
metadata.each do |n,v|
|
229
|
+
headers["x-amz-meta-#{n}"] = v
|
230
|
+
end
|
231
|
+
|
232
|
+
uri = generate_s3_uri(bucket_name, object_key)
|
233
|
+
do_rest('PUT', uri, data, headers)
|
234
|
+
return true
|
235
|
+
end
|
236
|
+
|
237
|
+
# The copy object feature was added to the S3 API after the release of
|
238
|
+
# "Programming Amazon Web Services" so it is not discussed in the book's
|
239
|
+
# text. For more details, see:
|
240
|
+
# http://www.jamesmurty.com/2008/05/06/s3-copy-object-in-beta/
|
241
|
+
def copy_object(source_bucket_name, source_object_key,
|
242
|
+
dest_bucket_name, dest_object_key, acl=nil, new_metadata=nil)
|
243
|
+
|
244
|
+
headers = {}
|
245
|
+
|
246
|
+
# Identify the source object
|
247
|
+
headers['x-amz-copy-source'] = CGI::escape(
|
248
|
+
source_bucket_name + '/' + source_object_key)
|
249
|
+
|
250
|
+
# Copy metadata from original object, or replace the metadata.
|
251
|
+
if new_metadata.nil?
|
252
|
+
headers['x-amz-metadata-directive'] = 'COPY'
|
253
|
+
else
|
254
|
+
headers['x-amz-metadata-directive'] = 'REPLACE'
|
255
|
+
headers.merge!(new_metadata)
|
256
|
+
end
|
257
|
+
|
258
|
+
# The Content-Length header must always be set when data is uploaded.
|
259
|
+
headers['Content-Length'] = '0'
|
260
|
+
|
261
|
+
# Set the canned policy, may be: 'private', 'public-read',
|
262
|
+
# 'public-read-write', 'authenticated-read'
|
263
|
+
headers['x-amz-acl'] = acl if acl
|
264
|
+
|
265
|
+
uri = generate_s3_uri(dest_bucket_name, dest_object_key)
|
266
|
+
do_rest('PUT', uri, nil, headers)
|
267
|
+
return true
|
268
|
+
end
|
269
|
+
|
270
|
+
def delete_object(bucket_name, object_key)
|
271
|
+
uri = generate_s3_uri(bucket_name, object_key)
|
272
|
+
do_rest('DELETE', uri)
|
273
|
+
return true
|
274
|
+
end
|
275
|
+
|
276
|
+
def get_object_metadata(bucket_name, object_key, headers={})
|
277
|
+
uri = generate_s3_uri(bucket_name, object_key)
|
278
|
+
response = do_rest('HEAD', uri, nil, headers)
|
279
|
+
|
280
|
+
response_headers = {}
|
281
|
+
metadata = {}
|
282
|
+
|
283
|
+
response.each_header do |name,value|
|
284
|
+
if name.index('x-amz-meta-') == 0
|
285
|
+
metadata[name['x-amz-meta-'.length..-1]] = value
|
286
|
+
else
|
287
|
+
response_headers[name] = value
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
return {
|
292
|
+
:metadata => metadata,
|
293
|
+
:headers => response_headers
|
294
|
+
}
|
295
|
+
end
|
296
|
+
|
297
|
+
def get_object(bucket_name, object_key, headers={})
|
298
|
+
uri = generate_s3_uri(bucket_name, object_key)
|
299
|
+
|
300
|
+
if block_given?
|
301
|
+
response = do_rest('GET', uri, nil, headers) {|segment| yield(segment)}
|
302
|
+
else
|
303
|
+
response = do_rest('GET', uri, nil, headers)
|
304
|
+
end
|
305
|
+
|
306
|
+
response_headers = {}
|
307
|
+
metadata = {}
|
308
|
+
|
309
|
+
response.each_header do |name,value|
|
310
|
+
if name.index('x-amz-meta-') == 0
|
311
|
+
metadata[name['x-amz-meta-'.length..-1]] = value
|
312
|
+
else
|
313
|
+
response_headers[name] = value
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
result = {
|
318
|
+
:metadata => metadata,
|
319
|
+
:headers => response_headers
|
320
|
+
}
|
321
|
+
result[:body] = response.body if not block_given?
|
322
|
+
|
323
|
+
return result
|
324
|
+
end
|
325
|
+
|
326
|
+
def get_logging(bucket_name)
|
327
|
+
uri = generate_s3_uri(bucket_name, '', [:logging=>nil])
|
328
|
+
response = do_rest('GET', uri)
|
329
|
+
|
330
|
+
xml_doc = REXML::Document.new(response.body)
|
331
|
+
|
332
|
+
if xml_doc.elements['//LoggingEnabled']
|
333
|
+
return {
|
334
|
+
:target_bucket => xml_doc.elements['//TargetBucket'].text,
|
335
|
+
:target_prefix => xml_doc.elements['//TargetPrefix'].text
|
336
|
+
}
|
337
|
+
else
|
338
|
+
# Logging is not enabled
|
339
|
+
return nil
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
def set_logging(bucket_name, target_bucket=nil,
|
344
|
+
target_prefix="#{bucket_name}.")
|
345
|
+
|
346
|
+
# Build BucketLoggingStatus XML document
|
347
|
+
xml_doc = REXML::Document.new("<BucketLoggingStatus xmlns='#{XMLNS}'/>")
|
348
|
+
|
349
|
+
if target_bucket
|
350
|
+
logging_enabled = xml_doc.root.add_element('LoggingEnabled')
|
351
|
+
logging_enabled.add_element('TargetBucket').text = target_bucket
|
352
|
+
logging_enabled.add_element('TargetPrefix').text = target_prefix
|
353
|
+
end
|
354
|
+
|
355
|
+
uri = generate_s3_uri(bucket_name, '', [:logging=>nil])
|
356
|
+
do_rest('PUT', uri, xml_doc.to_s, {'Content-Type'=>'application/xml'})
|
357
|
+
return true
|
358
|
+
end
|
359
|
+
|
360
|
+
def get_acl(bucket_name, object_key='')
|
361
|
+
uri = generate_s3_uri(bucket_name, object_key, [:acl=>nil])
|
362
|
+
response = do_rest('GET', uri)
|
363
|
+
|
364
|
+
xml_doc = REXML::Document.new(response.body)
|
365
|
+
|
366
|
+
grants = []
|
367
|
+
|
368
|
+
xml_doc.elements.each('//Grant') do |grant|
|
369
|
+
grantee = {}
|
370
|
+
|
371
|
+
grantee[:type] = grant.elements['Grantee'].attributes['type']
|
372
|
+
|
373
|
+
if grantee[:type] == 'Group'
|
374
|
+
grantee[:uri] = grant.elements['Grantee/URI'].text
|
375
|
+
else
|
376
|
+
grantee[:id] = grant.elements['Grantee/ID'].text
|
377
|
+
grantee[:display_name] = grant.elements['Grantee/DisplayName'].text
|
378
|
+
end
|
379
|
+
|
380
|
+
grants << {
|
381
|
+
:grantee => grantee,
|
382
|
+
:permission => grant.elements['Permission'].text
|
383
|
+
}
|
384
|
+
end
|
385
|
+
|
386
|
+
return {
|
387
|
+
:owner_id => xml_doc.elements['//Owner/ID'].text,
|
388
|
+
:owner_name => xml_doc.elements['//Owner/DisplayName'].text,
|
389
|
+
:grants => grants
|
390
|
+
}
|
391
|
+
end
|
392
|
+
|
393
|
+
def set_acl(owner_id, bucket_name, object_key='',
|
394
|
+
grants=[owner_id=>'FULL_CONTROL'])
|
395
|
+
|
396
|
+
xml_doc = REXML::Document.new("<AccessControlPolicy xmlns='#{XMLNS}'/>")
|
397
|
+
xml_doc.root.add_element('Owner').add_element('ID').text = owner_id
|
398
|
+
grant_list = xml_doc.root.add_element('AccessControlList')
|
399
|
+
|
400
|
+
grants.each do |hash|
|
401
|
+
hash.each do |grantee_id, permission|
|
402
|
+
|
403
|
+
grant = grant_list.add_element('Grant')
|
404
|
+
grant.add_element('Permission').text = permission
|
405
|
+
|
406
|
+
# Grantee may be of type email, group, or canonical user
|
407
|
+
if grantee_id.index('@')
|
408
|
+
# Email grantee
|
409
|
+
grantee = grant.add_element('Grantee',
|
410
|
+
{'xmlns:xsi'=>'http://www.w3.org/2001/XMLSchema-instance',
|
411
|
+
'xsi:type'=>'AmazonCustomerByEmail'})
|
412
|
+
grantee.add_element('EmailAddress').text = grantee_id
|
413
|
+
elsif grantee_id.index('://')
|
414
|
+
# Group grantee
|
415
|
+
grantee = grant.add_element('Grantee',
|
416
|
+
{'xmlns:xsi'=>'http://www.w3.org/2001/XMLSchema-instance',
|
417
|
+
'xsi:type'=>'Group'})
|
418
|
+
grantee.add_element('URI').text = grantee_id
|
419
|
+
else
|
420
|
+
# Canonical user grantee
|
421
|
+
grantee = grant.add_element('Grantee',
|
422
|
+
{'xmlns:xsi'=>'http://www.w3.org/2001/XMLSchema-instance',
|
423
|
+
'xsi:type'=>'CanonicalUser'})
|
424
|
+
grantee.add_element('ID').text = grantee_id
|
425
|
+
end
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
uri = generate_s3_uri(bucket_name, object_key, [:acl=>nil])
|
430
|
+
do_rest('PUT', uri, xml_doc.to_s, {'Content-Type'=>'application/xml'})
|
431
|
+
return true
|
432
|
+
end
|
433
|
+
|
434
|
+
def set_canned_acl(canned_acl, bucket_name, object_key='')
|
435
|
+
uri = generate_s3_uri(bucket_name, object_key, [:acl=>nil])
|
436
|
+
response = do_rest('PUT', uri, nil, {'x-amz-acl'=>canned_acl})
|
437
|
+
return true
|
438
|
+
end
|
439
|
+
|
440
|
+
def get_torrent(bucket_name, object_key, output)
|
441
|
+
uri = generate_s3_uri(bucket_name, object_key, [:torrent=>nil])
|
442
|
+
response = do_rest('GET', uri)
|
443
|
+
output.write(response.body)
|
444
|
+
end
|
445
|
+
|
446
|
+
def sign_uri(method, expires, bucket_name, object_key='', opts={})
|
447
|
+
parameters = opts[:parameters] || []
|
448
|
+
headers = opts[:headers] || {}
|
449
|
+
|
450
|
+
headers['Date'] = expires
|
451
|
+
|
452
|
+
uri = generate_s3_uri(bucket_name, object_key, parameters)
|
453
|
+
signature = generate_rest_signature(method, uri, headers)
|
454
|
+
|
455
|
+
uri.query = (uri.query.nil? ? '' : "#{uri.query}&")
|
456
|
+
uri.query << "Signature=" + CGI::escape(signature)
|
457
|
+
uri.query << "&Expires=" + expires.to_s
|
458
|
+
uri.query << "&AWSAccessKeyId=" + @aws_access_key
|
459
|
+
|
460
|
+
uri.host = bucket_name if opts[:is_virtual_host]
|
461
|
+
|
462
|
+
return uri.to_s
|
463
|
+
end
|
464
|
+
|
465
|
+
|
466
|
+
def build_post_policy(expiration_time, conditions)
|
467
|
+
if expiration_time.nil? or not expiration_time.respond_to?(:getutc)
|
468
|
+
raise 'Policy document must include a valid expiration Time object'
|
469
|
+
end
|
470
|
+
if conditions.nil? or not conditions.class == Hash
|
471
|
+
raise 'Policy document must include a valid conditions Hash object'
|
472
|
+
end
|
473
|
+
|
474
|
+
# Convert conditions object mappings to condition statements
|
475
|
+
conds = []
|
476
|
+
conditions.each_pair do |name,test|
|
477
|
+
if test.nil?
|
478
|
+
# A nil condition value means allow anything.
|
479
|
+
conds << %{["starts-with", "$#{name}", ""]}
|
480
|
+
elsif test.is_a? String
|
481
|
+
conds << %{{"#{name}": "#{test}"}}
|
482
|
+
elsif test.is_a? Array
|
483
|
+
conds << %{{"#{name}": "#{test.join(',')}"}}
|
484
|
+
elsif test.is_a? Hash
|
485
|
+
operation = test[:op]
|
486
|
+
value = test[:value]
|
487
|
+
conds << %{["#{operation}", "$#{name}", "#{value}"]}
|
488
|
+
elsif test.is_a? Range
|
489
|
+
conds << %{["#{name}", #{test.begin}, #{test.end}]}
|
490
|
+
else
|
491
|
+
raise "Unexpected value type for condition '#{name}': #{test.class}"
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
return %{{"expiration": "#{expiration_time.getutc.iso8601}",
|
496
|
+
"conditions": [#{conds.join(",")}]}}
|
497
|
+
end
|
498
|
+
|
499
|
+
def build_post_form(bucket_name, key, options={})
|
500
|
+
fields = []
|
501
|
+
|
502
|
+
# Form is only authenticated if a policy is specified.
|
503
|
+
if options[:expiration] or options[:conditions]
|
504
|
+
# Generate policy document
|
505
|
+
policy = build_post_policy(options[:expiration], options[:conditions])
|
506
|
+
puts "POST Policy\n===========\n#{policy}\n\n" if @debug
|
507
|
+
|
508
|
+
# Add the base64-encoded policy document as the 'policy' field
|
509
|
+
policy_b64 = encode_base64(policy)
|
510
|
+
fields << %{<input type="hidden" name="policy" value="#{policy_b64}">}
|
511
|
+
|
512
|
+
# Add the AWS access key as the 'AWSAccessKeyId' field
|
513
|
+
fields << %{<input type="hidden" name="AWSAccessKeyId"
|
514
|
+
value="#{@aws_access_key}">}
|
515
|
+
|
516
|
+
# Add signature for encoded policy document as the 'AWSAccessKeyId' field
|
517
|
+
signature = generate_signature(policy_b64)
|
518
|
+
fields << %{<input type="hidden" name="signature" value="#{signature}">}
|
519
|
+
end
|
520
|
+
|
521
|
+
# Include any additional fields
|
522
|
+
options[:fields].each_pair do |n,v|
|
523
|
+
if v.nil?
|
524
|
+
# Allow users to provide their own <input> fields as text.
|
525
|
+
fields << n
|
526
|
+
else
|
527
|
+
fields << %{<input type="hidden" name="#{n}" value="#{v}">}
|
528
|
+
end
|
529
|
+
end if options[:fields]
|
530
|
+
|
531
|
+
# Add the vital 'file' input item, which may be a textarea or file.
|
532
|
+
if options[:text_input]
|
533
|
+
# Use the text_input option which should specify a textarea or text
|
534
|
+
# input field. For example:
|
535
|
+
# '<textarea name="file" cols="80" rows="5">Default Text</textarea>'
|
536
|
+
fields << options[:text_input]
|
537
|
+
else
|
538
|
+
fields << %{<input name="file" type="file">}
|
539
|
+
end
|
540
|
+
|
541
|
+
# Construct a sub-domain URL to refer to the target bucket. The
|
542
|
+
# HTTPS protocol will be used if the secure HTTP option is enabled.
|
543
|
+
url = "http#{@secure_http ? 's' : ''}://#{bucket_name}.s3.amazonaws.com/"
|
544
|
+
|
545
|
+
# Construct the entire form.
|
546
|
+
form = %{
|
547
|
+
<form action="#{url}" method="post" enctype="multipart/form-data">
|
548
|
+
<input type="hidden" name="key" value="#{key}">
|
549
|
+
#{fields.join("\n")}
|
550
|
+
<br>
|
551
|
+
<input type="submit" value="Upload to Amazon S3">
|
552
|
+
</form>
|
553
|
+
}
|
554
|
+
puts "POST Form\n=========\n#{form}\n" if @debug
|
555
|
+
|
556
|
+
return form
|
557
|
+
end
|
558
|
+
|
559
|
+
end
|