aspera-cli 4.20.0 → 4.21.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.

Potentially problematic release.


This version of aspera-cli might be problematic. Click here for more details.

Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +20 -2
  4. data/README.md +281 -156
  5. data/bin/asession +2 -2
  6. data/lib/aspera/agent/alpha.rb +7 -12
  7. data/lib/aspera/agent/connect.rb +19 -1
  8. data/lib/aspera/agent/direct.rb +20 -29
  9. data/lib/aspera/agent/node.rb +1 -11
  10. data/lib/aspera/agent/trsdk.rb +4 -25
  11. data/lib/aspera/api/aoc.rb +5 -0
  12. data/lib/aspera/api/node.rb +45 -28
  13. data/lib/aspera/ascp/installation.rb +69 -38
  14. data/lib/aspera/ascp/management.rb +27 -6
  15. data/lib/aspera/cli/formatter.rb +149 -141
  16. data/lib/aspera/cli/info.rb +1 -1
  17. data/lib/aspera/cli/manager.rb +1 -0
  18. data/lib/aspera/cli/plugin.rb +2 -2
  19. data/lib/aspera/cli/plugins/aoc.rb +27 -17
  20. data/lib/aspera/cli/plugins/config.rb +31 -21
  21. data/lib/aspera/cli/plugins/faspex.rb +1 -1
  22. data/lib/aspera/cli/plugins/faspex5.rb +11 -3
  23. data/lib/aspera/cli/plugins/node.rb +44 -38
  24. data/lib/aspera/cli/version.rb +1 -1
  25. data/lib/aspera/command_line_builder.rb +1 -1
  26. data/lib/aspera/environment.rb +5 -6
  27. data/lib/aspera/node_simulator.rb +228 -112
  28. data/lib/aspera/oauth/base.rb +31 -42
  29. data/lib/aspera/oauth/factory.rb +41 -2
  30. data/lib/aspera/persistency_folder.rb +20 -2
  31. data/lib/aspera/preview/generator.rb +1 -1
  32. data/lib/aspera/preview/utils.rb +1 -1
  33. data/lib/aspera/products/alpha.rb +30 -0
  34. data/lib/aspera/products/connect.rb +48 -0
  35. data/lib/aspera/products/other.rb +82 -0
  36. data/lib/aspera/products/trsdk.rb +54 -0
  37. data/lib/aspera/rest.rb +18 -13
  38. data/lib/aspera/ssh.rb +28 -24
  39. data/lib/aspera/transfer/spec.yaml +22 -20
  40. data.tar.gz.sig +0 -0
  41. metadata +21 -4
  42. metadata.gz.sig +0 -0
  43. data/lib/aspera/ascp/products.rb +0 -168
@@ -3,11 +3,16 @@
3
3
  # cspell:ignore protobuf ckpt
4
4
  require 'aspera/environment'
5
5
  require 'aspera/data_repository'
6
- require 'aspera/ascp/products'
7
6
  require 'aspera/log'
8
7
  require 'aspera/rest'
9
8
  require 'aspera/assert'
10
9
  require 'aspera/web_server_simple'
10
+ require 'aspera/cli/info'
11
+ require 'aspera/cli/version'
12
+ require 'aspera/products/alpha'
13
+ require 'aspera/products/connect'
14
+ require 'aspera/products/trsdk'
15
+ require 'aspera/products/other'
11
16
  require 'English'
12
17
  require 'singleton'
13
18
  require 'xmlsimple'
@@ -20,7 +25,7 @@ module Aspera
20
25
  # Singleton that tells where to find ascp and other local resources (keys..) , using the "path(:name)" method.
21
26
  # It is used by object : AgentDirect to find necessary resources
22
27
  # By default it takes the first Aspera product found
23
- # but the user can specify ascp location by calling:
28
+ # The user can specify ascp location by calling:
24
29
  # Installation.instance.use_ascp_from_product(product_name)
25
30
  # or
26
31
  # Installation.instance.ascp_path=""
@@ -46,12 +51,16 @@ module Aspera
46
51
  TRANSFER_SDK_LOCATION_URL = 'https://ibm.biz/sdk_location'
47
52
  FILE_SCHEME_PREFIX = 'file:///'
48
53
  SDK_ARCHIVE_FOLDERS = ['/bin/', '/aspera/'].freeze
54
+ # filename for ascp with optional extension (Windows)
49
55
  private_constant :EXT_RUBY_PROTOBUF, :RB_SDK_SUBFOLDER, :DEFAULT_ASPERA_CONF, :FILES, :TRANSFER_SDK_LOCATION_URL, :FILE_SCHEME_PREFIX
50
56
  # options for SSH client private key
51
57
  CLIENT_SSH_KEY_OPTIONS = %i{dsa_rsa rsa per_client}.freeze
52
58
 
