sdb_dal 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/sdb_dal/s3.rb ADDED
@@ -0,0 +1,594 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This software code is made available "AS IS" without warranties of any
4
+ # kind. You may copy, display, modify and redistribute the software
5
+ # code either by itself or as incorporated into your code; provided that
6
+ # you do not remove any proprietary notices. Your use of this software
7
+ # code is at your own risk and you waive any claim against Amazon
8
+ # Digital Services, Inc. or its affiliates with respect to your use of
9
+ # this software code. (c) 2006 Amazon Digital Services, Inc. or its
10
+ # affiliates.
11
+
12
+ # Note that this is the Amazon S3 sample modified for DevPay. You can diff
13
+ # this file with the Amazon S3 Ruby library to see the DevPay modifications.
14
+
15
+ require 'base64'
16
+ require 'cgi'
17
+ require 'openssl'
18
+ require 'digest/sha1'
19
+ require 'net/https'
20
+ require 'rexml/document'
21
+ require 'time'
22
+
23
+ # this wasn't added until v 1.8.3
24
+ if (RUBY_VERSION < '1.8.3')
25
+ class Net::HTTP::Delete < Net::HTTPRequest
26
+ METHOD = 'DELETE'
27
+ REQUEST_HAS_BODY = false
28
+ RESPONSE_HAS_BODY = true
29
+ end
30
+ end
31
+ module SdbDal
32
+
33
+ # this module has two big classes: AWSAuthConnection and
34
+ # QueryStringAuthGenerator. both use identical apis, but the first actually
35
+ # performs the operation, while the second simply outputs urls with the
36
+ # appropriate authentication query string parameters, which could be used
37
+ # in another tool (such as your web browser for GETs).
38
+ module S3
39
+ DEFAULT_HOST = 's3.amazonaws.com'
40
+ PORTS_BY_SECURITY = { true => 443, false => 80 }
41
+ METADATA_PREFIX = 'x-amz-meta-'
42
+ AMAZON_HEADER_PREFIX = 'x-amz-'
43
+ AMAZON_TOKEN_HEADER_PREFIX = "x-amz-security-token"
44
+
45
+ # builds the canonical string for signing.
46
+ def S3.canonical_string(method, bucket="", path="", path_args={}, headers={}, expires=nil)
47
+ interesting_headers = {}
48
+ headers.each do |key, value|
49
+ lk = key.downcase
50
+ if (lk == 'content-md5' or
51
+ lk == 'content-type' or
52
+ lk == 'date' or
53
+ lk =~ /^#{AMAZON_HEADER_PREFIX}/o)
54
+ interesting_headers[lk] = value.to_s.strip
55
+ end
56
+ end
57
+
58
+ # these fields get empty strings if they don't exist.
59
+ interesting_headers['content-type'] ||= ''
60
+ interesting_headers['content-md5'] ||= ''
61
+
62
+ # just in case someone used this. it's not necessary in this lib.
63
+ if interesting_headers.has_key? 'x-amz-date'
64
+ interesting_headers['date'] = ''
65
+ end
66
+
67
+ # if you're using expires for query string auth, then it trumps date
68
+ # (and x-amz-date)
69
+ if not expires.nil?
70
+ interesting_headers['date'] = expires
71
+ end
72
+
73
+ buf = "#{method}\n"
74
+ interesting_headers.sort { |a, b| a[0] <=> b[0] }.each do |key, value|
75
+ if key =~ /^#{AMAZON_HEADER_PREFIX}/o
76
+ buf << "#{key}:#{value}\n"
77
+ else
78
+ buf << "#{value}\n"
79
+ end
80
+ end
81
+
82
+ # build the path using the bucket and key
83
+ if not bucket.empty?
84
+ buf << "/#{bucket}"
85
+ end
86
+ # append the key (it might be empty string)
87
+ # append a slash regardless
88
+ buf << "/#{path}"
89
+
90
+ # if there is an acl, logging, or torrent parameter
91
+ # add them to the string
92
+ if path_args.has_key?('acl')
93
+ buf << '?acl'
94
+ elsif path_args.has_key?('torrent')
95
+ buf << '?torrent'
96
+ elsif path_args.has_key?('logging')
97
+ buf << '?logging'
98
+ end
99
+
100
+ return buf
101
+ end
102
+
103
+ # encodes the given string with the aws_secret_access_key, by taking the
104
+ # hmac-sha1 sum, and then base64 encoding it. optionally, it will also
105
+ # url encode the result of that to protect the string if it's going to
106
+ # be used as a query string parameter.
107
+ def S3.encode(aws_secret_access_key, str, urlencode=false)
108
+ digest = OpenSSL::Digest::Digest.new('sha1')
109
+ b64_hmac =
110
+ Base64.encode64(
111
+ OpenSSL::HMAC.digest(digest, aws_secret_access_key, str)).strip
112
+
113
+ if urlencode
114
+ return CGI::escape(b64_hmac)
115
+ else
116
+ return b64_hmac
117
+ end
118
+ end
119
+
120
+ # build the path_argument string
121
+ def S3.path_args_hash_to_string(path_args={})
122
+ arg_string = ''
123
+ path_args.each { |k, v|
124
+ arg_string << k.to_s
125
+ if not v.nil?
126
+ arg_string << "=#{CGI::escape(v)}"
127
+ end
128
+ arg_string << '&'
129
+ }
130
+ return arg_string
131
+ end
132
+
133
+
134
+ # uses Net::HTTP to interface with S3. note that this interface should only
135
+ # be used for smaller objects, as it does not stream the data. if you were
136
+ # to download a 1gb file, it would require 1gb of memory. also, this class
137
+ # creates a new http connection each time. it would be greatly improved with
138
+ # some connection pooling.
139
+ class AWSAuthConnection
140
+ attr_accessor :calling_format
141
+
142
+ def initialize(aws_access_key_id,
143
+ aws_secret_access_key,
144
+ tokens=Array.new,
145
+ is_secure=true,
146
+ server=DEFAULT_HOST,
147
+ port=PORTS_BY_SECURITY[is_secure],
148
+ calling_format=CallingFormat::SUBDOMAIN)
149
+ @aws_access_key_id = aws_access_key_id
150
+ @aws_secret_access_key = aws_secret_access_key
151
+ @server = server
152
+ @is_secure = is_secure
153
+ @calling_format = calling_format
154
+ @port = port
155
+ @init_headers = {}
156
+ if tokens && !tokens.empty?
157
+ @init_headers[AMAZON_TOKEN_HEADER_PREFIX] = tokens.join(',')
158
+ end
159
+ end
160
+
161
+ def create_bucket(bucket, headers={})
162
+ return Response.new(make_request('PUT', bucket, '', {}, headers))
163
+ end
164
+
165
+ # takes options :prefix, :marker, :max_keys, and :delimiter
166
+ def list_bucket(bucket, options={}, headers={})
167
+ path_args = {}
168
+ options.each { |k, v|
169
+ path_args[k] = v.to_s
170
+ }
171
+
172
+ return ListBucketResponse.new(make_request('GET', bucket, '', path_args, headers))
173
+ end
174
+
175
+ def delete_bucket(bucket, headers={})
176
+ return Response.new(make_request('DELETE', bucket, '', {}, headers))
177
+ end
178
+
179
+ def put(bucket, key, object, headers={})
180
+ object = S3Object.new(object) if not object.instance_of? S3Object
181
+
182
+ return Response.new(
183
+ make_request('PUT', bucket, CGI::escape(key), {}, headers, object.data, object.metadata)
184
+ )
185
+ end
186
+ def copy(source_bucket, source_key, destination_bucket,destination_key, headers={})
187
+ headers['x-amz-copy-source']="#{source_bucket}/#{source_key}"
188
+ headers['x-amz-metadata-directive']="REPLACE "
189
+ return GetResponse.new(make_request('PUT', destination_bucket, CGI::escape(destination_key),{}, headers))
190
+ end
191
+
192
+ def get_head(bucket, key, headers={})
193
+ return GetResponse.new(make_request('HEAD',bucket, CGI::escape(key),{}, headers))
194
+ end
195
+ def get_content_type(bucket, key, headers={})
196
+ response= get_head(bucket, key, headers)
197
+ if response.http_response.code=='404'
198
+ return nil
199
+
200
+ elsif response.http_response.code=='200'
201
+ return response.http_response.header.content_type
202
+ end
203
+ raise response.http_response.code
204
+ end
205
+
206
+
207
+ def get(bucket, key, headers={})
208
+ return GetResponse.new(make_request('GET', bucket, CGI::escape(key), {}, headers))
209
+ end
210
+
211
+ def delete(bucket, key, headers={})
212
+ return Response.new(make_request('DELETE', bucket, CGI::escape(key), {}, headers))
213
+ end
214
+
215
+ def get_bucket_logging(bucket, headers={})
216
+ return GetResponse.new(make_request('GET', bucket, '', {'logging' => nil}, headers))
217
+ end
218
+
219
+ def put_bucket_logging(bucket, logging_xml_doc, headers={})
220
+ return Response.new(make_request('PUT', bucket, '', {'logging' => nil}, headers, logging_xml_doc))
221
+ end
222
+
223
+ def get_bucket_acl(bucket, headers={})
224
+ return get_acl(bucket, '', headers)
225
+ end
226
+
227
+ # returns an xml document representing the access control list.
228
+ # this could be parsed into an object.
229
+ def get_acl(bucket, key, headers={})
230
+ return GetResponse.new(make_request('GET', bucket, CGI::escape(key), {'acl' => nil}, headers))
231
+ end
232
+
233
+ def put_bucket_acl(bucket, acl_xml_doc, headers={})
234
+ return put_acl(bucket, '', acl_xml_doc, headers)
235
+ end
236
+
237
+ # sets the access control policy for the given resource. acl_xml_doc must
238
+ # be a string in the acl xml format.
239
+ def put_acl(bucket, key, acl_xml_doc, headers={})
240
+ return Response.new(
241
+ make_request('PUT', bucket, CGI::escape(key), {'acl' => nil}, headers, acl_xml_doc, {})
242
+ )
243
+ end
244
+
245
+ def list_all_my_buckets(headers={})
246
+ return ListAllMyBucketsResponse.new(make_request('GET', '', '', {}, headers))
247
+ end
248
+
249
+ private
250
+ def make_request(method, bucket='', key='', path_args={}, headers={}, data='', metadata={})
251
+
252
+ # build the domain based on the calling format
253
+ server = ''
254
+ if bucket.empty?
255
+ # for a bucketless request (i.e. list all buckets)
256
+ # revert to regular domain case since this operation
257
+ # does not make sense for vanity domains
258
+ server = @server
259
+ elsif @calling_format == CallingFormat::SUBDOMAIN
260
+ server = "#{bucket}.#{@server}"
261
+ elsif @calling_format == CallingFormat::VANITY
262
+ server = bucket
263
+ else
264
+ server = @server
265
+ end
266
+
267
+ # build the path based on the calling format
268
+ path = ''
269
+ if (not bucket.empty?) and (@calling_format == CallingFormat::REGULAR)
270
+ path << "/#{bucket}"
271
+ end
272
+ # add the slash after the bucket regardless
273
+ # the key will be appended if it is non-empty
274
+ path << "/#{key}"
275
+
276
+ # build the path_argument string
277
+ # add the ? in all cases since
278
+ # signature and credentials follow path args
279
+ path << '?'
280
+ path << S3.path_args_hash_to_string(path_args)
281
+
282
+ http = Net::HTTP.new(server, @port)
283
+ http.use_ssl = @is_secure
284
+ http.start do
285
+ req = method_to_request_class(method).new("#{path}")
286
+
287
+ set_headers(req, @init_headers)
288
+ set_headers(req, headers)
289
+ set_headers(req, metadata, METADATA_PREFIX)
290
+
291
+ set_aws_auth_header(req, @aws_access_key_id, @aws_secret_access_key, bucket, key, path_args)
292
+ if req.request_body_permitted?
293
+ return http.request(req, data)
294
+ else
295
+ return http.request(req)
296
+ end
297
+ end
298
+
299
+ end
300
+
301
+ def method_to_request_class(method)
302
+ case method
303
+ when 'GET'
304
+ return Net::HTTP::Get
305
+ when 'PUT'
306
+ return Net::HTTP::Put
307
+ when 'DELETE'
308
+ return Net::HTTP::Delete
309
+ when 'HEAD'
310
+ return Net::HTTP::Head
311
+ else
312
+ raise "Unsupported method #{method}"
313
+ end
314
+ end
315
+
316
+ # set the Authorization header using AWS signed header authentication
317
+ def set_aws_auth_header(request, aws_access_key_id, aws_secret_access_key, bucket='', key='', path_args={})
318
+ # we want to fix the date here if it's not already been done.
319
+ request['Date'] ||= Time.now.httpdate
320
+
321
+ # ruby will automatically add a random content-type on some verbs, so
322
+ # here we add a dummy one to 'suppress' it. change this logic if having
323
+ # an empty content-type header becomes semantically meaningful for any
324
+ # other verb.
325
+ request['Content-Type'] ||= ''
326
+
327
+ canonical_string =
328
+ S3.canonical_string(request.method, bucket, key, path_args, request.to_hash, nil)
329
+ encoded_canonical = S3.encode(aws_secret_access_key, canonical_string)
330
+
331
+ request['Authorization'] = "AWS #{aws_access_key_id}:#{encoded_canonical}"
332
+ end
333
+
334
+ def set_headers(request, headers, prefix='')
335
+ headers.each do |key, value|
336
+ request[prefix + key] = value
337
+ end
338
+ end
339
+ end
340
+
341
+
342
+
343
+ class S3Object
344
+ attr_accessor :data
345
+ attr_accessor :metadata
346
+ def initialize(data, metadata={})
347
+ @data, @metadata = data, metadata
348
+ end
349
+ end
350
+
351
+ # class for storing calling format constants
352
+ module CallingFormat
353
+ REGULAR = 0 # http://s3.amazonaws.com/bucket/key
354
+ SUBDOMAIN = 1 # http://bucket.s3.amazonaws.com/key
355
+ VANITY = 2 # http://<vanity_domain>/key -- vanity_domain resolves to s3.amazonaws.com
356
+
357
+ # build the url based on the calling format, and bucket
358
+ def CallingFormat.build_url_base(protocol, server, port, bucket, format)
359
+ build_url_base = "#{protocol}://"
360
+ if bucket.empty?
361
+ build_url_base << "#{server}:#{port}"
362
+ elsif format == SUBDOMAIN
363
+ build_url_base << "#{bucket}.#{server}:#{port}"
364
+ elsif format == VANITY
365
+ build_url_base << "#{bucket}:#{port}"
366
+ else
367
+ build_url_base << "#{server}:#{port}/#{bucket}"
368
+ end
369
+ return build_url_base
370
+ end
371
+ end
372
+
373
+ class Owner
374
+ attr_accessor :id
375
+ attr_accessor :display_name
376
+ end
377
+
378
+ class ListEntry
379
+ attr_accessor :key
380
+ attr_accessor :last_modified
381
+ attr_accessor :etag
382
+ attr_accessor :size
383
+ attr_accessor :storage_class
384
+ attr_accessor :owner
385
+ end
386
+
387
+ class ListProperties
388
+ attr_accessor :name
389
+ attr_accessor :prefix
390
+ attr_accessor :marker
391
+ attr_accessor :max_keys
392
+ attr_accessor :delimiter
393
+ attr_accessor :is_truncated
394
+ attr_accessor :next_marker
395
+ end
396
+
397
+ class CommonPrefixEntry
398
+ attr_accessor :prefix
399
+ end
400
+
401
+ # Parses the list bucket output into a list of ListEntry objects, and
402
+ # a list of CommonPrefixEntry objects if applicable.
403
+ class ListBucketParser
404
+ attr_reader :properties
405
+ attr_reader :entries
406
+ attr_reader :common_prefixes
407
+
408
+ def initialize
409
+ reset
410
+ end
411
+
412
+ def tag_start(name, attributes)
413
+ if name == 'ListBucketResult'
414
+ @properties = ListProperties.new
415
+ elsif name == 'Contents'
416
+ @curr_entry = ListEntry.new
417
+ elsif name == 'Owner'
418
+ @curr_entry.owner = Owner.new
419
+ elsif name == 'CommonPrefixes'
420
+ @common_prefix_entry = CommonPrefixEntry.new
421
+ end
422
+ end
423
+
424
+ # we have one, add him to the entries list
425
+ def tag_end(name)
426
+ # this prefix is the one we echo back from the request
427
+ if name == 'Name'
428
+ @properties.name = @curr_text
429
+ elsif name == 'Prefix' and @is_echoed_prefix
430
+ @properties.prefix = @curr_text
431
+ @is_echoed_prefix = nil
432
+ elsif name == 'Marker'
433
+ @properties.marker = @curr_text
434
+ elsif name == 'MaxKeys'
435
+ @properties.max_keys = @curr_text.to_i
436
+ elsif name == 'Delimiter'
437
+ @properties.delimiter = @curr_text
438
+ elsif name == 'IsTruncated'
439
+ @properties.is_truncated = @curr_text == 'true'
440
+ elsif name == 'NextMarker'
441
+ @properties.next_marker = @curr_text
442
+ elsif name == 'Contents'
443
+ @entries << @curr_entry
444
+ elsif name == 'Key'
445
+ @curr_entry.key = @curr_text
446
+ elsif name == 'LastModified'
447
+ @curr_entry.last_modified = @curr_text
448
+ elsif name == 'ETag'
449
+ @curr_entry.etag = @curr_text
450
+ elsif name == 'Size'
451
+ @curr_entry.size = @curr_text.to_i
452
+ elsif name == 'StorageClass'
453
+ @curr_entry.storage_class = @curr_text
454
+ elsif name == 'ID'
455
+ @curr_entry.owner.id = @curr_text
456
+ elsif name == 'DisplayName'
457
+ @curr_entry.owner.display_name = @curr_text
458
+ elsif name == 'CommonPrefixes'
459
+ @common_prefixes << @common_prefix_entry
460
+ elsif name == 'Prefix'
461
+ # this is the common prefix for keys that match up to the delimiter
462
+ @common_prefix_entry.prefix = @curr_text
463
+ end
464
+ @curr_text = ''
465
+ end
466
+
467
+ def text(text)
468
+ @curr_text += text
469
+ end
470
+
471
+ def xmldecl(version, encoding, standalone)
472
+ # ignore
473
+ end
474
+
475
+ # get ready for another parse
476
+ def reset
477
+ @is_echoed_prefix = true;
478
+ @entries = []
479
+ @curr_entry = nil
480
+ @common_prefixes = []
481
+ @common_prefix_entry = nil
482
+ @curr_text = ''
483
+ end
484
+ end
485
+
486
+ class Bucket
487
+ attr_accessor :name
488
+ attr_accessor :creation_date
489
+ end
490
+
491
+ class ListAllMyBucketsParser
492
+ attr_reader :entries
493
+
494
+ def initialize
495
+ reset
496
+ end
497
+
498
+ def tag_start(name, attributes)
499
+ if name == 'Bucket'
500
+ @curr_bucket = Bucket.new
501
+ end
502
+ end
503
+
504
+ # we have one, add him to the entries list
505
+ def tag_end(name)
506
+ if name == 'Bucket'
507
+ @entries << @curr_bucket
508
+ elsif name == 'Name'
509
+ @curr_bucket.name = @curr_text
510
+ elsif name == 'CreationDate'
511
+ @curr_bucket.creation_date = @curr_text
512
+ end
513
+ @curr_text = ''
514
+ end
515
+
516
+ def text(text)
517
+ @curr_text += text
518
+ end
519
+
520
+ def xmldecl(version, encoding, standalone)
521
+ # ignore
522
+ end
523
+
524
+ # get ready for another parse
525
+ def reset
526
+ @entries = []
527
+ @owner = nil
528
+ @curr_bucket = nil
529
+ @curr_text = ''
530
+ end
531
+ end
532
+
533
+ class Response
534
+ attr_reader :http_response
535
+ def initialize(response)
536
+ @http_response = response
537
+ end
538
+ end
539
+
540
+ class GetResponse < Response
541
+ attr_reader :object
542
+ def initialize(response)
543
+ super(response)
544
+ metadata = get_aws_metadata(response)
545
+ data = response.body
546
+ @object = S3Object.new(data, metadata)
547
+ end
548
+
549
+ # parses the request headers and pulls out the s3 metadata into a hash
550
+ def get_aws_metadata(response)
551
+ metadata = {}
552
+ response.each do |key, value|
553
+ if key =~ /^#{METADATA_PREFIX}(.*)$/oi
554
+ metadata[$1] = value
555
+ end
556
+ end
557
+ return metadata
558
+ end
559
+ end
560
+
561
+ class ListBucketResponse < Response
562
+ attr_reader :properties
563
+ attr_reader :entries
564
+ attr_reader :common_prefix_entries
565
+
566
+ def initialize(response)
567
+ super(response)
568
+ if response.is_a? Net::HTTPSuccess
569
+ parser = ListBucketParser.new
570
+ REXML::Document.parse_stream(response.body, parser)
571
+ @properties = parser.properties
572
+ @entries = parser.entries
573
+ @common_prefix_entries = parser.common_prefixes
574
+ else
575
+ @entries = []
576
+ end
577
+ end
578
+ end
579
+
580
+ class ListAllMyBucketsResponse < Response
581
+ attr_reader :entries
582
+ def initialize(response)
583
+ super(response)
584
+ if response.is_a? Net::HTTPSuccess
585
+ parser = ListAllMyBucketsParser.new
586
+ REXML::Document.parse_stream(response.body, parser)
587
+ @entries = parser.entries
588
+ else
589
+ @entries = []
590
+ end
591
+ end
592
+ end
593
+ end
594
+ end
@@ -0,0 +1,119 @@
1
+ module SdbDal
2
+
3
+
4
+ module SdbFormatter
5
+ def parse_reference_set(value)
6
+ result=YAML::load(value)
7
+ return result if(result.class==Reference)
8
+ return
9
+ end
10
+
11
+ def parse_date(value)
12
+ return nil if value==nil
13
+ if value.is_a? Date
14
+ return value
15
+ end
16
+ if value.is_a? Time
17
+ return value
18
+ end
19
+ return nil if value.length==0
20
+ return Time.at(value.to_f)
21
+ end
22
+ def parse_boolean(value)
23
+ return nil if value==nil
24
+ if value=="true"
25
+ return true
26
+ else
27
+ return false
28
+ end
29
+ end
30
+ def parse_integer(value)
31
+ return nil if value==nil
32
+ return value.to_i
33
+
34
+ end
35
+ def parse_float(value)
36
+ return nil if value==nil
37
+ return value.to_f
38
+ end
39
+ def parse_unsigned_integer(value)
40
+ return nil if value==nil
41
+ return value.to_i
42
+ end
43
+
44
+ def format_reference_set(value)
45
+ value.to_yaml
46
+
47
+ end
48
+
49
+ def format_date(value)
50
+ return nil if value==nil
51
+ return value.to_f.to_s
52
+ end
53
+ def format_boolean(value)
54
+
55
+ return ((value == true) || (value==1))?'true':'false'
56
+ end
57
+ def format_integer(value)
58
+
59
+
60
+ if value==nil
61
+ return nil
62
+ end
63
+ return zero_pad_integer(value)
64
+ # return nil if value==nil
65
+ # sign='p'
66
+ # if value<0
67
+ # sign='n'
68
+ # end
69
+ # return sign+zero_pad_integer(Math.abs(value))
70
+
71
+ end
72
+ def format_string(value)
73
+ if value==nil
74
+ return nil
75
+ end
76
+
77
+ if value.length>1024
78
+ value=value[0..1023]
79
+ end
80
+ value
81
+ end
82
+ def format_float(value)
83
+ if value==nil
84
+ return nil
85
+ end
86
+ return zero_pad_float(value)
87
+ end
88
+ def format_unsigned_integer(value)
89
+ if !value
90
+ return nil
91
+ end
92
+ return zero_pad_integer(value)
93
+ end
94
+
95
+
96
+
97
+ def zero_pad_integer(value)
98
+ value=value.to_i
99
+ temp= value.abs.to_s.rjust(30,"0")
100
+ if value>=0
101
+ temp="0"+temp
102
+ else
103
+
104
+ temp="-"+temp
105
+ end
106
+ return temp
107
+ end
108
+ def zero_pad_float(value)
109
+ temp= value.abs.to_s.rjust(30,"0")
110
+ if value>=0
111
+ temp="0"+temp
112
+ else
113
+
114
+ temp="-"+temp
115
+ end
116
+ return temp
117
+ end
118
+ end
119
+ end