aspera-cli 4.25.1 → 4.25.3

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 (54) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +456 -405
  4. data/CONTRIBUTING.md +22 -18
  5. data/README.md +33 -9741
  6. data/bin/asession +111 -88
  7. data/lib/aspera/agent/connect.rb +1 -1
  8. data/lib/aspera/agent/desktop.rb +1 -1
  9. data/lib/aspera/agent/direct.rb +19 -18
  10. data/lib/aspera/agent/node.rb +1 -1
  11. data/lib/aspera/api/aoc.rb +44 -20
  12. data/lib/aspera/api/faspex.rb +25 -6
  13. data/lib/aspera/api/node.rb +20 -16
  14. data/lib/aspera/ascp/installation.rb +32 -51
  15. data/lib/aspera/assert.rb +2 -2
  16. data/lib/aspera/cli/extended_value.rb +1 -0
  17. data/lib/aspera/cli/formatter.rb +0 -4
  18. data/lib/aspera/cli/hints.rb +18 -4
  19. data/lib/aspera/cli/main.rb +3 -6
  20. data/lib/aspera/cli/manager.rb +46 -30
  21. data/lib/aspera/cli/plugins/aoc.rb +155 -131
  22. data/lib/aspera/cli/plugins/base.rb +15 -18
  23. data/lib/aspera/cli/plugins/config.rb +50 -87
  24. data/lib/aspera/cli/plugins/factory.rb +2 -2
  25. data/lib/aspera/cli/plugins/faspex.rb +4 -4
  26. data/lib/aspera/cli/plugins/faspex5.rb +74 -76
  27. data/lib/aspera/cli/plugins/node.rb +3 -5
  28. data/lib/aspera/cli/plugins/oauth.rb +26 -25
  29. data/lib/aspera/cli/plugins/preview.rb +9 -14
  30. data/lib/aspera/cli/plugins/shares.rb +15 -7
  31. data/lib/aspera/cli/transfer_agent.rb +2 -2
  32. data/lib/aspera/cli/version.rb +1 -1
  33. data/lib/aspera/colors.rb +7 -0
  34. data/lib/aspera/environment.rb +30 -16
  35. data/lib/aspera/faspex_gw.rb +6 -6
  36. data/lib/aspera/faspex_postproc.rb +20 -14
  37. data/lib/aspera/hash_ext.rb +8 -0
  38. data/lib/aspera/log.rb +15 -15
  39. data/lib/aspera/markdown.rb +22 -0
  40. data/lib/aspera/node_simulator.rb +1 -1
  41. data/lib/aspera/oauth/base.rb +2 -2
  42. data/lib/aspera/oauth/url_json.rb +2 -2
  43. data/lib/aspera/oauth/web.rb +1 -1
  44. data/lib/aspera/preview/generator.rb +9 -9
  45. data/lib/aspera/rest.rb +44 -37
  46. data/lib/aspera/rest_call_error.rb +16 -8
  47. data/lib/aspera/rest_error_analyzer.rb +38 -36
  48. data/lib/aspera/rest_errors_aspera.rb +19 -18
  49. data/lib/aspera/transfer/resumer.rb +2 -2
  50. data/lib/aspera/yaml.rb +49 -0
  51. data.tar.gz.sig +0 -0
  52. metadata +17 -3
  53. metadata.gz.sig +0 -0
  54. data/release_notes.md +0 -8
data/lib/aspera/rest.rb CHANGED
@@ -53,14 +53,24 @@ module Aspera
53
53
  class EntityNotFound < Error
54
54
  end
55
55
 
56
- # a simple class to make HTTP calls, equivalent to rest-client
56
+ module Mime
57
+ JSON = 'application/json'
58
+ WWW = 'application/x-www-form-urlencoded'
59
+ TEXT = 'text/plain'
60
+ # Check if a MIME type is JSON, including parameters, e.g. application/json; charset=utf-8
61
+ def json?(mime)
62
+ JSON_LIST.include?(mime)
63
+ end
64
+ module_function :json?
65
+ # Content-Type that are JSON
66
+ JSON_LIST = [JSON, 'application/vnd.api+json', 'application/x-javascript'].freeze
67
+ private_constant :JSON_LIST
68
+ end
69
+
70
+ # Make HTTP calls, equivalent to rest-client
57
71
  # rest call errors are raised as exception RestCallError
