aspera-cli 4.27.0 → 4.27.2

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 (142) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +46 -1
  4. data/CONTRIBUTING.md +1 -7
  5. data/TODO.md +50 -0
  6. data/docs/README.md +709 -489
  7. data/lib/aspera/agent/base.rb +10 -2
  8. data/lib/aspera/agent/connect.rb +7 -7
  9. data/lib/aspera/agent/desktop.rb +3 -3
  10. data/lib/aspera/agent/direct.rb +48 -19
  11. data/lib/aspera/agent/factory.rb +3 -3
  12. data/lib/aspera/agent/httpgw.rb +55 -14
  13. data/lib/aspera/agent/node.rb +2 -2
  14. data/lib/aspera/agent/transferd.rb +12 -12
  15. data/lib/aspera/api/aoc.rb +28 -28
  16. data/lib/aspera/api/cos_node.rb +10 -9
  17. data/lib/aspera/api/faspex.rb +2 -2
  18. data/lib/aspera/api/httpgw.rb +20 -20
  19. data/lib/aspera/api/node.rb +31 -53
  20. data/lib/aspera/ascmd.rb +13 -13
  21. data/lib/aspera/ascp/installation.rb +43 -36
  22. data/lib/aspera/ascp/management.rb +6 -6
  23. data/lib/aspera/assert.rb +12 -13
  24. data/lib/aspera/cli/ascp_actions.rb +7 -7
  25. data/lib/aspera/cli/async_transfer_store.rb +49 -5
  26. data/lib/aspera/cli/bootstrapper.rb +9 -9
  27. data/lib/aspera/cli/command_registry.rb +1 -1
  28. data/lib/aspera/cli/command_spec.rb +6 -3
  29. data/lib/aspera/cli/context.rb +3 -3
  30. data/lib/aspera/cli/extended_value.rb +26 -26
  31. data/lib/aspera/cli/formatter.rb +76 -54
  32. data/lib/aspera/cli/gem_checker.rb +1 -1
  33. data/lib/aspera/cli/hints.rb +2 -2
  34. data/lib/aspera/cli/http.rb +22 -29
  35. data/lib/aspera/cli/mailer.rb +7 -7
  36. data/lib/aspera/cli/mcp_tool.rb +31 -4
  37. data/lib/aspera/cli/option_declarator.rb +77 -0
  38. data/lib/aspera/cli/options.schema.yaml +273 -3
  39. data/lib/aspera/cli/parser.rb +539 -280
  40. data/lib/aspera/cli/plugins/alee.rb +1 -1
  41. data/lib/aspera/cli/plugins/aoc.rb +303 -314
  42. data/lib/aspera/cli/plugins/ats.rb +25 -21
  43. data/lib/aspera/cli/plugins/base.rb +357 -186
  44. data/lib/aspera/cli/plugins/basic_auth.rb +0 -12
  45. data/lib/aspera/cli/plugins/config.rb +76 -73
  46. data/lib/aspera/cli/plugins/console.rb +8 -8
  47. data/lib/aspera/cli/plugins/cos.rb +4 -3
  48. data/lib/aspera/cli/plugins/factory.rb +2 -2
  49. data/lib/aspera/cli/plugins/faspex5.rb +259 -248
  50. data/lib/aspera/cli/plugins/faspio.rb +7 -2
  51. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  52. data/lib/aspera/cli/plugins/mcp.rb +11 -11
  53. data/lib/aspera/cli/plugins/node.rb +227 -283
  54. data/lib/aspera/cli/plugins/orchestrator.rb +23 -23
  55. data/lib/aspera/cli/plugins/preview.rb +61 -64
  56. data/lib/aspera/cli/plugins/server.rb +36 -31
  57. data/lib/aspera/cli/plugins/shares.rb +174 -135
  58. data/lib/aspera/cli/preset_actions.rb +39 -19
  59. data/lib/aspera/cli/preset_manager.rb +74 -32
  60. data/lib/aspera/cli/result.rb +18 -16
  61. data/lib/aspera/cli/runner.rb +75 -39
  62. data/lib/aspera/cli/secret_finder.rb +1 -1
  63. data/lib/aspera/cli/sync_actions.rb +36 -35
  64. data/lib/aspera/cli/terminal_formatter.rb +2 -2
  65. data/lib/aspera/cli/transfer_actions.rb +17 -10
  66. data/lib/aspera/cli/transfer_agent.rb +44 -44
  67. data/lib/aspera/cli/transfer_progress.rb +7 -7
  68. data/lib/aspera/cli/vault_manager.rb +34 -12
  69. data/lib/aspera/cli/version.rb +1 -1
  70. data/lib/aspera/cli/wizard.rb +17 -20
  71. data/lib/aspera/colors.rb +3 -3
  72. data/lib/aspera/command_line_builder.rb +17 -16
  73. data/lib/aspera/command_line_converter.rb +1 -1
  74. data/lib/aspera/coverage.rb +1 -2
  75. data/lib/aspera/data_repository.rb +1 -1
  76. data/lib/aspera/dot_container.rb +8 -8
  77. data/lib/aspera/environment.rb +13 -13
  78. data/lib/aspera/faspex_gw.rb +4 -4
  79. data/lib/aspera/faspex_postproc.rb +14 -9
  80. data/lib/aspera/graphql.rb +4 -2
  81. data/lib/aspera/hash_ext.rb +2 -2
  82. data/lib/aspera/json_rpc/client.rb +1 -1
  83. data/lib/aspera/keychain/base.rb +10 -3
  84. data/lib/aspera/keychain/encrypted_hash.rb +92 -17
  85. data/lib/aspera/keychain/factory.rb +17 -5
  86. data/lib/aspera/keychain/hashicorp_vault.rb +1 -1
  87. data/lib/aspera/keychain/macos_security.rb +23 -25
  88. data/lib/aspera/keychain/one_password_api.rb +86 -0
  89. data/lib/aspera/keychain/one_password_base.rb +34 -0
  90. data/lib/aspera/keychain/one_password_cli.rb +98 -0
  91. data/lib/aspera/link_header.rb +2 -2
  92. data/lib/aspera/log.rb +13 -13
  93. data/lib/aspera/markdown.rb +5 -5
  94. data/lib/aspera/nagios.rb +8 -8
  95. data/lib/aspera/node_simulator.rb +33 -27
  96. data/lib/aspera/oauth/base.rb +4 -5
  97. data/lib/aspera/oauth/boot.rb +3 -3
  98. data/lib/aspera/oauth/factory.rb +5 -5
  99. data/lib/aspera/oauth/jwt.rb +5 -7
  100. data/lib/aspera/oauth/web.rb +4 -4
  101. data/lib/aspera/persistency_action_once.rb +4 -4
  102. data/lib/aspera/persistency_folder.rb +8 -8
  103. data/lib/aspera/preview/file_types.rb +4 -4
  104. data/lib/aspera/preview/generator.rb +49 -46
  105. data/lib/aspera/preview/options.rb +3 -2
  106. data/lib/aspera/preview/terminal.rb +5 -5
  107. data/lib/aspera/preview/utils.rb +65 -18
  108. data/lib/aspera/products/connect.rb +2 -2
  109. data/lib/aspera/products/desktop.rb +1 -1
  110. data/lib/aspera/products/other.rb +2 -2
  111. data/lib/aspera/products/transferd.rb +3 -3
  112. data/lib/aspera/proxy_auto_config.rb +10 -10
  113. data/lib/aspera/rest.rb +149 -115
  114. data/lib/aspera/rest_error_analyzer.rb +3 -3
  115. data/lib/aspera/rest_list.rb +10 -9
  116. data/lib/aspera/schema/IBM Aspera Node API-4.4.6.yaml +6232 -0
  117. data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +249 -0
  118. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +39 -0
  119. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
  120. data/lib/aspera/schema/documentation.rb +17 -11
  121. data/lib/aspera/schema/reader.rb +78 -4
  122. data/lib/aspera/schema/registry.rb +26 -7
  123. data/lib/aspera/secret_hider.rb +5 -5
  124. data/lib/aspera/ssh.rb +64 -31
  125. data/lib/aspera/ssl.rb +3 -3
  126. data/lib/aspera/sync/database.rb +2 -2
  127. data/lib/aspera/sync/operations.rb +13 -13
  128. data/lib/aspera/temp_file_manager.rb +24 -4
  129. data/lib/aspera/transfer/faux_file.rb +2 -2
  130. data/lib/aspera/transfer/parameters.rb +15 -15
  131. data/lib/aspera/transfer/resumer.rb +9 -9
  132. data/lib/aspera/transfer/spec.rb +1 -1
  133. data/lib/aspera/transfer/spec.schema.yaml +1 -0
  134. data/lib/aspera/uri_reader.rb +52 -15
  135. data/lib/aspera/web_auth.rb +6 -5
  136. data/lib/aspera/web_server_simple.rb +9 -9
  137. data/lib/aspera/yaml.rb +4 -4
  138. data.tar.gz.sig +0 -0
  139. metadata +10 -4
  140. metadata.gz.sig +0 -0
  141. data/lib/aspera/cli/plugins/faspex.rb +0 -576
  142. data/lib/aspera/transfer/uri.rb +0 -56
