aspera-cli 4.27.0 → 4.27.1

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 (140) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +29 -0
  4. data/CONTRIBUTING.md +1 -7
  5. data/TODO.md +50 -0
  6. data/docs/README.md +708 -488
  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 +297 -313
  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 +16 -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 +12 -12
  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_Shares.yaml +4653 -0
  119. data/lib/aspera/schema/documentation.rb +17 -11
  120. data/lib/aspera/schema/reader.rb +78 -4
  121. data/lib/aspera/schema/registry.rb +26 -7
  122. data/lib/aspera/secret_hider.rb +5 -5
  123. data/lib/aspera/ssh.rb +64 -31
  124. data/lib/aspera/ssl.rb +3 -3
  125. data/lib/aspera/sync/database.rb +2 -2
  126. data/lib/aspera/sync/operations.rb +13 -13
  127. data/lib/aspera/temp_file_manager.rb +24 -4
  128. data/lib/aspera/transfer/faux_file.rb +2 -2
  129. data/lib/aspera/transfer/parameters.rb +15 -15
  130. data/lib/aspera/transfer/resumer.rb +9 -9
  131. data/lib/aspera/transfer/spec.rb +1 -1
  132. data/lib/aspera/uri_reader.rb +52 -15
  133. data/lib/aspera/web_auth.rb +6 -5
  134. data/lib/aspera/web_server_simple.rb +9 -9
  135. data/lib/aspera/yaml.rb +4 -4
  136. data.tar.gz.sig +0 -0
  137. metadata +10 -4
  138. metadata.gz.sig +0 -0
  139. data/lib/aspera/cli/plugins/faspex.rb +0 -576
  140. data/lib/aspera/transfer/uri.rb +0 -56
@@ -43,14 +43,11 @@ module Aspera
43
43
  SEPARATOR = ':'
44
44
  NODE_PREFIX = 'node.'
45
45
  end
46
- # Accepted types in `file_matcher`
47
- MATCH_TYPES = [String, Proc, Regexp, NilClass].freeze
48
46
  # Delimiter in decoded node token
49
47
  SIGNATURE_DELIMITER = '==SIGNATURE=='
50
48
  # Default validity when generating a bearer token "manually"
51
49
  BEARER_TOKEN_VALIDITY_DEFAULT = 86400
52
- private_constant :MATCH_TYPES,
53
- :SIGNATURE_DELIMITER, :BEARER_TOKEN_VALIDITY_DEFAULT
50
+ private_constant :SIGNATURE_DELIMITER, :BEARER_TOKEN_VALIDITY_DEFAULT
54
51
  # Node API permissions: delete list mkdir preview read rename write
55
52
  ACCESS_LEVELS = %w[delete list mkdir preview read rename write].freeze
56
53
  # Special HTTP Headers
@@ -63,7 +60,7 @@ module Aspera
63
60
  HEADER_X_TOTAL_COUNT = 'X-Total-Count'
64
61
 
65
62
  # Register node special token decoder
66
- OAuth::Factory.instance.register_decoder(lambda{ |token| Node.decode_bearer_token(token)})
63
+ OAuth::Factory.instance.register_decoder(lambda { |token| Node.decode_bearer_token(token) })
67
64
 
68
65
  # Class instance variable, access with accessors on class
69
66
  @api_options = {
@@ -82,8 +79,8 @@ module Aspera
82
79
  def api_options=(options_hash)
83
80
  Aspera.assert_type(options_hash, Hash)
84
81
  options_hash.each do |key, val|
85
- Aspera.assert(@api_options.key?(key.to_sym)){"unknown api option: #{key} (#{OPTIONS.join(', ')})"}
86
- Aspera.assert_type(val, TrueClass, FalseClass){"api options value for #{key} should be boolean"}
82
+ Aspera.assert(@api_options.key?(key.to_sym)) { "unknown api option: #{key} (#{OPTIONS.join(', ')})" }
83
+ Aspera.assert_type(val, TrueClass, FalseClass) { "api options value for #{key} should be boolean" }
87
84
  @api_options[key.to_sym] = val
88
85
  end
89
86
  end
@@ -127,25 +124,6 @@ module Aspera
127
124
  return parameters
128
125
  end
129
126
 
130
- # For access keys: provide expression to match entry in folder
131
- # @param match_expression [Proc, Regexp, String] one of supported types
132
- # @return [Proc] lambda function
133
- def file_matcher(match_expression)
134
- case match_expression
135
- when Proc then return match_expression
136
- when Regexp then return ->(f){f['name'].match?(match_expression)}
137
- when String
138
- return ->(f){File.fnmatch(match_expression, f['name'], File::FNM_DOTMATCH)}
139
- when NilClass then return ->(_){true}
140
- else Aspera.error_unexpected_value(match_expression.class.name, type: ParameterError)
141
- end
142
- end
143
-
144
- # @return [Proc] lambda from provided CLI options
145
- def file_matcher_from_argument(options)
146
- return file_matcher(options.get_next_argument('filter', validation: MATCH_TYPES, mandatory: false))
147
- end
148
-
149
127
  # Split path into folder + filename
150
128
  # @param path [String] file path to split
151
129
  # @return [Array(String, String)] containing folder + inside folder/file
@@ -166,8 +144,8 @@ module Aspera
166
144
  # @return [Hash] decoded scope with access key and scope parts
167
145
  def decode_scope(scope)
168
146
  items = scope.split(Scope::SEPARATOR, 2)
169
- Aspera.assert(items.length.eql?(2)){"invalid scope: #{scope}"}
170
- Aspera.assert(items[0].start_with?(Scope::NODE_PREFIX)){"invalid scope: #{scope}"}
147
+ Aspera.assert(items.length.eql?(2)) { "invalid scope: #{scope}" }
148
+ Aspera.assert(items[0].start_with?(Scope::NODE_PREFIX)) { "invalid scope: #{scope}" }
171
149
  return {access_key: items[0].delete_prefix(Scope::NODE_PREFIX), scope: items[1]}
172
150
  end
173
151
 
@@ -253,10 +231,10 @@ module Aspera
253
231
  workspace_name: @app_info.workspace_name
254
232
  )
