aspera-cli 4.25.6 → 4.26.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 (40) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +74 -47
  4. data/CONTRIBUTING.md +1 -1
  5. data/lib/aspera/api/aoc.rb +118 -78
  6. data/lib/aspera/api/node.rb +101 -49
  7. data/lib/aspera/ascp/installation.rb +94 -30
  8. data/lib/aspera/cli/extended_value.rb +1 -0
  9. data/lib/aspera/cli/formatter.rb +47 -40
  10. data/lib/aspera/cli/manager.rb +30 -4
  11. data/lib/aspera/cli/plugins/aoc.rb +214 -136
  12. data/lib/aspera/cli/plugins/ats.rb +3 -3
  13. data/lib/aspera/cli/plugins/base.rb +17 -42
  14. data/lib/aspera/cli/plugins/config.rb +5 -3
  15. data/lib/aspera/cli/plugins/console.rb +3 -3
  16. data/lib/aspera/cli/plugins/faspex.rb +5 -5
  17. data/lib/aspera/cli/plugins/faspex5.rb +20 -18
  18. data/lib/aspera/cli/plugins/node.rb +66 -70
  19. data/lib/aspera/cli/plugins/oauth.rb +5 -12
  20. data/lib/aspera/cli/plugins/orchestrator.rb +13 -13
  21. data/lib/aspera/cli/plugins/preview.rb +116 -80
  22. data/lib/aspera/cli/plugins/server.rb +2 -10
  23. data/lib/aspera/cli/plugins/shares.rb +7 -7
  24. data/lib/aspera/cli/version.rb +1 -1
  25. data/lib/aspera/dot_container.rb +7 -3
  26. data/lib/aspera/environment.rb +3 -2
  27. data/lib/aspera/log.rb +1 -1
  28. data/lib/aspera/preview/file_types.rb +1 -1
  29. data/lib/aspera/preview/generator.rb +146 -91
  30. data/lib/aspera/preview/options.rb +4 -1
  31. data/lib/aspera/preview/terminal.rb +50 -20
  32. data/lib/aspera/preview/utils.rb +76 -34
  33. data/lib/aspera/products/transferd.rb +1 -1
  34. data/lib/aspera/rest.rb +1 -0
  35. data/lib/aspera/rest_list.rb +23 -16
  36. data/lib/aspera/secret_hider.rb +3 -1
  37. data/lib/aspera/uri_reader.rb +17 -2
  38. data.tar.gz.sig +0 -0
  39. metadata +5 -5
  40. metadata.gz.sig +0 -0
@@ -28,12 +28,14 @@ module Aspera
28
28
  DISPLAY_LEVELS = %i[info data error].freeze
29
29
  # column names for single object display in table
30
30
  SINGLE_OBJECT_COLUMN_NAMES = %i[field value].freeze
31
+ # Terminal: vertical separator for list of strings.
32
+ STR_LST_SEP_VERT = "\n"
31
33
 
32
- private_constant :FIELDS_LESS, :DISPLAY_FORMATS, :DISPLAY_LEVELS, :SINGLE_OBJECT_COLUMN_NAMES
34
+ private_constant :FIELDS_LESS, :DISPLAY_FORMATS, :DISPLAY_LEVELS, :SINGLE_OBJECT_COLUMN_NAMES, :STR_LST_SEP_VERT
33
35
 
34
36
  class << self
35
37
  # nicer display for boolean
36
- # used by spec_doc
38
+ # used by `spec_doc`
37
39
  def tick(yes)
38
40
  result =
39
41
  if Environment.terminal_supports_unicode?
@@ -47,14 +49,14 @@ module Aspera
47
49
 
48
50
  # Highlight special values on terminal
49
51
  # empty values are dim
50
- # used by spec_doc
52
+ # used by `spec_doc`
51
53
  def special_format(what)
52
54
  result = "<#{what}>"
53
55
  return %w[null empty].any?{ |s| what.include?(s)} ? result.dim : result.reverse_color
