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.
@@ -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