aspera-cli 4.26.0 → 4.26.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 (99) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +36 -4
  4. data/CONTRIBUTING.md +2 -5
  5. data/bin/ascli +2 -2
  6. data/lib/aspera/agent/direct.rb +1 -1
  7. data/lib/aspera/agent/factory.rb +4 -0
  8. data/lib/aspera/agent/httpgw.rb +1 -1
  9. data/lib/aspera/agent/node.rb +2 -2
  10. data/lib/aspera/api/aoc.rb +45 -37
  11. data/lib/aspera/api/cos_node.rb +1 -1
  12. data/lib/aspera/api/faspex.rb +7 -6
  13. data/lib/aspera/api/node.rb +9 -9
  14. data/lib/aspera/ascmd.rb +4 -4
  15. data/lib/aspera/ascp/installation.rb +13 -6
  16. data/lib/aspera/assert.rb +17 -13
  17. data/lib/aspera/cli/context.rb +58 -0
  18. data/lib/aspera/cli/extended_value.rb +9 -4
  19. data/lib/aspera/cli/formatter.rb +91 -187
  20. data/lib/aspera/cli/http.rb +167 -0
  21. data/lib/aspera/cli/manager.rb +132 -77
  22. data/lib/aspera/cli/options.schema.yaml +82 -0
  23. data/lib/aspera/cli/plugins/alee.rb +2 -2
  24. data/lib/aspera/cli/plugins/aoc.rb +113 -77
  25. data/lib/aspera/cli/plugins/ats.rb +16 -16
  26. data/lib/aspera/cli/plugins/base.rb +64 -49
  27. data/lib/aspera/cli/plugins/config.rb +172 -444
  28. data/lib/aspera/cli/plugins/console.rb +7 -7
  29. data/lib/aspera/cli/plugins/cos.rb +2 -2
  30. data/lib/aspera/cli/plugins/factory.rb +3 -0
  31. data/lib/aspera/cli/plugins/faspex.rb +22 -22
  32. data/lib/aspera/cli/plugins/faspex5.rb +68 -49
  33. data/lib/aspera/cli/plugins/faspio.rb +1 -1
  34. data/lib/aspera/cli/plugins/httpgw.rb +2 -2
  35. data/lib/aspera/cli/plugins/node.rb +80 -80
  36. data/lib/aspera/cli/plugins/orchestrator.rb +21 -26
  37. data/lib/aspera/cli/plugins/preview.rb +9 -9
  38. data/lib/aspera/cli/plugins/server.rb +7 -7
  39. data/lib/aspera/cli/plugins/shares.rb +2 -2
  40. data/lib/aspera/cli/preset_manager.rb +235 -0
  41. data/lib/aspera/cli/result.rb +310 -0
  42. data/lib/aspera/cli/{main.rb → runner.rb} +48 -130
  43. data/lib/aspera/cli/sync_actions.rb +15 -11
  44. data/lib/aspera/cli/terminal_formatter.rb +65 -0
  45. data/lib/aspera/cli/transfer_agent.rb +17 -15
  46. data/lib/aspera/cli/transfer_progress.rb +6 -6
  47. data/lib/aspera/cli/version.rb +1 -1
  48. data/lib/aspera/cli/wizard.rb +5 -6
  49. data/lib/aspera/command_line_builder.rb +22 -18
  50. data/lib/aspera/data_repository.rb +4 -0
  51. data/lib/aspera/dot_container.rb +1 -1
  52. data/lib/aspera/environment.rb +25 -6
  53. data/lib/aspera/faspex_gw.rb +1 -1
  54. data/lib/aspera/formatter_interface.rb +14 -0
  55. data/lib/aspera/hash_ext.rb +6 -0
  56. data/lib/aspera/json_rpc.rb +6 -5
  57. data/lib/aspera/keychain/base.rb +1 -1
  58. data/lib/aspera/keychain/encrypted_hash.rb +1 -1
  59. data/lib/aspera/keychain/factory.rb +1 -1
  60. data/lib/aspera/keychain/macos_security.rb +1 -1
  61. data/lib/aspera/log.rb +10 -5
  62. data/lib/aspera/markdown.rb +4 -1
  63. data/lib/aspera/nagios.rb +2 -2
  64. data/lib/aspera/oauth/base.rb +5 -5
  65. data/lib/aspera/oauth/boot.rb +43 -0
  66. data/lib/aspera/oauth/factory.rb +38 -14
  67. data/lib/aspera/oauth/web.rb +2 -2
  68. data/lib/aspera/oauth.rb +1 -0
  69. data/lib/aspera/persistency_action_once.rb +2 -2
  70. data/lib/aspera/preview/file_types.rb +4 -0
  71. data/lib/aspera/products/connect.rb +3 -0
  72. data/lib/aspera/products/transferd.rb +2 -2
  73. data/lib/aspera/proxy_auto_config.rb +6 -3
  74. data/lib/aspera/rest.rb +9 -5
  75. data/lib/aspera/rest_error_analyzer.rb +4 -0
  76. data/lib/aspera/rest_list.rb +10 -3
  77. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +62811 -0
  78. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +9637 -0
  79. data/lib/aspera/schema/async_tables.yaml +361 -0
  80. data/lib/aspera/schema/documentation.rb +107 -0
  81. data/lib/aspera/schema/reader.rb +75 -0
  82. data/lib/aspera/schema/registry.rb +70 -0
  83. data/lib/aspera/secret_hider.rb +4 -0
  84. data/lib/aspera/sync/conf.schema.yaml +0 -26
  85. data/lib/aspera/sync/database.rb +14 -10
  86. data/lib/aspera/sync/operations.rb +14 -10
  87. data/lib/aspera/temp_file_manager.rb +4 -0
  88. data/lib/aspera/transfer/faux_file.rb +1 -1
  89. data/lib/aspera/transfer/parameters.rb +4 -4
  90. data/lib/aspera/transfer/resumer.rb +2 -2
  91. data/lib/aspera/transfer/spec.rb +3 -3
  92. data/lib/aspera/transfer/spec.schema.yaml +18 -9
  93. data/lib/aspera/transfer/uri.rb +1 -1
  94. data/lib/aspera/uri_reader.rb +1 -1
  95. data/lib/aspera/yaml.rb +4 -2
  96. data.tar.gz.sig +0 -0
  97. metadata +28 -15
  98. metadata.gz.sig +1 -1
  99. data/lib/aspera/transfer/spec_doc.rb +0 -76