data/lib/aspera/rest.rb CHANGED
@@ -87,7 +87,7 @@ module Aspera
87
87
  # @param query [Hash] A key can have Array value and result will use PHP format: a[]=1&a[]=2
88
88
  # @return [Hash] The query parameters.
89
89
  def php_style(query)
90
- Aspera.assert_type(query, Hash){'query'}
90
+ Aspera.assert_type(query, Hash) { 'query' }
91
91
  query[:x_array_php_style] = true
92
92
  query
93
93
  end
@@ -100,7 +100,7 @@ module Aspera
100
100
  # @return [URI] The built URI.
101
101
  def build_uri(url, query)
102
102
  uri = URI.parse(url)
103
- Aspera.assert_values(uri.scheme, %w[http https]){'URI scheme'}
103
+ Aspera.assert_values(uri.scheme, %w[http https]) { 'URI scheme' }
104
104
  return uri if query.nil? || query.respond_to?(:empty?) && query.empty?
105
105
  Log.dump(:query, query)
106
106
  uri.query =
@@ -110,9 +110,9 @@ module Aspera
110
110
  when Hash
111
111
  URI.encode_www_form(h_to_query_array(query))
112
112
  when Array
113
- Aspera.assert(query.all?{ |i| i.is_a?(Array) && i.length.eql?(2)}, 'Query must be array of arrays of 2 elements')
113
+ Aspera.assert(query.all? { |i| i.is_a?(Array) && i.length.eql?(2) }, 'Query must be array of arrays of 2 elements')
114
114
  URI.encode_www_form(query) # remove nil values
