ruby-manta 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4ac1cb7a31f6bd034a46082aa48450086999752e
4
- data.tar.gz: 6d19bdfbafe9f4a3ccaf820fb4bcbf246aa9cc99
3
+ metadata.gz: e741a0fbcb8ccef97c5efdac3290bd22c9bbcd74
4
+ data.tar.gz: 01bc9d49ec9fd487314fe5ef3f624b2f4f156f8c
5
5
  SHA512:
6
- metadata.gz: 316e6c5fda31c797fd8b17cecb1726ea252faed34982a193428529bff774c305850cf6cbc9446f30dafc74f658d43b0b955cece8d04fe08b2ce57049c82f0124
7
- data.tar.gz: be58ff1da8a33e125b29d509ab33f454cfaa54d2d594c67b46a1470c81ae6171b77d3eb786f8d69f9cb5116555e8c17f28f8aea72f7b2d572ce65c0b22c143f6
6
+ metadata.gz: d2a09a41b30212d25f174e5aa3c0672bcc66ae84870305e6fb7abf64195315314db2fdbdc12c4dfc3c5f07f547a223f1e3eb50e6778bb3ab6b70641d29451d60
7
+ data.tar.gz: 3e95d53baa10a0f77694e1490322421336714480c260447b867b78db9d85219ee92ab94c355f4891d98702476a5930717c566b9181cf23d2f95b6ecf2de5f0ba
data/README.md CHANGED
@@ -64,19 +64,19 @@ hurried friend, is an example demonstrating some of ruby-manta's usage:
64
64
 
65
65
  # You'll need to provide these four environment variables to run this
66
66
  # example. E.g.:
67
- # USER=john KEY=~/.ssh/john HOST=https://us-east.manta.joyent.com DIR=. \
68
- # ruby example.rb
69
- host = ENV['HOST']
70
- user = ENV['USER']
71
- priv_key = ENV['KEY' ]
72
- upload_dir = ENV['DIR' ]
67
+ # MANTA_USER=john MANTA_KEY=~/.ssh/john \
68
+ # MANTA_URL=https://us-east.manta.joyent.com LOCAL_DIR=. ruby example.rb
69
+ host = ENV['MANTA_URL']
70
+ user = ENV['MANTA_USER']
71
+ priv_key = ENV['MANTA_KEY' ]
72
+ upload_dir = ENV['LOCAL_DIR' ]
73
73
 
74
74
  # Read in private key, create a MantaClient instance. MantaClient is
75
75
  # thread-safe and provides persistent connections with pooling, so you'll
76
76
  # only ever need a single instance of this in a program.
77
77
  priv_key_data = File.read(priv_key)
78
- client = MantaClient.new(host, user, priv_key_data,
79
- :disable_ssl_verification => true)
78
+ client = RubyManta::MantaClient.new(host, user, priv_key_data,
79
+ :disable_ssl_verification => true)
80
80
 
81
81
  # Create an directory in Manta solely for this example run.
82
82
  dir_path = '/' + user + '/stor/ruby-manta-example'
@@ -183,7 +183,7 @@ see his image.png? In this case there is also the "public" space:
183
183
  Signed URLs
184
184
  -----------
185
185
 
186
- Objects put in the public space are accessible by everyone. Objects in the
186
+ Objects put in the public space are accessible by everyone. Objects in the
187
187
  private space are only accessible by individuals authenticated and authorized
188
188
  by Manta. Manta also supports temporary signed URLs that allow unauthenticated
189
189
  individuals to operate on private objects, until the link expires. See
@@ -312,8 +312,8 @@ Example:
312
312
  ````` ruby
313
313
 
314
314
  priv_key_data = File.read('/home/john/.ssh/john')
315
- client = MantaClient.new('https://manta.joyentcloud.com', 'john',
316
- priv_key_data, :disable_ssl_verification => true)
315
+ client = RubyManta::MantaClient.new('https://manta.joyentcloud.com', 'john',
316
+ priv_key_data, :disable_ssl_verification => true)
317
317
  `````
318
318
 
319
319
 
@@ -440,6 +440,20 @@ Examples:
440
440
 
441
441
 
442
442
 
443
+ find(dir_path, _options_)
444
+ -----------------------
445
+
446
+ Finds all Manta objects underneath a given path,
447
+
448
+ The path must be a valid directory path and point at an actual directory.
449
+
450
+ The path must be a valid directory path and point at an actual directory.
451
+ :limit optionally changes the maximum number of entries; the default is 1000.
452
+ If given :marker, an object name in the directory, returned directory entries
453
+ will begin from that point. :regex => can optionally be passed in to filter
454
+ filenames by a given regular expression.
455
+
456
+
443
457
  delete_directory(dir_path, _options_)
444
458
  -------------------------------------
445
459
 
data/example.rb CHANGED
@@ -1,19 +1,24 @@
1
- require 'ruby-manta'
1
+ require_relative 'lib/ruby-manta'
2
2
 
3
3
  # You'll need to provide these four environment variables to run this
4
4
  # example. E.g.:
5
- # USER=john KEY=~/.ssh/john HOST=https://us-east.manta.joyent.com DIR=. ruby example.rb
6
- host = ENV['HOST']
7
- user = ENV['USER']
8
- priv_key = ENV['KEY' ]
9
- upload_dir = ENV['DIR' ]
5
+ # MANTA_USER=john KEY=~/.ssh/john MANTA_URL=https://us-east.manta.joyent.com LOCAL_DIR=. ruby example.rb
6
+ host = ENV['MANTA_URL']
7
+ user = ENV['MANTA_USER']
8
+ priv_key = ENV['MANTA_KEY' ]
9
+ upload_dir = ENV['LOCAL_DIR' ]
10
+
11
+ raise 'You must specify MANTA_URL' unless host
12
+ raise 'You must specify MANTA_USER' unless user
13
+ raise 'You must specify MANTA_KEY' unless priv_key
14
+ raise 'You must specify LOCAL_DIR' unless upload_dir
10
15
 
11
16
  # Read in private key, create a MantaClient instance. MantaClient is
12
17
  # thread-safe and provides persistent connections with pooling, so you'll
13
18
  # only ever need a single instance of this in a program.
14
19
  priv_key_data = File.read(priv_key)
15
- client = MantaClient.new(host, user, priv_key_data,
16
- :disable_ssl_verification => true)
20
+ client = RubyManta::MantaClient.new(host, user, priv_key_data,
21
+ :disable_ssl_verification => true)
17
22
 
18
23
  # Create an directory in Manta solely for this example run.
19
24
  dir_path = '/' + user + '/stor/ruby-manta-example'