@@ -1,63 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'aspera/cli/context'
3
4
  require 'aspera/cli/manager'
4
5
  require 'aspera/cli/formatter'
6
+ require 'aspera/cli/plugins/factory'
5
7
  require 'aspera/cli/plugins/config'
6
8
  require 'aspera/cli/extended_value'
7
- require 'aspera/cli/plugins/factory'
8
9
  require 'aspera/cli/transfer_agent'
9
10
  require 'aspera/cli/version'
10
11
  require 'aspera/cli/info'
11
12
  require 'aspera/cli/hints'
13
+ require 'aspera/cli/result'
12
14
  require 'aspera/secret_hider'
13
15
  require 'aspera/log'
14
16
  require 'aspera/assert'
17
+ require 'aspera/schema/documentation'
18
+ require 'aspera/schema/registry'
15
19
  require 'net/ssh/errors'
16
20
  require 'openssl'
17
21
 
18
22
  module Aspera
19
23
  module Cli
20
- # Global objects shared with plugins
21
- class Context
22
- # @!attribute [rw] options
23
- # @return [Manager] the command line options manager
24
- # @!attribute [rw] transfer
25
- # @return [TransferAgent] the transfer agent, used by transfer plugins
26
- # @!attribute [rw] config
27
- # @return [Plugins::Config] the configuration plugin, used by plugins to get configuration values and presets
28
- # @!attribute [rw] formatter
29
- # @return [Formatter] the formatter, used by plugins to display results and messages
30
- # @!attribute [rw] persistency
31
- # @return [Object] # whatever the type is
32
- # @!attribute [rw] man_header
33
- # @return [Boolean] whether to display the manual header in plugin help
34
- MEMBERS = %i[options transfer config formatter persistency man_header].freeze
35
- attr_accessor(*MEMBERS)
36
-
37
- # Initialize all members to nil, so that they are defined and can be validated later
38
- def initialize
39
- MEMBERS.each{ |i| instance_variable_set(:"@#{i}", nil)}
40
- end
41
-
42
- # Validate that all members are set, raise exception if not
43
- def validate
44
- MEMBERS.each do |i|
45
- Aspera.assert(instance_variable_defined?(:"@#{i}"))
46
- Aspera.assert(!instance_variable_get(:"@#{i}").nil?)
47
- end
48
- end
49
-
50
- def only_manual?
51
- transfer.eql?(:only_manual)
52
- end
53
-
54
- def only_manual!
55
- @transfer = :only_manual
56
- end
57
- end
58
-
59
24
  # The main CLI class
60
- class Main
25
+ class Runner
61
26
  # Plugins store transfer result using this key and use result_transfer_multiple()
62
27
  STATUS_FIELD = 'status'
63
28
  COMMAND_CONFIG = :config
@@ -69,35 +34,19 @@ module Aspera
69
34
  private_constant :COMMAND_CONFIG, :COMMAND_HELP, :SCALAR_TYPES, :USER_INTERFACES
70
35
 
71
36
  class << self
