aspera-cli 4.18.0 → 4.19.0

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 (60) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +23 -0
  4. data/CONTRIBUTING.md +5 -12
  5. data/README.md +152 -84
  6. data/examples/build_exec +85 -0
  7. data/examples/build_package.sh +28 -0
  8. data/lib/aspera/agent/alpha.rb +4 -4
  9. data/lib/aspera/agent/base.rb +2 -0
  10. data/lib/aspera/agent/connect.rb +3 -4
  11. data/lib/aspera/agent/direct.rb +108 -104
  12. data/lib/aspera/agent/httpgw.rb +1 -1
  13. data/lib/aspera/api/aoc.rb +2 -2
  14. data/lib/aspera/api/httpgw.rb +95 -57
  15. data/lib/aspera/api/node.rb +110 -77
  16. data/lib/aspera/ascp/installation.rb +47 -32
  17. data/lib/aspera/ascp/management.rb +4 -1
  18. data/lib/aspera/ascp/products.rb +2 -8
  19. data/lib/aspera/cli/extended_value.rb +27 -14
  20. data/lib/aspera/cli/formatter.rb +35 -28
  21. data/lib/aspera/cli/main.rb +11 -11
  22. data/lib/aspera/cli/manager.rb +109 -94
  23. data/lib/aspera/cli/plugin.rb +4 -7
  24. data/lib/aspera/cli/plugin_factory.rb +10 -1
  25. data/lib/aspera/cli/plugins/aoc.rb +15 -14
  26. data/lib/aspera/cli/plugins/config.rb +35 -29
  27. data/lib/aspera/cli/plugins/faspex.rb +5 -4
  28. data/lib/aspera/cli/plugins/faspex5.rb +16 -13
  29. data/lib/aspera/cli/plugins/node.rb +50 -41
  30. data/lib/aspera/cli/plugins/orchestrator.rb +3 -2
  31. data/lib/aspera/cli/plugins/preview.rb +1 -1
  32. data/lib/aspera/cli/plugins/server.rb +2 -2
  33. data/lib/aspera/cli/plugins/shares.rb +11 -7
  34. data/lib/aspera/cli/special_values.rb +13 -0
  35. data/lib/aspera/cli/sync_actions.rb +73 -32
  36. data/lib/aspera/cli/transfer_agent.rb +3 -2
  37. data/lib/aspera/cli/transfer_progress.rb +1 -1
  38. data/lib/aspera/cli/version.rb +1 -1
  39. data/lib/aspera/environment.rb +100 -7
  40. data/lib/aspera/faspex_gw.rb +1 -1
  41. data/lib/aspera/keychain/encrypted_hash.rb +2 -0
  42. data/lib/aspera/log.rb +1 -0
  43. data/lib/aspera/node_simulator.rb +1 -1
  44. data/lib/aspera/oauth/jwt.rb +1 -1
  45. data/lib/aspera/oauth/url_json.rb +2 -0
  46. data/lib/aspera/oauth/web.rb +7 -6
  47. data/lib/aspera/rest.rb +46 -15
  48. data/lib/aspera/secret_hider.rb +3 -2
  49. data/lib/aspera/ssh.rb +1 -1
  50. data/lib/aspera/transfer/faux_file.rb +7 -5
  51. data/lib/aspera/transfer/parameters.rb +27 -19
  52. data/lib/aspera/transfer/spec.rb +8 -10
  53. data/lib/aspera/transfer/sync.rb +52 -47
  54. data/lib/aspera/web_auth.rb +0 -1
  55. data/lib/aspera/web_server_simple.rb +24 -13
  56. data.tar.gz.sig +0 -0
  57. metadata +5 -4
  58. metadata.gz.sig +0 -0
  59. data/examples/rubyc +0 -24
  60. data/lib/aspera/open_application.rb +0 -69
@@ -20,7 +20,7 @@ module Aspera
20
20
  @method = method_name
21
21
  @option_name = option_name
22
22
  @has_writer = @object.respond_to?(writer_method)
23
- Log.log.debug{"AttrAccessor: #{@option_name}: #{@object.class}.#{@method}: writer=#{@has_writer}"}
23
+ Log.log.trace1{"AttrAccessor: #{@option_name}: #{@object.class}.#{@method}: writer=#{@has_writer}"}
24
24
  Aspera.assert(@object.respond_to?(@method)) {"#{object} does not respond to #{method_name}"}
25
25
  end
26
26
 
@@ -55,12 +55,13 @@ module Aspera
55
55
  # option name separator in code (symbol)
56
56
  OPTION_SEP_SYMBOL = '_'
57
57
  SOURCE_USER = 'cmdline' # cspell:disable-line
58
- TYPE_INTEGER = [Integer].freeze
59
58
  OPTION_VALUE_SEPARATOR = '='
60
59
  OPTION_PREFIX = '--'
61
60
  OPTIONS_STOP = '--'
62
61
 
