aspera-cli 4.19.0 → 4.21.1

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 (91) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +46 -0
  4. data/CONTRIBUTING.md +18 -4
  5. data/README.md +886 -510
  6. data/bin/asession +27 -20
  7. data/examples/build_exec +65 -76
  8. data/examples/build_exec_rubyc +40 -0
  9. data/examples/get_proto_file.rb +7 -0
  10. data/lib/aspera/agent/alpha.rb +18 -24
  11. data/lib/aspera/agent/base.rb +2 -18
  12. data/lib/aspera/agent/connect.rb +34 -15
  13. data/lib/aspera/agent/direct.rb +44 -54
  14. data/lib/aspera/agent/httpgw.rb +2 -3
  15. data/lib/aspera/agent/node.rb +11 -21
  16. data/lib/aspera/agent/{trsdk.rb → transferd.rb} +27 -51
  17. data/lib/aspera/api/alee.rb +15 -0
  18. data/lib/aspera/api/aoc.rb +139 -105
  19. data/lib/aspera/api/ats.rb +1 -1
  20. data/lib/aspera/api/cos_node.rb +1 -1
  21. data/lib/aspera/api/httpgw.rb +15 -10
  22. data/lib/aspera/api/node.rb +70 -32
  23. data/lib/aspera/ascmd.rb +56 -48
  24. data/lib/aspera/ascp/installation.rb +166 -70
  25. data/lib/aspera/ascp/management.rb +30 -8
  26. data/lib/aspera/assert.rb +10 -5
  27. data/lib/aspera/cli/formatter.rb +166 -162
  28. data/lib/aspera/cli/hints.rb +2 -1
  29. data/lib/aspera/cli/info.rb +12 -10
  30. data/lib/aspera/cli/main.rb +28 -13
  31. data/lib/aspera/cli/manager.rb +7 -2
  32. data/lib/aspera/cli/plugin.rb +17 -31
  33. data/lib/aspera/cli/plugins/alee.rb +3 -3
  34. data/lib/aspera/cli/plugins/aoc.rb +246 -208
  35. data/lib/aspera/cli/plugins/ats.rb +16 -14
  36. data/lib/aspera/cli/plugins/config.rb +154 -94
  37. data/lib/aspera/cli/plugins/console.rb +3 -3
  38. data/lib/aspera/cli/plugins/cos.rb +1 -0
  39. data/lib/aspera/cli/plugins/faspex.rb +15 -23
  40. data/lib/aspera/cli/plugins/faspex5.rb +64 -50
  41. data/lib/aspera/cli/plugins/faspio.rb +2 -2
  42. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  43. data/lib/aspera/cli/plugins/node.rb +174 -109
  44. data/lib/aspera/cli/plugins/orchestrator.rb +14 -13
  45. data/lib/aspera/cli/plugins/preview.rb +8 -9
  46. data/lib/aspera/cli/plugins/server.rb +5 -9
  47. data/lib/aspera/cli/plugins/shares.rb +2 -2
  48. data/lib/aspera/cli/sync_actions.rb +2 -2
  49. data/lib/aspera/cli/transfer_agent.rb +12 -14
  50. data/lib/aspera/cli/transfer_progress.rb +37 -17
  51. data/lib/aspera/cli/version.rb +1 -1
  52. data/lib/aspera/command_line_builder.rb +4 -5
  53. data/lib/aspera/coverage.rb +13 -1
  54. data/lib/aspera/environment.rb +75 -25
  55. data/lib/aspera/faspex_gw.rb +2 -2
  56. data/lib/aspera/json_rpc.rb +1 -1
  57. data/lib/aspera/keychain/macos_security.rb +7 -12
  58. data/lib/aspera/log.rb +3 -4
  59. data/lib/aspera/node_simulator.rb +230 -112
  60. data/lib/aspera/oauth/base.rb +64 -83
  61. data/lib/aspera/oauth/factory.rb +52 -6
  62. data/lib/aspera/oauth/generic.rb +4 -8
  63. data/lib/aspera/oauth/jwt.rb +6 -3
  64. data/lib/aspera/oauth/url_json.rb +1 -2
  65. data/lib/aspera/oauth/web.rb +5 -2
  66. data/lib/aspera/persistency_action_once.rb +16 -8
  67. data/lib/aspera/persistency_folder.rb +20 -2
  68. data/lib/aspera/preview/generator.rb +1 -1
  69. data/lib/aspera/preview/utils.rb +11 -17
  70. data/lib/aspera/products/alpha.rb +30 -0
  71. data/lib/aspera/products/connect.rb +48 -0
  72. data/lib/aspera/products/other.rb +82 -0
  73. data/lib/aspera/products/transferd.rb +54 -0
  74. data/lib/aspera/rest.rb +116 -87
  75. data/lib/aspera/secret_hider.rb +2 -2
  76. data/lib/aspera/ssh.rb +31 -24
  77. data/lib/aspera/transfer/faux_file.rb +4 -4
  78. data/lib/aspera/transfer/parameters.rb +16 -17
  79. data/lib/aspera/transfer/spec.rb +12 -12
  80. data/lib/aspera/transfer/spec.yaml +22 -20
  81. data/lib/aspera/transfer/sync.rb +2 -10
  82. data/lib/aspera/transfer/uri.rb +3 -3
  83. data/lib/aspera/uri_reader.rb +1 -1
  84. data/lib/aspera/web_auth.rb +166 -17
  85. data/lib/aspera/web_server_simple.rb +4 -3
  86. data/lib/transferd_pb.rb +86 -0
  87. data/lib/transferd_services_pb.rb +84 -0
  88. data.tar.gz.sig +0 -0
  89. metadata +58 -22
  90. metadata.gz.sig +0 -0
  91. data/lib/aspera/ascp/products.rb +0 -156
