aspera-cli 4.20.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 (58) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +29 -3
  4. data/CONTRIBUTING.md +2 -0
  5. data/README.md +571 -375
  6. data/bin/asession +2 -2
  7. data/examples/get_proto_file.rb +1 -1
  8. data/lib/aspera/agent/alpha.rb +10 -16
  9. data/lib/aspera/agent/connect.rb +20 -2
  10. data/lib/aspera/agent/direct.rb +21 -30
  11. data/lib/aspera/agent/node.rb +1 -11
  12. data/lib/aspera/agent/{trsdk.rb → transferd.rb} +13 -34
  13. data/lib/aspera/api/aoc.rb +13 -8
  14. data/lib/aspera/api/node.rb +45 -28
  15. data/lib/aspera/ascp/installation.rb +87 -48
  16. data/lib/aspera/ascp/management.rb +27 -6
  17. data/lib/aspera/cli/formatter.rb +148 -154
  18. data/lib/aspera/cli/info.rb +1 -1
  19. data/lib/aspera/cli/main.rb +12 -0
  20. data/lib/aspera/cli/manager.rb +2 -2
  21. data/lib/aspera/cli/plugin.rb +2 -2
  22. data/lib/aspera/cli/plugins/aoc.rb +28 -18
  23. data/lib/aspera/cli/plugins/config.rb +106 -54
  24. data/lib/aspera/cli/plugins/cos.rb +1 -0
  25. data/lib/aspera/cli/plugins/faspex.rb +4 -2
  26. data/lib/aspera/cli/plugins/faspex5.rb +21 -9
  27. data/lib/aspera/cli/plugins/node.rb +45 -38
  28. data/lib/aspera/cli/transfer_progress.rb +2 -0
  29. data/lib/aspera/cli/version.rb +1 -1
  30. data/lib/aspera/command_line_builder.rb +1 -1
  31. data/lib/aspera/environment.rb +48 -14
  32. data/lib/aspera/node_simulator.rb +230 -112
  33. data/lib/aspera/oauth/base.rb +34 -47
  34. data/lib/aspera/oauth/factory.rb +41 -2
  35. data/lib/aspera/oauth/jwt.rb +4 -1
  36. data/lib/aspera/persistency_action_once.rb +1 -1
  37. data/lib/aspera/persistency_folder.rb +20 -2
  38. data/lib/aspera/preview/generator.rb +1 -1
  39. data/lib/aspera/preview/utils.rb +8 -3
  40. data/lib/aspera/products/alpha.rb +30 -0
  41. data/lib/aspera/products/connect.rb +48 -0
  42. data/lib/aspera/products/other.rb +82 -0
  43. data/lib/aspera/products/transferd.rb +54 -0
  44. data/lib/aspera/rest.rb +18 -13
  45. data/lib/aspera/secret_hider.rb +2 -2
  46. data/lib/aspera/ssh.rb +31 -24
  47. data/lib/aspera/transfer/parameters.rb +2 -1
  48. data/lib/aspera/transfer/spec.yaml +22 -20
  49. data/lib/aspera/transfer/sync.rb +1 -5
  50. data/lib/aspera/transfer/uri.rb +2 -2
  51. data/lib/transferd_pb.rb +86 -0
  52. data/lib/transferd_services_pb.rb +84 -0
  53. data.tar.gz.sig +0 -0
  54. metadata +38 -21
  55. metadata.gz.sig +0 -0
  56. data/lib/aspera/ascp/products.rb +0 -168
  57. data/lib/transfer_pb.rb +0 -84
  58. data/lib/transfer_services_pb.rb +0 -82
@@ -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,7 +53,7 @@ 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
59
  DEMO_SERVER = 'demo'
@@ -78,6 +77,7 @@ module Aspera
78
77
  GEM_CHECK_DATE_FMT = '%Y/%m/%d'
79
78
  # for testing only
80
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
81
81
  private_constant :DEFAULT_CONFIG_FILENAME,
82
82
  :CONF_PRESET_CONFIG,
83
83
  :CONF_PRESET_VERSION,
@@ -98,7 +98,8 @@ module Aspera
98
98
  :WIZARD_RESULT_KEYS,
99
99
  :SELF_SIGNED_CERT,
