bare-ruby-aws 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,141 @@
1
+ # $Id: cache.rb,v 1.8 2008/06/10 06:33:46 ianmacd Exp $
2
+ #
3
+
4
+ module Amazon
5
+
6
+ module AWS
7
+
8
+ # This class provides a simple results caching system for operations
9
+ # performed by AWS.
10
+ #
11
+ # To use it, set _cache_ to *true* in either <tt>/etc/amazonrc</tt> or
12
+ # <tt>~/.amazonrc</tt>.
13
+ #
14
+ # By default, the cache directory used is <tt>/tmp/amazon</tt>, but this
15
+ # can be changed by defining _cache_dir_ in either <tt>/etc/amazonrc</tt>
16
+ # or <tt>~/.amazonrc</tt>.
17
+ #
18
+ # When a cache is used, Ruby/AWS will check the cache directory for a
19
+ # recent copy of a response to the exact operation that you are
20
+ # performing. If found, the cached response will be returned instead of
21
+ # the request being forwarded to the AWS servers for processing. If no
22
+ # (recent) copy is found, the request will be forwarded to the AWS servers
23
+ # as usual. Recency is defined here as less than 24 hours old.
24
+ #
25
+ class Cache
26
+
27
+ require 'fileutils'
28
+
29
+ begin
30
+ require 'md5'
31
+ rescue LoadError
32
+ # Ruby 1.9 has moved MD5.
33
+ #
34
+ require 'digest/md5'
35
+ end
36
+
37
+ # Exception class for bad cache paths.
38
+ #
39
+ class PathError < StandardError; end
40
+
41
+ # Length of one day in seconds
42
+ #
43
+ ONE_DAY = 86400 # :nodoc:
44
+
45
+ # Age in days below which to consider cache files valid.
46
+ #
47
+ MAX_AGE = 1.0
48
+
49
+ # Default cache location.
50
+ #
51
+ DEFAULT_CACHE_DIR = '/tmp/amazon'
52
+
53
+ attr_reader :path
54
+
55
+ def initialize(path=DEFAULT_CACHE_DIR)
56
+ path ||= DEFAULT_CACHE_DIR
57
+
58
+ ::FileUtils::mkdir_p( path ) unless File.exists? path
59
+
60
+ unless File.directory? path
61
+ raise PathError, "cache path #{path} is not a directory"
62
+ end
63
+
64
+ unless File.readable? path
65
+ raise PathError, "cache path #{path} is not readable"
66
+ end
67
+
68
+ unless File.writable? path
69
+ raise PathError, "cache path #{path} is not writable"
70
+ end
71
+
72
+ @path = path
73
+ end
74
+
75
+
76
+ # Determine whether or not the the response to a given URL is cached.
77
+ # Returns *true* or *false*.
78
+ #
79
+ def cached?(url)
80
+ digest = Digest::MD5.hexdigest( url )
81
+
82
+ cache_files = Dir.glob( File.join( @path, '*' ) ).map do |d|
83
+ File.basename( d )
84
+ end
85
+
86
+ return cache_files.include?( digest ) &&
87
+ ( Time.now - File.mtime( File.join( @path, digest ) ) ) /
88
+ ONE_DAY <= MAX_AGE
89
+ end
90
+
91
+
92
+ # Retrieve the cached response associated with _url_.
93
+ #
94
+ def fetch(url)
95
+ digest = Digest::MD5.hexdigest( url )
96
+ cache_file = File.join( @path, digest )
97
+
98
+ return nil unless File.exist? cache_file
99
+
100
+ Amazon.dprintf( 'Fetching %s from cache...', digest )
101
+ File.open( File.join( cache_file ) ).readlines.to_s
102
+ end
103
+
104
+
105
+ # Cache the data from _contents_ and associate it with _url_.
106
+ #
107
+ def store(url, contents)
108
+ digest = Digest::MD5.hexdigest( url )
109
+ cache_file = File.join( @path, digest )
110
+
111
+ Amazon.dprintf( 'Caching %s...', digest )
112
+ File.open( cache_file, 'w' ) { |f| f.puts contents }
113
+ end
114
+
115
+
116
+ # This method flushes all files from the cache directory specified
117
+ # in the object's <i>@path</i> variable.
118
+ #
119
+ def flush_all
120
+ FileUtils.rm Dir.glob( File.join( @path, '*' ) )
121
+ end
122
+
123
+
124
+ # This method flushes expired files from the cache directory specified
125
+ # in the object's <i>@path</i> variable.
126
+ #
127
+ def flush_expired
128
+ now = Time.now
129
+
130
+ expired_files = Dir.glob( File.join( @path, '*' ) ).find_all do |f|
131
+ ( now - File.mtime( f ) ) / ONE_DAY > MAX_AGE
132
+ end
133
+
134
+ FileUtils.rm expired_files
135
+ end
136
+
137
+ end
138
+
139
+ end
140
+
141
+ end
@@ -0,0 +1,458 @@
1
+ # $Id: search.rb,v 1.49 2010/03/19 19:28:19 ianmacd Exp $
2
+ #
3
+
4
+ module Amazon
5
+
6
+ module AWS
7
+
8
+ require 'amazon/aws'
9
+ require 'net/http'
10
+ require 'rexml/document'
11
+ require 'openssl'
12
+
13
+ # Load this library with:
14
+ #
15
+ # require 'amazon/aws/search'
16
+ #
17
+ module Search
18
+
19
+ class Request
20
+
21
+ include REXML
22
+
23
+ # Exception class for bad access key ID.
24
+ #
25
+ class AccessKeyIdError < Amazon::AWS::Error::AWSError; end
26
+
27
+ # Exception class for bad locales.
28
+ #
29
+ class LocaleError < Amazon::AWS::Error::AWSError; end
30
+
31
+ # Do we have support for the SHA-256 Secure Hash Algorithm?
32
+ #
33
+ # Note that Module#constants returns Strings in Ruby 1.8 and Symbols
34
+ # in 1.9.
35
+ #
36
+ DIGEST_SUPPORT = OpenSSL::Digest.constants.include?( 'SHA256' ) ||
37
+ OpenSSL::Digest.constants.include?( :SHA256 )
38
+
39
+ # Requests are authenticated using the SHA-256 Secure Hash Algorithm.
40
+ #
41
+ DIGEST = OpenSSL::Digest::Digest.new( 'sha256' ) if DIGEST_SUPPORT
42
+
43
+ attr_reader :conn, :config, :locale, :query, :user_agent
44
+ attr_writer :cache
45
+ attr_accessor :encoding
46
+
47
+ # This method is used to generate an AWS search request object.
48
+ #
49
+ # _key_id_ is your AWS {access key
50
+ # ID}[https://aws-portal.amazon.com/gp/aws/developer/registration/index.html].
51
+ # Note that your secret key, used for signing requests, can be
52
+ # specified only in your <tt>~/.amazonrc</tt> configuration file.
53
+ #
54
+ # _associate_ is your
55
+ # Associates[http://docs.amazonwebservices.com/AWSECommerceService/2009-11-01/GSG/BecominganAssociate.html]
56
+ # tag (if any), _locale_ is the locale in which you which to work
57
+ # (*us* for amazon.com[http://www.amazon.com/], *uk* for
58
+ # amazon.co.uk[http://www.amazon.co.uk], etc.), _cache_ is whether or
59
+ # not you wish to utilise a response cache, and _user_agent_ is the
60
+ # client name to pass when performing calls to AWS. By default,
61
+ # _user_agent_ will be set to a string identifying the Ruby/AWS
62
+ # library and its version number.
63
+ #
64
+ # _locale_ and _cache_ can also be set later, if you wish to change
65
+ # the current behaviour.
66
+ #
67
+ # Example:
68
+ #
69
+ # req = Request.new( '0Y44V8FAFNM119CX4TR2', 'calibanorg-20' )
70
+ #
71
+ def initialize(config=nil)
72
+
73
+ puts "Config: #{config}"
74
+ @config = Amazon::Config.new(config)
75
+
76
+ locale = @config['locale'] || 'us'
77
+ locale.downcase!
78
+
79
+ key_id = @config['key_id']
80
+ cache = @config['cache'] if cache.nil?
81
+
82
+ validate_locale( locale )
83
+
84
+ if key_id.nil?
85
+ raise AccessKeyIdError, 'key_id may not be nil'
86
+ end
87
+
88
+ @key_id = key_id
89
+ @tag = @config['associate'] || DEF_ASSOC[locale]
90
+ @user_agent = USER_AGENT
91
+ @cache = unless cache == 'false' || cache == false
92
+ Amazon::AWS::Cache.new( @config['cache_dir'] )
93
+ else
94
+ nil
95
+ end
96
+
97
+ # Set the following two variables from the config file. Will be
98
+ # *nil* if not present in config file.
99
+ #
100
+ @api = @config['api']
101
+ @encoding = @config['encoding']
102
+
103
+ self.locale = locale
104
+ end
105
+
106
+
107
+ # Assign a new locale. If the locale we're coming from is using the
108
+ # default Associate ID for that locale, then we use the new locale's
109
+ # default ID, too.
110
+ #
111
+ def locale=(l) # :nodoc:
112
+ old_locale = @locale ||= nil
113
+ @locale = validate_locale( l )
114
+
115
+ # Use the new locale's default ID if the ID currently in use is the
116
+ # current locale's default ID.
117
+ #
118
+ if @tag == Amazon::AWS::DEF_ASSOC[old_locale]
119
+ @tag = Amazon::AWS::DEF_ASSOC[@locale]
120
+ end
121
+
122
+ if @config.key?( @locale ) && @config[@locale].key?( 'associate' )
123
+ @tag = @config[@locale]['associate']
124
+ end
125
+
126
+ # We must now set up a new HTTP connection to the correct server for
127
+ # this locale, unless the same server is used for both.
128
+ #
129
+ unless Amazon::AWS::ENDPOINT[@locale] ==
130
+ Amazon::AWS::ENDPOINT[old_locale]
131
+ #connect( @locale )
132
+ @conn = nil
133
+ end
134
+ end
135
+
136
+
137
+ # If @cache has simply been assigned *true* at some point in time,
138
+ # assign a proper cache object to it when it is referenced. Otherwise,
139
+ # just return its value.
140
+ #
141
+ def cache # :nodoc:
142
+ if @cache == true
143
+ @cache = Amazon::AWS::Cache.new( @config['cache_dir'] )
144
+ else
145
+ @cache
146
+ end
147
+ end
148
+
149
+
150
+ # Verify the validity of a locale string. _l_ is the locale string.
151
+ #
152
+ def validate_locale(l)
153
+ unless Amazon::AWS::ENDPOINT.has_key? l
154
+ raise LocaleError, "invalid locale: #{l}"
155
+ end
156
+ l
157
+ end
158
+ private :validate_locale
159
+
160
+
161
+ # Return an HTTP connection for the current _locale_.
162
+ #
163
+ def connect(locale)
164
+ if ENV.key? 'http_proxy'
165
+ uri = URI.parse( ENV['http_proxy'] )
166
+ proxy_user = proxy_pass = nil
167
+ proxy_user, proxy_pass = uri.userinfo.split( /:/ ) if uri.userinfo
168
+ @conn = Net::HTTP::Proxy( uri.host, uri.port, proxy_user,
169
+ proxy_pass ).start(
170
+ Amazon::AWS::ENDPOINT[locale].host )
171
+ else
172
+ @conn = Net::HTTP::start( Amazon::AWS::ENDPOINT[locale].host )
173
+ end
174
+ end
175
+ private :connect
176
+
177
+
178
+ # Reconnect to the server if our connection has been lost (due to a
179
+ # time-out, etc.).
180
+ #
181
+ def reconnect # :nodoc:
182
+ connect( self.locale )
183
+ self
184
+ end
185
+
186
+
187
+ # This method checks for errors in an XML response returned by AWS.
188
+ # _xml_ is the XML node below which to search.
189
+ #
190
+ def error_check(xml)
191
+ if ! xml.nil? && xml = xml.elements['Errors/Error']
192
+ raise Amazon::AWS::Error.exception( xml )
193
+ end
194
+ end
195
+ private :error_check
196
+
197
+
198
+ # Add a timestamp to a request object's query string.
199
+ #
200
+ def timestamp # :nodoc:
201
+ @query << '&Timestamp=%s' %
202
+ [ Amazon.url_encode(
203
+ Time.now.utc.strftime( '%Y-%m-%dT%H:%M:%SZ' ) ) ]
204
+ end
205
+ private :timestamp
206
+
207
+
208
+ # Add a signature to a request object's query string. This implicitly
209
+ # also adds a timestamp.
210
+ #
211
+ def sign # :nodoc:
212
+ return false unless DIGEST_SUPPORT
213
+
214
+ timestamp
215
+ params = @query[1..-1].split( '&' ).sort.join( '&' )
216
+
217
+ sign_str = "GET\n%s\n%s\n%s" % [ ENDPOINT[@locale].host,
218
+ ENDPOINT[@locale].path,
219
+ params ]
220
+
221
+ Amazon.dprintf( 'Calculating SHA256 HMAC of "%s"...', sign_str )
222
+
223
+ hmac = OpenSSL::HMAC.digest( DIGEST,
224
+ @config['secret_key_id'],
225
+ sign_str )
226
+ Amazon.dprintf( 'SHA256 HMAC is "%s"', hmac.inspect )
227
+
228
+ base64_hmac = [ hmac ].pack( 'm' ).chomp
229
+ Amazon.dprintf( 'Base64-encoded HMAC is "%s".', base64_hmac )
230
+
231
+ signature = Amazon.url_encode( base64_hmac )
232
+
233
+ params << '&Signature=%s' % [ signature ]
234
+ @query = '?' + params
235
+
236
+ true
237
+ end
238
+
239
+
240
+ # Perform a search of the AWS database, returning an AWSObject.
241
+ #
242
+ # _operation_ is an object of a subclass of _Operation_, such as
243
+ # _ItemSearch_, _ItemLookup_, etc. It may also be a _MultipleOperation_
244
+ # object.
245
+ #
246
+ # In versions of Ruby/AWS up to prior to 0.8.0, the second parameter to
247
+ # this method was _response_group_. This way of passing response
248
+ # groups has been deprecated since 0.7.0 and completely removed in
249
+ # 0.8.0. To pair a set of response groups with an operation, assign
250
+ # directly to the operation's @response_group attribute.
251
+ #
252
+ # _nr_pages_ is the number of results pages to return. It defaults to
253
+ # <b>1</b>. If a higher number is given, pages 1 to _nr_pages_ will be
254
+ # returned. If the special value <b>:ALL_PAGES</b> is given, all
255
+ # results pages will be returned.
256
+ #
257
+ # Note that _ItemLookup_ operations can use several different
258
+ # pagination parameters. An _ItemLookup_ will typically return just
259
+ # one results page containing a single product, but <b>:ALL_PAGES</b>
260
+ # can still be used to apply the _OfferPage_ parameter to paginate
261
+ # through multiple pages of offers.
262
+ #
263
+ # Similarly, a single product may have multiple pages of reviews
264
+ # available. In such a case, it is up to the user to manually supply
265
+ # the _ReviewPage_ parameter and an appropriate value.
266
+ #
267
+ # In the same vein, variations can be returned by using the
268
+ # _VariationPage_ parameter.
269
+ #
270
+ # The pagination parameters supported by each type of operation,
271
+ # together with the maximum page number that can be retrieved for each
272
+ # type of data, are # documented in the AWS Developer's Guide:
273
+ #
274
+ # http://docs.amazonwebservices.com/AWSECommerceService/2009-11-01/DG/index.html?MaximumNumberofPages.html
275
+ #
276
+ # The pagination parameter used by <b>:ALL_PAGES</b> can be looked up
277
+ # in the Amazon::AWS::PAGINATION hash.
278
+ #
279
+ # If _operation_ is of class _MultipleOperation_, the operations
280
+ # encapsulated within will return only the first page of results,
281
+ # regardless of whether a higher number of pages is requested.
282
+ #
283
+ # If a block is passed to this method, each successive page of results
284
+ # will be yielded to the block.
285
+ #
286
+ def search(operation, nr_pages=1)
287
+ parameters = Amazon::AWS::SERVICE.
288
+ merge( { 'AWSAccessKeyId' => @key_id,
289
+ 'AssociateTag' => @tag } ).
290
+ merge( operation.query_parameters )
291
+
292
+ if nr_pages.is_a? Amazon::AWS::ResponseGroup
293
+ raise ObsolescenceError, 'Request#search method no longer accepts response_group parameter.'
294
+ end
295
+
296
+ # Pre-0.8.0 user code may have passed *nil* as the second parameter,
297
+ # in order to use the @response_group of the operation.
298
+ #
299
+ nr_pages ||= 1
300
+
301
+ # Check to see whether a particular version of the API has been
302
+ # requested. If so, overwrite Version with the new value.
303
+ #
304
+ parameters.merge!( { 'Version' => @api } ) if @api
305
+
306
+ @query = Amazon::AWS.assemble_query( parameters, @encoding )
307
+ page = Amazon::AWS.get_page( self )
308
+
309
+ # Ruby 1.9 needs to know that the page is UTF-8, not ASCII-8BIT.
310
+ #
311
+ page.force_encoding( 'utf-8' ) if RUBY_VERSION >= '1.9.0'
312
+
313
+ doc = Document.new( page )
314
+
315
+ # Some errors occur at the very top level of the XML. For example,
316
+ # when no Operation parameter is given. This should not be possible
317
+ # with user code, but occurred during debugging of this library.
318
+ #
319
+ error_check( doc )
320
+
321
+ # Another possible error results in a document containing nothing
322
+ # but <Result>Internal Error</Result>. This occurs when a specific
323
+ # version of the AWS API is requested, in combination with an
324
+ # operation that did not yet exist in that version of the API.
325
+ #
326
+ # For example:
327
+ #
328
+ # http://ecs.amazonaws.com/onca/xml?AWSAccessKeyId=foo&Operation=VehicleSearch&Year=2008&ResponseGroup=VehicleMakes&Service=AWSECommerceService&Version=2008-03-03
329
+ #
330
+ if xml = doc.elements['Result']
331
+ raise Amazon::AWS::Error::AWSError, xml.text
332
+ end
333
+
334
+ # Fundamental errors happen at the OperationRequest level. For
335
+ # example, if an invalid AWSAccessKeyId is used.
336
+ #
337
+ error_check( doc.elements['*/OperationRequest'] )
338
+
339
+ # Check for parameter and value errors deeper down, inside Request.
340
+ #
341
+ if operation.kind == 'MultipleOperation'
342
+
343
+ # Everything is a level deeper, because of the
344
+ # <MultiOperationResponse> container.
345
+ #
346
+ # Check for errors in the first operation.
347
+ #
348
+ error_check( doc.elements['*/*/*/Request'] )
349
+
350
+ # Check for errors in the second operation.
351
+ #
352
+ error_check( doc.elements['*/*[3]/*/Request'] )
353
+
354
+ # If second operation is batched, check for errors in its 2nd set
355
+ # of results.
356
+ #
357
+ if batched = doc.elements['*/*[3]/*[2]/Request']
358
+ error_check( batched )
359
+ end
360
+ else
361
+ error_check( doc.elements['*/*/Request'] )
362
+
363
+ # If operation is batched, check for errors in its 2nd set of
364
+ # results.
365
+ #
366
+ if batched = doc.elements['*/*[3]/Request']
367
+ error_check( batched )
368
+ end
369
+ end
370
+
371
+ if doc.elements['*/*[2]/TotalPages']
372
+ total_pages = doc.elements['*/*[2]/TotalPages'].text.to_i
373
+
374
+ # FIXME: ListLookup and MultipleOperation (and possibly others) have
375
+ # TotalPages nested one level deeper. I should take some time to
376
+ # ensure that all operations that can return multiple results pages
377
+ # are covered by either the 'if' above or the 'elsif' here.
378
+ #
379
+ elsif doc.elements['*/*[2]/*[2]/TotalPages']
380
+ total_pages = doc.elements['*/*[2]/*[2]/TotalPages'].text.to_i
381
+ else
382
+ total_pages = 1
383
+ end
384
+
385
+ # Create a root AWS object and walk the XML response tree.
386
+ #
387
+ aws = AWS::AWSObject.new( operation )
388
+ aws.walk( doc )
389
+ result = aws
390
+
391
+ # If only one page has been requested or only one page is available,
392
+ # we can stop here. First yield to the block, if given.
393
+ #
394
+ if nr_pages == 1 || ( tp = total_pages ) == 1
395
+ yield result if block_given?
396
+ return result
397
+ end
398
+
399
+ # Limit the number of pages to the maximum number available.
400
+ #
401
+ nr_pages = tp.to_i if nr_pages == :ALL_PAGES || nr_pages > tp.to_i
402
+
403
+ if PAGINATION.key? operation.kind
404
+ page_parameter = PAGINATION[operation.kind]['parameter']
405
+ max_pages = PAGINATION[operation.kind]['max_page']
406
+ else
407
+ page_parameter = 'ItemPage'
408
+ max_pages = 400
409
+ end
410
+
411
+ # Iterate over pages 2 and higher, but go no higher than MAX_PAGES.
412
+ #
413
+ 2.upto( nr_pages < max_pages ? nr_pages : max_pages ) do |page_nr|
414
+ @query = Amazon::AWS.assemble_query(
415
+ parameters.merge( { page_parameter => page_nr } ),
416
+ @encoding)
417
+ page = Amazon::AWS.get_page( self )
418
+
419
+ # Ruby 1.9 needs to know that the page is UTF-8, not ASCII-8BIT.
420
+ #
421
+ page.force_encoding( 'utf-8' ) if RUBY_VERSION >= '1.9.0'
422
+
423
+ doc = Document.new( page )
424
+
425
+ # Check for errors.
426
+ #
427
+ error_check( doc.elements['*/OperationRequest'] )
428
+ error_check( doc.elements['*/*/Request'] )
429
+
430
+ # Create a new AWS object and walk the XML response tree.
431
+ #
432
+ aws = AWS::AWSObject.new( operation )
433
+ aws.walk( doc )
434
+
435
+ # When dealing with multiple pages, we return not just an
436
+ # AWSObject, but an array of them.
437
+ #
438
+ result = [ result ] unless result.is_a? Array
439
+
440
+ # Append the new object to the array.
441
+ #
442
+ result << aws
443
+ end
444
+
445
+ # Yield each object to the block, if given.
446
+ #
447
+ result.each { |r| yield r } if block_given?
448
+
449
+ result
450
+ end
451
+
452
+ end
453
+
454
+ end
455
+
456
+ end
457
+
458
+ end