53
59
  # set ascp executable path
54
60
  def ascp_path=(v)
61
+ Aspera.assert_type(v, String)
62
+ Aspera.assert(!v.empty?) {'ascp path cannot be empty: check your config file'}
63
+ Aspera.assert(File.exist?(v)) {"No such file: [#{v}]"}
55
64
  @path_to_ascp = v
56
65
  end
57
66
 
@@ -59,31 +68,19 @@ module Aspera
59
68
  path(:ascp)
60
69
  end
61
70
 
62
- # location of SDK files
71
+ # Compatibility
63
72
  def sdk_folder=(v)
64
- Log.log.debug{"sdk_folder=#{v}"}
65
- @sdk_dir = v
66
- sdk_folder
67
- end
68
-
69
- # backward compatibility in sample program
70
- alias_method :folder=, :sdk_folder=
71
-
72
- # @return the path to folder where SDK is installed
73
- def sdk_folder
74
- raise 'SDK path was ot initialized' if @sdk_dir.nil?
75
- FileUtils.mkdir_p(@sdk_dir)
76
- @sdk_dir
73
+ Products::Trsdk.sdk_directory = v
77
74
  end
78
75
 
79
76
  # find ascp in named product (use value : FIRST_FOUND='FIRST' to just use first one)
80
- # or select one from Products.installed_products()
77
+ # or select one from installed_products()
81
78
  def use_ascp_from_product(product_name)
82
79
  if product_name.eql?(FIRST_FOUND)
83
- pl = Products.installed_products.first
84
- raise "ascp found: no Aspera transfer module or SDK found.\nRefer to the manual or install SDK with command:\nascli conf ascp install" if pl.nil?
80
+ pl = installed_products.first
81
+ raise "no Aspera transfer module or SDK found.\nRefer to the manual or install SDK with command:\nascli conf ascp install" if pl.nil?
85
82
  else
86
- pl = Products.installed_products.find{|i|i[:name].eql?(product_name)}
83
+ pl = installed_products.find{|i|i[:name].eql?(product_name)}
87
84
  raise "no such product installed: #{product_name}" if pl.nil?
88
85
  end
89
86
  self.ascp_path = pl[:ascp_path]
@@ -103,7 +100,7 @@ module Aspera
103
100
  end
104
101
 
105
102
  def check_or_create_sdk_file(filename, force: false, &block)
106
- return Environment.write_file_restricted(File.join(sdk_folder, filename), force: force, mode: 0o644, &block)
103
+ return Environment.write_file_restricted(File.join(Products::Trsdk.sdk_directory, filename), force: force, mode: 0o644, &block)
107
104
  end
108
105
 
109
106
  # get path of one resource file of currently activated product
@@ -118,7 +115,7 @@ module Aspera
118
115
  file = @path_to_ascp.gsub('ascp', k.to_s)
119
116
  when :transferd
120
117
  file_is_optional = true
121
- file = transferd_filepath
118
+ file = Products::Trsdk.transferd_path
122
119
  when :ssh_private_dsa, :ssh_private_rsa
123
120
  # assume last 3 letters are type
124
121
  type = k.to_s[-3..-1].to_sym
@@ -128,8 +125,8 @@ module Aspera
128
125
  when :aspera_conf
129
126
  file = check_or_create_sdk_file('aspera.conf') {DEFAULT_ASPERA_CONF}
130
127
  when :fallback_certificate, :fallback_private_key
131
- file_key = File.join(sdk_folder, 'aspera_fallback_cert_private_key.pem')
132
- file_cert = File.join(sdk_folder, 'aspera_fallback_cert.pem')
128
+ file_key = File.join(Products::Trsdk.sdk_directory, 'aspera_fallback_cert_private_key.pem')
129
+ file_cert = File.join(Products::Trsdk.sdk_directory, 'aspera_fallback_cert.pem')
133
130
  if !File.exist?(file_key) || !File.exist?(file_cert)
134
131
  require 'openssl'
135
132
  # create new self signed certificate for http fallback
@@ -143,7 +140,7 @@ module Aspera
143
140
  else Aspera.error_unexpected_value(k)
144
141
  end
145
142
  return nil if file_is_optional && !File.exist?(file)
146
- Aspera.assert(File.exist?(file)){"no such file: #{file}"}
143
+ Aspera.assert(File.exist?(file)){"no such file for #{k}: [#{file}]"}
147
144
  return file
148
145
  end
149
146
 
@@ -239,7 +236,14 @@ module Aspera
239
236
  # Loads YAML from cloud with locations of SDK archives for all platforms
240
237
  # @return location structure
241
238
  def sdk_locations