115
- else Aspera.error_unexpected_value(query.class){'query type'}
115
+ else Aspera.error_unexpected_value(query.class) { 'query type' }
116
116
  end.gsub('%5B%5D=', '[]=')
117
117
  # [] is allowed in url parameters
118
118
  uri
@@ -165,7 +165,7 @@ module Aspera
165
165
  # @return [Net::HTTP] A started HTTP session
166
166
  def start_http_session(base_url)
167
167
  uri = URI.parse(base_url)
168
- Aspera.assert_values(uri.scheme, %w[http https]){'URI scheme'}
168
+ Aspera.assert_values(uri.scheme, %w[http https]) { 'URI scheme' }
169
169
  # This honors http_proxy env var
170
170
  http_session = Net::HTTP.new(uri.host, uri.port)
171
171
  http_session.use_ssl = uri.scheme.eql?('https')
@@ -185,7 +185,7 @@ module Aspera
185
185
  Aspera.assert_type(http_session, Net::HTTP)
186
186
  # Net::BufferedIO in net/protocol.rb
187
187
  result = http_session.instance_variable_get(:@socket)
188
- Aspera.assert(!result.nil?){"no socket for #{http_session}"}
188
+ Aspera.assert(!result.nil?) { "no socket for #{http_session}" }
189
189
  return result
190
190
  end
191
191
 
@@ -290,7 +290,7 @@ module Aspera
290
290
  # remove trailing port if it is 443 and scheme is https
291
291
  @base_url = @base_url.gsub(/:443$/, '') if @base_url.start_with?('https://')
292
292
  @base_url = @base_url.gsub(/:80$/, '') if @base_url.start_with?('http://')
293
- Log.log.debug{"Rest.new(#{@base_url})"}
293
+ Log.log.debug { "Rest.new(#{@base_url})" }
294
294
  # default is no auth
295
295
  @auth_params = auth
296
296
  Aspera.assert_type(@auth_params, Hash)
@@ -312,7 +312,7 @@ module Aspera
312
312
  def oauth
313
313
  if @oauth.nil?
314
314
  Aspera.assert(@auth_params[:type].eql?(:oauth2), 'no OAuth defined')
315
- oauth_parameters = @auth_params.reject{ |k, _v| k.eql?(:type)}
315
+ oauth_parameters = @auth_params.reject { |k, _v| k.eql?(:type) }
316
316
  Log.dump(:oauth_parameters, oauth_parameters)