@@ -9,11 +9,10 @@ require 'aspera/cli/formatter'
9
9
  require 'aspera/cli/info'
10
10
  require 'aspera/cli/transfer_progress'
11
11
  require 'aspera/ascp/installation'
12
- require 'aspera/ascp/products'
12
+ require 'aspera/products/transferd'
13
13
  require 'aspera/transfer/error_info'
14
14
  require 'aspera/transfer/parameters'
15
15
  require 'aspera/transfer/spec'
16
- require 'aspera/keychain/encrypted_hash'
17
16
  require 'aspera/keychain/macos_security'
18
17
  require 'aspera/proxy_auto_config'
19
18
  require 'aspera/environment'
@@ -54,18 +53,17 @@ module Aspera
54
53
  PERSISTENCY_FOLDER = 'persist_store'
55
54
  ASPERA = 'aspera'
56
55
  SERVER_COMMAND = 'server'
57
- APP_NAME_SDK = 'sdk'
56
+ DIR_SDK = 'sdk'
58
57
  CONNECT_WEB_URL = 'https://d3gcli72yxqn2z.cloudfront.net/connect'
59
58
  CONNECT_VERSIONS = 'connectversions.js' # cspell: disable-line
60
- TRANSFER_SDK_ARCHIVE_URL = 'https://ibm.biz/aspera_transfer_sdk'
61
59
  DEMO_SERVER = 'demo'
62
60
  DEMO_PRESET = 'demoserver' # cspell: disable-line
63
61
  EMAIL_TEST_TEMPLATE = <<~END_OF_TEMPLATE
64
62
  From: <%=from_name%> <<%=from_email%>>
65
63
  To: <<%=to%>>
66
- Subject: #{GEM_NAME} email test
64
+ Subject: #{Info::GEM_NAME} email test
67
65
 
68
- This email was sent to test #{PROGRAM_NAME}.
66
+ This email was sent to test #{Info::CMD_NAME}.
69
67
  END_OF_TEMPLATE
70
68
  # special extended values
71
69
  EXTEND_PRESET = :preset
@@ -79,6 +77,7 @@ module Aspera
79
77
  GEM_CHECK_DATE_FMT = '%Y/%m/%d'
80
78
  # for testing only
81
79
  SELF_SIGNED_CERT = OpenSSL::SSL.const_get(:enon_yfirev.to_s.upcase.reverse) # cspell: disable-line
80
+ CONF_OVERVIEW_KEYS = %w[preset parameter value].freeze
82
81
  private_constant :DEFAULT_CONFIG_FILENAME,
83
82
  :CONF_PRESET_CONFIG,
84
83
  :CONF_PRESET_VERSION,
@@ -86,7 +85,6 @@ module Aspera
86
85
  :CONF_PRESET_GLOBAL,
87
86
  :ASPERA_PLUGINS_FOLDERNAME,
88
87
  :ASPERA,
89
- :TRANSFER_SDK_ARCHIVE_URL,
90
88
  :DEMO_SERVER,
91
89
  :DEMO_PRESET,
92
90
  :EMAIL_TEST_TEMPLATE,
@@ -100,7 +98,8 @@ module Aspera
100
98
  :WIZARD_RESULT_KEYS,
101
99
  :SELF_SIGNED_CERT,
102
100
  :PERSISTENCY_FOLDER,
103
- :DEFAULT_PRIV_KEY_LENGTH
101
+ :DEFAULT_PRIV_KEY_LENGTH,
102
+ :CONF_OVERVIEW_KEYS
104
103
 
105
104
  class << self
106
105
  def generate_rsa_private_key(path:, length: DEFAULT_PRIV_KEY_LENGTH)
@@ -126,8 +125,8 @@ module Aspera
126
125
  end
127
126
 
128
127
  # deep clone hash so that it does not get modified in case of display and secret hide
129
- def protect_presets(val)
130
- return JSON.parse(JSON.generate(val))
128
+ def deep_clone(val)
129
+ return Marshal.load(Marshal.dump(val))
131
130
  end
132
131
 
133
132
  # return product family folder (~/.aspera)
@@ -145,13 +144,9 @@ module Aspera
145
144
  end
146
145
  end
147
146
 
148
- def initialize(gem:, name:, help:, version:, **env)
147
+ def initialize(**env)
149
148
  # we need to defer parsing of options until we have the config file, so we can use @extend with @preset
150
- super(**env)
151
- @gem = gem
152
- @name = name
153
- @help = help
154
- @version = version
149
+ super
155
150
  @use_plugin_defaults = true
156
151
  @config_presets = nil
157
152
  @config_checksum_on_disk = nil
@@ -177,9 +172,9 @@ module Aspera
177
172
  :home, 'Home folder for tool',
178
173
  handler: {o: self, m: :main_folder},
179
174
  types: String,