242
- yaml_text = Aspera::Rest.new(base_url: TRANSFER_SDK_LOCATION_URL, redirect_max: 3).call(operation: 'GET')[:data]
239
+ yaml_text = Aspera::Rest.new(
240
+ base_url: TRANSFER_SDK_LOCATION_URL,
241
+ redirect_max: 3).call(
242
+ operation: 'GET',
243
+ headers: {
244
+ 'Referer' => "http://version.#{Cli::VERSION}"
245
+ }
246
+ )[:data]
243
247
  YAML.load(yaml_text)
244
248
  end
245
249
 
@@ -255,6 +259,7 @@ module Aspera
255
259
  return info.first['url']
256
260
  end
257
261
 
262
+ # @param &block called with entry information
258
263
  def extract_archive_files(sdk_archive_path)
259
264
  raise 'missing block' unless block_given?
260
265
  case sdk_archive_path
@@ -265,7 +270,7 @@ module Aspera
265
270
  Zip::File.open(sdk_archive_path) do |zip_file|
266
271
  zip_file.each do |entry|
267
272
  next if entry.name.end_with?('/')
268
- yield(entry.name, entry.get_input_stream)
273
+ yield(entry.name, entry.get_input_stream, nil)
269
274
  end
270
275
  end
271
276
  # Other Unixes use tar.gz
@@ -276,7 +281,7 @@ module Aspera
276
281
  Gem::Package::TarReader.new(gzip) do |tar|
277
282
  tar.each do |entry|
278
283
  next if entry.directory?
279
- yield(entry.full_name, entry)
284
+ yield(entry.full_name, entry, entry.symlink? ? entry.header.linkname : nil)
280
285
  end
281
286
  end
282
287
  end
@@ -287,11 +292,15 @@ module Aspera
287
292
 
288
293
  # download aspera SDK or use local file
289
294
  # extracts ascp binary for current system architecture
290
- # @param url [String] URL to SDK archive, or SpecialValues::DEF
295
+ # @param url [String] URL to SDK archive, or SpecialValues::DEF
296
+ # @param folder [String] destination
297
+ # @param backup [Bool]
298
+ # @param with_exe [Bool]
299
+ # @param &block [Proc] a lambda that receives a file path from archive and tells detination sub folder, or nil to not extract
291
300
  # @return ascp version (from execution)
292
301
  def install_sdk(url: nil, folder: nil, backup: true, with_exe: true, &block)
293
302
  url = sdk_url_for_platform if url.nil? || url.eql?('DEF')
294
- folder = sdk_folder if folder.nil?
303
+ folder = Products::Trsdk.sdk_directory if folder.nil?
295
304
  subfolder_lambda = block
296
305
  if subfolder_lambda.nil?
297
306
  subfolder_lambda = ->(name) do
@@ -318,13 +327,16 @@ module Aspera
318
327
  File.rename(folder, "#{folder}.#{Time.now.strftime('%Y%m%d%H%M%S')}")
319
328
  # TODO: delete old archives ?
320
329
  end
321
- extract_archive_files(sdk_archive_path) do |entry_name, entry_stream|
330
+ extract_archive_files(sdk_archive_path) do |entry_name, entry_stream, link_target|
322
331
  subfolder = subfolder_lambda.call(entry_name)
323
332
  next if subfolder.nil?
324
333
  dest_folder = File.join(folder, subfolder)
325
334
  FileUtils.mkdir_p(dest_folder)
326
- File.open(File.join(dest_folder, File.basename(entry_name)), 'wb') do |output_stream|
327
- IO.copy_stream(entry_stream, output_stream)
335
+ new_file = File.join(dest_folder, File.basename(entry_name))
336
+ if link_target.nil?
337
+ File.open(new_file, 'wb') { |output_stream|IO.copy_stream(entry_stream, output_stream)}
338
+ else
339
+ File.symlink(link_target, new_file)
328
340
  end
329
341
  end
330
342
  File.unlink(sdk_archive_path) rescue nil if delete_archive # Windows may give error
@@ -332,7 +344,7 @@ module Aspera
332
344
  # ensure license file are generated so that ascp invocation for version works
333
345
  path(:aspera_license)
334
346
  path(:aspera_conf)
335
- sdk_ascp_file = Products.ascp_filename
347
+ sdk_ascp_file = Environment.exe_file('ascp')
336
348
  sdk_ascp_path = File.join(folder, sdk_ascp_file)
337
349
  raise "No #{sdk_ascp_file} found in SDK archive" unless File.exist?(sdk_ascp_path)
338
350
  EXE_FILES.each do |exe_sym|
@@ -340,13 +352,13 @@ module Aspera
340
352
  Environment.restrict_file_access(exe_path, mode: 0o755) if File.exist?(exe_path)
341
353
  end
342
354
  sdk_ascp_version = get_ascp_version(sdk_ascp_path)
