aspera-cli 4.27.2 → 4.27.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 (130) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +50 -0
  4. data/bin/ascli +2 -1
  5. data/docs/README.md +804 -746
  6. data/lib/aspera/agent/connect.rb +6 -4
  7. data/lib/aspera/agent/desktop.rb +2 -2
  8. data/lib/aspera/agent/direct.rb +3 -1
  9. data/lib/aspera/agent/node.rb +3 -3
  10. data/lib/aspera/api/alee.rb +1 -1
  11. data/lib/aspera/api/aoc.rb +14 -12
  12. data/lib/aspera/api/ats.rb +1 -1
  13. data/lib/aspera/api/cos_node.rb +2 -2
  14. data/lib/aspera/api/faspex.rb +9 -7
  15. data/lib/aspera/api/httpgw.rb +37 -33
  16. data/lib/aspera/api/node.rb +38 -33
  17. data/lib/aspera/ascmd.rb +3 -1
  18. data/lib/aspera/ascp/installation.rb +62 -27
  19. data/lib/aspera/ascp/management.rb +1 -0
  20. data/lib/aspera/assert.rb +4 -0
  21. data/lib/aspera/cli/ascp_actions.rb +20 -41
  22. data/lib/aspera/cli/async_transfer_store.rb +2 -2
  23. data/lib/aspera/cli/bootstrapper.rb +11 -15
  24. data/lib/aspera/cli/command_line.rb +252 -0
  25. data/lib/aspera/cli/command_registry.rb +149 -33
  26. data/lib/aspera/cli/command_spec.rb +103 -14
  27. data/lib/aspera/cli/completion/ascli.bash +12 -0
  28. data/lib/aspera/cli/completion/ascli.fish +16 -0
  29. data/lib/aspera/cli/completion/ascli.zsh +19 -0
  30. data/lib/aspera/cli/context.rb +3 -0
  31. data/lib/aspera/cli/deprecation.rb +37 -0
  32. data/lib/aspera/cli/extended_value.rb +2 -0
  33. data/lib/aspera/cli/formatter.rb +87 -75
  34. data/lib/aspera/cli/gem_checker.rb +1 -1
  35. data/lib/aspera/cli/hints.rb +7 -6
  36. data/lib/aspera/cli/http.rb +21 -21
  37. data/lib/aspera/cli/info.rb +3 -0
  38. data/lib/aspera/cli/mcp_tool.rb +47 -83
  39. data/lib/aspera/cli/option_declarator.rb +33 -42
  40. data/lib/aspera/cli/option_registry.rb +69 -0
  41. data/lib/aspera/cli/option_types.rb +103 -0
  42. data/lib/aspera/cli/option_value.rb +281 -0
  43. data/lib/aspera/cli/options.schema.yaml +38 -5
  44. data/lib/aspera/cli/parser.rb +307 -848
  45. data/lib/aspera/cli/plugins/alee.rb +7 -4
  46. data/lib/aspera/cli/plugins/aoc.rb +430 -380
  47. data/lib/aspera/cli/plugins/ats.rb +58 -73
  48. data/lib/aspera/cli/plugins/base.rb +190 -240
  49. data/lib/aspera/cli/plugins/basic_auth.rb +2 -10
  50. data/lib/aspera/cli/plugins/config.rb +244 -178
  51. data/lib/aspera/cli/plugins/console.rb +102 -38
  52. data/lib/aspera/cli/plugins/cos.rb +6 -23
  53. data/lib/aspera/cli/plugins/factory.rb +3 -0
  54. data/lib/aspera/cli/plugins/faspex5.rb +176 -173
  55. data/lib/aspera/cli/plugins/faspio.rb +5 -10
  56. data/lib/aspera/cli/plugins/httpgw.rb +8 -11
  57. data/lib/aspera/cli/plugins/mcp.rb +20 -55
  58. data/lib/aspera/cli/plugins/node.rb +277 -311
  59. data/lib/aspera/cli/plugins/orchestrator.rb +90 -77
  60. data/lib/aspera/cli/plugins/preview.rb +79 -90
  61. data/lib/aspera/cli/plugins/server.rb +76 -50
  62. data/lib/aspera/cli/plugins/shares.rb +68 -116
  63. data/lib/aspera/cli/preset_actions.rb +17 -10
  64. data/lib/aspera/cli/preset_manager.rb +12 -2
  65. data/lib/aspera/cli/prompt.rb +35 -0
  66. data/lib/aspera/cli/result.rb +13 -18
  67. data/lib/aspera/cli/runner.rb +31 -54
  68. data/lib/aspera/cli/special_values.rb +5 -0
  69. data/lib/aspera/cli/sync_actions.rb +41 -37
  70. data/lib/aspera/cli/terminal_formatter.rb +9 -3
  71. data/lib/aspera/cli/transfer_actions.rb +0 -6
  72. data/lib/aspera/cli/transfer_agent.rb +29 -35
  73. data/lib/aspera/cli/vault_manager.rb +0 -17
  74. data/lib/aspera/cli/version.rb +1 -1
  75. data/lib/aspera/cli/wizard.rb +4 -2
  76. data/lib/aspera/coverage.rb +1 -0
  77. data/lib/aspera/environment.rb +6 -0
  78. data/lib/aspera/faspex_gw.rb +2 -1
  79. data/lib/aspera/faspex_postproc.rb +1 -0
  80. data/lib/aspera/graphql.rb +5 -5
  81. data/lib/aspera/json_rpc/client.rb +5 -5
  82. data/lib/aspera/keychain/encrypted_hash.rb +1 -1
  83. data/lib/aspera/keychain/one_password_api.rb +1 -1
  84. data/lib/aspera/link_header.rb +2 -2
  85. data/lib/aspera/log.rb +22 -25
  86. data/lib/aspera/markdown.rb +2 -0
  87. data/lib/aspera/mime.rb +25 -0
  88. data/lib/aspera/node_simulator.rb +1 -0
  89. data/lib/aspera/oauth/base.rb +35 -25
  90. data/lib/aspera/oauth/factory.rb +1 -0
  91. data/lib/aspera/oauth/generic.rb +1 -1
  92. data/lib/aspera/oauth/jwt.rb +1 -1
  93. data/lib/aspera/oauth/web.rb +9 -8
  94. data/lib/aspera/preview/file_types.rb +4 -4
  95. data/lib/aspera/preview/generator.rb +7 -0
  96. data/lib/aspera/preview/options.rb +4 -4
  97. data/lib/aspera/preview/terminal.rb +4 -3
  98. data/lib/aspera/preview/utils.rb +9 -6
  99. data/lib/aspera/products/connect.rb +1 -1
  100. data/lib/aspera/rainbow.rb +7 -0
  101. data/lib/aspera/rest/aspera_errors.rb +60 -0
  102. data/lib/aspera/rest/call_error.rb +27 -0
  103. data/lib/aspera/rest/client.rb +514 -0
  104. data/lib/aspera/rest/error_analyzer.rb +113 -0
  105. data/lib/aspera/rest/list.rb +143 -0
  106. data/lib/aspera/rest/parameters.rb +55 -0
  107. data/lib/aspera/rest/util.rb +176 -0
  108. data/lib/aspera/rest.rb +7 -621
  109. data/lib/aspera/schema/IBM Aspera Console-enhanced.yaml +1125 -0
  110. data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +2395 -0
  111. data/lib/aspera/schema/documentation.rb +13 -3
  112. data/lib/aspera/schema/registry.rb +18 -1
  113. data/lib/aspera/schema/validator.rb +92 -0
  114. data/lib/aspera/secret_hider.rb +36 -25
  115. data/lib/aspera/string_ext.rb +15 -0
  116. data/lib/aspera/temp_file_manager.rb +6 -5
  117. data/lib/aspera/transfer/parameters.rb +2 -0
  118. data/lib/aspera/transfer/spec.rb +1 -0
  119. data/lib/aspera/uri_reader.rb +11 -11
  120. data/lib/aspera/web_auth/index.html +147 -0
  121. data/lib/aspera/web_auth/server.rb +81 -0
  122. data.tar.gz.sig +0 -0
  123. metadata +39 -7
  124. metadata.gz.sig +0 -0
  125. data/lib/aspera/colors.rb +0 -79
  126. data/lib/aspera/rest_call_error.rb +0 -25
  127. data/lib/aspera/rest_error_analyzer.rb +0 -111
  128. data/lib/aspera/rest_errors_aspera.rb +0 -58
  129. data/lib/aspera/rest_list.rb +0 -136
  130. data/lib/aspera/web_auth.rb +0 -211