63
- private_constant :FALSE_VALUES, :TRUE_VALUES, :BOOLEAN_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :SOURCE_USER, :TYPE_INTEGER
62
+ DEFAULT_PARSER_TYPES = [Array, Hash].freeze
63
+
64
+ private_constant :FALSE_VALUES, :TRUE_VALUES, :BOOLEAN_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :SOURCE_USER
64
65
 
65
66
  class << self
66
67
  def enum_to_bool(enum)
@@ -86,9 +87,9 @@ module Aspera
86
87
 
87
88
  # Generates error message with list of allowed values
88
89
  # @param error_msg [String] error message
89
- # @param choices [Array] list of allowed values
90
- def multi_choice_assert(assertion, error_msg, choices)
91
- raise Cli::BadArgument, [error_msg, 'Use:'].concat(choices.map{|c|"- #{c}"}.sort).join("\n") unless assertion
90
+ # @param accept_list [Array] list of allowed values
91
+ def multi_choice_assert(assertion, error_msg, accept_list)
92
+ raise Cli::BadArgument, [error_msg, 'Use:'].concat(accept_list.map{|c|"- #{c}"}.sort).join("\n") unless assertion
92
93
  end
93
94
 
94
95
  # change option name with dash to name with underscore
@@ -102,14 +103,17 @@ module Aspera
102
103
 
103
104
  # @param what [Symbol] :option or :argument
104
105
  # @param descr [String] description for help
105
- # @param value [Object] value to check
106
+ # @param to_check [Object] value to check
106
107
  # @param type_list [NilClass, Class, Array[Class]] accepted value type(s)
107
- def validate_type(what, descr, value, type_list)
108
+ def validate_type(what, descr, to_check, type_list, check_array: false)
108
109
  return nil if type_list.nil?
109
110
  Aspera.assert(type_list.is_a?(Array) && type_list.all?(Class)){'types must be a Class Array'}
110
- raise Cli::BadArgument,
111
- "#{what.to_s.capitalize} #{descr} is a #{value.class} but must be #{type_list.length > 1 ? 'one of ' : ''}#{type_list.map(&:name).join(',')}" unless \
112
- type_list.any?{|t|value.is_a?(t)}
111
+ value_list = check_array ? to_check : [to_check]
112
+ value_list.each do |value|
113
+ raise Cli::BadArgument,
114
+ "#{what.to_s.capitalize} #{descr} is a #{value.class} but must be #{type_list.length > 1 ? 'one of ' : ''}#{type_list.map(&:name).join(',')}" unless \
115
+ type_list.any?{|t|value.is_a?(t)}
116
+ end
113
117
  end
114
118
  end
115
119
 
@@ -117,7 +121,7 @@ module Aspera
117
121
  attr_accessor :ask_missing_mandatory, :ask_missing_optional
118
122
  attr_writer :fail_on_missing_mandatory
119
123
 
120
- def initialize(program_name)
124
+ def initialize(program_name, argv = nil)
121
125
  # command line values *not* starting with '-'
122
126
  @unprocessed_cmd_line_arguments = []
123
127
  # command line values starting with '-'
@@ -149,14 +153,7 @@ module Aspera
149
153
  Log.log.debug{"env=#{@option_pairs_env}".red}
150
154
  @unprocessed_cmd_line_options = []
151
155
  @unprocessed_cmd_line_arguments = []
152
- end
153
-
154
- def parse_command_line(argv)
155
- @parser.separator('')
156
- @parser.separator('OPTIONS: global')
157
- declare(:interactive, 'Use interactive input of missing params', values: :bool, handler: {o: self, m: :ask_missing_mandatory})
158
- declare(:ask_options, 'Ask even optional options', values: :bool, handler: {o: self, m: :ask_missing_optional})
159
- parse_options!
156
+ return if argv.nil?
160
157
  process_options = true
161
158
  until argv.empty?
162
159
  value = argv.shift
@@ -174,65 +171,63 @@ module Aspera
174
171
  end
175
172
  @initial_cli_options = @unprocessed_cmd_line_options.dup.freeze
176
173
  Log.log.debug{"add_cmd_line_options:commands/arguments=#{@unprocessed_cmd_line_arguments},options=#{@unprocessed_cmd_line_options}".red}
174
+ @parser.separator('')
175
+ @parser.separator('OPTIONS: global')
176
+ declare(:interactive, 'Use interactive input of missing params', values: :bool, handler: {o: self, m: :ask_missing_mandatory})
177
+ declare(:ask_options, 'Ask even optional options', values: :bool, handler: {o: self, m: :ask_missing_optional})
178
+ declare(:struct_parser, 'Default parser when expected value is a struct', values: %i[json ruby])
179
+ # do not parse options yet, let's wait for option `-h` to be overriden
177
180
  end
178
181
 
179
182
  # @param descr [String] description for help
180
- # @param expected is
181
- # - Array of allowed value (single value)
182
- # - :multiple for remaining values
183
- # - :single for a single unconstrained value
184
- # - :integer for a single integer value
185
183
  # @param mandatory [Boolean] if true, raise error if option not set
