aws-s3 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/COPYING +19 -0
  2. data/INSTALL +35 -0
  3. data/README +529 -0
  4. data/Rakefile +284 -0
  5. data/bin/s3sh +4 -0
  6. data/bin/setup.rb +10 -0
  7. data/lib/aws/s3.rb +64 -0
  8. data/lib/aws/s3/acl.rb +631 -0
  9. data/lib/aws/s3/authentication.rb +218 -0
  10. data/lib/aws/s3/base.rb +232 -0
  11. data/lib/aws/s3/bittorrent.rb +58 -0
  12. data/lib/aws/s3/bucket.rb +323 -0
  13. data/lib/aws/s3/connection.rb +212 -0
  14. data/lib/aws/s3/error.rb +69 -0
  15. data/lib/aws/s3/exceptions.rb +130 -0
  16. data/lib/aws/s3/extensions.rb +186 -0
  17. data/lib/aws/s3/logging.rb +163 -0
  18. data/lib/aws/s3/object.rb +565 -0
  19. data/lib/aws/s3/owner.rb +44 -0
  20. data/lib/aws/s3/parsing.rb +138 -0
  21. data/lib/aws/s3/response.rb +180 -0
  22. data/lib/aws/s3/service.rb +43 -0
  23. data/lib/aws/s3/version.rb +12 -0
  24. data/support/faster-xml-simple/lib/faster_xml_simple.rb +115 -0
  25. data/support/faster-xml-simple/test/regression_test.rb +16 -0
  26. data/support/faster-xml-simple/test/xml_simple_comparison_test.rb +22 -0
  27. data/support/rdoc/code_info.rb +211 -0
  28. data/test/acl_test.rb +243 -0
  29. data/test/authentication_test.rb +96 -0
  30. data/test/base_test.rb +143 -0
  31. data/test/bucket_test.rb +48 -0
  32. data/test/connection_test.rb +120 -0
  33. data/test/error_test.rb +75 -0
  34. data/test/extensions_test.rb +282 -0
  35. data/test/fixtures.rb +89 -0
  36. data/test/fixtures/buckets.yml +102 -0
  37. data/test/fixtures/errors.yml +34 -0
  38. data/test/fixtures/headers.yml +3 -0
  39. data/test/fixtures/logging.yml +15 -0
  40. data/test/fixtures/policies.yml +16 -0
  41. data/test/logging_test.rb +36 -0
  42. data/test/mocks/base.rb +89 -0
  43. data/test/object_test.rb +177 -0
  44. data/test/parsing_test.rb +82 -0
  45. data/test/remote/acl_test.rb +117 -0
  46. data/test/remote/bittorrent_test.rb +45 -0
  47. data/test/remote/bucket_test.rb +127 -0
  48. data/test/remote/logging_test.rb +82 -0
  49. data/test/remote/object_test.rb +267 -0
  50. data/test/remote/test_file.data +0 -0
  51. data/test/remote/test_helper.rb +30 -0
  52. data/test/response_test.rb +70 -0
  53. data/test/service_test.rb +26 -0
  54. data/test/test_helper.rb +82 -0
  55. metadata +125 -0