@@ -9,6 +9,8 @@ require 'aspera/assert'
9
9
  require 'aspera/yaml'
10
10
  require 'digest'
11
11
  require 'yaml'
12
+ require 'aspera/rainbow'
13
+ using Rainbow
12
14
 
13
15
  module Aspera
14
16
  module Cli
@@ -113,8 +115,8 @@ module Aspera
113
115
  Log.log.error do
114
116
  "Default config name [#{default_name}] specified for plugin [#{plugin_name_sym}], but it does not exist in config file.\n" \
115
117
  "Please fix: either create preset:\n" \
116
- "#{Info::CMD_NAME} config id #{default_name} init @json:'{}'\n" \
117
- "or remove default:\n#{Info::CMD_NAME} config id default remove #{plugin_name_sym}"
118
+ "#{Info::CMD_NAME} config preset initialize #{default_name} @json:'{}'\n" \
119
+ "or remove default:\n#{Info::CMD_NAME} config preset unset #{Key::DEFAULTS} #{plugin_name_sym}"
118
120
  end
119
121
  raise Cli::Error, "No such preset: #{default_name}"
120
122
  end
@@ -192,10 +194,18 @@ module Aspera
192
194
  if result.nil?
193
195
  result = Key::GLOBAL
194
196
  set_key(Key::DEFAULTS, CONF_GLOBAL_SYM, result)
197
+ # A default declaration requires the preset to exist
198
+ @config_presets[result] ||= {}
195
199
  end
196
200
  result
197
201
  end
198
202
 
203
+ # @return [Boolean] true if `preset_name` is declared as the global default preset (does not create it)
204
+ def global_preset?(preset_name)
205
+ defaults = @config_presets[Key::DEFAULTS]
206
+ defaults.is_a?(Hash) && defaults[CONF_GLOBAL_SYM.to_s].eql?(preset_name)
207
+ end
208
+
199
209
  # Set param in the global defaults preset
200
210
  def set_global_default(key, value)