343
- sdk_daemon_path = transferd_filepath
355
+ sdk_daemon_path = Products::Trsdk.transferd_path
344
356
  Log.log.warn{"No #{sdk_daemon_path} in SDK archive"} unless File.exist?(sdk_daemon_path)
345
357
  Environment.restrict_file_access(sdk_daemon_path, mode: 0o755) if File.exist?(sdk_daemon_path)
346
358
  transferd_version = get_exe_version(sdk_daemon_path, 'version')
347
359
  sdk_name = 'IBM Aspera Transfer SDK'
348
360
  sdk_version = transferd_version || sdk_ascp_version
349
- File.write(File.join(folder, Products::INFO_META_FILE), "<product><name>#{sdk_name}</name><version>#{sdk_version}</version></product>")
361
+ File.write(File.join(folder, Products::Other::INFO_META_FILE), "<product><name>#{sdk_name}</name><version>#{sdk_version}</version></product>")
350
362
  return sdk_name, sdk_version
351
363
  end
352
364
 
@@ -358,10 +370,29 @@ module Aspera
358
370
  def initialize
359
371
  @path_to_ascp = nil
360
372
  @sdk_dir = nil
373
+ @found_products = nil
361
374
  end
362
375
 
363
- def transferd_filepath
364
- return File.join(sdk_folder, 'asperatransferd' + Environment.exe_extension) # cspell:disable-line
376
+ public
377
+
378
+ # @return the list of installed products in format of product_locations_on_current_os
379
+ def installed_products
380
+ if @found_products.nil?
381
+ # :expected M app name is taken from the manifest if present, else defaults to this value
382
+ # :app_root M main folder for the application
383
+ # :log_root O location of log files (Linux uses syslog)
384
+ # :run_root O only for Connect Client, location of http port file
385
+ # :sub_bin O subfolder with executables, default : bin
386
+ scan_locations = Products::Trsdk.locations.concat(
387
+ Products::Alpha.locations,
388
+ Products::Connect.locations,
389
+ Products::Other::LOCATION_ON_THIS_OS
390
+ )
391
+ # .each {|item| item.deep_do {|h, _k, _v, _m|h.freeze}}.freeze
392
+ # search installed products: with ascp
393
+ @found_products = Products::Other.find(scan_locations)
394
+ end
395
+ return @found_products
365
396
  end
366
397
  end
367
398
  end
@@ -198,18 +198,39 @@ module Aspera
198
198
  BOOLEAN_FIELDS = %w[Encryption Remote RateLock MinRateLock PolicyLock FilesEncrypt FilesDecrypt VLinkLocalEnabled VLinkRemoteEnabled
199
199
  MoveRange Keepalive TestLogin UseProxy Precalc RTTAutocorrect].freeze
200
200
  BOOLEAN_TRUE = 'Yes'
201
+
202
+ private_constant :OPERATIONS, :PARAMETERS, :MGT_HEADER, :MGT_FRAME_SEPARATOR, :INTEGER_FIELDS, :BOOLEAN_FIELDS, :BOOLEAN_TRUE
201
203
  # cspell: enable
202
204
 
203
205
  class << self
204
206
  # translates mgt port event into (enhanced) typed event
205
207
  def enhanced_event_format(event)
206
208
  return event.keys.each_with_object({}) do |e, h|
207
- new_name = e.capital_to_snake.gsub(/(usec)$/, '_\1').downcase
208
- value = event[e]
209
- value = value.to_i if INTEGER_FIELDS.include?(e)
210
- value = value.eql?(BOOLEAN_TRUE) if BOOLEAN_FIELDS.include?(e)
211
- h[new_name] = value
212
- end
209
+ new_name =
210
+ case e
211
+ when 'Elapsedusec' then 'elapsed_usec'
212
+ when 'Bytescont' then 'bytes_cont'
213
+ else e.capital_to_snake
214
+ end
215
+ h[new_name] =
216
+ if INTEGER_FIELDS.include?(e) then event[e].to_i
217
+ elsif BOOLEAN_FIELDS.include?(e) then event[e].eql?(BOOLEAN_TRUE)
218
+ else
219
+ event[e]
220
+ end
221
+ end
222
+ end
223
+
224
+ # build command to send on management port
225
+ # @param data [Hash] {'type'=>'START','source'=>_path_,'destination'=>_path_}
226
+ def command_to_stream(data)
227
+ # TODO: translate enhanced to capitalized ?
228
+ data
229
+ .keys
230
+ .map{|k|"#{k.capitalize}: #{data[k]}"}
231
+ .unshift(MGT_HEADER)
232
+ .push('', '')
233
+ .join("\n")
213
234
  end
214
235
  end
215
236
 
@@ -101,9 +101,10 @@ module Aspera
101
101
  DISPLAY_FORMATS = %i[text nagios ruby json jsonpp yaml table csv image].freeze
102
102
  # user output levels
