aspera-cli 4.24.1 → 4.25.0.pre

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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +1064 -745
  4. data/CONTRIBUTING.md +43 -100
  5. data/README.md +1281 -720
  6. data/bin/ascli +20 -1
  7. data/bin/asession +23 -27
  8. data/lib/aspera/agent/base.rb +10 -21
  9. data/lib/aspera/agent/connect.rb +2 -3
  10. data/lib/aspera/agent/desktop.rb +2 -2
  11. data/lib/aspera/agent/direct.rb +49 -32
  12. data/lib/aspera/agent/factory.rb +31 -0
  13. data/lib/aspera/api/aoc.rb +134 -76
  14. data/lib/aspera/api/cos_node.rb +3 -2
  15. data/lib/aspera/api/faspex.rb +213 -0
  16. data/lib/aspera/api/node.rb +107 -94
  17. data/lib/aspera/ascmd.rb +1 -2
  18. data/lib/aspera/ascp/installation.rb +73 -58
  19. data/lib/aspera/ascp/management.rb +119 -23
  20. data/lib/aspera/assert.rb +39 -11
  21. data/lib/aspera/cli/error.rb +4 -2
  22. data/lib/aspera/cli/extended_value.rb +91 -67
  23. data/lib/aspera/cli/formatter.rb +62 -27
  24. data/lib/aspera/cli/hints.rb +8 -0
  25. data/lib/aspera/cli/info.rb +4 -4
  26. data/lib/aspera/cli/main.rb +76 -84
  27. data/lib/aspera/cli/manager.rb +352 -248
  28. data/lib/aspera/cli/plugins/alee.rb +5 -4
  29. data/lib/aspera/cli/plugins/aoc.rb +175 -195
  30. data/lib/aspera/cli/plugins/ats.rb +4 -4
  31. data/lib/aspera/cli/plugins/base.rb +343 -0
  32. data/lib/aspera/cli/plugins/basic_auth.rb +45 -0
  33. data/lib/aspera/cli/plugins/config.rb +283 -269
  34. data/lib/aspera/cli/plugins/console.rb +27 -22
  35. data/lib/aspera/cli/plugins/cos.rb +3 -3
  36. data/lib/aspera/cli/plugins/factory.rb +78 -0
  37. data/lib/aspera/cli/plugins/faspex.rb +49 -46
  38. data/lib/aspera/cli/plugins/faspex5.rb +113 -225
  39. data/lib/aspera/cli/plugins/faspio.rb +19 -18
  40. data/lib/aspera/cli/plugins/httpgw.rb +14 -13
  41. data/lib/aspera/cli/plugins/node.rb +162 -149
  42. data/lib/aspera/cli/plugins/oauth.rb +48 -0
  43. data/lib/aspera/cli/plugins/orchestrator.rb +129 -45
  44. data/lib/aspera/cli/plugins/preview.rb +30 -50
  45. data/lib/aspera/cli/plugins/server.rb +21 -21
  46. data/lib/aspera/cli/plugins/shares.rb +45 -47
  47. data/lib/aspera/cli/sync_actions.rb +50 -39
  48. data/lib/aspera/cli/transfer_agent.rb +35 -49
  49. data/lib/aspera/cli/transfer_progress.rb +6 -6
  50. data/lib/aspera/cli/version.rb +3 -3
  51. data/lib/aspera/cli/wizard.rb +70 -55
  52. data/lib/aspera/colors.rb +6 -0
  53. data/lib/aspera/command_line_builder.rb +59 -61
  54. data/lib/aspera/command_line_converter.rb +2 -1
  55. data/lib/aspera/coverage.rb +2 -2
  56. data/lib/aspera/data_repository.rb +1 -1
  57. data/lib/aspera/environment.rb +51 -41
  58. data/lib/aspera/faspex_gw.rb +7 -5
  59. data/lib/aspera/faspex_postproc.rb +1 -1
  60. data/lib/aspera/keychain/factory.rb +1 -2
  61. data/lib/aspera/keychain/macos_security.rb +1 -1
  62. data/lib/aspera/log.rb +37 -9
  63. data/lib/aspera/markdown.rb +31 -0
  64. data/lib/aspera/nagios.rb +7 -6
  65. data/lib/aspera/oauth/base.rb +25 -28
  66. data/lib/aspera/oauth/factory.rb +9 -9
  67. data/lib/aspera/oauth/url_json.rb +2 -1
  68. data/lib/aspera/oauth/web.rb +2 -2
  69. data/lib/aspera/preview/file_types.rb +23 -37
  70. data/lib/aspera/products/connect.rb +7 -6
  71. data/lib/aspera/products/desktop.rb +1 -4
  72. data/lib/aspera/products/other.rb +9 -1
  73. data/lib/aspera/products/transferd.rb +0 -1
  74. data/lib/aspera/rest.rb +168 -113
  75. data/lib/aspera/rest_error_analyzer.rb +4 -4
  76. data/lib/aspera/ssh.rb +7 -4
  77. data/lib/aspera/ssl.rb +41 -0
  78. data/lib/aspera/sync/args.schema.yaml +46 -3
  79. data/lib/aspera/sync/conf.schema.yaml +307 -123
  80. data/lib/aspera/sync/database.rb +2 -1
  81. data/lib/aspera/sync/operations.rb +135 -79
  82. data/lib/aspera/temp_file_manager.rb +17 -5
  83. data/lib/aspera/transfer/error.rb +16 -7
  84. data/lib/aspera/transfer/parameters.rb +35 -22
  85. data/lib/aspera/transfer/resumer.rb +74 -0
  86. data/lib/aspera/transfer/spec.rb +5 -5
  87. data/lib/aspera/transfer/spec.schema.yaml +170 -59
  88. data/lib/aspera/transfer/spec_doc.rb +49 -43
  89. data/lib/aspera/uri_reader.rb +2 -2
  90. data/lib/aspera/web_auth.rb +6 -6
  91. data/lib/transferd_pb.rb +2 -2
  92. data.tar.gz.sig +0 -0
  93. metadata +26 -11
  94. metadata.gz.sig +0 -0
  95. data/lib/aspera/cli/basic_auth_plugin.rb +0 -43
  96. data/lib/aspera/cli/plugin.rb +0 -333
  97. data/lib/aspera/cli/plugin_factory.rb +0 -81
  98. data/lib/aspera/resumer.rb +0 -77
  99. data/lib/aspera/transfer/error_info.rb +0 -91