201
211
  set_key(global_default_preset, key, value)
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
+ require 'aspera/assert'
5
+
6
+ module Aspera
7
+ module Cli
8
+ # Console input
9
+ module Prompt
10
+ # Prompt user for console input
11
+ # @param prompt [String] prompt string to display
12
+ # @param sensitive [Boolean] whether to hide typed input
13
+ # @return [String] user input stripped of trailing newline
14
+ def prompt_user_input(prompt, sensitive: false)
15
+ return $stdin.getpass("#{prompt}> ") if sensitive
16
+ print("#{prompt}> ")
17
+ line = $stdin.gets
18
+ Aspera.assert_type(line, String) { 'Unexpected end of standard input' }
19
+ line.chomp
20
+ end
21
+
22
+ # Prompt user for input in a list of symbols
23
+ # @param prompt [String] prompt to display
24
+ # @param sym_list [Array] list of symbols to select from
25
+ # @return [Symbol] selected symbol
26
+ def prompt_user_input_in_list(prompt, sym_list)
27
+ loop do
28
+ input = prompt_user_input(prompt).to_sym
29
+ return input if sym_list.any? { |a| a.eql?(input) }
30
+ $stderr.puts("No such #{prompt}: #{input}, select one of: #{sym_list.join(', ')}") # rubocop:disable Style/StderrPuts
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -20,9 +20,11 @@ module Aspera
20
20
  class Result
21
21
  attr_reader :data, :fields
22
22
 
23
- # @param data [Object,nil] The result data
24
- # @param fields [Object] Specification of fields to include
23
+ # @param data [nil, Object] The result data
24
+ # @param fields [nil, Array<String>, Proc] Specification of fields to include
25
25
  def initialize(data: nil, fields: nil)
26
+ Aspera.assert_type(fields, NilClass, Array, Proc)
27
+ Aspera.assert_array_all(fields, String) if fields.is_a?(Array)
26
28
  @data = data
27
29
  @fields = fields
28
30
  end
@@ -39,18 +41,12 @@ module Aspera
39
41
  # Apply field filtering once for formats that need it
40
42
  filtered_data = formatter.filter_list_on_fields(@data)
41
43
  case formatter.format_type
42
- when :text
43
- formatter.display_message(:data, @data.to_s)
44
- when :nagios
45
- Nagios.process(@data)
46
- when :ruby
47
- formatter.display_message(:data, PP.pp(filtered_data, +''))
48
- when :json
49
- formatter.display_message(:data, JSON.generate(filtered_data))
50
- when :jsonpp
51
- formatter.display_message(:data, JSON.pretty_generate(filtered_data))
52
- when :yaml
53
- formatter.display_message(:data, YAML.dump(filtered_data))
44
+ when :nagios then Nagios.process(@data)
45
+ when :text then formatter.display_message(:data, @data.to_s)
46
+ when :ruby then formatter.display_message(:data, PP.pp(filtered_data, +''))
47
+ when :json then formatter.display_message(:data, JSON.generate(filtered_data))
48
+ when :jsonpp then formatter.display_message(:data, JSON.pretty_generate(filtered_data))
49
+ when :yaml then formatter.display_message(:data, YAML.dump(filtered_data))
54
50
  when :image
55
51
  if @data.nil?
56
52
  formatter.display_message(:data, formatter.special_format('null (no image)'))
@@ -82,8 +78,7 @@ module Aspera
82
78
  # here, data is the image blob
83
79
  formatter.display_message(:data, Preview::Terminal.build(data, **formatter.image_options))
84
80
  end
85
- else
86
- Aspera.error_unexpected_value(formatter.format_type) { 'format' }
81
+ else Aspera.error_unexpected_value(formatter.format_type) { 'format' }
87
82
  end
88
83
  end
89
84
  end
@@ -228,7 +223,7 @@ module Aspera
228
223
  end
229
224
 
230
225
  # Object list result (Array of Hash)
231
- # @note The +total+ parameter is used to display pagination information (e.g., "Items: 10/100")
226
+ # @note The `total` parameter is used to display pagination information (e.g., "Items: 10/100")
232
227
  class ObjectList < Result
233
228
  attr_reader :total
234
229
 
@@ -299,7 +294,7 @@ module Aspera
299
294
  # @param id_result [String] Key in the result Hash used as item identifier
300
295
  # @param fields [Object] Fields hint passed to the Result constructor (non-bulk only, when not :default)
301
296
  # @param bfail [Boolean] When true, re-raise errors; when false, capture them as a status string
302
- # @yieldparam item [Object] Each item in +items+
297
+ # @yieldparam item [Object] Each item in `items`
303
298
  # @yieldreturn [Hash, Array, nil] REST response; a Hash replaces the default id-keyed result
304
299
  # @return [Result::ObjectList, Result::SingleObject]
305
300
  def bulk(items, is_bulk:, command:, id_result: 'id', fields: :default, bfail: true)
@@ -166,7 +166,7 @@ module Aspera
166
166
  rescue Cli::SchemaRequest => e; exception_info = {e: e, t: 'Schema'}
167
167
  rescue Cli::Error => e; exception_info = {e: e, t: 'Tool', usage: true}
168
168
  rescue Transfer::Error => e; exception_info = {e: e, t: 'Transfer'}
169
- rescue RestCallError => e; exception_info = {e: e, t: 'Rest'}
169
+ rescue Rest::CallError => e; exception_info = {e: e, t: 'Rest'}
170
170
  rescue SocketError => e; exception_info = {e: e, t: 'Network'}
171
171
  rescue StandardError => e; exception_info = {e: e, t: "Other(#{e.class.name})", debug: true}
172
172
  rescue Interrupt => e; exception_info = {e: e, t: 'Interruption', debug: true}
@@ -228,29 +228,19 @@ module Aspera
228
228
  Result::Text.new(usage_text(plugin: plugin))
229
229
  end
230
230
 
