aspera-cli 4.22.0 → 4.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +374 -364
  4. data/README.md +255 -155
  5. data/lib/aspera/agent/direct.rb +1 -1
  6. data/lib/aspera/api/aoc.rb +9 -12
  7. data/lib/aspera/api/httpgw.rb +8 -4
  8. data/lib/aspera/ascmd.rb +14 -6
  9. data/lib/aspera/ascp/installation.rb +6 -3
  10. data/lib/aspera/assert.rb +3 -3
  11. data/lib/aspera/cli/hints.rb +9 -1
  12. data/lib/aspera/cli/main.rb +1 -1
  13. data/lib/aspera/cli/manager.rb +1 -1
  14. data/lib/aspera/cli/plugin.rb +1 -1
  15. data/lib/aspera/cli/plugins/aoc.rb +33 -23
  16. data/lib/aspera/cli/plugins/config.rb +20 -15
  17. data/lib/aspera/cli/plugins/node.rb +96 -92
  18. data/lib/aspera/cli/plugins/server.rb +1 -0
  19. data/lib/aspera/cli/transfer_agent.rb +7 -11
  20. data/lib/aspera/cli/version.rb +1 -1
  21. data/lib/aspera/data_repository.rb +1 -0
  22. data/lib/aspera/environment.rb +1 -0
  23. data/lib/aspera/log.rb +1 -0
  24. data/lib/aspera/oauth/base.rb +2 -0
  25. data/lib/aspera/oauth/factory.rb +1 -0
  26. data/lib/aspera/preview/file_types.rb +40 -33
  27. data/lib/aspera/preview/generator.rb +1 -1
  28. data/lib/aspera/products/connect.rb +1 -0
  29. data/lib/aspera/rest.rb +18 -7
  30. data/lib/aspera/rest_error_analyzer.rb +1 -0
  31. data/lib/aspera/ssh.rb +1 -1
  32. data/lib/aspera/temp_file_manager.rb +1 -0
  33. data/lib/aspera/timer_limiter.rb +7 -5
  34. data/lib/aspera/transfer/async_conf.schema.yaml +716 -0
  35. data/lib/aspera/transfer/sync.rb +14 -4
  36. data/lib/aspera/transfer/sync_instance.schema.yaml +7 -0
  37. data/lib/aspera/transfer/sync_session.schema.yaml +7 -0
  38. data.tar.gz.sig +0 -0
  39. metadata +3 -5
  40. metadata.gz.sig +0 -0
  41. data/examples/dascli +0 -30
  42. data/examples/get_proto_file.rb +0 -8
  43. data/examples/proxy.pac +0 -60
@@ -325,7 +325,7 @@ module Aspera
325
325
  session.delete(:io)
326
326
  # if command was successfully started, check its status
327
327
  unless command_pid.nil?
328
- Process.kill(:INT, command_pid) if @monitor
328
+ Process.kill(:INT, command_pid) if @monitor && !Environment.os.eql?(Environment::OS_WINDOWS)
329
329
  # collect process exit status or wait for termination
330
330
  _, status = Process.wait2(command_pid)
331
331
  # process stderr of ascp
@@ -34,6 +34,8 @@ module Aspera
34
34
  # types of events for shared folder creation
35
35
  # Node events: permission.created permission.modified permission.deleted
36
36
  PERMISSIONS_CREATED = ['permission.created'].freeze
37
+ # Special name when creating workspace shared folders
38
+ ID_AK_ADMIN = 'ASPERA_ACCESS_KEY_ADMIN'
37
39
 
38
40
  private_constant :MAX_AOC_URL_REDIRECT,
39
41
  :CLIENT_ID_PREFIX,
@@ -43,7 +45,8 @@ module Aspera
43
45
  :JWT_AUDIENCE,
44
46
  :OAUTH_API_SUBPATH,
45
47
  :USER_INFO_FIELDS_MIN,
46
- :PERMISSIONS_CREATED
48
+ :PERMISSIONS_CREATED,
49
+ :ID_AK_ADMIN
47
50
 
48
51
  # various API scopes supported
49
52
  SCOPE_FILES_SELF = 'self'