180
- default: self.class.default_app_main_folder(app_name: @name))
175
+ default: self.class.default_app_main_folder(app_name: Info::CMD_NAME))
181
176
  options.parse_options!
182
- Log.log.debug{"#{@name} folder: #{@main_folder}"}
177
+ Log.log.debug{"#{Info::CMD_NAME} folder: #{@main_folder}"}
183
178
  # data persistency manager, created by config plugin
184
179
  @persistency = PersistencyFolder.new(File.join(@main_folder, PERSISTENCY_FOLDER))
185
180
  # set folders for plugin lookup
@@ -218,8 +213,8 @@ module Aspera
218
213
  # Transfer SDK options
219
214
  options.declare(:ascp_path, 'Path to ascp', handler: {o: Ascp::Installation.instance, m: :ascp_path})
220
215
  options.declare(:use_product, 'Use ascp from specified product', handler: {o: self, m: :option_use_product})
221
- options.declare(:sdk_url, 'URL to get SDK', default: TRANSFER_SDK_ARCHIVE_URL)
222
- options.declare(:sdk_folder, 'SDK folder path', handler: {o: Ascp::Installation.instance, m: :sdk_folder})
216
+ options.declare(:sdk_url, 'URL to get SDK', default: SpecialValues::DEF)
217
+ options.declare(:sdk_folder, 'SDK folder path', handler: {o: Products::Transferd, m: :sdk_directory})
223
218
  options.declare(:progress_bar, 'Display progress bar', values: :bool, default: Environment.terminal?)
224
219
  # email options
225
220
  options.declare(:smtp, 'SMTP configuration', types: Hash)
@@ -234,26 +229,26 @@ module Aspera
234
229
  options.declare(:http_proxy, 'URL for HTTP proxy with optional credentials', types: String, handler: {o: self, m: :option_http_proxy})
235
230
  options.declare(:cache_tokens, 'Save and reuse OAuth tokens', values: :bool, handler: {o: self, m: :option_cache_tokens})
236
231
  options.declare(:fpac, 'Proxy auto configuration script')
237
- options.declare(:proxy_credentials, 'HTTP proxy credentials for fpac. Array: user,password', types: Array)
232
+ options.declare(:proxy_credentials, 'HTTP proxy credentials for fpac: user, password', types: Array)
238
233
  options.parse_options!
239
234
  @progress_bar = TransferProgress.new if options.get_option(:progress_bar)
240
235
  # Check SDK folder is set or not, for compatibility, we check in two places
241
- sdk_folder = Ascp::Installation.instance.sdk_folder rescue nil
242
- if sdk_folder.nil?
236
+ sdk_dir = Products::Transferd.sdk_directory rescue nil
237
+ if sdk_dir.nil?
243
238
  @sdk_default_location = true
244
239
  Log.log.debug('SDK folder is not set, checking default')
245
240
  # new location
246
- sdk_folder = self.class.default_app_main_folder(app_name: APP_NAME_SDK)
247
- Log.log.debug{"checking: #{sdk_folder}"}
248
- if !Dir.exist?(sdk_folder)
249
- Log.log.debug{"not exists: #{sdk_folder}"}
241
+ sdk_dir = self.class.default_app_main_folder(app_name: DIR_SDK)
242
+ Log.log.debug{"checking: #{sdk_dir}"}
243
+ if !Dir.exist?(sdk_dir)
244
+ Log.log.debug{"not exists: #{sdk_dir}"}
250
245
  # former location
251
- former_sdk_folder = File.join(self.class.default_app_main_folder(app_name: @name), APP_NAME_SDK)
246
+ former_sdk_folder = File.join(self.class.default_app_main_folder(app_name: Info::CMD_NAME), DIR_SDK)
252
247
  Log.log.debug{"checking: #{former_sdk_folder}"}
253
- sdk_folder = former_sdk_folder if Dir.exist?(former_sdk_folder)
248
+ sdk_dir = former_sdk_folder if Dir.exist?(former_sdk_folder)
254
249
  end
255
- Log.log.debug{"using: #{sdk_folder}"}
256
- Ascp::Installation.instance.sdk_folder = sdk_folder
250
+ Log.log.debug{"using: #{sdk_dir}"}
251
+ Products::Transferd.sdk_directory = sdk_dir
257
252
  end
258
253
  pac_script = options.get_option(:fpac)
259
254
  # create PAC executor
@@ -266,11 +261,16 @@ module Aspera
266
261
  @pac_exec.proxy_pass = proxy_user_pass[1]
267
262
  end
268
263
  end
269
- Rest.set_parameters(
270
- user_agent: PROGRAM_NAME,
271
- session_cb: lambda{|http_session|update_http_session(http_session)},
272
- progress_bar: @progress_bar)
264
+ RestParameters.instance.user_agent = Info::CMD_NAME
265
+ RestParameters.instance.progress_bar = @progress_bar
266
+ RestParameters.instance.session_cb = lambda{|http_session|update_http_session(http_session)}
267
+ @option_http_options.keys.select{|i|RestParameters.instance.respond_to?(i)}.each do |k|
268
+ method = "#{k}=".to_sym
269
+ RestParameters.instance.send(method, @option_http_options[k])
270
+ @option_http_options.delete(k)
271
+ end
273
272
  OAuth::Factory.instance.persist_mgr = persistency if @option_cache_tokens