72
- def result_special(how); {type: :special, data: how}; end
73
-
74
- # Expect some list, but nothing to display
75
- def result_empty; result_special(:empty); end
76
-
77
- # Nothing expected
78
- def result_nothing; result_special(:nothing); end
79
-
80
- # Result is some status, such as "complete", "deleted"...
81
- # @param status [String] The status
82
- def result_status(status); return {type: :status, data: status}; end
83
-
84
- # Text result coming from command result
85
- def result_text(data); return {type: :text, data: data}; end
86
-
87
- def result_success; return result_status('complete'); end
88
-
89
37
  # Process statuses of finished transfer sessions
90
- # @raise exception if there is one error
91
- # else returns an empty status
38
+ # @param statuses [Array] array of transfer session statuses
39
+ # @raise [Symbol] exception if there is one error
40
+ # @return [Result] empty status result if all transfers succeeded
92
41
  def result_transfer(statuses)
93
42
  worst = TransferAgent.session_status(statuses)
94
43
  raise worst unless worst.eql?(:success)
95
- return Main.result_nothing
44
+ return Result::Nothing.new
96
45
  end
97
46
 
98
47
  # Used when one command executes several transfer jobs (each job being possibly multi session)
99
48
  # @param status_table [Array] [{STATUS_FIELD=>[status array],...},...]
100
- # @return a status object suitable as command result
49
+ # @return [Result] a status object suitable as command result
101
50
  # Each element has a key STATUS_FIELD which contains the result of possibly multiple sessions
102
51
  def result_transfer_multiple(status_table)
103
52
  global_status = :success
@@ -108,68 +57,25 @@ module Aspera
108
57
  item[STATUS_FIELD] = item[STATUS_FIELD].join(',')
109
58
  end
110
59
  raise global_status unless global_status.eql?(:success)
111
- return result_object_list(status_table)
112
- end
113
-
114
- # Display image for that URL or directly blob
115
- #
116
- # @param url_or_blob [String] URL or blob to display as image
117
- def result_image(url_or_blob)
118
- return {type: :image, data: url_or_blob}
119
- end
120
-
121
- # A single object, must be Hash
122
- def result_single_object(data, fields: nil)
123
- return {type: :single_object, data: data, fields: fields}
124
- end
125
-
126
- # An Array of Hash
127
- def result_object_list(data, fields: nil, total: nil)
128
- return {type: :object_list, data: data, fields: fields, total: total}
129
- end
130
-
131
- # A list of values
132
- #
133
- # @param data [Array] The list of values
134
- # @param name [String] The name of the list (used for display)
135
- def result_value_list(data, name: 'id')
136
- Aspera.assert_type(data, Array)
137
- Aspera.assert_type(name, String)
138
- return {type: :value_list, data: data, name: name}
139
- end
140
-
141
- # Determines type of result based on data
142
- def result_auto(data)
143
- case data
144
- when NilClass
145
- return result_special(:null)
146
- when Hash
147
- return result_single_object(data)
148
- when Array
149
- all_types = data.map(&:class).uniq
150
- return result_object_list(data) if all_types.eql?([Hash])
151
- unsupported_types = all_types - SCALAR_TYPES
152
- return result_value_list(data, name: 'list') if unsupported_types.empty?
153
- Aspera.error_unexpected_value(unsupported_types){'list item types'}
154
- when *SCALAR_TYPES
155
- return result_text(data)
156
- else Aspera.error_unexpected_value(data.class.name){'result type'}
157
- end
60
+ return Result::ObjectList.new(status_table)
158
61
  end
159
62
  end
160
63
 
161
64
  # Minimum initialization, no exception raised
65
+ # @param argv [Array<String>] command line arguments
66
+ # @return [nil]
162
67
  def initialize(argv)
163
68
  @argv = argv
164
69
  Log.dump(:argv, @argv, level: :trace2)
165
70
  @option_help = false
166
71
  @option_show_config = false
167
- @bash_completion = false
168
72
  @context = Context.new
169
73
  end
170
74
 
171
75
  # This is the main function called by initial script just after constructor
172
- def process_command_line
76
+ # Processes command line arguments, executes commands, and handles exceptions
77
+ # @return [nil]
78
+ def run
173
79
  # Catch exception information , if any
174
80
  exception_info = nil
175
81
  # False if command shall not be executed (e.g. --show-config)
@@ -181,7 +87,6 @@ module Aspera
181
87
  Plugins::Factory.instance.add_plugins_from_lookup_folders
182
88
  # Help requested without command ? (plugins must be known here)
183
89
  show_usage if @option_help && @context.options.command_or_arg_empty?
184
- generate_bash_completion if @bash_completion
185
90
  @context.config.periodic_check_newer_gem_version
186
91
  command_sym =
187
92
  if @option_show_config && @context.options.command_or_arg_empty?
@@ -206,7 +111,7 @@ module Aspera
206
111
  # Help requested for current plugin