186
- # @param type [Class, Array] accepted value type(s)
184
+ # @param multiple [Boolean] if true, return remaining arguments
185
+ # @param accept_list [Array] list of allowed values (Symbol)
186
+ # @param validation [Class, Array] accepted value type(s) or list of Symbols
187
187
  # @param aliases [Hash] map of aliases: key = alias, value = real value
188
188
  # @param default [Object] default value
189
- # @return value, list or nil
190
- def get_next_argument(descr, expected: :single, mandatory: true, type: nil, aliases: nil, default: nil)
191
- Aspera.assert(%i[single multiple].include?(expected) || (expected.is_a?(Array) && expected.all?(Symbol))) do
192
- 'expected must be single, multiple, or array of symbol'
193
- end
194
- Aspera.assert(type.nil? || type.is_a?(Class) || (type.is_a?(Array) && type.all?(Class))){'type must be Class or Array of Class'}
195
- Aspera.assert(aliases.nil? || (aliases.is_a?(Hash) && aliases.keys.all?(Symbol) && aliases.values.all?(Symbol))){'aliases must be Hash'}
196
- allowed_types = type
189
+ # @return one value, list or nil (if optional and no default)
190
+ def get_next_argument(descr, mandatory: true, multiple: false, accept_list: nil, validation: String, aliases: nil, default: nil)
191
+ Aspera.assert(accept_list.nil? || (accept_list.is_a?(Array) && accept_list.all?(Symbol)))
192
+ validation = Symbol if accept_list
193
+ Aspera.assert(validation.nil? || validation.is_a?(Class) || (validation.is_a?(Array) && validation.all?(Class))){'validation must be Class or Array of Class'}
194
+ Aspera.assert(aliases.nil? || (aliases.is_a?(Hash) && aliases.keys.all?(Symbol) && aliases.values.all?(Symbol))){'aliases must be Hash:Symbol: Symbol'}
195
+ allowed_types = validation
197
196
  unless allowed_types.nil?
198
197
  allowed_types = [allowed_types] unless allowed_types.is_a?(Array)
199
198
  descr = "#{descr} (#{allowed_types.join(', ')})"
200
199
  end
201
200
  result =
202
201
  if !@unprocessed_cmd_line_arguments.empty?
203
- # there are values
204
- case expected
205
- when :single
206
- ExtendedValue.instance.evaluate(@unprocessed_cmd_line_arguments.shift)
207
- when :multiple
208
- value = @unprocessed_cmd_line_arguments.shift(@unprocessed_cmd_line_arguments.length).map{|v|ExtendedValue.instance.evaluate(v)}
209
- # if expecting list and only one arg of type array : it is the list
210
- if value.length.eql?(1) && value.first.is_a?(Array)
211
- value = value.first
212
- end
213
- value
214
- when Array
215
- allowed_values = [].concat(expected)
202
+ how_many = multiple ? @unprocessed_cmd_line_arguments.length : 1
203
+ values = @unprocessed_cmd_line_arguments.shift(how_many)
204
+ values = values.map{|v|evaluate_extended_value(v, allowed_types)}
205
+ # if expecting list and only one arg of type array : it is the list
206
+ values = values.first if values.length.eql?(1) && values.first.is_a?(Array)
207
+ if accept_list
208
+ allowed_values = [].concat(accept_list)
216
209
  allowed_values.concat(aliases.keys) unless aliases.nil?
217
- self.class.get_from_list(@unprocessed_cmd_line_arguments.shift, descr, allowed_values)
218
- else Aspera.error_unexpected_value(expected)
210
+ values = values.map{|v|self.class.get_from_list(v, descr, allowed_values)}
219
211
  end
212
+ multiple ? values : values.first
220
213
  elsif !default.nil? then default
221
214
  # no value provided, either get value interactively, or exception
222
- elsif mandatory then get_interactive(:argument, descr, expected: expected)
215
+ elsif mandatory then get_interactive(descr, multiple: multiple, accept_list: accept_list)
223
216
  end
224
- if result.is_a?(String) && allowed_types.eql?(TYPE_INTEGER)
217
+ if result.is_a?(String) && validation.eql?(Integer)
225
218
  int_result = Integer(result, exception: false)
226
219
  raise Cli::BadArgument, "Invalid integer: #{result}" if int_result.nil?
227
220
  result = int_result
228
221
  end
229
222
  Log.log.debug{"#{descr}=#{result}"}
230
223
  result = aliases[result] if aliases&.key?(result)
231
- self.class.validate_type(:argument, descr, result, allowed_types) unless result.nil? && !mandatory
224
+ # if value comes from JSON/YAML, it may come as Integer
225
+ result = result.to_s if result.is_a?(Integer) && validation.eql?(String)
226
+ self.class.validate_type(:argument, descr, result, allowed_types, check_array: multiple) unless result.nil? && !mandatory
232
227
  return result
233
228
  end
234
229
 