100
100
  :PERSISTENCY_FOLDER,
101
- :DEFAULT_PRIV_KEY_LENGTH
101
+ :DEFAULT_PRIV_KEY_LENGTH,
102
+ :CONF_OVERVIEW_KEYS
102
103
 
103
104
  class << self
104
105
  def generate_rsa_private_key(path:, length: DEFAULT_PRIV_KEY_LENGTH)
@@ -145,7 +146,7 @@ module Aspera
145
146
 
146
147
  def initialize(**env)
147
148
  # we need to defer parsing of options until we have the config file, so we can use @extend with @preset
148
- super(**env)
149
+ super
149
150
  @use_plugin_defaults = true
150
151
  @config_presets = nil
151
152
  @config_checksum_on_disk = nil
@@ -213,7 +214,7 @@ module Aspera
213
214
  options.declare(:ascp_path, 'Path to ascp', handler: {o: Ascp::Installation.instance, m: :ascp_path})
214
215
  options.declare(:use_product, 'Use ascp from specified product', handler: {o: self, m: :option_use_product})
215
216
  options.declare(:sdk_url, 'URL to get SDK', default: SpecialValues::DEF)
216
- options.declare(:sdk_folder, 'SDK folder path', handler: {o: Ascp::Installation.instance, m: :sdk_folder})
217
+ options.declare(:sdk_folder, 'SDK folder path', handler: {o: Products::Transferd, m: :sdk_directory})
217
218
  options.declare(:progress_bar, 'Display progress bar', values: :bool, default: Environment.terminal?)
218
219
  # email options
219
220
  options.declare(:smtp, 'SMTP configuration', types: Hash)
@@ -232,22 +233,22 @@ module Aspera
232
233
  options.parse_options!
233
234
  @progress_bar = TransferProgress.new if options.get_option(:progress_bar)
234
235
  # Check SDK folder is set or not, for compatibility, we check in two places
235
- sdk_folder = Ascp::Installation.instance.sdk_folder rescue nil
236
- if sdk_folder.nil?
236
+ sdk_dir = Products::Transferd.sdk_directory rescue nil
237
+ if sdk_dir.nil?
237
238
  @sdk_default_location = true
238
239
  Log.log.debug('SDK folder is not set, checking default')
239
240
  # new location
240
- sdk_folder = self.class.default_app_main_folder(app_name: APP_NAME_SDK)
241
- Log.log.debug{"checking: #{sdk_folder}"}
242
- if !Dir.exist?(sdk_folder)
243
- 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}"}
244
245
  # former location
245
- former_sdk_folder = File.join(self.class.default_app_main_folder(app_name: Info::CMD_NAME), APP_NAME_SDK)
246
+ former_sdk_folder = File.join(self.class.default_app_main_folder(app_name: Info::CMD_NAME), DIR_SDK)
246
247
  Log.log.debug{"checking: #{former_sdk_folder}"}
247
- sdk_folder = former_sdk_folder if Dir.exist?(former_sdk_folder)
248
+ sdk_dir = former_sdk_folder if Dir.exist?(former_sdk_folder)
248
249
  end
249
- Log.log.debug{"using: #{sdk_folder}"}
250
- Ascp::Installation.instance.sdk_folder = sdk_folder
250
+ Log.log.debug{"using: #{sdk_dir}"}
251
+ Products::Transferd.sdk_directory = sdk_dir
251
252
  end
252
253
  pac_script = options.get_option(:fpac)
253
254
  # create PAC executor
@@ -414,7 +415,7 @@ module Aspera
414
415
 
415
416
  def periodic_check_newer_gem_version
416
417
  # get verification period
417
- delay_days = options.get_option(:version_check_days, mandatory: true)
418
+ delay_days = options.get_option(:version_check_days, mandatory: true).to_i
418
419
  # check only if not zero day
419
420
  return if delay_days.eql?(0)
420
421
  # get last date from persistency
@@ -663,10 +664,10 @@ module Aspera
663
664
  end
664
665
  case command
665
666
  when :list
666
- 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])
667
668
  when :info
668
669
  one_res.delete('links')
669
- return {type: :single_object, data: one_res}
670
+ return Main.result_single_object(one_res)
670
671
  when :version