273
+ OAuth::Web.additionnal_info = "#{Info::CMD_NAME} v#{Cli::VERSION}"
274
274
  Transfer::Parameters.file_list_folder = File.join(@main_folder, 'filelists')
275
275
  RestErrorAnalyzer.instance.log_file = File.join(@main_folder, 'rest_exceptions.log')
276
276
  # register aspera REST call error handlers
@@ -394,19 +394,19 @@ module Aspera
394
394
  def check_gem_version
395
395
  latest_version =
396
396
  begin
397
- Rest.new(base_url: 'https://rubygems.org/api/v1').read("versions/#{@gem}/latest.json")[:data]['version']
397
+ Rest.new(base_url: 'https://rubygems.org/api/v1').read("versions/#{Info::GEM_NAME}/latest.json")['version']
398
398
  rescue StandardError
399
399
  Log.log.warn('Could not retrieve latest gem version on rubygems.')
400
400
  '0'
401
401
  end
402
- if Gem::Version.new(Environment.ruby_version) < Gem::Version.new(RUBY_FUTURE_MINIMUM_VERSION)
402
+ if Gem::Version.new(Environment.ruby_version) < Gem::Version.new(Info::RUBY_FUTURE_MINIMUM_VERSION)
403
403
  Log.log.warn do
404
- "Note that a future version will require Ruby version #{RUBY_FUTURE_MINIMUM_VERSION} at minimum, " \
404
+ "Note that a future version will require Ruby version #{Info::RUBY_FUTURE_MINIMUM_VERSION} at minimum, " \
405
405
  "you are using #{Environment.ruby_version}"
406
406
  end
407
407
  end