data/lib/aspera/rest.rb CHANGED
@@ -50,84 +50,95 @@ module Aspera
50
50
  end
51
51
  end
52
52
 
53
+ class EntityNotFound < Error
54
+ end
55
+
53
56
  # a simple class to make HTTP calls, equivalent to rest-client
54
57
  # rest call errors are raised as exception RestCallError
55
58
  # and error are analyzed in RestErrorAnalyzer
56
59
  class Rest
57
- # flag for array parameters prefixed with []
58
- ARRAY_PARAMS = '[]'
59
-
60
- private_constant :ARRAY_PARAMS
61
-
62
- # error message when entity not found (TODO: use specific exception)
63
- ENTITY_NOT_FOUND = 'No such'
64
-
65
60
  MIME_JSON = 'application/json'
66
61
  MIME_WWW = 'application/x-www-form-urlencoded'
67
62
  MIME_TEXT = 'text/plain'
68
63
 
69
- # Content-Type that are JSON
70
- JSON_DECODE = [MIME_JSON, 'application/vnd.api+json', 'application/x-javascript'].freeze
71
-
72
- UNAVAILABLE_CODES = ['503']
64
+ # Special query parameter: max number of items for list command
65
+ MAX_ITEMS = 'max'
66
+ # Special query parameter: max number of pages for list command
67
+ MAX_PAGES = 'pmax'
73
68
 
74
69
  class << self
75
70
  # @return [String] Basic auth token
76
71
  def basic_authorization(user, pass); return "Basic #{Base64.strict_encode64("#{user}:#{pass}")}"; end
77
72
 
78
- # Build a parameter list prefixed with "[]"
79
- # @param values [Array] list of values
80
- def array_params(values)
81
- return [ARRAY_PARAMS].concat(values)
82
- end
83
-
84
- def array_params?(values)
85
- return values.first.eql?(ARRAY_PARAMS)
73
+ # Indicate that the given Hash query uses php style for array parameters
74
+ # @param query [Hash] A key can have Array value and result will use PHP format: a[]=1&a[]=2
75
+ def php_style(query)
76
+ Aspera.assert_type(query, Hash){'query'}
77
+ query[:x_array_php_style] = true
78
+ query
86
79
  end