671
672
  all_links = one_res['links']
672
673
  command = options.get_next_command(%i[list download open])
@@ -677,10 +678,9 @@ module Aspera
677
678
  end
678
679
  case command
679
680
  when :list
680
- return {type: :object_list, data: all_links}
681
+ return Main.result_object_list(all_links)
681
682
  when :download
682
683
  folder_dest = transfer.destination_folder(Transfer::Spec::DIRECTION_RECEIVE)
683
- # folder_dest=self.options.get_next_argument('destination folder')
684
684
  api_connect_cdn = Rest.new(base_url: CONNECT_WEB_URL)
685
685
  file_url = one_link['href']
686
686
  filename = file_url.gsub(%r{.*/}, '')
@@ -700,11 +700,12 @@ module Aspera
700
700
  return execute_connect_action
701
701
  when :use
702
702
  ascp_path = options.get_next_argument('path to ascp')
703
+ Ascp::Installation.instance.ascp_path = ascp_path
703
704
  formatter.display_status("ascp version: #{Ascp::Installation.instance.get_ascp_version(ascp_path)}")
704
705
  set_global_default(:ascp_path, ascp_path)
705
706
  return Main.result_nothing
706
707
  when :show
707
- return {type: :status, data: Ascp::Installation.instance.path(:ascp)}
708
+ return Main.result_status(Ascp::Installation.instance.path(:ascp))
708
709
  when :info
709
710
  # collect info from ascp executable
710
711
  data = Ascp::Installation.instance.ascp_info
@@ -713,13 +714,13 @@ module Aspera
713
714
  # add keys
714
715
  DataRepository::ELEMENTS.each_with_object(data){|i, h|h[i.to_s] = DataRepository.instance.item(i)}
715
716
  # declare those as secrets
716
- SecretHider::ADDITIONAL_KEYS_TO_HIDE.push(*DataRepository::ELEMENTS.map(&:to_s))
717
- 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)
718
719
  when :products
719
720
  command = options.get_next_command(%i[list use])
720
721
  case command
721
722
  when :list
722
- 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])
723
724
  when :use
724
725
  default_product = options.get_next_argument('product name')
725
726
  Ascp::Installation.instance.use_ascp_from_product(default_product)
@@ -728,23 +729,44 @@ module Aspera
728
729
  end
729
730
  when :install
730
731
  # reset to default location, if older default was used
731
- Ascp::Installation.instance.sdk_folder = self.class.default_app_main_folder(app_name: APP_NAME_SDK) if @sdk_default_location
732
- n, v = Ascp::Installation.instance.install_sdk(url: 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)
733
735
  return Main.result_status("Installed #{n} version #{v}")
734
736
  when :spec
735
- return {
736
- type: :object_list,
737
- data: Transfer::Parameters.man_table(formatter),
737
+ return Main.result_object_list(
738
+ Transfer::Parameters.man_table(formatter),
738
739
  fields: [%w[name type], Transfer::Parameters::SUPPORTED_AGENTS_SHORT.map(&:to_s), %w[description]].flatten.freeze
739
- }
740
+ )
740
741
  when :errors
741
742
  error_data = []
742
743
  Transfer::ERROR_INFO.each_pair do |code, prop|
743
744
  error_data.push(code: code, mnemonic: prop[:c], retry: prop[:r], info: prop[:a])
744
745
  end
745
- return {type: :object_list, data: error_data}
746
+ return Main.result_object_list(error_data)
747
+ else Aspera.error_unexpected_value(command)
746
748
  end
747
- raise "unexpected case: #{command}"
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)
768
+ end
769
+ Aspera.error_unreachable_line
748
770
  end
749
771
 
750
772
  # legacy actions available globally
@@ -763,12 +785,20 @@ module Aspera
763
785
  raise "no such preset: #{name}" if PRESET_EXIST_ACTIONS.include?(action) && !@config_presets.key?(name)
764
786
  case action
765
787
  when :list
766
- return {type: :value_list, data: @config_presets.keys, name: 'name'}
788
+ return Main.result_value_list(@config_presets.keys, 'name')
767
789
  when :overview
768
790
  # display process modifies the value (hide secrets): we do not want to save removed secrets