54
56
  end
55
57
 
56
- # for transfer spec table, build line for display in terminal
57
- # used by spec_doc
58
+ # For transfer spec table, build line for display in terminal
59
+ # used by `spec_doc`
58
60
  def check_row(row)
59
61
  row.each_key do |k|
60
62
  row[k] = row[k].map{ |i| WordWrap.ww(i.to_s, 120).chomp}.join("\n") if row[k].is_a?(Array)
@@ -62,7 +64,7 @@ module Aspera
62
64
  end
63
65
 
64
66
  # Give Markdown String, or matched data, return formatted string for terminal
65
- # used by spec_doc
67
+ # used by `spec_doc`
66
68
  # @param match [MatchData,String]
67
69
  def markdown_text(match)
68
70
  if match.is_a?(String)
@@ -83,7 +85,7 @@ module Aspera
83
85
  end
84
86
 
85
87
  # Replace special values with a readable version on terminal
86
- def replace_specific_for_terminal(input_hash)
88
+ def replace_specific_for_terminal(input_hash, string_list_separator)
87
89
  hash_to_process = [input_hash]
88
90
  until hash_to_process.empty?
89
91
  current = hash_to_process.pop
@@ -99,7 +101,7 @@ module Aspera
99
101
  if value.empty?
100
102
  current[key] = special_format('empty list')
101
103
  elsif value.all?(String)
102
- current[key] = value.join(',')
104
+ current[key] = value.join(string_list_separator)
103
105
  else
104
106
  value.each do |item|
105
107
  hash_to_process.push(item) if item.is_a?(Hash)
@@ -117,8 +119,7 @@ module Aspera
117
119
  end
118
120
 
119
121
  def all_but(list)
120
- list = [list] unless list.is_a?(Array)
121
- return list.map{ |i| "#{FIELDS_LESS}#{i}"}.unshift(SpecialValues::ALL)
122
+ Array(list).map{ |i| "#{FIELDS_LESS}#{i}"}.unshift(SpecialValues::ALL)
122
123
  end
123
124
  end
124
125
 
@@ -159,11 +160,6 @@ module Aspera
159
160
  end
160
161
 
161
162
  def declare_options(options)
162
- default_table_style = if Environment.terminal_supports_unicode?
163
- {border: :unicode_round}
164
- else
165
- {}
166
- end
167
163
  options.declare(:display, 'Output only some information', allowed: DISPLAY_LEVELS, handler: {o: self, m: :option_handler}, default: :data)
168
164
  options.declare(:format, 'Output format', allowed: DISPLAY_FORMATS, handler: {o: self, m: :option_handler}, default: :table)
169
165
  options.declare(:output, 'Destination for results', handler: {o: self, m: :option_handler})
@@ -173,7 +169,7 @@ module Aspera
173
169
  default: SpecialValues::DEF
174
170
  )
175
171
  options.declare(:select, 'Select only some items in lists: column, value', allowed: [Hash, Proc], handler: {o: self, m: :option_handler})
176
- options.declare(:table_style, '(Table) Display style', allowed: [Hash], handler: {o: self, m: :option_handler}, default: default_table_style)
172
+ options.declare(:table_style, '(Table) Display style', allowed: [Hash], handler: {o: self, m: :option_handler}, default: {})
177
173
  options.declare(:flat_hash, '(Table) Display deep values as additional keys', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :option_handler}, default: true)