58
72
  # and error are analyzed in RestErrorAnalyzer
59
73
  class Rest
60
- MIME_JSON = 'application/json'
61
- MIME_WWW = 'application/x-www-form-urlencoded'
62
- MIME_TEXT = 'text/plain'
63
-
64
74
  # Special query parameter: max number of items for list command
65
75
  MAX_ITEMS = 'max'
66
76
  # Special query parameter: max number of pages for list command
@@ -143,8 +153,8 @@ module Aspera
143
153
  end
144
154
 
145
155
  # Start a HTTP/S session, also used for web sockets
146
- # @param base_url [String] base url of HTTP/S session
147
- # @return [Net::HTTP] a started HTTP session
156
+ # @param base_url [String] Base url of HTTP/S session
157
+ # @return [Net::HTTP] A started HTTP session
148
158
  def start_http_session(base_url)
149
159
  uri = URI.parse(base_url)
150
160
  Aspera.assert_values(uri.scheme, %w[http https]){'URI scheme'}
@@ -302,16 +312,16 @@ module Aspera
302
312
  # HTTP/S REST call
303
313
  # @param operation [String] HTTP operation (GET, POST, PUT, DELETE)
304
314
  # @param subpath [String] subpath of REST API
305
- # @param query [Hash] URL parameters
315
+ # @param query [Hash{String,Symbol => Object}] URL parameters
306
316
  # @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 [Boolean] `true`, error raise exception
311
- # @param ret [:data, :resp, :both] Tell to return only data, only http response, or both
312
- # @return [(HTTPResponse,Hash)] If ret is :both
313
- # @return [HTTPResponse] If ret is :resp
314
- # @return [Hash] If ret is :data
317
+ # @param body [Hash, String, nil] Body parameters
318
+ # @param headers [Hash{String => String}] Additional headers (override Content-Type)
319
+ # @param save_to_file [String, nil] File path to save response body
320
+ # @param exception [Boolean] Whether to raise an exception on HTTP error
321
+ # @param ret [Symbol] One of :data, :resp, :both controls return value
322
+ # @return [Array(Hash, Net::HTTPResponse)] When `ret` is :both
323
+ # @return [Net::HTTPResponse] When `ret` is :resp
324
+ # @return [Hash] When `ret` is :data
315
325
  # @raise [RestCallError] on error if `exception` is true