769
- return {type: :config_over, data: self.class.deep_clone(@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)
770
800
  when :show
771
- return {type: :single_object, data: self.class.deep_clone(@config_presets[name])}
801
+ return Main.result_single_object(self.class.deep_clone(@config_presets[name]))
772
802
  when :delete
773
803
  @config_presets.delete(name)
774
804
  return Main.result_status("Deleted: #{name}")
@@ -779,7 +809,7 @@ module Aspera
779
809
  case value
780
810
  when Numeric, String then return {type: :text, data: ExtendedValue.instance.evaluate(value.to_s)}
781
811
  end
782
- return {type: :single_object, data: value}
812
+ return Main.result_single_object(value)
783
813
  when :unset
784
814
  param_name = options.get_next_argument('parameter name')
785
815
  @config_presets[name].delete(param_name)
@@ -818,7 +848,7 @@ module Aspera
818
848
  user = options.get_option(:username, mandatory: true)
819
849
  result = lookup_preset(url: url, username: user)
820
850
  raise 'no such config found' if result.nil?
821
- return {type: :single_object, data: result}
851
+ return Main.result_single_object(result)
822
852
  when :secure
823
853
  identifier = options.get_next_argument('config name', mandatory: false)
824
854
  preset_names = identifier.nil? ? @config_presets.keys : [identifier]
@@ -856,13 +886,15 @@ module Aspera
856
886
  remote_certificate
857
887
  gem
858
888
  plugins
859
- flush_tokens
889
+ tokens
860
890
  echo
891
+ download
861
892
  wizard
862
893
  detect
863
894
  coffee
864
895
  image
865
896
  ascp
897
+ transferd
866
898
  email_test
867
899
  smtp_settings
868
900
  proxy_check
@@ -912,9 +944,27 @@ module Aspera
912
944
  end
913
945
  when :echo # display the content of a value given on command line
914
946
  return Formatter.auto_type(options.get_next_argument('value', validation: nil))
915
- when :flush_tokens
916
- deleted_files = OAuth::Factory.instance.flush_tokens
917
- 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
918
968
  when :plugins
919
969
  case options.get_next_command(%i[list create])
920
970
  when :list
@@ -928,7 +978,7 @@ module Aspera
928
978
  path: PluginFactory.instance.plugin_source(name)
929
979
  })
930
980
  end
931
- 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])
932
982
  when :create
933
983
  plugin_name = options.get_next_argument('name').downcase
934
984
  destination_folder = options.get_next_argument('folder', mandatory: false) || File.join(@main_folder, ASPERA_PLUGINS_FOLDERNAME)
@@ -954,10 +1004,7 @@ module Aspera
954
1004
  options.ask_missing_mandatory = true
955
1005
  # detect plugins by url and optional query
956
1006
  apps = identify_plugins_for_url.freeze
957
- return {
958
- type: :object_list,
959
- data: apps
960
- } if action.eql?(:detect)
1007
+ return Main.result_object_list(apps) if action.eql?(:detect)
961
1008
  return wizard_find(apps)
962
1009
  when :coffee
963
1010
  return Main.result_image(COFFEE_IMAGE, formatter: formatter)
@@ -965,6 +1012,8 @@ module Aspera
965
1012
  return Main.result_image(options.get_next_argument('image uri or blob'), formatter: formatter)
966
1013
  when :ascp
967
1014
  execute_action_ascp
1015
+ when :transferd
1016
+ execute_action_transferd
968
1017
  when :gem
969
1018
  case options.get_next_command(%i[path version name])
970
1019
  when :path then return Main.result_status(self.class.gem_src_root)
@@ -979,14 +1028,14 @@ module Aspera
979
1028
  send_email_template(email_template_default: EMAIL_TEST_TEMPLATE)
980
1029
  return Main.result_nothing
981
1030
  when :smtp_settings
982
- return {type: :single_object, data: email_settings}
1031
+ return Main.result_single_object(email_settings)
983
1032
  when :proxy_check
984
1033
  # ensure fpac was provided
985
1034
  options.get_option(:fpac, mandatory: true)
986
1035
  server_url = options.get_next_argument('server url')
987
1036
  return Main.result_status(@pac_exec.find_proxy_for_url(server_url))
988
1037
  when :check_update