255
233
  end
256
- Log.log.warn{"Cannot resolve link with node id #{node_id}, no resolver"}
234
+ Log.log.warn { "Cannot resolve link with node id #{node_id}, no resolver" }
257
235
  return
258
236
  rescue RestCallError => e
259
- Log.log.warn{"Cannot resolve link with node id #{node_id}: #{e.message}"}
237
+ Log.log.warn { "Cannot resolve link with node id #{node_id}: #{e.message}" }
260
238
  return
261
239
  end
262
240
 
@@ -271,7 +249,7 @@ module Aspera
271
249
  entry['target_id'] = link_entry['target_id']
272
250
  end
273
251
  return true unless entry['target_node_id'].nil? || entry['target_id'].nil?
274
- Log.log.warn{"Missing target information for link: #{entry['name']}"}
252
+ Log.log.warn { "Missing target information for link: #{entry['name']}" }
275
253
  return false
276
254
  end
277
255
 
@@ -323,8 +301,8 @@ module Aspera
323
301
  end
324
302
  rescue StandardError => e
325
303
  raise e if exception
326
- Log.log.warn{"#{path || file_id}: #{e.class} #{e.message}"}
327
- Log.log.debug{(['Backtrace:'] + e.backtrace).join("\n")}
304
+ Log.log.warn { "#{path || file_id}: #{e.class} #{e.message}" }
305
+ Log.log.debug { (['Backtrace:'] + e.backtrace).join("\n") }
328
306
  ensure
329
307
  RestParameters.instance.spinner_cb.call(folder_items.count, action: :success)
330
308
  end
@@ -341,14 +319,14 @@ module Aspera
341
319
  # @para query [Hash, nil] optional query for `read`
342
320
  def process_folder_tree(method_sym:, state:, top_file_id:, top_file_path: '/', query: nil)
343
321
  Aspera.assert(!top_file_path.nil?, 'top_file_path not set')
344
- Log.log.debug{"process_folder_tree: node=#{@app_info ? @app_info.node_info['id'] : 'nil'}, file id=#{top_file_id}, path=#{top_file_path}"}
322
+ Log.log.debug { "process_folder_tree: node=#{@app_info ? @app_info.node_info['id'] : 'nil'}, file id=#{top_file_id}, path=#{top_file_path}" }
345
323
  # Start at top folder
346
324
  folders_to_explore = [{id: top_file_id, path: top_file_path}]
347
325
  Log.dump(:folders_to_explore, folders_to_explore)
348
326
  until folders_to_explore.empty?
349
327
  # Consume first in job list
350
328
  current_item = folders_to_explore.shift
351
- Log.log.debug{"Exploring #{current_item[:path]}".bg_green}
329
+ Log.log.debug { "Exploring #{current_item[:path]}".bg_green }
352
330
  # Get folder content
353
331
  folder_contents = read_folder_content(current_item[:id], query, exception: false, path: current_item[:path])
354
332
  Log.dump(:folder_contents, folder_contents)
@@ -358,7 +336,7 @@ module Aspera
358
336
  next
359
337
  end
360
338
  current_path = File.join(current_item[:path], entry['name'])
361
- Log.log.debug{"process_folder_tree: checking #{current_path}"}
339
+ Log.log.debug { "process_folder_tree: checking #{current_path}" }
362
340
  # Call block, continue only if method returns true
363
341
  next unless send(method_sym, entry, current_path, state)
364
342
  # Entry type is file, folder or link
@@ -393,8 +371,8 @@ module Aspera
393
371
  return NodeFileId.new(self, top_file_id) if path_elements.empty?
394
372
  resolve_state = {path: path_elements, consumed: [], result: nil, process_last_link: process_last_link}
395
373
  process_folder_tree(method_sym: :process_api_fid, state: resolve_state, top_file_id: top_file_id)