178
174
  options.declare(
179
175
  :multi_single, '(Table) Control how object list is displayed as single table, or multiple objects', allowed: %i[no yes single],
@@ -249,15 +245,16 @@ module Aspera
249
245
  SecretHider.instance.deep_remove_secret(data) if hide_secrets?
250
246
  end
251
247
 
252
- # this method displays the results, especially the table format
253
- # @param type [Symbol] type of data
254
- # @param data [Object] data to display
255
- # @param total [Integer] total number of items
256
- # @param fields [Array<String>] list of fields to display
257
- # @param name [String] name of the column to display
258
- def display_results(type:, data: nil, total: nil, fields: nil, name: nil)
259
- Log.log.debug{"display_results: type=#{type} class=#{data.class}"}
260
- Log.log.trace1{"display_results: data=#{data}"}
248
+ # Display results, especially the table format
249
+ # @param type [Symbol] Type of data
250
+ # @param data [Object] Data to display
251
+ # @param fields [Array<String>] List of fields to display
252
+ # @param total [Integer] Total number of items
253
+ # @param name [String] Name of the column to display
254
+ def display_results(type:, data: nil, fields: nil, total: nil, name: nil)
255
+ Log.log.debug{"display_results: type=#{type}"}
256
+ Log.dump(:data, data, level: :trace1)
257
+ Log.dump(:fields, fields, level: :trace1)
261
258
  Aspera.assert_type(type, Symbol){'result must have type'}
262
259
  Aspera.assert(!data.nil? || %i[empty nothing].include?(type)){'result must have data'}
263
260
  display_item_count(data.length, total) unless total.nil?
@@ -317,7 +314,7 @@ module Aspera
317
314
  case type
318
315
  when :single_object
319
316
  # :single_object is a Hash, where key=column name
320
- Aspera.assert_type(data, Hash)
317
+ Aspera.assert_type(data, Hash){'result'}
321
318
  if data.empty?
322
319
  display_message(:data, self.class.special_format('empty dict'))
323
320
  else
@@ -364,6 +361,7 @@ module Aspera
364
361
  # @param default [Array<String>, Proc] list of fields to display by default (may contain special values)
365
362
  def compute_fields(data, default)
366
363
  Log.log.debug{"compute_fields: data:#{data.class} default:#{default.class} #{default}"}
364
+ Log.dump(:compute_fields_default, default, level: :trace1)
367
365
  # the requested list of fields, but if can contain special values
368
366
  request =
369
367
  case @options[:fields]
@@ -374,6 +372,7 @@ module Aspera
374
372
  when Proc then return all_fields(data).select{ |i| @options[:fields].call(i)}
375
373
  else Aspera.error_unexpected_value(@options[:fields])
376
374
  end
375
+ Aspera.assert_array_all(request, String)
377
376
  result = []
378
377
  until request.empty?
379
378
  item = request.shift
@@ -398,11 +397,14 @@ module Aspera
398
397
  end
399
398
  end
400
399
  end
400
+ Log.dump(:compute_fields, result, level: :trace1)
401
401
  return result
402
402
  end
403
403
 
404
404
  # filter the list of items on the fields option
405
405
  def filter_list_on_fields(data)
406
+ # no filter for single element
407
+ return data unless data.is_a?(Array)
406
408
  # by default, keep all data intact
407
409
  return data if @options[:fields].eql?(SpecialValues::DEF) && @options[:select].nil?
408
410
  Aspera.assert_array_all(data, Hash){'filter or select'}
@@ -428,21 +430,26 @@ module Aspera
428
430
  end
429
431
  end
430
432
 
431
- # displays a list of objects
432
- # @param object_array [Array] array of hash
433
- # @param fields [Array] list of column names
433
+ # Displays a list of objects
434
+ # @param object_array [Array<Hash>] Array of hash
435
+ # @param fields [Array<String>] List of column names
436
+ # @param single [Boolean] Contains a single object to display
434
437
  def display_table(object_array, fields, single: false)
435
- Aspera.assert(!fields.nil?){'missing fields parameter'}
438
+ Aspera.assert_array_all(object_array, Hash)
439
+ Aspera.assert_array_all(fields, String)
436
440
  if object_array.empty?
437
441
  # no display for csv
438
442
  display_message(:info, self.class.special_format('empty')) if @options[:format].eql?(:table)
439
443
  return
440
444
  end
441
445
  filter_columns_on_select(object_array)
442
- object_array.each{ |i| self.class.replace_specific_for_terminal(i)}
446
+ format_style = @options[:table_style].symbolize_keys
447
+ string_list_separator = format_style.delete(:str_lst_sep) || STR_LST_SEP_VERT
448
+ # convert data to string, and keep only display fields
449
+ object_array.each{ |i| self.class.replace_specific_for_terminal(i, string_list_separator)}
443
450
  # if table has only one element, and only one field, display the value
444
451
  if object_array.length == 1 && fields.length == 1
445
- Log.log.debug("display_table: single element, field: #{fields.first}")
452
+ Log.log.debug("single element, field: #{fields.first}")
446
453
  data = object_array.first[fields.first]
447
454
  unless data.is_a?(Array) && data.all?(Hash)
448
455
  display_message(:data, data)
@@ -453,6 +460,7 @@ module Aspera
453
460
  single = false
454
461
  end
455
462
  Log.dump(:object_array, object_array)
463
+ Log.dump(:fields, fields)
456
464
  # convert data to string, and keep only display fields
457
465
  final_table_rows = object_array.map{ |r| fields.map{ |c| r[c].to_s}}
458
466
  # remove empty rows
@@ -460,6 +468,7 @@ module Aspera
460
468
  # here : fields : list of column names
461
469
  case @options[:format]
462
470
  when :table
471
+ format_style[:border] = :unicode_round if Environment.terminal_supports_unicode?
463
472
  if single || @options[:multi_single].eql?(:yes) ||
464
473
  (@options[:multi_single].eql?(:single) && final_table_rows.length.eql?(1))
465
474
  # display multiple objects as multiple transposed tables
@@ -467,23 +476,20 @@ module Aspera
467
476
  display_message(:data, Terminal::Table.new(
468
477
  headings: SINGLE_OBJECT_COLUMN_NAMES,
469
478
  rows: fields.zip(row),
470
- style: @options[:table_style].symbolize_keys
479
+ style: format_style
471
480
  ))
472
481
  end
473
482
  else
474
- # display the table ! as single table
483
+ # display the table as single table
475
484
  display_message(:data, Terminal::Table.new(
476
485
  headings: fields,
477
486
  rows: final_table_rows,
478
- style: @options[:table_style].symbolize_keys
487
+ style: format_style
479
488
  ))
480
489
  end
481
490
  when :csv
482
- params = @options[:table_style].symbolize_keys
483
- # delete default
484
- params.delete(:border)
485
- add_headers = params.delete(:headers)
486
- output = CSV.generate(**params) do |csv|
491
+ add_headers = format_style.delete(:headers)
492
+ output = CSV.generate(**format_style) do |csv|
487
493
  csv << fields if add_headers
488
494
  final_table_rows.each do |row|
489
495
  csv << row
@@ -493,6 +499,7 @@ module Aspera
493
499
  else
494
500
  raise "not expected: #{@options[:format]}"
495
501
  end
502
+ nil
496
503
  end
497
504
  end
498
505
  end
@@ -146,7 +146,7 @@ module Aspera
146
146
  new_value = [new_value] if @types.eql?(Allowed::TYPES_STRING_ARRAY) && new_value.is_a?(String)
147
147
  # Setting a Hash to null set an empty hash
148
148
  new_value = {} if new_value.eql?(nil) && @types&.first.eql?(Hash)
149
- # Setting a Array to null set an empty hash
149
+ # Setting a Array to null set an empty array
150
150
  new_value = [] if new_value.eql?(nil) && @types&.first.eql?(Array)
151
151
  if @types.eql?(Aspera::Cli::Allowed::TYPES_SYMBOL_ARRAY)
152
152
  new_value = [new_value] if new_value.is_a?(String)
@@ -204,6 +204,15 @@ module Aspera
204
204
  def option_name_to_line(name)
205
205
  return "#{OPTION_PREFIX}#{name.to_s.gsub(OPTION_SEP_SYMBOL, OPTION_SEP_LINE)}"
206
206
  end
207
+
208
+ # @return [Hash{Symbol => String}, nil] `{field:,value:}` if identifier is a percent selector, else `nil`
209
+ def percent_selector(identifier)
210
+ Aspera.assert_type(identifier, String)
211
+ if (m = identifier.match(REGEX_LOOKUP_ID_BY_FIELD))
212
+ return {field: m[1], value: ExtendedValue.instance.evaluate(m[2], context: "percent selector: #{m[1]}")}
213
+ end
214
+ nil
215
+ end
207
216
  end
208
217
 
209
218
  attr_reader :parser
@@ -281,7 +290,7 @@ module Aspera
281
290
  Aspera.assert(!@declared_options.key?(option_symbol)){"#{option_symbol} already declared"}
282
291
  Aspera.assert(description[-1] != '.'){"#{option_symbol} ends with dot"}
283
292
  Aspera.assert(description[0] == description[0].upcase){"#{option_symbol} description does not start with an uppercase"}
284
- Aspera.assert(!['hash', 'extended value'].any?{ |s| description.downcase.include?(s)}){"#{option_symbol} shall use :allowed"}
293
+ Aspera.assert(!['hash', 'extended value'].any?{ |s| description.downcase.include?(s)}){"#{option_symbol} shall use :allowed instead of hash/extended value in option description"}
285
294
  Aspera.assert_type(handler, Hash) if handler
286
295
  Aspera.assert(handler.keys.sort.eql?(%i[m o])) if handler
287
296
  option_attrs = @declared_options[option_symbol] = OptionValue.new(
@@ -390,6 +399,21 @@ module Aspera
390
399
  return result
391
400
  end
392
401
 
402
+ # Resource identifier as positional parameter
403
+ #
404
+ # @param description [String] description of the identifier
405
+ # @param block [Proc] block to search for identifier based on attribute value
406
+ # @return [String, Array] identifier or list of ids
407
+ def instance_identifier(description: 'identifier', &block)
408
+ res_id = get_next_argument(description, multiple: get_option(:bulk)) if res_id.nil?
409
+ # Can be an Array
410
+ if res_id.is_a?(String) && (m = Manager.percent_selector(res_id))
411
+ Aspera.assert(block_given?, type: Cli::BadArgument){"Percent syntax for #{description} not supported in this context"}
412
+ res_id = yield(m[:field], m[:value])
413
+ end
414
+ return res_id
415
+ end
416
+
393
417
  def get_next_command(command_list, aliases: nil); return get_next_argument('command', accept_list: command_list, aliases: aliases); end
394
418
 
395
419
  # Get an option value by name
@@ -508,7 +532,7 @@ module Aspera
508
532
  begin
509
533
  # remove known options one by one, exception if unknown
510
534
  Log.log.trace1('Before parse')
511
- Log.dump(:unprocessed_cmd_line_options, @unprocessed_cmd_line_options)
535
+ Log.dump(:unprocessed_cmd_line_options, @unprocessed_cmd_line_options, level: :trace1)
512
536
  @parser.parse!(@unprocessed_cmd_line_options)
513
537
  Log.log.trace1('After parse')
514
538
  rescue OptionParser::InvalidOption => e
@@ -678,8 +702,10 @@ module Aspera
678
702
  # when this is alone, this stops option processing
679
703
  OPTIONS_STOP = '--'
680
704
  SOURCE_USER = 'cmdline' # cspell:disable-line
705
+ # Percent selector: select by this field for this value
706
+ REGEX_LOOKUP_ID_BY_FIELD = /^%([^:]+):(.*)$/
681
707
 
682
- private_constant :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :OPTION_VALUE_SEPARATOR, :OPTION_PREFIX, :OPTIONS_STOP, :SOURCE_USER
708
+ private_constant :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :OPTION_VALUE_SEPARATOR, :OPTION_PREFIX, :OPTIONS_STOP, :SOURCE_USER, :REGEX_LOOKUP_ID_BY_FIELD
683
709
  end
684
710
  end
685
711
  end