steamcannon-s3 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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