ruby-atmos 0.6.0

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