231
- # Composite option handler for the `log` option (dot-notation sub-properties).
232
- # Supported sub-properties: +level+, +type+, +format+
233
- # @param _option_sym [Symbol] Option name (unused, always :log)
234
- # @param operation [Symbol] +:set+ or +:get+
235
- # @param value [Hash,nil] Hash of sub-properties to set (only for +:set+)
236
- def option_log(_option_sym, operation, value = nil)
237
- Aspera.assert_values(operation, %i[set get])
238
- case operation
239
- when :set
240
- Aspera.assert_type(value, Hash)
241
- value.each do |k, v|
242
- case k.to_sym
243
- when :level then Log.instance.level = v.to_sym
244
- when :type then Log.instance.logger_type = v.to_sym
245
- when :format then Log.instance.formatter = v
246
- when :secrets then SecretHider.instance.log_secrets = BoolValue.true?(v)
247
- else Aspera.error_unexpected_value(k) { 'log sub-option (level, type, format, secrets)' }
248
- end
231
+ # `on_set` callback of the `log` option (dot-notation sub-properties): `level`, `type`, `format`, `secrets`
232
+ # @param value [Hash] sub-properties to set
233
+ def option_log=(value)
234
+ Aspera.assert_type(value, Hash)
235
+ value.each do |k, v|
236
+ case k.to_sym
237
+ when :level then Log.instance.level = v.to_sym
238
+ when :type then Log.instance.logger_type = v.to_sym
239
+ when :format then Log.instance.formatter = v
240
+ when :secrets then SecretHider.instance.log_secrets = Parser.get_from_list(v.to_s, 'log secrets', BoolValue::ALL)
241
+ else Aspera.error_unexpected_value(k) { 'log sub-option (level, type, format, secrets)' }
249
242
  end
250
- when :get
251
- return {level: Log.instance.level, type: Log.instance.logger_type, format: Log.instance.formatter, secrets: SecretHider.instance.log_secrets}
252
243
  end
253
- nil
254
244
  end
255
245
 
256
246
  private
@@ -279,25 +269,11 @@ module Aspera
279
269
  registry = plugin.class.command_registry
280
270
  cmds = registry.children_of(path)
281
271
  label = plugin.class.name.split('::').last.downcase
282
- # Build label with positional argument slots inserted after each intermediate node
272
+ # Build label with positional argument slots inserted after each command
283
273
  # e.g. [:access_keys, :do, :download] -> "node access_keys do <access_key_id> download"
284
- path.each_with_index do |seg, i|
285
- label += " #{seg}"
286
- seg_path = path[0..i]
287
- seg_spec = registry[seg_path]
288
- if seg_spec && registry.children_of(seg_path).any? && seg_spec.arguments
289
- seg_spec.arguments.each do |arg_spec|
290
- label += " <#{arg_spec.name}>"
291
- end
292
- end
293
- end
294
- if cmds.none?
295
- leaf_spec = registry[path]
296
- leaf_spec&.arguments&.each do |arg_spec|
297
- argument = arg_spec.mandatory ? "<#{arg_spec.name}>" : "[#{arg_spec.name}]"
298
- argument += '...' if arg_spec.multiple
299
- label += " #{argument}"
300
- end
274
+ path.each_index do |i|
275
+ label += " #{path[i]}"
276
+ registry.arguments_at(path[0..i]).each { |arg_spec| label += " #{arg_spec.syntax}" }
301
277
  end
302
278
  if cmds.any?
303
279
  # Intermediate node: show current command + description, then list subcommands
@@ -314,7 +290,7 @@ module Aspera
314
290
  spec = registry[path]
315
291
  lines << "\nCOMMAND: #{label}"
316
292
  lines << " #{spec.description}" if spec&.description
317
- display_args = spec&.arguments || []
293
+ display_args = registry.arguments_at(path)
318
294
  # transfer_paths commands use --sources for the file list; default is positional args (@args)
319
295
  if spec&.transfer_paths
320
296
  file_desc = if spec.transfer_paths == :receive
@@ -395,7 +371,7 @@ module Aspera
395
371
  @context.config = Plugins::Config.new(context: @context)
396
372
  @context.man_header = true
397
373
  # Sync cache_tokens from Config into the OAuth persist_mgr (now that option is parsed)
398
- OAuth::Factory.instance.persist_mgr = @context.persistency if @context.config.option_cache_tokens
374
+ OAuth::Factory.instance.persist_mgr = @context.persistency if @context.options.get_option(:cache_tokens)
399
375
  # Email service: depends on options declared by Config
400
376
  @context.mailer = Mailer.new(@context.options, @context.main_folder)
401
377
  # Secret finder: depends on options (:secret) and presets, both set by Bootstrapper
@@ -450,32 +426,33 @@ module Aspera
450
426
  Log.log.debug('declare_global_options')
451
427
  @context.options.declare(:help, description: 'Show this message', allowed: Type::NONE, short: 'h') do
452
428
  @option_help = true
453
- @context.options.help_requested = true
429
+ @context.help_requested = true
454
430
  end
455
431
  @context.options.declare(:show_config, description: 'Display parameters used for the provided action', allowed: Type::NONE) { @option_show_config = true }
456
432
  @context.options.declare(:version, description: 'Display version', allowed: Type::NONE, short: 'v') { @context.formatter.display_message(:data, Cli::VERSION); Process.exit(0) } # rubocop:disable Style/Semicolon
457
433
  @context.options.declare(
458
434
  :ui, description: 'Method to start browser',
459
- allowed: USER_INTERFACES,
460
- handler: {o: Environment.instance, m: :url_method}
435
+ allowed: USER_INTERFACES, default: Environment.instance.url_method,
436
+ on_set: Environment.instance.method(:url_method=)
461
437
  )
462
438
  @context.options.declare(
463
439
  :invalid_characters, description: 'Replacement character and invalid filename characters',
464
- handler: {o: Environment.instance, m: :file_illegal_characters}
440
+ default: Environment.instance.file_illegal_characters,
441
+ on_set: Environment.instance.method(:file_illegal_characters=)
465
442
  )