@@ -1,928 +1,7 @@
1
- # Copyright (c) 2012, Joyent, Inc. All rights reserved.
2
- #
3
- # ruby-manta is a simple low-abstraction layer which communicates with Joyent's
4
- # Manta service.
5
- #
6
- # Manta is an HTTP-accessible object store supporting UNIX-based map-reduce
7
- # jobs. Through ruby-manta a programmer can save/overwrite/delete objects
8
- # stored on a Manta service, or run map-reduce jobs over those objects.
9
- #
10
- # ruby-manta should be thread-safe, and supports pooling of keep-alive
11
- # connections to the same server (through HTTPClient). It only relies on the
12
- # standard library and two pure Ruby libraries, so it should work anywhere.
13
- #
14
- # For more information about Manta and general ruby-manta usage, please see
15
- # README.md.
16
-
17
-
18
-
19
- require 'openssl'
20
- require 'net/ssh'
21
- require 'httpclient'
22
- require 'base64'
23
- require 'digest'
24
- require 'time'
25
- require 'json'
26
- require 'cgi'
27
- require 'uri'
28
-
29
- require File.expand_path('../version', __FILE__)
30
-
31
-
32
-
33
- class MantaClient
34
- DEFAULT_ATTEMPTS = 3
35
- DEFAULT_CONNECT_TIMEOUT = 5
36
- DEFAULT_SEND_TIMEOUT = 60
37
- DEFAULT_RECEIVE_TIMEOUT = 60
38
- MAX_LIMIT = 1000
39
- HTTP_AGENT = "ruby-manta/#{LIB_VERSION} (#{RUBY_PLATFORM}; #{OpenSSL::OPENSSL_VERSION}) ruby/#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"
40
- HTTP_SIGNATURE = 'Signature keyId="/%s/keys/%s",algorithm="%s",signature="%s"'
41
- OBJ_PATH_REGEX = Regexp.new('^/[^/]+(?:/?$|/stor|/public|/reports|/jobs)(?:/|$)')
42
- JOB_PATH_REGEX = Regexp.new('^/[^/]+/jobs(?:/|$)')
43
-
44
- # match one or more protocol and hostnames, with optional port numbers.
45
- # E.g. "http://example.com https://example.com:8443"
46
- CORS_ORIGIN_REGEX = Regexp.new('^\w+://[^\s\:]+(?:\:\d+)?' +
47
- '(?:\s\w+://[^\s\:]+(?:\:\d+)?)*$')
48
- CORS_HEADERS_REGEX = Regexp.new('^[\w-]+(?:, [\w-]+)*$')
49
- CORS_METHODS = [ 'GET', 'POST', 'PUT', 'DELETE', 'OPTIONS' ]
50
-
51
- ERROR_CLASSES = [ 'AuthorizationFailed', 'AuthSchemeNotAllowed',
52
- 'BadRequest', 'Checksum', 'ConcurrentRequest',
53
- 'ContentLength', 'ContentMD5Mismatch',
54
- 'DirectoryDoesNotExist', 'DirectoryExists',
55
- 'DirectoryNotEmpty', 'DirectoryOperation',
56
- 'EntityExists', 'Internal', 'InvalidArgument',
57
- 'InvalidAuthToken', 'InvalidCredentials',
58
- 'InvalidDurabilityLevel', 'InvalidJob', 'InvalidKeyId',
59
- 'InvalidLink', 'InvalidSignature', 'InvalidJobState',
60
- 'JobNotFound', 'JobState', 'KeyDoesNotExist',
61
- 'LinkNotFound', 'LinkNotObject', 'LinkRequired',
62
- 'NotAcceptable', 'NotEnoughSpace', 'ParentNotDirectory',
63
- 'PreconditionFailed', 'PreSignedRequest',
64
- 'RequestEntityTooLarge', 'ResourceNotFound',
65
- 'RootDirectory', 'SecureTransportRequired',
66
- 'ServiceUnavailable', 'SourceObjectNotFound',
67
- 'SSLRequired', 'TaskInit', 'UploadTimeout',
68
- 'UserDoesNotExist', 'UserTaskError',
69
- # and errors that are specific to this class:
70
- 'CorruptResult', 'UnknownError',
71
- 'UnsupportedKey' ]
72
-
73
-
74
-
75
- # Initialize a MantaClient instance.
76
- #
77
- # priv_key_data is data read directly from an SSH private key (i.e. RFC 4716
78
- # format). The method can also accept several optional args: :connect_timeout,
79
- # :send_timeout, :receive_timeout, :disable_ssl_verification and :attempts.
80
- # The timeouts are in seconds, and :attempts determines the default number of
81
- # attempts each method will make upon receiving recoverable errors.
82
- #
83
- # Will throw an exception if given a key whose format it doesn't understand.
84
- def initialize(host, user, priv_key_data, opts = {})
85
- raise ArgumentError unless host =~ /^https{0,1}:\/\/.*[^\/]/
86
- raise ArgumentError unless user.is_a?(String) && user.size > 0
87
-
88
- @host = host
89
- @user = user
90
-
91
- @attempts = opts[:attempts] || DEFAULT_ATTEMPTS
92
- raise ArgumentError unless @attempts > 0
93
-
94
- if priv_key_data =~ /BEGIN RSA/
95
- @digest = OpenSSL::Digest::SHA1.new
96
- @digest_name = 'rsa-sha1'
97
- algorithm = OpenSSL::PKey::RSA
98
- elsif priv_key_data =~ /BEGIN DSA/
99
- @digest = OpenSSL::Digest::DSS1.new
100
- @digest_name = 'dsa-sha1'
101
- algorithm = OpenSSL::PKey::DSA
102
- else
103
- raise UnsupportedKey
104
- end
105
-
106
- @priv_key = algorithm.new(priv_key_data)
107
- @fingerprint = OpenSSL::Digest::MD5.hexdigest(@priv_key.to_blob).
108
- scan(/../).join(':')
109
-
110
- @client = HTTPClient.new
111
- @client.connect_timeout = opts[:connect_timeout] || DEFAULT_CONNECT_TIMEOUT
112
- @client.send_timeout = opts[:send_timeout ] || DEFAULT_SEND_TIMEOUT
113
- @client.receive_timeout = opts[:receive_timeout] || DEFAULT_RECEIVE_TIMEOUT
114
- @client.ssl_config.verify_mode = nil if opts[:disable_ssl_verification]
115
-
116
- @job_base = '/' + user + '/jobs'
117
- end
118
-
119
-
120
-
121
- # Uploads object data to Manta to the given path, along with a computed MD5
122
- # hash.
123
- #
124
- # The path must start with /<user>/stor or /<user/public. Data can be any
125
- # sequence of octets. The HTTP Content-Type stored on Manta can be set
126
- # with an optional :content_type argument; the default is
127
- # application/octet-stream. The number of distributed replicates of an object
128
- # stored in Manta can be set with an optional :durability_level; the default
129
- # is 2.
130
- #
131
- # Returns true along with received HTTP headers.
132
- #
133
- # If there was an unrecoverable error, throws an exception. On connection or
134
- # corruption errors, more attempts will be made; the number of attempts can
135
- # be altered by passing in :attempts.
136
- def put_object(obj_path, data, opts = {})
137
- url = obj_url(obj_path)
138
-
139
- opts[:data] = data
140
- headers = gen_headers(opts)
141
-
142
- cors_headers = gen_cors_headers(opts)
143
- headers = headers.concat(cors_headers)
144
-
145
- durability_level = opts[:durability_level]
146
- if durability_level
147
- raise ArgumentError unless durability_level > 0
148
- headers.push([ 'Durability-Level', durability_level ])
149
- end
150
-
151
- content_type = opts[:content_type]
152
- if content_type
153
- raise ArgumentError unless content_type.is_a? String
154
- headers.push([ 'Content-Type', content_type ])
155
- end
156
-
157
- attempt(opts[:attempts]) do
158
- result = @client.put(url, data, headers)
159
- raise unless result.is_a? HTTP::Message
160
-
161
- return true, result.headers if [204, 304].include? result.status
162
- raise_error(result)
163
- end
164
- end
165
-
166
-
167
-
168
- # Get an object from Manta at a given path, and checks it's uncorrupted.
169
- #
170
- # The path must start with /<user>/stor or /<user/public and point at an
171
- # actual object, as well as output objects for jobs. :head => true can
172
- # optionally be passed in to do a HEAD instead of a GET.
173
- #
174
- # Returns the retrieved data along with received HTTP headers.
175
- #
176
- # If there was an unrecoverable error, throws an exception. On connection or
177
- # corruption errors, more attempts will be made; the number of attempts can
178
- # be altered by passing in :attempts.
179
- def get_object(obj_path, opts = {})
180
- url = obj_url(obj_path)
181
- headers = gen_headers(opts)
182
-
183
- attempt(opts[:attempts]) do
184
- method = opts[:head] ? :head : :get
185
- result = @client.send(method, url, nil, headers)
186
- raise unless result.is_a? HTTP::Message
187
-
188
- if result.status == 200
189
- return true, result.headers if method == :head
190
-
191
- sent_md5 = result.headers['Content-MD5']
192
- received_md5 = Digest::MD5.base64digest(result.body)
193
- raise CorruptResult if sent_md5 != received_md5
194
-
195
- return result.body, result.headers
196
- elsif result.status == 304
197
- return nil, result.headers
198
- end
199
-
200
- raise_error(result)
201
- end
202
- end
203
-
204
-
205
-
206
- # Deletes an object off Manta at a given path.
207
- #
208
- # The path must start with /<user>/stor or /<user/public and point at an
209
- # actual object.
210
- #
211
- # Returns true along with received HTTP headers.
212
- #
213
- # If there was an unrecoverable error, throws an exception. On connection or
214
- # corruption errors, more attempts will be made; the number of attempts can
215
- # be altered by passing in :attempts.
216
- def delete_object(obj_path, opts = {})
217
- url = obj_url(obj_path)
218
- headers = gen_headers(opts)
219
-
220
- attempt(opts[:attempts]) do
221
- result = @client.delete(url, nil, headers)
222
- raise unless result.is_a? HTTP::Message
223
-
224
- return true, result.headers if result.status == 204
225
- raise_error(result)
226
- end
227
- end
228
-
229
-
230
-
231
- # Creates a directory on Manta at a given path.
232
- #
233
- # The path must start with /<user>/stor or /<user/public.
234
- #
235
- # Returns true along with received HTTP headers.
236
- #
237
- # If there was an unrecoverable error, throws an exception. On connection or
238
- # corruption errors, more attempts will be made; the number of attempts can
239
- # be altered by passing in :attempts.
240
- def put_directory(dir_path, opts = {})
241
- url = obj_url(dir_path)
242
- headers = gen_headers(opts)
243
- headers.push([ 'Content-Type', 'application/json; type=directory' ])
244
-
245
- cors_headers = gen_cors_headers(opts)
246
- headers = headers.concat(cors_headers)
247
-
248
- attempt(opts[:attempts]) do
249
- result = @client.put(url, nil, headers)
250
- raise unless result.is_a? HTTP::Message
251
-
252
- return true, result.headers if result.status == 204
253
- raise_error(result)
254
- end
255
- end
256
-
257
-
258
-
259
- # Gets a lexicographically sorted directory listing on Manta at a given path,
260
- #
261
- # The path must start with /<user>/stor or /<user/public and point at an
262
- # actual directory. :limit optionally changes the maximum number of entries;
263
- # the default is 1000. If given :marker, an object name in the directory,
264
- # returned directory entries will begin from that point. :head => true can
265
- # optionally be passed in to do a HEAD instead of a GET.
266
- #
267
- # Returns an array of hash objects, each object representing a directory
268
- # entry. Also returns the received HTTP headers.
269
- #
270
- # If there was an unrecoverable error, throws an exception. On connection or
271
- # corruption errors, more attempts will be made; the number of attempts can
272
- # be altered by passing in :attempts.
273
- def list_directory(dir_path, opts = {})
274
- url = obj_url(dir_path)
275
- headers = gen_headers(opts)
276
- query_parameters = {}
277
-
278
- limit = opts[:limit] || MAX_LIMIT
279
- raise ArgumentError unless 0 < limit && limit <= MAX_LIMIT
280
- query_parameters[:limit] = limit
281
-
282
- marker = opts[:marker]
283
- if marker
284
- raise ArgumentError unless marker.is_a? String
285
- query_parameters[:marker] = marker
286
- end
287
-
288
- attempt(opts[:attempts]) do
289
- method = opts[:head] ? :head : :get
290
- result = @client.send(method, url, query_parameters, headers)
291
- raise unless result.is_a? HTTP::Message
292
-
293
- if result.status == 200
294
- raise unless result.headers['Content-Type'] ==
295
- 'application/x-json-stream; type=directory'
296
-
297
- return true, result.headers if method == :head
298
-
299
- json_chunks = result.body.split("\n")
300
-
301
- if json_chunks.size > limit
302
- raise CorruptResult
303
- end
304
-
305
- dir_entries = json_chunks.map { |i| JSON.parse(i) }
306
-
307
- return dir_entries, result.headers
308
- end
309
-
310
- raise_error(result)
311
- end
312
- end
313
-
314
-
315
-
316
- # Removes a directory from Manta at a given path.
317
- #
318
- # The path must start with /<user>/stor or /<user/public and point at an
319
- # actual object.
320
- #
321
- # Returns true along with received HTTP headers.
322
- #
323
- # If there was an unrecoverable error, throws an exception. On connection or
324
- # corruption errors, more attempts will be made; the number of attempts can
325
- # be altered by passing in :attempts.
326
- def delete_directory(dir_path, opts = {})
327
- url = obj_url(dir_path)
328
- headers = gen_headers(opts)
329
-
330
- attempt(opts[:attempts]) do
331
- result = @client.delete(url, nil, headers)
332
- raise unless result.is_a? HTTP::Message
333
-
334
- return true, result.headers if result.status == 204
335
- raise_error(result)
336
- end
337
- end
338
-
339
-
340
-
341
- # Creates a snaplink from one object in Manta at a given path to a different
342
- # path.
343
- #
344
- # Both paths should start with /<user>/stor or /<user/public.
345
- #
346
- # Returns true along with received HTTP headers.
347
- #
348
- # If there was an unrecoverable error, throws an exception. On connection or
349
- # corruption errors, more attempts will be made; the number of attempts can
350
- # be altered by passing in :attempts.
351
- def put_snaplink(orig_path, link_path, opts = {})
352
- headers = gen_headers(opts)
353
- headers.push([ 'Content-Type', 'application/json; type=link' ],
354
- [ 'Location', obj_url(orig_path) ])
355
-
356
- attempt(opts[:attempts]) do
357
- result = @client.put(obj_url(link_path), nil, headers)
358
- raise unless result.is_a? HTTP::Message
359
-
360
- return true, result.headers if result.status == 204
361
- raise_error(result)
362
- end
363
- end
364
-
365
-
366
-
367
- # Creates a job in Manta.
368
- #
369
- # The job must be a hash, containing at minimum a :phases key. See README.md
370
- # or the Manta docs to see the format and options for setting up a job on
371
- # Manta; this method effectively just converts the job hash to JSON and sends
372
- # to the Manta service.
373
- #
374
- # Returns the path for the new job, along with received HTTP headers.
375
- #
376
- # If there was an unrecoverable error, throws an exception. On connection or
377
- # corruption errors, more attempts will be made; the number of attempts can
378
- # be altered by passing in :attempts.
379
- def create_job(job, opts = {})
380
- raise ArgumentError unless job[:phases] || job['phases']
381
-
382
- headers = gen_headers(opts)
383
- headers.push([ 'Content-Type', 'application/json; type=job' ])
384
- data = job.to_json
385
-
386
- attempt(opts[:attempts]) do
387
- result = @client.post(job_url(), data, headers)
388
- raise unless result.is_a? HTTP::Message
389
-
390
- if result.status == 201
391
- location = result.headers['Location']
392
- raise unless location
393
-
394
- return location, result.headers
395
- end
396
-
397
- raise_error(result)
398
- end
399
- end
400
-
401
-
402
-
403
- # Gets various information about a job in Manta at a given path.
404
- #
405
- # The path must start with /<user>/jobs/<job UUID> and point at an actual job.
406
- # :head => true can optionally be passed in to do a HEAD instead of a GET.
407
- #
408
- # Returns a hash with job information, along with received HTTP headers.
409
- #
410
- # If there was an unrecoverable error, throws an exception. On connection or
411
- # corruption errors, more attempts will be made; the number of attempts can
412
- # be altered by passing in :attempts.
413
- def get_job(job_path, opts = {})
414
- url = job_url(job_path, '/live/status')
415
- headers = gen_headers(opts)
416
-
417
- attempt(opts[:attempts]) do
418
- method = opts[:head] ? :head : :get
419
- result = @client.send(method, url, nil, headers)
420
- raise unless result.is_a? HTTP::Message
421
-
422
- if result.status == 200
423
- raise unless result.headers['Content-Type'] == 'application/json'
424
-
425
- return true, result.headers if method == :head
426
-
427
- job = JSON.parse(result.body)
428
- return job, result.headers
429
- end
430
-
431
- raise_error(result)
432
- end
433
- end
434
-
435
-
436
-
437
- # Gets errors that occured during the execution of a job in Manta at a given
438
- # path.
439
- #
440
- # The path must start with /<user>/jobs/<job UUID> and point at an actual job.
441
- # :head => true can optionally be passed in to do a HEAD instead of a GET.
442
- #
443
- # Returns an array of hashes, each hash containing information about an
444
- # error; this information is best-effort by Manta, so it may not be complete.
445
- # Also returns received HTTP headers.
446
- #
447
- # If there was an unrecoverable error, throws an exception. On connection or
448
- # corruption errors, more attempts will be made; the number of attempts can
449
- # be altered by passing in :attempts.
450
- def get_job_errors(job_path, opts = {})
451
- url = job_url(job_path, '/live/err')
452
- headers = gen_headers(opts)
453
-
454
- attempt(opts[:attempts]) do
455
- method = opts[:head] ? :head : :get
456
- result = @client.send(method, url, nil, headers)
457
- raise unless result.is_a? HTTP::Message
458
-
459
- if result.status == 200
460
- raise unless result.headers['Content-Type'] ==
461
- 'application/x-json-stream; type=job-error'
462
-
463
- return true, result.headers if method == :head
464
-
465
- json_chunks = result.body.split("\n")
466
- errors = json_chunks.map { |i| JSON.parse(i) }
467
-
468
- return errors, result.headers
469
- end
470
-
471
- raise_error(result)
472
- end
473
- end
474
-
475
-
476
-
477
- # Cancels a running job in Manta at a given path.
478
- #
479
- # The path must start with /<user>/jobs/<job UUID> and point at an actual job.
480
- #
481
- # Returns true, along with received HTTP headers.
482
- #
483
- # If there was an unrecoverable error, throws an exception. On connection or
484
- # corruption errors, more attempts will be made; the number of attempts can
485
- # be altered by passing in :attempts.
486
- def cancel_job(job_path, opts = {})
487
- url = job_url(job_path, '/live/cancel')
488
- headers = gen_headers(opts)
489
-
490
- attempt(opts[:attempts]) do
491
- result = @client.post(url, nil, headers)
492
- raise unless result.is_a? HTTP::Message
493
-
494
- return true, result.headers if result.status == 202
495
- raise_error(result)
496
- end
497
- end
498
-
499
-
500
-
501
- # Adds objects for a running job in Manta to process.
502
- #
503
- # The job_path must start with /<user>/jobs/<job UUID> and point at an actual
504
- # running job. The obj_paths must be an array of paths, starting with
505
- # /<user>/stor or /<user>/public, pointing at actual objects.
506
- #
507
- # Returns true, along with received HTTP headers.
508
- #
509
- # If there was an unrecoverable error, throws an exception. On connection or
510
- # corruption errors, more attempts will be made; the number of attempts can
511
- # be altered by passing in :attempts.
512
- def add_job_keys(job_path, obj_paths, opts = {})
513
- url = job_url(job_path, '/live/in')
514
- headers = gen_headers(opts)
515
- headers.push([ 'Content-Type', 'text/plain' ])
516
-
517
- data = obj_paths.join("\n")
518
-
519
- attempt(opts[:attempts]) do
520
- result = @client.post(url, data, headers)
521
- raise unless result.is_a? HTTP::Message
522
-
523
- return true, result.headers if result.status == 204
524
- raise_error(result)
525
- end
526
- end
527
-
528
-
529
-
530
- # Inform Manta that no more objects will be added for processing by a job,
531
- # and that the job should finish all phases and terminate.
532
- #
533
- # The job_path must start with /<user>/jobs/<job UUID> and point at an actual
534
- # running job.
535
- #
536
- # Returns true, along with received HTTP headers.
537
- #
538
- # If there was an unrecoverable error, throws an exception. On connection or
539
- # corruption errors, more attempts will be made; the number of attempts can
540
- # be altered by passing in :attempts.
541
- def end_job_input(job_path, opts = {})
542
- url = job_url(job_path, '/live/in/end')
543
- headers = gen_headers(opts)
544
-
545
- attempt(opts[:attempts]) do
546
- result = @client.post(url, nil, headers)
547
- raise unless result.is_a? HTTP::Message
548
-
549
- return true, result.headers if result.status == 202
550
- raise_error(result)
551
- end
552
- end
553
-
554
-
555
-
556
- # Get a list of objects that have been given to a Manta job for processing.
557
- #
558
- # The job_path must start with /<user>/jobs/<job UUID> and point at an actual
559
- # running job.
560
- #
561
- # Returns an array of object paths, along with received HTTP headers.
562
- #
563
- # If there was an unrecoverable error, throws an exception. On connection or
564
- # corruption errors, more attempts will be made; the number of attempts can
565
- # be altered by passing in :attempts.
566
- def get_job_input(job_path, opts = {})
567
- get_job_state_streams(:in, job_path, opts)
568
- end
569
-
570
-
571
-
572
- # Get a list of objects that contain the intermediate results of a running
573
- # Manta job.
574
- #
575
- # The job_path must start with /<user>/jobs/<job UUID> and point at an actual
576
- # running job.
577
- #
578
- # Returns an array of object paths, along with received HTTP headers.
579
- #
580
- # If there was an unrecoverable error, throws an exception. On connection or
581
- # corruption errors, more attempts will be made; the number of attempts can
582
- # be altered by passing in :attempts.
583
- def get_job_output(job_path, opts = {})
584
- get_job_state_streams(:out, job_path, opts)
585
- end
586
-
587
-
588
-
589
- # Get a list of objects that had failures during processing in a Manta job.
590
- #
591
- # The job_path must start with /<user>/jobs/<job UUID> and point at an actual
592
- # running job.
593
- #
594
- # Returns an array of object paths, along with received HTTP headers.
595
- #
596
- # If there was an unrecoverable error, throws an exception. On connection or
597
- # corruption errors, more attempts will be made; the number of attempts can
598
- # be altered by passing in :attempts.
599
- def get_job_failures(job_path, opts = {})
600
- get_job_state_streams(:fail, job_path, opts)
601
- end
602
-
603
-
604
-
605
- # Get lists of Manta jobs.
606
- #
607
- # The state indicates which kind of jobs to return. :running is for jobs
608
- # that are currently processing, :done and :all should be obvious. Be careful
609
- # of the latter two if you've run a lot of jobs -- the list could be quite
610
- # long.
611
- #
612
- # Returns an array of hashes, each hash containing some information about a
613
- # job. Also returns received HTTP headers.
614
- #
615
- # If there was an unrecoverable error, throws an exception. On connection or
616
- # corruption errors, more attempts will be made; the number of attempts can
617
- # be altered by passing in :attempts.
618
- def list_jobs(state, opts = {})
619
- raise ArgumentError unless [:all, :running, :done].include? state
620
- state = nil if state == :all
621
-
622
- headers = gen_headers(opts)
623
-
624
- attempt(opts[:attempts]) do
625
- # method = opts[:head] ? :head : :get
626
- method = :get # until added to Manta service
627
- result = @client.send(method, job_url(), { :state => state }, headers)
628
- raise unless result.is_a? HTTP::Message
629
-
630
- if result.status == 200
631
- # return true, result.headers if method == :head
632
- return [], result.headers if result.body.size == 0
633
-
634
- raise unless result.headers['Content-Type'] ==
635
- 'application/x-json-stream; type=job'
636
-
637
- json_chunks = result.body.split("\n")
638
- job_entries = json_chunks.map { |i| JSON.parse(i) }
639
-
640
- return job_entries, result.headers
641
- end
642
-
643
- raise_error(result)
644
- end
645
- end
646
-
647
-
648
-
649
- # Generates a signed URL which can be used by unauthenticated users to
650
- # make a request to Manta at the given path. This is typically used to GET
651
- # an object, or to make a CORS preflighted PUT request.
652
- #
653
- # expires is a Time object or integer representing time after epoch; this
654
- # determines how long the signed URL will be valid for. The method is either a
655
- # single HTTP method (:get, :put, :post, :delete, :options) or a list of such
656
- # methods that the signed URL is allowed to be used for. The path must start
657
- # with /<user>/stor. Lastly, the optional args is an array containing pairs of
658
- # query args that will be appended at the end of the URL.
659
- #
660
- # The returned URL is signed, and can be used either over HTTP or HTTPS until
661
- # it reaches the expiry date.
662
- def gen_signed_url(expires, method, path, args=[])
663
- methods = method.is_a?(Array) ? method : [method]
664
- raise ArgumentError unless (methods - [:get, :put, :post, :delete, :options]).empty?
665
- raise ArgumentError unless path =~ OBJ_PATH_REGEX
666
-
667
- key_id = '/%s/keys/%s' % [@user, @fingerprint]
668
-
669
- args.push([ 'expires', expires.to_i ])
670
- args.push([ 'algorithm', @digest_name ])
671
- args.push([ 'keyId', key_id ])
672
-
673
- method = methods.map {|m| m.to_s.upcase }.sort.join(",")
674
- host = URI.encode(@host.split('/').last)
675
- path = URI.encode(path)
676
-
677
- args.push(['method', method]) if methods.count > 1
678
-
679
- encoded_args = args.sort.map do |key, val|
680
- # to comply with RFC 3986
681
- CGI.escape(key.to_s) + '=' + CGI.escape(val.to_s)
682
- end.join('&')
683
-
684
- plaintext = "#{method}\n#{host}\n#{path}\n#{encoded_args}"
685
- signature = @priv_key.sign(@digest, plaintext)
686
- encoded_signature = CGI.escape(Base64.strict_encode64(signature))
687
-
688
- host + path + '?' + encoded_args + '&signature=' + encoded_signature
689
- end
690
-
691
-
692
-
693
- # Create some Manta error classes
694
- class MantaClientError < StandardError; end
695
- for class_name in ERROR_CLASSES
696
- MantaClient.const_set(class_name, Class.new(MantaClientError))
697
- end
698
-
699
-
700
-
701
- # ---------------------------------------------------------------------------
702
- protected
703
-
704
-
705
-
706
- # Fetch lists of objects that have a given status.
707
- #
708
- # type takes one of three values (:in, :out, fail), path must start with
709
- # /<user>/jobs/<job UUID> and point at an actual job.
710
- #
711
- # Returns an array of object paths, along with received HTTP headers.
712
- #
713
- # If there was an unrecoverable error, throws an exception. On connection or
714
- # corruption errors, more attempts will be made; the number of attempts can
715
- # be altered by passing in :attempts.
716
- def get_job_state_streams(type, path, opts)
717
- raise ArgumentError unless [:in, :out, :fail].include? type
718
-
719
- url = job_url(path, '/live/' + type.to_s)
720
- headers = gen_headers(opts)
721
-
722
- attempt(opts[:attempts]) do
723
- #method = opts[:head] ? :head : :get
724
- method = :get # until added to Manta service
725
- result = @client.send(method, url, nil, headers)
726
- raise unless result.is_a? HTTP::Message
727
-
728
- if result.status == 200
729
- raise unless result.headers['Content-Type'] == 'text/plain'
730
- return true, result.headers if method == :head
731
- paths = result.body.split("\n")
732
- return paths, result.headers
733
- end
734
-
735
- raise_error(result)
736
- end
737
- end
738
-
739
-
740
-
741
- # Returns a full URL for a given path to an object.
742
- def obj_url(path)
743
- raise ArgumentError unless path =~ OBJ_PATH_REGEX
744
-
745
- URI.encode(@host + path)
746
- end
747
-
748
-
749
-
750
- # Returns a full URL for a given path to a job.
751
- def job_url(*args)
752
- path = if args.size == 0
753
- @job_base
754
- else
755
- raise ArgumentError unless args.first =~ JOB_PATH_REGEX
756
- args.join('/')
757
- end
758
-
759
- URI.encode(@host + path)
760
- end
761
-
762
-
763
-
764
- # Executes a block. If there is a connection- or corruption-related exception
765
- # the block will be reexecuted up to the `tries' argument. It will sleep
766
- # for an exponentially-increasing number of seconds between retries.
767
- def attempt(tries, &blk)
768
- if tries
769
- raise ArgumentError unless tries > 0
770
- else
771
- tries ||= @attempts
772
- end
773
-
774
- attempt = 1
775
-
776
- while true
777
- begin
778
- return yield blk
779
- rescue Errno::ECONNREFUSED, HTTPClient::TimeoutError,
780
- CorruptResult => e
781
- raise e if attempt == tries
782
- sleep 2 ** attempt
783
- attempt += 1
784
- end
785
- end
786
- end
787
-
788
-
789
-
790
- # Creates headers to be given to the HTTP client and sent to the Manta
791
- # service. The most important is the Authorization header, without which
792
- # none of this class would work.
793
- def gen_headers(opts)
794
- now = Time.now.httpdate
795
- sig = gen_signature('date: ' + now)
796
-
797
- headers = [[ 'Date', now ],
798
- [ 'Authorization', sig ],
799
- [ 'User-Agent', HTTP_AGENT ],
800
- [ 'Accept-Version', '~1.0' ]]
801
-
802
-
803
- # headers for conditional requests (dates)
804
- for arg, conditional in [[:if_modified_since, 'If-Modified-Since' ],
805
- [:if_unmodified_since, 'If-Unmodified-Since']]
806
- date = opts[arg]
807
- next unless date
808
-
809
- date = Time.parse(date.to_s) unless date.kind_of? Time
810
- headers.push([conditional, date])
811
- end
812
-
813
- # headers for conditional requests (etags)
814
- for arg, conditional in [[:if_match, 'If-Match' ],
815
- [:if_none_match, 'If-None-Match']]
816
- etag = opts[arg]
817
- next unless etag
818
-
819
- raise ArgumentError unless etag.kind_of? String
820
- headers.push([conditional, etag])
821
- end
822
-
823
- origin = opts[:origin]
824
- if origin
825
- raise ArgumentError unless origin == 'null' || origin =~ CORS_ORIGIN_REGEX
826
- headers.push([ 'Origin', origin ])
827
- end
828
-
829
- # add md5 hash when sending data
830
- data = opts[:data]
831
- if data
832
- md5 = Digest::MD5.base64digest(data)
833
- headers.push([ 'Content-MD5', md5 ])
834
- end
835
-
836
- return headers
837
- end
838
-
839
-
840
-
841
- # Do some sanity checks and create CORS-related headers
842
- #
843
- # For more details, see http://www.w3.org/TR/cors/ and
844
- # https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS#Access-Control-Expose-Headers
845
- def gen_cors_headers(opts)
846
- headers = []
847
-
848
- allow_credentials = opts[:access_control_allow_credentials]
849
- if allow_credentials
850
- allow_credentials = allow_credentials.to_s
851
- raise ArgumentError unless allow_credentials == 'true' ||
852
- allow_credentials == 'false'
853
- headers.push([ 'Access-Control-Allow-Credentials', allow_credentials ])
854
- end
855
-
856
- allow_headers = opts[:access_control_allow_headers]
857
- if allow_headers
858
- raise ArgumentError unless allow_headers =~ CORS_HEADERS_REGEX
859
- allow_headers = allow_headers.split(', ').map(&:downcase).sort.join(', ')
860
- headers.push([ 'Access-Control-Allow-Headers', allow_headers ])
861
- end
862
-
863
- allow_methods = opts[:access_control_allow_methods]
864
- if allow_methods
865
- raise ArgumentError unless allow_methods.kind_of? String
866
-
867
- unknown_methods = allow_methods.split(', ').reject do |str|
868
- CORS_METHODS.include? str
869
- end
870
- raise ArgumentError unless unknown_methods.size == 0
871
-
872
- headers.push([ 'Access-Control-Allow-Methods', allow_methods ])
873
- end
874
-
875
- allow_origin = opts[:access_control_allow_origin]
876
- if allow_origin
877
- raise ArgumentError unless allow_origin.kind_of? String
878
- raise ArgumentError unless allow_origin == '*' ||
879
- allow_origin == 'null' ||
880
- allow_origin =~ CORS_ORIGIN_REGEX
881
- headers.push([ 'Access-Control-Allow-Origin', allow_origin ])
882
- end
883
-
884
- expose_headers = opts[:access_control_expose_headers]
885
- if expose_headers
886
- raise ArgumentError unless expose_headers =~ CORS_HEADERS_REGEX
887
- expose_headers = expose_headers.split(', ').map(&:downcase).sort.join(', ')
888
- headers.push([ 'Access-Control-Expose-Headers', expose_headers ])
889
- end
890
-
891
- max_age = opts[:access_control_max_age]
892
- if max_age
893
- raise ArgumentError unless max_age.kind_of?(Integer) && max_age >= 0
894
- headers.push([ 'Access-Control-Max-Age', max_age.to_s ])
895
- end
896
-
897
- headers
898
- end
899
-
900
-
901
-
902
- # Given a chunk of data, creates an HTTP signature which the Manta service
903
- # understands and uses for authentication.
904
- def gen_signature(data)
905
- raise ArgumentError unless data
906
-
907
- sig = @priv_key.sign(@digest, data)
908
- base64sig = Base64.strict_encode64(sig)
909
-
910
- return HTTP_SIGNATURE % [@user, @fingerprint, @digest_name, base64sig]
911
- end
912
-
913
-
914
-
915
- # Raises an appropriate exception given the HTTP response. If a 40* is
916
- # returned, attempts to look up an appropriate error class and raise,
917
- # otherwise raises an UnknownError.
918
- def raise_error(result)
919
- raise unless result.is_a? HTTP::Message
920
-
921
- err = JSON.parse(result.body)
922
- klass = MantaClient.const_get err['code']
923
- raise klass, err['message']
924
- rescue NameError, TypeError, JSON::ParserError
925
- raise UnknownError, result.status.to_s + ': ' + result.body
926
- end
1
+ module RubyManta
927
2
  end
928
3
 
4
+ require_relative 'ruby-manta/manta_client'
5
+
6
+ # Added API compatibility with 1.xx versions. This may be removed in the future
7
+ MantaClient = RubyManta::MantaClient