408
408
  return {
409
- name: @gem,
409
+ name: Info::GEM_NAME,
410
410
  current: Cli::VERSION,
411
411
  latest: latest_version,
412
412
  need_update: Gem::Version.new(Cli::VERSION) < Gem::Version.new(latest_version)
@@ -415,7 +415,7 @@ module Aspera
415
415
 
416
416
  def periodic_check_newer_gem_version
417
417
  # get verification period
418
- delay_days = options.get_option(:version_check_days, mandatory: true)
418
+ delay_days = options.get_option(:version_check_days, mandatory: true).to_i
419
419
  # check only if not zero day
420
420
  return if delay_days.eql?(0)
421
421
  # get last date from persistency
@@ -522,7 +522,7 @@ module Aspera
522
522
  current = current[name]
523
523
  raise Cli::Error, "No such config preset: #{include_path}" if current.nil?
524
524
  end
525
- current = self.class.protect_presets(current) unless current.is_a?(String)
525
+ current = self.class.deep_clone(current) unless current.is_a?(String)
526
526
  return ExtendedValue.instance.evaluate(current)
527
527
  end
528
528
 
@@ -592,7 +592,7 @@ module Aspera
592
592
  @config_presets[CONF_PRESET_DEFAULTS].delete(true) if @config_presets[CONF_PRESET_DEFAULTS].is_a?(Hash)
593
593
  # ^^^ Place new compatibility code before this line
594
594
  # set version to current
595
- @config_presets[CONF_PRESET_CONFIG][CONF_PRESET_VERSION] = @version
595
+ @config_presets[CONF_PRESET_CONFIG][CONF_PRESET_VERSION] = Cli::VERSION
596
596
  unless files_to_copy.empty?
597
597
  Log.log.warn('Copying referenced files')
598
598
  files_to_copy.each do |file|
@@ -607,7 +607,7 @@ module Aspera
607
607
  Log.log.debug{"-> #{e.class.name} : #{e}"}
608
608
  if File.exist?(@option_config_file)
609
609
  # then there is a problem with that file.
610
- new_name = "#{@option_config_file}.pre#{@version}.manual_conversion_needed"
610
+ new_name = "#{@option_config_file}.pre#{Cli::VERSION}.manual_conversion_needed"
611
611
  File.rename(@option_config_file, new_name)
612
612
  Log.log.warn{"Renamed config file to #{new_name}."}
613
613
  Log.log.warn('Manual Conversion is required. Next time, a new empty file will be created.')
@@ -664,10 +664,10 @@ module Aspera
664
664
  end
665
665
  case command
666
666
  when :list
667
- return {type: :object_list, data: connect_versions, fields: %w[id title version]}
667
+ return Main.result_object_list(connect_versions, fields: %w[id title version])
668
668
  when :info
669
669
  one_res.delete('links')
670
- return {type: :single_object, data: one_res}
670
+ return Main.result_single_object(one_res)
671
671
  when :version
672
672
  all_links = one_res['links']
673
673
  command = options.get_next_command(%i[list download open])
@@ -678,10 +678,9 @@ module Aspera
678
678
  end
679
679
  case command
680
680
  when :list
681
- return {type: :object_list, data: all_links}
681
+ return Main.result_object_list(all_links)
682
682
  when :download
683
683
  folder_dest = transfer.destination_folder(Transfer::Spec::DIRECTION_RECEIVE)
684
- # folder_dest=self.options.get_next_argument('destination folder')
685
684
  api_connect_cdn = Rest.new(base_url: CONNECT_WEB_URL)
686
685
  file_url = one_link['href']
687
686
  filename = file_url.gsub(%r{.*/}, '')
@@ -701,11 +700,12 @@ module Aspera
701
700
  return execute_connect_action
702
701
  when :use
703
702
  ascp_path = options.get_next_argument('path to ascp')
703
+ Ascp::Installation.instance.ascp_path = ascp_path
704
704
  formatter.display_status("ascp version: #{Ascp::Installation.instance.get_ascp_version(ascp_path)}")
705
705
  set_global_default(:ascp_path, ascp_path)
706
706
  return Main.result_nothing
707
707
  when :show
708
- return {type: :status, data: Ascp::Installation.instance.path(:ascp)}
708
+ return Main.result_status(Ascp::Installation.instance.path(:ascp))
709
709
  when :info
710
710
  # collect info from ascp executable
711
711
  data = Ascp::Installation.instance.ascp_info
@@ -714,13 +714,13 @@ module Aspera
714
714
  # add keys
715
715
  DataRepository::ELEMENTS.each_with_object(data){|i, h|h[i.to_s] = DataRepository.instance.item(i)}
716
716
  # declare those as secrets
717
- SecretHider::ADDITIONAL_KEYS_TO_HIDE.push(*DataRepository::ELEMENTS.map(&:to_s))
718
- return {type: :single_object, data: data}
717
+ SecretHider::ADDITIONAL_KEYS_TO_HIDE.concat(DataRepository::ELEMENTS.map(&:to_s))
718
+ return Main.result_single_object(data)
719
719
  when :products
720
720
  command = options.get_next_command(%i[list use])
721
721
  case command
722
722
  when :list
723
- return {type: :object_list, data: Ascp::Products.installed_products, fields: %w[name app_root]}
723
+ return Main.result_object_list(Ascp::Installation.instance.installed_products, fields: %w[name app_root])
724
724
  when :use
725
725
  default_product = options.get_next_argument('product name')
726
726
  Ascp::Installation.instance.use_ascp_from_product(default_product)
@@ -729,23 +729,44 @@ module Aspera
729
729
  end
730
730
  when :install
731
731
  # reset to default location, if older default was used
732
- Ascp::Installation.instance.sdk_folder = self.class.default_app_main_folder(app_name: APP_NAME_SDK) if @sdk_default_location
733
- n, v = Ascp::Installation.instance.install_sdk(options.get_option(:sdk_url, mandatory: true))
732
+ Products::Transferd.sdk_directory = self.class.default_app_main_folder(app_name: DIR_SDK) if @sdk_default_location
733
+ version = options.get_next_argument('transferd version', mandatory: false)
734
+ n, v = Ascp::Installation.instance.install_sdk(url: options.get_option(:sdk_url, mandatory: true), version: version)
734
735
  return Main.result_status("Installed #{n} version #{v}")
735
736
  when :spec
736
- return {
737
- type: :object_list,
738
- data: Transfer::Parameters.man_table(formatter),
737
+ return Main.result_object_list(
738
+ Transfer::Parameters.man_table(formatter),
739
739
  fields: [%w[name type], Transfer::Parameters::SUPPORTED_AGENTS_SHORT.map(&:to_s), %w[description]].flatten.freeze
740
- }
740
+ )
741
741
  when :errors
742
742
  error_data = []
743
743
  Transfer::ERROR_INFO.each_pair do |code, prop|
744
744
  error_data.push(code: code, mnemonic: prop[:c], retry: prop[:r], info: prop[:a])
745
745
  end
746
- return {type: :object_list, data: error_data}
746
+ return Main.result_object_list(error_data)
747
+ else Aspera.error_unexpected_value(command)
748
+ end
749
+ Aspera.error_unreachable_line
750
+ end
751
+
752
+ def execute_action_transferd
753
+ command = options.get_next_command(%i[list install])
754
+ case command
755
+ when :install
756
+ # reset to default location, if older default was used
757
+ Products::Transferd.sdk_directory = self.class.default_app_main_folder(app_name: DIR_SDK) if @sdk_default_location
758
+ version = options.get_next_argument('transferd version', mandatory: false)
759
+ n, v = Ascp::Installation.instance.install_sdk(url: options.get_option(:sdk_url, mandatory: true), version: version)
760
+ return Main.result_status("Installed #{n} version #{v}")
761
+ when :list
762
+ sdk_list = Ascp::Installation.sdk_locations
763
+ return Main.result_object_list(
764
+ sdk_list,
765
+ fields: sdk_list.first.keys - ['url']
766
+ )
767
+ else Aspera.error_unexpected_value(command)
747
768
  end
748
- raise "unexpected case: #{command}"
769
+ Aspera.error_unreachable_line
749
770
  end
750
771
 
751
772
  # legacy actions available globally
@@ -764,12 +785,20 @@ module Aspera
764
785
  raise "no such preset: #{name}" if PRESET_EXIST_ACTIONS.include?(action) && !@config_presets.key?(name)
765
786
  case action
766
787
  when :list
767
- return {type: :value_list, data: @config_presets.keys, name: 'name'}
788
+ return Main.result_value_list(@config_presets.keys, 'name')
768
789
  when :overview
769
790
  # display process modifies the value (hide secrets): we do not want to save removed secrets
770
- return {type: :config_over, data: self.class.protect_presets(@config_presets)}
791
+ data = self.class.deep_clone(@config_presets)
792
+ formatter.hide_secrets(data)
793
+ result = []
794
+ data.each do |config, preset|
795
+ preset.each do |parameter, value|
796
+ result.push(CONF_OVERVIEW_KEYS.zip([config, parameter, value]).to_h)
797
+ end
798
+ end
799
+ return Main.result_object_list(result, fields: CONF_OVERVIEW_KEYS)
771
800
  when :show
772
- return {type: :single_object, data: self.class.protect_presets(@config_presets[name])}
801
+ return Main.result_single_object(self.class.deep_clone(@config_presets[name]))
773
802
  when :delete
774
803
  @config_presets.delete(name)
775
804
  return Main.result_status("Deleted: #{name}")
@@ -780,7 +809,7 @@ module Aspera
780
809
  case value
781
810
  when Numeric, String then return {type: :text, data: ExtendedValue.instance.evaluate(value.to_s)}
782
811
  end
783
- return {type: :single_object, data: value}
812
+ return Main.result_single_object(value)
784
813
  when :unset
785
814
  param_name = options.get_next_argument('parameter name')
786
815
  @config_presets[name].delete(param_name)
@@ -819,7 +848,7 @@ module Aspera
819
848
  user = options.get_option(:username, mandatory: true)
820
849
  result = lookup_preset(url: url, username: user)
821
850
  raise 'no such config found' if result.nil?
822
- return {type: :single_object, data: result}
851
+ return Main.result_single_object(result)
823
852
  when :secure
824
853
  identifier = options.get_next_argument('config name', mandatory: false)
825
854
  preset_names = identifier.nil? ? @config_presets.keys : [identifier]
@@ -857,13 +886,15 @@ module Aspera
857
886
  remote_certificate
858
887
  gem
859
888
  plugins
860
- flush_tokens
889
+ tokens
861
890
  echo
891
+ download
862
892
  wizard
863
893
  detect
864
894
  coffee
865
895
  image
866
896
  ascp
897
+ transferd
867
898
  email_test
868
899
  smtp_settings
869
900
  proxy_check
@@ -872,7 +903,7 @@ module Aspera
872
903
  check_update
873
904
  initdemo
874
905
  vault
875
- throw
906
+ test
876
907
  platform
877
908
  ].freeze