@@ -0,0 +1,44 @@
1
+ module AWS
2
+ module S3
3
+ # Entities in S3 have an associated owner (the person who created them). The owner is a canonical representation of an
4
+ # entity in the S3 system. It has an <tt>id</tt> and a <tt>display_name</tt>.
5
+ #
6
+ # These attributes can be used when specifying a ACL::Grantee for an ACL::Grant.
7
+ #
8
+ # You can retrieve the owner of the current account by calling Owner.current.
9
+ class Owner
10
+ undef_method :id # Get rid of Object#id
11
+ include SelectiveAttributeProxy
12
+
13
+ class << self
14
+ # The owner of the current account.
15
+ def current
16
+ response = Service.get('/')
17
+ new(response.parsed['owner']) if response.parsed['owner']
18
+ end
19
+ memoized :current
20
+ end
21
+
22
+ def initialize(attributes = {}) #:nodoc:
23
+ @attributes = attributes
24
+ end
25
+
26
+ def ==(other_owner) #:nodoc:
27
+ hash == other_owner.hash
28
+ end
29
+
30
+ def hash #:nodoc
31
+ [id, display_name].join.hash
32
+ end
33
+
34
+ private
35
+ def proxiable_attribute?(name)
36
+ valid_attributes.include?(name)
37
+ end
38
+
39
+ def valid_attributes
40
+ %w(id display_name)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,138 @@
1
+ #:stopdoc:
2
+ module AWS
3
+ module S3
4
+ module Parsing
5
+ class << self
6
+ def parser=(parsing_library)
7
+ XmlParser.parsing_library = parsing_library
8
+ end
9
+
10
+ def parser
11
+ XmlParser.parsing_library
12
+ end
13
+ end
14
+
15
+ module Typecasting
16
+ def typecast(object)
17
+ case object
18
+ when Hash
19
+ typecast_hash(object)
20
+ when Array
21
+ object.map {|element| typecast(element)}
22
+ when String
23
+ CoercibleString.coerce(object)
24
+ else
25
+ object
26
+ end
27
+ end
28
+
29
+ def typecast_hash(hash)
30
+ if content = hash['__content__']
31
+ typecast(content)
32
+ else
33
+ keys = hash.keys.map {|key| key.underscore}
34
+ values = hash.values.map {|value| typecast(value)}
35
+ keys.inject({}) do |new_hash, key|
36
+ new_hash[key] = values.shift
37
+ new_hash
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ class CoercibleString < String
44
+ attr_accessor :generator
45
+ class << self
46
+ def coerce(string)
47
+ new(string).coerce
48
+ end
49
+ end
50
+
51
+ def coerce
52
+ attempt = nil
53
+ break unless (attempt = coercions.next).nil? while coercions.next?
54
+ attempt.nil? ? self : attempt
55
+ end
56
+
57
+ private
58
+
59
+ def coercions
60
+ Generator.new do |self.generator|
61
+ try { self == 'true' }
62
+ try { [self == 'false', false] }
63
+ try { Integer(self) }
64
+ try { Time.parse(self) } if appears_to_be_date?
65
+ end
66
+ end
67
+ memoized :coercions
68
+
69
+ def try
70
+ attempt, desired = yield
71
+ generator.yield(desired.nil? ? attempt : desired) if attempt
72
+ rescue ArgumentError
73
+ generator.yield nil
74
+ end
75
+
76
+ # Lame hack since Date._parse is so accepting. S3 dates are of the form: '2006-10-29T23:14:47.000Z'
77
+ # so unless the string looks like that, don't even try, otherwise it might convert an object's
78
+ # key from something like '03 1-2-3-Apple-Tree.mp3' to Sat Feb 03 00:00:00 CST 2001.
79
+ def appears_to_be_date?
80
+ self =~ /^\d{4}-\d{2}-\d{2}\w\d{2}:\d{2}:\d{2}/
81
+ end
82
+ end
83
+
84
+ class XmlParser < Hash
85
+ include Typecasting
86
+
87
+ class << self
88
+ attr_accessor :parsing_library
89
+ end
90
+
91
+ attr_reader :body, :xml_in, :root
92
+
93
+ def initialize(body)
94
+ @body = body
95
+ parse
96
+ set_root
97
+ typecast_xml_in
98
+ end
99
+
100
+ private
101
+
102
+ def parse
103
+ @xml_in = self.class.parsing_library.xml_in(body, parsing_options)
104
+ end
105
+
106
+ def parsing_options
107
+ {
108
+ # Includes the enclosing tag as the top level key
109
+ 'keeproot' => true,
110
+ # Makes tag value available via the '__content__' key
111
+ 'contentkey' => '__content__',
112
+ # Always parse tags into a hash, even when there are no attributes
113
+ # (unless there is also no value, in which case it is nil)
114
+ 'forcecontent' => true,
115
+ # If a tag is empty, makes its content nil
116
+ 'suppressempty' => nil,
117
+ # Force nested elements to be put into an array, even if there is only one of them
118
+ 'forcearray' => ['Contents', 'Bucket', 'Grant']
119
+ }
120
+ end
121
+
122
+ def set_root
123
+ @root = @xml_in.keys.first.underscore
124
+ end
125
+
126
+ def typecast_xml_in
127
+ typecast_xml = {}
128
+ @xml_in.dup.each do |key, value| # Some typecasting is destructive so we dup
129
+ typecast_xml[key.underscore] = typecast(value)
130
+ end
131
+ # An empty body will try to update with a string so only update if the result is a hash
132
+ update(typecast_xml[root]) if typecast_xml[root].is_a?(Hash)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ #:startdoc:
@@ -0,0 +1,180 @@
1
+ #:stopdoc:
2
+ module AWS
3
+ module S3
4
+ class Base
5
+ class Response < String
6
+ attr_reader :response, :body, :parsed
7
+ def initialize(response)
8
+ @response = response
9
+ @body = response.body.to_s
10
+ super(body)
11
+ end
12
+
13
+ def headers
14
+ headers = {}
15
+ response.each do |header, value|
16
+ headers[header] = value
17
+ end
18
+ headers
19
+ end
20
+ memoized :headers
21
+
22
+ def [](header)
23
+ headers[header]
24
+ end
25
+
26
+ def each(&block)
27
+ headers.each(&block)
28
+ end
29
+
30
+ def code
31
+ response.code.to_i
32
+ end
33
+
34
+ {:success => 200..299, :redirect => 300..399,
35
+ :client_error => 400..499, :server_error => 500..599}.each do |result, code_range|
36
+ class_eval(<<-EVAL, __FILE__, __LINE__)
37
+ def #{result}?
38
+ return false unless response
39
+ (#{code_range}).include? code
40
+ end
41
+ EVAL
42
+ end
43
+
44
+ def error?
45
+ !success? && response['content-type'] == 'application/xml' && parsed.root == 'error'
46
+ end
47
+
48
+ def error
49
+ Error.new(parsed, self)
50
+ end
51
+ memoized :error
52
+
53
+ def parsed
54
+ # XmlSimple is picky about what kind of object it parses, so we pass in body rather than self
55
+ Parsing::XmlParser.new(body)
56
+ end
57
+ memoized :parsed
58
+
59
+ def inspect
60
+ "#<#{self.class}:0x#{object_id} #{response.code} #{response.message}>"
61
+ end
62
+ end
63
+ end
64
+
65
+ class Bucket
66
+ class Response < Base::Response
67
+ def bucket
68
+ parsed
69
+ end
70
+ end
71
+ end
72
+
73
+ class S3Object
74
+ class Response < Base::Response
75
+ def etag
76
+ headers['etag'][1...-1]
77
+ end
78
+ end
79
+ end
80
+
81
+ class Service
82
+ class Response < Base::Response
83
+ def empty?
84
+ parsed['buckets'].nil?
85
+ end
86
+
87
+ def buckets
88
+ parsed['buckets']['bucket'] || []
89
+ end
90
+ end
91
+ end
92
+
93
+ module ACL
94
+ class Policy
95
+ class Response < Base::Response
96
+ alias_method :policy, :parsed
97
+ end
98
+ end
99
+ end
100
+
101
+ # Requests whose response code is between 300 and 599 and contain an <Error></Error> in their body
102
+ # are wrapped in an Error::Response. This Error::Response contains an Error object which raises an exception
103
+ # that corresponds to the error in the response body. The exception object contains the ErrorResponse, so
104
+ # in all cases where a request happens, you can rescue ResponseError and have access to the ErrorResponse and
105
+ # its Error object which contains information about the ResponseError.
106
+ #
107
+ # begin
108
+ # Bucket.create(..)
109
+ # rescue ResponseError => exception
110
+ # exception.response
111
+ # # => <Error::Response>
112
+ # exception.response.error
113
+ # # => <Error>
114
+ # end
115
+ class Error
116
+ class Response < Base::Response
117
+ def error?
118
+ true
119
+ end
120
+
121
+ def inspect
122
+ "#<#{self.class.name}:0x#{object_id} #{response.code} #{error.code}: '#{error.message}'>"
123
+ end
124
+ end
125
+ end
126
+
127
+ # Guess response class name from current class name. If the guessed response class doesn't exist
128
+ # do the same thing to the current class's parent class, up the inheritance heirarchy until either
129
+ # a response class is found or until we get to the top of the heirarchy in which case we just use
130
+ # the the Base response class.
131
+ #
132
+ # Important: This implemantation assumes that the Base class has a corresponding Base::Response.
133
+ class FindResponseClass #:nodoc:
134
+ class << self
135
+ def for(start)
136
+ new(start).find
137
+ end
138
+ end
139
+
140
+ def initialize(start)
141
+ @container = AWS::S3
142
+ @current_class = start
143
+ end
144
+
145
+ def find
146
+ self.current_class = current_class.superclass until response_class_found?
147
+ target.const_get(class_to_find)
148
+ end
149
+
150
+ private
151
+ attr_reader :container
152
+ attr_accessor :current_class
153
+
154
+ def target
155
+ container.const_get(current_name)
156
+ end
157
+
158
+ def target?
159
+ container.const_defined?(current_name)
160
+ end
161
+
162
+ def response_class_found?
163
+ target? && target.const_defined?(class_to_find)
164
+ end
165
+
166
+ def class_to_find
167
+ :Response
168
+ end
169
+
170
+ def current_name
171
+ truncate(current_class)
172
+ end
173
+
174
+ def truncate(klass)
175
+ klass.name[/[^:]+$/]
176
+ end
177
+ end
178
+ end
179
+ end
180
+ #:startdoc:
@@ -0,0 +1,43 @@
1
+ module AWS
2
+ module S3
3
+ # The service lets you find out general information about your account, like what buckets you have.
4
+ #
5
+ # Service.buckets
6
+ # # => []
7
+ class Service < Base
8
+ @@response = nil #:nodoc:
9
+
10
+ class << self
11
+ # List all your buckets
12
+ def buckets
13
+ response = get('/')
14
+ if response.empty?
15
+ []
16
+ else
17
+ response.buckets.map {|attributes| Bucket.new(attributes)}
18
+ end
19
+ end
20
+ memoized :buckets
21
+
22
+ # Sometimes methods that make requests to the S3 servers return some object, like a Bucket or an S3Object.
23
+ # Othertimes they return just <tt>true</tt>. Other times they raise an exception that you may want to rescue. Despite all these
24
+ # possible outcomes, every method that makes a request stores its response object for you in Service.response. You can always
25
+ # get to the last request's response via Service.response.
26
+ #
27
+ # objects = Bucket.objects('jukebox')
28
+ # Service.response.success?
29
+ # # => true
30
+ #
31
+ # This is also useful when an error exception is raised in the console which you weren't expecting. You can
32
+ # root around in the response to get more details of what might have gone wrong.
33
+ def response
34
+ @@response
35
+ end
36
+
37
+ def response=(response) #:nodoc:
38
+ @@response = response
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,12 @@
1
+ module AWS
2
+ module S3
3
+ module VERSION #:nodoc:
4
+ MAJOR = '0'
5
+ MINOR = '1'
6
+ TINY = '0'
7
+ REVISION = '$Revision: 66 $'[/\d+/]
8
+ end
9
+
10
+ Version = [VERSION::MAJOR, VERSION::MINOR, VERSION::TINY || VERSION::REVISION] * '.'
11
+ end
12
+ end
@@ -0,0 +1,115 @@
1
+ #
2
+ # Copyright (c) 2006 Michael Koziarski
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ # this software and associated documentation files (the "Software"), to deal in the
6
+ # Software without restriction, including without limitation the rights to use,
7
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8
+ # Software, and to permit persons to whom the Software is furnished to do so,
9
+ # subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in all
12
+ # copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
18
+ # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
19
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+
21
+ require 'rubygems'
22
+ require 'xml/libxml'
23
+
24
+ class FasterXmlSimple
25
+ class << self
26
+ def xml_in(string, options={})
27
+ new(string, options).out
28
+ end
29
+ end
30
+
31
+ attr_reader :doc, :options, :current_element
32
+
33
+ def initialize(string, options)
34
+ @doc = parse(string)
35
+ @options = default_options.merge options
36
+ @current_element = nil
37
+ end
38
+
39
+ def out
40
+ {doc.root.name => collapse(doc.root, doc.root.name)}
41
+ end
42
+
43
+ private
44
+ def default_options
45
+ {'contentkey' => '__content__', 'forcearray' => []}
46
+ end
47
+
48
+ def collapse(element, key_name)
49
+ result = hash_of_attributes(element)
50
+ if text_node? element
51
+ text = collapse_text(element)
52
+ result.merge!(content_key => text) if text =~ /\S/
53
+ elsif element.children?
54
+ element.inject(result) do |hash, child|
55
+ (hash[child.name] ||= []) << collapse(child, element.name) unless child.text?
56
+ hash
57
+ end
58
+ end
59
+ return nil if result.empty?
60
+ # Compact them to ensure it complies with the user's requests
61
+ inline_single_element_arrays(result)
62
+ suppress_empty(result) if suppress_empty?
63
+
64
+ result
65
+ end
66
+
67
+ def content_key
68
+ options['contentkey']
69
+ end
70
+
71
+ def force_array?(key_name)
72
+ options['forcearray'].include?(key_name)
73
+ end
74
+
75
+ def inline_single_element_arrays(result)
76
+ result.each do |key, value|
77
+ if value.size == 1 && value.is_a?(Array) && !force_array?(key)
78
+ result[key] = value.first
79
+ end
80
+ end
81
+ end
82
+
83
+ def suppress_empty(result)
84
+ result.delete content_key if result[content_key] !~ /\S/
85
+ end
86
+
87
+ def suppress_empty?
88
+ options.has_key? 'suppressempty'
89
+ end
90
+
91
+ # a text node is one with 1 or more child nodes which are
92
+ # text nodes, and no non-text children
93
+ def text_node?(element)
94
+ !element.text? && element.all? {|c| c.text?}
95
+ end
96
+
97
+ # takes a text node, and collapses it into a string
98
+ def collapse_text(element)
99
+ element.map {|c| c.content } * ''
100
+ end
101
+
102
+ def hash_of_attributes(element)
103
+ result = {}
104
+ element.each_attr do |attribute|
105
+ name = attribute.name
106
+ name = [attribute.ns, attribute.name].join(':') if attribute.ns?
107
+ result[name] = attribute.value
108
+ end
109
+ result
110
+ end
111
+
112
+ def parse(string)
113
+ XML::Parser.string(string).parse
114
+ end
115
+ end