235
- def get_next_command(command_list, aliases: nil); return get_next_argument('command', expected: command_list, aliases: aliases); end
230
+ def get_next_command(command_list, aliases: nil); return get_next_argument('command', accept_list: command_list, aliases: aliases); end
236
231
 
237
232
  # Get an option value by name
238
233
  # either return value or calls handler, can return nil
@@ -261,13 +256,13 @@ module Aspera
261
256
  raise Cli::BadArgument, "Missing mandatory option: #{option_symbol}" if mandatory
262
257
  elsif @ask_missing_optional || mandatory
263
258
  # ask_missing_mandatory
264
- expected = :single
259
+ accept_list = nil
265
260
  # print "please enter: #{option_symbol.to_s}"
266
261
  if @declared_options.key?(option_symbol) && attributes.key?(:values)
267
- expected = attributes[:values]
262
+ accept_list = attributes[:values]
268
263
  end
269
- result = get_interactive(:option, option_symbol.to_s, expected: expected)
270
- set_option(option_symbol, result, 'interactive')
264
+ result = get_interactive(option_symbol.to_s, option: true, accept_list: accept_list)
265
+ set_option(option_symbol, result, where: 'interactive')
271
266
  end
272
267
  end
273
268
  self.class.validate_type(:option, option_symbol, result, attributes[:types]) unless result.nil? && !mandatory
@@ -275,12 +270,16 @@ module Aspera
275
270
  end
276
271
 
277
272
  # set an option value by name, either store value or call handler
278
- def set_option(option_symbol, value, where='code override')
273
+ # @param option_symbol [Symbol] option name
274
+ # @param value [String] value to set
275
+ # @param where [String] where the value comes from
276
+ # @param expect [Class, Array] expected value type(s)
277
+ def set_option(option_symbol, value, where: 'code override')
279
278
  Aspera.assert_type(option_symbol, Symbol)
280
279
  raise Cli::BadArgument, "Unknown option: #{option_symbol}" unless @declared_options.key?(option_symbol)
281
280
  attributes = @declared_options[option_symbol]
282
281
  Log.log.warn("#{option_symbol}: Option is deprecated: #{attributes[:deprecation]}") if attributes[:deprecation]
283
- value = ExtendedValue.instance.evaluate(value)
282
+ value = evaluate_extended_value(value, attributes[:types])
284
283
  value = Manager.enum_to_bool(value) if attributes[:values].eql?(BOOLEAN_VALUES)
285
284
  Log.log.debug{"(#{attributes[:read_write]}/#{where}) set #{option_symbol}=#{value}"}
286
285
  self.class.validate_type(:option, option_symbol, value, attributes[:types])
@@ -329,21 +328,21 @@ module Aspera
329
328
  if opt[:read_write].eql?(:accessor)
330
329
  Aspera.assert_type(handler, Hash)
331
330
  Aspera.assert(handler.keys.sort.eql?(%i[m o]))
332
- Log.log.debug{"set attr obj #{option_symbol} (#{handler[:o]},#{handler[:m]})"}
331
+ Log.log.trace1{"set attr obj: #{option_symbol} (#{handler[:o]},#{handler[:m]})"}
333
332
  opt[:accessor] = AttrAccessor.new(handler[:o], handler[:m], option_symbol)
334
333
  end
335
- set_option(option_symbol, default, 'default') unless default.nil?
334
+ set_option(option_symbol, default, where: 'default') unless default.nil?
336
335
  on_args = [description]
337
336
  case values
338
337
  when nil
339
338
  on_args.push(symbol_to_option(option_symbol, 'VALUE'))
340
339
  on_args.push("-#{short}VALUE") unless short.nil?
341
340
  on_args.push(coerce) unless coerce.nil?
342
- @parser.on(*on_args) { |v| set_option(option_symbol, v, SOURCE_USER) }
341
+ @parser.on(*on_args) { |v| set_option(option_symbol, v, where: SOURCE_USER) }
343
342
  when Array, :bool
344
343
  if values.eql?(:bool)
345
344
  values = BOOLEAN_VALUES
346
- set_option(option_symbol, Manager.enum_to_bool(default), 'default') unless default.nil?
345
+ set_option(option_symbol, Manager.enum_to_bool(default), where: 'default') unless default.nil?
347
346
  end
348
347
  # this option value must be a symbol
349
348
  opt[:values] = values
@@ -355,7 +354,7 @@ module Aspera
355
354
  on_args[0] = "#{description}: #{help_values}"
356
355
  on_args.push(symbol_to_option(option_symbol, 'ENUM'))
357
356
  on_args.push(values)
358
- @parser.on(*on_args){|v|set_option(option_symbol, self.class.get_from_list(v.to_s, description, values), SOURCE_USER)}
357
+ @parser.on(*on_args){|v|set_option(option_symbol, self.class.get_from_list(v.to_s, description, values), where: SOURCE_USER)}
359
358
  when :date
360
359
  on_args.push(symbol_to_option(option_symbol, 'DATE'))
361
360
  @parser.on(*on_args) do |v|