317
317
  @oauth = OAuth::Factory.instance.create(**oauth_parameters)
318
318
  end
@@ -326,7 +326,7 @@ module Aspera
326
326
  # @param content_type [String, nil] Type of body parameters (one of MIME_*) and serialization, else use headers
327
327
  # @param body [Hash, String, nil] Body parameters
328
328
  # @param headers [Hash{String => String}] Additional headers (override Content-Type)
329
- # @param save_to_file [String, nil] File path to save response body
329
+ # @param save_to [String, IO, nil] File path or IO object to save response body; progress bar is used when set
330
330
  # @param exception [Boolean] Whether to raise an exception on HTTP error
331
331
  # @param ret [Symbol] One of :data, :resp, :both - controls return value
332
332
  # @return [Array(Hash, Net::HTTPResponse)] When `ret` is :both
@@ -340,42 +340,20 @@ module Aspera
340
340
  content_type: nil,
341
341
  body: nil,
342
342
  headers: nil,
343
- save_to_file: nil,
343
+ save_to: nil,
344
344
  exception: true,
345
345
  ret: :data
346
346
  )
347
347
  subpath = subpath.to_s if subpath.is_a?(Symbol)
348
348
  subpath = '' if subpath.nil?
349
- Log.log.debug{"call #{operation} [#{subpath}]".red.bold.bg_green}
349
+ Log.log.debug { "call #{operation} [#{subpath}]".red.bold.bg_green }
350
350
  Log.dump(:body, body, level: :trace1)
351
351
  Log.dump(:query, query, level: :trace1)
352
352
  Log.dump(:headers, headers, level: :trace1)
353
353
  Aspera.assert_type(subpath, String)
354
354
  # We must have a way to check return code
355
355
  Aspera.assert(exception || !ret.eql?(:data), 'ret: :data requires exception handler')
356
- if headers.nil?
357
- headers = @headers.clone
358
- else
359
- h = headers
360
- headers = @headers.clone
361
- headers.merge!(h)
362
- end
363
- Aspera.assert_type(headers, Hash)
364
- case @auth_params[:type]
365
- when :none
366
- # no auth
367
- when :basic
368
- Log.log.debug('using Basic auth')
369
- # done in build_req
370
- when :oauth2
371
- headers['Authorization'] = oauth.authorization unless headers.key?('Authorization')
372
- when :url
373
- query ||= {}
374
- @auth_params[:url_query].each do |key, value|
375
- query[key] = value
376
- end
377
- else Aspera.error_unexpected_value(@auth_params[:type])
378
- end
356
+ headers, query = prepare_call(headers, query)
379
357
  result_http = nil
380
358
  result_data = nil
381
359
  # initialize with number of initial retries allowed, nil gives zero
@@ -383,37 +361,7 @@ module Aspera
383
361
  # start a block to be able to retry the actual HTTP request in case of OAuth token expiration
384
362
  begin
385
363
  Log.log.debug("send request (retries=#{tries_remain_redirect})")
386
- # TODO: shall we percent encode subpath (spaces) test with access key delete with space in id
387
- # URI.escape()
388
- separator = ['', '/'].include?(subpath) ? '' : '/'
389
- uri = self.class.build_uri("#{@base_url}#{separator}#{subpath}", query)
390
- Log.log.debug{"URI=#{uri}"}
391
- begin
392
- # instantiate request object based on string name
393
- req = Net::HTTP.const_get(operation.capitalize).new(uri)
394
- rescue NameError
395
- raise "unsupported operation : #{operation}"
396
- end
397
- case content_type
398
- when nil # ignore
399
- when Mime::JSON
400
- req.body = JSON.generate(body) # , ascii_only: true
401
- req['Content-Type'] = Mime::JSON
402
- when Mime::WWW
403
- req.body = URI.encode_www_form(body)
404
- req['Content-Type'] = Mime::WWW
405
- when Mime::TEXT
406
- req.body = body
407
- req['Content-Type'] = Mime::TEXT
408
- else Aspera.error_unexpected_value(content_type){'body type'}
409
- end
410
- # set headers
411
- headers.each do |key, value|
412
- req[key] = value
413
- end
414
- # :type = :basic
415
- req.basic_auth(@auth_params[:username], @auth_params[:password]) if @auth_params[:type].eql?(:basic)
416
- Log.dump(:req_body, req.body, level: :trace1)
364
+ req = build_request(operation, subpath, query, content_type, body, headers)
417
365
  # we try the call, and will retry on some error types
