sdb_dal 0.0.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/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