87
80
 
88
81
  # Build URI from URL and parameters and check it is http or https
89
- # encode array [] parameters
90
- # @param query [Hash,Array]
91
- def build_uri(url, query = nil)
82
+ # Check iof php style is specified
83
+ # @param url [String] The URL without query.
84
+ # @param query [Hash,Array,String] The query.
85
+ def build_uri(url, query)
92
86
  uri = URI.parse(url)
93
- Aspera.assert(%w[http https].include?(uri.scheme)){"REST endpoint shall be http/s not #{uri.scheme}"}
87
+ Aspera.assert_values(uri.scheme, %w[http https]){'URI scheme'}
94
88
  return uri if query.nil? || query.respond_to?(:empty?) && query.empty?
95
89
  Log.dump(:query, query)
96
- query_array = []
97
- case query
98
- when Hash
99
- query.each do |k, v|
100
- case v
101
- when Array
102
- # support array for query parameter, there is no standard. Either p[]=1&p[]=2, or p=1&p=2
103
- suffix = array_params?(v) ? v.shift : ''
104
- v.each do |e|
105
- query_array.push(["#{k}#{suffix}", e])
106
- end
107
- else
108
- query_array.push([k, v])
90
+ uri.query =
91
+ case query
92
+ when String
93
+ query
94
+ when Hash
95
+ URI.encode_www_form(h_to_query_array(query))
96
+ when Array
97
+ Aspera.assert(query.all?{ |i| i.is_a?(Array) && i.length.eql?(2)}){'Query must be array of arrays of 2 elements'}
98
+ URI.encode_www_form(query)
99
+ else Aspera.error_unexpected_value(query.class){'query type'}
100
+ end.gsub('%5B%5D=', '[]=')
101
+ # [] is allowed in url parameters
102
+ uri
103
+ end
104
+
105
+ # @param query [Hash] HTTP query as hash
106
+ def h_to_query_array(query)
107
+ Aspera.assert_type(query, Hash)
108
+ # Support array for query parameter, there is no standard.
109
+ # Either p[]=1&p[]=2, or p=1&p=2
110
+ suffix = query.delete(:x_array_php_style) ? '[]' : nil
111
+ query.each_with_object([]) do |(k, v), query_array|
112
+ case v
113
+ when Array
114
+ v.each do |e|
115
+ query_array.push(["#{k}#{suffix}", e])
109
116
  end
117
+ else
118
+ query_array.push([k, v])
110
119
  end
111
- when Array
112
- Aspera.assert(query.all?{ |i| i.is_a?(Array) && i.length.eql?(2)}){'Query must be array of arrays or 2 elements'}
113
- query_array = query
114
- else
115
- raise "Query must be Hash or Array, not #{query.class}"
116
120
  end
117
- # [] is allowed in url parameters
118
- uri.query = URI.encode_www_form(query_array).gsub('%5B%5D=', '[]=')
119
- return uri
120
121
  end
121
122
 
122
123
  # Decode query string as Hash
123
- # Does not support arrays in query string, no standard, e.g. PHP's way is p[]=1&p[]=2
124
+ # if parameter is only once, then it's a scalar
125
+ # if a parameter is several, then it's array
126
+ # if parameter has [] then it's an array, and [] is removed
127
+ # Support arrays in query string, e.g. PHP's way is p[]=1&p[]=2
124
128
  # @param query [String] query string as in URI.query
125
129
  # @return [Hash] decoded query
126
130
  def query_to_h(query)
127
- URI.decode_www_form(query).each_with_object({}) do |pair, h|
128
- key = pair.first
129
- raise "Array not supported in query string: #{key}" if key.include?('[]') || h.key?(key)
130
- h[key] = pair.last
131
+ URI.decode_www_form(query).each_with_object({}) do |(key, value), h|
132
+ if key.end_with?('[]')
133
+ key = key[..-3]
134
+ h[key] = [] unless h.key?(key)
135
+ end
136
+ if h.key?(key)
137
+ h[key] = [h[key]] if !h[key].is_a?(Array)
138
+ h[key].push(value)
139
+ else
140
+ h[key] = value
141
+ end
131
142
  end
132
143
  end
133
144
 
@@ -135,13 +146,14 @@ module Aspera
135
146
  # @param base_url [String] base url of HTTP/S session
136
147
  # @return [Net::HTTP] a started HTTP session