878
909
 
@@ -888,7 +919,7 @@ module Aspera
888
919
  when :documentation
889
920
  section = options.get_next_argument('private key file path', mandatory: false)
890
921
  section = "##{section}" unless section.nil?
891
- Environment.instance.open_uri("#{@help}#{section}")
922
+ Environment.instance.open_uri("#{Info::DOC_URL}#{section}")
892
923
  return Main.result_nothing
893
924
  when :genkey # generate new rsa key
894
925
  private_key_path = options.get_next_argument('private key file path')
@@ -913,9 +944,27 @@ module Aspera
913
944
  end
914
945
  when :echo # display the content of a value given on command line
915
946
  return Formatter.auto_type(options.get_next_argument('value', validation: nil))
916
- when :flush_tokens
917
- deleted_files = OAuth::Factory.instance.flush_tokens
918
- return {type: :value_list, data: deleted_files, name: 'file'}
947
+ when :download
948
+ file_url = options.get_next_argument('source URL').chomp
949
+ file_dest = options.get_next_argument('file path', mandatory: false)
950
+ if file_dest.nil?
951
+ file_dest = File.join(transfer.destination_folder(Transfer::Spec::DIRECTION_RECEIVE), file_url.gsub(%r{.*/}, ''))
952
+ end
953
+ formatter.display_status("Downloading: #{file_url}")
954
+ Rest.new(base_url: file_url).call(operation: 'GET', save_to_file: file_dest)
955
+ return Main.result_status("Saved to: #{file_dest}")
956
+ when :tokens
957
+ require 'aspera/api/node'
958
+ case options.get_next_command(%i{flush list show})
959
+ when :flush
960
+ return Main.result_value_list(OAuth::Factory.instance.flush_tokens, name: 'file')
961
+ when :list
962
+ return Main.result_object_list(OAuth::Factory.instance.persisted_tokens)
963
+ when :show
964
+ data = OAuth::Factory.instance.get_token_info(instance_identifier)
965
+ raise Cli::Error, 'No such identifier' if data.nil?
966
+ return Main.result_single_object(data)
967
+ end
919
968
  when :plugins
920
969
  case options.get_next_command(%i[list create])
921
970
  when :list
@@ -929,7 +978,7 @@ module Aspera
929
978
  path: PluginFactory.instance.plugin_source(name)
930
979
  })
931
980
  end
932
- return {type: :object_list, data: result, fields: %w[plugin detect wizard path]}
981
+ return Main.result_object_list(result, fields: %w[plugin detect wizard path])
933
982
  when :create
934
983
  plugin_name = options.get_next_argument('name').downcase
935
984
  destination_folder = options.get_next_argument('folder', mandatory: false) || File.join(@main_folder, ASPERA_PLUGINS_FOLDERNAME)
@@ -955,10 +1004,7 @@ module Aspera
955
1004
  options.ask_missing_mandatory = true
956
1005
  # detect plugins by url and optional query
957
1006
  apps = identify_plugins_for_url.freeze
958
- return {
959
- type: :object_list,
960
- data: apps
961
- } if action.eql?(:detect)
1007
+ return Main.result_object_list(apps) if action.eql?(:detect)
962
1008
  return wizard_find(apps)
