sndacs 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,110 @@
1
+ module Sndacs
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 Sndacs
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_snda_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_snda_copy_source] = full_key
154
+ headers[:x_snda_metadata_directive] = options[:replace] == false ? "COPY" : "REPLACE"
155
+ headers[:x_snda_copy_source_if_match] = options[:if_match] if options[:if_match]
156
+ headers[:x_snda_copy_source_if_none_match] = options[:if_none_match] if options[:if_none_match]
157
+ headers[:x_snda_copy_source_if_unmodified_since] = options[:if_modified_since] if options[:if_modified_since]
158
+ headers[:x_snda_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-snda-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_snda_acl] = @acl || "public-read"
229
+ headers[:x_snda_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-snda-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 unless response.body.nil?
250
+ end
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,37 @@
1
+ module Sndacs
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,54 @@
1
+ require 'rexml/document'
2
+
3
+ module Sndacs
4
+ module Parser
5
+ include REXML
6
+
7
+ def rexml_document(xml)
8
+ xml.force_encoding(::Encoding::UTF_8) if xml.respond_to? :force_encoding
9
+ Document.new(xml)
10
+ end
11
+
12
+ def parse_list_all_my_buckets_result(xml)
13
+ names = []
14
+ rexml_document(xml).elements.each("ListAllMyBucketsResult/Buckets/Bucket/Name") { |e| names << e.text }
15
+ names
16
+ end
17
+
18
+ def parse_location_constraint(xml)
19
+ rexml_document(xml).elements["LocationConstraint"].text
20
+ end
21
+
22
+ def parse_list_bucket_result(xml)
23
+ objects_attributes = []
24
+ rexml_document(xml).elements.each("ListBucketResult/Contents") do |e|
25
+ object_attributes = {}
26
+ object_attributes[:key] = e.elements["Key"].text
27
+ object_attributes[:etag] = e.elements["ETag"].text
28
+ object_attributes[:last_modified] = e.elements["LastModified"].text
29
+ object_attributes[:size] = e.elements["Size"].text
30
+ objects_attributes << object_attributes
31
+ end
32
+ objects_attributes
33
+ end
34
+
35
+ def parse_copy_object_result(xml)
36
+ object_attributes = {}
37
+ document = rexml_document(xml)
38
+ object_attributes[:etag] = document.elements["CopyObjectResult/ETag"].text
39
+ object_attributes[:last_modified] = document.elements["CopyObjectResult/LastModified"].text
40
+ object_attributes
41
+ end
42
+
43
+ def parse_error(xml)
44
+ document = rexml_document(xml)
45
+ code = document.elements["Error/Code"].text
46
+ message = document.elements["Error/Message"].text
47
+ [code, message]
48
+ end
49
+
50
+ def parse_is_truncated xml
51
+ rexml_document(xml).elements["ListBucketResult/IsTruncated"].text =='true'
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,31 @@
1
+ module Sndacs
2
+ # Class responsible for sending chunked requests
3
+ # properly. Net::HTTPGenericRequest has hardcoded chunk_size, so we
4
+ # inherit the class and override chunk_size.
5
+ class Request < Net::HTTPGenericRequest
6
+ def initialize(chunk_size, m, reqbody, resbody, path, initheader = nil)
7
+ @chunk_size = chunk_size
8
+ super(m, reqbody, resbody, path, initheader)
9
+ end
10
+
11
+ private
12
+
13
+ def send_request_with_body_stream(sock, ver, path, f)
14
+ unless content_length() or chunked?
15
+ raise ArgumentError, "Content-Length not given and Transfer-Encoding is not `chunked'"
16
+ end
17
+ supply_default_content_type
18
+ write_header sock, ver, path
19
+ if chunked?
20
+ while s = f.read(@chunk_size)
21
+ sock.write(sprintf("%x\r\n", s.length) << s << "\r\n")
22
+ end
23
+ sock.write "0\r\n\r\n"
24
+ else
25
+ while s = f.read(@chunk_size)
26
+ sock.write s
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,95 @@
1
+ require 'net/http'
2
+
3
+ require 'proxies'
4
+
5
+ require 'sndacs/parser'
6
+ require 'sndacs/buckets_extension'
7
+ require 'sndacs/connection'
8
+
9
+ module Sndacs
10
+ class Service
11
+ include Parser
12
+ include Proxies
13
+
14
+ attr_reader :access_key_id, :secret_access_key, :use_ssl, :proxy
15
+
16
+ # Compares service to other, by <tt>access_key_id</tt> and
17
+ # <tt>secret_access_key</tt>
18
+ def ==(other)
19
+ self.access_key_id == other.access_key_id and self.secret_access_key == other.secret_access_key
20
+ end
21
+
22
+ # Creates new service.
23
+ #
24
+ # ==== Options
25
+ # * <tt>:access_key_id</tt> - Access key id (REQUIRED)
26
+ # * <tt>:secret_access_key</tt> - Secret access key (REQUIRED)
27
+ # * <tt>:use_ssl</tt> - Use https or http protocol (false by
28
+ # default)
29
+ # * <tt>:debug</tt> - Display debug information on the STDOUT
30
+ # (false by default)
31
+ # * <tt>:timeout</tt> - Timeout to use by the Net::HTTP object
32
+ # (60 by default)
33
+ def initialize(options)
34
+ @access_key_id = options.fetch(:access_key_id)
35
+ @secret_access_key = options.fetch(:secret_access_key)
36
+ @use_ssl = options.fetch(:use_ssl, false)
37
+ @timeout = options.fetch(:timeout, 60)
38
+ @debug = options.fetch(:debug, false)
39
+
40
+ raise ArgumentError, "Missing proxy settings. Must specify at least :host." if options[:proxy] && !options[:proxy][:host]
41
+ @proxy = options.fetch(:proxy, nil)
42
+ end
43
+
44
+ # Returns all buckets in the service and caches the result (see
45
+ # +reload+)
46
+ def buckets
47
+ Proxy.new(lambda { list_all_my_buckets }, :owner => self, :extend => BucketsExtension)
48
+ end
49
+
50
+ # Returns the bucket with the given name. Does not check whether the
51
+ # bucket exists. But also does not issue any HTTP requests, so it's
52
+ # much faster than buckets.find
53
+ def bucket(name)
54
+ Bucket.send(:new, self, name)
55
+ end
56
+
57
+ # Returns "http://" or "https://", depends on <tt>:use_ssl</tt>
58
+ # value from initializer
59
+ def protocol
60
+ use_ssl ? "https://" : "http://"
61
+ end
62
+
63
+ # Returns 443 or 80, depends on <tt>:use_ssl</tt> value from
64
+ # initializer
65
+ def port
66
+ use_ssl ? 443 : 80
67
+ end
68
+
69
+ def inspect #:nodoc:
70
+ "#<#{self.class}:#@access_key_id>"
71
+ end
72
+
73
+ private
74
+
75
+ def list_all_my_buckets
76
+ response = service_request(:get)
77
+ names = parse_list_all_my_buckets_result(response.body)
78
+ names.map { |name| Bucket.send(:new, self, name) }
79
+ end
80
+
81
+ def service_request(method, options = {})
82
+ connection.request(method, options.merge(:path => "/#{options[:path]}"))
83
+ end
84
+
85
+ def connection
86
+ return @connection if defined?(@connection)
87
+ @connection = Connection.new(:access_key_id => @access_key_id,
88
+ :secret_access_key => @secret_access_key,
89
+ :use_ssl => @use_ssl,
90
+ :timeout => @timeout,
91
+ :debug => @debug,
92
+ :proxy => @proxy)
93
+ end
94
+ end
95
+ end