@@ -546,17 +549,12 @@ module Aspera
546
549
  transfer_spec['tags'][Transfer::Spec::TAG_RESERVED]['app'] = app_info[:app]
547
550
  end
548
551
 
549
- ID_AK_ADMIN = 'ASPERA_ACCESS_KEY_ADMIN'
550
552
  # Callback from Plugins::Node
551
553
  # add application specific tags to permissions creation
552
554
  # @param perm_data [Hash] parameters for creating permissions
553
555
  # @param app_info [Hash] application information
554
556
  def permissions_set_create_params(perm_data:, app_info:)
555
- # workspace shared folder:
556
- # access_id = "#{ID_AK_ADMIN}_WS_#{app_info[:workspace_id]}"
557
557
  defaults = {
558
- # 'access_type' => 'user', # mandatory: user or group
559
- # 'access_id' => access_id, # id of user or group or special
560
558
  'tags' => {
561
559
  Transfer::Spec::TAG_RESERVED => {
562
560
  'files' => {
@@ -567,8 +565,6 @@ module Aspera
567
565
  'shared_by_user_id' => current_user_info['id'],
568
566
  'shared_by_name' => current_user_info['name'],
569
567
  'shared_by_email' => current_user_info['email'],
570
- # 'shared_with_name' => access_id,
571
- # 'share_as' => new_name_for_folder,
572
568
  'access_key' => app_info[:node_info]['access_key'],
573
569
  'node' => app_info[:node_info]['name']
574
570
  }
@@ -578,19 +574,20 @@ module Aspera
578
574
  }
579
575
  perm_data.deep_merge!(defaults)
580
576
  tag_workspace = perm_data['tags'][Transfer::Spec::TAG_RESERVED]['files']['workspace']
581
- case perm_data['with']
577
+ shared_with = perm_data.delete('with')
578
+ case shared_with
582
579
  when NilClass
583
580
  when ''
581
+ # workspace shared folder
584
582
  perm_data['access_type'] = 'user'
585
583
  perm_data['access_id'] = "#{ID_AK_ADMIN}_WS_#{app_info[:workspace_id]}"
586
584
  tag_workspace['shared_with_name'] = perm_data['access_id']
587
585
  else
588
- entity_info = lookup_by_name('contacts', perm_data['with'], query: {'current_workspace_id' => app_info[:workspace_id]})
586
+ entity_info = lookup_by_name('contacts', shared_with, query: {'current_workspace_id' => app_info[:workspace_id]})
589
587
  perm_data['access_type'] = entity_info['source_type']
590
588
  perm_data['access_id'] = entity_info['source_id']
591
- tag_workspace['shared_with_name'] = entity_info['email']
589
+ tag_workspace['shared_with_name'] = entity_info['email'] # TODO: check that ???
592
590
  end
593
- perm_data.delete('with')
594
591
  if perm_data.key?('as')
595
592
  tag_workspace['share_as'] = perm_data['as']
596
593
  perm_data.delete('as')
@@ -244,18 +244,22 @@ module Aspera
244
244
  end
245
245
 
246
246
  def download(transfer_spec)
247
- transfer_spec['zip_required'] ||= false
248
247
  transfer_spec['source_root'] ||= '/'
248
+ default_file_name = transfer_spec['paths'].first['source']
249
+ source_is_folder = %w[. /].include?(default_file_name)
250
+ default_file_name = 'http_download' if source_is_folder
251
+ transfer_spec['zip_required'] ||= source_is_folder || transfer_spec['paths'].length > 1
249
252
  # is normally provided by application, like package name
250
253
  if !transfer_spec.key?('download_name')
251
254
  # by default it is the name of first file
252
- download_name = File.basename(transfer_spec['paths'].first['source'], '.*')
253
- # ands add indication of number of files if there is more than one
255
+ download_name = File.basename(default_file_name, '.*')
256
+ # add indication of number of files if there is more than one
254
257
  if transfer_spec['paths'].length > 1
255
258
  download_name += " #{transfer_spec['paths'].length} Files"
256
259
  end
257
260
  transfer_spec['download_name'] = download_name
258
261
  end
262
+ # start transfer session on httpgw
259
263
  creation = create('download', {'transfer_spec' => transfer_spec})
260
264
  transfer_uuid = creation['url'].split('/').last
261
265
  file_name =
@@ -264,7 +268,7 @@ module Aspera
264
268
  transfer_spec['download_name'] + '.zip'
265
269
  else
266
270
  # it is a plain file if we don't require zip and there is only one file
267
- File.basename(transfer_spec['paths'].first['source'])
271
+ File.basename(default_file_name)
268
272
  end
269
273
  file_path = File.join(transfer_spec['destination_root'], file_name)
270
274
  call(operation: 'GET', subpath: "download/#{transfer_uuid}", save_to_file: file_path)
data/lib/aspera/ascmd.rb CHANGED
@@ -10,7 +10,7 @@ module Aspera
10
10
  # execute: "ascmd -h" to get syntax
11
11
  # Note: "ls" can take filters: as_ls -f *.txt -f *.bin /
12
12
  class AsCmd
13
- # number of arguments for each operation
13
+ # number of arguments for each operation (to allow splitting into batches)
14
14
  OPS_ARGS = {
15
15
  cp: 2,
16
16
  df: 0,
@@ -23,11 +23,11 @@ module Aspera
23
23
  rm: 1
24
24
  }.freeze
25
25
 
26
- # protocol is based on Type-Length-Value
27
- # type start at one, but array index start at zero
26
+ # Protocol is based on Type-Length-Value
27
+ # Type start at one, but array index start at zero
28
28
  ENUM_START = 1
29
29
 
30
- # description of result structures (see ascmdtypes.h).
30
+ # Description of result structures (see ascmdtypes.h).
31
31
  # Base types are big endian
32
32
  # key = name of type
33
33
  # index in array `fields` is the type (minus ENUM_START)
@@ -79,17 +79,25 @@ module Aspera
79
79
  end
80
80
 
81
81
  # execute an "as" command on a remote server
82
+ # Version 2 allows use of reverse proxy with multiple addresses.
82
83
  # @param [Symbol] one of OPERATIONS
83
84
  # @param [Array] parameters for "as" command
84
85
  # @return result of command, type depends on command (bool, array, hash)
85
- def execute_single(action_sym, arguments)
86
+ def execute_single(action_sym, arguments, version: 1, host: nil)
86
87
  arguments = [] if arguments.nil?
87
88
  Log.log.debug{"execute_single:#{action_sym}:#{arguments}"}
88
89
  Aspera.assert_type(action_sym, Symbol)
89
90
  Aspera.assert_type(arguments, Array)
90
91
  Aspera.assert(arguments.all?(String), 'arguments must be strings')
92
+ remote_cmd = 'ascmd'
91
93
  # lines of commands (String's)
92
94
  command_lines = []
95
+ if version.eql?(2)
96
+ cmd = "as_session_init --protocol=#{version}"
97
+ cmd += " --host=#{host}" if host
98
+ command_lines.push(cmd)
99
+ remote_cmd += ' -V2'
100
+ end
93
101
  # add "as_" command
94
102
  main_command = "as_#{action_sym}"
95
103
  arg_batches =
@@ -114,7 +122,7 @@ module Aspera
114
122
  stdin_input = command_lines.join("\n")
115
123
  Log.log.trace1{"execute_single:#{stdin_input}"}
116
124
  # execute, get binary output
117
- byte_buffer = @command_executor.execute('ascmd', stdin_input).unpack('C*')
125
+ byte_buffer = @command_executor.execute(remote_cmd, stdin_input).unpack('C*')
118
126
  raise 'ERROR: empty answer from server' if byte_buffer.empty?
119
127
  # get hash or table result
120
128
  result = self.class.parse(byte_buffer, :result)
@@ -32,6 +32,7 @@ module Aspera
32
32
  # Installation.instance.ascp_path=""
33
33
  class Installation
34
34
  include Singleton
35
+
35
36
  DEFAULT_ASPERA_CONF = <<~END_OF_CONFIG_FILE
36
37
  <?xml version='1.0' encoding='UTF-8'?>
37
38
  <CONF version="2">
@@ -196,6 +197,8 @@ module Aspera
196
197
  last_line = ''
197
198
  while (line = stderr.gets)
198
199
  line.chomp!
200
+ # skip lines that may have accents
201
+ next unless line.valid_encoding?
199
202
  last_line = line
200
203
  case line
201
204
  when /^DBG Path ([^ ]+) (dir|file) +: (.*)$/
@@ -211,7 +214,7 @@ module Aspera
211
214
  data['product_name'] = Regexp.last_match(1)
212
215
  data['product_version'] = Regexp.last_match(2)
213
216
  when /^LOG Initializing FASP version ([^,]+),/
214
- data['sdk_ascp_version'] = Regexp.last_match(1)
217
+ data['ascp_version'] = Regexp.last_match(1)
215
218
  end
216
219
  end
217
220
  if !thread.value.exitstatus.eql?(1) && !data.key?('root')
@@ -226,9 +229,9 @@ module Aspera
226
229
  data = {}
227
230
  File.binread(ascp_path).scan(/[\x20-\x7E]{10,}/) do |bin_string|
228
231
  if (m = bin_string.match(/OPENSSLDIR.*"(.*)"/))
229
- data['openssldir'] = m[1]
232
+ data['ascp_openssl_dir'] = m[1]
230
233
  elsif (m = bin_string.match(/OpenSSL (\d[^ -]+)/))
231
- data['openssl_version'] = m[1]
234
+ data['ascp_openssl_version'] = m[1]
232
235
  end
233
236
  end if File.file?(ascp_path)
234
237
  return data
data/lib/aspera/assert.rb CHANGED
@@ -22,7 +22,7 @@ module Aspera
22
22
  # @param value [Object] the value to check
23
23
  # @param type [Class] the expected type
24
24
  def assert_type(value, type, exception_class: AssertError)
25
- assert(value.is_a?(type), exception_class: exception_class){"#{block_given? ? "#{yield}: " : nil}expecting #{type}, but have #{value.inspect}"}
25
+ assert(value.is_a?(type), exception_class: exception_class){"#{"#{yield}: " if block_given?}expecting #{type}, but have #{value.inspect}"}
26
26
  end
27
27
 
28
28
  # assert that value is one of the given values
@@ -33,7 +33,7 @@ module Aspera
33
33
  assert(values.include?(value), exception_class: exception_class) do
34
34
  val_list = values.inspect
35
35
  val_list = "one of #{val_list}" if values.is_a?(Array)
36
- "#{block_given? ? "#{yield}: " : nil}expecting #{val_list}, but have #{value.inspect}"
36
+ "#{"#{yield}: " if block_given?}expecting #{val_list}, but have #{value.inspect}"
37
37
  end
38
38
  end
39
39
 
@@ -47,7 +47,7 @@ module Aspera
47
47
  # @param exception_class exception to raise
48
48
  # @param block additional description in front
49
49
  def error_unexpected_value(value, exception_class: InternalError)
50
- raise exception_class, "#{block_given? ? "#{yield}: " : nil}unexpected value: #{value.inspect}"
50
+ raise exception_class, "#{"#{yield}: " if block_given?}unexpected value: #{value.inspect}"
51
51
  end
52
52
 
53
53
  def require_method!(name)
@@ -64,7 +64,15 @@ module Aspera
64
64
  remediation: [
65
65
  'If remote node is Cloud Pak For Integration',
66
66
  'Make sure that a LoadBalancer is active on cluster',
67
- 'Check the external address of Aspera tcp-proxy pod'
67
+ 'Check the external address of Aspera tcp-proxy Pod'
68
+ ]
69
+ },
70
+ {
71
+ exception: Aspera::RestCallError,
72
+ match: /Invalid subject\./,
73
+ remediation: [
74
+ 'It seems that this user name is not registered on the server',
75
+ 'Check the user name and try again'
68
76
  ]
69
77
  }
70
78
  ]
@@ -198,7 +198,7 @@ module Aspera
198
198
  rescue Net::SSH::AuthenticationFailed => e; exception_info = {e: e, t: 'SSH', security: true}
199
199
  rescue OpenSSL::SSL::SSLError => e; exception_info = {e: e, t: 'SSL'}
200
200
  rescue Cli::BadArgument => e; exception_info = {e: e, t: 'Argument', usage: true}
201
- rescue Cli::BadIdentifier => e; exception_info = {e: e, t: 'Identifier'}
201
+ rescue Cli::BadIdentifier => e; exception_info = {e: e, t: 'Identifier'}
202
202
  rescue Cli::Error => e; exception_info = {e: e, t: 'Tool', usage: true}
203
203
  rescue Transfer::Error => e; exception_info = {e: e, t: 'Transfer'}
204
204
  rescue RestCallError => e; exception_info = {e: e, t: 'Rest'}
@@ -112,7 +112,7 @@ module Aspera
112
112
  value_list = check_array ? to_check : [to_check]
113
113
  value_list.each do |value|
114
114
  raise Cli::BadArgument,
115
- "#{what.to_s.capitalize} #{descr} is a #{value.class} but must be #{type_list.length > 1 ? 'one of: ' : ''}#{type_list.map(&:name).join(', ')}" unless
115
+ "#{what.to_s.capitalize} #{descr} is a #{value.class} but must be #{'one of: ' if type_list.length > 1}#{type_list.map(&:name).join(', ')}" unless
116
116
  type_list.any?{ |t| value.is_a?(t)}
117
117
  end
118
118
  end
@@ -240,7 +240,7 @@ module Aspera
240
240
  # @param default [Object] default value if not provided
241
241
  def value_create_modify(command:, description: nil, type: Hash, bulk: false, default: nil)
242
242
  value = options.get_next_argument(
243
- "parameters for #{command}#{description.nil? ? '' : " (#{description})"}", mandatory: default.nil?,
243
+ "parameters for #{command}#{" (#{description})" unless description.nil?}", mandatory: default.nil?,
244
244
  validation: bulk ? Array : type)
245
245
  value = default if value.nil?
246
246
  unless type.nil?
@@ -50,10 +50,11 @@ module Aspera
50
50
  'archived' => false,
51
51
  'has_content' => true,
52
52
  'received' => true,
53
- 'completed' => true}.freeze
53
+ 'completed' => true
54
+ }.freeze
55
+ PACKAGE_LIST_DEFAULT_FIELDS = %w[id name created_at files_completed bytes_transferred].freeze
54
56
  # options and parameters for Api::AoC.new
55
57
  OPTIONS_NEW = %i[url auth client_id client_secret scope redirect_uri private_key passphrase username password workspace].freeze
56
- PACKAGE_LIST_DEFAULT_FIELDS = %w[id name created_at files_completed bytes_transferred].freeze
57
58
 
58
59
  private_constant :REDIRECT_LOCALHOST, :STD_AUTH_TYPES, :ADMIN_OBJECTS, :PACKAGE_RECEIVED_BASE_QUERY, :OPTIONS_NEW, :PACKAGE_LIST_DEFAULT_FIELDS
59
60
  class << self
@@ -191,22 +192,28 @@ module Aspera
191
192
  options.declare(:auth, 'OAuth type of authentication', values: STD_AUTH_TYPES, default: :jwt)
192
193
  options.declare(:client_id, 'OAuth API client identifier')
193
194
  options.declare(:client_secret, 'OAuth API client secret')
194
- options.declare(:scope, 'OAuth scope for AoC API calls', default: Api::AoC::SCOPE_FILES_USER)
195
+ options.declare(:scope, 'OAuth scope for AoC API calls')
195
196
  options.declare(:redirect_uri, 'OAuth API client redirect URI')
196
197
  options.declare(:private_key, 'OAuth JWT RSA private key PEM value (prefix file path with @file:)')
197
198
  options.declare(:passphrase, 'RSA private key passphrase', types: String)
198
199
  options.declare(:workspace, 'Name of workspace', types: [String, NilClass], default: Api::AoC::DEFAULT_WORKSPACE)
199
200
  options.declare(:new_user_option, 'New user creation option for unknown package recipients', types: Hash)
200
201
  options.declare(:validate_metadata, 'Validate shared inbox metadata', values: :bool, default: true)
202
+ options.declare(:package_folder, 'Field of package to use as folder name, or @none:', types: [String, NilClass])
201
203
  options.parse_options!
202
204
  # add node plugin options (for manual)
203
205
  Node.declare_options(options)
204
206
  end
205
207
 
206
- def api_from_options(new_base_path)
207
- create_values = {subpath: new_base_path, secret_finder: config}
208
+ def api_from_options(aoc_base_path)
209
+ create_values = OPTIONS_NEW.each_with_object({
210
+ subpath: aoc_base_path,
211
+ secret_finder: config}) do |i, m|
212
+ m[i] = options.get_option(i) unless options.get_option(i).nil?
213
+ end
214
+ create_values[:scope] = Api::AoC::SCOPE_FILES_USER if create_values[:scope].nil?
208
215
  # create an API object with the same options, but with a different subpath
209
- return Api::AoC.new(**OPTIONS_NEW.each_with_object(create_values){ |i, m| m[i] = options.get_option(i) unless options.get_option(i).nil?})
216
+ return Api::AoC.new(**create_values)
210
217
  rescue ArgumentError => e
211
218
  if (m = e.message.match(/missing keyword: :(.*)$/))
212
219
  raise Cli::Error, "Missing option: #{m[1]}"
@@ -443,7 +450,9 @@ module Aspera
443
450
  default_fields.push('app_type', 'app_name', 'available', 'direct_authorizations_allowed', 'workspace_authorizations_allowed')
444
451
  when :client, :client_access_key, :dropbox, :group, :package, :saml_configuration, :workspace then default_fields.push('name')
445
452
  when :client_registration_token then default_fields.push('value', 'data.client_subject_scopes', 'created_at')
446
- when :contact then default_fields = %w[email name source_id source_type]
453
+ when :contact
454
+ default_fields = %w[source_type source_id name email]
455
+ default_query = {'include_only_user_personal_contacts' => true} if aoc_api.oauth.scope == Api::AoC::SCOPE_FILES_USER
447
456
  when :node then default_fields.push('name', 'host', 'access_key')
448
457
  when :operation then default_fields = nil
449
458
  when :short_link then default_fields.push('short_url', 'data.url_token_data.purpose')
@@ -486,8 +495,8 @@ module Aspera
486
495
  ADMIN_ACTIONS = %i[ats resource usage_reports analytics subscription auth_providers].concat(ADMIN_OBJECTS).freeze
487
496
 
488
497
  def execute_admin_action
489
- # upgrade scope to admin
490
- aoc_api.oauth.scope = Api::AoC::SCOPE_FILES_ADMIN
498
+ # default scope to admin
499
+ aoc_api.oauth.scope = Api::AoC::SCOPE_FILES_ADMIN if options.get_option(:scope).nil?
491
500
  command_admin = options.get_next_command(ADMIN_ACTIONS)
492
501
  case command_admin
493
502
  when :resource
@@ -801,7 +810,9 @@ module Aspera
801
810
  when :tier_restrictions
802
811
  return Main.result_single_object(aoc_api.read('tier_restrictions'))
803
812
  when :user
804
- case options.get_next_command(%i[workspaces profile preferences])
813
+ case options.get_next_command(%i[workspaces profile preferences contacts])
814
+ when :contacts
815
+ return execute_resource_action(:contact)
805
816
  # when :settings
806
817
  # return Main.result_object_list(aoc_api.read('client_settings/'))
807
818
  when :workspaces
@@ -890,31 +901,30 @@ module Aspera
890
901
  # remove from list the ones already downloaded
891
902
  reject_packages_from_persistency(all_packages, skip_ids_persistency)
892
903
  ids_to_download = all_packages.map{ |e| e['id']}
904
+ formatter.display_status("Found #{ids_to_download.length} package(s).")
893
905
  else
894
906
  # single id to array
895
907
  ids_to_download = [ids_to_download] unless ids_to_download.is_a?(Array)
896
908
  end
897
- file_list =
898
- begin
899
- transfer.source_list.map{ |i| {'source'=>i}}
900
- rescue Cli::BadArgument
901
- [{'source' => '.'}]
902
- end
903
- # list here
909
+ # download all files, or specified list only
910
+ ts_paths = transfer.ts_source_paths(default: ['.'])
911
+ per_package_field = options.get_option(:package_folder)
912
+ destination_folder = transfer.destination_folder(Transfer::Spec::DIRECTION_RECEIVE)
904
913
  result_transfer = []
905
- formatter.display_status("Found #{ids_to_download.length} package(s).")
906
914
  ids_to_download.each do |package_id|
907
915
  package_info = aoc_api.read("packages/#{package_id}")
908
- formatter.display_status("downloading package: [#{package_info['id']}] #{package_info['name']}")
909
916
  package_node_api = aoc_api.node_api_from(
910
917
  node_id: package_info['node_id'],
911
918
  package_info: package_info,
912
919
  **workspace_id_hash(name: true))
920
+ transfer_spec = package_node_api.transfer_spec_gen4(
921
+ package_info['contents_file_id'],
922
+ Transfer::Spec::DIRECTION_RECEIVE,
923
+ {'paths'=> ts_paths})
924
+ transfer.option_transfer_spec['destination_root'] = File.join(destination_folder, package_info[per_package_field]) unless per_package_field.nil?
925
+ formatter.display_status(%Q{Downloading package: [#{package_info['id']}] "#{package_info['name']}" to [#{transfer.option_transfer_spec['destination_root']}]})
913
926
  statuses = transfer.start(
914
- package_node_api.transfer_spec_gen4(
915
- package_info['contents_file_id'],
916
- Transfer::Spec::DIRECTION_RECEIVE,
917
- {'paths'=> file_list}),
927
+ transfer_spec,
918
928
  rest_token: package_node_api)
919
929
  result_transfer.push({'package' => package_id, Main::STATUS_FIELD => statuses})
920
930
  # update skip list only if all transfer sessions completed
@@ -210,23 +210,23 @@ module Aspera
210
210
  options.declare(:test_mode, 'Wizard: skip private key check step', values: :bool, default: false)
211
211
  options.declare(:key_path, 'Wizard: path to private key for JWT')
212
212
  # Transfer SDK options
213
- options.declare(:ascp_path, 'Path to ascp', handler: {o: Ascp::Installation.instance, m: :ascp_path})
214
- options.declare(:use_product, 'Use ascp from specified product', handler: {o: self, m: :option_use_product})
215
- options.declare(:sdk_url, 'URL to get Aspera Transfer Daemon', default: SpecialValues::DEF)
216
- options.declare(:locations_url, 'URL to get locations of Aspera Transfer Daemon', handler: {o: Ascp::Installation.instance, m: :transferd_urls})
217
- options.declare(:sdk_folder, 'SDK folder path', handler: {o: Products::Transferd, m: :sdk_directory})
213
+ options.declare(:ascp_path, 'Ascp: Path to ascp', handler: {o: Ascp::Installation.instance, m: :ascp_path})
214
+ options.declare(:use_product, 'Ascp: Use ascp from specified product', handler: {o: self, m: :option_use_product})
215
+ options.declare(:sdk_url, 'Ascp: URL to get Aspera Transfer Executables', default: SpecialValues::DEF)
216
+ options.declare(:locations_url, 'Ascp: URL to get locations of Aspera Transfer Daemon', handler: {o: Ascp::Installation.instance, m: :transferd_urls})
217
+ options.declare(:sdk_folder, 'Ascp: SDK folder path', handler: {o: Products::Transferd, m: :sdk_directory})
218
218
  options.declare(:progress_bar, 'Display progress bar', values: :bool, default: Environment.terminal?)
219
219
  # email options
220
- options.declare(:smtp, 'SMTP configuration', types: Hash)
221
- options.declare(:notify_to, 'Email recipient for notification of transfers')
222
- options.declare(:notify_template, 'Email ERB template for notification of transfers')
220
+ options.declare(:smtp, 'Email: SMTP configuration', types: Hash)
221
+ options.declare(:notify_to, 'Email: Recipient for notification of transfers')
222
+ options.declare(:notify_template, 'Email: ERB template for notification of transfers')
223
223
  # HTTP options
224
- options.declare(:insecure, 'Do not validate any HTTPS certificate', values: :bool, handler: {o: self, m: :option_insecure}, default: :no)
225
- options.declare(:ignore_certificate, 'Do not validate HTTPS certificate for these URLs', types: Array, handler: {o: self, m: :option_ignore_cert_host_port})
226
- options.declare(:silent_insecure, 'Issue a warning if certificate is ignored', values: :bool, handler: {o: self, m: :option_warn_insecure_cert}, default: :yes)
227
- options.declare(:cert_stores, 'List of folder with trusted certificates', types: [Array, String], handler: {o: self, m: :trusted_cert_locations})
228
- options.declare(:http_options, 'Options for HTTP/S socket', types: Hash, handler: {o: self, m: :option_http_options}, default: {})
229
- options.declare(:http_proxy, 'URL for HTTP proxy with optional credentials', types: String, handler: {o: self, m: :option_http_proxy})
224
+ options.declare(:insecure, 'HTTP/S: Do not validate any certificate', values: :bool, handler: {o: self, m: :option_insecure}, default: :no)
225
+ options.declare(:ignore_certificate, 'HTTP/S: Do not validate certificate for these URLs', types: Array, handler: {o: self, m: :option_ignore_cert_host_port})
226
+ options.declare(:silent_insecure, 'HTTP/S: Issue a warning if certificate is ignored', values: :bool, handler: {o: self, m: :option_warn_insecure_cert}, default: :yes)
227
+ options.declare(:cert_stores, 'HTTP/S: List of folder with trusted certificates', types: [Array, String], handler: {o: self, m: :trusted_cert_locations})
228
+ options.declare(:http_options, 'HTTP/S: Options for HTTP/S socket', types: Hash, handler: {o: self, m: :option_http_options}, default: {})
229
+ options.declare(:http_proxy, 'HTTP/S: URL for proxy with optional credentials', types: String, handler: {o: self, m: :option_http_proxy})
230
230
  options.declare(:cache_tokens, 'Save and reuse OAuth tokens', values: :bool, handler: {o: self, m: :option_cache_tokens})
231
231
  options.declare(:fpac, 'Proxy auto configuration script')
232
232
  options.declare(:proxy_credentials, 'HTTP proxy credentials for fpac: user, password', types: Array)
@@ -691,7 +691,7 @@ module Aspera
691
691
  # collect info from ascp executable
692
692
  data = Ascp::Installation.instance.ascp_info
693
693
  # add command line transfer spec
694
- data['ts'] = transfer.updated_ts
694
+ data['ts'] = transfer.option_transfer_spec
695
695
  # add keys
696
696
  DataRepository::ELEMENTS.each_with_object(data){ |i, h| h[i.to_s] = DataRepository.instance.item(i)}
697
697
  # declare those as secrets
@@ -1298,6 +1298,7 @@ module Aspera
1298
1298
  )
1299
1299
  end
1300
1300
 
1301
+ # Artifically raise an exception for tests
1301
1302
  def execute_test
1302
1303
  case options.get_next_command(%i[throw web])
1303
1304
  when :throw
@@ -1317,6 +1318,8 @@ module Aspera
1317
1318
  url.sub(%r{/+$}, '').sub(%r{^(https://[^/]+):443$}, '\1')
1318
1319
  end
1319
1320
 
1321
+ # Look for a preset that has the corresponding URL and username
1322
+ # @return the first one matching
1320
1323
  def lookup_preset(url:, username:)
1321
1324
  # remove extra info to maximize match
1322
1325
  url = canonical_url(url)
@@ -1329,6 +1332,8 @@ module Aspera
1329
1332
  nil
1330
1333
  end
1331
1334
 
1335
+ # Lookup the corresponding secret for the given URL and usernames
1336
+ # @raise Exception if mandatory and not found
1332
1337
  def lookup_secret(url:, username:, mandatory: false)
1333
1338
  secret = options.get_option(:secret)
1334
1339
  if secret.nil?