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.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +33 -0
- data/LICENSE +21 -0
- data/README.rdoc +80 -0
- data/Rakefile +6 -0
- data/lib/sndacs/bucket.rb +179 -0
- data/lib/sndacs/buckets_extension.rb +26 -0
- data/lib/sndacs/connection.rb +225 -0
- data/lib/sndacs/exceptions.rb +110 -0
- data/lib/sndacs/object.rb +253 -0
- data/lib/sndacs/objects_extension.rb +37 -0
- data/lib/sndacs/parser.rb +54 -0
- data/lib/sndacs/request.rb +31 -0
- data/lib/sndacs/service.rb +95 -0
- data/lib/sndacs/signature.rb +243 -0
- data/lib/sndacs/version.rb +3 -0
- data/lib/sndacs.rb +27 -0
- data/sndacs.gemspec +29 -0
- data/spec/sndacs/service_spec.rb +25 -0
- data/spec/spec_helper.rb +5 -0
- data/test/bucket_test.rb +215 -0
- data/test/connection_test.rb +214 -0
- data/test/object_test.rb +205 -0
- data/test/service_test.rb +111 -0
- data/test/signature_test.rb +205 -0
- data/test/test_helper.rb +3 -0
- metadata +128 -0
@@ -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
|