963
1009
  when :coffee
964
1010
  return Main.result_image(COFFEE_IMAGE, formatter: formatter)
@@ -966,11 +1012,13 @@ module Aspera
966
1012
  return Main.result_image(options.get_next_argument('image uri or blob'), formatter: formatter)
967
1013
  when :ascp
968
1014
  execute_action_ascp
1015
+ when :transferd
1016
+ execute_action_transferd
969
1017
  when :gem
970
1018
  case options.get_next_command(%i[path version name])
971
1019
  when :path then return Main.result_status(self.class.gem_src_root)
972
1020
  when :version then return Main.result_status(Cli::VERSION)
973
- when :name then return Main.result_status(@gem)
1021
+ when :name then return Main.result_status(Info::GEM_NAME)
974
1022
  end
975
1023
  when :folder
976
1024
  return Main.result_status(@main_folder)
@@ -980,14 +1028,14 @@ module Aspera
980
1028
  send_email_template(email_template_default: EMAIL_TEST_TEMPLATE)
981
1029
  return Main.result_nothing
982
1030
  when :smtp_settings
983
- return {type: :single_object, data: email_settings}
1031
+ return Main.result_single_object(email_settings)
984
1032
  when :proxy_check
985
1033
  # ensure fpac was provided
986
1034
  options.get_option(:fpac, mandatory: true)
987
1035
  server_url = options.get_next_argument('server url')
988
1036
  return Main.result_status(@pac_exec.find_proxy_for_url(server_url))
989
1037
  when :check_update
990
- return {type: :single_object, data: check_gem_version}
1038
+ return Main.result_single_object(check_gem_version)
991
1039
  when :initdemo
992
1040
  if @config_presets.key?(DEMO_PRESET)
993
1041
  Log.log.warn{"Demo server preset already present: #{DEMO_PRESET}"}
@@ -1010,14 +1058,7 @@ module Aspera
1010
1058
  end
1011
1059
  return Main.result_status('Done')
1012
1060
  when :vault then execute_vault
1013
- when :throw
1014
- # :type [String]
1015
- options
1016
- exception_class_name = options.get_next_argument('exception class name', mandatory: true)
1017
- exception_text = options.get_next_argument('exception text', mandatory: true)
1018
- exception_class = Object.const_get(exception_class_name)
1019
- Aspera.assert(exception_class <= Exception){"#{exception_class} is not an exception: #{exception_class.class}"}
1020
- raise exception_class, exception_text
1061
+ when :test then return execute_test
1021
1062
  when :platform
1022
1063
  return Main.result_status(Environment.architecture)
1023
1064
  else Aspera.error_unreachable_line
@@ -1093,7 +1134,7 @@ module Aspera
1093
1134
  wiz_preset_name = elements.join('_').strip.downcase.gsub(/[^a-z0-9]/, '_').squeeze('_')
1094
1135
  end
1095
1136
  # test mode does not change conf file
1096
- return {type: :single_object, data: wizard_result} if options.get_option(:test_mode)
1137
+ return Main.result_single_object(wizard_result) if options.get_option(:test_mode)
1097
1138
  # Write configuration file
1098
1139
  formatter.display_status("Preparing preset: #{wiz_preset_name}")
1099
1140
  # init defaults if necessary
@@ -1112,7 +1153,7 @@ module Aspera
1112
1153
  test_args = "-P#{wiz_preset_name} #{test_args}"
1113
1154
  end
1114
1155
  # TODO: actually test the command
1115
- return Main.result_status("You can test with:\n#{@name} #{identification[:product]} #{test_args}")
1156
+ return Main.result_status("You can test with:\n#{Info::CMD_NAME} #{identification[:product]} #{test_args}")
1116
1157
  end
1117
1158
 
1118
1159
  # @return [Hash] email server setting with defaults if not defined
@@ -1141,6 +1182,8 @@ module Aspera
1141
1182
  end
1142
1183
 
1143
1184
  # send email using ERB template
1185
+ # @param email_template_default [String] default template, can be overriden by option
1186
+ # @param values [Hash] values to be used in template, keys with default: to, from_name, from_email
1144
1187
  def send_email_template(email_template_default: nil, values: {})
1145
1188
  values[:to] ||= options.get_option(:notify_to, mandatory: true)
1146
1189
  notify_template = options.get_option(:notify_template, mandatory: email_template_default.nil?) || email_template_default
@@ -1202,7 +1245,8 @@ module Aspera
1202
1245
  Log.log.error do
1203
1246
  "Default config name [#{default_config_name}] specified for plugin [#{plugin_name_sym}], but it does not exist in config file.\n" \
1204
1247
  'Please fix the issue: either create preset with one parameter: ' \
1205
- "(#{@name} config id #{default_config_name} init @json:'{}') or remove default (#{@name} config id default remove #{plugin_name_sym})."
1248
+ "(#{Info::CMD_NAME} config id #{default_config_name} init @json:'{}') " \
1249
+ "or remove default (#{Info::CMD_NAME} config id default remove #{plugin_name_sym})."
1206
1250
  end
1207
1251
  end
1208
1252
  raise Cli::Error, "Config name [#{default_config_name}] must be a hash, check config file." if !@config_presets[default_config_name].is_a?(Hash)