137
148
  def start_http_session(base_url)
138
- uri = build_uri(base_url)
139
- # this honors http_proxy env var
149
+ uri = URI.parse(base_url)
150
+ Aspera.assert_values(uri.scheme, %w[http https]){'URI scheme'}
151
+ # This honors http_proxy env var
140
152
  http_session = Net::HTTP.new(uri.host, uri.port)
141
153
  http_session.use_ssl = uri.scheme.eql?('https')
142
- # set http options in callback, such as timeout and cert. verification
154
+ # Set http options in callback, such as timeout and cert. verification
143
155
  RestParameters.instance.session_cb&.call(http_session)
144
- # manually start session for keep alive (if supported by server, else, session is closed every time)
156
+ # Manually start session for keep alive (if supported by server, else, session is closed every time)
145
157
  http_session.start
146
158
  return http_session
147
159
  end
@@ -174,19 +186,31 @@ module Aspera
174
186
  return result
175
187
  end
176
188
 
177
- # Parse a header string as returned by HTTP
178
- # @param header [String] header string, e.g. "application/json; charset=utf-8"
179
- # @return [Hash] parsed header with type and parameters
180
- # {type: 'application/json', parameters: {charset: 'utf-8'}}
189
+ # Parses an HTTP Content-Type header string into its media type and parameters
190
+ # according to RFC 9110 and RFC 6838.
191
+ # TODO: use gem: content_type
192
+ #
193
+ # @param header [String] The Content-Type header string, e.g., "application/json; charset=utf-8"
194
+ # @return [Hash] A hash with :type and :parameters keys.
195
+ # Example:
196
+ # {
197
+ # type: "application/json",
198
+ # parameters: {
199
+ # charset: "utf-8",
200
+ # version: "1.0"
201
+ # }
202
+ # }
181
203
  def parse_header(header)
182
- type, *params = header.split(/;\s*/)
183
- parameters = params.map do |param|
184
- one = param.split(/=\s*/)
185
- one[0] = one[0].to_sym
186
- one[1] = one[1].gsub(/\A"|"\z/, '')
187
- one
204
+ parts = header.split(';').map(&:strip)
205
+ media_type = parts.shift.downcase
206
+ parameters = parts.filter_map do |param|
207
+ key, value = param.split('=', 2)
208
+ next unless key && value
209
+ key = key.strip.downcase.to_sym
210
+ value = value.strip.gsub(/\A"|"\z/, '')
211
+ [key, value]
188
212
  end.to_h
189
- {type: type.downcase, parameters: parameters}
213
+ {type: media_type, parameters: parameters}
190
214
  end
191
215
  end
192
216
 
@@ -200,8 +224,13 @@ module Aspera
200
224
 
201
225
  public
202
226
 
203
- attr_reader :base_url
227
+ # All original constructor parameters
204
228
  attr_reader :auth_params
229
+
230
+ # The root URL for the API
231
+ attr_reader :base_url
232
+
233
+ # Base common headers of API
205
234
  attr_reader :headers
206
235
 
207
236
  # @return creation parameters
@@ -271,14 +300,16 @@ module Aspera
271
300
  end
272
301
 
273
302
  # HTTP/S REST call
274
- # @param operation [String] HTTP operation (GET, POST, PUT, DELETE)
275
- # @param subpath [String] subpath of REST API
276
- # @param query [Hash] URL parameters
277
- # @param content_type [String,nil] Type of body parameters (one of MIME_*) and serialization, else use headers
278
- # @param body [Hash, String] body parameters
279
- # @param headers [Hash] additional headers (override Content-Type)
280
- # @param save_to_file (filepath)
281
- # @param return_error (bool)
303
+ # @param operation [String] HTTP operation (GET, POST, PUT, DELETE)
304
+ # @param subpath [String] subpath of REST API
305
+ # @param query [Hash] URL parameters
306
+ # @param content_type [String, nil] Type of body parameters (one of MIME_*) and serialization, else use headers
307
+ # @param body [Hash, String] body parameters
308
+ # @param headers [Hash] additional headers (override Content-Type)
309
+ # @param save_to_file [String, nil](filepath)
310
+ # @param exception [Bool] `true`, error raise exception
311
+ # @param ret [:data, :resp, :both] Tell to return only data, only http response, or both
312
+ # @return [Object, Array] only data, only http response, or both
282
313
  def call(
283
314
  operation:,
284
315
  subpath: nil,
@@ -287,13 +318,18 @@ module Aspera
287
318
  body: nil,
288
319
  headers: nil,
289
320
  save_to_file: nil,
290
- return_error: false
321
+ exception: true,
322
+ ret: :data
291
323
  )