418
366
  error_tries ||= 1 + RestParameters.instance.retry_max
419
367
  result_mime = nil
@@ -422,58 +370,21 @@ module Aspera
422
370
  http_session.request(req) do |response|
423
371
  result_http = response
424
372
  result_mime = self.class.parse_header(result_http['Content-Type'] || Mime::TEXT)[:type]
425
- Log.log.debug{"response: code=#{result_http.code}, mime=#{result_mime}, content-type=#{response['Content-Type']}"}
373
+ Log.log.debug { "response: code=#{result_http.code}, mime=#{result_mime}, content-type=#{response['Content-Type']}" }
426
374
  # JSON data needs to be parsed, in case it contains an error code
427
- if !save_to_file.nil? &&
428
- result_http.code.to_s.start_with?('2') &&
429
- !Mime.json?(result_mime)
430
- total_size = result_http['Content-Length']&.to_i
431
- Log.log.debug('before write file')
432
- target_file = save_to_file
433
- # override user's path to path in header
434
- if !response['Content-Disposition'].nil?
435
- disposition = self.class.parse_header(response['Content-Disposition'])
436
- target_file = File.join(File.dirname(target_file), disposition[:parameters][:filename]) if disposition[:parameters].key?(:filename) && !disposition[:parameters][:filename].eql?('.')
437
- end
438
- # download with temp filename
439
- target_file_tmp = "#{target_file}#{RestParameters.instance.download_partial_suffix}"
440
- Log.log.debug{"saving to: #{target_file}"}
441
- written_size = 0
442
- session_id = SecureRandom.uuid.freeze
443
- RestParameters.instance.progress_bar&.event(:session_start, session_id: session_id)
444
- RestParameters.instance.progress_bar&.event(:session_size, session_id: session_id, info: total_size) if total_size
445
- FileUtils.mkdir_p(File.dirname(target_file_tmp))
446
- limiter = TimerLimiter.new(0.5)
447
- File.open(target_file_tmp, 'wb') do |file|
448
- result_http.read_body do |fragment|
449
- file.write(fragment)
450
- written_size += fragment.length
451
- RestParameters.instance.progress_bar&.event(:transfer, session_id: session_id, info: written_size) if limiter.trigger?
452
- end
453
- end
454
- RestParameters.instance.progress_bar&.event(:session_end, session_id: session_id)
455
- RestParameters.instance.progress_bar&.event(:end)
456
- # rename at the end
457
- File.rename(target_file_tmp, target_file)
458
- file_saved = true
459
- end
375
+ file_saved = save_response(response, result_http, result_mime, save_to)
460
376
  end
461
- Log.log.debug{"result: code=#{result_http.code} mime=#{result_mime}"}
377
+ Log.log.debug { "result: code=#{result_http.code} mime=#{result_mime}" }
462
378
  # sometimes there is a UTF8 char (e.g. (c) )
463
379
  # TODO : related to mime type encoding ?
464
380
  # result_http.body.force_encoding('UTF-8') if result_http.body.is_a?(String)
465
381
  # Log.log.debug{"result: body=#{result_http.body}"}
466
- result_data = result_http.body
467
- Log.dump(:result_data_raw, result_data, level: :trace1)
468
- # TODO: Remove next 2 lines when bug in async node api is fixed. (Aspera/core/issues/4490)
469
- node_api_bug = result_data&.index('}HTTP/1.1 400 Bad Request') if result_data.is_a?(String)
470
- result_data = result_data[0..node_api_bug] if node_api_bug
471
- result_data = JSON.parse(result_data) if Mime.json?(result_mime) && !result_data.nil? && !result_data.empty?
472
- Log.dump(:result_data, result_data)
382
+ result_data = parse_response(result_http, result_mime)
473
383
  RestErrorAnalyzer.instance.raise_on_error(req, result_data, result_http)
474
- unless file_saved || save_to_file.nil?
475
- FileUtils.mkdir_p(File.dirname(save_to_file))
476
- File.write(save_to_file, result_http.body, binmode: true)
384
+ unless file_saved || save_to.nil?
385
+ raise 'save_to: IO object requires a streaming response' if save_to.respond_to?(:write)
386
+ FileUtils.mkdir_p(File.dirname(save_to))
387
+ File.write(save_to, result_http.body, binmode: true)
477
388
  end
478
389
  rescue RestCallError => e
479
390
  do_retry = false