989
- return {type: :single_object, data: check_gem_version}
1038
+ return Main.result_single_object(check_gem_version)
990
1039
  when :initdemo
991
1040
  if @config_presets.key?(DEMO_PRESET)
992
1041
  Log.log.warn{"Demo server preset already present: #{DEMO_PRESET}"}
@@ -1085,7 +1134,7 @@ module Aspera
1085
1134
  wiz_preset_name = elements.join('_').strip.downcase.gsub(/[^a-z0-9]/, '_').squeeze('_')
1086
1135
  end
1087
1136
  # test mode does not change conf file
1088
- 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)
1089
1138
  # Write configuration file
1090
1139
  formatter.display_status("Preparing preset: #{wiz_preset_name}")
1091
1140
  # init defaults if necessary
@@ -1196,7 +1245,8 @@ module Aspera
1196
1245
  Log.log.error do
1197
1246
  "Default config name [#{default_config_name}] specified for plugin [#{plugin_name_sym}], but it does not exist in config file.\n" \
1198
1247
  'Please fix the issue: either create preset with one parameter: ' \
1199
- "(#{Info::CMD_NAME} config id #{default_config_name} init @json:'{}') or remove default (#{Info::CMD_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})."
1200
1250
  end
1201
1251
  end
1202
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)
@@ -1211,11 +1261,11 @@ module Aspera
1211
1261
  command = options.get_next_command(%i[info list show create delete password])
1212
1262
  case command
1213
1263
  when :info
1214
- return {type: :single_object, data: vault_info}
1264
+ return Main.result_single_object(vault_info)
1215
1265
  when :list
1216
- 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))
1217
1267
  when :show
1218
- 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')))
1219
1269
  when :create
1220
1270
  label = options.get_next_argument('label', validation: String)
1221
1271
  info = options.get_next_argument('info', validation: Hash)
@@ -1264,6 +1314,8 @@ module Aspera
1264
1314
  info = vault_info
1265
1315
  case info[:type]
1266
1316
  when 'file'
1317
+ # this module requires comilation, so it is optinal
1318
+ require 'aspera/keychain/encrypted_hash'
1267
1319
  # absolute_path? introduced in ruby 2.7
