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,69 @@
1
+ module AWS
2
+ module S3
3
+ # Anything you do that makes a request to S3 could result in an error. If it does, the AWS::S3 library will raise an exception
4
+ # specific to the error. All exception that are raised as a result of a request returning an error response inherit from the
5
+ # ResponseError exception. So should you choose to rescue any such exception, you can simple rescue ResponseError.
6
+ #
7
+ # Say you go to delete a bucket, but the bucket turns out to not be empty. This results in a BucketNotEmpty error (one of the many
8
+ # errors listed at http://docs.amazonwebservices.com/AmazonS3/2006-03-01/ErrorCodeList.html):
9
+ #
10
+ # begin
11
+ # Bucket.delete('jukebox')
12
+ # rescue ResponseError => error
13
+ # # ...
14
+ # end
15
+ #
16
+ # Once you've captured the exception, you can extract the error message from S3, as well as the full error response, which includes
17
+ # things like the HTTP response code:
18
+ #
19
+ # error
20
+ # # => #<AWS::S3::BucketNotEmpty The bucket you tried to delete is not empty>
21
+ # error.message
22
+ # # => "The bucket you tried to delete is not empty"
23
+ # error.response.code
24
+ # # => 409
25
+ #
26
+ # You could use this information to redisplay the error in a way you see fit, or just to log the error and continue on.
27
+ class Error
28
+ #:stopdoc:
29
+ attr_accessor :response
30
+ def initialize(error, response = nil)
31
+ @error = error
32
+ @response = response
33
+ @container = AWS::S3
34
+ find_or_create_exception!
35
+ end
36
+
37
+ def raise
38
+ Kernel.raise exception.new(message, response)
39
+ end
40
+
41
+ private
42
+ attr_reader :error, :exception, :container
43
+
44
+ def find_or_create_exception!
45
+ @exception = container.const_defined?(code) ? find_exception : create_exception
46
+ end
47
+
48
+ def find_exception
49
+ exception_class = container.const_get(code)
50
+ Kernel.raise ExceptionClassClash.new(exception_class) unless exception_class.ancestors.include?(ResponseError)
51
+ exception_class
52
+ end
53
+
54
+ def create_exception
55
+ container.const_set(code, Class.new(ResponseError))
56
+ end
57
+
58
+ def method_missing(method, *args, &block)
59
+ # We actually want nil if the attribute is nil. So we use has_key? rather than [] + ||.
60
+ if error.has_key?(method.to_s)
61
+ error[method.to_s]
62
+ else
63
+ super
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ #:startdoc:
@@ -0,0 +1,130 @@
1
+ module AWS
2
+ module S3
3
+
4
+ # Abstract super class of all AWS::S3 exceptions
5
+ class S3Exception < StandardError
6
+ end
7
+
8
+ # All responses with a code between 300 and 599 that contain an <Error></Error> body are wrapped in an
9
+ # ErrorResponse which contains an Error object. This Error class generates a custom exception with the name
10
+ # of the xml Error and its message. All such runtime generated exception classes descend from ResponseError
11
+ # and contain the ErrorResponse object so that all code that makes a request can rescue ResponseError and get
12
+ # access to the ErrorResponse.
13
+ class ResponseError < S3Exception
14
+ attr_reader :response
15
+ def initialize(message, response)
16
+ @response = response
17
+ super(message)
18
+ end
19
+ end
20
+
21
+ #:stopdoc:
22
+
23
+ # Most ResponseError's are created just time on a need to have basis, but we explicitly define the
24
+ # InternalError exception because we want to explicitly rescue InternalError in some cases.
25
+ class InternalError < ResponseError
26
+ end
27
+
28
+ class NoSuchKey < ResponseError
29
+ end
30
+
31
+ # Abstract super class for all invalid options.
32
+ class InvalidOption < S3Exception
33
+ end
34
+
35
+ # Raised if an invalid value is passed to the <tt>:access</tt> option when creating a Bucket or an S3Object.
36
+ class InvalidAccessControlLevel < InvalidOption
37
+ def initialize(valid_levels, access_level)
38
+ super("Valid access control levels are #{valid_levels.inspect}. You specified `#{access_level}'.")
39
+ end
40
+ end
41
+
42
+ # Raised if either the access key id or secret access key arguments are missing when establishing a connection.
43
+ class MissingAccessKey < InvalidOption
44
+ def initialize(missing_keys)
45
+ key_list = missing_keys.map {|key| key.to_s}.join(' and the ')
46
+ super("You did not provide both required access keys. Please provide the #{key_list}.")
47
+ end
48
+ end
49
+
50
+ # Raised if a request is attempted before any connections have been established.
51
+ class NoConnectionEstablished < S3Exception
52
+ end
53
+
54
+ # Raised if an unrecognized option is passed when establishing a connection.
55
+ class InvalidConnectionOption < InvalidOption
56
+ def initialize(invalid_options)
57
+ message = "The following connection options are invalid: #{invalid_options.join(', ')}. " +
58
+ "The valid connection options are: #{Connection::Options.valid_options.join(', ')}."
59
+ super(message)
60
+ end
61
+ end
62
+
63
+ # Raised if an invalid bucket name is passed when creating a new Bucket.
64
+ class InvalidBucketName < S3Exception
65
+ def initialize(invalid_name)
66
+ message = "`#{invalid_name}' is not a valid bucket name. " +
67
+ "Bucket names must be between 3 and 255 bytes and " +
68
+ "can contain letters, numbers, dashes and underscores."
69
+ super(message)
70
+ end
71
+ end
72
+
73
+ # Raised if an invalid key name is passed when creating an S3Object.
74
+ class InvalidKeyName < S3Exception
75
+ def initialize(invalid_name)
76
+ message = "`#{invalid_name}' is not a valid key name. " +
77
+ "Key names must be no more than 1024 bytes long."
78
+ super(message)
79
+ end
80
+ end
81
+
82
+ # Raised if an invalid value is assigned to an S3Object's specific metadata name.
83
+ class InvalidMetadataValue < S3Exception
84
+ def initialize(invalid_names)
85
+ message = "The following metadata names have invalid values: #{invalid_names.join(', ')}. " +
86
+ "Metadata can not be larger than 2kilobytes."
87
+ super(message)
88
+ end
89
+ end
90
+
91
+ # Raised if the current bucket can not be infered when not explicitly specifying the target bucket in the calling
92
+ # method's arguments.
93
+ class CurrentBucketNotSpecified < S3Exception
94
+ def initialize(address)
95
+ message = "No bucket name can be inferred from your current connection's address (`#{address}')"
96
+ super(message)
97
+ end
98
+ end
99
+
100
+ # Raised when an orphaned S3Object belonging to no bucket tries to access its (non-existant) bucket.
101
+ class NoBucketSpecified < S3Exception
102
+ def initialize
103
+ super('The current object must have its bucket set')
104
+ end
105
+ end
106
+
107
+ # Raised if an attempt is made to save an S3Object that does not have a key set.
108
+ class NoKeySpecified < S3Exception
109
+ def initialize
110
+ super('The current object must have its key set')
111
+ end
112
+ end
113
+
114
+ # Raised if you try to save a deleted object.
115
+ class DeletedObject < S3Exception
116
+ def initialize
117
+ super('You can not save a deleted object')
118
+ end
119
+ end
120
+
121
+ class ExceptionClassClash < S3Exception #:nodoc:
122
+ def initialize(klass)
123
+ message = "The exception class you tried to create (`#{klass}') exists and is not an exception"
124
+ super(message)
125
+ end
126
+ end
127
+
128
+ #:startdoc:
129
+ end
130
+ end
@@ -0,0 +1,186 @@
1
+ #:stopdoc:
2
+
3
+ class Hash
4
+ def to_query_string(include_question_mark = true)
5
+ return '' if empty?
6
+ query_string = ''
7
+ query_string << '?' if include_question_mark
8
+ query_string << inject([]) do |parameters, (key, value)|
9
+ parameters << [key, value].map {|element| CGI.escape(element.to_s)}.join('=')
10
+ end.join('&')
11
+ end
12
+
13
+ def to_normalized_options
14
+ # Convert all option names to downcased strings, and replace underscores with hyphens
15
+ inject({}) do |normalized_options, (name, value)|
16
+ normalized_options[name.to_header] = value.to_s
17
+ normalized_options
18
+ end
19
+ end
20
+
21
+ def to_normalized_options!
22
+ replace(to_normalized_options)
23
+ end
24
+ end
25
+
26
+ class String
27
+ def previous!
28
+ self[-1] = self[-1] - 1
29
+ self
30
+ end
31
+
32
+ def previous
33
+ dup.previous!
34
+ end
35
+
36
+ def to_header
37
+ downcase.gsub('_', '-')
38
+ end
39
+ end
40
+
41
+ class Symbol
42
+ def to_header
43
+ to_s.to_header
44
+ end
45
+ end
46
+
47
+ module Kernel
48
+ def __method__(depth = 0)
49
+ caller[depth][/`([^']+)'/, 1]
50
+ end if RUBY_VERSION < '1.9'
51
+
52
+ def memoize(reload = false, storage = nil)
53
+ storage = "@#{storage || __method__(1)}"
54
+ if reload
55
+ instance_variable_set(storage, nil)
56
+ else
57
+ if cache = instance_variable_get(storage)
58
+ return cache
59
+ end
60
+ end
61
+ instance_variable_set(storage, yield)
62
+ end
63
+
64
+ def require_library_or_gem(library)
65
+ require library
66
+ rescue LoadError => library_not_installed
67
+ begin
68
+ require 'rubygems'
69
+ require library
70
+ rescue LoadError
71
+ raise library_not_installed
72
+ end
73
+ end
74
+ end
75
+
76
+ class Module
77
+ def memoized(method_name)
78
+ original_method = "unmemoized_#{method_name}_#{Time.now.to_i}"
79
+ alias_method original_method, method_name
80
+ module_eval(<<-EVAL, __FILE__, __LINE__)
81
+ def #{method_name}(reload = false, *args, &block)
82
+ memoize(reload) do
83
+ send(:#{original_method}, *args, &block)
84
+ end
85
+ end
86
+ EVAL
87
+ end
88
+
89
+ def constant(name, value)
90
+ unless const_defined?(name)
91
+ const_set(name, value)
92
+ module_eval(<<-EVAL, __FILE__, __LINE__)
93
+ def self.#{name.to_s.downcase}
94
+ #{name.to_s}
95
+ end
96
+ EVAL
97
+ end
98
+ end
99
+
100
+ # Transforms MarcelBucket into
101
+ #
102
+ # class MarcelBucket < AWS::S3::Bucket
103
+ # set_current_bucket_to 'marcel'
104
+ # end
105
+ def const_missing_from_s3_library(sym)
106
+ if sym.to_s =~ /^(\w+)(Bucket|S3Object)$/
107
+ const = const_set(sym, Class.new(AWS::S3.const_get($2)))
108
+ const.current_bucket = $1.underscore
109
+ const
110
+ else
111
+ const_missing_not_from_s3_library(sym)
112
+ end
113
+ end
114
+ alias_method :const_missing_not_from_s3_library, :const_missing
115
+ alias_method :const_missing, :const_missing_from_s3_library
116
+ end
117
+
118
+ module SelectiveAttributeProxy
119
+ def self.included(klass)
120
+ klass.extend(ClassMethods)
121
+ klass.class_eval(<<-EVAL, __FILE__, __LINE__)
122
+ # Default name for attribute storage
123
+ @@attribute_proxy = :attributes
124
+ @@attribute_proxy_options = {:exclusively => true}
125
+
126
+ def self.attribute_proxy
127
+ @@attribute_proxy
128
+ end
129
+
130
+ def self.attribute_proxy_options
131
+ @@attribute_proxy_options
132
+ end
133
+
134
+ private
135
+ # By default proxy all attributes
136
+ def proxiable_attribute?(name)
137
+ return true unless self.class.attribute_proxy_options[:exclusively]
138
+ send(self.class.attribute_proxy).has_key?(name)
139
+ end
140
+
141
+ def method_missing(method, *args, &block)
142
+ # Autovivify attribute storage
143
+ if method == self.class.attribute_proxy
144
+ ivar = "@\#{method}"
145
+ instance_variable_set(ivar, {}) unless instance_variable_get(ivar).is_a?(Hash)
146
+ instance_variable_get(ivar)
147
+ # Delegate to attribute storage
148
+ elsif method.to_s =~ /^(\\w+)(=?)$/ && proxiable_attribute?($1)
149
+ attributes_hash_name = self.class.attribute_proxy
150
+ $2.empty? ? send(attributes_hash_name)[$1] : send(attributes_hash_name)[$1] = args.first
151
+ else
152
+ super
153
+ end
154
+ end
155
+ EVAL
156
+ end
157
+
158
+ module ClassMethods
159
+ def proxy_to(attribute_name, options = {})
160
+ if attribute_name.is_a?(Hash)
161
+ options = attribute_name
162
+ else
163
+ class_variable_set(:@@attribute_proxy, attribute_name)
164
+ end
165
+ class_variable_set(:@@attribute_proxy_options, options)
166
+ end
167
+ end
168
+ end
169
+
170
+ class String
171
+ def underscore
172
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
173
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
174
+ downcase
175
+ end
176
+ end
177
+
178
+ class XmlGenerator < String #:nodoc:
179
+ attr_reader :xml
180
+ def initialize
181
+ @xml = Builder::XmlMarkup.new(:indent => 2, :target => self)
182
+ super()
183
+ build
184
+ end
185
+ end
186
+ #:startdoc:
@@ -0,0 +1,163 @@
1
+ module AWS
2
+ module S3
3
+ # A bucket can be set to log the requests made on it. By default logging is turned off. You can check if a bucket has logging enabled:
4
+ #
5
+ # Bucket.logging_enabled_for? 'jukebox'
6
+ # # => false
7
+ #
8
+ # Enabling it is easy:
9
+ #
10
+ # Bucket.enable_logging_for('jukebox')
11
+ #
12
+ # Unless you specify otherwise, logs will be written to the bucket you want to log. The logs are just like any other object. By default they will start with the prefix 'log-'. You can customize what bucket you want the logs to be delivered to, as well as customize what the log objects' key is prefixed with by setting the <tt>target_bucket</tt> and <tt>target_prefix</tt> option:
13
+ #
14
+ # Bucket.enable_logging_for(
15
+ # 'jukebox', 'target_bucket' => 'jukebox-logs'
16
+ # )
17
+ #
18
+ # Now instead of logging right into the jukebox bucket, the logs will go into the bucket called jukebox-logs.
19
+ #
20
+ # Once logs have accumulated, you can access them using the <tt>log</tt> method:
21
+ #
22
+ # pp Bucket.logs('jukebox')
23
+ # [#<AWS::S3::S3Object '/jukebox-logs/log-2006-11-14-07-15-24-2061C35880A310A1'>,
24
+ # #<AWS::S3::S3Object '/jukebox-logs/log-2006-11-14-08-15-27-D8EEF536EC09E6B3'>,
25
+ # #<AWS::S3::S3Object '/jukebox-logs/log-2006-11-14-08-15-29-355812B2B15BD789'>]
26
+ #
27
+ # Disabling logging is just as simple as enabling it:
28
+ #
29
+ # Bucket.disable_logging_for('jukebox')
30
+ module Logging
31
+ # Logging status captures information about the calling bucket's logging settings. If logging is enabled for the bucket
32
+ # the status object will indicate what bucket the logs are written to via the <tt>target_bucket</tt> method as well as
33
+ # the logging key prefix with via <tt>target_prefix</tt>.
34
+ #
35
+ # See the documentation for Logging::Management::ClassMethods for more information on how to get the logging status of a bucket.
36
+ class Status
37
+ include SelectiveAttributeProxy #:nodoc:
38
+ attr_reader :enabled
39
+ alias_method :logging_enabled?, :enabled
40
+
41
+ def initialize(attributes = {}) #:nodoc:
42
+ attributes = {'target_bucket' => nil, 'target_prefix' => nil}.merge(attributes)
43
+ @enabled = attributes.has_key?('logging_enabled')
44
+ @attributes = attributes.delete('logging_enabled') || attributes
45
+ end
46
+
47
+ def to_xml #:nodoc:
48
+ Builder.new(self).to_s
49
+ end
50
+
51
+ private
52
+ attr_reader :attributes
53
+
54
+ class Builder < XmlGenerator #:nodoc:
55
+ attr_reader :logging_status
56
+ def initialize(logging_status)
57
+ @logging_status = logging_status
58
+ super()
59
+ end
60
+
61
+ def build
62
+ xml.tag!('BucketLoggingStatus', 'xmlns' => 'http://s3.amazonaws.com/doc/2006-03-01/') do
63
+ if logging_status.target_bucket && logging_status.target_prefix
64
+ xml.LoggingEnabled do
65
+ xml.TargetBucket logging_status.target_bucket
66
+ xml.TargetPrefix logging_status.target_prefix
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ module Management #:nodoc:
75
+ def self.included(klass) #:nodoc:
76
+ klass.extend(ClassMethods)
77
+ klass.extend(LoggingGrants)
78
+ end
79
+
80
+ module ClassMethods
81
+ # Returns the logging status for the bucket named <tt>name</tt>. From the logging status you can determine the bucket logs are delivered to
82
+ # and what the bucket object's keys are prefixed with. For more information see the Logging::Status class.
83
+ #
84
+ # Bucket.logging_status_for 'marcel'
85
+ def logging_status_for(name = nil, status = nil)
86
+ if name.is_a?(Status)
87
+ status = name
88
+ name = nil
89
+ end
90
+
91
+ path = path(name) << '?logging'
92
+ status ? put(path, {}, status.to_xml) : Status.new(get(path).parsed)
93
+ end
94
+ alias_method :logging_status, :logging_status_for
95
+
96
+ # Enables logging for the bucket named <tt>name</tt>. You can specify what bucket to log to with the <tt>'target_bucket'</tt> option as well
97
+ # as what prefix to add to the log files with the <tt>'target_prefix'</tt> option. Unless you specify otherwise, logs will be delivered to
98
+ # the same bucket that is being logged and will be prefixed with <tt>log-</tt>.
99
+ def enable_logging_for(name = nil, options = {})
100
+ name = bucket_name(name)
101
+ default_options = {'target_bucket' => name, 'target_prefix' => 'log-'}
102
+ options = default_options.merge(options)
103
+ grant_logging_access_to_target_bucket(options['target_bucket'])
104
+ logging_status(name, Status.new(options))
105
+ end
106
+ alias_method :enable_logging, :enable_logging_for
107
+
108
+ # Disables logging for the bucket named <tt>name</tt>.
109
+ def disable_logging_for(name = nil)
110
+ logging_status(bucket_name(name), Status.new)
111
+ end
112
+ alias_method :disable_logging, :disable_logging_for
113
+
114
+ # Returns true if logging has been enabled for the bucket named <tt>name</tt>.
115
+ def logging_enabled_for?(name = nil)
116
+ logging_status(bucket_name(name)).logging_enabled?
117
+ end
118
+ alias_method :logging_enabled?, :logging_enabled_for?
119
+
120
+ # Returns the collection of logs for the bucket named <tt>name</tt>.
121
+ #
122
+ # Bucket.logs_for 'marcel'
123
+ def logs_for(name = nil)
124
+ name = bucket_name(name)
125
+ logging_status = logging_status_for(name)
126
+ return [] unless logging_status.logging_enabled?
127
+ objects(logging_status.target_bucket, :prefix => logging_status.target_prefix)
128
+ end
129
+ alias_method :logs, :logs_for
130
+ end
131
+
132
+ module LoggingGrants #:nodoc:
133
+ def grant_logging_access_to_target_bucket(target_bucket)
134
+ acl = acl(target_bucket)
135
+ acl.grants << ACL::Grant.grant(:logging_write)
136
+ acl.grants << ACL::Grant.grant(:logging_read_acp)
137
+ acl(target_bucket, acl)
138
+ end
139
+ end
140
+
141
+ def logging_status
142
+ self.class.logging_status_for(name)
143
+ end
144
+
145
+ def enable_logging(*args)
146
+ self.class.enable_logging_for(name, *args)
147
+ end
148
+
149
+ def disable_logging(*args)
150
+ self.class.disable_logging_for(name, *args)
151
+ end
152
+
153
+ def logging_enabled?
154
+ self.class.logging_enabled_for?(name)
155
+ end
156
+
157
+ def logs
158
+ self.class.logs_for(name)
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end