@@ -494,7 +405,7 @@ module Aspera
494
405
  # regenerate a brand new token
495
406
  req['Authorization'] = oauth.authorization(cache: false)
496
407
  end
497
- Log.log.debug{"using new token=#{headers['Authorization']}"}
408
+ Log.log.debug('using new token')
498
409
  do_retry ||= true
499
410
  end
500
411
  if do_retry && (error_tries -= 1).positive?
@@ -509,7 +420,7 @@ module Aspera
509
420
  # special case: relative redirect
510
421
  if URI.parse(new_url).host.nil?
511
422
  # we don't manage relative redirects with non-absolute path
512
- Aspera.assert(new_url.start_with?('/')){"redirect location is relative: #{new_url}, but does not start with /."}
423
+ Aspera.assert(new_url.start_with?('/')) { "redirect location is relative: #{new_url}, but does not start with /." }
513
424
  new_url = "#{current_uri.scheme}://#{current_uri.host}#{new_url}"
514
425
  end
515
426
  # forwards the request to the new location
@@ -522,7 +433,7 @@ module Aspera
522
433
  query: query,
523
434
  body: body,
524
435
  content_type: content_type,
525
- save_to_file: save_to_file,
436
+ save_to: save_to,
526
437
  exception: exception,
527
438
  headers: headers,
528
439
  ret: ret
@@ -531,15 +442,138 @@ module Aspera
531
442
  # raise exception if could not retry and not return error in result
532
443
  raise e if exception
533
444
  end
534
- Log.log.debug{"result=http:#{result_http}, data:#{result_data.class}"}
445
+ Log.log.debug { "result=http:#{result_http}, data:#{result_data.class}" }
535
446
  return case ret
536
447
  when :data then result_data
537
448
  when :resp then result_http
538
449
  when :both then [result_data, result_http]
539
- else Aspera.error_unexpected_value(ret){'Type of result for REST'}
450
+ else Aspera.error_unexpected_value(ret) { 'Type of result for REST' }
540
451
  end
541
452
  end
542
453
 