1268
1320
  @vault = Keychain::EncryptedHash.new(
1269
1321
  info[:name].eql?(File.absolute_path(info[:name])) ? info[:name] : File.join(@main_folder, info[:name]),
@@ -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
@@ -305,7 +305,7 @@ module Aspera
305
305
  # authenticated user
306
306
  delivery_info['sources'] ||= [{'paths' => []}]
307
307
  first_source = delivery_info['sources'].first
308
- first_source['paths'].push(*transfer.source_list)
308
+ first_source['paths'].concat(transfer.source_list)
309
309
  source_id = instance_identifier(as_option: :remote_source) do |field, value|
310
310
  Aspera.assert(field.eql?('name'), exception_class: Cli::BadArgument){'only name as selector, or give id'}
311
311
  source_list = api_v3.read('source_shares')['items']
@@ -441,7 +441,9 @@ module Aspera
441
441
  Aspera.assert(field.eql?('name'), exception_class: Cli::BadArgument){'only name as selector, or give id'}
442
442
  self.class.get_source_id_by_name(value, source_list)
443
443
  end.to_i
444
- source_name = source_list.find{|i|i['id'].eql?(source_id)}['name']
444
+ selected_source = source_list.find{|i|i['id'].eql?(source_id)}
445
+ raise 'No such source' if selected_source.nil?
446
+ source_name = selected_source['name']
445
447
  source_hash = options.get_option(:storage, mandatory: true)
446
448
  # check value of option
447
449
  Aspera.assert_type(source_hash, Hash, exception_class: Cli::Error){'storage option'}
@@ -237,15 +237,18 @@ module Aspera
237
237
  end
238
238
  end
239
239
 
240
+ # @param [Srting] job identifier
241
+ # @return [Hash] result of API call for job status
240
242
  def wait_for_job(job_id)
243
+ result = nil
241
244
  loop do
242
- status = @api_v5.read("jobs/#{job_id}", {type: :formatted})
243
- return status unless JOB_RUNNING.include?(status['status'])
244
- formatter.long_operation_running(status['status'])
245
+ result = @api_v5.read("jobs/#{job_id}", {type: :formatted})
246
+ break unless JOB_RUNNING.include?(result['status'])
247
+ formatter.long_operation_running(result['status'])
245
248
  sleep(0.5)
246
249
  end
247
250
  formatter.long_operation_terminated
248
- Aspera.error_unreachable_line
251
+ return result
249
252
  end
250
253
 
251
254
  # Get a (full or partial) list of all entities of a given type with query: offset/limit
@@ -253,9 +256,10 @@ module Aspera
253
256
  # @param query [Hash,nil] additional query parameters
254
257
  # @param real_path [String] real path if it's n ot just the type
255
258
  # @param item_list_key [String] key in the result to get the list of items
256
- def list_entities(type:, real_path: nil, item_list_key: nil, query: {})
259
+ def list_entities(type:, real_path: nil, item_list_key: nil, query: nil)
257
260
  Log.log.trace1{"list_entities t=#{type} p=#{real_path} k=#{item_list_key} q=#{query}"}
258
261
  type = type.to_s if type.is_a?(Symbol)
262
+ query = {} if query.nil?
259
263
  Aspera.assert_type(type, String)
260
264
  Aspera.assert_type(query, Hash)
261
265
  item_list_key = type if item_list_key.nil?
@@ -555,9 +559,10 @@ module Aspera
555
559
  id_as_arg = 'type'
556
560
  when :accounts
557
561
  display_fields = Formatter.all_but('user_profile_data_attributes')
562
+ available_commands.push(:reset_password)
558
563
  when :oauth_clients
559
564
  display_fields = Formatter.all_but('public_key')
560
- adm_api = Rest.new(**@api_v5.params.merge(base_url: "#{@faspex5_api_base_url}/#{PATH_AUTH}"))
565
+ adm_api = Rest.new(**@api_v5.params, base_url: "#{@faspex5_api_base_url}/#{PATH_AUTH}")
561
566
  when :shared_inboxes, :workgroups
562
567
  available_commands.push(:members, :saml_groups, :invite_external_collaborator)
563
568
  res_id_query = {'all': true}
@@ -642,7 +647,12 @@ module Aspera
642
647
  value: value,
643
648
  query: {type: Rest.array_params(%w{local_user saml_user self_registered_user external_user})})['id']
644
649
  end
650
+ when :reset_password
651
+ contact_id = instance_identifier { |field, value| lookup_entity_by_field(type: res_type.to_s, field: field, value: value, query: res_id_query)['id']}
652
+ adm_api.create("#{res_type}/#{contact_id}/reset_password", {})
653
+ return Main.result_status('password reset, user shall check email')
645
654
  end
655
+ Aspera.error_unreachable_line
646
656
  end
647
657
 
648
658
  def execute_admin
@@ -655,16 +665,18 @@ module Aspera
655
665
  when *ADMIN_RESOURCES
656
666
  return execute_resource(command)
657
667
  when :clean_deleted
658
- delete_data = value_create_modify(command: command, default: {days_before_deleting_package_records: 365})
668
+ delete_data = value_create_modify(command: command, default: {})
669
+ delete_data = @api_v5.read('configuration').slice('days_before_deleting_package_records') if delete_data.empty?
659
670
  res = @api_v5.create('internal/packages/clean_deleted', delete_data)
660
671
  return {type: :single_object, data: res}
661
672
  when :events
662
673
  event_type = options.get_next_command(%i[application webhook])
663
674
  case event_type
664
675
  when :application
665
- return {type: :object_list, data: list_entities(type: 'application_events'), fields: %w[event_type created_at application user.name]}
676
+ return {type: :object_list, data: list_entities(type: 'application_events', query: query_read_delete),
677
+ fields: %w[event_type created_at application user.name]}
666
678
  when :webhook
667
- return {type: :object_list, data: list_entities(type: 'all_webhooks_events', item_list_key: 'events')}
679
+ return {type: :object_list, data: list_entities(type: 'all_webhooks_events', query: query_read_delete, item_list_key: 'events')}
668
680
  end
669
681
  when :configuration
670
682
  conf_path = 'configuration'