396
- Aspera.assert(!resolve_state[:result].nil?, type: ParameterError){"Entry not found: #{resolve_state[:path].first} in /#{resolve_state[:consumed].join(PATH_SEPARATOR)}"}
397
- Log.log.debug{"resolve_api_fid: #{path} -> #{resolve_state[:result].node_api.base_url} #{resolve_state[:result].file_id}"}
374
+ Aspera.assert(!resolve_state[:result].nil?, type: ParameterError) { "Entry not found: #{resolve_state[:path].first} in /#{resolve_state[:consumed].join(PATH_SEPARATOR)}" }
375
+ Log.log.debug { "resolve_api_fid: #{path} -> #{resolve_state[:result].node_api.base_url} #{resolve_state[:result].file_id}" }
398
376
  return resolve_state[:result]
399
377
  end
400
378
 
@@ -409,10 +387,10 @@ module Aspera
409
387
  def resolve_api_fid_paths(top_file_id, paths)
410
388
  Aspera.assert_type(paths, Array)
411
389
  Aspera.assert(paths.size.positive?, 'paths must not be empty')
412
- split_sources = paths.map{ |p| Pathname(p['source']).each_filename.to_a}
390
+ split_sources = paths.map { |p| Pathname(p['source']).each_filename.to_a }
413
391
  root = []
414
392
  split_sources.map(&:size).min.times do |i|
415
- parts = split_sources.map{ |s| s[i]}
393
+ parts = split_sources.map { |s| s[i] }
416
394
  break unless parts.uniq.size == 1
417
395
  root << parts.first
418
396
  end
@@ -440,7 +418,7 @@ module Aspera
440
418
  # Single source is 'folder' or 'link'
441
419
  # TODO: add this ? , 'destination'=>file_info['name']
442
420
  [{'source' => '.'}]
443
- else Aspera.error_unexpected_value(file_info['type']){'source type'}
421
+ else Aspera.error_unexpected_value(file_info['type']) { 'source type' }
444
422
  end
445
423
  end
446
424
  [apifid, source_paths]
@@ -450,7 +428,7 @@ module Aspera
450
428
  # @param top_file_id [String] Search root
451
429
  # @param test_lambda [Proc] Test function
452
430
  def find_files(top_file_id, test_lambda)
453
- Log.log.debug{"find_files: file id=#{top_file_id}"}
431
+ Log.log.debug { "find_files: file id=#{top_file_id}" }
454
432
  find_state = {found: [], test_lambda: test_lambda}
455
433
  process_folder_tree(method_sym: :process_find_files, state: find_state, top_file_id: top_file_id)
456
434
  return find_state[:found]
@@ -535,7 +513,7 @@ module Aspera
535
513
  # Get the transfer user from info on access key
536
514
  transfer_spec['remote_user'] = info['transfer_user'] if info['transfer_user']
537
515
  # Get settings from name.value array to hash key.value
538
- settings = info['settings']&.to_h{ |i| [i['name'], i['value']]}
516
+ settings = info['settings']&.to_h { |i| [i['name'], i['value']] }
539
517
  # Check WSS ports
540
518
  Transfer::Spec::WSS_FIELDS.each do |i|
541
519
  transfer_spec[i] = settings[i] if settings.key?(i)
@@ -543,7 +521,7 @@ module Aspera
543
521
  else
544
522
  transfer_spec.merge!(transport_params)
545
523
  end
546
- Aspera.assert_values(transfer_spec['remote_user'], [Transfer::Spec::ACCESS_KEY_TRANSFER_USER], type: :warn){'transfer user'}
524
+ Aspera.assert_values(transfer_spec['remote_user'], [Transfer::Spec::ACCESS_KEY_TRANSFER_USER], type: :warn) { 'transfer user' }
547
525
  return transfer_spec
548
526
  end
549
527
 
@@ -552,8 +530,8 @@ module Aspera
552
530
  # @param call_args [Hash] additional arguments to pass to `Rest.call`
553
531
  # @return [Array] list of items returned by the API call
554
532
  def read_with_paging(subpath, query = nil, iteration: nil, **call_args)
555
- Aspera.assert_type(iteration, Array, NilClass){'iteration'}
556
- Aspera.assert_type(query, Hash, NilClass){'query'}
533
+ Aspera.assert_type(iteration, Array, NilClass) { 'iteration' }
534
+ Aspera.assert_type(query, Hash, NilClass) { 'query' }
557
535
  Aspera.assert(!call_args.key?(:query), ':query must not be in call_args (use query parameter)')
558
536
  query = {} if query.nil?
559
537
  query[:iteration_token] = iteration[0] unless iteration.nil? || iteration[0].nil?
@@ -563,7 +541,7 @@ module Aspera
563
541
  item_list = []
564
542
  loop do
565
543
  data, http = read(subpath, query, **call_args, ret: :both)
566
- Aspera.assert_type(data, Array){"Expected data to be an Array, got: #{data.class}"}
544
+ Aspera.assert_type(data, Array) { "Expected data to be an Array, got: #{data.class}" }
567
545
  # no data
568
546
  break if data.empty?
569
547
  item_list.concat(data)
@@ -583,7 +561,7 @@ module Aspera
583
561
  query_params = Rest.query_to_h(parsed_uri.query) if parsed_uri.query
584
562
  next_iteration_token = query_params['iteration_token'] if query_params
585
563
  rescue URI::InvalidURIError => e