466
- @context.options.declare(:log_level, description: 'Log level', allowed: Log::LEVELS, handler: {o: Log.instance, m: :level})
467
- @context.options.declare(:log_format, description: 'Log formatter', allowed: [Proc, Logger::Formatter, String], handler: {o: Log.instance, m: :formatter})
468
- @context.options.declare(:logger, description: 'Logging method', allowed: Log::LOG_TYPES, handler: {o: Log.instance, m: :logger_type})
469
- @context.options.declare(:log, description: 'Logging options (dot-notation: level, type, format, secrets)', handler: {o: self, m: :option_log}, schema: Schema::Registry::LOG_OPTIONS)
443
+ @context.options.declare(:log_level, description: 'Log level', allowed: Log::LEVELS, default: Log.instance.level, on_set: Log.instance.method(:level=))
444
+ @context.options.declare(:log_format, description: 'Log formatter', allowed: [Proc, Logger::Formatter, String], on_set: Log.instance.method(:formatter=))
445
+ @context.options.declare(:logger, description: 'Logging method', allowed: Log::LOG_TYPES, on_set: Log.instance.method(:logger_type=))
446
+ @context.options.declare(:log, description: 'Logging options (dot-notation: level, type, format, secrets)', on_set: method(:option_log=), schema: Schema::Registry::LOG_OPTIONS)
470
447
  @context.options.declare(:lock_port, description: 'Prevent dual execution of a command, e.g. in cron', allowed: Type::INTEGER)
471
448
  @context.options.declare(:once_only, description: 'Process only new items (some commands)', allowed: Type::BOOLEAN, default: false)
472
- @context.options.declare(:log_secrets, description: 'Show passwords in logs', allowed: Type::BOOLEAN, handler: {o: SecretHider.instance, m: :log_secrets})
473
- @context.options.declare(:clean_temp, description: 'Cleanup temporary files on exit', allowed: Type::BOOLEAN, handler: {o: TempFileManager.instance, m: :cleanup_on_exit})
474
- @context.options.declare(:temp_folder, description: 'Temporary folder', handler: {o: TempFileManager.instance, m: :global_temp})
449
+ @context.options.declare(:log_secrets, description: 'Show passwords in logs', allowed: Type::BOOLEAN, default: SecretHider.instance.log_secrets, on_set: SecretHider.instance.method(:log_secrets=))
450
+ @context.options.declare(:clean_temp, description: 'Cleanup temporary files on exit', allowed: Type::BOOLEAN, default: TempFileManager.instance.cleanup_on_exit, on_set: TempFileManager.instance.method(:cleanup_on_exit=))
451
+ @context.options.declare(:temp_folder, description: 'Temporary folder', default: TempFileManager.instance.global_temp, on_set: TempFileManager.instance.method(:global_temp=))
475
452
  @context.options.declare(:pid_file, description: 'Write process identifier to file, delete on exit')
476
453
  @context.options.declare(
477
454
  :parser, description: 'Default parser for structured parameters and options',
478
- handler: {o: ExtendedValue.instance, m: :default_decoder},
455
+ on_set: ExtendedValue.instance.method(:default_decoder=),
479
456
  allowed: ExtendedValue::DEFAULT_DECODERS,
480
457
  default: ExtendedValue::DEFAULT_DECODERS.first
481
458
  )
@@ -5,10 +5,15 @@ module Aspera
5
5
  # base class for plugins modules
6
6
  module SpecialValues
7
7
  # special values
8
+ # `INIT` to unitialize with all current IDs
8
9
  INIT = 'INIT'
10
+ # `ALL` means "all ids"
9
11
  ALL = 'ALL'
12
+ # `DEF` for the default list
10
13
  DEF = 'DEF'
14
+ # `END` of arguments
11
15
  EOA = 'END'
16
+ # `LATEST`
12
17
  LATEST = 'LATEST'
13
18
  end
14
19
  end
@@ -16,7 +16,7 @@ module Aspera
16
16
  %w[Syncd Error Confl Pconf] +
17
17
  (23..24).map { |i| "P(#{i})" }).freeze
18
18
  # Positional arguments shared by sync transfer commands (push/pull/bidi) and sync admin commands.
19
- PATH_AND_INFO_ARGS = [{name: :path, type: String}, {name: :sync_info, type: Hash, mandatory: false, default: {}}].freeze
19
+ PATH_AND_INFO_ARGS = [{name: :path}, {name: :sync_info, type: Hash, mandatory: false, default: {}}].freeze
20
20
  # Names of the leaf commands registered under any `sync admin` node.
21
21
  ADMIN_COMMANDS = %i[find status meta counters file_info overview query].freeze
22
22
  # When a plugin class includes SyncActions, register the :sql option
@@ -32,13 +32,46 @@ module Aspera
32
32
  # @param admin_path [Symbol, Array<Symbol>] path relative to current scope, e.g. :admin
33
33
  def register_sync_admin_commands(base, admin_path)
34
34
  base.commands_under(admin_path) do
