ruby-atmos 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/lib/atmos/rest.rb
ADDED
@@ -0,0 +1,254 @@
|
|
1
|
+
module Atmos
|
2
|
+
REST = {
|
3
|
+
:listable_tags => { :verb => :get,
|
4
|
+
:uri => '/rest/objects?listabletags',
|
5
|
+
:required_headers => ['x-emc-tags'],
|
6
|
+
:optional_headers => ['x-emc-token'],
|
7
|
+
:http_response => 200,
|
8
|
+
:return_headers => ['x-emc-listable-tags'],
|
9
|
+
},
|
10
|
+
:set_group_acl => { :verb => :post,
|
11
|
+
:uri => '/rest/objects/:id?acl',
|
12
|
+
:required_headers => ['x-emc-groupacl'],
|
13
|
+
:http_response => 200,
|
14
|
+
},
|
15
|
+
:set_user_acl => { :verb => :post,
|
16
|
+
:uri => '/rest/objects/:id?acl',
|
17
|
+
:required_headers => ['x-emc-useracl'],
|
18
|
+
:http_response => 200,
|
19
|
+
},
|
20
|
+
:set_metadata => { :verb => :post,
|
21
|
+
:uri => '/rest/objects/:id?metadata/user',
|
22
|
+
:http_response => 200,
|
23
|
+
:required_headers => ['x-emc-meta'],
|
24
|
+
},
|
25
|
+
:set_listable_metadata => { :verb => :post,
|
26
|
+
:uri => '/rest/objects/:id?metadata/user',
|
27
|
+
:http_response => 200,
|
28
|
+
:required_headers => ['x-emc-listable-meta'],
|
29
|
+
},
|
30
|
+
:delete_metadata => { :verb => :delete,
|
31
|
+
:uri => '/rest/objects/:id?metadata/user',
|
32
|
+
:http_response => 204,
|
33
|
+
:required_headers => ['x-emc-tags'],
|
34
|
+
},
|
35
|
+
:read_object => { :verb => :get,
|
36
|
+
:uri => '/rest/objects/:id',
|
37
|
+
:http_response => 200,
|
38
|
+
:optional_headers => ['Range'],
|
39
|
+
},
|
40
|
+
:update_object => { :verb => :put,
|
41
|
+
:uri => '/rest/objects/:id',
|
42
|
+
:http_response => 200,
|
43
|
+
:required_headers => ['Content-Type', 'Content-Length'],
|
44
|
+
:optional_headers => ['x-emc-useracl', 'x-emc-groupacl', 'Range'],
|
45
|
+
},
|
46
|
+
:trunc_object => { :verb => :put,
|
47
|
+
:uri => '/rest/objects/:id',
|
48
|
+
:http_response => 200,
|
49
|
+
:required_headers => ['Content-Length'],
|
50
|
+
:optional_headers => ['x-emc-useracl', 'x-emc-groupacl', 'Range'],
|
51
|
+
},
|
52
|
+
:get_service_info => { :verb => :get,
|
53
|
+
:uri => '/rest/service',
|
54
|
+
:response => :xml,
|
55
|
+
:http_response => 200,
|
56
|
+
},
|
57
|
+
|
58
|
+
:list_objects => { :verb => :get,
|
59
|
+
:uri => '/rest/objects',
|
60
|
+
:response => :xml,
|
61
|
+
:http_response => 200,
|
62
|
+
:required_headers => ['x-emc-tags', 'Content-Type'],
|
63
|
+
:optional_headers => ['x-emc-include-meta', 'x-emc-tags'],
|
64
|
+
:errors => { '1003' => { :class => Atmos::Exceptions::ArgumentException ,
|
65
|
+
:message => "No such listable tag found." } },
|
66
|
+
},
|
67
|
+
|
68
|
+
:create_object => { :verb => :post,
|
69
|
+
:uri => '/rest/objects',
|
70
|
+
:http_response => 201,
|
71
|
+
:optional_headers => ['x-emc-wschecksum', 'x-emc-useracl', 'x-emc-groupacl', 'x-emc-meta', 'x-emc-listable-meta'],
|
72
|
+
:required_headers => ['Content-Type', 'Content-Length'],
|
73
|
+
:return_headers => ['x-emc-delta', 'x-emc-policy', 'location'],
|
74
|
+
:header2sym => {'x-emc-wschecksum' => :checksum,
|
75
|
+
'x-emc-useracl' => :user_acl,
|
76
|
+
'x-emc-groupacl' => :group_acl,
|
77
|
+
'x-emc-meta' => :metadata,
|
78
|
+
'x-emc-listable-meta' => :listable_metadata},
|
79
|
+
},
|
80
|
+
|
81
|
+
:delete_object => { :verb => :delete,
|
82
|
+
:uri => '/rest/objects/:id',
|
83
|
+
:http_response => 204,
|
84
|
+
:required_headers => ['Content-Type'],
|
85
|
+
:return_headers => ['x-emc-delta', 'x-emc-policy'],
|
86
|
+
:errors => { '1003' => { :class => Atmos::Exceptions::NoSuchObjectException ,
|
87
|
+
:message => "Object not found to delete." } },
|
88
|
+
},
|
89
|
+
|
90
|
+
:list_system_metadata=> { :verb => :get,
|
91
|
+
:uri => '/rest/objects/:id?metadata/system',
|
92
|
+
:http_response => 200,
|
93
|
+
:return_headers => ['x-emc-meta'],
|
94
|
+
:errors => { '1003' => { :class => Atmos::Exceptions::NoSuchObjectException ,
|
95
|
+
:message => "Object not found to delete." } },
|
96
|
+
},
|
97
|
+
:list_metadata => { :verb => :get,
|
98
|
+
:uri => '/rest/objects/:id?metadata/user',
|
99
|
+
:http_response => 200,
|
100
|
+
:required_headers => ['Content-Type'],
|
101
|
+
:return_headers => ['x-emc-meta', 'x-emc-listable-meta'],
|
102
|
+
},
|
103
|
+
|
104
|
+
:list_tags => { :verb => :get,
|
105
|
+
:uri => '/rest/objects/:id?metadata/tags',
|
106
|
+
:http_response => 200,
|
107
|
+
:return_headers => ['x-emc-tags', 'x-emc-listable-tags'],
|
108
|
+
},
|
109
|
+
|
110
|
+
:list_acl => { :verb => :get,
|
111
|
+
:uri => '/rest/objects/:id?acl',
|
112
|
+
:http_response => 200,
|
113
|
+
:return_headers => ['x-emc-useracl', 'x-emc-groupacl'],
|
114
|
+
},
|
115
|
+
|
116
|
+
|
117
|
+
|
118
|
+
|
119
|
+
|
120
|
+
|
121
|
+
|
122
|
+
:delete_version => { :verb => :delete,
|
123
|
+
:uri => '/rest/objects/:id?versions',
|
124
|
+
:http_response => 204,
|
125
|
+
},
|
126
|
+
:get_object_info => { :verb => :get,
|
127
|
+
:uri => '/rest/objects/:id?info',
|
128
|
+
:http_response => 200,
|
129
|
+
},
|
130
|
+
:create_version => { :verb => :post,
|
131
|
+
:uri => '/rest/objects/:id?versions' ,
|
132
|
+
:http_response => 201,
|
133
|
+
},
|
134
|
+
:list_versions => { :verb => :get, :uri => '/rest/objects/:id?versions', :http_response => 200},
|
135
|
+
:restore_version => { :verb => :put, :uri => '/rest/objects/:id?versions', :http_response => 200},
|
136
|
+
} # :nodoc:
|
137
|
+
|
138
|
+
|
139
|
+
|
140
|
+
class Response # :nodoc:
|
141
|
+
attr_reader :http_response
|
142
|
+
|
143
|
+
def initialize(response, action)
|
144
|
+
@http_response = response
|
145
|
+
@action = action
|
146
|
+
|
147
|
+
if (response.kind_of?(Net::HTTPServerError))
|
148
|
+
raise Atmos::Exceptions::ServerException, response.message
|
149
|
+
elsif (response.kind_of?(Net::HTTPClientError))
|
150
|
+
|
151
|
+
raise Atmos::Exceptions::AtmosException, "Atmos got a bad request. This is probably a problem in this library." if (response.kind_of?(Net::HTTPBadRequest))
|
152
|
+
|
153
|
+
Atmos::LOG.debug("#{response.class}")
|
154
|
+
Atmos::LOG.debug("#{response.body}")
|
155
|
+
|
156
|
+
Atmos::Parser::response_check_action_error(@action, response)
|
157
|
+
|
158
|
+
elsif (!response.kind_of?(Net::HTTPSuccess))
|
159
|
+
raise Atmos::Exceptions::NotImplementedException, "This library doesn't handle these kinds of responses: #{response}"
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
def each(header)
|
165
|
+
if (!self[header].nil?)
|
166
|
+
self[header].split(',').each do |elt|
|
167
|
+
if (!elt.index('=').nil?)
|
168
|
+
key,val = elt.split('=')
|
169
|
+
yield key.strip, val.strip
|
170
|
+
else
|
171
|
+
yield elt.strip
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def body
|
178
|
+
@http_response.body
|
179
|
+
end
|
180
|
+
|
181
|
+
def method_missing(sym, *args)
|
182
|
+
Atmos::LOG.info("method missing: #{sym}")
|
183
|
+
header = "x-emc-#{sym.id2name.sub(/_/, '-')}"
|
184
|
+
if (REST[@action][:return_headers].include?(header))
|
185
|
+
Atmos::LOG.info("header: #{header}")
|
186
|
+
rv = Atmos::Util.header2hash(header, @http_response[header])
|
187
|
+
Atmos::LOG.debug("rv: #{rv.inspect}")
|
188
|
+
return rv
|
189
|
+
else
|
190
|
+
return super.method_missing(sym, *args)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def delta
|
195
|
+
if (REST[@action][:return_headers].include?('x-emc-delta'))
|
196
|
+
@http_response['x-emc-delta']
|
197
|
+
else
|
198
|
+
raise "A #{@action} request doesn't return a delta."
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def policy
|
203
|
+
if (REST[@action][:return_headers].include?('x-emc-policy'))
|
204
|
+
@http_response['x-emc-policy']
|
205
|
+
else
|
206
|
+
raise "A #{@action} request doesn't return a policy description."
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def id
|
211
|
+
if (REST[@action][:return_headers].include?('location'))
|
212
|
+
@http_response['location'][@http_response['location'].rindex('/')+1..-1]
|
213
|
+
else
|
214
|
+
raise "A #{@action} request doesn't return an id."
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def headers
|
219
|
+
headers = {}
|
220
|
+
@http_response.each do |header, value|
|
221
|
+
headers[header] = value
|
222
|
+
end
|
223
|
+
headers
|
224
|
+
end
|
225
|
+
|
226
|
+
def [](header)
|
227
|
+
@http_response[header]
|
228
|
+
end
|
229
|
+
|
230
|
+
end
|
231
|
+
|
232
|
+
|
233
|
+
|
234
|
+
|
235
|
+
|
236
|
+
|
237
|
+
# while (response.kind_of?(Net::HTTPRedirection))
|
238
|
+
|
239
|
+
#From http://railstips.org/blog/archives/2009/03/04/following-redirects-with-nethttp/
|
240
|
+
# rurl = (response['location'].nil?) ? response.body.match(/<a href=\"([^>]+)\">/i)[1] : response['location']
|
241
|
+
|
242
|
+
# puts("Got a redirect \nfrom: #{options[:url]}\n to: #{rurl}")
|
243
|
+
|
244
|
+
# if rurl.start_with?('/')
|
245
|
+
# puts("Got a relative redirect url: #{options[:url]}")
|
246
|
+
# options[:url] = URI.parse("#{url.scheme}://#{url.host}#{redirect_url}")
|
247
|
+
# end
|
248
|
+
|
249
|
+
# options[:redirects] = (options[:redirects].nil?) ? 0 : options[:redirects] += 1
|
250
|
+
# response = self.generic_request(options)
|
251
|
+
# end
|
252
|
+
# raise "Too many redirects (#{options[:redirects]}): #{url}" if (!options[:redirects].nil? && (options[:redirects] > 3))
|
253
|
+
|
254
|
+
end
|
data/lib/atmos/store.rb
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
module Atmos
|
2
|
+
|
3
|
+
class Store
|
4
|
+
attr_reader :http, :uid, :secret, :uri, :tenant, :user, :proxy # :nodoc:
|
5
|
+
|
6
|
+
#
|
7
|
+
# Create and validate a connection to an Atmos server.
|
8
|
+
# This API currently uses only the Atmos object interface,
|
9
|
+
# not the namespace interface. Proxy support has been tested
|
10
|
+
# against squid 2.7.STABLE9 on ubuntu 10.10.
|
11
|
+
#
|
12
|
+
# If you have used Atmos Online this example will look familiar:
|
13
|
+
#
|
14
|
+
# store = Atmos::Store.new(:url => 'https://accesspoint.atmosonline.com'
|
15
|
+
# :uid => 'ab9090e754b549eeb460a468abdf7fc2/A797558526171ea693ce'
|
16
|
+
# :secret => 'pEauABL07ujkF2hJN7r6wA9/HPs='
|
17
|
+
# )
|
18
|
+
#
|
19
|
+
# Required:
|
20
|
+
# * <tt>:url</tt> - the url to the Atmos server the Store object will
|
21
|
+
# represent (e.g. \https://accesspoint.atmosonline.com)
|
22
|
+
# * <tt>:uid</tt> - a string, "<tenant id>/<username>"
|
23
|
+
# ("Full Token ID" in Atmos Online)
|
24
|
+
# * <tt>:secret</tt> - a string, the secret key associated with the user
|
25
|
+
# (shared secret for a token in Atmos Online)
|
26
|
+
# --
|
27
|
+
# Optional:
|
28
|
+
# * <tt>:proxy</tt> - url to proxy of the form
|
29
|
+
# scheme://[user[:password]@]host:port
|
30
|
+
#
|
31
|
+
#
|
32
|
+
def initialize(options = {})
|
33
|
+
valid = [:url,:secret,:uid,:tag,:version,:proxy,:unsupported].freeze
|
34
|
+
invalid = options.keys - valid
|
35
|
+
raise Atmos::Exceptions::ArgumentException,
|
36
|
+
"Unrecognized options: #{invalid.inspect}" if (!invalid.empty?)
|
37
|
+
Atmos::LOG.info("Store.initialize: options: #{options.inspect}")
|
38
|
+
|
39
|
+
raise Atmos::Exceptions::ArgumentException,
|
40
|
+
"The kind of XML parser you would like to use has not been set yet." if (Atmos::Parser::parser.nil?)
|
41
|
+
|
42
|
+
@uri = URI.parse(options[:url])
|
43
|
+
@secret = options[:secret]
|
44
|
+
@uid = options[:uid]
|
45
|
+
@tag = options[:tag]
|
46
|
+
|
47
|
+
if (@uid =~ /^([^\/]+)\/([^\/]+)$/)
|
48
|
+
@tenant = $1
|
49
|
+
@user = $2
|
50
|
+
else
|
51
|
+
@tenant = nil
|
52
|
+
@user = @uid
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
proxyopts = [nil]
|
57
|
+
if !options[:proxy].nil?
|
58
|
+
|
59
|
+
proxyurl = URI.parse(options[:proxy])
|
60
|
+
|
61
|
+
if (!proxyurl.kind_of?(URI::HTTP) &&
|
62
|
+
!proxyurl.kind_of?(URI::HTTPS))
|
63
|
+
|
64
|
+
msg = "Please check the proxy URL scheme. "
|
65
|
+
msg += "It's not being recognized as an http or https url. "
|
66
|
+
msg += "You do have to include the http(s):// part."
|
67
|
+
raise Atmos::Exceptions::ArgumentException, msg
|
68
|
+
end
|
69
|
+
|
70
|
+
Atmos::LOG.warn("using proxy: #{options[:proxy]}")
|
71
|
+
proxyopts = [proxyurl.host, proxyurl.port]
|
72
|
+
|
73
|
+
if (proxyurl.userinfo =~ /([^:]+):([^:])/)
|
74
|
+
proxyopts.push([$1, $2])
|
75
|
+
elsif (!proxyurl.userinfo.nil?)
|
76
|
+
proxyopts.push(proxyurl.userinfo)
|
77
|
+
end
|
78
|
+
Atmos::LOG.info("proxy options: #{proxyopts.inspect}")
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
@http = Net::HTTP.Proxy(*proxyopts).new(@uri.host, @uri.port)
|
83
|
+
if @uri.scheme == 'https'
|
84
|
+
@http.use_ssl = true
|
85
|
+
@http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
86
|
+
end
|
87
|
+
Atmos::LOG.debug("http class: #{@http.inspect}")
|
88
|
+
|
89
|
+
@request = Atmos::Request.new(:store => self, :default_tag => @tag)
|
90
|
+
res = @request.do(:get_service_info)
|
91
|
+
|
92
|
+
if (res.http_response.kind_of?(Net::HTTPMovedPermanently))
|
93
|
+
rurl = (res['location'].nil?) ? res.body.match(/<a href=\"([^>]+)\">/i)[1] : res['location']
|
94
|
+
raise ArgumentException, "The Atmos URL is redirecting. Please supply the correct URL. (#{@uri.to_s} => #{rurl})"
|
95
|
+
end
|
96
|
+
|
97
|
+
@server_version = Atmos::Parser::response_get_string(res.http_response, '//xmlns:Atmos')
|
98
|
+
|
99
|
+
if ((!SUPPORTED_VERSIONS.include?(@server_version)) &&
|
100
|
+
(options[:unsupported] != true))
|
101
|
+
msg ="This library does not support Atmos version #{@server_version}. "
|
102
|
+
msg += "Supported versions: #{SUPPORTED_VERSIONS.inspect}. "
|
103
|
+
msg += "To silence this, pass :unsupported => true when instantiating."
|
104
|
+
|
105
|
+
raise Atmos::Exceptions::NotImplementedException, msg
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
if (!options[:version].nil? && !options[:version].eql?(@server_version))
|
110
|
+
raise Atmos::Exceptions::ArgumentException,
|
111
|
+
"Atmos server version and version you were expecting do not match."
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
#
|
117
|
+
# Create an Atmos object. This can be done with no arguments,
|
118
|
+
# or with any of the listed optional arguments. Checksums are
|
119
|
+
# not yet supported.
|
120
|
+
#
|
121
|
+
# If no data is passed, a 0 length object is created.
|
122
|
+
# If no ACLs are passed, the default access control is applied.
|
123
|
+
# If no mime type is passed, the default is 'binary/octet-stream'.
|
124
|
+
#
|
125
|
+
# Optional:
|
126
|
+
# * <tt>:user_acl</tt> - hash of username/permissions (<tt>:none</tt>, <tt>:read</tt>, <tt>:write</tt>, <tt>:full</tt>) pairs
|
127
|
+
# * <tt>:group_acl</tt> - hash of groupname/permissions (<tt>:none</tt>, <tt>:read</tt>, <tt>:write</tt>, <tt>:full</tt>) pairs
|
128
|
+
# * <tt>:metadata</tt> - hash of string pairs
|
129
|
+
# * <tt>:listable_metadata</tt> - hash of string pairs
|
130
|
+
# * <tt>:mimetype</tt> - defaults to application/octet-stream
|
131
|
+
# * <tt>:data</tt> - a String or an IO stream
|
132
|
+
# * <tt>:length</tt> - a number (requires <tt>:data</tt>)
|
133
|
+
#
|
134
|
+
def create(options = {})
|
135
|
+
Atmos::Object.new(self, nil, options)
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
#
|
140
|
+
# Retrieve an Atmos object given an id.
|
141
|
+
# When the object is retrieved, the ACL and
|
142
|
+
# Metadata information is loaded from the server.
|
143
|
+
# The blob data is not loaded until
|
144
|
+
# accessed, so it can be progressively downloaded.
|
145
|
+
#
|
146
|
+
def get(id)
|
147
|
+
Atmos::Object.new(self, id)
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
#
|
152
|
+
# Tags are the key part of key/value metadata pairs.
|
153
|
+
# Metadata that has been created as listable can be
|
154
|
+
# retrieved based on the tag. This method iterates
|
155
|
+
# through all listable tags in Atmos.
|
156
|
+
#
|
157
|
+
# Tags can be hierarchical: foo/bar/baz. And each
|
158
|
+
# level anc have multiple tags under it. So to
|
159
|
+
# list all tags under the "foo" tag pass "foo" as the
|
160
|
+
# argument. To list all tags under "foo/bar", pass
|
161
|
+
# "foo/bar" as the argument.
|
162
|
+
#
|
163
|
+
def each_listable_tag(tag = nil) #:nodoc:
|
164
|
+
token = ""
|
165
|
+
headers = {}
|
166
|
+
headers['x-emc-tags'] = tag if (!tag.nil?)
|
167
|
+
|
168
|
+
while (!token.nil?)
|
169
|
+
response = @request.do(:listable_tags, headers)
|
170
|
+
response.each('x-emc-listable-tags') do |tag|
|
171
|
+
yield tag
|
172
|
+
end
|
173
|
+
|
174
|
+
token = response['x-emc-token']
|
175
|
+
headers['x-emc-token'] = token if (!token.nil?)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
#
|
181
|
+
# Iterate through objects that are indexed by a particular tag. Tag can't be nil,
|
182
|
+
# and must be a ruby String. Note that as the objects are instantiated, ACLs and
|
183
|
+
# Metadata will be retrieved, and object data will not.
|
184
|
+
#
|
185
|
+
# Note that there may not be any objects associated with a listable tag even though
|
186
|
+
# that tag shows up when listing all listable tags.
|
187
|
+
#
|
188
|
+
def each_object_with_listable_tag(tag)
|
189
|
+
raise Atmos::Exceptions::ArgumentException, "The 'tag' parameter cannot be nil." if (tag.nil?)
|
190
|
+
raise Atmos::Exceptions::ArgumentException, "Only one tag is allowed at a time." if (!tag.kind_of?(String))
|
191
|
+
|
192
|
+
response = @request.do(:list_objects, 'x-emc-include-meta' => 0, 'x-emc-tags' => tag)
|
193
|
+
|
194
|
+
Atmos::Parser::response_get_array(response, '//xmlns:ObjectID').each do |idxml|
|
195
|
+
yield Atmos::Object.new(self, idxml)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
|
200
|
+
#
|
201
|
+
# Returns the version of the Atmos server authenticated to as a string.
|
202
|
+
#
|
203
|
+
def server_version
|
204
|
+
@server_version
|
205
|
+
end
|
206
|
+
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|