586
- Log.log.warn{"Invalid URI in Link header: #{next_url} - #{e.message}"}
564
+ Log.log.warn { "Invalid URI in Link header: #{next_url} - #{e.message}" }
587
565
  end
588
566
  end
589
567
  # Stop if no next token
@@ -622,8 +600,8 @@ module Aspera
622
600
  query['page'] += 1
623
601
  end
624
602
  rescue StandardError => e
625
- Log.log.warn{"#{e.class} #{e.message}"}
626
- Log.log.debug{(['Backtrace:'] + e.backtrace).join("\n")}
603
+ Log.log.warn { "#{e.class} #{e.message}" }
604
+ Log.log.debug { (['Backtrace:'] + e.backtrace).join("\n") }
627
605
  ensure
628
606
  RestParameters.instance.spinner_cb.call(items.count, action: :success)
629
607
  return items # rubocop:disable Lint/EnsureReturn
@@ -644,7 +622,7 @@ module Aspera
644
622
  # File must be terminal
645
623
  raise "#{entry['name']} is a file, expecting folder to find: #{state[:path]}" unless path_fully_consumed
646
624
  # It's terminal, we found it
647
- Log.log.debug{"process_api_fid: found #{path} -> #{entry['id']}"}
625
+ Log.log.debug { "process_api_fid: found #{path} -> #{entry['id']}" }
648
626
  state[:result] = NodeFileId.new(self, entry['id'])
649
627
  return false
650
628
  when 'folder'
@@ -659,7 +637,7 @@ module Aspera
659
637
  # We found it
660
638
  other_node = nil
661
639
  other_node = node_id_to_node(entry['target_node_id']) if entry_has_link_information(entry)
662
- Aspera.assert(!other_node.nil?, type: Error){'Cannot resolve link'}
640
+ Aspera.assert(!other_node.nil?, type: Error) { 'Cannot resolve link' }
663
641
  state[:result] = NodeFileId.new(other_node, entry['target_id'])
664
642
  else
665
643
  # We found it but we do not process the link
@@ -667,7 +645,7 @@ module Aspera
667
645
  end
668
646
  return false
669
647
  end
670
- else Aspera.error_unexpected_value(entry['type'], type: :warn){'folder entry type'}
648
+ else Aspera.error_unexpected_value(entry['type'], type: :warn) { 'folder entry type' }
671
649
  end
672
650
  # Continue to dig folder
673
651
  return true
data/lib/aspera/ascmd.rb CHANGED
@@ -89,11 +89,11 @@ module Aspera
89
89
  # @param action_sym [Symbol] one of OPERATIONS
90
90
  # @param arguments [Array] parameters for "as" command
91
91
  # @return [Boolean,Array,Hash] result of command, type depends on command
92
- def execute_single(action_sym, arguments, version: 1, host: nil)
92
+ def execute_single(action_sym, arguments, version: 2, host: nil)
93
93
  arguments = [] if arguments.nil?
94
- Log.log.debug{"execute_single:#{action_sym}:#{arguments}"}
94
+ Log.log.debug { "execute_single:#{action_sym}:#{arguments}" }
95
95
  Aspera.assert_type(action_sym, Symbol)
96
- Aspera.assert_array_all(arguments, String){'arguments'}
96
+ Aspera.assert_array_all(arguments, String) { 'arguments' }
97
97
  remote_cmd = 'ascmd'
98
98
  # lines of commands (String's)
99
99
  command_lines = []
@@ -117,7 +117,7 @@ module Aspera
117
117
  # enclose arguments in double quotes, protect backslash and double quotes
118
118
  # ascmd uses space as token separator, and optional quotes ('") or \ to escape
119
119
  args.each do |v|
120
- command.push(%Q{"#{v.gsub(/["\\]/){ |s| "\\#{s}"}}"})
120
+ command.push(%Q{"#{v.gsub(/["\\]/) { |s| "\\#{s}" }}"})
121
121
  end
122
122
  command_lines.push(command.join(' '))
123
123
  end
@@ -125,7 +125,7 @@ module Aspera
125
125
  command_lines.push('')
126
126
  # execute the main command and then exit
127
127
  stdin_input = command_lines.join("\n")
128
- Log.log.trace1{"execute_single:#{stdin_input}"}
128
+ Log.log.trace1 { "execute_single:#{stdin_input}" }
129
129
  # execute, get binary output
130
130
  byte_buffer = @command_executor.execute(remote_cmd, input: stdin_input).unpack('C*')
131
131
  Aspera.assert(!byte_buffer.empty?, 'empty answer from server')
@@ -154,7 +154,7 @@ module Aspera
154
154
  end
155
155
  # raise error as exception
156
156
  raise Error.new(result[:errno], result[:errstr], action_sym, arguments) if
157
- result.is_a?(Hash) && (result.keys.sort == TYPES_DESCR[:error][:fields].map{ |i| i[:name]}.sort)
157
+ result.is_a?(Hash) && (result.keys.sort == TYPES_DESCR[:error][:fields].map { |i| i[:name] }.sort)
158
158
  return result
159
159
  end
160
160
 
@@ -174,7 +174,7 @@ module Aspera
174
174
  # @return [Hash] field description