207
112
  show_usage(all: false) if @option_help
208
113
  if @option_show_config
209
- @context.formatter.display_results(type: :single_object, data: @context.options.known_options(only_defined: true).stringify_keys)
114
+ @context.formatter.display_results(Result::SingleObject.new(@context.options.known_options(only_defined: true).stringify_keys))
210
115
  execute_command = false
211
116
  end
212
117
  # Locking for single execution (only after "per plugin" option, in case lock port is there)
@@ -229,7 +134,7 @@ module Aspera
229
134
  at_exit{File.delete(pid_file)}
230
135
  end
231
136
  # Execute and display (if not exclusive execution)
232
- @context.formatter.display_results(**command_plugin.execute_action) if execute_command
137
+ @context.formatter.display_results(command_plugin.execute_action) if execute_command
233
138
  # Save config file if command modified it
234
139
  @context.config.save_config_file_if_needed
235
140
  # Finish
@@ -237,8 +142,9 @@ module Aspera
237
142
  rescue Net::SSH::AuthenticationFailed => e; exception_info = {e: e, t: 'SSH', security: true}
238
143
  rescue OpenSSL::SSL::SSLError => e; exception_info = {e: e, t: 'SSL'}
239
144
  rescue Cli::BadArgument => e; exception_info = {e: e, t: 'Argument', usage: true}
240
- rescue Cli::MissingArgument => e; exception_info = {e: e, t: 'Missing', usage: true}
145
+ rescue Cli::MissingArgument => e; exception_info = {e: e, t: 'Missing'}
241
146
  rescue Cli::BadIdentifier => e; exception_info = {e: e, t: 'Identifier'}
147
+ rescue Cli::SchemaRequest => e; exception_info = {e: e, t: 'Schema'}
242
148
  rescue Cli::Error => e; exception_info = {e: e, t: 'Tool', usage: true}
243
149
  rescue Transfer::Error => e; exception_info = {e: e, t: 'Transfer'}
244
150
  rescue RestCallError => e; exception_info = {e: e, t: 'Rest'}
@@ -251,11 +157,22 @@ module Aspera
251
157
  # 1- processing of error condition
252
158
  unless exception_info.nil?
253
159
  Log.log.warn(exception_info[:e].message) if Log.instance.logger_type.eql?(:syslog) && exception_info[:security]
254
- Log.log.error{"#{exception_info[:t]}: #{exception_info[:e].message}"}
160
+ Log.log.error{"#{exception_info[:t]}: #{exception_info[:e].message}"} unless exception_info[:e].is_a?(Cli::SchemaRequest)
255
161
  Log.log.debug{(['Backtrace:'] + exception_info[:e].backtrace).join("\n")} if exception_info[:debug]
256
162
  @context.formatter.display_message(:error, 'Use option -h to get help.') if exception_info[:usage]
257
163
  # Is that a known error condition with proposal for remediation ?
258
164
  Hints.hint_for(exception_info[:e], @context.formatter)
165
+ # Requested help for a Hash parameter/option ?
166
+ if exception_info[:e].is_a?(Cli::SchemaRequest)
167
+ Log.log.info{"#{exception_info[:t]}: #{exception_info[:e].message}"}
168
+ schema_path = exception_info[:e].path
169
+ if schema_path.nil?
170
+ Log.log.warn{'Sorry, no schema provided yet. Please refer to the manual or API.'}
171
+ else
172
+ builder = Schema::Documentation.new(TerminalFormatter, Schema::Registry.instance.reader(schema_path)).build
173
+ @context.formatter.display_results(Result::ObjectList.new(builder.rows, fields: builder.columns))
174
+ end
175
+ end
259
176
  end
260
177
  # 2- processing of command not processed (due to exception or bad command line)
261
178
  if execute_command || @option_show_config
@@ -276,6 +193,10 @@ module Aspera
276
193
  return
277
194
  end
278
195
 
196
+ # Display usage information and help
197
+ # @param all [Boolean] if true, show help for all plugins; if false, show only current plugin
198
+ # @param exit [Boolean] if true, exit the process after displaying help
199
+ # @return [nil]
279
200
  def show_usage(all: true, exit: true)
280
201
  # Display main plugin options (+config)
281
202
  @context.formatter.display_message(:error, @context.options.parser)
@@ -298,7 +219,10 @@ module Aspera
298
219
 
299
220
  private
300
221
 
222
+ # Initialize agents and options
301
223
  # This can throw exception if there is a problem with the environment, needs to be caught by execute method
224
+ # @raise [StandardError] if there is a problem with the environment
225
+ # @return [nil]
302
226
  def init_agents_and_options
303
227
  @context.man_header = true
304
228
  # Create formatter, in case there is an exception, it is used to display.
@@ -318,7 +242,7 @@ module Aspera
318
242
  @context.config = Plugins::Config.new(context: @context)