316
326
  def call(
317
327
  operation:,
@@ -373,15 +383,15 @@ module Aspera
373
383
  end
374
384
  case content_type
375
385
  when nil # ignore
376
- when MIME_JSON
386
+ when Mime::JSON
377
387
  req.body = JSON.generate(body) # , ascii_only: true
378
- req['Content-Type'] = MIME_JSON
379
- when MIME_WWW
388
+ req['Content-Type'] = Mime::JSON
389
+ when Mime::WWW
380
390
  req.body = URI.encode_www_form(body)
381
- req['Content-Type'] = MIME_WWW
382
- when MIME_TEXT
391
+ req['Content-Type'] = Mime::WWW
392
+ when Mime::TEXT
383
393
  req.body = body
384
- req['Content-Type'] = MIME_TEXT
394
+ req['Content-Type'] = Mime::TEXT
385
395
  else Aspera.error_unexpected_value(content_type){'body type'}
386
396
  end
387
397
  # set headers
@@ -401,12 +411,12 @@ module Aspera
401
411
  # make http request (pipelined)
402
412
  http_session.request(req) do |response|
403
413
  result_http = response
404
- result_mime = self.class.parse_header(result_http['Content-Type'] || MIME_TEXT)[:type]
414
+ result_mime = self.class.parse_header(result_http['Content-Type'] || Mime::TEXT)[:type]
405
415
  Log.log.debug{"response: code=#{result_http.code}, mime=#{result_mime}, mime2= #{response['Content-Type']}"}
406
416
  # JSON data needs to be parsed, in case it contains an error code
407
417
  if !save_to_file.nil? &&
408
418
  result_http.code.to_s.start_with?('2') &&
409
- !JSON_DECODE.include?(result_mime)
419
+ !Mime.json?(result_mime)
410
420
  total_size = result_http['Content-Length']&.to_i
411
421
  Log.log.debug('before write file')
412
422
  target_file = save_to_file
@@ -439,14 +449,14 @@ module Aspera
439
449
  end
440
450
  end
441
451
  Log.log.debug{"result: code=#{result_http.code} mime=#{result_mime}"}
442
- # sometimes there is a UTF8 char (e.g. (c) ), TODO : related to mime type encoding ?
452
+ # sometimes there is a UTF8 char (e.g. © )
453
+ # TODO : related to mime type encoding ?
443
454
  # result_http.body.force_encoding('UTF-8') if result_http.body.is_a?(String)
444
455
  # Log.log.debug{"result: body=#{result_http.body}"}
445
- case result_mime
446
- when *JSON_DECODE
456
+ if Mime.json?(result_mime)
447
457
  result_data = JSON.parse(result_http.body) rescue result_http.body
448
458
  Log.dump(:result_data, result_data)
449
- else # when MIME_TEXT
459
+ else # Mime::TEXT
450
460
  result_data = result_http.body
451
461
  end
452
462
  RestErrorAnalyzer.instance.raise_on_error(req, result_data, result_http)
@@ -526,27 +536,27 @@ module Aspera
526
536
 
527
537
  # Create: `POST`
528
538
  def create(subpath, params, **kwargs)
529
- return call(operation: 'POST', subpath: subpath, headers: {'Accept' => MIME_JSON}, body: params, content_type: MIME_JSON, **kwargs)
539
+ return call(operation: 'POST', subpath: subpath, headers: {'Accept' => Mime::JSON}, body: params, content_type: Mime::JSON, **kwargs)
530
540
  end
531
541
 
532
542
  # Read: `GET`
533
543
  def read(subpath, query = nil, **kwargs)
534
- return call(operation: 'GET', subpath: subpath, headers: {'Accept' => MIME_JSON}, query: query, **kwargs)
544
+ return call(operation: 'GET', subpath: subpath, headers: {'Accept' => Mime::JSON}, query: query, **kwargs)
535
545
  end
536
546
 
537
547
  # Update: `PUT`
538
548
  def update(subpath, params, **kwargs)
539
- return call(operation: 'PUT', subpath: subpath, headers: {'Accept' => MIME_JSON}, body: params, content_type: MIME_JSON, **kwargs)
549
+ return call(operation: 'PUT', subpath: subpath, headers: {'Accept' => Mime::JSON}, body: params, content_type: Mime::JSON, **kwargs)
540
550
  end
541
551
 
542
552
  # Delete: `DELETE`
543
553
  def delete(subpath, params = nil, **kwargs)
544
- return call(operation: 'DELETE', subpath: subpath, headers: {'Accept' => MIME_JSON}, query: params, **kwargs)
554
+ return call(operation: 'DELETE', subpath: subpath, headers: {'Accept' => Mime::JSON}, query: params, **kwargs)
545
555
  end
546
556
 
547
557
  # Cancel: `CANCEL`
548
558
  def cancel(subpath, **kwargs)
549
- return call(operation: 'CANCEL', subpath: subpath, headers: {'Accept' => MIME_JSON}, **kwargs)
559
+ return call(operation: 'CANCEL', subpath: subpath, headers: {'Accept' => Mime::JSON}, **kwargs)
550
560
  end
551
561
 
552
562
  # Query entity by general search (read with parameter `q`)
@@ -577,11 +587,8 @@ module Aspera
577
587
  end
578
588
  end
579
589
 
580
- # Content-Type that are JSON
581
- JSON_DECODE = [MIME_JSON, 'application/vnd.api+json', 'application/x-javascript'].freeze
582
-
583
590
  UNAVAILABLE_CODES = ['503']
584
591
 
585
- private_constant :JSON_DECODE, :UNAVAILABLE_CODES
592
+ private_constant :UNAVAILABLE_CODES
586
593
  end
587
594
  end
@@ -3,15 +3,23 @@
3
3
  module Aspera
4
4
  # raised on error after REST call
5
5
  class RestCallError < StandardError
6
- attr_reader :request, :response
6
+ def request
7
+ @context[:request]
8
+ end
9
+
10
+ def response
11
+ @context[:response]
12
+ end
13
+
14
+ def data
15
+ @context[:data]
16
+ end
7
17
 
8
- # @param req HTTP Request object
9
- # @param resp HTTP Response object
10
- # @param msg Error message
11
- def initialize(msg, req = nil, resp = nil)
12
- @request = req
13
- @response = resp
14
- super(msg)
18
+ # @param context [Hash,String] with keys :messages, :request, :response, :data
19
+ def initialize(context)
20
+ context = {messages: [context]} if context.is_a?(String)
21
+ @context = context
22
+ super(@context[:messages].join("\n"))
15
23
  end
16
24
  end
17
25
  end
@@ -3,6 +3,7 @@
3
3
  require 'aspera/rest_call_error'
4
4
  require 'aspera/log'
5
5
  require 'singleton'
6
+ require 'net/http'
6
7
 
7
8
  module Aspera
8
9
  # analyze error codes returned by REST calls and raise ruby exception
@@ -16,10 +17,10 @@ module Aspera
16
17
  # list of handlers
17
18
  @error_handlers = []
18
19
  @log_file = nil
19
- add_handler('Type Generic') do |type, call_context|
20
- if !call_context[:response].code.start_with?('2')
20
+ add_handler('Type Generic') do |type, context|
21
+ if !context[:response].code.start_with?('2')
21
22
  # add generic information
22
- RestErrorAnalyzer.add_error(call_context, type, "#{call_context[:request]['host']} #{call_context[:response].code} #{call_context[:response].message}")
23
+ RestErrorAnalyzer.add_error(context, type, "#{context[:request]['host']} #{context[:response].code} #{context[:response].message}")
23
24
  end
24
25
  end
25
26
  end
@@ -27,9 +28,12 @@ module Aspera
27
28
  # Use this method to analyze a EST result and raise an exception
28
29
  # Analyzes REST call response and raises a RestCallError exception
29
30
  # if HTTP result code is not 2XX
31
+ # @param req [Net::HTTPRequest]
32
+ # @param data [Object]
33
+ # @param http [Net::HTTPResponse]
30
34
  def raise_on_error(req, data, http)
31
35
  Log.log.debug{"raise_on_error #{req.method} #{req.path} #{http.code}"}
32
- call_context = {
36
+ context = {
33
37
  messages: [],
34
38
  request: req,
35
39
  response: http,
@@ -39,44 +43,42 @@ module Aspera
39
43
  # analyze errors from provided handlers
40
44
  # note that there can be an error even if code is 2XX
41
45
  @error_handlers.each do |handler|
42
- begin # rubocop:disable Style/RedundantBegin
43
- # Log.log.debug{"test exception: #{handler[:name]}"}
44
- handler[:block].call(handler[:name], call_context)
45
- rescue StandardError => e
46
- Log.log.error{"ERROR in handler:\n#{e.message}\n#{e.backtrace}"}
47
- end
46
+ handler[:block].call(handler[:name], context)
47
+ rescue StandardError => e
48
+ Log.log.error{"ERROR in handler:\n#{e.message}\n#{e.backtrace}"}
48
49
  end
49
- raise RestCallError.new(call_context[:messages].join("\n"), call_context[:request], call_context[:response]) unless call_context[:messages].empty?
50
+ raise RestCallError, context unless context[:messages].empty?
50
51
  end
51
52
 
52
- # add a new error handler (done at application initialization)
53
- # @param name : name of error handler (for logs)
54
- # @param block : processing of response: takes two parameters: name, call_context
53
+ # Add a new error handler (done at application initialization)
54
+ # @param name [String] name of error handler (for logs)
55
+ # @param block [Proc] processing of response: takes two parameters: `name`, `context`
55
56
  # name is the one provided here
56
- # call_context is built in method raise_on_error
57
+ # context is built in method raise_on_error
57
58
  def add_handler(name, &block)
58
59
  @error_handlers.unshift({name: name, block: block})
60
+ nil
59
61
  end
60
62
 
61
- # add a simple error handler
62
- # check that key exists and is string under specified path (hash)
63
- # adds other keys as secondary information
64
- # @param name [String] name of error handler (for logs)
65
- # @param always [boolean] if true, always add error message, even if response code is 2XX
66
- # @param path [Array] path to error message in response
63
+ # Add a simple error handler
64
+ # Check that key exists and is string under specified path (hash)
65
+ # Adds other keys as secondary information
66
+ # @param name [String] name of error handler (for logs)
67
+ # @param always [Boolean] if true, always add error message, even if response code is 2XX
68
+ # @param path [Array] path to error message in response
67
69
  def add_simple_handler(name:, always: false, path:)
68
70
  path.freeze
69
- add_handler(name) do |type, call_context|
70
- if call_context[:data].is_a?(Hash) && (!call_context[:response].code.start_with?('2') || always)
71
+ add_handler(name) do |type, context|
72
+ if context[:data].is_a?(Hash) && (!context[:response].code.start_with?('2') || always)
71
73
  # Log.log.debug{"simple_handler: #{type} #{path} #{path.last}"}
72
74
  # dig and find hash containing error message
73
- error_struct = path.length.eql?(1) ? call_context[:data] : call_context[:data].dig(*path[0..-2])
75
+ error_struct = path.length.eql?(1) ? context[:data] : context[:data].dig(*path[0..-2])
74
76
  # Log.log.debug{"found: #{error_struct.class} #{error_struct}"}
75
77
  if error_struct.is_a?(Hash) && error_struct[path.last].is_a?(String)
76
- RestErrorAnalyzer.add_error(call_context, type, error_struct[path.last])
78
+ RestErrorAnalyzer.add_error(context, type, error_struct[path.last])
77
79
  error_struct.each do |k, v|
78
80
  next if k.eql?(path.last)
79
- RestErrorAnalyzer.add_error(call_context, "#{type}(sub)", "#{k}: #{v}") if [String, Integer].include?(v.class)
81
+ RestErrorAnalyzer.add_error(context, "#{type}(sub)", "#{k}: #{v}") if [String, Integer].include?(v.class)
80
82
  end
81
83
  end
82
84
  end
@@ -84,20 +86,20 @@ module Aspera
84
86
  end
85
87
 
86
88
  class << self
87
- # used by handler to add an error description to list of errors
88
- # for logging and tracing : collect error descriptions (create file to activate)
89
- # @param call_context a Hash containing the result call_context, provided to handler
90
- # @param type a string describing type of exception, for logging purpose
91
- # @param msg one error message to add to list
92
- def add_error(call_context, type, msg)
93
- call_context[:messages].push(msg)
94
- Log.log.trace1{"Found error: #{type}: #{msg}"}
89
+ # Used by handler to add an error description to list of errors
90
+ # For logging and tracing : collect error descriptions (create file to activate)
91
+ # @param context [Hash] the result context, provided to handler
92
+ # @param type [String] type of exception, for logging purpose
93
+ # @param message [String] one error message to add to list
94
+ def add_error(context, type, message)
95
+ context[:messages].push(message)
96
+ Log.log.trace1{"Found error: #{type}: #{message}"}
95
97
  log_file = instance.log_file
96
98
  # log error for further analysis (file must exist to activate)
97
99
  return if log_file.nil? || !File.exist?(log_file)
98
100
  File.open(log_file, 'a+') do |f|
99
- f.write("\n=#{type}=====\n#{call_context[:request].method} #{call_context[:request].path}\n#{call_context[:response].code}\n" \
100
- "#{JSON.generate(call_context[:data])}\n#{call_context[:messages].join("\n")}")
101
+ f.write("\n=#{type}=====\n#{context[:request].method} #{context[:request].path}\n#{context[:response].code}\n" \
102
+ "#{JSON.generate(context[:data])}\n#{context[:messages].join("\n")}")
101
103
  end
102
104
  end
103
105
  end
@@ -17,48 +17,49 @@ module Aspera
17
17
  RestErrorAnalyzer.instance.add_simple_handler(name: 'Type 3: error:internal_message', path: %w[error internal_message])
18
18
  RestErrorAnalyzer.instance.add_simple_handler(name: 'Type 5', path: ['error_description'])
19
19
  RestErrorAnalyzer.instance.add_simple_handler(name: 'Type 6', path: ['message'])
20
+ RestErrorAnalyzer.instance.add_simple_handler(name: 'Type 6', path: ['failure'], always: true)
20
21
  # AoC Automation
21
22
  RestErrorAnalyzer.instance.add_simple_handler(name: 'AoC Automation', path: ['error'])
22
- RestErrorAnalyzer.instance.add_handler('Type 7: errors[]') do |type, call_context|
23
- next unless call_context[:data].is_a?(Hash) && call_context[:data]['errors'].is_a?(Hash)
23
+ RestErrorAnalyzer.instance.add_handler('Type 7: errors[]') do |type, context|
24
+ next unless context[:data].is_a?(Hash) && context[:data]['errors'].is_a?(Hash)
24
25
  # special for Shares: false positive ? (update global transfer_settings)
25
- next if call_context[:data].key?('min_connect_version')
26
- call_context[:data]['errors'].each do |k, v|
27
- RestErrorAnalyzer.add_error(call_context, type, "#{k}: #{v}")
26
+ next if context[:data].key?('min_connect_version')
27
+ context[:data]['errors'].each do |k, v|
28
+ RestErrorAnalyzer.add_error(context, type, "#{k}: #{v}")
28
29
  end
29
30
  end
30
31
  # call to upload_setup and download_setup of node api
31
- RestErrorAnalyzer.instance.add_handler('T8:node: *_setup') do |type, call_context|
32
- next unless call_context[:data].is_a?(Hash)
33
- d_t_s = call_context[:data]['transfer_specs']
32
+ RestErrorAnalyzer.instance.add_handler('T8:node: *_setup') do |type, context|
33
+ next unless context[:data].is_a?(Hash)
34
+ d_t_s = context[:data]['transfer_specs']
34
35
  next unless d_t_s.is_a?(Array)
35
36
  d_t_s.each do |res|
36
37
  r_err = res.dig(*%w[transfer_spec error]) || res['error']
37
38
  next unless r_err.is_a?(Hash)
38
- RestErrorAnalyzer.add_error(call_context, type, r_err.values.join(': '))
39
+ RestErrorAnalyzer.add_error(context, type, r_err.values.join(': '))
39
40
  end
40
41
  end
41
42
  RestErrorAnalyzer.instance.add_simple_handler(name: 'T9:IBM cloud IAM', path: ['errorMessage'])
42
43
  RestErrorAnalyzer.instance.add_simple_handler(name: 'T10:faspex v4', path: ['user_message'])
43
- RestErrorAnalyzer.instance.add_handler('bss graphql') do |type, call_context|
44
- next unless call_context[:data].is_a?(Hash)
45
- d_t_s = call_context[:data]['errors']
44
+ RestErrorAnalyzer.instance.add_handler('bss graphql') do |type, context|
45
+ next unless context[:data].is_a?(Hash)
46
+ d_t_s = context[:data]['errors']
46
47
  next unless d_t_s.is_a?(Array)
47
48
  d_t_s.each do |res|
48
49
  r_err = res['message']
49
50
  next unless r_err.is_a?(String)
50
- RestErrorAnalyzer.add_error(call_context, type, r_err)
51
+ RestErrorAnalyzer.add_error(context, type, r_err)
51
52
  end
52
53
  end
53
- RestErrorAnalyzer.instance.add_handler('Orchestrator') do |type, call_context|
54
- next if call_context[:response].code.start_with?('2')
55
- data = call_context[:data]
54
+ RestErrorAnalyzer.instance.add_handler('Orchestrator') do |type, context|
55
+ next if context[:response].code.start_with?('2')
56
+ data = context[:data]
56
57
  next unless data.is_a?(Hash)
57
58
  work_order = data['work_order']
58
59
  next unless work_order.is_a?(Hash)
59
- RestErrorAnalyzer.add_error(call_context, type, work_order['statusDetails'])
60
+ RestErrorAnalyzer.add_error(context, type, work_order['statusDetails'])
60
61
  data['missing_parameters']&.each do |param|
61
- RestErrorAnalyzer.add_error(call_context, type, "missing parameter: #{param}")
62
+ RestErrorAnalyzer.add_error(context, type, "missing parameter: #{param}")
62
63
  end
63
64
  end
64
65
  end
@@ -41,14 +41,14 @@ module Aspera
41
41
  Log.log.debug{"retries=#{remaining_resumes}"}
42
42
  # try to send the file until ascp is successful
43
43
  loop do
44
- Log.log.debug('Transfer session starting')
44
+ Log.log.debug('Starting task execution')
45
45
  begin
46
46
  # Call provided block: execute transfer
47
47
  yield
48
48
  # Exit retry loop if success
49
49
  break
50
50
  rescue Error => e
51
- Log.log.warn{"A transfer error occurred during transfer: #{e.message}"}
51
+ Log.log.warn{"An error occurred during task: #{e.message}"}
52
52
  Log.log.debug{"Retryable ? #{e.retryable?}"}
53
53
  # do not retry non-retryable
54
54
  raise unless e.retryable?
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Aspera
6
+ module Yaml
7
+ # @param node [Psych::Nodes::Node] YAML node
8
+ # @param parent_path [Array<String>] Path of parent keys
9
+ # @param duplicate_keys [Array<Hash>] Accumulated duplicate keys
10
+ # @return [Array<String>] List of duplicate keys with their paths and occurrences
11
+ def find_duplicate_keys(node, parent_path = nil, duplicate_keys = nil)
12
+ duplicate_keys ||= []
13
+ parent_path ||= []
14
+ return duplicate_keys unless node.respond_to?(:children)
15
+ if node.is_a?(Psych::Nodes::Mapping)
16
+ counts = Hash.new(0)
17
+ key_nodes = Hash.new{ |h, k| h[k] = []}
18
+ node.children.each_slice(2) do |key_node, value_node|
19
+ if key_node&.value
20
+ counts[key_node.value] += 1
21
+ key_nodes[key_node.value] << key_node
22
+ find_duplicate_keys(value_node, parent_path + [key_node.value], duplicate_keys)
23
+ end
24
+ end
25
+ counts.each do |key_str, count|
26
+ next if count <= 1
27
+ path = (parent_path + [key_str]).join('.')
28
+ occurrences = key_nodes[key_str].map{ |kn| kn.start_line ? kn.start_line + 1 : 'unknown'}.map(&:to_s).join(', ')
29
+ duplicate_keys << "#{path}: #{occurrences}"
30
+ end
31
+ else
32
+ node.children.to_a.each{ |child| find_duplicate_keys(child, parent_path, duplicate_keys)}
33
+ end
34
+ duplicate_keys
35
+ end
36
+
37
+ # Safely load YAML content, raising an error if duplicate keys are found
38
+ # @param yaml [String] YAML content
39
+ # @return [Object] Parsed YAML content
40
+ # @raise [RuntimeError] If duplicate keys are found
41
+ def safe_load(yaml)
42
+ duplicate_keys = find_duplicate_keys(Psych.parse_stream(yaml))
43
+ raise "Duplicate keys: #{duplicate_keys.join('; ')}" unless duplicate_keys.empty?
44
+ YAML.safe_load(yaml)
45
+ end
46
+
47
+ module_function :find_duplicate_keys, :safe_load
48
+ end
49
+ end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aspera-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.25.1
4
+ version: 4.25.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Laurent Martin
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
110
  version: '3.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: marcel
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.1'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.1'
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: mime-types
113
127
  requirement: !ruby/object:Gem::Requirement
@@ -430,9 +444,9 @@ files:
430
444
  - lib/aspera/uri_reader.rb
431
445
  - lib/aspera/web_auth.rb
432
446
  - lib/aspera/web_server_simple.rb
447
+ - lib/aspera/yaml.rb
433
448
  - lib/transferd_pb.rb
434
449
  - lib/transferd_services_pb.rb
435
- - release_notes.md
436
450
  homepage: https://github.com/IBM/aspera-cli
437
451
  licenses:
438
452
  - Apache-2.0
@@ -440,7 +454,7 @@ metadata:
440
454
  allowed_push_host: https://rubygems.org
441
455
  homepage_uri: https://github.com/IBM/aspera-cli
442
456
  source_code_uri: https://github.com/IBM/aspera-cli/tree/main/lib/aspera
443
- changelog_uri: https://github.com/IBM/aspera-cli/CHANGELOG.md
457
+ changelog_uri: https://github.com/IBM/aspera-cli/blob/main/CHANGELOG.md
444
458
  rubygems_uri: https://rubygems.org/gems/aspera-cli
445
459
  documentation_uri: https://www.rubydoc.info/gems/aspera-cli
446
460
  rdoc_options: []
metadata.gz.sig CHANGED
Binary file
data/release_notes.md DELETED
@@ -1,8 +0,0 @@
1
- ## 4.25.1
2
-
3
- Released: 2026-01-21
4
-
5
- ### Issues Fixed
6
-
7
- * `build`: Fixed deploy workflow to use global rake instead of bundle exec.
8
- * `build`: Made rspec require conditional in test.rake for deploy environment.