sndacs 0.0.1

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.
@@ -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