103
103
  DISPLAY_LEVELS = %i[info data error].freeze
104
- FIELD_VALUE_HEADINGS = %i[key value].freeze
104
+ # column names for single object display in table
105
+ SINGLE_OBJECT_COLUMN_NAMES = %i[field value].freeze
105
106
 
106
- private_constant :DISPLAY_FORMATS, :DISPLAY_LEVELS, :CSV_RECORD_SEPARATOR, :CSV_FIELD_SEPARATOR, :FIELD_VALUE_HEADINGS
107
+ private_constant :FIELDS_LESS, :CSV_RECORD_SEPARATOR, :CSV_FIELD_SEPARATOR, :DISPLAY_FORMATS, :DISPLAY_LEVELS, :SINGLE_OBJECT_COLUMN_NAMES
107
108
  # prefix to display error messages in user messages (terminal)
108
109
  ERROR_FLASH = 'ERROR:'.bg_red.gray.blink.freeze
109
110
  WARNING_FLASH = 'WARNING:'.bg_brown.black.blink.freeze
@@ -172,12 +173,37 @@ module Aspera
172
173
  @spinner = nil
173
174
  end
174
175
 
175
- # options are: format, output, display, fields, select, table_style, flat_hash, transpose_single
176
+ def declare_options(options)
177
+ default_table_style = if Environment.instance.terminal_supports_unicode?
178
+ {border: :unicode_round}
179
+ else
180
+ {}
181
+ end
182
+ options.declare(:format, 'Output format', values: DISPLAY_FORMATS, handler: {o: self, m: :option_handler}, default: :table)
183
+ options.declare(:output, 'Destination for results', types: String, handler: {o: self, m: :option_handler})
184
+ options.declare(:display, 'Output only some information', values: DISPLAY_LEVELS, handler: {o: self, m: :option_handler}, default: :info)
185
+ options.declare(
186
+ :fields, "Comma separated list of: fields, or #{SpecialValues::ALL}, or #{SpecialValues::DEF}", handler: {o: self, m: :option_handler},
187
+ types: [String, Array, Regexp, Proc],
188
+ default: SpecialValues::DEF)
189
+ options.declare(:select, 'Select only some items in lists: column, value', types: [Hash, Proc], handler: {o: self, m: :option_handler})
190
+ options.declare(:table_style, 'Table display style', types: [Hash], handler: {o: self, m: :option_handler}, default: default_table_style)
191
+ options.declare(:flat_hash, '(Table) Display deep values as additional keys', values: :bool, handler: {o: self, m: :option_handler}, default: true)
192
+ options.declare(
193
+ :multi_single, '(Table) Control how object list is displayed as single table, or multiple objects', values: %i[no yes single],
194
+ handler: {o: self, m: :option_handler}, default: :no)
195
+ options.declare(:show_secrets, 'Show secrets on command output', values: :bool, handler: {o: self, m: :option_handler}, default: false)
196
+ options.declare(:image, 'Options for image display', types: Hash, handler: {o: self, m: :option_handler}, default: {})
197
+ end
198
+
199
+ # method accessed by option manager
200
+ # options are: format, output, display, fields, select, table_style, flat_hash, multi_single
176
201
  def option_handler(option_symbol, operation, value=nil)
177
202
  Aspera.assert_values(operation, %i[set get])
178
203
  case operation
179
204
  when :set
180
205
  @options[option_symbol] = value
206
+ # special handling of some options
181
207
  case option_symbol
182
208
  when :output
183
209
  $stdout = if value.eql?('-')
@@ -186,7 +212,9 @@ module Aspera
186
212
  File.open(value, 'w')
187
213
  end
188
214
  when :image
215
+ # get list if key arguments of method
189
216
  allowed_options = Preview::Terminal.method(:build).parameters.select{|i|i[0].eql?(:key)}.map{|i|i[1]}
217
+ # check that only supported options are given
190
218
  unknown_options = value.keys.map(&:to_sym) - allowed_options
191
219
  raise "Invalid parameter(s) for option image: #{unknown_options.join(', ')}, use #{allowed_options.join(', ')}" unless unknown_options.empty?
192
220
  end
@@ -196,28 +224,6 @@ module Aspera
196
224
  nil
197
225
  end
198
226
 