@@ -364,7 +363,7 @@ module Aspera
364
363
  when /^-([0-9]+)h/ then Manager.time_to_string(Time.now - (Regexp.last_match(1).to_i * 3600))
365
364
  else v
366
365
  end
367
- set_option(option_symbol, time_string, SOURCE_USER)
366
+ set_option(option_symbol, time_string, where: SOURCE_USER)
368
367
  end
369
368
  when :none
370
369
  Aspera.assert(!block.nil?){"missing block for #{option_symbol}"}
@@ -373,7 +372,7 @@ module Aspera
373
372
  @parser.on(*on_args, &block)
374
373
  else Aspera.error_unexpected_value(values)
375
374
  end
376
- Log.log.debug{"on_args=#{on_args}"}
375
+ Log.log.trace1{"on_args=#{on_args}"}
377
376
  end
378
377
 
379
378
  # Adds each of the keys of specified hash as an option
@@ -401,6 +400,7 @@ module Aspera
401
400
  end
402
401
 
403
402
  # get all original options on command line used to generate a config in config file
403
+ # @return [Hash] options as taken from config file and command line just before command execution
404
404
  def unprocessed_options_with_value
405
405
  result = {}
406
406
  @initial_cli_options.each do |option_value|
@@ -435,7 +435,7 @@ module Aspera
435
435
 
436
436
  # removes already known options from the list
437
437
  def parse_options!
438
- Log.log.debug('parse_options!'.red)
438
+ Log.log.trace1('parse_options!'.red)
439
439
  # first conf file, then env var
440
440
  consume_option_pairs(@option_pairs_batch, 'set')
441
441
  consume_option_pairs(@option_pairs_env, 'env')
@@ -443,21 +443,21 @@ module Aspera
443
443
  unknown_options = []
444
444
  begin
445
445
  # remove known options one by one, exception if unknown
446
- Log.log.debug('before parse'.red)
446
+ Log.log.trace1('before parse'.red)
447
447
  @parser.parse!(@unprocessed_cmd_line_options)
448
- Log.log.debug('After parse'.red)
448
+ Log.log.trace1('After parse'.red)
449
449
  rescue OptionParser::InvalidOption => e
450
- Log.log.debug{"InvalidOption #{e}".red}
450
+ Log.log.trace1{"InvalidOption #{e}".red}
451
451
  # save for later processing
452
452
  unknown_options.push(e.args.first)
453
453
  retry
454
454
  end
455
- Log.log.debug{"remains: #{unknown_options}"}
455
+ Log.log.trace1{"remains: #{unknown_options}"}
456
456
  # set unprocessed options for next time
457
457
  @unprocessed_cmd_line_options = unknown_options
458
458
  end
459
459
 
460
- def prompt_user_input(prompt, sensitive)
460
+ def prompt_user_input(prompt, sensitive: false)
461
461
  return $stdin.getpass("#{prompt}> ") if sensitive
462
462
  print("#{prompt}> ")
463
463
  line = $stdin.gets
@@ -471,7 +471,7 @@ module Aspera
471
471
  # @return [Symbol] selected symbol
472
472
  def prompt_user_input_in_list(prompt, sym_list)
473
473
  loop do
474
- input = prompt_user_input(prompt, false).to_sym
474
+ input = prompt_user_input(prompt).to_sym
475
475
  if sym_list.any?{|a|a.eql?(input)}
476
476
  return input
477
477
  else
@@ -480,34 +480,49 @@ module Aspera
480
480
  end
481
481
  end
482
482
 
483
- def get_interactive(type, descr, expected: :single)
483
+ # Prompt user for input in a list of symbols
484
+ # @param descr [String] description for help
485
+ # @param option [Boolean] true if command line option
486
+ # @param multiple [Boolean] true if multiple values expected
487
+ # @param accept_list [Array] list of expected values
488
+ def get_interactive(descr, option: false, multiple: false, accept_list: nil)
489
+ what = option ? 'option' : 'argument'
484
490
  if !@ask_missing_mandatory
485
- raise Cli::BadArgument, "missing argument (#{expected}): #{descr}" unless expected.is_a?(Array)
486
- self.class.multi_choice_assert(false, "missing: #{descr}", expected)
491
+ message = "missing #{what}: #{descr}"
492
+ if accept_list.nil?
493
+ raise Cli::BadArgument, message
494
+ else
495
+ self.class.multi_choice_assert(false, message, accept_list)
496
+ end
487
497
  end
488
498
  result = nil
489
- sensitive = type.eql?(:option) && @declared_options[descr.to_sym].is_a?(Hash) && @declared_options[descr.to_sym][:sensitive]
490
- default_prompt = "#{type}: #{descr}"
499
+ sensitive = option && @declared_options[descr.to_sym].is_a?(Hash) && @declared_options[descr.to_sym][:sensitive]
500
+ default_prompt = "#{what}: #{descr}"
491
501
  # ask interactively