319
243
  @context.man_header = true
320
244
  # Data persistency is set in config
321
- Aspera.assert(@context.persistency){'missing persistency object'}
245
+ Aspera.assert(@context.persistency, 'missing persistency object')
322
246
  # The TransferAgent plugin may use the @preset parser
323
247
  @context.transfer = TransferAgent.new(@context.options, @context.config)
324
248
  # Add commands for config plugin after all options have been added
@@ -328,6 +252,8 @@ module Aspera
328
252
  @context.options.parser.banner = app_banner
329
253
  end
330
254
 
255
+ # Generate the application banner for help display
256
+ # @return [String] formatted banner text
331
257
  def app_banner
332
258
  t = ' ' * 8
333
259
  return <<~END_OF_BANNER
@@ -362,11 +288,11 @@ module Aspera
362
288
  END_OF_BANNER
363
289
  end
364
290
 
365
- # Define header for manual
291
+ # Define header for manual and declare all global options
292
+ # @return [nil]
366
293
  def declare_global_options
367
294
  Log.log.debug('declare_global_options')
368
295
  @context.options.declare(:help, 'Show this message', allowed: Allowed::TYPES_NONE, short: 'h'){@option_help = true}
369
- @context.options.declare(:bash_comp, 'Generate bash completion for command', allowed: Allowed::TYPES_NONE){@bash_completion = true}
370
296
  @context.options.declare(:show_config, 'Display parameters used for the provided action', allowed: Allowed::TYPES_NONE){@option_show_config = true}
371
297
  @context.options.declare(:version, 'Display version', allowed: Allowed::TYPES_NONE, short: 'v'){@context.formatter.display_message(:data, Cli::VERSION); Process.exit(0)} # rubocop:disable Style/Semicolon
372
298
  @context.options.declare(
@@ -397,9 +323,10 @@ module Aspera
397
323
  @context.options.parse_options!
398
324
  end
399
325
 
400
- # @return the plugin instance, based on name
326
+ # Get the plugin instance based on name
401
327
  # Also loads the plugin options, and default values from conf file
402
- # @param plugin_name_sym : symbol for plugin name
328
+ # @param plugin_name_sym [Symbol] symbol for plugin name
329
+ # @return [Plugins::Base] the plugin instance
403
330
  def get_plugin_instance_with_options(plugin_name_sym)
404
331
  Log.log.debug{"get_plugin_instance_with_options(#{plugin_name_sym})"}
405
332
  # Load default params only if no param already loaded before plugin instantiation
@@ -407,15 +334,6 @@ module Aspera
407
334
  command_plugin = Plugins::Factory.instance.create(plugin_name_sym, context: @context)
408
335
  return command_plugin
409
336
  end
410
-
411
- def generate_bash_completion
412
- if @context.options.get_next_argument('', multiple: true, mandatory: false).nil?
413
- Plugins::Factory.instance.plugin_list.each{ |p| puts p}
414
- else
415
- Log.log.warn('only first level completion so far')
416
- end
417
- Process.exit(0)
418
- end
419
337
  end
420
338
  end
421
339
  end
@@ -15,7 +15,8 @@ module Aspera
15
15
  %w[Syncd Error Confl Pconf] +
16
16
  (23..24).map{ |i| "P(#{i})"}).freeze
17
17
  class << self
18
- def declare_options(_options)
18
+ def declare_options(options)
19
+ options.declare(:sql, 'SQL suffix appended to sqlite3 queries for admin subcommands (e.g. WHERE clause)')
19
20
  end
20
21
  end
21
22
 
@@ -27,13 +28,13 @@ module Aspera
27
28
  # @return [Hash] sync info
28
29
  def async_info_from_args(direction: nil)
29
30
  path = options.get_next_argument('path')
30
- sync_info = options.get_next_argument('sync info', mandatory: false, validation: Hash, default: {})
31
+ sync_info = options.get_next_argument('sync info', mandatory: false, validation: Hash, default: {}, schema: Schema::Registry::SYNC_CONF)
31
32
  # is the positional path a remote path ?
32
33
  path_is_remote = direction.eql?(:pull)
33
34
  if sync_info.key?('sessions') || sync_info.key?('instance')
34
35
  # `args`
35
36
  sync_info['sessions'] ||= [{}]
36
- Aspera.assert(sync_info['sessions'].length == 1){'Only one session is supported'}
37
+ Aspera.assert(sync_info['sessions'].length == 1, 'Only one session is supported')
37
38
  session = sync_info['sessions'].first
38
39
  dir_key = path_is_remote ? 'remote_dir' : 'local_dir'
39
40
  raise "Parameter #{dir_key} shall not be in sync_info" if session.key?(dir_key)
@@ -94,31 +95,34 @@ module Aspera
94
95
  end