199
- def declare_options(options)
200
- default_table_style = if Environment.instance.terminal_supports_unicode?
201
- {border: :unicode_round}
202
- else
203
- {}
204
- end
205
- options.declare(:format, 'Output format', values: DISPLAY_FORMATS, handler: {o: self, m: :option_handler}, default: :table)
206
- options.declare(:output, 'Destination for results', types: String, handler: {o: self, m: :option_handler})
207
- options.declare(:display, 'Output only some information', values: DISPLAY_LEVELS, handler: {o: self, m: :option_handler}, default: :info)
208
- options.declare(
209
- :fields, "Comma separated list of: fields, or #{SpecialValues::ALL}, or #{SpecialValues::DEF}", handler: {o: self, m: :option_handler},
210
- types: [String, Array, Regexp, Proc],
211
- default: SpecialValues::DEF)
212
- options.declare(:select, 'Select only some items in lists: column, value', types: [Hash, Proc], handler: {o: self, m: :option_handler})
213
- options.declare(:table_style, 'Table display style', types: [Hash], handler: {o: self, m: :option_handler}, default: default_table_style)
214
- options.declare(:flat_hash, '(Table) Display deep values as additional keys', values: :bool, handler: {o: self, m: :option_handler}, default: true)
215
- options.declare(:transpose_single, '(Table) Single object fields output vertically', values: :bool, handler: {o: self, m: :option_handler}, default: true)
216
- options.declare(:multi_table, '(Table) Each element of a table are displayed as a table', values: :bool, handler: {o: self, m: :option_handler}, default: false)
217
- options.declare(:show_secrets, 'Show secrets on command output', values: :bool, handler: {o: self, m: :option_handler}, default: false)
218
- options.declare(:image, 'Options for image display', types: Hash, handler: {o: self, m: :option_handler}, default: {})
219
- end
220
-
221
227
  # main output method
222
228
  # data: for requested data, not displayed if level==error
223
229
  # info: additional info, displayed if level==info
@@ -244,6 +250,118 @@ module Aspera
244
250
  display_status(count_msg)
245
251
  end
246
252
 