492
- case expected
493
- when :multiple
494
- result = []
495
- puts(' (one per line, end with empty line)')
496
- loop do
497
- entry = prompt_user_input(default_prompt, sensitive)
498
- break if entry.empty?
499
- result.push(ExtendedValue.instance.evaluate(entry))
500
- end
501
- when :single
502
- result = ExtendedValue.instance.evaluate(prompt_user_input(default_prompt, sensitive))
503
- else # one fixed
504
- result = self.class.get_from_list(prompt_user_input("#{expected.join(' ')}\n#{default_prompt}", sensitive), descr, expected)
502
+ result = []
503
+ puts(' (one per line, end with empty line)') if multiple
504
+ loop do
505
+ prompt = default_prompt
506
+ prompt = "#{accept_list.join(' ')}\n#{default_prompt}" if accept_list
507
+ entry = prompt_user_input(prompt, sensitive: sensitive)
508
+ break if entry.empty? && multiple
509
+ entry = ExtendedValue.instance.evaluate(entry)
510
+ entry = self.class.get_from_list(entry, descr, accept_list) if accept_list
511
+ return entry unless multiple
512
+ result.push(entry)
505
513
  end
506
514
  return result
507
515
  end
508
516
 
509
517
  private
510
518
 
519
+ def evaluate_extended_value(value, types)
520
+ if DEFAULT_PARSER_TYPES.include?(types) || (types.is_a?(Array) && types.all?{|t|DEFAULT_PARSER_TYPES.include?(t)})
521
+ return ExtendedValue.instance.evaluate_with_default(value)
522
+ end
523
+ return ExtendedValue.instance.evaluate(value)
524
+ end
525
+
511
526
  # generate command line option from option symbol
512
527
  def symbol_to_option(symbol, opt_val = nil)
513
528
  result = [OPTION_PREFIX, symbol.to_s.gsub(OPTION_SEP_SYMBOL, OPTION_SEP_LINE)].join
@@ -523,7 +538,7 @@ module Aspera
523
538
  # @param unprocessed_options [Array] list of options to apply (key_sym,value)
524
539
  # @param where [String] where the options come from
525
540
  def consume_option_pairs(unprocessed_options, where)
526
- Log.log.debug{"consume_option_pairs: #{where}"}
541
+ Log.log.trace1{"consume_option_pairs: #{where}"}
527
542
  options_to_set = {}
528
543
  unprocessed_options.each do |k, v|
529
544
  if @declared_options.key?(k)
@@ -533,11 +548,11 @@ module Aspera
533
548
  end
534
549
  options_to_set[k] = v
535
550
  else
536
- Log.log.debug{"unprocessed: #{k}: #{v}"}
551
+ Log.log.trace1{"unprocessed: #{k}: #{v}"}
537
552
  end
538
553
  end
539
554
  options_to_set.each do |k, v|
540
- set_option(k, v, where)
555
+ set_option(k, v, where: where)
541
556
  # keep only unprocessed values for next parse
542
557
  unprocessed_options.delete(k)
543
558
  end
@@ -5,7 +5,7 @@ require 'aspera/assert'
5
5
 
6
6
  module Aspera
7
7
  module Cli
8
- # base class for plugins modules
8
+ # Base class for plugins
9
9
  class Plugin
10
10
  # operations without id
11
11
  GLOBAL_OPS = %i[create list].freeze
@@ -29,7 +29,6 @@ module Aspera
29
29
  :value, 'Value for create, update, list filter', types: Hash,
30
30
  deprecation: '(4.14) Use positional value for create/modify or option: query for list/delete')
31
31
  options.declare(:property, 'Name of property to set (modify operation)')
32
- options.declare(:id, 'Resource identifier', deprecation: "(4.14) Use positional identifier after verb (#{INSTANCE_OPS.join(',')})")
33
32
  options.declare(:bulk, 'Bulk operation (only some)', values: :bool, default: :no)
34
33
  options.declare(:bfail, 'Bulk operation error handling', values: :bool, default: :yes)
35
34
  end
@@ -54,8 +53,8 @@ module Aspera
54
53
  options.parser.separator('OPTIONS:')
55
54
  end
56
55
 
56
+ # @return a hash of instance variables
57
57
  def init_params
58
- # return a hash of instance variables
59
58
  INIT_PARAMS.map{|p| [p, instance_variable_get("@#{p}".to_sym)]}.to_h
60
59
  end
61
60
 
@@ -66,9 +65,7 @@ module Aspera
66
65
  # @return [String, Array] identifier or list of ids
67
66
  def instance_identifier(description: 'identifier', as_option: nil, &block)
68
67
  if as_option.nil?
69
- # use of option `id` is deprecated
70
- res_id = options.get_option(:id)
71
- res_id = options.get_next_argument(description) if res_id.nil?
68
+ res_id = options.get_next_argument(description, multiple: options.get_option(:bulk)) if res_id.nil?
72
69
  else
73
70
  res_id = options.get_option(as_option)
74
71
  end
@@ -255,7 +252,7 @@ module Aspera
255
252
  Log.log.warn("option `value` is deprecated. Use positional parameter for #{command}") unless value.nil?