35
- base.command(:find, description: 'Find sync database files', arguments: [{name: :path, type: String}], action: :action_sync_admin_find)
36
- base.command(:status, description: 'Show sync session status', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_status)
37
- base.command(:meta, description: 'Show sync session metadata', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_meta)
38
- base.command(:counters, description: 'Show sync counters', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_counters)
39
- base.command(:file_info, description: 'Show per-file sync state', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_file_info)
40
- base.command(:overview, description: 'Show sync database overview', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_overview)
41
- base.command(:query, description: 'Execute a raw SQL query', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_query)
35
+ base.command(
36
+ :find, description: 'Find sync database files', arguments: [{name: :path}],
37
+ action: lambda do |path:, **|
38
+ dbs = Sync::Operations.list_db_files(path)
39
+ Result::ObjectList.new(dbs.keys.map { |n| {name: n, path: dbs[n]} })
40
+ end
41
+ )
42
+ base.command(
43
+ :status, description: 'Show sync session status', arguments: PATH_AND_INFO_ARGS,
44
+ action: ->(path:, sync_info: {}, **) { Result::SingleObject.new(Sync::Operations.admin_status(async_info_from_args(path: path, sync_info: sync_info))) }
45
+ )
46
+ base.command(
47
+ :meta, description: 'Show sync session metadata', arguments: PATH_AND_INFO_ARGS,
48
+ action: lambda do |path:, sync_info: {}, **|
49
+ require 'aspera/sync/database'
50
+ Result::SingleObject.new(db_from_args(path: path, sync_info: sync_info).meta(options.get_option(:sql)))
51
+ end
52
+ )
53
+ base.command(
54
+ :counters, description: 'Show sync counters', arguments: PATH_AND_INFO_ARGS,
55
+ action: lambda do |path:, sync_info: {}, **|
56
+ require 'aspera/sync/database'
57
+ Result::SingleObject.new(db_from_args(path: path, sync_info: sync_info).counters(options.get_option(:sql)))
58
+ end
59
+ )
60
+ base.command(:file_info, description: 'Show per-file sync state', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_file_info)
61
+ base.command(
62
+ :overview, description: 'Show sync database overview', arguments: PATH_AND_INFO_ARGS,
63
+ action: lambda do |path:, sync_info: {}, **|
64
+ require 'aspera/sync/database'
65
+ Result::ObjectList.new(db_from_args(path: path, sync_info: sync_info).overview, fields: %w[table name type])
66
+ end
67
+ )
68
+ base.command(
69
+ :query, description: 'Execute a raw SQL query', arguments: PATH_AND_INFO_ARGS,
70
+ action: lambda do |path:, sync_info: {}, **|
71
+ require 'aspera/sync/database'
72
+ Result.auto(db_from_args(path: path, sync_info: sync_info).execute(options.get_option(:sql, mandatory: true)))
73
+ end
74
+ )
42
75
  end
43
76
  end
44
77
  end
@@ -119,25 +152,6 @@ module Aspera
119
152
  Sync::Database.new(Sync::Operations.session_db_file(sync_info))
120
153
  end
121
154
 
122
- def action_sync_admin_status(path:, sync_info: {}, **)
123
- Result::SingleObject.new(Sync::Operations.admin_status(async_info_from_args(path: path, sync_info: sync_info)))
124
- end
125
-
126
- def action_sync_admin_find(path:, **)
127
- dbs = Sync::Operations.list_db_files(path)
128
- Result::ObjectList.new(dbs.keys.map { |n| {name: n, path: dbs[n]} })
129
- end
130
-
131
- def action_sync_admin_meta(path:, sync_info: {}, **)
132
- require 'aspera/sync/database'
133
- Result::SingleObject.new(db_from_args(path: path, sync_info: sync_info).meta(options.get_option(:sql)))
134
- end
135
-
136
- def action_sync_admin_counters(path:, sync_info: {}, **)
137
- require 'aspera/sync/database'
138
- Result::SingleObject.new(db_from_args(path: path, sync_info: sync_info).counters(options.get_option(:sql)))
139
- end
140
-
141
155
  def action_sync_admin_file_info(path:, sync_info: {}, **)
142
156
  require 'aspera/sync/database'
143
157
  result = db_from_args(path: path, sync_info: sync_info).file_info(options.get_option(:sql))
@@ -147,16 +161,6 @@ module Aspera
147
161
  Result::ObjectList.new(result, fields: %w[sstate record_id f_meta_path message])
148
162
  end
149
163
 
150
- def action_sync_admin_overview(path:, sync_info: {}, **)
151
- require 'aspera/sync/database'
152
- Result::ObjectList.new(db_from_args(path: path, sync_info: sync_info).overview, fields: %w[table name type])
153
- end
154
-
155
- def action_sync_admin_query(path:, sync_info: {}, **)
156
- require 'aspera/sync/database'
157
- Result.auto(db_from_args(path: path, sync_info: sync_info).execute(options.get_option(:sql, mandatory: true)))
158
- end
159
-
160
164
  # Execute a sync transfer for a given direction.
161
165
  # @param direction [Symbol] one of Sync::Operations::DIRECTIONS (:push, :pull, :bidi)
162
166
  # @param path [String] local or remote path (depends on direction)
@@ -4,6 +4,8 @@ require 'aspera/environment'
4
4
  require 'aspera/formatter_interface'
5
5
  require 'aspera/markdown'
6
6
  require 'aspera/assert'
7
+ require 'aspera/rainbow'
8
+ using Rainbow
7
9
 
8
10
  module Aspera
9
11
  module Cli
@@ -11,9 +13,13 @@ module Aspera
11
13
  # @see FormatterInterface
12
14
  # @see MarkdownFormatter (in build/lib/doc_helper.rb)
13
15
  module TerminalFormatter
14
- HINT = 'HINT:'.bg_green.gray.blink.freeze
15
16
  include FormatterInterface
16
17
 
18
+ # Prefix for hint messages (evaluated on each call, as colors can be enabled or disabled by option)
19
+ def hint
20
+ 'HINT:'.bg(:green).white.blink
21
+ end
22
+
17
23
  # Format boolean with colored symbol (+/- or Y/ )
18
24
  def tick(yes)
19
25
  result =
@@ -29,7 +35,7 @@ module Aspera
29
35
  # Format special values with colors (dim for empty, reverse for others)
