steamcannon-s3 0.3.2

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/s3/bucket.rb ADDED
@@ -0,0 +1,153 @@
1
+ module S3
2
+ class Bucket
3
+ include Parser
4
+ include Proxies
5
+ extend Forwardable
6
+
7
+ attr_reader :name, :service
8
+
9
+ def_instance_delegators :service, :service_request
10
+ private_class_method :new
11
+
12
+ # Retrieves the bucket information from the server. Raises an
13
+ # S3::Error exception if the bucket doesn't exist or you don't
14
+ # have access to it, etc.
15
+ def retrieve
16
+ bucket_headers
17
+ self
18
+ end
19
+
20
+ # Returns location of the bucket, e.g. "EU"
21
+ def location(reload = false)
22
+ if reload or @location.nil?
23
+ @location = location_constraint
24
+ else
25
+ @location
26
+ end
27
+ end
28
+
29
+ # Compares the bucket with other bucket. Returns true if the names
30
+ # of the buckets are the same, and both have the same services
31
+ # (see Service equality)
32
+ def ==(other)
33
+ self.name == other.name and self.service == other.service
34
+ end
35
+
36
+ # Similar to retrieve, but catches S3::Error::NoSuchBucket
37
+ # exceptions and returns false instead.
38
+ def exists?
39
+ retrieve
40
+ true
41
+ rescue Error::NoSuchBucket
42
+ false
43
+ end
44
+
45
+ # Destroys given bucket. Raises an S3::Error::BucketNotEmpty
46
+ # exception if the bucket is not empty. You can destroy non-empty
47
+ # bucket passing true (to force destroy)
48
+ def destroy(force = false)
49
+ delete_bucket
50
+ true
51
+ rescue Error::BucketNotEmpty
52
+ if force
53
+ objects.destroy_all
54
+ retry
55
+ else
56
+ raise
57
+ end
58
+ end
59
+
60
+ # Saves the newly built bucket. Optionally you can pass location
61
+ # of the bucket (<tt>:eu</tt> or <tt>:us</tt>)
62
+ def save(location = nil)
63
+ create_bucket_configuration(location)
64
+ true
65
+ end
66
+
67
+ # Returns true if the name of the bucket can be used like +VHOST+
68
+ # name. If the bucket contains characters like underscore it can't
69
+ # be used as +VHOST+ (e.g. <tt>bucket_name.s3.amazonaws.com</tt>)
70
+ def vhost?
71
+ "#@name.#{HOST}" =~ /\A#{URI::REGEXP::PATTERN::HOSTNAME}\Z/
72
+ end
73
+
74
+ # Returns host name of the bucket according (see #vhost? method)
75
+ def host
76
+ vhost? ? "#@name.#{HOST}" : "#{HOST}"
77
+ end
78
+
79
+ # Returns path prefix for non +VHOST+ bucket. Path prefix is used
80
+ # instead of +VHOST+ name, e.g. "bucket_name/"
81
+ def path_prefix
82
+ vhost? ? "" : "#@name/"
83
+ end
84
+
85
+ # Returns the objects in the bucket and caches the result (see
86
+ # #reload method).
87
+ def objects
88
+ MethodProxy.new(self, :list_bucket, :extend => ObjectsExtension)
89
+ end
90
+
91
+ def inspect #:nodoc:
92
+ "#<#{self.class}:#{name}>"
93
+ end
94
+
95
+ private
96
+
97
+ attr_writer :service
98
+
99
+ def location_constraint
100
+ response = bucket_request(:get, :params => { :location => nil })
101
+ parse_location_constraint(response.body)
102
+ end
103
+
104
+ def list_bucket(options = {})
105
+ response = bucket_request(:get, :params => options)
106
+ objects_attributes = parse_list_bucket_result(response.body)
107
+ objects_attributes.map { |object_attributes| Object.send(:new, self, object_attributes) }
108
+ end
109
+
110
+ def bucket_headers(options = {})
111
+ response = bucket_request(:head, :params => options)
112
+ rescue Error::ResponseError => e
113
+ if e.response.code.to_i == 404
114
+ raise Error::ResponseError.exception("NoSuchBucket").new("The specified bucket does not exist.", nil)
115
+ else
116
+ raise e
117
+ end
118
+ end
119
+
120
+ def create_bucket_configuration(location = nil)
121
+ location = location.to_s.upcase if location
122
+ options = { :headers => {} }
123
+ if location and location != "US"
124
+ options[:body] = "<CreateBucketConfiguration><LocationConstraint>#{location}</LocationConstraint></CreateBucketConfiguration>"
125
+ options[:headers][:content_type] = "application/xml"
126
+ end
127
+ bucket_request(:put, options)
128
+ end
129
+
130
+ def delete_bucket
131
+ bucket_request(:delete)
132
+ end
133
+
134
+ def initialize(service, name) #:nodoc:
135
+ self.service = service
136
+ self.name = name
137
+ end
138
+
139
+ def name=(name)
140
+ raise ArgumentError.new("Invalid bucket name: #{name}") unless name_valid?(name)
141
+ @name = name
142
+ end
143
+
144
+ def bucket_request(method, options = {})
145
+ path = "#{path_prefix}#{options[:path]}"
146
+ service_request(method, options.merge(:host => host, :path => path))
147
+ end
148
+
149
+ def name_valid?(name)
150
+ name =~ /\A[a-z0-9][a-z0-9\._-]{2,254}\Z/i and name !~ /\A#{URI::REGEXP::PATTERN::IPV4ADDR}\Z/
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,27 @@
1
+ module S3
2
+ module BucketsExtension
3
+ # Builds new bucket with given name
4
+ def build(name)
5
+ Bucket.send(:new, proxy_owner, name)
6
+ end
7
+
8
+ # Finds the bucket with given name
9
+ def find_first(name)
10
+ bucket = build(name)
11
+ bucket.retrieve
12
+ end
13
+ alias :find :find_first
14
+
15
+ # Find all buckets in the service
16
+ def find_all
17
+ proxy_target
18
+ end
19
+
20
+ # Destroy all buckets in the service. Doesn't destroy non-empty
21
+ # buckets by default, pass true to force destroy (USE WITH
22
+ # CARE!).
23
+ def destroy_all(force = false)
24
+ proxy_target.each { |bucket| bucket.destroy(force) }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,224 @@
1
+ module S3
2
+
3
+ # Class responsible for handling connections to amazon hosts
4
+ class Connection
5
+ include Parser
6
+
7
+ attr_accessor :access_key_id, :secret_access_key, :use_ssl, :timeout, :debug, :proxy
8
+ alias :use_ssl? :use_ssl
9
+
10
+ # Creates new connection object.
11
+ #
12
+ # ==== Options
13
+ # * <tt>:access_key_id</tt> - Access key id (REQUIRED)
14
+ # * <tt>:secret_access_key</tt> - Secret access key (REQUIRED)
15
+ # * <tt>:use_ssl</tt> - Use https or http protocol (false by
16
+ # default)
17
+ # * <tt>:debug</tt> - Display debug information on the STDOUT
18
+ # (false by default)
19
+ # * <tt>:timeout</tt> - Timeout to use by the Net::HTTP object
20
+ # (60 by default)
21
+ # * <tt>:proxy</tt> - Hash for Net::HTTP Proxy settings
22
+ # { :host => "proxy.mydomain.com", :port => "80, :user => "user_a", :password => "secret" }
23
+ def initialize(options = {})
24
+ @access_key_id = options.fetch(:access_key_id)
25
+ @secret_access_key = options.fetch(:secret_access_key)
26
+ @use_ssl = options.fetch(:use_ssl, false)
27
+ @debug = options.fetch(:debug, false)
28
+ @timeout = options.fetch(:timeout, 60)
29
+ @proxy = options.fetch(:proxy, nil)
30
+ end
31
+
32
+ # Makes request with given HTTP method, sets missing parameters,
33
+ # adds signature to request header and returns response object
34
+ # (Net::HTTPResponse)
35
+ #
36
+ # ==== Parameters
37
+ # * <tt>method</tt> - HTTP Method symbol, can be <tt>:get</tt>,
38
+ # <tt>:put</tt>, <tt>:delete</tt>
39
+ #
40
+ # ==== Options:
41
+ # * <tt>:host</tt> - Hostname to connecto to, defaults
42
+ # to <tt>s3.amazonaws.com</tt>
43
+ # * <tt>:path</tt> - path to send request to (REQUIRED)
44
+ # * <tt>:body</tt> - Request body, only meaningful for
45
+ # <tt>:put</tt> request
46
+ # * <tt>:params</tt> - Parameters to add to query string for
47
+ # request, can be String or Hash
48
+ # * <tt>:headers</tt> - Hash of headers fields to add to request
49
+ # header
50
+ #
51
+ # ==== Returns
52
+ # Net::HTTPResponse object -- response from the server
53
+ def request(method, options)
54
+ host = options.fetch(:host, HOST)
55
+ path = options.fetch(:path)
56
+ body = options.fetch(:body, "")
57
+ params = options.fetch(:params, {})
58
+ headers = options.fetch(:headers, {})
59
+
60
+ if params
61
+ params = params.is_a?(String) ? params : self.class.parse_params(params)
62
+ path << "?#{params}"
63
+ end
64
+
65
+ path = URI.escape(path)
66
+ request = request_class(method).new(path)
67
+
68
+ headers = self.class.parse_headers(headers)
69
+ headers.each do |key, value|
70
+ request[key] = value
71
+ end
72
+
73
+ request.body = body
74
+
75
+ send_request(host, request)
76
+ end
77
+
78
+ # Helper function to parser parameters and create single string of
79
+ # params added to questy string
80
+ #
81
+ # ==== Parameters
82
+ # * <tt>params</tt> - Hash of parameters
83
+ #
84
+ # ==== Returns
85
+ # String -- containing all parameters joined in one params string,
86
+ # i.e. <tt>param1=val&param2&param3=0</tt>
87
+ def self.parse_params(params)
88
+ interesting_keys = [:max_keys, :prefix, :marker, :delimiter, :location]
89
+
90
+ result = []
91
+ params.each do |key, value|
92
+ if interesting_keys.include?(key)
93
+ parsed_key = key.to_s.gsub("_", "-")
94
+ case value
95
+ when nil
96
+ result << parsed_key
97
+ else
98
+ result << "#{parsed_key}=#{value}"
99
+ end
100
+ end
101
+ end
102
+ result.join("&")
103
+ end
104
+
105
+ # Helper function to change headers from symbols, to in correct
106
+ # form (i.e. with '-' instead of '_')
107
+ #
108
+ # ==== Parameters
109
+ # * <tt>headers</tt> - Hash of pairs <tt>headername => value</tt>,
110
+ # where value can be Range (for Range header) or any other value
111
+ # which can be translated to string
112
+ #
113
+ # ==== Returns
114
+ # Hash of headers translated from symbol to string, containing
115
+ # only interesting headers
116
+ def self.parse_headers(headers)
117
+ interesting_keys = [:content_type, :x_amz_acl, :x_amz_storage_class, :range,
118
+ :if_modified_since, :if_unmodified_since,
119
+ :if_match, :if_none_match,
120
+ :content_disposition, :content_encoding,
121
+ :x_amz_copy_source, :x_amz_metadata_directive,
122
+ :x_amz_copy_source_if_match,
123
+ :x_amz_copy_source_if_none_match,
124
+ :x_amz_copy_source_if_unmodified_since,
125
+ :x_amz_copy_source_if_modified_since]
126
+
127
+ parsed_headers = {}
128
+ if headers
129
+ headers.each do |key, value|
130
+ if interesting_keys.include?(key)
131
+ parsed_key = key.to_s.gsub("_", "-")
132
+ parsed_value = value
133
+ case value
134
+ when Range
135
+ parsed_value = "bytes=#{value.first}-#{value.last}"
136
+ end
137
+ parsed_headers[parsed_key] = parsed_value
138
+ end
139
+ end
140
+ end
141
+ parsed_headers
142
+ end
143
+
144
+ private
145
+
146
+ def request_class(method)
147
+ case method
148
+ when :get
149
+ request_class = Net::HTTP::Get
150
+ when :head
151
+ request_class = Net::HTTP::Head
152
+ when :put
153
+ request_class = Net::HTTP::Put
154
+ when :delete
155
+ request_class = Net::HTTP::Delete
156
+ end
157
+ end
158
+
159
+ def port
160
+ use_ssl ? 443 : 80
161
+ end
162
+
163
+ def proxy_settings
164
+ @proxy.values_at(:host, :port, :user, :password) unless @proxy.nil? || @proxy.empty?
165
+ end
166
+
167
+ def http(host)
168
+ http = Net::HTTP.new(host, port, *proxy_settings)
169
+ http.set_debug_output(STDOUT) if @debug
170
+ http.use_ssl = @use_ssl
171
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @use_ssl
172
+ http.read_timeout = @timeout if @timeout
173
+ http
174
+ end
175
+
176
+ def send_request(host, request, skip_authorization = false)
177
+ response = http(host).start do |http|
178
+ host = http.address
179
+
180
+ request['Date'] ||= Time.now.httpdate
181
+
182
+ if request.body
183
+ request["Content-Type"] ||= "application/octet-stream"
184
+ request["Content-MD5"] = Base64.encode64(Digest::MD5.digest(request.body)).chomp unless request.body.empty?
185
+ end
186
+
187
+ unless skip_authorization
188
+ request["Authorization"] = Signature.generate(:host => host,
189
+ :request => request,
190
+ :access_key_id => access_key_id,
191
+ :secret_access_key => secret_access_key)
192
+ end
193
+
194
+ http.request(request)
195
+ end
196
+
197
+ if response.code.to_i == 307
198
+ if response.body
199
+ doc = Document.new response.body
200
+ send_request(doc.elements['Error'].elements['Endpoint'].text, request, true)
201
+ end
202
+ else
203
+ handle_response(response)
204
+ end
205
+ end
206
+
207
+ def handle_response(response)
208
+ case response.code.to_i
209
+ when 200...300
210
+ response
211
+ when 300...600
212
+ if response.body.nil? || response.body.empty?
213
+ raise Error::ResponseError.new(nil, response)
214
+ else
215
+ code, message = parse_error(response.body)
216
+ raise Error::ResponseError.exception(code).new(message, response)
217
+ end
218
+ else
219
+ raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
220
+ end
221
+ response
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,110 @@
1
+ module S3
2
+ module Error
3
+
4
+ # All responses with a code between 300 and 599 that contain an
5
+ # <Error></Error> body are wrapped in an ErrorResponse which
6
+ # contains an Error object. This Error class generates a custom
7
+ # exception with the name of the xml Error and its message. All
8
+ # such runtime generated exception classes descend from
9
+ # ResponseError and contain the ErrorResponse object so that all
10
+ # code that makes a request can rescue ResponseError and get
11
+ # access to the ErrorResponse.
12
+ class ResponseError < StandardError
13
+ attr_reader :response
14
+
15
+ # Creates new S3::ResponseError.
16
+ #
17
+ # ==== Parameters
18
+ # * <tt>message</tt> - what went wrong
19
+ # * <tt>response</tt> - Net::HTTPResponse object or nil
20
+ def initialize(message, response)
21
+ @response = response
22
+ super(message)
23
+ end
24
+
25
+ # Factory for all other Exception classes in module, each for
26
+ # every error response available from AmazonAWS
27
+ #
28
+ # ==== Parameters
29
+ # * <tt>code</tt> - Code name of exception
30
+ #
31
+ # ==== Returns
32
+ # Descendant of ResponseError suitable for that exception code
33
+ # or ResponseError class if no class found
34
+ def self.exception(code)
35
+ S3::Error.const_get(code)
36
+ rescue NameError
37
+ ResponseError
38
+ end
39
+ end
40
+
41
+ #:stopdoc:
42
+
43
+ class AccessDenied < ResponseError; end
44
+ class AccountProblem < ResponseError; end
45
+ class AmbiguousGrantByEmailAddress < ResponseError; end
46
+ class BadDigest < ResponseError; end
47
+ class BucketAlreadyExists < ResponseError; end
48
+ class BucketAlreadyOwnedByYou < ResponseError; end
49
+ class BucketNotEmpty < ResponseError; end
50
+ class CredentialsNotSupported < ResponseError; end
51
+ class CrossLocationLoggingProhibited < ResponseError; end
52
+ class EntityTooSmall < ResponseError; end
53
+ class EntityTooLarge < ResponseError; end
54
+ class ExpiredToken < ResponseError; end
55
+ class IncompleteBody < ResponseError; end
56
+ class IncorrectNumberOfFilesInPostRequestPOST < ResponseError; end
57
+ class InlineDataTooLarge < ResponseError; end
58
+ class InternalError < ResponseError; end
59
+ class InvalidAccessKeyId < ResponseError; end
60
+ class InvalidAddressingHeader < ResponseError; end
61
+ class InvalidArgument < ResponseError; end
62
+ class InvalidBucketName < ResponseError; end
63
+ class InvalidDigest < ResponseError; end
64
+ class InvalidLocationConstraint < ResponseError; end
65
+ class InvalidPayer < ResponseError; end
66
+ class InvalidPolicyDocument < ResponseError; end
67
+ class InvalidRange < ResponseError; end
68
+ class InvalidSecurity < ResponseError; end
69
+ class InvalidSOAPRequest < ResponseError; end
70
+ class InvalidStorageClass < ResponseError; end
71
+ class InvalidTargetBucketForLogging < ResponseError; end
72
+ class InvalidToken < ResponseError; end
73
+ class InvalidURI < ResponseError; end
74
+ class KeyTooLong < ResponseError; end
75
+ class MalformedACLError < ResponseError; end
76
+ class MalformedACLError < ResponseError; end
77
+ class MalformedPOSTRequest < ResponseError; end
78
+ class MalformedXML < ResponseError; end
79
+ class MaxMessageLengthExceeded < ResponseError; end
80
+ class MaxPostPreDataLengthExceededErrorYour < ResponseError; end
81
+ class MetadataTooLarge < ResponseError; end
82
+ class MethodNotAllowed < ResponseError; end
83
+ class MissingAttachment < ResponseError; end
84
+ class MissingContentLength < ResponseError; end
85
+ class MissingRequestBodyError < ResponseError; end
86
+ class MissingSecurityElement < ResponseError; end
87
+ class MissingSecurityHeader < ResponseError; end
88
+ class NoLoggingStatusForKey < ResponseError; end
89
+ class NoSuchBucket < ResponseError; end
90
+ class NoSuchKey < ResponseError; end
91
+ class NotImplemented < ResponseError; end
92
+ class NotSignedUp < ResponseError; end
93
+ class OperationAborted < ResponseError; end
94
+ class PermanentRedirect < ResponseError; end
95
+ class PreconditionFailed < ResponseError; end
96
+ class Redirect < ResponseError; end
97
+ class RequestIsNotMultiPartContent < ResponseError; end
98
+ class RequestTimeout < ResponseError; end
99
+ class RequestTimeTooSkewed < ResponseError; end
100
+ class RequestTorrentOfBucketError < ResponseError; end
101
+ class SignatureDoesNotMatch < ResponseError; end
102
+ class SlowDown < ResponseError; end
103
+ class TemporaryRedirect < ResponseError; end
104
+ class TokenRefreshRequired < ResponseError; end
105
+ class TooManyBuckets < ResponseError; end
106
+ class UnexpectedContent < ResponseError; end
107
+ class UnresolvableGrantByEmailAddress < ResponseError; end
108
+ class UserKeyMustBeSpecified < ResponseError; end
109
+ end
110
+ end