ruby-atmos 0.6.0
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/COPYING +8 -0
- data/README +129 -0
- data/Rakefile +94 -0
- data/lib/atmos/attributes.rb +545 -0
- data/lib/atmos/exceptions.rb +28 -0
- data/lib/atmos/object.rb +263 -0
- data/lib/atmos/parser.rb +110 -0
- data/lib/atmos/request.rb +188 -0
- data/lib/atmos/rest.rb +254 -0
- data/lib/atmos/store.rb +209 -0
- data/lib/atmos/util.rb +40 -0
- data/lib/atmos/version.rb +12 -0
- data/test/credentials.rb +141 -0
- data/test/esutest.rb +499 -0
- data/test/files/SmallImageForTest.iso +0 -0
- data/test/files/dragaf-tiny-from-vsphere.ova +0 -0
- data/test/files/something.txt +1 -0
- data/test/request_test.rb +50 -0
- data/test/suite.rb +9 -0
- data/test/suite_noproxy.rb +12 -0
- data/test/suite_proxy.rb +14 -0
- data/test/test_acl.rb +283 -0
- data/test/test_metadata.rb +162 -0
- data/test/test_object_create.rb +152 -0
- data/test/test_object_misc.rb +80 -0
- data/test/test_object_read.rb +114 -0
- data/test/test_object_update.rb +58 -0
- data/test/test_store.rb +125 -0
- data/test/test_util.rb +58 -0
- metadata +164 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
module Atmos
|
2
|
+
module Exceptions
|
3
|
+
|
4
|
+
class AtmosException < Exception
|
5
|
+
end
|
6
|
+
|
7
|
+
class ArgumentException < AtmosException
|
8
|
+
end
|
9
|
+
|
10
|
+
class AuthException < AtmosException
|
11
|
+
end
|
12
|
+
|
13
|
+
class InvalidStateException < AtmosException
|
14
|
+
end
|
15
|
+
|
16
|
+
class NoSuchObjectException < AtmosException
|
17
|
+
end
|
18
|
+
|
19
|
+
class ServerException < AtmosException
|
20
|
+
end
|
21
|
+
|
22
|
+
class NotImplementedException < AtmosException
|
23
|
+
end
|
24
|
+
|
25
|
+
class InternalLibraryException < AtmosException
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/atmos/object.rb
ADDED
@@ -0,0 +1,263 @@
|
|
1
|
+
module Atmos
|
2
|
+
|
3
|
+
#
|
4
|
+
# == Object
|
5
|
+
# This class represents an object in an Atmos store.
|
6
|
+
#
|
7
|
+
# === Object Data
|
8
|
+
# When an object is instantiated, it's data is not loaded
|
9
|
+
# due to memory considerations. You can access an object's
|
10
|
+
# data as a ruby String or as a progressive download via a block.
|
11
|
+
#
|
12
|
+
# ==== all at once
|
13
|
+
#
|
14
|
+
# obj.data
|
15
|
+
#
|
16
|
+
# ==== progressive download
|
17
|
+
#
|
18
|
+
# obj.data_as_stream do |chunk|
|
19
|
+
# datafile.write(chunk)
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# ==== partial data
|
23
|
+
#
|
24
|
+
# obj.data_as_stream(0...59) do |chunk|
|
25
|
+
# readin.concat(chunk)
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
#
|
29
|
+
# === System Metadata
|
30
|
+
# Each object has some information about it stored by the system.
|
31
|
+
# This information is read only, and available as a hash on the object:
|
32
|
+
#
|
33
|
+
# obj.system_metadata => Hash
|
34
|
+
#
|
35
|
+
# obj.system_metadata.each do |key,value|
|
36
|
+
# puts "#{key}=#{value}"
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# See Atmos::Metadata for more detailed information.
|
40
|
+
#
|
41
|
+
#
|
42
|
+
# === User Metadata
|
43
|
+
# There are two kinds of user metadata, listable and non-listable.
|
44
|
+
# Each of these is available as a hash
|
45
|
+
# on the object class. These can both be modified.
|
46
|
+
#
|
47
|
+
# obj.listable_metadata => Hash
|
48
|
+
#
|
49
|
+
# obj.listable_metadata.each do |key,value|
|
50
|
+
# puts "#{key}=#{value}"
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# obj.metadata => Hash
|
54
|
+
#
|
55
|
+
# obj.metadata.each do |key,value|
|
56
|
+
# puts "#{key}=#{value}"
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# See Atmos::Metadata for more detailed information.
|
60
|
+
#
|
61
|
+
#
|
62
|
+
# === Access Control Lists (ACLs)
|
63
|
+
#
|
64
|
+
# There are two hashes for access control available as properties
|
65
|
+
# on the object: +user_acl+ and +group_acl+.
|
66
|
+
# The keys are the Atmos usernames and the values are one of
|
67
|
+
# <tt>:none</tt>, <tt>:read</tt>, <tt>:write</tt>, <tt>:full</tt>.
|
68
|
+
#
|
69
|
+
# puts obj.user_acl.inspect => {user => :full}
|
70
|
+
# puts obj.group_acl.inspect => {other => :none}
|
71
|
+
#
|
72
|
+
# See Atmos::ACL for more detailed information.
|
73
|
+
#
|
74
|
+
#
|
75
|
+
class Object
|
76
|
+
attr_reader :aoid, :request, :user # :nodoc:
|
77
|
+
|
78
|
+
# Hash-like object containing user access control properties of the object.
|
79
|
+
attr_reader :user_acl
|
80
|
+
|
81
|
+
# Hash-like object containing group access control properties of the object.
|
82
|
+
attr_reader :group_acl
|
83
|
+
|
84
|
+
# Hash-like object containing non-listable metadata associated with the object.
|
85
|
+
attr_reader :metadata
|
86
|
+
|
87
|
+
# Hash-like object containing listable metadata associated with the object.
|
88
|
+
attr_reader :listable_metadata
|
89
|
+
|
90
|
+
# Hash-like object containing read-only system metadata associated with the object.
|
91
|
+
attr_reader :system_metadata
|
92
|
+
|
93
|
+
@request = nil
|
94
|
+
@checksum = nil
|
95
|
+
|
96
|
+
|
97
|
+
#
|
98
|
+
# This constructor is only meant for internal use. Get or create an object
|
99
|
+
# with an Atmos::Store object:
|
100
|
+
#
|
101
|
+
# obj = store.create
|
102
|
+
# obj = store.get(obj_id)
|
103
|
+
#
|
104
|
+
def initialize(store, aoid, options = {})
|
105
|
+
Atmos::LOG.debug("obj.new options: #{options.inspect}")
|
106
|
+
validate_options(options)
|
107
|
+
|
108
|
+
@deleted = false
|
109
|
+
@aoid = aoid
|
110
|
+
@store = store
|
111
|
+
@request = Atmos::Request.new(:store => @store)
|
112
|
+
@user = @store.user
|
113
|
+
|
114
|
+
# this means we're creating a new object,
|
115
|
+
# not representing an existing one
|
116
|
+
if (@aoid.nil?)
|
117
|
+
response = @request.do(:create_object, options)
|
118
|
+
@aoid = response.id
|
119
|
+
end
|
120
|
+
|
121
|
+
@system_metadata = Atmos::Metadata.new(self, Atmos::Metadata::SYSTEM)
|
122
|
+
|
123
|
+
# These guys need the object to have a valid id, so we do it after the create
|
124
|
+
@user_acl = Atmos::ACL.new(self, Atmos::ACL::USER)
|
125
|
+
@group_acl = Atmos::ACL.new(self, Atmos::ACL::GROUP)
|
126
|
+
@metadata = Atmos::Metadata.new(self, Atmos::Metadata::NON_LISTABLE)
|
127
|
+
@listable_metadata = Atmos::Metadata.new(self, Atmos::Metadata::LISTABLE)
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
#
|
132
|
+
# Truncates the object to size 0 without changing any of the Metadata or ACLs.
|
133
|
+
#
|
134
|
+
def truncate
|
135
|
+
do_delete_check
|
136
|
+
response = @request.do(:trunc_object, :id => @aoid, :data => nil, :length => 0)
|
137
|
+
end
|
138
|
+
|
139
|
+
#
|
140
|
+
# Deletes the object from Atmos and invalidates the object.
|
141
|
+
#
|
142
|
+
# obj = store.create
|
143
|
+
# obj.delete
|
144
|
+
#
|
145
|
+
def delete
|
146
|
+
do_delete_check
|
147
|
+
response = @request.do(:delete_object, :id => @aoid)
|
148
|
+
@deleted = true
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
#
|
153
|
+
# Returns all the object data in a single string. Be judicious about
|
154
|
+
# use of this method, since it can load the entire blob into memory.
|
155
|
+
#
|
156
|
+
# Optional:
|
157
|
+
# * <tt>:range</tt> - range of bytes to retrieve (e.g. 0...10000)
|
158
|
+
#
|
159
|
+
def data(range = nil)
|
160
|
+
do_delete_check
|
161
|
+
response = @request.do(:read_object, :id => @aoid, 'Range' => range)
|
162
|
+
response.http_response.body
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
#
|
167
|
+
# Allows progressive download of the object's data. Takes a block:
|
168
|
+
#
|
169
|
+
# obj.data_as_stream do |chunk|
|
170
|
+
# datafile.write(chunk)
|
171
|
+
# end
|
172
|
+
#
|
173
|
+
# Optional:
|
174
|
+
# * <tt>:range</tt> - range of bytes to retrieve (e.g. 0...10000)
|
175
|
+
#
|
176
|
+
def data_as_stream(range = nil, &block)
|
177
|
+
do_delete_check
|
178
|
+
@request.do(:read_object, :id => @aoid, 'Range' => range) do |response|
|
179
|
+
response.read_body do |chunk|
|
180
|
+
block.call(chunk)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
|
186
|
+
def update(data, range=nil)
|
187
|
+
response = @request.do(:update_object, :id => @aoid, :data => data, 'Range' => range)
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
#
|
192
|
+
# Checks to see if the represented object exists on Atmos
|
193
|
+
# by requesting it's system metadata.
|
194
|
+
#
|
195
|
+
# Returns boolean +true+ or +false+.
|
196
|
+
#
|
197
|
+
def exists?
|
198
|
+
rv = true
|
199
|
+
begin
|
200
|
+
@request.do(:list_system_metadata, :id => @aoid)
|
201
|
+
rescue Atmos::Exceptions::NoSuchObjectException
|
202
|
+
rv = false
|
203
|
+
end
|
204
|
+
rv
|
205
|
+
end
|
206
|
+
|
207
|
+
|
208
|
+
def copy #:nodoc:
|
209
|
+
do_delete_check
|
210
|
+
end
|
211
|
+
|
212
|
+
|
213
|
+
def headers #:nodoc:
|
214
|
+
do_delete_check
|
215
|
+
headers = {}
|
216
|
+
[@user_acl, @group_acl, @tags, @listable_tags, @metadata, @listable_metadata].each do |attr|
|
217
|
+
val = attr.header_value
|
218
|
+
next if (val.nil? || val.empty?)
|
219
|
+
headers[attr.header_name] = attr.header_value
|
220
|
+
end
|
221
|
+
headers
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
private
|
226
|
+
def do_delete_check
|
227
|
+
raise Atmos::Exceptions::InvalidStateException if (@deleted)
|
228
|
+
end
|
229
|
+
|
230
|
+
def validate_options(options)
|
231
|
+
invalid_values = []
|
232
|
+
|
233
|
+
valid_options = [:checksum, :user_acl, :group_acl, :metadata, :listable_metadata, :mimetype, :length, :data].freeze
|
234
|
+
invalid_options = options.keys - valid_options
|
235
|
+
raise Atmos::Exceptions::ArgumentException, "Unrecognized options: #{invalid_options.inspect}" if (!invalid_options.empty?)
|
236
|
+
|
237
|
+
options.each do |k,v|
|
238
|
+
case k
|
239
|
+
when :checksum
|
240
|
+
invalid_values.push(k) if (options[k].nil? || !options[k].kind_of?(Boolean))
|
241
|
+
when :user_acl
|
242
|
+
invalid_values.push(k) if (options[k].nil? || !options[k].kind_of?(Hash))
|
243
|
+
when :group_acl
|
244
|
+
invalid_values.push(k) if (options[k].nil? || !options[k].kind_of?(Hash))
|
245
|
+
when :metadata
|
246
|
+
invalid_values.push(k) if (options[k].nil? || !options[k].kind_of?(Hash))
|
247
|
+
when :listable_metadata
|
248
|
+
invalid_values.push(k) if (options[k].nil? || !options[k].kind_of?(Hash))
|
249
|
+
when :mimetype
|
250
|
+
invalid_values.push(k) if (options[k].nil? || !options[k].kind_of?(String))
|
251
|
+
when :length
|
252
|
+
invalid_values.push(k) if (options[k].nil? || !options[k].kind_of?(Integer))
|
253
|
+
when :data
|
254
|
+
invalid_values.push(k) if (options[k].nil? || (!options[k].kind_of?(String) && !options[k].class.ancestors.include?(IO)))
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
raise Atmos::Exceptions::ArgumentException, "Options of invalid type: #{invalid_values.inspect}" if (!invalid_values.empty?)
|
259
|
+
end
|
260
|
+
|
261
|
+
end
|
262
|
+
|
263
|
+
end
|
data/lib/atmos/parser.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
module Atmos
|
2
|
+
class Parser
|
3
|
+
|
4
|
+
NOKOGIRI = "nokogiri"
|
5
|
+
REXML = "rexml"
|
6
|
+
@@parser = nil
|
7
|
+
|
8
|
+
|
9
|
+
def self.parser
|
10
|
+
@@parser
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
def self.parser=(which)
|
15
|
+
|
16
|
+
@@parser = which
|
17
|
+
if (@@parser == NOKOGIRI)
|
18
|
+
require 'nokogiri'
|
19
|
+
elsif (@@parser == REXML)
|
20
|
+
require 'rexml/document'
|
21
|
+
else
|
22
|
+
raise Atmos::Exceptions::ArgumentException, "The XML parser has not been set to a known parser."
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def self.response_get_array(response, string)
|
28
|
+
rv = nil
|
29
|
+
|
30
|
+
if (parser == NOKOGIRI)
|
31
|
+
|
32
|
+
doc = Nokogiri::XML(response.body)
|
33
|
+
rv = doc.xpath(string).map do |elt|
|
34
|
+
elt.content
|
35
|
+
end
|
36
|
+
|
37
|
+
elsif (parser == REXML)
|
38
|
+
|
39
|
+
doc = ::REXML::Document.new(response.body)
|
40
|
+
rv = doc.elements.to_a(string).map do |elt|
|
41
|
+
elt.text
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
rv
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def self.response_get_string(response, string)
|
51
|
+
response_get_array(response,string)[0]
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
#
|
56
|
+
# Utility method to check the atmos server XML response for errors.
|
57
|
+
# Throws exception on error.
|
58
|
+
#
|
59
|
+
def self.response_check_action_error(action, response)
|
60
|
+
doc = nil
|
61
|
+
code = nil
|
62
|
+
msg = nil
|
63
|
+
exclass = Atmos::Exceptions::AtmosException
|
64
|
+
|
65
|
+
Atmos::LOG.info("body: #{response.body}")
|
66
|
+
if (!response.body.nil? && response.body.match(/<\?xml/))
|
67
|
+
if (parser == NOKOGIRI)
|
68
|
+
|
69
|
+
doc = Nokogiri::XML(response.body)
|
70
|
+
|
71
|
+
code = doc.xpath('//Error/Code')[0]
|
72
|
+
code = code.content if (!code.nil?)
|
73
|
+
|
74
|
+
msg = doc.xpath('//Error/Message')[0]
|
75
|
+
msg = msg.content if (!msg.nil?)
|
76
|
+
|
77
|
+
elsif (parser == REXML)
|
78
|
+
|
79
|
+
code = response_get_string(response, '//Error/Code')
|
80
|
+
msg = response_get_string(response, '//Error/Message')
|
81
|
+
|
82
|
+
else
|
83
|
+
raise Atmos::Exceptions::InvalidStateException, "The XML parser has not been set to a known parser."
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
if (!code.nil?)
|
88
|
+
|
89
|
+
Atmos::LOG.info("code: #{code}")
|
90
|
+
Atmos::LOG.info("msg: #{msg}")
|
91
|
+
|
92
|
+
if (!REST[action][:errors].nil? && !REST[action][:errors][code].nil?)
|
93
|
+
tmp = REST[action][:errors][code][:message]
|
94
|
+
msg = tmp if (!tmp.nil?)
|
95
|
+
|
96
|
+
tmp = REST[action][:errors][code][:class]
|
97
|
+
exclass = tmp if (!tmp.nil?)
|
98
|
+
end
|
99
|
+
|
100
|
+
raise exclass, msg
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
true
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
module Atmos
|
2
|
+
class Request # :nodoc:
|
3
|
+
|
4
|
+
def initialize(options = {})
|
5
|
+
valid = [:store, :default_tag].freeze
|
6
|
+
invalid = options.keys - valid
|
7
|
+
raise Atmos::Exceptions::ArgumentException,
|
8
|
+
"Unrecognized options: #{invalid.inspect}" if (!invalid.empty?)
|
9
|
+
Atmos::LOG.debug("Request.initialize: options: #{options.inspect}")
|
10
|
+
|
11
|
+
@baseurl = options[:store].uri
|
12
|
+
@uid = options[:store].uid
|
13
|
+
@secret = options[:store].secret
|
14
|
+
@http = options[:store].http
|
15
|
+
@tag = options[:default_tag]
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def do(actionname, options = {}, &block)
|
20
|
+
verbs = [:head, :get, :put, :post, :delete].freeze
|
21
|
+
Atmos::LOG.info("do #{actionname} options: #{options.inspect}")
|
22
|
+
raise Atmos::Exceptions::InternalLibraryException,
|
23
|
+
"Invalid REST action: #{actionname}" if (REST[actionname].nil?)
|
24
|
+
|
25
|
+
action = REST[actionname]
|
26
|
+
uri = (options[:id].nil?) ? action[:uri] : action[:uri].sub(/:id/, options[:id])
|
27
|
+
url = URI::join(@baseurl.to_s, uri.to_s)
|
28
|
+
verb = action[:verb]
|
29
|
+
|
30
|
+
if (options[:id].nil? && !action[:uri].index(':id').nil?)
|
31
|
+
raise Atmos::Exceptions::ArgumentException, "An id is required for this action (#{actionname})."
|
32
|
+
end
|
33
|
+
|
34
|
+
request = (verbs.include?(verb)) ? Net::HTTP.const_get(verb.to_s.capitalize).new(uri) : nil
|
35
|
+
raise Atmos::Exceptions::AtmosException,
|
36
|
+
"Couldn't create Net::HTTP request object for #{verb}" if (request.nil?)
|
37
|
+
|
38
|
+
headers = {}
|
39
|
+
headers['Date'] = Time.now().httpdate()
|
40
|
+
headers['x-emc-date'] = Time.now().httpdate()
|
41
|
+
headers['x-emc-uid'] = @uid
|
42
|
+
|
43
|
+
if (!action[:required_headers].nil?)
|
44
|
+
action[:required_headers].each do |header|
|
45
|
+
case header
|
46
|
+
when 'Content-Type' then
|
47
|
+
headers[header] = (!options[:mimetype].nil?) ? options[:mimetype] : 'binary/octet-stream'
|
48
|
+
when 'Content-Length' then
|
49
|
+
headers[header] = (!options[:length].nil?) ? options[:length] : 0
|
50
|
+
when 'x-emc-tags' then
|
51
|
+
headers[header] = (!options[header].nil?) ? options[header] : ""
|
52
|
+
else
|
53
|
+
if (options[header])
|
54
|
+
headers[header] = options[header]
|
55
|
+
else
|
56
|
+
raise "Value not supplied for required header: '#{header}'"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
if (!action[:optional_headers].nil?)
|
63
|
+
action[:optional_headers].each do |header|
|
64
|
+
if (header.eql?('Range') && options.has_key?('Range') && !options['Range'].nil?)
|
65
|
+
r = options['Range']
|
66
|
+
options.delete('Range')
|
67
|
+
#headers[header] = (r.kind_of?(Range)) ? "Bytes=#{r.first}-#{r.last}" : "Bytes=#{r.to_s}"
|
68
|
+
if (r.kind_of?(Range))
|
69
|
+
headers[header] = "Bytes=#{r.first}-#{r.last}"
|
70
|
+
Atmos::LOG.info("Request.initialize: given Range object: #{headers[header]}")
|
71
|
+
else
|
72
|
+
Atmos::LOG.info("Request.initialize: given range string: #{headers[header]}")
|
73
|
+
headers[header] = "Bytes=#{r.to_s}"
|
74
|
+
end
|
75
|
+
if ((!options[:data].nil?) and ((r.end-r.begin+1) != options[:data].length))
|
76
|
+
raise Atmos::Exceptions::ArgumentException, "The range length (#{r.end - r.begin + 1}) doesn't match the data length (#{options[:data].length})."
|
77
|
+
end
|
78
|
+
end
|
79
|
+
key = (action[:header2sym] && action[:header2sym][header]) ? action[:header2sym][header] : header
|
80
|
+
|
81
|
+
if (!options[key].nil?)
|
82
|
+
if (options[key].kind_of?(Hash))
|
83
|
+
headers[header] = ""
|
84
|
+
options[key].each do |key,val|
|
85
|
+
headers[header] += "#{key}=#{val}, "
|
86
|
+
end
|
87
|
+
headers[header].chop!
|
88
|
+
headers[header].chop!
|
89
|
+
elsif (options[key].kind_of?(Array))
|
90
|
+
headers[header] = ""
|
91
|
+
options[key].each do |val|
|
92
|
+
headers[header] += "#{val}, "
|
93
|
+
end
|
94
|
+
headers[header].chop!
|
95
|
+
headers[header].chop!
|
96
|
+
elsif (options[key].respond_to?(:to_s))
|
97
|
+
headers[header] = options[key]
|
98
|
+
else
|
99
|
+
raise "value for optional header '#{header}' id bad type: #{options[header].class}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
if (!@tag.nil? && !action[:optional_headers].nil? && action[:optional_headers].include?('x-emc-listable-meta'))
|
106
|
+
if (headers.has_key?('x-emc-listable-meta'))
|
107
|
+
headers['x-emc-listable-meta'] += ", #{@tag}=atmos-ruby-default"
|
108
|
+
else
|
109
|
+
headers['x-emc-listable-meta'] = "#{@tag}=atmos-ruby-default"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
headers['x-emc-signature'] = calculate_signature(action[:verb], @secret, url, headers)
|
114
|
+
|
115
|
+
if (options[:data])
|
116
|
+
Atmos::LOG.info("there is data: [#{options[:data]}]")
|
117
|
+
if (options[:data].respond_to?(:read))
|
118
|
+
request.body_stream = options[:data]
|
119
|
+
else
|
120
|
+
request.body = options[:data]
|
121
|
+
end
|
122
|
+
headers['Content-Length'] =
|
123
|
+
options[:data].respond_to?(:lstat) ? options[:data].stat.size : options[:data].size
|
124
|
+
Atmos::LOG.info("set Content-Length to: #{headers['Content-Length']}\n")
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
headers.each do |key,val|
|
129
|
+
request[key] = val
|
130
|
+
end
|
131
|
+
|
132
|
+
@http.set_debug_output($stdout) if (Atmos::LOG.level == Log4r::DEBUG)
|
133
|
+
|
134
|
+
|
135
|
+
#
|
136
|
+
# so this is weird. because some of these atmos request may be
|
137
|
+
# insanely long, we need to allow a mechanism for our API layer
|
138
|
+
# to read progressively, not only all at once
|
139
|
+
#
|
140
|
+
if (block)
|
141
|
+
@http.request(request) do |response|
|
142
|
+
block.call(response)
|
143
|
+
end
|
144
|
+
return
|
145
|
+
end
|
146
|
+
|
147
|
+
response = @http.request(request)
|
148
|
+
Atmos::Parser::response_check_action_error(actionname, response)
|
149
|
+
|
150
|
+
Atmos::Response.new(response, actionname)
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
def calculate_signature(verb, secret, url, headers)
|
155
|
+
headers_copy = headers.clone
|
156
|
+
|
157
|
+
# normalize emc headers
|
158
|
+
headers_copy.each do |header, value|
|
159
|
+
if (header.start_with?('x-emc-'))
|
160
|
+
headers.delete(header)
|
161
|
+
headers[header.to_s.strip.downcase] = value.to_s.strip.sub(/\s+/, ' ').sub(/\n/, '')
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# string together all emc headers, no newline at end
|
166
|
+
emc = ""
|
167
|
+
headers.keys.sort.each do |header|
|
168
|
+
if (header.start_with?('x-emc-'))
|
169
|
+
emc += "#{header}:#{headers[header]}\n"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
emc.chomp!
|
173
|
+
|
174
|
+
hashstring = verb.to_s.upcase+"\n"
|
175
|
+
['Content-Type', 'Range', 'Date'].each do |key|
|
176
|
+
hashstring += headers[key] if (!headers[key].nil?)
|
177
|
+
hashstring += "\n"
|
178
|
+
end
|
179
|
+
|
180
|
+
hashstring += url.path
|
181
|
+
hashstring += '?'+url.query if (!url.query.nil?)
|
182
|
+
hashstring += "\n"+ emc
|
183
|
+
|
184
|
+
Atmos::LOG.debug("calculate_signature: hashstring: #{hashstring}")
|
185
|
+
Base64.encode64(HMAC::SHA1.digest(Base64.decode64(secret), hashstring)).chomp()
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|