seanwalbran-scashin133-s3 0.3.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,253 @@
1
+ module S3
2
+
3
+ # Class responsible for handling objects stored in S3 buckets
4
+ class Object
5
+ include Parser
6
+ extend Forwardable
7
+
8
+ attr_accessor :content_type, :content_disposition, :content_encoding, :cache_control
9
+ attr_reader :last_modified, :etag, :size, :bucket, :key, :acl, :storage_class, :metadata
10
+ attr_writer :content
11
+
12
+ def_instance_delegators :bucket, :name, :service, :bucket_request, :vhost?, :host, :path_prefix
13
+ def_instance_delegators :service, :protocol, :port, :secret_access_key
14
+ private_class_method :new
15
+
16
+ # Compares the object with other object. Returns true if the key
17
+ # of the objects are the same, and both have the same buckets (see
18
+ # Bucket equality)
19
+ def ==(other)
20
+ other.equal?(self) || (other.instance_of?(self.class) && self.key == other.key && self.bucket == other.bucket)
21
+ end
22
+
23
+ # Returns full key of the object: e.g. <tt>bucket-name/object/key.ext</tt>
24
+ def full_key
25
+ [name, key].join("/")
26
+ end
27
+
28
+ # Assigns a new +key+ to the object, raises ArgumentError if given
29
+ # key is not valid key name
30
+ def key=(key)
31
+ raise ArgumentError.new("Invalid key name: #{key}") unless key_valid?(key)
32
+ @key ||= key
33
+ end
34
+
35
+ # Assigns a new ACL to the object. Please note that ACL is not
36
+ # retrieved from the server and set to "public-read" by default.
37
+ #
38
+ # ==== Example
39
+ # object.acl = :public_read
40
+ def acl=(acl)
41
+ @acl = acl.to_s.gsub("_", "-") if acl
42
+ end
43
+
44
+ # Assigns a new storage class (RRS) to the object. Please note
45
+ # that the storage class is not retrieved from the server and set
46
+ # to "STANDARD" by default.
47
+ #
48
+ # ==== Example
49
+ # object.storage_class = :reduced_redundancy
50
+ def storage_class=(storage_class)
51
+ @storage_class = storage_class.to_s.upcase if storage_class
52
+ end
53
+
54
+ # Retrieves the object from the server. Method is used to download
55
+ # object information only (content type, size and so on). It does
56
+ # NOT download the content of the object (use the #content method
57
+ # to do it).
58
+ def retrieve
59
+ object_headers
60
+ self
61
+ end
62
+
63
+ # Retrieves the object from the server, returns true if the object
64
+ # exists or false otherwise. Uses #retrieve method, but catches
65
+ # S3::Error::NoSuchKey exception and returns false when it happens
66
+ def exists?
67
+ retrieve
68
+ true
69
+ rescue Error::NoSuchKey
70
+ false
71
+ end
72
+
73
+ # Downloads the content of the object, and caches it. Pass true to
74
+ # clear the cache and download the object again.
75
+ def content(reload = false)
76
+ return @content if defined?(@content) and not reload
77
+ get_object
78
+ @content
79
+ end
80
+
81
+ # Saves the object, returns true if successfull.
82
+ def save
83
+ put_object
84
+ true
85
+ end
86
+
87
+ # Copies the file to another key and/or bucket.
88
+ #
89
+ # ==== Options
90
+ # * <tt>:key</tt> - New key to store object in
91
+ # * <tt>:bucket</tt> - New bucket to store object in (instance of
92
+ # S3::Bucket)
93
+ # * <tt>:acl</tt> - ACL of the copied object (default:
94
+ # "public-read")
95
+ # * <tt>:content_type</tt> - Content type of the copied object
96
+ # (default: "application/octet-stream")
97
+ def copy(options = {})
98
+ copy_object(options)
99
+ end
100
+
101
+ # Destroys the file on the server
102
+ def destroy
103
+ delete_object
104
+ true
105
+ end
106
+
107
+ # Returns Object's URL using protocol specified in service,
108
+ # e.g. <tt>http://domain.com.s3.amazonaws.com/key/with/path.extension</tt>
109
+ def url
110
+ URI.escape("#{protocol}#{host}/#{path_prefix}#{key}")
111
+ end
112
+
113
+ # Returns a temporary url to the object that expires on the
114
+ # timestamp given. Defaults to one hour expire time.
115
+ def temporary_url(expires_at = Time.now + 3600)
116
+ signature = Signature.generate_temporary_url_signature(:bucket => name,
117
+ :resource => key,
118
+ :expires_at => expires_at,
119
+ :secret_access_key => secret_access_key)
120
+
121
+ "#{url}?AWSAccessKeyId=#{self.bucket.service.access_key_id}&Expires=#{expires_at.to_i.to_s}&Signature=#{signature}"
122
+ end
123
+
124
+ # Returns Object's CNAME URL (without <tt>s3.amazonaws.com</tt>
125
+ # suffix) using protocol specified in Service,
126
+ # e.g. <tt>http://domain.com/key/with/path.extension</tt>. (you
127
+ # have to set the CNAME in your DNS before using the CNAME URL
128
+ # schema).
129
+ def cname_url
130
+ URI.escape("#{protocol}#{name}/#{key}") if bucket.vhost?
131
+ end
132
+
133
+ def inspect #:nodoc:
134
+ "#<#{self.class}:/#{name}/#{key}>"
135
+ end
136
+
137
+ private
138
+
139
+ attr_writer :last_modified, :etag, :size, :original_key, :bucket
140
+
141
+ def copy_object(options = {})
142
+ key = options[:key] or raise ArgumentError, "No key given"
143
+ raise ArgumentError.new("Invalid key name: #{key}") unless key_valid?(key)
144
+ bucket = options[:bucket] || self.bucket
145
+
146
+ headers = {}
147
+
148
+ headers[:x_amz_acl] = options[:acl] || acl || "public-read"
149
+ headers[:content_type] = options[:content_type] || content_type || "application/octet-stream"
150
+ headers[:content_encoding] = options[:content_encoding] if options[:content_encoding]
151
+ headers[:content_disposition] = options[:content_disposition] if options[:content_disposition]
152
+ headers[:cache_control] = options[:cache_control] if options[:cache_control]
153
+ headers[:x_amz_copy_source] = full_key
154
+ headers[:x_amz_metadata_directive] = options[:replace] == false ? "COPY" : "REPLACE"
155
+ headers[:x_amz_copy_source_if_match] = options[:if_match] if options[:if_match]
156
+ headers[:x_amz_copy_source_if_none_match] = options[:if_none_match] if options[:if_none_match]
157
+ headers[:x_amz_copy_source_if_unmodified_since] = options[:if_modified_since] if options[:if_modified_since]
158
+ headers[:x_amz_copy_source_if_modified_since] = options[:if_unmodified_since] if options[:if_unmodified_since]
159
+
160
+ response = bucket.send(:bucket_request, :put, :path => key, :headers => headers)
161
+ object_attributes = parse_copy_object_result(response.body)
162
+
163
+ object = Object.send(:new, bucket, object_attributes.merge(:key => key, :size => size))
164
+ object.acl = response["x-amz-acl"]
165
+ object.content_type = response["content-type"]
166
+ object.content_encoding = response["content-encoding"]
167
+ object.content_disposition = response["content-disposition"]
168
+ object.cache_control = response["cache-control"]
169
+ object
170
+ end
171
+
172
+ def get_object(options = {})
173
+ response = object_request(:get, options)
174
+ parse_headers(response)
175
+ end
176
+
177
+ def object_headers(options = {})
178
+ response = object_request(:head, options)
179
+ parse_headers(response)
180
+ rescue Error::ResponseError => e
181
+ if e.response.code.to_i == 404
182
+ raise Error::ResponseError.exception("NoSuchKey").new("The specified key does not exist.", nil)
183
+ else
184
+ raise e
185
+ end
186
+ end
187
+
188
+ def put_object
189
+ response = object_request(:put, :body => content, :headers => dump_headers)
190
+ parse_headers(response)
191
+ end
192
+
193
+ def delete_object(options = {})
194
+ object_request(:delete)
195
+ end
196
+
197
+ def initialize(bucket, options = {})
198
+ self.bucket = bucket
199
+ self.key = options[:key]
200
+ self.last_modified = options[:last_modified]
201
+ self.etag = options[:etag]
202
+ self.size = options[:size]
203
+ self.cache_control = options[:cache_control]
204
+ end
205
+
206
+ def object_request(method, options = {})
207
+ bucket_request(method, options.merge(:path => key))
208
+ end
209
+
210
+ def last_modified=(last_modified)
211
+ @last_modified = Time.parse(last_modified) if last_modified
212
+ end
213
+
214
+ def etag=(etag)
215
+ @etag = etag[1..-2] if etag
216
+ end
217
+
218
+ def key_valid?(key)
219
+ if (key.nil? or key.empty? or key =~ %r#//#)
220
+ false
221
+ else
222
+ true
223
+ end
224
+ end
225
+
226
+ def dump_headers
227
+ headers = {}
228
+ headers[:x_amz_acl] = @acl || "public-read"
229
+ headers[:x_amz_storage_class] = @storage_class || "STANDARD"
230
+ headers[:content_type] = @content_type || "application/octet-stream"
231
+ headers[:content_encoding] = @content_encoding if @content_encoding
232
+ headers[:content_disposition] = @content_disposition if @content_disposition
233
+ headers[:cache_control] = @cache_control if @cache_control
234
+ headers
235
+ end
236
+
237
+ def parse_headers(response)
238
+ @metadata = response.to_hash.select { |k, v| k.to_s.start_with?("x-amz-meta") }
239
+ self.etag = response["etag"] if response.key?("etag")
240
+ self.content_type = response["content-type"] if response.key?("content-type")
241
+ self.content_disposition = response["content-disposition"] if response.key?("content-disposition")
242
+ self.cache_control = response["cache-control"] if response.key?("cache-control")
243
+ self.content_encoding = response["content-encoding"] if response.key?("content-encoding")
244
+ self.last_modified = response["last-modified"] if response.key?("last-modified")
245
+ if response.key?("content-range")
246
+ self.size = response["content-range"].sub(/[^\/]+\//, "").to_i
247
+ else
248
+ self.size = response["content-length"]
249
+ self.content = response.body
250
+ end
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,37 @@
1
+ module S3
2
+ module ObjectsExtension
3
+ # Builds the object in the bucket with given key
4
+ def build(key)
5
+ Object.send(:new, proxy_owner, :key => key)
6
+ end
7
+
8
+ # Finds first object with given name or raises the exception if
9
+ # not found
10
+ def find_first(name)
11
+ object = build(name)
12
+ object.retrieve
13
+ end
14
+ alias :find :find_first
15
+
16
+ # Finds the objects in the bucket.
17
+ #
18
+ # ==== Options
19
+ # * <tt>:prefix</tt> - Limits the response to keys which begin
20
+ # with the indicated prefix
21
+ # * <tt>:marker</tt> - Indicates where in the bucket to begin
22
+ # listing
23
+ # * <tt>:max_keys</tt> - The maximum number of keys you'd like
24
+ # to see
25
+ # * <tt>:delimiter</tt> - Causes keys that contain the same
26
+ # string between the prefix and the first occurrence of the
27
+ # delimiter to be rolled up into a single result element
28
+ def find_all(options = {})
29
+ proxy_owner.send(:list_bucket, options)
30
+ end
31
+
32
+ # Destroys all keys in the bucket
33
+ def destroy_all
34
+ proxy_target.each { |object| object.destroy }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,52 @@
1
+ module S3
2
+ module Parser
3
+ include REXML
4
+
5
+ def rexml_document(xml)
6
+ xml.force_encoding(::Encoding::UTF_8) if xml.respond_to? :force_encoding
7
+ Document.new(xml)
8
+ end
9
+
10
+ def parse_list_all_my_buckets_result(xml)
11
+ names = []
12
+ rexml_document(xml).elements.each("ListAllMyBucketsResult/Buckets/Bucket/Name") { |e| names << e.text }
13
+ names
14
+ end
15
+
16
+ def parse_location_constraint(xml)
17
+ rexml_document(xml).elements["LocationConstraint"].text
18
+ end
19
+
20
+ def parse_list_bucket_result(xml)
21
+ objects_attributes = []
22
+ rexml_document(xml).elements.each("ListBucketResult/Contents") do |e|
23
+ object_attributes = {}
24
+ object_attributes[:key] = e.elements["Key"].text
25
+ object_attributes[:etag] = e.elements["ETag"].text
26
+ object_attributes[:last_modified] = e.elements["LastModified"].text
27
+ object_attributes[:size] = e.elements["Size"].text
28
+ objects_attributes << object_attributes
29
+ end
30
+ objects_attributes
31
+ end
32
+
33
+ def parse_copy_object_result(xml)
34
+ object_attributes = {}
35
+ document = rexml_document(xml)
36
+ object_attributes[:etag] = document.elements["CopyObjectResult/ETag"].text
37
+ object_attributes[:last_modified] = document.elements["CopyObjectResult/LastModified"].text
38
+ object_attributes
39
+ end
40
+
41
+ def parse_error(xml)
42
+ document = rexml_document(xml)
43
+ code = document.elements["Error/Code"].text
44
+ message = document.elements["Error/Message"].text
45
+ [code, message]
46
+ end
47
+
48
+ def parse_is_truncated xml
49
+ rexml_document(xml).elements["ListBucketResult/IsTruncated"].text =='true'
50
+ end
51
+ end
52
+ end