ruby-atmos 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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