95
96
 
96
97
  def execute_sync_admin
97
- command2 = options.get_next_command(%i[status find meta counters file_info overview])
98
+ command2 = options.get_next_command(%i[status find meta counters file_info overview query])
98
99
  require 'aspera/sync/database' unless command2.eql?(:status)
100
+ sql_suffix = options.get_option(:sql)
99
101
  case command2
100
102
  when :status
101
- return Main.result_single_object(Sync::Operations.admin_status(async_info_from_args))
103
+ return Result::SingleObject.new(Sync::Operations.admin_status(async_info_from_args))
102
104
  when :find
103
105
  folder = options.get_next_argument('path')
104
106
  dbs = Sync::Operations.list_db_files(folder)
105
- return Main.result_object_list(dbs.keys.map{ |n| {name: n, path: dbs[n]}})
107
+ return Result::ObjectList.new(dbs.keys.map{ |n| {name: n, path: dbs[n]}})
106
108
  when :meta, :counters
107
- return Main.result_single_object(db_from_args.send(command2))
109
+ return Result::SingleObject.new(db_from_args.send(command2, sql_suffix))
108
110
  when :file_info
109
- result = db_from_args.send(command2)
111
+ result = db_from_args.file_info(sql_suffix)
110
112
  result.each do |r|
111
113
  r['sstate'] = SyncActions::STATE_STR[r['state']] if r['state']
112
114
  end