@@ -127,10 +127,13 @@ module Aspera
127
127
  # commands for execute_command_gen4
128
128
  COMMANDS_GEN4 = %i[mkdir rename delete upload download sync http_node_download show modify permission thumbnail v3].concat(NODE4_READ_ACTIONS).freeze
129
129
 
130
+ # commands supported in ATS for COS
130
131
  COMMANDS_COS = %i[upload download info access_keys api_details transfer].freeze
131
132
  COMMANDS_SHARES = (BASE_ACTIONS - %i[search]).freeze
132
133
  COMMANDS_FASPEX = COMMON_ACTIONS
133
134
 
135
+ GEN4_LS_FIELDS = %w[name type recursive_size size modified_time access_level].freeze
136
+
134
137
  def initialize(api: nil, **env)
135
138
  super(**env, basic_options: api.nil?)
136
139
  Node.declare_options(options) if api.nil?
@@ -484,22 +487,15 @@ module Aspera
484
487
  when :browse
485
488
  apifid = apifid_from_next_arg(top_file_id)
486
489
  file_info = apifid[:api].read_with_cache("files/#{apifid[:file_id]}")
487
- if file_info['type'].eql?('folder')
488
- result = apifid[:api].call(
489
- operation: 'GET',
490
- subpath: "files/#{apifid[:file_id]}/files",
491
- headers: Api::Node.cache_control_headers,
492
- query: query_read_delete)
493
- items = result[:data]
494
- formatter.display_item_count(result[:data].length, result[:http]['X-Total-Count'])
495
- else
496
- items = [file_info]
490
+ unless file_info['type'].eql?('folder')
491
+ # a single file
492
+ return {type: :object_list, data: [file_info], fields: GEN4_LS_FIELDS}
497
493
  end
498
- return {type: :object_list, data: items, fields: %w[name type recursive_size size modified_time access_level]}
494
+ return {type: :object_list, data: apifid[:api].list_files(apifid[:file_id]), fields: GEN4_LS_FIELDS}
499
495
  when :find
500
496
  apifid = apifid_from_next_arg(top_file_id)
501
- test_block = Api::Node.file_matcher_from_argument(options)
502
- return {type: :object_list, data: @api_node.find_files(apifid[:file_id], test_block), fields: ['path']}
497
+ find_lambda = Api::Node.file_matcher_from_argument(options)
498
+ return {type: :object_list, data: @api_node.find_files(apifid[:file_id], find_lambda), fields: ['path']}
503
499
  when :mkdir
504
500
  containing_folder_path = options.get_next_argument('path').split(Api::Node::PATH_SEPARATOR)
505
501
  new_folder = containing_folder_path.pop
@@ -608,12 +604,12 @@ module Aspera
608
604
  command_perm = options.get_next_command(%i[list create delete])
609
605
  case command_perm
610
606
  when :list
611
- # generic options : TODO: as arg ? query_read_delete
612
- list_options ||= {'include' => Rest.array_params(%w[access_level permission_count])}
613
- # add which one to get
614
- list_options['file_id'] = apifid[:file_id]
615
- list_options['inherited'] ||= false
616
- items = apifid[:api].read('permissions', list_options)
607
+ list_query = query_read_delete(default: {'include' => Rest.array_params(%w[access_level permission_count])})
608
+ # specify file to get permissions for unless not specified
609
+ list_query['file_id'] = apifid[:file_id] unless apifid[:file_id].to_s.empty?
610
+ list_query['inherited'] = false if list_query.key?('file_id') && !list_query.key?('inherited')
611
+ # NOTE: supports per_page and page and header X-Total-Count
612
+ items = apifid[:api].read('permissions', list_query)
617
613
  return {type: :object_list, data: items}
618
614
  when :delete
619
615
  return do_bulk_operation(command: command_perm, descr: 'identifier', values: :identifier) do |one_id|
@@ -802,42 +798,53 @@ module Aspera
802
798
  end
803
799
  case command
804
800
  when :list
801
+ transfer_filter = query_read_delete(default: {})
802
+ last_iteration_token = nil
805
803
  iteration_persistency = nil
806
- iteration_data = []
807
804
  if options.get_option(:once_only, mandatory: true)