454
+ private
455
+
456
+ def prepare_call(headers, query)
457
+ if headers.nil?
458
+ headers = @headers.clone
459
+ else
460
+ h = headers
461
+ headers = @headers.clone
462
+ headers.merge!(h)
463
+ end
464
+ Aspera.assert_type(headers, Hash)
465
+ case @auth_params[:type]
466
+ when :none
467
+ # no auth
468
+ when :basic
469
+ Log.log.debug('using Basic auth')
470
+ # done in build_req
471
+ when :oauth2
472
+ headers['Authorization'] = oauth.authorization unless headers.key?('Authorization')
473
+ when :url
474
+ query ||= {}
475
+ @auth_params[:url_query].each do |key, value|
476
+ query[key] = value
477
+ end
478
+ else Aspera.error_unexpected_value(@auth_params[:type])
479
+ end
480
+ [headers, query]
481
+ end
482
+
483
+ def build_request(operation, subpath, query, content_type, body, headers)
484
+ # TODO: shall we percent encode subpath (spaces) test with access key delete with space in id
485
+ # URI.escape()
486
+ separator = ['', '/'].include?(subpath) ? '' : '/'
487
+ uri = self.class.build_uri("#{@base_url}#{separator}#{subpath}", query)
488
+ Log.log.debug { "URI=#{uri}" }
489
+ begin
490
+ # instantiate request object based on string name
491
+ req = Net::HTTP.const_get(operation.capitalize).new(uri)
492
+ rescue NameError
493
+ raise "unsupported operation : #{operation}"
494
+ end
495
+ case content_type
496
+ when nil # ignore
497
+ when Mime::JSON
498
+ req.body = JSON.generate(body) # , ascii_only: true
499
+ req['Content-Type'] = Mime::JSON
500
+ when Mime::WWW
501
+ req.body = URI.encode_www_form(body)
502
+ req['Content-Type'] = Mime::WWW
503
+ when Mime::TEXT
504
+ req.body = body
505
+ req['Content-Type'] = Mime::TEXT
506
+ else Aspera.error_unexpected_value(content_type) { 'body type' }
507
+ end
508
+ # set headers
509
+ headers.each do |key, value|
510
+ req[key] = value
511
+ end
512
+ # :type = :basic
513
+ req.basic_auth(@auth_params[:username], @auth_params[:password]) if @auth_params[:type].eql?(:basic)
514
+ Log.dump(:req_body, req.body, level: :trace1)
515
+ req
516
+ end
517
+
518
+ def save_response(response, result_http, result_mime, save_to)
519
+ return false unless !save_to.nil? && result_http.code.to_s.start_with?('2') && !Mime.json?(result_mime)
520
+
521
+ total_size = result_http['Content-Length']&.to_i
522
+ Log.log.debug('before write file')
523
+ target_file = save_to
524
+ # override user's path to path in header
525
+ unless response['Content-Disposition'].nil?
526
+ disposition = self.class.parse_header(response['Content-Disposition'])
527
+ if disposition[:parameters].key?(:filename) && !disposition[:parameters][:filename].eql?('.')
528
+ # Use only the basename to prevent path traversal via a server-controlled Content-Disposition header
529
+ safe_filename = File.basename(disposition[:parameters][:filename])
530
+ target_file = File.join(File.dirname(target_file), safe_filename) unless safe_filename.empty?
531
+ end
532
+ end
533
+ Log.log.debug { "saving to: #{target_file}" }
534
+ written_size = 0
535
+ session_id = SecureRandom.uuid.freeze
536
+ RestParameters.instance.progress_bar&.event(:session_start, session_id: session_id)
537
+ RestParameters.instance.progress_bar&.event(:session_size, session_id: session_id, info: total_size) if total_size
538
+ limiter = TimerLimiter.new(0.5)
539
+ if target_file.respond_to?(:write)
540
+ # IO object: stream directly into it
541
+ result_http.read_body do |fragment|
542
+ target_file.write(fragment)
543
+ written_size += fragment.length
544
+ RestParameters.instance.progress_bar&.event(:transfer, session_id: session_id, info: written_size) if limiter.trigger?
545
+ end
546
+ else
547
+ # file path: download to partial name first, then rename atomically
548
+ target_file_tmp = "#{target_file}#{RestParameters.instance.download_partial_suffix}"
549
+ FileUtils.mkdir_p(File.dirname(target_file_tmp))
550
+ File.open(target_file_tmp, 'wb') do |file|
551
+ result_http.read_body do |fragment|
552
+ file.write(fragment)
553
+ written_size += fragment.length
554
+ RestParameters.instance.progress_bar&.event(:transfer, session_id: session_id, info: written_size) if limiter.trigger?
555
+ end
556
+ end
557
+ File.rename(target_file_tmp, target_file)
558
+ end
559
+ RestParameters.instance.progress_bar&.event(:session_end, session_id: session_id)
560
+ RestParameters.instance.progress_bar&.event(:end)
561
+ true
562
+ end
563
+
564
+ def parse_response(result_http, result_mime)
565
+ result_data = result_http.body
566
+ Log.dump(:result_data_raw, result_data, level: :trace1)
567
+ # TODO: Remove next 2 lines when bug in async node api is fixed. (Aspera/core/issues/4490)
568
+ node_api_bug = result_data&.index('}HTTP/1.1 400 Bad Request') if result_data.is_a?(String)
569
+ result_data = result_data[0..node_api_bug] if node_api_bug
570
+ result_data = JSON.parse(result_data) if Mime.json?(result_mime) && !result_data.nil? && !result_data.empty?
571
+ Log.dump(:result_data, result_data)
572
+ result_data
573
+ end
574
+
575
+ public
576
+
543
577
  #
544
578
  # CRUD simplified methods here
545
579
  # If specific elements are needed, then use the full `call` method
@@ -36,7 +36,7 @@ module Aspera
36
36
  # @param data [Object] the parsed response body
37
37
  # @param http [Net::HTTPResponse] the raw HTTP response
38
38
  def raise_on_error(req, data, http)