175
175
  def field_description(struct_name, typed_buffer)
176
176
  result = TYPES_DESCR[struct_name][:fields][typed_buffer[:btype] - ENUM_START]
177
- Aspera.assert(!result.nil?){"Unrecognized field for #{struct_name}: #{typed_buffer[:btype]}\n#{typed_buffer[:buffer]}"}
177
+ Aspera.assert(!result.nil?) { "Unrecognized field for #{struct_name}: #{typed_buffer[:btype]}\n#{typed_buffer[:buffer]}" }
178
178
  return result
179
179
  end
180
180
 
@@ -183,8 +183,8 @@ module Aspera
183
183
  def parse(buffer, type_name, indent_level = nil)
184
184
  indent_level = (indent_level || -1) + 1
185
185
  type_descr = TYPES_DESCR[type_name]
186
- Aspera.assert(!type_descr.nil?){"Unexpected type #{type_name}"}
187
- Log.log.trace1{"#{' .' * indent_level}parse:#{type_name}:#{type_descr[:decode]}:#{buffer[0, 16]}...".red}
186
+ Aspera.assert(!type_descr.nil?) { "Unexpected type #{type_name}" }
187
+ Log.log.trace1 { "#{' .' * indent_level}parse:#{type_name}:#{type_descr[:decode]}:#{buffer[0, 16]}...".red }
188
188
  result = nil
189
189
  case type_descr[:decode]
190
190
  when :base
@@ -194,7 +194,7 @@ module Aspera
194
194
  byte_array = [byte_array] unless byte_array.is_a?(Array)
195
195
  result = byte_array.pack('C*').unpack1(type_descr[:unpack])
196
196
  result.force_encoding('UTF-8') if type_name.eql?(:zstr)
197
- Log.log.trace1{"#{' .' * indent_level}-> base:#{byte_array} -> #{result}"}
197
+ Log.log.trace1 { "#{' .' * indent_level}-> base:#{byte_array} -> #{result}" }
198
198
  result = Time.at(result) if type_name.eql?(:epoch)
199
199
  when :buffer_list
200
200
  # return a list of type_buffer
@@ -205,7 +205,7 @@ module Aspera
205
205
  Aspera.assert(buffer.length >= length, 'not enough bytes')
206
206
  value = buffer.shift(length)
207
207
  result.push({btype: btype, buffer: value})
208
- Log.log.trace1{"#{' .' * indent_level}:buffer_list[#{result.length - 1}] #{result.last}"}
208
+ Log.log.trace1 { "#{' .' * indent_level}:buffer_list[#{result.length - 1}] #{result.last}" }
209
209
  end
210
210
  when :field_list
211
211
  # by default the result is one struct
@@ -214,7 +214,7 @@ module Aspera
214
214
  parse(buffer, :blist, indent_level).each do |typed_buffer|
215
215
  # what type of field is it ?
216
216
  field_info = field_description(type_name, typed_buffer)
217
- Log.log.trace1{"#{' .' * indent_level}+ field(special=#{field_info[:special]})=#{field_info[:name]}".green}
217
+ Log.log.trace1 { "#{' .' * indent_level}+ field(special=#{field_info[:special]})=#{field_info[:name]}".green }
218
218
  case field_info[:special]
219
219
  when nil # normal case
220
220
  result[field_info[:name]] = parse(typed_buffer[:buffer], field_info[:is_a], indent_level)
@@ -224,7 +224,7 @@ module Aspera
224
224
  result[field_info[:name]] ||= []
225
225
  result[field_info[:name]].push(parse(typed_buffer[:buffer], field_info[:is_a], indent_level))
226
226
  when :list_tlv_list # field is an array of values in a list of buffers
227
- result[field_info[:name]] = parse(typed_buffer[:buffer], :blist, indent_level).map{ |r| parse(r[:buffer], field_info[:is_a], indent_level)}
227
+ result[field_info[:name]] = parse(typed_buffer[:buffer], :blist, indent_level).map { |r| parse(r[:buffer], field_info[:is_a], indent_level) }
228
228
  when :list_tlv_restart # field is an array of values, but a new value is started on index 1
229
229
  fl = result[field_info[:name]] = []
230
230
  parse(typed_buffer[:buffer], :blist, indent_level).map do |tb|
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # cspell:ignore protobuf ckpt
4
+ require 'aspera/yaml'
4
5
  require 'aspera/environment'
5
6
  require 'aspera/data_repository'
6
7
  require 'aspera/log'
@@ -46,9 +47,9 @@ module Aspera
46
47
  def sdk_locations
47
48
  location_url = @transferd_urls
48
49
  transferd_locations = UriReader.read(location_url)
49
- Log.log.debug{"Retrieving SDK locations from #{location_url}"}
50
+ Log.log.debug { "Retrieving SDK locations from #{location_url}" }
50
51
  begin
51
- return YAML.load(transferd_locations)
52
+ return Yaml.safe_load(transferd_locations)
52
53
  rescue Psych::SyntaxError
53
54
  raise "Error when parsing yaml data from: #{location_url}"
54
55
  end