30
36
  def special_format(what)
31
37
  result = "<#{what}>"
32
- return %w[null empty].any? { |s| what.include?(s) } ? result.dim : result.reverse_color
38
+ return %w[null empty].any? { |s| what.include?(s) } ? result.faint : result.inverse
33
39
  end
34
40
 
35
41
  # Prepare table row for terminal display (word wrap arrays)
@@ -59,7 +65,7 @@ module Aspera
59
65
  end
60
66
  end
61
67
 
62
- module_function :tick, :special_format, :check_row, :markdown_text
68
+ module_function :hint, :tick, :special_format, :check_row, :markdown_text
63
69
  end
64
70
  end
65
71
  end
@@ -36,12 +36,6 @@ module Aspera
36
36
  Result::SingleObject.new(entry)
37
37
  end
38
38
 
39
- # List all persisted async transfer entries.
40
- def action_transfer_list(**)
41
- rows = async_transfer_store.list
42
- Result::ObjectList.new(rows, fields: %w[job_id agent_type status started_at ended_at bytes_transferred transfer_id])
43
- end
44
-
45
39
  TERMINAL_STATUSES = %w[completed failed cancelled].freeze
46
40
  private_constant :TERMINAL_STATUSES
47
41
 
@@ -32,9 +32,15 @@ module Aspera
32
32
  <%=ts.to_yaml%>
33
33
  END_OF_TEMPLATE
34
34
  CP4I_REMOTE_HOST_LB = 'N/A'
35
+ # Transfer spec parameters overridden by option `ts`
36
+ TS_DEFAULTS = {
37
+ 'create_dir' => true,
38
+ 'resume_policy' => 'sparse_csum'
39
+ }.freeze
35
40
  private_constant :FILE_LIST_FROM_TRANSFER_SPEC,
36
41
  :FILE_LIST_OPTIONS,
37
- :DEFAULT_TRANSFER_NOTIFY_TEMPLATE
42
+ :DEFAULT_TRANSFER_NOTIFY_TEMPLATE,
43
+ :TS_DEFAULTS
38
44
 
39
45
  extend OptionDeclarator
40
46
 
@@ -42,8 +48,8 @@ module Aspera
42
48
  option :to_folder, description: 'Destination folder for transferred files'
43
49
  option :sources, description: "How list of transferred files is provided (#{FILE_LIST_OPTIONS.join(',')})", default: FILE_LIST_FROM_ARGS
44
50
  option :src_type, description: 'Type of file list', allowed: %i[list pair], default: :list
45
- option :transfer, description: 'Transfer agent type, or agent parameters with optional agent key', allowed: [Hash, String], schema: Schema::Registry::TRANSFER_AGENT_OPTIONS
46
- option :transfer_info, description: 'Parameters for transfer agent', deprecation: 'use --transfer instead', schema: Schema::Registry::TRANSFER_AGENT_OPTIONS
51
+ option :transfer, description: 'Transfer agent type, or agent parameters with optional agent key', allowed: [Hash, String], shorthand: 'agent', schema: Schema::Registry::TRANSFER_AGENT_OPTIONS
52
+ option :transfer_info, description: 'Parameters for transfer agent', deprecation: {last: '4.26.2', message: 'use --transfer instead'}, schema: Schema::Registry::TRANSFER_AGENT_OPTIONS
47
53
 
48
54
  # @param context [Context] Application context
49
55
  def initialize(context)
@@ -51,12 +57,7 @@ module Aspera
51
57
  Aspera.assert_type(context.options, Parser) { 'context.options' }
52
58
  @context = context
53
59
  # Command line can override transfer spec
54
- @user_transfer_spec = {
55
- 'create_dir' => true,
56
- 'resume_policy' => 'sparse_csum'
57
- }
58
- # options for transfer agent (agent type + agent-specific parameters)
59
- @transfer_options = {}
60
+ @user_transfer_spec = TS_DEFAULTS.dup
60
61
  # the currently selected transfer agent
61
62
  @agent = nil
62
63
  # source/destination pair, like "paths" of transfer spec
@@ -64,9 +65,7 @@ module Aspera
64
65
  # HTTPGW URL provided by webapp
65
66
  @httpgw_url_lambda = nil
66
67
  self.class.declare_options(@context.options)
67
- @context.options.set_handler(:ts, object: self, method: :user_transfer_spec)
68
- @context.options.set_handler(:transfer, object: self, method: :option_transfer)
69
- @context.options.set_handler(:transfer_info, object: self, method: :transfer_options)
68
+ @context.options.on_set(:ts, method(:user_transfer_spec=))
70
69
  @context.options.parse_options!
71
70
  @notification_cb = nil
72
71
  if !@context.options.get_option(:notify_to).nil?
@@ -80,22 +79,17 @@ module Aspera
80
79
  end
81
80
  end
82
81
 
83
- attr_accessor :user_transfer_spec, :transfer_options
82
+ attr_reader :user_transfer_spec
84
83
 
85
- # Composite option handler for :transfer
86
- # String value: shorthand for agent type, stored as {'agent' => value}
87
- # Hash value: merged into @transfer_options (may include 'agent' key)
88
- def option_transfer(_option_sym, operation, value = nil)
89
- Aspera.assert_values(operation, %i[set get])
90
- case operation
91
- when :set
92
- value = {'agent' => value} if value.is_a?(String)
93
- Aspera.assert_type(value, Hash)
94
- @transfer_options = @transfer_options.deep_merge(value)
95
- when :get
96
- return @transfer_options
97
- end
98
- nil
84
+ # `on_set` callback of option `ts`: values override defaults
85
+ # @param value [Hash] transfer spec parameters
86
+ def user_transfer_spec=(value)
87
+ @user_transfer_spec = TS_DEFAULTS.merge(value)
88
+ end
89
+
90
+ # @return [Hash] agent type (key `agent`) and agent parameters, from options `transfer` and deprecated `transfer_info`
91
+ def transfer_options
92
+ @context.options.get_option(:transfer_info).deep_merge(@context.options.get_option(:transfer))
99
93
  end