113
- return Main.result_object_list(
115
+ return Result::ObjectList.new(
114
116
  result,
115
117
  fields: %w[sstate record_id f_meta_path message]
116
118
  )
117
119
  when :overview
118
- return Main.result_object_list(
120
+ return Result::ObjectList.new(
119
121
  db_from_args.overview,
120
122
  fields: %w[table name type]
121
123
  )
124
+ when :query
125
+ return Result.auto(db_from_args.execute(options.get_option(:sql, mandatory: true)))
122
126
  else Aspera.error_unexpected_value(command2)
123
127
  end
124
128
  end
@@ -131,7 +135,7 @@ module Aspera
131
135
  case command
132
136
  when *Sync::Operations::DIRECTIONS
133
137
  Sync::Operations.start(async_info_from_args(direction: command), transfer.user_transfer_spec, &block)
134
- return Main.result_success
138
+ return Result::Success.new
135
139
  when :admin
136
140
  return execute_sync_admin
137
141
  else Aspera.error_unexpected_value(command)
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/environment'
4
+ require 'aspera/formatter_interface'
5
+ require 'aspera/markdown'
6
+ require 'aspera/assert'
7
+
8
+ module Aspera
9
+ module Cli
10
+ # Terminal formatter with ANSI colors and Unicode support
11
+ # @see FormatterInterface
12
+ # @see MarkdownFormatter (in build/lib/doc_helper.rb)
13
+ module TerminalFormatter
14
+ HINT = 'HINT:'.bg_green.gray.blink.freeze
15
+ include FormatterInterface
16
+
17
+ # Format boolean with colored symbol (✓/✗ or Y/ )
18
+ def tick(yes)
19
+ result =
20
+ if Environment.terminal_supports_unicode?
21
+ yes ? "\u2713" : "\u2717"
22
+ else
23
+ yes ? 'Y' : ' '
24
+ end
25
+ return result.green if yes
26
+ return result.red
27
+ end
28
+
29
+ # Format special values with colors (dim for empty, reverse for others)
30
+ def special_format(what)
31
+ result = "<#{what}>"
32
+ return %w[null empty].any?{ |s| what.include?(s)} ? result.dim : result.reverse_color
33
+ end
34
+
35
+ # Prepare table row for terminal display (word wrap arrays)
36
+ def check_row(row)
37
+ row.each_key do |k|
38
+ row[k] = row[k].map{ |i| WordWrap.ww(i.to_s, 120).chomp}.join("\n") if row[k].is_a?(Array)
39
+ end
40
+ end
41
+
42
+ # Convert Markdown to terminal format (**bold** -> blue, `code` -> bold)
43
+ # @param match [MatchData, String]
44
+ def markdown_text(match)
45
+ if match.is_a?(String)
46
+ match = Markdown::FORMATS.match(match)
47
+ Aspera.assert(match, 'markdown text does not match any known format')
48
+ end
49
+ Aspera.assert_type(match, MatchData)
50
+ if match[:entity]
51
+ Aspera.assert_values(match[:entity], %w[bsol])
52
+ '\\'
53
+ elsif match[:bold]
54
+ match[:bold].to_s.blue
55
+ elsif match[:code]
56
+ match[:code].to_s.bold
57
+ else
58
+ Aspera.error_unexpected_value(match.to_s)
59
+ end
60
+ end
61
+
62
+ module_function :tick, :special_format, :check_row, :markdown_text
63
+ end
64
+ end
65
+ end
@@ -5,6 +5,7 @@ require 'aspera/transfer/spec'
5
5
  require 'aspera/cli/info'
6
6
  require 'aspera/log'
7
7
  require 'aspera/assert'
8
+ require 'aspera/schema/registry'
8
9
 
9
10
  module Aspera
10
11
  module Cli
@@ -12,9 +13,9 @@ module Aspera
12
13
  # one of the supported transfer agents.
13
14
  # Provide CLI options to select one of the transfer agents (FASP/ascp client)
14
15
  class TransferAgent
15
- # @args special value for --sources : read file list from arguments
16
+ # `@args` special value for --sources : read file list from arguments
16
17
  FILE_LIST_FROM_ARGS = '@args'
17
- # @ts special value for --sources : read file list from transfer spec (--ts)
18
+ # `@ts` special value for --sources : read file list from transfer spec (--ts)
18
19
  FILE_LIST_FROM_TRANSFER_SPEC = '@ts'
19
20
  FILE_LIST_OPTIONS = [FILE_LIST_FROM_ARGS, FILE_LIST_FROM_TRANSFER_SPEC, 'Array'].freeze
20
21
  DEFAULT_TRANSFER_NOTIFY_TEMPLATE = <<~END_OF_TEMPLATE
@@ -63,12 +64,12 @@ module Aspera
63
64
  @transfer_paths = nil
64
65
  # HTTPGW URL provided by webapp
65
66
  @httpgw_url_lambda = nil
66
- @opt_mgr.declare(:ts, 'Override transfer spec values', allowed: Hash, handler: {o: self, m: :user_transfer_spec})
67
+ @opt_mgr.declare(:ts, 'Override transfer spec values', allowed: Hash, handler: {o: self, m: :user_transfer_spec}, schema: Schema::Registry::TRANSFER_SPEC)
67
68
  @opt_mgr.declare(:to_folder, 'Destination folder for transferred files')
68
69
  @opt_mgr.declare(:sources, "How list of transferred files is provided (#{FILE_LIST_OPTIONS.join(',')})", default: FILE_LIST_FROM_ARGS)
69
70
  @opt_mgr.declare(:src_type, 'Type of file list', allowed: %i[list pair], default: :list)
70
71
  @opt_mgr.declare(:transfer, 'Type of transfer agent', allowed: Agent::Factory::ALL.keys, default: :direct)
71
- @opt_mgr.declare(:transfer_info, 'Parameters for transfer agent', allowed: Hash, handler: {o: self, m: :transfer_info})
72
+ @opt_mgr.declare(:transfer_info, 'Parameters for transfer agent', allowed: Hash, handler: {o: self, m: :transfer_info}, schema: Schema::Registry::TRANSFER_INFO)
72
73
  @opt_mgr.parse_options!
73
74
  @notification_cb = nil
74
75
  if !@opt_mgr.get_option(:notify_to).nil?
@@ -154,24 +155,25 @@ module Aspera
154
155
  end
155
156
 
156
157
  # Transform the list of paths to a list of hash with source/dest
157
- # @param file_list [Array]
158
+ # @param file_list [Array<Hash>]
158
159
  def list_to_paths(file_list)
159
160
  source_type = @opt_mgr.get_option(:src_type, mandatory: true)
160
- case source_type
161
- when :list
162
- # when providing a list, just specify source
163
- @transfer_paths = file_list.map{ |i| {'source' => i}}
164
- when :pair
165
- Aspera.assert(file_list.length.even?, type: Cli::BadArgument){"When using pair, provide an even number of paths: #{file_list.length}"}
166
- @transfer_paths = file_list.each_slice(2).map{ |s, d| {'source' => s, 'destination' => d}}
167
- else Aspera.error_unexpected_value(source_type)
168
- end
161
+ @transfer_paths =
162
+ case source_type
163
+ when :list
164
+ # when providing a list, just specify source
165
+ file_list.map{ |i| {'source' => i}}
166
+ when :pair
167
+ Aspera.assert(file_list.length.even?, type: Cli::BadArgument){"When using pair, provide an even number of paths: #{file_list.length}"}
168
+ file_list.each_slice(2).map{ |s, d| {'source' => s, 'destination' => d}}
169
+ else Aspera.error_unexpected_value(source_type)
170
+ end
169
171
  end
170
172
 
171
173
  # This is how the list of files to be transferred is specified
172
174
  # get paths suitable for transfer spec from command line
173
175
  # computation is done only once, cache is kept in @transfer_paths
174
- # @param default [Array] of [String] if set, used as default file for --sources=@args
176
+ # @param default [nil, Array<String>] If set, used as default file for --sources=@args
175
177
  # @return [Array, nil] of Hash {source: (mandatory), destination: (optional)}
176
178
  def ts_source_paths(default: nil)
177
179
  # return cache if set
@@ -43,13 +43,13 @@ module Aspera
43
43
  case type
44
44
  when :sessions_init
45
45
  # Give opportunity to show progress of initialization with multiple status
46
- Aspera.assert(session_id.nil?)
46
+ Aspera.assert(session_id.nil?, 'session_id must be nil for :sessions_init event')
47
47
  Aspera.assert_type(info, String)
48
48
  # Initialization of progress bar
49
49
  @title = info
50
50
  when :session_start
51
51
  Aspera.assert_type(session_id, String)
52
- Aspera.assert(info.nil?)
52
+ Aspera.assert(info.nil?, 'info must be nil for :session_start event')
53
53
  raise "Session #{session_id} already started" if @sessions[session_id]
54
54
  @sessions[session_id] = {
55
55
  job_size: 0, # Total size of transfer (pre-calc)
@@ -60,7 +60,7 @@ module Aspera
60
60
  @title = nil
61
61
  when :session_size
62
62
  Aspera.assert_type(session_id, String)
63
- Aspera.assert(!info.nil?)
63
+ Aspera.assert(!info.nil?, 'info must not be nil for :session_size event')
64
64
  Aspera.assert_type(@sessions[session_id], Hash)
65
65
  @sessions[session_id][:job_size] = info.to_i
66
66
  sessions_total = total(:job_size)
@@ -76,12 +76,12 @@ module Aspera
76
76
  end
77
77
  when :session_end
78
78
  Aspera.assert_type(session_id, String)
79
- Aspera.assert(info.nil?)
79
+ Aspera.assert(info.nil?, 'info must be nil for :session_end event')
80
80
  # A session may be too short and finish before it has been started
81
81
  @sessions[session_id][:running] = false if @sessions[session_id].is_a?(Hash)
82
82
  when :end
83
- Aspera.assert(session_id.nil?)
84
- Aspera.assert(info.nil?)
83
+ Aspera.assert(session_id.nil?, 'session_id must be nil for :end event')
84
+ Aspera.assert(info.nil?, 'info must be nil for :end event')
85
85
  @progress_bar.finish
86
86
  else Aspera.error_unexpected_value(type){'event type'}
87
87
  end
@@ -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.26.0'
7
+ VERSION = '4.26.2'
8
8
  end
9
9
  end
@@ -80,7 +80,7 @@ module Aspera
80
80
  end
81
81
  RestParameters.instance.spinner_cb.call(action: :success)
82
82
  raise "No known application found at #{app_url}" if found_apps.empty?
83
- Aspera.assert(found_apps.all?{ |a| a.keys.all?(Symbol)})
83
+ Aspera.assert(found_apps.all?{ |a| a.keys.all?(Symbol)}, 'all app info keys must be symbols')
84
84
  return found_apps
85
85
  end
86
86
 
@@ -128,7 +128,8 @@ module Aspera
128
128
  apps.first
129
129
  else
130
130
  formatter.display_status('Multiple applications detected, please select from:')
131
- formatter.display_results(type: :object_list, data: apps, fields: %w[product url version])
131
+ require 'aspera/cli/result'
132
+ formatter.display_results(Result::ObjectList.new(apps, fields: %w[product url version]))
132
133
  answer = options.prompt_user_input_in_list('product', apps.map{ |a| a[:product]})
133
134
  apps.find{ |a| a[:product].eql?(answer)}
134
135
  end
@@ -140,9 +141,7 @@ module Aspera
140
141
  options.add_option_preset({url: wiz_url}, 'wizard')
141
142
  # Instantiate plugin: command line options will be known, e.g. private_key, and wizard can be called
142
143
  plugin_instance = Plugins::Factory.instance.plugin_class(identification[:product]).new(context: @parent.context)
143
- Aspera.assert(plugin_instance.respond_to?(:wizard), type: Cli::BadArgument) do
144
- "Detected: #{identification[:product]}, but this application has no wizard"
145
- end
144
+ Aspera.assert(plugin_instance.respond_to?(:wizard), type: Cli::BadArgument){"Detected: #{identification[:product]}, but this application has no wizard"}
146
145
  # Call the wizard
147
146
  wizard_result = plugin_instance.wizard(self, wiz_url)
148
147
  Log.log.debug{"wizard result: #{wizard_result}"}
@@ -166,7 +165,7 @@ module Aspera
166
165
  test_args = "-P#{wiz_preset_name} #{test_args}" unless option_default
167
166
  # TODO: actually test the command
168
167
  test_cmd = "#{Info::CMD_NAME} #{identification[:product]} #{test_args}"
169
- return Main.result_status("You can test with:\n#{test_cmd.red}")
168
+ return Result::Status.new("You can test with:\n#{test_cmd.red}")
170
169
  end
171
170
  end
172
171
  end