@@ -60,23 +61,23 @@ module Aspera
60
61
  # - "product:PRODUCT_NAME" to use ascp from named product
61
62
  # - "product:FIRST" to use ascp from first found product
62
63
  def sdk_folder=(ascp_location)
63
- Aspera.assert_type(ascp_location, String){'ascp_location'}
64
+ Aspera.assert_type(ascp_location, String) { 'ascp_location' }
64
65
  Aspera.assert(!ascp_location.empty?, 'ascp location cannot be empty: check your config file')
65
66
  folder =
66
67
  if ascp_location.start_with?(USE_PRODUCT_PREFIX)
67
68
  product_name = ascp_location.delete_prefix(USE_PRODUCT_PREFIX)
68
69
  if product_name.eql?(FIRST_FOUND)
69
70
  pl = installed_products.first
70
- Aspera.assert(!pl.nil?){"No Aspera transfer module or SDK found.\nRefer to the manual or install SDK with command:\nascli conf transferd install"}
71
+ Aspera.assert(!pl.nil?) { "No Aspera transfer module or SDK found.\nRefer to the manual or install SDK with command:\nascli conf transferd install" }
71
72
  else
72
- pl = installed_products.find{ |i| i[:name].eql?(product_name)}
73
- Aspera.assert(!pl.nil?){"No such product installed: #{product_name}"}
73
+ pl = installed_products.find { |i| i[:name].eql?(product_name) }
74
+ Aspera.assert(!pl.nil?) { "No such product installed: #{product_name}" }
74
75
  end
75
76
  File.dirname(pl[:ascp_path])
76
77
  else
77
78
  ascp_location.include?('/ascp') ? File.dirname(ascp_location) : ascp_location
78
79
  end
79
- Log.log.debug{"ascp_folder=#{folder}"}
80
+ Log.log.debug { "ascp_folder=#{folder}" }
80
81
  Products::Transferd.sdk_directory = folder
81
82
  nil
82
83
  end
@@ -118,11 +119,11 @@ module Aspera
118
119
  when :ssh_private_dsa, :ssh_private_rsa
119
120
  # assume last 3 letters are type
120
121
  type = file_type.to_s[-3..].to_sym
121
- file = check_or_create_sdk_file("aspera_bypass_#{type}.pem"){DataRepository.instance.item(type)}
122
+ file = check_or_create_sdk_file("aspera_bypass_#{type}.pem") { DataRepository.instance.item(type) }
122
123
  when :aspera_license
123
- file = check_or_create_sdk_file('aspera-license'){DataRepository.instance.item(:license)}
124
+ file = check_or_create_sdk_file('aspera-license') { DataRepository.instance.item(:license) }
124
125
  when :aspera_conf
125
- file = check_or_create_sdk_file('aspera.conf'){DEFAULT_ASPERA_CONF}
126
+ file = check_or_create_sdk_file('aspera.conf') { DEFAULT_ASPERA_CONF }
126
127
  when :fallback_certificate, :fallback_private_key
127
128
  file_key = File.join(Products::Transferd.sdk_directory, 'aspera_fallback_cert_private_key.pem')
128
129
  file_cert = File.join(Products::Transferd.sdk_directory, 'aspera_fallback_cert.pem')
@@ -131,14 +132,14 @@ module Aspera
131
132
  # create new self signed certificate for http fallback
132
133
  private_key = OpenSSL::PKey::RSA.new(4096)
133
134
  cert = WebServerSimple.self_signed_cert(private_key)
134
- check_or_create_sdk_file('aspera_fallback_cert_private_key.pem', force: true){private_key.to_pem}
135
- check_or_create_sdk_file('aspera_fallback_cert.pem', force: true){cert.to_pem}
135
+ check_or_create_sdk_file('aspera_fallback_cert_private_key.pem', force: true) { private_key.to_pem }
136
+ check_or_create_sdk_file('aspera_fallback_cert.pem', force: true) { cert.to_pem }
136
137
  end
137
138
  file = file_type.eql?(:fallback_certificate) ? file_cert : file_key
138
139
  else Aspera.error_unexpected_value(file_type)
139
140
  end
140
141
  return unless file_is_required || File.exist?(file)
141
- Aspera.assert(File.exist?(file), type: Errno::ENOENT){"#{file_type} not found (#{file})"}
142
+ Aspera.assert(File.exist?(file), type: Errno::ENOENT) { "#{file_type} not found (#{file})" }
142
143
  return file
143
144
  end
144
145
 
@@ -155,7 +156,7 @@ module Aspera
155
156
  Aspera.assert_values(types, CLIENT_SSH_KEY_OPTIONS)
156
157
  return case types
157
158
  when :dsa_rsa, :rsa
158
- types.to_s.split('_').map{ |i| Installation.instance.path("ssh_private_#{i}".to_sym)}
159
+ types.to_s.split('_').map { |i| Installation.instance.path("ssh_private_#{i}".to_sym) }
159
160
  when :per_client
160
161
  Aspera.error_not_implemented
161
162
  end
@@ -237,26 +238,26 @@ module Aspera
237
238
  def sdk_url_for_platform(platform: nil, version: nil)
238
239
  all_locations = sdk_locations