292
324
  subpath = subpath.to_s if subpath.is_a?(Symbol)
293
325
  subpath = '' if subpath.nil?
294
326
  Log.log.debug{"call #{operation} [#{subpath}]".red.bold.bg_green}
295
- Log.dump(:body, body)
327
+ Log.dump(:body, body, level: :trace1)
328
+ Log.dump(:query, query, level: :trace1)
329
+ Log.dump(:headers, headers, level: :trace1)
296
330
  Aspera.assert_type(subpath, String)
331
+ # We must have a way to check return code
332
+ Aspera.assert(exception || !ret.eql?(:data))
297
333
  if headers.nil?
298
334
  headers = @headers.clone
299
335
  else
@@ -317,7 +353,8 @@ module Aspera
317
353
  end
318
354
  else Aspera.error_unexpected_value(@auth_params[:type])
319
355
  end
320
- result = {http: nil}
356
+ result_http = nil
357
+ result_data = nil
321
358
  # start a block to be able to retry the actual HTTP request in case of OAuth token expiration
322
359
  begin
323
360
  # TODO: shall we percent encode subpath (spaces) test with access key delete with space in id
@@ -360,14 +397,14 @@ module Aspera
360
397
  file_saved = false
361
398
  # make http request (pipelined)
362
399
  http_session.request(req) do |response|
363
- result[:http] = response
364
- result_mime = self.class.parse_header(result[:http]['Content-Type'] || MIME_TEXT)[:type]
365
- Log.log.debug{"response: code=#{result[:http].code}, mime=#{result_mime}, mime2= #{response['Content-Type']}"}
400
+ result_http = response
401
+ result_mime = self.class.parse_header(result_http['Content-Type'] || MIME_TEXT)[:type]
402
+ Log.log.debug{"response: code=#{result_http.code}, mime=#{result_mime}, mime2= #{response['Content-Type']}"}
366
403
  # JSON data needs to be parsed, in case it contains an error code
367
404
  if !save_to_file.nil? &&
368
- result[:http].code.to_s.start_with?('2') &&
405
+ result_http.code.to_s.start_with?('2') &&
369
406
  !JSON_DECODE.include?(result_mime)
370
- total_size = result[:http]['Content-Length']&.to_i
407
+ total_size = result_http['Content-Length']&.to_i
371
408
  Log.log.debug('before write file')
372
409
  target_file = save_to_file
373
410
  # override user's path to path in header
@@ -385,7 +422,7 @@ module Aspera
385
422
  FileUtils.mkdir_p(File.dirname(target_file_tmp))
386
423
  limiter = TimerLimiter.new(0.5)
387
424
  File.open(target_file_tmp, 'wb') do |file|
388
- result[:http].read_body do |fragment|
425
+ result_http.read_body do |fragment|
389
426
  file.write(fragment)
390
427
  written_size += fragment.length
391
428
  RestParameters.instance.progress_bar&.event(:transfer, session_id: session_id, info: written_size) if limiter.trigger?
@@ -398,32 +435,32 @@ module Aspera
398
435
  file_saved = true
399
436
  end
400
437
  end
401
- Log.log.debug{"result: code=#{result[:http].code} mime=#{result_mime}"}
438
+ Log.log.debug{"result: code=#{result_http.code} mime=#{result_mime}"}
402
439
  # sometimes there is a UTF8 char (e.g. (c) ), TODO : related to mime type encoding ?
403
- # result[:http].body.force_encoding('UTF-8') if result[:http].body.is_a?(String)
404
- # Log.log.debug{"result: body=#{result[:http].body}"}
440
+ # result_http.body.force_encoding('UTF-8') if result_http.body.is_a?(String)
441
+ # Log.log.debug{"result: body=#{result_http.body}"}
405
442
  case result_mime
406
443
  when *JSON_DECODE
407
- result[:data] = JSON.parse(result[:http].body) rescue result[:http].body
408
- Log.dump(:result_data, result[:data])
444
+ result_data = JSON.parse(result_http.body) rescue result_http.body
445
+ Log.dump(:result_data, result_data)
409
446
  else # when MIME_TEXT