253
+ # this method displays the results, especially the table format
254
+ # @param type [Symbol] type of data
255
+ # @param data [Object] data to display
256
+ # @param total [Integer] total number of items
257
+ # @param fields [Array<String>] list of fields to display
258
+ # @param name [String] name of the column to display
259
+ def display_results(type:, data: nil, total: nil, fields: nil, name: nil)
260
+ Log.log.debug{"display_results: #{type} class=#{data.class}"}
261
+ Log.log.trace1{"display_results:data=#{data}"}
262
+ Aspera.assert_type(type, Symbol){'result must have type'}
263
+ Aspera.assert(!data.nil? || %i[empty nothing].include?(type)){'result must have data'}
264
+ display_item_count(data.length, total) unless total.nil?
265
+ SecretHider.deep_remove_secret(data) unless @options[:show_secrets] || @options[:display].eql?(:data)
266
+ case @options[:format]
267
+ when :text
268
+ display_message(:data, data.to_s)
269
+ when :nagios
270
+ Nagios.process(data)
271
+ when :ruby
272
+ display_message(:data, PP.pp(filter_list_on_fields(data), +''))
273
+ when :json
274
+ display_message(:data, JSON.generate(filter_list_on_fields(data)))
275
+ when :jsonpp
276
+ display_message(:data, JSON.pretty_generate(filter_list_on_fields(data)))
277
+ when :yaml
278
+ display_message(:data, YAML.dump(filter_list_on_fields(data)))
279
+ when :image
280
+ # assume it is an url
281
+ url = data
282
+ case type
283
+ when :single_object, :object_list
284
+ url = [url] if type.eql?(:single_object)
285
+ raise 'image display requires a single result' unless url.length == 1
286
+ fields = compute_fields(url, fields)
287
+ raise 'select a field to display' unless fields.length == 1
288
+ url = url.first
289
+ raise 'no such field' unless url.key?(fields.first)
290
+ url = url[fields.first]
291
+ end
292
+ raise "not url: #{url.class} #{url}" unless url.is_a?(String)
293
+ display_message(:data, status_image(url))
294
+ when :table, :csv
295
+ case type
296
+ when :config_over
297
+ display_table(Flattener.new(self).config_over(data), CONF_OVERVIEW_KEYS)
298
+ when :object_list, :single_object
299
+ obj_list = data
300
+ if type.eql?(:single_object)
301
+ obj_list = [obj_list]
302
+ @options[:multi_single] = :yes
303
+ end
304
+ Aspera.assert_type(obj_list, Array)
305
+ Aspera.assert(obj_list.all?(Hash)){"expecting Array of Hash: #{obj_list.inspect}"}
306
+ # :object_list is an array of hash tables, where key=colum name
307
+ obj_list = obj_list.map{|obj|Flattener.new(self).flatten(obj)} if @options[:flat_hash]
308
+ display_table(obj_list, compute_fields(obj_list, fields))
309
+ when :value_list
310
+ # :value_list is a simple array of values, name of column provided in the :name
311
+ display_table(data.map { |i| { name => i } }, [name])
312
+ when :empty # no table
313
+ display_message(:info, special_format('empty'))
314
+ return
315
+ when :nothing # no result expected
316
+ Log.log.debug('no result expected')
317
+ when :status # no table
318
+ # :status displays a simple message
319
+ display_message(:info, data)
320
+ when :text # no table
321
+ # :status displays a simple message
322
+ display_message(:data, data)
323
+ when :other_struct # no table
324
+ # :other_struct is any other type of structure
325
+ display_message(:data, PP.pp(data, +''))
326
+ else
327
+ raise "unknown data type: #{type}"
328
+ end
329
+ else
330
+ raise "not expected: #{@options[:format]}"
331
+ end
332
+ end
333
+
334
+ # @return text suitable to display an image from url
335
+ # # can be used in
336
+ def status_image(blob)
337
+ begin
338
+ raise URI::InvalidURIError, 'not uri' if !(blob =~ /\A#{URI::RFC2396_PARSER.make_regexp}\z/)
339
+ # it's a url
340
+ url = blob
341
+ unless Environment.instance.url_method.eql?(:text)
342
+ Environment.instance.open_uri(url)
343
+ return ''
344
+ end
345
+ # remote_image = Rest.new(base_url: url).read('')
346
+ # mime = remote_image[:http]['content-type']
347
+ # blob = remote_image[:http].body
348
+ # Log.log.warn("Image ? #{remote_image[:http]['content-type']}") unless mime.include?('image/')
349
+ blob = UriReader.read(url)
350
+ rescue URI::InvalidURIError
351
+ nil
352
+ end
353
+ # try base64
354
+ begin
355
+ blob = Base64.strict_decode64(blob)
356
+ rescue
357
+ nil
358
+ end
359
+ return Preview::Terminal.build(blob, **@options[:image].symbolize_keys)
360
+ end
361
+ #==========================================================================================
362
+
363
+ private
364
+
247
365
  def all_fields(data)
248
366
  data.each_with_object({}){|v, m|v.each_key{|c|m[c] = true}}.keys
249
367
  end
@@ -314,9 +432,9 @@ module Aspera
314
432
  end
315
433
  end
316
434
 
317
- # this method displays a table
318
- # object_array: array of hash
319
- # fields: list of column names
435
+ # displays a list of objects
436
+ # @param object_array [Array] array of hash
437
+ # @param fields [Array] list of column names
320
438
  def display_table(object_array, fields)
321
439
  Aspera.assert(!fields.nil?){'missing fields parameter'}
322
440
  filter_columns_on_select(object_array)
@@ -330,13 +448,6 @@ module Aspera
330
448
  display_message(:data, object_array.first[fields.first])
331
449
  return
332
450
  end
333
- single_transposed = @options[:transpose_single] && object_array.length == 1
334
- # Special case if only one row (it could be object_list or single_object)
335
- if single_transposed
336
- single = object_array.first
337
- object_array = fields.map { |i| FIELD_VALUE_HEADINGS.zip([i, single[i]]).to_h }
338
- fields = FIELD_VALUE_HEADINGS
339
- end
340
451
  Log.log.debug{Log.dump(:object_array, object_array)}
341
452
  # convert data to string, and keep only display fields
342
453
  final_table_rows = object_array.map { |r| fields.map { |c| r[c].to_s } }
@@ -345,11 +456,12 @@ module Aspera
345
456
  # here : fields : list of column names
346
457
  case @options[:format]
347
458
  when :table
348
- if @options[:multi_table] && !single_transposed
459
+ if @options[:multi_single].eql?(:yes) ||
460
+ (@options[:multi_single].eql?(:single) && final_table_rows.length.eql?(1))
349
461
  final_table_rows.each do |row|
350
462
  Log.log.debug{Log.dump(:row, row)}
351
463
  display_message(:data, Terminal::Table.new(
352
- headings: FIELD_VALUE_HEADINGS,
464
+ headings: SINGLE_OBJECT_COLUMN_NAMES,
353
465
  rows: fields.zip(row),
354
466
  style: @options[:table_style]&.symbolize_keys))
355
467
  end
@@ -366,110 +478,6 @@ module Aspera
366
478
  raise "not expected: #{@options[:format]}"
367
479
  end
368
480
  end
369
-
370
- # @return text suitable to display an image from url
371
- def status_image(blob)
372
- begin
373
- raise URI::InvalidURIError, 'not uri' if !(blob =~ /\A#{URI::RFC2396_PARSER.make_regexp}\z/)
374
- # it's a url
375
- url = blob
376
- unless Environment.instance.url_method.eql?(:text)
377
- Environment.instance.open_uri(url)
378
- return ''
379
- end
380
- # remote_image = Rest.new(base_url: url).read('')
381
- # mime = remote_image[:http]['content-type']
382
- # blob = remote_image[:http].body
383
- # Log.log.warn("Image ? #{remote_image[:http]['content-type']}") unless mime.include?('image/')
384
- blob = UriReader.read(url)
385
- rescue URI::InvalidURIError
386
- nil
387
- end
388
- # try base64
389
- begin
390
- blob = Base64.strict_decode64(blob)
391
- rescue
392
- nil
393
- end
394
- return Preview::Terminal.build(blob, **@options[:image].symbolize_keys)
395
- end
396
-
397
- # this method displays the results, especially the table format
398
- # @param type [Symbol] type of data
399
- # @param data [Object] data to display
400
- # @param total [Integer] total number of items
401
- # @param fields [Array<String>] list of fields to display
402
- # @param name [String] name of the column to display
403
- def display_results(type:, data: nil, total: nil, fields: nil, name: nil)
404
- Log.log.debug{"display_results: #{type} class=#{data.class} data=#{data}"}
405
- Aspera.assert_type(type, Symbol){'result must have type'}
406
- Aspera.assert(!data.nil? || %i[empty nothing].include?(type)){'result must have data'}
407
- display_item_count(data.length, total) unless total.nil?
408
- SecretHider.deep_remove_secret(data) unless @options[:show_secrets] || @options[:display].eql?(:data)
409
- case @options[:format]
410
- when :text
411
- display_message(:data, data.to_s)
412
- when :nagios
413
- Nagios.process(data)
414
- when :ruby
415
- display_message(:data, PP.pp(filter_list_on_fields(data), +''))
416
- when :json
417
- display_message(:data, JSON.generate(filter_list_on_fields(data)))
418
- when :jsonpp
419
- display_message(:data, JSON.pretty_generate(filter_list_on_fields(data)))
420
- when :yaml
421
- display_message(:data, YAML.dump(filter_list_on_fields(data)))
422
- when :image
423
- # assume it is an url
424
- url = data
425
- case type
426
- when :single_object, :object_list
427
- url = [url] if type.eql?(:single_object)
428
- raise 'image display requires a single result' unless url.length == 1
429
- fields = compute_fields(url, fields)
430
- raise 'select a field to display' unless fields.length == 1
431
- url = url.first
432
- raise 'no such field' unless url.key?(fields.first)
433
- url = url[fields.first]
434
- end
435
- raise "not url: #{url.class} #{url}" unless url.is_a?(String)
436
- display_message(:data, status_image(url))
437
- when :table, :csv
438
- case type
439
- when :config_over
440
- display_table(Flattener.new(self).config_over(data), CONF_OVERVIEW_KEYS)
441
- when :object_list, :single_object
442
- obj_list = data
443
- obj_list = [obj_list] if type.eql?(:single_object)
444
- Aspera.assert_type(obj_list, Array)
445
- Aspera.assert(obj_list.all?(Hash)){"expecting Array of Hash: #{obj_list.inspect}"}
446
- # :object_list is an array of hash tables, where key=colum name
447
- obj_list = obj_list.map{|obj|Flattener.new(self).flatten(obj)} if @options[:flat_hash]
448
- display_table(obj_list, compute_fields(obj_list, fields))
449
- when :value_list
450
- # :value_list is a simple array of values, name of column provided in the :name
451
- display_table(data.map { |i| { name => i } }, [name])
452
- when :empty # no table
453
- display_message(:info, special_format('empty'))
454
- return
455
- when :nothing # no result expected
456
- Log.log.debug('no result expected')
457
- when :status # no table
458
- # :status displays a simple message
459
- display_message(:info, data)
460
- when :text # no table
461
- # :status displays a simple message
462
- display_message(:data, data)
463
- when :other_struct # no table
464
- # :other_struct is any other type of structure
465
- display_message(:data, PP.pp(data, +''))
466
- else
467
- raise "unknown data type: #{type}"
468
- end
469
- else
470
- raise "not expected: #{@options[:format]}"
471
- end
472
- end
473
481
  end
474
482
  end
475
483
  end
@@ -12,7 +12,7 @@ module Aspera
12
12
  SRC_URL = 'https://github.com/IBM/aspera-cli'
13
13
  # set this to warn in advance when minimum required ruby version will increase
14
14
  # see also required_ruby_version in gemspec file
15
- RUBY_FUTURE_MINIMUM_VERSION = '3.0'
15
+ RUBY_FUTURE_MINIMUM_VERSION = '3.1'
16
16
  end
17
17
  end
18
18
  end
@@ -105,6 +105,7 @@ module Aspera
105
105
  # @param descr [String] description for help
106
106
  # @param to_check [Object] value to check
107
107
  # @param type_list [NilClass, Class, Array[Class]] accepted value type(s)
108
+ # @param check_array [bool] set to true if it is a list of values to check
108
109
  def validate_type(what, descr, to_check, type_list, check_array: false)
109
110
  return nil if type_list.nil?
110
111
  Aspera.assert(type_list.is_a?(Array) && type_list.all?(Class)){'types must be a Class Array'}