239
240
  platform = Environment.instance.architecture if platform.nil?
240
- locations = all_locations.select{ |l| l['platform'].eql?(platform)}
241
- Aspera.assert(!locations.empty?){"No SDK for platform: #{platform}, available: #{all_locations.map{ |i| i['platform']}.uniq}"}
242
- version = locations.max_by{ |entry| Gem::Version.new(entry['version'])}['version'] if version.nil?
243
- info = locations.select{ |entry| entry['version'].eql?(version)}
244
- Aspera.assert(!info.empty?){"No such version: #{version} for #{platform}"}
241
+ locations = all_locations.select { |l| l['platform'].eql?(platform) }
242
+ Aspera.assert(!locations.empty?) { "No SDK for platform: #{platform}, available: #{all_locations.map { |i| i['platform'] }.uniq}" }
243
+ version = locations.max_by { |entry| Gem::Version.new(entry['version']) }['version'] if version.nil?
244
+ info = locations.select { |entry| entry['version'].eql?(version) }
245
+ Aspera.assert(!info.empty?) { "No such version: #{version} for #{platform}" }
245
246
  return info.first['url']
246
247
  end
247
248
 
248
- # @param sdk_archive_path [String] path to SDK archive
249
+ # @param url [String] URL or filename used to detect archive format
250
+ # @param archive_io [StringIO] archive content as an in-memory IO stream
249
251
  # @yieldparam entry_name [String] File path in archive
250
252
  # @yieldparam entry_stream [IO, Gem::Package::TarReader::Entry] Data stream
251
253
  # @yieldparam link_target [String, nil] Link target if symlink, nil otherwise
252
- def extract_archive_files(sdk_archive_path)
254
+ def extract_archive_files(url, archive_io)
253
255
  Aspera.assert(block_given?, 'missing block')
254
- case sdk_archive_path
256
+ case url
255
257
  # Windows and Mac use zip
256
258
  when /\.zip$/
257
259
  require 'zip'
258
- # extract files from archive
259
- Zip::File.open(sdk_archive_path) do |zip_file|
260
+ Zip::File.open_buffer(archive_io) do |zip_file|
260
261
  zip_file.each do |entry|
261
262
  next if entry.name.end_with?('/')
262
263
  entry.get_input_stream do |io|
@@ -268,7 +269,7 @@ module Aspera
268
269
  when /\.tar\.gz/
269
270
  require 'zlib'
270
271
  require 'rubygems/package'
271
- Zlib::GzipReader.open(sdk_archive_path) do |gzip|
272
+ Zlib::GzipReader.wrap(archive_io) do |gzip|
272
273
  Gem::Package::TarReader.new(gzip) do |tar|
273
274
  tar.each do |entry|
274
275
  next if entry.directory?
@@ -277,7 +278,7 @@ module Aspera
277
278
  end
278
279
  end
279
280
  else
280
- raise "unknown archive extension: #{sdk_archive_path}"
281
+ raise "unknown archive extension: #{url}"
281
282
  end
282
283
  end
283
284
 
@@ -301,13 +302,19 @@ module Aspera
301
302
  extracted_files = {}
302
303
  # Security: Get canonical path of installation directory for boundary checks
303
304
  install_boundary = File.realpath(folder)
304
- sdk_archive_path = UriReader.read_as_file(url)
305
- extract_archive_files(sdk_archive_path) do |entry_name, entry_stream, link_target|
305
+ archive_io = StringIO.new
306
+ if UriReader.file?(url)
307
+ archive_io.write(File.binread(UriReader.file_path(url)))
308
+ else
309
+ Rest.new(base_url: url, redirect_max: 3).call(operation: 'GET', save_to: archive_io)
310
+ archive_io.rewind
311
+ end
312
+ extract_archive_files(url, archive_io) do |entry_name, entry_stream, link_target|
306
313
  dest_folder = if block_given?
307
314
  yield(entry_name)
308
315
  else
309
316
  # default files to extract directly to main folder if in selected source folders