410
- result[:data] = result[:http].body
447
+ result_data = result_http.body
411
448
  end
412
- RestErrorAnalyzer.instance.raise_on_error(req, result)
449
+ RestErrorAnalyzer.instance.raise_on_error(req, result_data, result_http)
413
450
  unless file_saved || save_to_file.nil?
414
451
  FileUtils.mkdir_p(File.dirname(save_to_file))
415
- File.write(save_to_file, result[:http].body, binmode: true)
452
+ File.write(save_to_file, result_http.body, binmode: true)
416
453
  end
417
454
  rescue RestCallError => e
418
455
  do_retry = false
419
456
  # AoC have some timeout , like Connect to platform.bss.asperasoft.com:443 ...
420
457
  do_retry ||= true if e.response.body.include?('failed: connect timed out') && RestParameters.instance.retry_on_timeout
421
458
  # AoC sometimes not available
422
- do_retry ||= true if RestParameters.instance.retry_on_unavailable && UNAVAILABLE_CODES.include?(result[:http].code.to_s)
459
+ do_retry ||= true if RestParameters.instance.retry_on_unavailable && UNAVAILABLE_CODES.include?(result_http.code.to_s)
423
460
  # possibility to retry anything if it fails
424
461
  do_retry ||= true if RestParameters.instance.retry_on_error
425
462
  # not authorized: oauth token expired
426
- if @not_auth_codes.include?(result[:http].code.to_s) && @auth_params[:type].eql?(:oauth2)
463
+ if @not_auth_codes.include?(result_http.code.to_s) && @auth_params[:type].eql?(:oauth2)
427
464
  begin
428
465
  # try to use refresh token
429
466
  req['Authorization'] = oauth.authorization(refresh: true)
@@ -462,15 +499,21 @@ module Aspera
462
499
  body: body,
463
500
  content_type: content_type,
464
501
  save_to_file: save_to_file,
465
- return_error: return_error,
466
- headers: headers
502
+ exception: exception,
503
+ headers: headers,
504
+ ret: ret
467
505
  )
468
506
  end
469
507
  # raise exception if could not retry and not return error in result
470
- raise e unless return_error
508
+ raise e if exception
471
509
  end
472
- Log.log.debug{"result=http:#{result[:http]}, data:#{result[:data].class}"}
473
- return result
510
+ Log.log.debug{"result=http:#{result_http}, data:#{result_data.class}"}
511
+ return case ret
512
+ when :data then result_data
513
+ when :resp then result_http
514
+ when :both then [result_data, result_http]
515
+ else Aspera.error_unexpected_value(ret){'Type of result for REST'}
516
+ end
474
517
  end
475
518
 
476
519
  #
@@ -478,31 +521,36 @@ module Aspera
478
521
  # If specific elements are needed, then use the full `call` method
479
522
  #
480
523
 
481
- def create(subpath, params)
482
- return call(operation: 'POST', subpath: subpath, headers: {'Accept' => MIME_JSON}, body: params, content_type: MIME_JSON)[:data]
524
+ # Create: `POST`
525
+ def create(subpath, params, **kwargs)
526
+ return call(operation: 'POST', subpath: subpath, headers: {'Accept' => MIME_JSON}, body: params, content_type: MIME_JSON, **kwargs)
483
527
  end
484
528
 
485
- def read(subpath, query = nil)
486
- return call(operation: 'GET', subpath: subpath, headers: {'Accept' => MIME_JSON}, query: query)[:data]
529
+ # Read: `GET`
530
+ def read(subpath, query = nil, **kwargs)
531
+ return call(operation: 'GET', subpath: subpath, headers: {'Accept' => MIME_JSON}, query: query, **kwargs)
487
532
  end
488
533
 
489
- def update(subpath, params)
490
- return call(operation: 'PUT', subpath: subpath, headers: {'Accept' => MIME_JSON}, body: params, content_type: MIME_JSON)[:data]
534
+ # Update: `PUT`
535
+ def update(subpath, params, **kwargs)
536
+ return call(operation: 'PUT', subpath: subpath, headers: {'Accept' => MIME_JSON}, body: params, content_type: MIME_JSON, **kwargs)
491
537
  end
492
538
 