39
- Log.log.debug{"raise_on_error #{req.method} #{req.path} #{http.code}"}
39
+ Log.log.debug { "raise_on_error #{req.method} #{req.path} #{http.code}" }
40
40
  context = {
41
41
  messages: [],
42
42
  request: req,
@@ -49,7 +49,7 @@ module Aspera
49
49
  @error_handlers.each do |handler|
50
50
  handler[:block].call(handler[:name], context)
51
51
  rescue StandardError => e
52
- Log.log.error{"ERROR in handler:\n#{e.message}\n#{e.backtrace}"}
52
+ Log.log.error { "ERROR in handler:\n#{e.message}\n#{e.backtrace}" }
53
53
  end
54
54
  raise RestCallError, context unless context[:messages].empty?
55
55
  end
@@ -97,7 +97,7 @@ module Aspera
97
97
  # @param message [String] one error message to add to list
98
98
  def add_error(context, type, message)
99
99
  context[:messages].push(message)
100
- Log.log.trace1{"Found error: #{type}: #{message}"}
100
+ Log.log.trace1 { "Found error: #{type}: #{message}" }
101
101
  log_file = instance.log_file
102
102
  # log error for further analysis (file must exist to activate)
103
103
  return if log_file.nil? || !File.exist?(log_file)
@@ -17,8 +17,8 @@ module Aspera
17
17
  # @param query [Hash] Optional additional search query parameters
18
18
  # @return [Hash] A single entity matching the search, or an exception if not found or multiple found
19
19
  def lookup_with_q(entity, value:, field: 'name', query: {})
20
- Aspera.assert_type(query, Hash){'query'}
21
- Aspera.assert_type(field, String){'field'}
20
+ Aspera.assert_type(query, Hash) { 'query' }
21
+ Aspera.assert_type(field, String) { 'field' }
22
22
  # returns entities matching the query (it matches against several fields in case insensitive way)
23
23
  # We don't do paging, as anyway, we look for only one match
24
24
  matching_items = read(entity, query.merge({'q' => value}))
@@ -32,11 +32,11 @@ module Aspera
32
32
  else
33
33
  # multiple case insensitive partial matches, try case insensitive full match
34
34
  # (anyway AoC does not allow creation of 2 entities with same case insensitive field value)
35
- value_matches = matching_items.select{ |i| i[field].casecmp?(value)}
35
+ value_matches = matching_items.select { |i| i[field].casecmp?(value) }
36
36
  case value_matches.length
37
37
  when 1 then return value_matches.first
38
- when 0 then raise Error, "#{entity}: Multiple case insensitive partial match for: \"#{value}\" in #{matching_items.map{ |i| i[field]}.join(', ')} but no case insensitive full match. Please be more specific or give exact #{field}."
39
- else raise Error, "Two entities cannot have the same case insensitive #{field}: #{value_matches.map{ |i| i[field]}}"
38
+ when 0 then raise Error, "#{entity}: Multiple case insensitive partial match for: \"#{value}\" in #{matching_items.map { |i| i[field] }.join(', ')} but no case insensitive full match. Please be more specific or give exact #{field}."
39
+ else raise Error, "Two entities cannot have the same case insensitive #{field}: #{value_matches.map { |i| i[field] }}"
40
40
  end
41
41
  end
42
42
  end
@@ -60,7 +60,8 @@ module Aspera
60
60
  Aspera.assert_type(entity, String)
61
61
  Aspera.assert_type(items_key, String)
62
62
  Aspera.assert_type(query, Hash)
63
- Log.log.debug{"list_entities t=#{entity} k=#{items_key} q=#{query}"}
63
+ Log.log.debug { "list_entities t=#{entity} k=#{items_key}" }
64
+ Log.dump(:query, query)
64
65
  result = []
65
66
  offset = 0
66
67
  max_items = query.delete(MAX_ITEMS)
@@ -104,10 +105,10 @@ module Aspera
104
105
  # @param query [Hash] Additional query parameters (Default: `:default`)
105
106
  def lookup_entity_by_field(entity:, value:, field: 'name', items_key: nil, query: :default)
106
107
  if query.eql?(:default)
107
- Aspera.assert_values(field, ['name']){'Default query field'}
108
+ Aspera.assert_values(field, ['name']) { 'Default query field' }
108
109
  query = {'q'=> value}
109
110
  end
110
- lookup_entity_generic(entity: entity, field: field, value: value){list_entities_limit_offset_total_count(entity: entity, items_key: items_key, query: query).first}
111
+ lookup_entity_generic(entity: entity, field: field, value: value) { list_entities_limit_offset_total_count(entity: entity, items_key: items_key, query: query).first }
111
112
  end
112
113
 
113
114
  # Lookup entity by field and value.
@@ -123,7 +124,7 @@ module Aspera
123
124
  Aspera.assert(block_given?, 'block required for lookup_entity_generic')
124
125
  found = yield
125
126
  Aspera.assert_array_all(found, Hash)
126
- found = found.select{ |i| i[field].eql?(value)}
127
+ found = found.select { |i| i[field].eql?(value) }
127
128
  return found.first if found.length.eql?(1)
128
129
  raise Cli::BadIdentifier.new(entity, value, field: field, count: found.length)
129
130
  end