256
253
  value = options.get_next_argument(
257
254
  "parameters for #{command}#{description.nil? ? '' : " (#{description})"}", mandatory: default.nil?,
258
- type: bulk ? Array : type) if value.nil?
255
+ validation: bulk ? Array : type) if value.nil?
259
256
  value = default if value.nil?
260
257
  unless type.nil?
261
258
  type = [type] unless type.is_a?(Array)
@@ -3,7 +3,7 @@
3
3
  require 'singleton'
4
4
  module Aspera
5
5
  module Cli
6
- # option is retrieved from another object using accessor
6
+ # Instantiate plugin from well-known locations
7
7
  class PluginFactory
8
8
  include Singleton
9
9
 
@@ -19,14 +19,17 @@ module Aspera
19
19
  @plugins = {}
20
20
  end
21
21
 
22
+ # @return list of registered plugins
22
23
  def plugin_list
23
24
  @plugins.keys
24
25
  end
25
26
 
27
+ # @return path to source file of plugin
26
28
  def plugin_source(plugin_name_sym)
27
29
  @plugins[plugin_name_sym][:source]
28
30
  end
29
31
 
32
+ # add a folder to the list of folders to look for plugins
30
33
  def add_lookup_folder(folder)
31
34
  @lookup_folders.unshift(folder)
32
35
  end
@@ -43,6 +46,7 @@ module Aspera
43
46
  end
44
47
  end
45
48
 
49
+ # @return Class object for plugin
46
50
  def plugin_class(plugin_name_sym)
47
51
  raise "ERROR: plugin not found: #{plugin_name_sym}" unless @plugins.key?(plugin_name_sym)
48
52
  require @plugins[plugin_name_sym][:require_stanza]
@@ -50,6 +54,9 @@ module Aspera
50
54
  return Object.const_get("#{Module.nesting[1]}::#{PLUGINS_MODULE}::#{plugin_name_sym.to_s.capitalize}")
51
55
  end
52
56
 
57
+ # Create specified plugin
58
+ # @param plugin_name_sym [Symbol] name of plugin
59
+ # @param args [Hash] arguments to pass to plugin constructor
53
60
  def create(plugin_name_sym, **args)
54
61
  # TODO: check that ancestor is Plugin?
55
62
  plugin_class(plugin_name_sym).new(**args)
@@ -57,6 +64,8 @@ module Aspera
57
64
 
58
65
  private
59
66
 
67
+ # add plugin information to list
68
+ # @param path [String] path to plugin source file
60
69
  def add_plugin_info(path)
61
70
  raise "ERROR: plugin path must end with #{RUBY_FILE_EXT}" if !path.end_with?(RUBY_FILE_EXT)
62
71
  plugin_symbol = File.basename(path, RUBY_FILE_EXT).to_sym
@@ -4,6 +4,7 @@ require 'aspera/cli/plugins/node'
4
4
  require 'aspera/cli/plugins/ats'
5
5
  require 'aspera/cli/basic_auth_plugin'
6
6
  require 'aspera/cli/transfer_agent'
7
+ require 'aspera/cli/special_values'
7
8
  require 'aspera/agent/node'
8
9
  require 'aspera/transfer/spec'
9
10
  require 'aspera/api/aoc'
@@ -123,7 +124,7 @@ module Aspera
123
124
  formatter.display_status(pub_key_pem)
124
125
  if !options.get_option(:test_mode)
125
126
  formatter.display_status('Once updated or validated, press enter.')
126
- OpenApplication.instance.uri(instance_url)
127
+ Environment.instance.open_uri(instance_url)
127
128
  $stdin.gets
128
129
  end
129
130
  else
@@ -137,7 +138,7 @@ module Aspera
137
138
  formatter.display_status('- origin: localhost')
138
139
  formatter.display_status('Use the generated client id and secret in the following prompts.'.red)
139
140
  end
140
- OpenApplication.instance.uri("#{instance_url}/admin/api-clients")
141
+ Environment.instance.open_uri("#{instance_url}/admin/api-clients")
141
142
  options.get_option(:client_id, mandatory: true)
142
143
  options.get_option(:client_secret, mandatory: true)
143
144
  use_browser_authentication = true
@@ -319,8 +320,8 @@ module Aspera
319
320
  # client side is agent
320
321
  # server side is transfer server
321
322
  # in same workspace
322
- push_pull = options.get_next_argument('direction', expected: %i[push pull])
323
- source_folder = options.get_next_argument('folder of source files', type: String)
323
+ push_pull = options.get_next_argument('direction', accept_list: %i[push pull])
324
+ source_folder = options.get_next_argument('folder of source files', validation: String)
324
325
  case push_pull
325
326
  when :push
326
327
  client_direction = Transfer::Spec::DIRECTION_SEND
@@ -415,7 +416,7 @@ module Aspera
415
416
  fields = object.keys.reject{|k|k.eql?('certificate')}
416
417
  return { type: :single_object, data: object, fields: fields }