493
- def delete(subpath, params = nil)
494
- return call(operation: 'DELETE', subpath: subpath, headers: {'Accept' => MIME_JSON}, query: params)[:data]
539
+ # Delete: `DELETE`
540
+ def delete(subpath, params = nil, **kwargs)
541
+ return call(operation: 'DELETE', subpath: subpath, headers: {'Accept' => MIME_JSON}, query: params, **kwargs)
495
542
  end
496
543
 
497
- def cancel(subpath)
498
- return call(operation: 'CANCEL', subpath: subpath, headers: {'Accept' => MIME_JSON})[:data]
544
+ # Cancel: `CANCEL`
545
+ def cancel(subpath, **kwargs)
546
+ return call(operation: 'CANCEL', subpath: subpath, headers: {'Accept' => MIME_JSON}, **kwargs)
499
547
  end
500
548
 
501
549
  # Query entity by general search (read with parameter `q`)
502
550
  # TODO: not generic enough ? move somewhere ? inheritance ?
503
- # @param subpath path of entity in API
504
- # @param search_name name of searched entity
505
- # @param query additional search query parameters
551
+ # @param subpath [String] Path of entity in API
552
+ # @param search_name [String] Name of searched entity
553
+ # @param query [Hash] Additional search query parameters
506
554
  # @returns [Hash] A single entity matching the search, or an exception if not found or multiple found
507
555
  def lookup_by_name(subpath, search_name, query: nil)
508
556
  query = {} if query.nil?
@@ -513,17 +561,24 @@ module Aspera
513
561
  Aspera.assert_type(matching_items, Array)
514
562
  case matching_items.length
515
563
  when 1 then return matching_items.first