310
- Products::Transferd::RUNTIME_FOLDERS.any?{ |i| entry_name.match?(%r{^[^/]*/#{i}/})} ? '/' : nil
317
+ Products::Transferd::RUNTIME_FOLDERS.any? { |i| entry_name.match?(%r{^[^/]*/#{i}/}) } ? '/' : nil
311
318
  end
312
319
  next if dest_folder.nil?
313
320
  dest_folder = File.join(folder, dest_folder)
@@ -320,7 +327,7 @@ module Aspera
320
327
  # Security: Detect basename collisions that could overwrite symlinks
321
328
  file_basename = File.basename(dest_file)
322
329
  if extracted_files.key?(file_basename)
323
- Log.log.warn{"Rejecting file with duplicate basename: #{entry_name} (basename: #{file_basename}, previous: #{extracted_files[file_basename]})"}
330
+ Log.log.warn { "Rejecting file with duplicate basename: #{entry_name} (basename: #{file_basename}, previous: #{extracted_files[file_basename]})" }
324
331
  next
325
332
  end
326
333
  extracted_files[file_basename] = entry_name
@@ -328,7 +335,7 @@ module Aspera
328
335
  if link_target.nil?
329
336
  # Security: Check if destination already exists
330
337
  if File.exist?(dest_file)
331
- Log.log.warn{"Rejecting write to existing file or link: #{dest_file}"}
338
+ Log.log.warn { "Rejecting write to existing file or link: #{dest_file}" }
332
339
  next
333
340
  end
334
341
  # Security: Verify the resolved path stays within installation boundary
@@ -338,17 +345,17 @@ module Aspera
338
345
  # Check where the file would resolve to (handles existing symlinks in path)
339
346
  resolved_dest = File.realpath(File.dirname(dest_file))
340
347
  unless resolved_dest.start_with?(install_boundary)
341
- Log.log.warn{"Rejecting file outside installation directory: #{dest_file} resolves to #{resolved_dest}"}
348
+ Log.log.warn { "Rejecting file outside installation directory: #{dest_file} resolves to #{resolved_dest}" }
342
349
  next
343
350
  end
344
351
  rescue Errno::ENOENT
345
352
  # Directory doesn't exist yet, verify the intended path
346
353
  unless dest_file.start_with?(folder)
347
- Log.log.warn{"Rejecting file with path outside installation directory: #{dest_file}"}
354
+ Log.log.warn { "Rejecting file with path outside installation directory: #{dest_file}" }
348
355
  next
349
356
  end
350
357
  end
351
- File.open(dest_file, 'wb'){ |output_stream| IO.copy_stream(entry_stream, output_stream)}
358
+ File.open(dest_file, 'wb') { |output_stream| IO.copy_stream(entry_stream, output_stream) }
352
359
  else
353
360
  # Security: Validate symlink target stays within installation boundary
354
361
  # Resolve the symlink target relative to its location
@@ -362,7 +369,7 @@ module Aspera
362
369
  end
363
370
  # Check if resolved target would be outside installation directory
364
371
  unless resolved_target.start_with?(install_boundary)
365
- Log.log.warn{"Rejecting symlink pointing outside installation directory: #{entry_name} -> #{link_target} (resolves to #{resolved_target})"}
372
+ Log.log.warn { "Rejecting symlink pointing outside installation directory: #{entry_name} -> #{link_target} (resolves to #{resolved_target})" }
366
373
  next
367
374
  end
368
375
  File.symlink(link_target, dest_file)
@@ -419,7 +426,7 @@ module Aspera
419
426
  # all executable files from SDK
420
427
  EXE_FILES = %i[ascp ascp4 async transferd].freeze
421
428
  # IDs of files present in SDK
422
- SDK_FILES = %i[ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key].unshift(*EXE_FILES).freeze
429
+ SDK_FILES = (EXE_FILES + %i[ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key]).freeze
423
430
  TRANSFERD_ARCHIVE_LOCATION_URL = 'https://ibm.biz/sdk_location'
424
431
  # filename for ascp with optional extension (Windows)
425
432
  private_constant :DEFAULT_ASPERA_CONF, :EXE_FILES, :SDK_FILES, :TRANSFERD_ARCHIVE_LOCATION_URL
@@ -305,8 +305,8 @@ module Aspera
305
305
  # @param name [String] Field name
306
306
  def field_snake_to_native(name)
307
307
  field = name.delete('_')
308
- result = PARAMETERS.find{ |w| w.casecmp?(field)}
309
- Aspera.assert(!result.nil?){"No such field: #{name}"}
308
+ result = PARAMETERS.find { |w| w.casecmp?(field) }
309
+ Aspera.assert(!result.nil?) { "No such field: #{name}" }
310
310
  result
311
311
  end
312
312
 
@@ -327,7 +327,7 @@ module Aspera
327
327
  # @return [String] frame to send on management port
328
328
  def command_to_stream(data)
329
329
  data
330
- .map{ |key, value| "#{field_snake_to_native(key)}: #{value}"}
330
+ .map { |key, value| "#{field_snake_to_native(key)}: #{value}" }
331
331
  .unshift(MGT_HEADER)
332
332
  .push(MGT_FRAME_SEPARATOR, '')
333
333
  .join("\n")
@@ -352,15 +352,15 @@ module Aspera
352
352
  # begin event
353
353
  @event_build = {}
354
354
  when /^([^:]+): (.*)$/
355
- Aspera.assert_type(@event_build, Hash){'mgt port: unexpected line: data without header'}
355
+ Aspera.assert_type(@event_build, Hash) { 'mgt port: unexpected line: data without header' }
356
356
  # event field
357
357
  @event_build[Regexp.last_match(1)] = Regexp.last_match(2)
358
358
  when MGT_FRAME_SEPARATOR
359
- Aspera.assert_type(@event_build, Hash){'mgt port: unexpected line: end frame without header'}
359
+ Aspera.assert_type(@event_build, Hash) { 'mgt port: unexpected line: end frame without header' }
360
360
  @last_event = @event_build
361
361
  @event_build = nil
362
362
  return @last_event
363
- else Aspera.error_unexpected_value(line){'mgt port'}
363
+ else Aspera.error_unexpected_value(line) { 'mgt port' }
364
364
  end
365
365
  return
366
366
  end