@@ -1217,11 +1261,11 @@ module Aspera
1217
1261
  command = options.get_next_command(%i[info list show create delete password])
1218
1262
  case command
1219
1263
  when :info
1220
- return {type: :single_object, data: vault_info}
1264
+ return Main.result_single_object(vault_info)
1221
1265
  when :list
1222
- return {type: :object_list, data: vault.list, fields: %w(label url username password description)}
1266
+ return Main.result_object_list(vault.list, fields: %w(label url username password description))
1223
1267
  when :show
1224
- return {type: :single_object, data: vault.get(label: options.get_next_argument('label'))}
1268
+ return Main.result_single_object(vault.get(label: options.get_next_argument('label')))
1225
1269
  when :create
1226
1270
  label = options.get_next_argument('label', validation: String)
1227
1271
  info = options.get_next_argument('info', validation: Hash)
@@ -1257,7 +1301,7 @@ module Aspera
1257
1301
  info = options.get_option(:vault) || {}
1258
1302
  info = info.symbolize_keys
1259
1303
  info[:type] ||= 'file'
1260
- info[:name] ||= (info[:type].eql?('file') ? DEFAULT_VAULT_FILENAME : PROGRAM_NAME)
1304
+ info[:name] ||= (info[:type].eql?('file') ? DEFAULT_VAULT_FILENAME : Info::CMD_NAME)
1261
1305
  Aspera.assert(info.keys.sort == %i[name type]) {"vault info shall have exactly keys 'type' and 'name'"}
1262
1306
  Aspera.assert(info.values.all?(String)){'vault info shall have only string values'}
1263
1307
  info[:password] = options.get_option(:vault_password, mandatory: true)
@@ -1270,6 +1314,8 @@ module Aspera
1270
1314
  info = vault_info
1271
1315
  case info[:type]
1272
1316
  when 'file'
1317
+ # this module requires comilation, so it is optinal
1318
+ require 'aspera/keychain/encrypted_hash'
1273
1319
  # absolute_path? introduced in ruby 2.7
1274
1320
  @vault = Keychain::EncryptedHash.new(
1275
1321
  info[:name].eql?(File.absolute_path(info[:name])) ? info[:name] : File.join(@main_folder, info[:name]),
@@ -1289,6 +1335,20 @@ module Aspera
1289
1335
  @vault
1290
1336
  end
1291
1337
 
1338
+ def execute_test
1339
+ case options.get_next_command(%i[throw web])
1340
+ when :throw
1341
+ # :type [String]
1342
+ # options
1343
+ exception_class_name = options.get_next_argument('exception class name', mandatory: true)
1344
+ exception_text = options.get_next_argument('exception text', mandatory: true)
1345
+ exception_class = Object.const_get(exception_class_name)
1346
+ Aspera.assert(exception_class <= Exception){"#{exception_class} is not an exception: #{exception_class.class}"}
1347
+ raise exception_class, exception_text
1348
+ when :web
1349
+ end
1350
+ end
1351
+
1292
1352
  # version of URL without trailing "/" and removing default port
1293
1353
  def canonical_url(url)
1294
1354
  url.sub(%r{/+$}, '').sub(%r{^(https://[^/]+):443$}, '\1')
@@ -1301,7 +1361,7 @@ module Aspera
1301
1361
  @config_presets.each_value do |v|
1302
1362
  next unless v.is_a?(Hash)
1303
1363
  conf_url = v['url'].is_a?(String) ? canonical_url(v['url']) : nil
1304
- return self.class.protect_presets(v) if conf_url.eql?(url) && v['username'].eql?(username)
1364
+ return self.class.deep_clone(v) if conf_url.eql?(url) && v['username'].eql?(username)
1305
1365
  end
1306
1366
  nil
1307
1367
  end
@@ -84,11 +84,11 @@ module Aspera
84
84
  command = options.get_next_command(%i[list submit])
85
85
  case command
86
86
  when :list
87
- return {type: :object_list, data: api_console.read('smart_transfers')[:data]}
87
+ return {type: :object_list, data: api_console.read('smart_transfers')}
88
88
  when :submit
89
89
  smart_id = options.get_next_argument('smart_id')
90
90
  params = options.get_next_argument('transfer parameters')
91
- return {type: :object_list, data: api_console.create("smart_transfers/#{smart_id}", params)[:data]}
91
+ return {type: :object_list, data: api_console.create("smart_transfers/#{smart_id}", params)}
92
92
  end
93
93
  when :current
94
94
  command = options.get_next_command([:list])
@@ -99,7 +99,7 @@ module Aspera
99
99
  data: api_console.read('transfers', {
100
100
  'from' => options.get_option(:filter_from, mandatory: true),
101
101
  'to' => options.get_option(:filter_to, mandatory: true)
102
- })[:data],
102
+ }),
103
103
  fields: %w[id contact name status]}
104
104
  end
105
105
  end
@@ -19,6 +19,7 @@ module Aspera
19
19
  options.declare(:region, 'Storage region')
20
20
  options.declare(:identity, "Authentication URL (#{Api::CosNode::IBM_CLOUD_TOKEN_URL})", default: Api::CosNode::IBM_CLOUD_TOKEN_URL)
21
21
  options.parse_options!
22
+ Node.declare_options(options)
22
23
  end
23
24
 
24
25
  ACTIONS = %i[node].freeze