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
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
sndacs (0.0.1)
|
5
|
+
proxies (~> 0.2.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: http://rubygems.org/
|
9
|
+
specs:
|
10
|
+
diff-lcs (1.1.3)
|
11
|
+
metaclass (0.0.1)
|
12
|
+
mocha (0.10.5)
|
13
|
+
metaclass (~> 0.0.1)
|
14
|
+
proxies (0.2.1)
|
15
|
+
rspec (2.9.0)
|
16
|
+
rspec-core (~> 2.9.0)
|
17
|
+
rspec-expectations (~> 2.9.0)
|
18
|
+
rspec-mocks (~> 2.9.0)
|
19
|
+
rspec-core (2.9.0)
|
20
|
+
rspec-expectations (2.9.0)
|
21
|
+
diff-lcs (~> 1.1.3)
|
22
|
+
rspec-mocks (2.9.0)
|
23
|
+
test-unit (2.4.8)
|
24
|
+
|
25
|
+
PLATFORMS
|
26
|
+
ruby
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
bundler (>= 1.0.0)
|
30
|
+
mocha
|
31
|
+
rspec (~> 2.0)
|
32
|
+
sndacs!
|
33
|
+
test-unit (>= 2.0)
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2009 Jakub Kuźma, Mirosław Boruta
|
2
|
+
Copyright (c) 2012 LI Daobing <lidaobing@snda.com>
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
a copy of this software and associated documentation files (the
|
6
|
+
"Software"), to deal in the Software without restriction, including
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
= sndacs
|
2
|
+
|
3
|
+
sndacs library provides access to {SNDA Cloud Storage}[http://www.grandcloud.cn/product/ecs].
|
4
|
+
|
5
|
+
bases on the s3 gem: https://github.com/qoobaa/s3
|
6
|
+
|
7
|
+
== Installation
|
8
|
+
|
9
|
+
gem install sndacs
|
10
|
+
|
11
|
+
== Usage
|
12
|
+
|
13
|
+
=== Initialize the service
|
14
|
+
|
15
|
+
require "sndacs"
|
16
|
+
service = Sndacs::Service.new(:access_key_id => "...",
|
17
|
+
:secret_access_key => "...")
|
18
|
+
#=> #<Sndacs::Service:...>
|
19
|
+
|
20
|
+
=== List buckets
|
21
|
+
|
22
|
+
service.buckets
|
23
|
+
#=> [#<Sndacs::Bucket:first-bucket>,
|
24
|
+
# #<Sndacs::Bucket:second-bucket>]
|
25
|
+
|
26
|
+
=== Find bucket
|
27
|
+
|
28
|
+
first_bucket = service.buckets.find("first-bucket")
|
29
|
+
#=> #<Sndacs::Bucket:first-bucket>
|
30
|
+
|
31
|
+
=== List objects in a bucket
|
32
|
+
|
33
|
+
first_bucket.objects
|
34
|
+
#=> [#<Sndacs::Object:/first-bucket/lenna.png>,
|
35
|
+
# #<Sndacs::Object:/first-bucket/lenna_mini.png>]
|
36
|
+
|
37
|
+
=== Find object in a bucket
|
38
|
+
|
39
|
+
object = first_bucket.objects.find("lenna.png")
|
40
|
+
#=> #<Sndacs::Object:/first-bucket/lenna.png>
|
41
|
+
|
42
|
+
=== Access object metadata (cached from find)
|
43
|
+
|
44
|
+
object.content_type
|
45
|
+
#=> "image/png"
|
46
|
+
|
47
|
+
=== Access object content (downloads the object)
|
48
|
+
|
49
|
+
object.content
|
50
|
+
#=> "\x89PNG\r\n\x1A\n\x00\x00\x00\rIHDR\x00..."
|
51
|
+
|
52
|
+
=== Delete an object
|
53
|
+
|
54
|
+
object.destroy
|
55
|
+
#=> true
|
56
|
+
|
57
|
+
=== Create an object
|
58
|
+
|
59
|
+
new_object = bucket.objects.build("bender.png")
|
60
|
+
#=> #<Sndacs::Object:/synergy-staging/bender.png>
|
61
|
+
|
62
|
+
new_object.content = open("bender.png")
|
63
|
+
|
64
|
+
new_object.save
|
65
|
+
#=> true
|
66
|
+
|
67
|
+
Please note that new objects are created with "public-read" ACL by
|
68
|
+
default.
|
69
|
+
|
70
|
+
== See also
|
71
|
+
|
72
|
+
* rubygems[http://rubygems.org/gems/s3]
|
73
|
+
* repository[http://github.com/qoobaa/s3]
|
74
|
+
* {issue tracker}[http://github.com/qoobaa/s3/issues]
|
75
|
+
* documentation[http://rubydoc.info/github/qoobaa/s3/master/frames]
|
76
|
+
|
77
|
+
== Copyright
|
78
|
+
|
79
|
+
Copyright (c) 2012 LI Daobing. See LICENSE[http://github.com/grandcloud/sndacs-ruby/raw/master/LICENSE] for details.
|
80
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
module Sndacs
|
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
|
+
return @location if defined?(@location) and not reload
|
23
|
+
@location = location_constraint
|
24
|
+
end
|
25
|
+
|
26
|
+
# Compares the bucket with other bucket. Returns true if the names
|
27
|
+
# of the buckets are the same, and both have the same services
|
28
|
+
# (see Service equality)
|
29
|
+
def ==(other)
|
30
|
+
self.name == other.name and self.service == other.service
|
31
|
+
end
|
32
|
+
|
33
|
+
# Similar to retrieve, but catches S3::Error::NoSuchBucket
|
34
|
+
# exceptions and returns false instead.
|
35
|
+
def exists?
|
36
|
+
retrieve
|
37
|
+
true
|
38
|
+
rescue Error::NoSuchBucket
|
39
|
+
false
|
40
|
+
end
|
41
|
+
|
42
|
+
# Destroys given bucket. Raises an S3::Error::BucketNotEmpty
|
43
|
+
# exception if the bucket is not empty. You can destroy non-empty
|
44
|
+
# bucket passing true (to force destroy)
|
45
|
+
def destroy(force = false)
|
46
|
+
delete_bucket
|
47
|
+
true
|
48
|
+
rescue Error::BucketNotEmpty
|
49
|
+
if force
|
50
|
+
objects.destroy_all
|
51
|
+
retry
|
52
|
+
else
|
53
|
+
raise
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Saves the newly built bucket.
|
58
|
+
#
|
59
|
+
# ==== Options
|
60
|
+
# * <tt>:location</tt> - location of the bucket
|
61
|
+
# (<tt>:eu</tt> or <tt>us</tt>)
|
62
|
+
# * Any other options are passed through to
|
63
|
+
# Connection#request
|
64
|
+
def save(options = {})
|
65
|
+
options = {:location => options} unless options.is_a?(Hash)
|
66
|
+
create_bucket_configuration(options)
|
67
|
+
true
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns true if the name of the bucket can be used like +VHOST+
|
71
|
+
# name. If the bucket contains characters like underscore it can't
|
72
|
+
# be used as +VHOST+ (e.g. <tt>bucket_name.s3.amazonaws.com</tt>)
|
73
|
+
def vhost?
|
74
|
+
#"#@name.#{HOST}" =~ /\A#{URI::REGEXP::PATTERN::HOSTNAME}\Z/
|
75
|
+
false
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns host name of the bucket according (see #vhost? method)
|
79
|
+
def host
|
80
|
+
vhost? ? "#@name.#{HOST}" : "#{HOST}"
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns path prefix for non +VHOST+ bucket. Path prefix is used
|
84
|
+
# instead of +VHOST+ name, e.g. "bucket_name/"
|
85
|
+
def path_prefix
|
86
|
+
vhost? ? "" : "#@name/"
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns the objects in the bucket and caches the result
|
90
|
+
def objects
|
91
|
+
Proxy.new(lambda { list_bucket }, :owner => self, :extend => ObjectsExtension)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns the object with the given key. Does not check whether the
|
95
|
+
# object exists. But also does not issue any HTTP requests, so it's
|
96
|
+
# much faster than objects.find
|
97
|
+
def object(key)
|
98
|
+
Object.send(:new, self, :key => key)
|
99
|
+
end
|
100
|
+
|
101
|
+
def inspect #:nodoc:
|
102
|
+
"#<#{self.class}:#{name}>"
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
attr_writer :service
|
108
|
+
|
109
|
+
def location_constraint
|
110
|
+
response = bucket_request(:get, :params => {:location => nil})
|
111
|
+
parse_location_constraint(response.body)
|
112
|
+
end
|
113
|
+
|
114
|
+
def list_bucket(options = {})
|
115
|
+
response = bucket_request(:get, :params => options)
|
116
|
+
max_keys = options[:max_keys]
|
117
|
+
objects_attributes = parse_list_bucket_result(response.body)
|
118
|
+
|
119
|
+
# If there are more than 1000 objects S3 truncates listing and
|
120
|
+
# we need to request another listing for the remaining objects.
|
121
|
+
while parse_is_truncated(response.body)
|
122
|
+
next_request_options = {:marker => objects_attributes.last[:key]}
|
123
|
+
|
124
|
+
if max_keys
|
125
|
+
break if objects_attributes.length >= max_keys
|
126
|
+
next_request_options[:max_keys] = max_keys - objects_attributes.length
|
127
|
+
end
|
128
|
+
|
129
|
+
response = bucket_request(:get, :params => options.merge(next_request_options))
|
130
|
+
objects_attributes += parse_list_bucket_result(response.body)
|
131
|
+
end
|
132
|
+
|
133
|
+
objects_attributes.map { |object_attributes| Object.send(:new, self, object_attributes) }
|
134
|
+
end
|
135
|
+
|
136
|
+
def bucket_headers(options = {})
|
137
|
+
response = bucket_request(:head, :params => options)
|
138
|
+
rescue Error::ResponseError => e
|
139
|
+
if e.response.code.to_i == 404
|
140
|
+
raise Error::ResponseError.exception("NoSuchBucket").new("The specified bucket does not exist.", nil)
|
141
|
+
else
|
142
|
+
raise e
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def create_bucket_configuration(options = {})
|
147
|
+
location = options[:location].to_s.upcase if options[:location]
|
148
|
+
options[:headers] ||= {}
|
149
|
+
if location and location != "US"
|
150
|
+
options[:body] = "<CreateBucketConfiguration><LocationConstraint>#{location}</LocationConstraint></CreateBucketConfiguration>"
|
151
|
+
options[:headers][:content_type] = "application/xml"
|
152
|
+
end
|
153
|
+
bucket_request(:put, options)
|
154
|
+
end
|
155
|
+
|
156
|
+
def delete_bucket
|
157
|
+
bucket_request(:delete)
|
158
|
+
end
|
159
|
+
|
160
|
+
def initialize(service, name) #:nodoc:
|
161
|
+
self.service = service
|
162
|
+
self.name = name
|
163
|
+
end
|
164
|
+
|
165
|
+
def name=(name)
|
166
|
+
raise ArgumentError.new("Invalid bucket name: #{name}") unless name_valid?(name)
|
167
|
+
@name = name
|
168
|
+
end
|
169
|
+
|
170
|
+
def bucket_request(method, options = {})
|
171
|
+
path = "#{path_prefix}#{options[:path]}"
|
172
|
+
service_request(method, options.merge(:host => host, :path => path))
|
173
|
+
end
|
174
|
+
|
175
|
+
def name_valid?(name)
|
176
|
+
name =~ /\A[a-z0-9][a-z0-9\._-]{2,254}\Z/i and name !~ /\A#{URI::REGEXP::PATTERN::IPV4ADDR}\Z/
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Sndacs
|
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
|
+
# Finds all buckets in the service
|
16
|
+
def find_all
|
17
|
+
proxy_target
|
18
|
+
end
|
19
|
+
|
20
|
+
# Destroys all buckets in the service. Doesn't destroy non-empty
|
21
|
+
# buckets by default, pass true to force destroy (USE WITH CARE!).
|
22
|
+
def destroy_all(force = false)
|
23
|
+
proxy_target.each { |bucket| bucket.destroy(force) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
module Sndacs
|
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
|
+
# * <tt>:proxy</tt> - Hash for Net::HTTP Proxy settings
|
24
|
+
# * <tt>:chunk_size</tt> - Size of a chunk when streaming
|
25
|
+
# (1048576 (1 MiB) by default)
|
26
|
+
def initialize(options = {})
|
27
|
+
@access_key_id = options.fetch(:access_key_id)
|
28
|
+
@secret_access_key = options.fetch(:secret_access_key)
|
29
|
+
@use_ssl = options.fetch(:use_ssl, false)
|
30
|
+
@debug = options.fetch(:debug, false)
|
31
|
+
@timeout = options.fetch(:timeout, 60)
|
32
|
+
@proxy = options.fetch(:proxy, nil)
|
33
|
+
@chunk_size = options.fetch(:chunk_size, 1048576)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Makes request with given HTTP method, sets missing parameters,
|
37
|
+
# adds signature to request header and returns response object
|
38
|
+
# (Net::HTTPResponse)
|
39
|
+
#
|
40
|
+
# ==== Parameters
|
41
|
+
# * <tt>method</tt> - HTTP Method symbol, can be <tt>:get</tt>,
|
42
|
+
# <tt>:put</tt>, <tt>:delete</tt>
|
43
|
+
#
|
44
|
+
# ==== Options:
|
45
|
+
# * <tt>:host</tt> - Hostname to connecto to, defaults
|
46
|
+
# to <tt>s3.amazonaws.com</tt>
|
47
|
+
# * <tt>:path</tt> - path to send request to (REQUIRED)
|
48
|
+
# * <tt>:body</tt> - Request body, only meaningful for
|
49
|
+
# <tt>:put</tt> request
|
50
|
+
# * <tt>:params</tt> - Parameters to add to query string for
|
51
|
+
# request, can be String or Hash
|
52
|
+
# * <tt>:headers</tt> - Hash of headers fields to add to request
|
53
|
+
# header
|
54
|
+
#
|
55
|
+
# ==== Returns
|
56
|
+
# Net::HTTPResponse object -- response from the server
|
57
|
+
def request(method, options)
|
58
|
+
host = options.fetch(:host, HOST)
|
59
|
+
path = options.fetch(:path)
|
60
|
+
body = options.fetch(:body, nil)
|
61
|
+
params = options.fetch(:params, {})
|
62
|
+
headers = options.fetch(:headers, {})
|
63
|
+
|
64
|
+
# Must be done before adding params
|
65
|
+
# Encodes all characters except forward-slash (/) and explicitly legal URL characters
|
66
|
+
path = URI.escape(path, /[^#{URI::REGEXP::PATTERN::UNRESERVED}\/]/)
|
67
|
+
|
68
|
+
if params
|
69
|
+
params = params.is_a?(String) ? params : self.class.parse_params(params)
|
70
|
+
path << "?#{params}"
|
71
|
+
end
|
72
|
+
|
73
|
+
request = Request.new(@chunk_size, method.to_s.upcase, !!body, method.to_s.upcase != "HEAD", path)
|
74
|
+
|
75
|
+
headers = self.class.parse_headers(headers)
|
76
|
+
headers.each do |key, value|
|
77
|
+
request[key] = value
|
78
|
+
end
|
79
|
+
|
80
|
+
if body
|
81
|
+
if body.respond_to?(:read)
|
82
|
+
request.body_stream = body
|
83
|
+
else
|
84
|
+
request.body = body
|
85
|
+
end
|
86
|
+
request.content_length = body.respond_to?(:lstat) ? body.stat.size : body.size
|
87
|
+
end
|
88
|
+
|
89
|
+
send_request(host, request)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Helper function to parser parameters and create single string of
|
93
|
+
# params added to questy string
|
94
|
+
#
|
95
|
+
# ==== Parameters
|
96
|
+
# * <tt>params</tt> - Hash of parameters
|
97
|
+
#
|
98
|
+
# ==== Returns
|
99
|
+
# String -- containing all parameters joined in one params string,
|
100
|
+
# i.e. <tt>param1=val¶m2¶m3=0</tt>
|
101
|
+
def self.parse_params(params)
|
102
|
+
interesting_keys = [:max_keys, :prefix, :marker, :delimiter, :location]
|
103
|
+
|
104
|
+
result = []
|
105
|
+
params.each do |key, value|
|
106
|
+
if interesting_keys.include?(key)
|
107
|
+
parsed_key = key.to_s.gsub("_", "-")
|
108
|
+
case value
|
109
|
+
when nil
|
110
|
+
result << parsed_key
|
111
|
+
else
|
112
|
+
result << "#{parsed_key}=#{value}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
result.join("&")
|
117
|
+
end
|
118
|
+
|
119
|
+
# Helper function to change headers from symbols, to in correct
|
120
|
+
# form (i.e. with '-' instead of '_')
|
121
|
+
#
|
122
|
+
# ==== Parameters
|
123
|
+
# * <tt>headers</tt> - Hash of pairs <tt>headername => value</tt>,
|
124
|
+
# where value can be Range (for Range header) or any other value
|
125
|
+
# which can be translated to string
|
126
|
+
#
|
127
|
+
# ==== Returns
|
128
|
+
# Hash of headers translated from symbol to string, containing
|
129
|
+
# only interesting headers
|
130
|
+
def self.parse_headers(headers)
|
131
|
+
interesting_keys = [:content_type, :cache_control, :x_snda_acl, :x_snda_storage_class, :range,
|
132
|
+
:if_modified_since, :if_unmodified_since,
|
133
|
+
:if_match, :if_none_match,
|
134
|
+
:content_disposition, :content_encoding,
|
135
|
+
:x_snda_copy_source, :x_snda_metadata_directive,
|
136
|
+
:x_snda_copy_source_if_match,
|
137
|
+
:x_snda_copy_source_if_none_match,
|
138
|
+
:x_snda_copy_source_if_unmodified_since,
|
139
|
+
:x_snda_copy_source_if_modified_since]
|
140
|
+
|
141
|
+
parsed_headers = {}
|
142
|
+
if headers
|
143
|
+
headers.each do |key, value|
|
144
|
+
if interesting_keys.include?(key)
|
145
|
+
parsed_key = key.to_s.gsub("_", "-")
|
146
|
+
parsed_value = value
|
147
|
+
case value
|
148
|
+
when Range
|
149
|
+
parsed_value = "bytes=#{value.first}-#{value.last}"
|
150
|
+
end
|
151
|
+
parsed_headers[parsed_key] = parsed_value
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
parsed_headers
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
def port
|
161
|
+
use_ssl ? 443 : 80
|
162
|
+
end
|
163
|
+
|
164
|
+
def proxy_settings
|
165
|
+
@proxy.values_at(:host, :port, :user, :password) unless @proxy.nil? || @proxy.empty?
|
166
|
+
end
|
167
|
+
|
168
|
+
def http(host)
|
169
|
+
http = Net::HTTP.new(host, port, *proxy_settings)
|
170
|
+
http.set_debug_output(STDOUT) if @debug
|
171
|
+
http.use_ssl = @use_ssl
|
172
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @use_ssl
|
173
|
+
http.read_timeout = @timeout if @timeout
|
174
|
+
http
|
175
|
+
end
|
176
|
+
|
177
|
+
def send_request(host, request, skip_authorization = false)
|
178
|
+
response = http(host).start do |http|
|
179
|
+
host = http.address
|
180
|
+
|
181
|
+
request["Date"] ||= Time.now.httpdate
|
182
|
+
|
183
|
+
if request.body
|
184
|
+
request["Content-Type"] ||= "application/octet-stream"
|
185
|
+
request["Content-MD5"] = Base64.encode64(Digest::MD5.digest(request.body)).chomp unless request.body.empty?
|
186
|
+
end
|
187
|
+
|
188
|
+
unless skip_authorization
|
189
|
+
request["Authorization"] = Signature.generate(:host => host,
|
190
|
+
:request => request,
|
191
|
+
:access_key_id => access_key_id,
|
192
|
+
:secret_access_key => secret_access_key)
|
193
|
+
end
|
194
|
+
|
195
|
+
http.request(request)
|
196
|
+
end
|
197
|
+
|
198
|
+
if response.code.to_i == 307
|
199
|
+
if response.body
|
200
|
+
doc = Document.new response.body
|
201
|
+
send_request(doc.elements["Error"].elements["Endpoint"].text, request, true)
|
202
|
+
end
|
203
|
+
else
|
204
|
+
handle_response(response)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def handle_response(response)
|
209
|
+
case response.code.to_i
|
210
|
+
when 200...300
|
211
|
+
response
|
212
|
+
when 300...600
|
213
|
+
if response.body.nil? || response.body.empty?
|
214
|
+
raise Error::ResponseError.new(nil, response)
|
215
|
+
else
|
216
|
+
code, message = parse_error(response.body)
|
217
|
+
raise Error::ResponseError.exception(code).new(message, response)
|
218
|
+
end
|
219
|
+
else
|
220
|
+
raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
|
221
|
+
end
|
222
|
+
response
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|