516
- when 0 then raise %Q{#{ENTITY_NOT_FOUND} #{subpath}: "#{search_name}"}
564
+ when 0 then raise EntityNotFound, %Q{No such #{subpath}: "#{search_name}"}
517
565
  else
518
566
  # multiple case insensitive partial matches, try case insensitive full match
519
567
  # (anyway AoC does not allow creation of 2 entities with same case insensitive name)
520
568
  name_matches = matching_items.select{ |i| i['name'].casecmp?(search_name)}
521
569
  case name_matches.length
522
570
  when 1 then return name_matches.first
523
- when 0 then raise %Q(#{subpath}: multiple case insensitive partial match for: "#{search_name}": #{matching_items.map{ |i| i['name']}} but no case insensitive full match. Please be more specific or give exact name.)
571
+ when 0 then raise %Q(#{subpath}: Multiple case insensitive partial match for: "#{search_name}": #{matching_items.map{ |i| i['name']}} but no case insensitive full match. Please be more specific or give exact name.)
524
572
  else raise "Two entities cannot have the same case insensitive name: #{name_matches.map{ |i| i['name']}}"
525
573
  end
526
574
  end
527
575
  end
576
+
577
+ # Content-Type that are JSON
578
+ JSON_DECODE = [MIME_JSON, 'application/vnd.api+json', 'application/x-javascript'].freeze
579
+
580
+ UNAVAILABLE_CODES = ['503']
581
+
582
+ private_constant :JSON_DECODE, :UNAVAILABLE_CODES
528
583
  end
529
584
  end
@@ -27,13 +27,13 @@ module Aspera
27
27
  # Use this method to analyze a EST result and raise an exception
28
28
  # Analyzes REST call response and raises a RestCallError exception
29
29
  # if HTTP result code is not 2XX
30
- def raise_on_error(req, res)
31
- Log.log.debug{"raise_on_error #{req.method} #{req.path} #{res[:http].code}"}
30
+ def raise_on_error(req, data, http)
31
+ Log.log.debug{"raise_on_error #{req.method} #{req.path} #{http.code}"}
32
32
  call_context = {
33
33
  messages: [],
34
34
  request: req,
35
- response: res[:http],
36
- data: res[:data]
35
+ response: http,
36
+ data: data
37
37
  }
38
38
  # multiple error messages can be found
39
39
  # analyze errors from provided handlers
data/lib/aspera/ssh.rb CHANGED
@@ -11,19 +11,21 @@ module Aspera
11
11
  class Error < Aspera::Error
12
12
  end
13
13
  class << self
14
+ # HACK: disable some key type
14
15
  def disable_ed25519_keys
15
16
  Log.log.debug('Disabling SSH ed25519 user keys')
16
17
  old_verbose = $VERBOSE
17
18
  $VERBOSE = nil
18
19
  Net::SSH::Authentication::Session.class_eval do
19
20
  define_method(:default_keys) do
20
- %w[~/.ssh/id_dsa ~/.ssh/id_rsa ~/.ssh2/id_dsa ~/.ssh2/id_rsa].freeze
21
+ %w[.ssh .ssh2].product(%w[rsa dsa ecdsa]).map{"~/#{_1}/id_#{_2}"}.freeze
21
22
  end
22
23
  private(:default_keys)
23
24
  end rescue nil
24
25
  $VERBOSE = old_verbose
25
26
  end
26
27
 
28
+ # HACK: disable some algorithms
27
29
  def disable_ecd_sha2_algorithms
28
30
  Log.log.debug('Disabling SSH ecdsa')
29
31
  Net::SSH::Transport::Algorithms::ALGORITHMS.each_value{ |a| a.reject!{ |a| a =~ /^ecd(sa|h)-sha2/}}
@@ -38,6 +40,7 @@ module Aspera
38
40
  Aspera.assert_type(host, String)
39
41
  Aspera.assert_type(username, String)
40
42
  Aspera.assert_type(ssh_options, Hash)
43
+ ssh_options[:use_agent] = false unless ssh_options.key?(:use_agent)
41
44
  @host = host
42
45
  @username = username
43
46
  @ssh_options = ssh_options
@@ -62,7 +65,7 @@ module Aspera
62
65
  exit_code = data.read_long
63
66
  next if exit_code.zero?
64
67
  error_message = "#{cmd}: exit #{exit_code}, #{error.join.chomp}"
65
- raise Error, error_message if exception
68
+ raise Error, error_message if exception
66
69
  # Happens when windows user hasn't logged in and created home account.
67
70
  error_message += "\nHint: home not created in Windows?" if data.include?('Could not chdir to home directory')
68
71
  Log.log.debug(error_message)
@@ -81,6 +84,6 @@ module Aspera
81
84
  end
82
85
  end
83
86
 
84
- # HACK: deactivate ed25519 and ecdsa private keys from SSH identities, as it usually causes problems
85
- Aspera::Ssh.disable_ed25519_keys if ENV.fetch('ASCLI_ENABLE_ED25519', 'false').eql?('false')
87
+ # Deactivate ed25519 and ecdsa private keys from SSH identities, as it usually causes problems
88
+ Aspera::Ssh.disable_ed25519_keys if Gem::Specification.find_all_by_name('ed25519').none? || ENV.fetch('ASCLI_ENABLE_ED25519', 'true').eql?('false')
86
89
  Aspera::Ssh.disable_ecd_sha2_algorithms if defined?(JRUBY_VERSION) && ENV.fetch('ASCLI_ENABLE_ECDSHA2', 'false').eql?('false')
data/lib/aspera/ssl.rb ADDED
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'aspera/assert'
5
+ require 'aspera/log'
6
+
7
+ module Aspera
8
+ # Give possibility to globally override SSL options
9
+ module SSL
10
+ @extra_options = 0
11
+ class << self
12
+ @extra_options = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options]
13
+ attr_reader :extra_options
14
+
15
+ def option_list=(v)
16
+ Aspera.assert_type(v, Array){'ssl_options'}
17
+ v.each do |opt|
18
+ Aspera.assert_type(opt, String, Integer){'Expected String or Integer in ssl_options'}
19
+ case opt
20
+ when Integer
21
+ @extra_options = opt
22
+ when String
23
+ name = "OP_#{opt.start_with?('-') ? opt[1..] : opt}".upcase
24
+ raise Cli::BadArgument, "Unknown ssl_option: #{name}, use one of: #{OpenSSL::SSL.constants.grep(/^OP_/).map{ |c| c.to_s.sub(/^OP_/, '')}.join(', ')}" if !OpenSSL::SSL.const_defined?(name)
25
+ if opt.start_with?('-')
26
+ @extra_options &= ~OpenSSL::SSL.const_get(name)
27
+ else
28
+ @extra_options |= OpenSSL::SSL.const_get(name)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ def set_params(params = {})
35
+ super(params)
36
+ self.options = Aspera::SSL.extra_options unless Aspera::SSL.extra_options.nil?
37
+ self
38
+ end
39
+ end
40
+ end
41
+ OpenSSL::SSL::SSLContext.prepend(Aspera::SSL)