808
805
  iteration_persistency = PersistencyActionOnce.new(
809
806
  manager: persistency,
810
- data: iteration_data,
807
+ data: [],
811
808
  id: IdGenerator.from_list([
812
809
  'node_transfers',
813
810
  options.get_option(:url, mandatory: true),
814
811
  options.get_option(:username, mandatory: true)
815
812
  ]))
813
+ if transfer_filter.delete('reset')
814
+ iteration_persistency.data.clear
815
+ iteration_persistency.save
816
+ return Main.result_status('Persistency reset')
817
+ end
818
+ last_iteration_token = iteration_persistency.data.first
816
819
  end
817
- transfer_filter = query_read_delete(default: {})
818
- if transfer_filter.delete('reset')
819
- iteration_data.clear
820
- iteration_persistency&.save
821
- return Main.result_status('Persistency reset')
822
- end
820
+ raise 'reset only with once_only' if transfer_filter.key?('reset') && iteration_persistency.nil?
823
821
  max_items = transfer_filter.delete(MAX_ITEMS)
824
- transfer_filter['iteration_token'] = iteration_persistency.data[0] unless iteration_data.empty?
825
822
  transfers_data = []
826
823
  loop do
824
+ transfer_filter['iteration_token'] = last_iteration_token unless last_iteration_token.nil?
827
825
  result = @api_node.call(operation: 'GET', subpath: res_class_path, query: transfer_filter)
828
- data = result[:data]
829
- transfers_data.concat(data)
830
- if !max_items.nil? && (transfers_data.length >= max_items)
826
+ # no data
827
+ break if result[:data].empty?
828
+ # get next iteration token from link
829
+ next_iteration_token = nil
830
+ link_info = result[:http]['Link']
831
+ unless link_info.nil?
832
+ m = link_info.match(/<([^>]+)>/)
833
+ raise "Cannot parse iteration in Link: #{link_info}" if m.nil?
834
+ next_iteration_token = CGI.parse(URI.parse(m[1]).query)['iteration_token']&.first
835
+ end
836
+ # same as last iteration: stop
837
+ break if next_iteration_token&.eql?(last_iteration_token)
838
+ last_iteration_token = next_iteration_token
839
+ transfers_data.concat(result[:data])
840
+ if max_items&.<=(transfers_data.length)
841
+ # if !max_items.nil? && (transfers_data.length >= max_items)
831
842
  transfers_data = transfers_data.slice(0, max_items)
832
843
  break
833
844
  end
834
- link_info = result[:http]['Link']
835
- break if iteration_persistency.nil? || data.empty? || link_info.nil?
836
- m = link_info.match(/<([^>]+)>/)
837
- raise "Problem with iteration: #{link_info}" if m.nil?
838
- iteration_token = CGI.parse(URI.parse(m[1]).query)['iteration_token']&.first
839
- iteration_data[0] = transfer_filter['iteration_token'] = iteration_token
845
+ break if last_iteration_token.nil?
840
846
  end
847
+ iteration_persistency&.data&.[]=(0, last_iteration_token)
841
848
  iteration_persistency&.save
842
849
  return {
843
850
  type: :object_list,
@@ -1019,7 +1026,7 @@ module Aspera
1019
1026
  raise 'Missing key: url' unless parameters.key?(:url)
1020
1027
  uri = URI.parse(parameters[:url])
1021
1028
  server = WebServerSimple.new(uri, certificate: parameters[:certificate])
1022
- server.mount(uri.path, NodeSimulatorServlet, parameters[:credentials], transfer)
1029
+ server.mount(uri.path, NodeSimulatorServlet, parameters[:credentials], NodeSimulator.new)
1023
1030
  server.start
1024
1031
  return Main.result_status('Simulator terminated')
1025
1032
  end
@@ -79,6 +79,8 @@ module Aspera
79
79
  new_title = @sessions.length < 2 ? @title.to_s : "[#{@sessions.length}] #{@title}"
80
80
  @progress_bar.title = new_title unless @progress_bar.title.eql?(new_title)
81
81
  @progress_bar.increment if !progress_provided && !@completed
82
+ rescue ProgressBar::InvalidProgressError => e
83
+ Log.log.error{"Progress error: #{e}"}
82
84
  end
83
85
 
84
86
  private