100
94
 
101
95
  def agent_instance=(instance)
@@ -106,10 +100,10 @@ module Aspera
106
100
  def agent_instance
107
101
  return @agent unless @agent.nil?
108
102
  # agent type: from composite option 'agent' key, default :direct
109
- raw_type = @transfer_options['agent'] || :direct
103
+ raw_type = transfer_options['agent'] || :direct
110
104
  agent_type = Parser.get_from_list(raw_type.to_s, 'transfer agent', Agent::Factory::ALL.keys)
111
105
  # set keys as symbols, strip internal keys not forwarded to the agent constructor
112
- agent_options = @transfer_options.except('agent', 'asynchronous').symbolize_keys
106
+ agent_options = transfer_options.except('agent', 'asynchronous').symbolize_keys
113
107
  agent_options[:progress] = @context.progress_bar
114
108
  agent_options[:config_dir] = @context.main_folder
115
109
  # special cases
@@ -210,8 +204,8 @@ module Aspera
210
204
  when FILE_LIST_FROM_TRANSFER_SPEC
211
205
  Log.log.debug('assume list provided in transfer spec')
212
206
  special_case_direct_with_list =
213
- (@transfer_options['agent'] || :direct).to_sym.eql?(:direct) &&
214
- Transfer::Parameters.ascp_args_file_list?(@transfer_options['ascp_args'])
207
+ (transfer_options['agent'] || :direct).to_sym.eql?(:direct) &&
208
+ Transfer::Parameters.ascp_args_file_list?(transfer_options['ascp_args'])
215
209
  Aspera.assert(!@transfer_paths.nil? || special_case_direct_with_list, type: Cli::BadArgument) { 'transfer spec on command line must have sources' }
216
210
  # can be nil
217
211
  @transfer_paths
@@ -227,7 +221,7 @@ module Aspera
227
221
 
228
222
  # Start a transfer and wait for completion, plugins shall use this method
229
223
  # @param transfer_spec [Hash] transfer specification
230
- # @param rest_token [Rest, nil] if oauth token regeneration supported
224
+ # @param rest_token [Rest::Client, nil] if oauth token regeneration supported
231
225
  def start(transfer_spec, rest_token: nil)
232
226
  # check parameters
233
227
  Aspera.assert_type(transfer_spec, Hash) { 'transfer_spec' }
@@ -264,13 +258,13 @@ module Aspera
264
258
  # create transfer agent
265
259
  agent_instance.start_transfer(transfer_spec, token_regenerator: rest_token)
266
260
  # --- async mode ---
267
- if @transfer_options['asynchronous']
268
- agent_type = (@transfer_options['agent'] || 'direct').to_s
261
+ if transfer_options['asynchronous']
262
+ agent_type = (transfer_options['agent'] || 'direct').to_s
269
263
  # In-process agents (direct, httpgw) expose last_job_id directly.
270
264
  # Remote-daemon agents do not override last_job_id (returns nil): generate a UUID.
271
265
  job_id = agent_instance.last_job_id || SecureRandom.uuid
272
266
  # Base agent_params from transfer options (strip internal keys)
273
- agent_params = @transfer_options.except('agent', 'asynchronous')
267
+ agent_params = transfer_options.except('agent', 'asynchronous')
274
268
  # For daemon agents: capture any runtime-resolved connection detail
275
269
  case agent_type
276
270
  when 'desktop'
@@ -10,17 +10,6 @@ module Aspera
10
10
  # Mixin providing vault/keychain functionality to Plugin::Config.
11
11
  # Depends on `options` and `context.main_folder` being available in the including class.
12
12
  module VaultManager
13
- def action_vault_show(label:, id: nil, **)
14
- v = vault_required
15
- kwargs = id && v.method(:get).parameters.any? { |_t, n| n == :id } ? {id: id} : {}
16
- Result::SingleObject.new(v.get(label: label, **kwargs))
17
- end
18
-
19
- def action_vault_create(info:, **)
20
- vault_required.set(info.symbolize_keys)
21
- Result::Status.new('Secret added')
22
- end
23
-
24
13
  # Import secrets from a JSON array; skips entries missing :label
25
14
  def action_vault_import(secrets:, **)
26
15
  bulk_result(secrets, command: :import, id_result: 'label') do |entry|
@@ -36,12 +25,6 @@ module Aspera
36
25
  Result::Status.new("Secret deleted: #{label}")
37
26
  end
38
27
 
39
- def action_vault_password(new_password:, **)
40
- Aspera.assert(vault_required.respond_to?(:change_password), 'Vault does not support password change')
41
- vault_required.change_password(new_password)
42
- Result::Status.new('Vault password updated')
43
- end
44
-
45
28
  # @return [Keychain::Base] vault instance, raises if not configured
46
29
  def vault_required
47
30
  Aspera.assert(!vault.nil?, type: Cli::BadArgument) { 'Missing mandatory option: vault' }
@@ -4,6 +4,6 @@ module Aspera
4
4
  module Cli
5
5
  # For beta add extension : .beta1
6
6
  # For dev version add extension : .pre
7
- VERSION = '4.27.2'
7
+ VERSION = '4.27.3'
8
8
  end
9
9
  end