417
418
  when :modify
418
- changes = options.get_next_argument('properties', type: Hash)
419
+ changes = options.get_next_argument('properties', validation: Hash)
419
420
  return do_bulk_operation(command: command, descr: 'identifier', values: res_id) do |one_id|
420
421
  aoc_api.update("#{resource_class_path}/#{one_id}", changes)
421
422
  {'id' => one_id}
@@ -427,7 +428,7 @@ module Aspera
427
428
  end
428
429
  when :set_pub_key
429
430
  # special : reads private and generate public
430
- the_private_key = options.get_next_argument('private_key PEM value', type: String)
431
+ the_private_key = options.get_next_argument('private_key PEM value', validation: String)
431
432
  the_public_key = OpenSSL::PKey::RSA.new(the_private_key).public_key.to_s
432
433
  aoc_api.update(resource_instance_path, {jwt_grant_enabled: true, public_key: the_public_key})
433
434
  return Main.result_success
@@ -449,7 +450,7 @@ module Aspera
449
450
  case command_admin
450
451
  when :resource
451
452
  Log.log.warn('resource command is deprecated (4.18), directly use the specific command instead')
452
- return execute_resource_action(options.get_next_argument('resource', expected: ADMIN_OBJECTS))
453
+ return execute_resource_action(options.get_next_argument('resource', accept_list: ADMIN_OBJECTS))
453
454
  when *ADMIN_OBJECTS
454
455
  return execute_resource_action(command_admin)
455
456
  when :auth_providers
@@ -534,7 +535,7 @@ module Aspera
534
535
  return {type: :object_list, data: events}
535
536
  when :transfers
536
537
  event_type = command_analytics.to_s
537
- filter_resource = options.get_next_argument('resource', expected: %i[organizations users nodes])
538
+ filter_resource = options.get_next_argument('resource', accept_list: %i[organizations users nodes])
538
539
  filter_id = options.get_next_argument('identifier', mandatory: false) ||
539
540
  case filter_resource
540
541
  when :organizations then aoc_api.current_user_info['organization_id']
@@ -622,7 +623,7 @@ module Aspera
622
623
  when :show
623
624
  return { type: :single_object, data: aoc_api.current_user_info(exception: true) }
624
625
  when :modify
625
- aoc_api.update("users/#{aoc_api.current_user_info(exception: true)['id']}", options.get_next_argument('properties', type: Hash))
626
+ aoc_api.update("users/#{aoc_api.current_user_info(exception: true)['id']}", options.get_next_argument('properties', validation: Hash))
626
627
  return Main.result_status('modified')
627
628
  end
628
629
  when :preferences
@@ -631,7 +632,7 @@ module Aspera
631
632
  when :show
632
633
  return { type: :single_object, data: aoc_api.read(user_preferences_res)[:data] }
633
634
  when :modify
634
- aoc_api.update(user_preferences_res, options.get_next_argument('properties', type: Hash))
635
+ aoc_api.update(user_preferences_res, options.get_next_argument('properties', validation: Hash))
635
636
  return Main.result_status('modified')
636
637
  end
637
638
  end
@@ -690,13 +691,13 @@ module Aspera
690
691
  ].concat(aoc_api.additional_persistence_ids)))
691
692
  end
692
693
  case ids_to_download
693
- when ExtendedValue::ALL, ExtendedValue::INIT
694
+ when SpecialValues::ALL, SpecialValues::INIT
694
695
  query = query_read_delete(default: PACKAGE_RECEIVED_BASE_QUERY)
695
696
  Aspera.assert_type(query, Hash){'query'}
696
697
  resolve_dropbox_name_default_ws_id(query)
697
698
  # remove from list the ones already downloaded
698
699
  all_ids = api_read_all('packages', query)[:data].map{|e|e['id']}
699
- if ids_to_download.eql?(ExtendedValue::INIT)
700
+ if ids_to_download.eql?(SpecialValues::INIT)
700
701
  Aspera.assert(skip_ids_persistency){'Only with option once_only'}
701
702
  skip_ids_persistency.data.clear.concat(all_ids)
702
703
  skip_ids_persistency.save
@@ -758,9 +759,9 @@ module Aspera
758
759
  when *NODE4_EXT_COMMANDS
759
760
  return execute_nodegen4_command(command_repo, aoc_api.context[:home_node_id], file_id: aoc_api.context[:home_file_id], scope: Api::Node::SCOPE_USER)
760
761
  when :short_link
761
- link_type = options.get_next_argument('link type', expected: %i[public private])
762
+ link_type = options.get_next_argument('link type', accept_list: %i[public private])
762
763
  short_link_command = options.get_next_command(%i[create delete list])
763
- folder_dest = options.get_next_argument('path', type: String)
764
+ folder_dest = options.get_next_argument('path', validation: String)
764
765
  home_node_api = aoc_api.node_api_from(
765
766
  node_id: aoc_api.context[:home_node_id],
766
767
  workspace_id: aoc_api.context[:workspace_id],