aspera-cli 4.25.0.pre → 4.25.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 (63) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +23 -17
  4. data/CONTRIBUTING.md +119 -47
  5. data/README.md +325 -239
  6. data/lib/aspera/agent/direct.rb +14 -12
  7. data/lib/aspera/agent/factory.rb +9 -6
  8. data/lib/aspera/agent/transferd.rb +8 -8
  9. data/lib/aspera/api/aoc.rb +33 -24
  10. data/lib/aspera/api/ats.rb +1 -0
  11. data/lib/aspera/api/faspex.rb +11 -5
  12. data/lib/aspera/ascmd.rb +1 -1
  13. data/lib/aspera/ascp/installation.rb +7 -7
  14. data/lib/aspera/ascp/management.rb +9 -5
  15. data/lib/aspera/assert.rb +3 -3
  16. data/lib/aspera/cli/extended_value.rb +10 -2
  17. data/lib/aspera/cli/formatter.rb +15 -62
  18. data/lib/aspera/cli/manager.rb +9 -43
  19. data/lib/aspera/cli/plugins/aoc.rb +71 -66
  20. data/lib/aspera/cli/plugins/ats.rb +30 -36
  21. data/lib/aspera/cli/plugins/base.rb +11 -6
  22. data/lib/aspera/cli/plugins/config.rb +21 -16
  23. data/lib/aspera/cli/plugins/console.rb +2 -1
  24. data/lib/aspera/cli/plugins/faspex.rb +7 -4
  25. data/lib/aspera/cli/plugins/faspex5.rb +12 -9
  26. data/lib/aspera/cli/plugins/faspio.rb +5 -2
  27. data/lib/aspera/cli/plugins/httpgw.rb +2 -1
  28. data/lib/aspera/cli/plugins/node.rb +10 -6
  29. data/lib/aspera/cli/plugins/oauth.rb +12 -11
  30. data/lib/aspera/cli/plugins/orchestrator.rb +2 -1
  31. data/lib/aspera/cli/plugins/preview.rb +2 -2
  32. data/lib/aspera/cli/plugins/server.rb +3 -2
  33. data/lib/aspera/cli/plugins/shares.rb +59 -20
  34. data/lib/aspera/cli/transfer_agent.rb +1 -2
  35. data/lib/aspera/cli/version.rb +1 -1
  36. data/lib/aspera/command_line_builder.rb +5 -5
  37. data/lib/aspera/coverage.rb +5 -1
  38. data/lib/aspera/dot_container.rb +108 -0
  39. data/lib/aspera/environment.rb +69 -89
  40. data/lib/aspera/faspex_postproc.rb +1 -1
  41. data/lib/aspera/id_generator.rb +7 -10
  42. data/lib/aspera/keychain/macos_security.rb +2 -2
  43. data/lib/aspera/log.rb +2 -1
  44. data/lib/aspera/oauth/base.rb +25 -38
  45. data/lib/aspera/oauth/factory.rb +5 -6
  46. data/lib/aspera/oauth/generic.rb +1 -1
  47. data/lib/aspera/oauth/jwt.rb +1 -1
  48. data/lib/aspera/oauth/url_json.rb +4 -3
  49. data/lib/aspera/oauth/web.rb +2 -2
  50. data/lib/aspera/preview/file_types.rb +1 -1
  51. data/lib/aspera/preview/terminal.rb +95 -29
  52. data/lib/aspera/preview/utils.rb +6 -5
  53. data/lib/aspera/rest.rb +5 -2
  54. data/lib/aspera/ssh.rb +6 -5
  55. data/lib/aspera/sync/conf.schema.yaml +2 -2
  56. data/lib/aspera/sync/operations.rb +3 -3
  57. data/lib/aspera/transfer/parameters.rb +6 -6
  58. data/lib/aspera/transfer/spec.schema.yaml +4 -4
  59. data/lib/aspera/transfer/spec_doc.rb +11 -21
  60. data/lib/aspera/uri_reader.rb +17 -3
  61. data.tar.gz.sig +0 -0
  62. metadata +17 -2
  63. metadata.gz.sig +0 -0
@@ -6,6 +6,7 @@ require 'aspera/colors'
6
6
  require 'aspera/secret_hider'
7
7
  require 'aspera/log'
8
8
  require 'aspera/assert'
9
+ require 'aspera/dot_container'
9
10
  require 'io/console'
10
11
  require 'optparse'
11
12
 
@@ -345,7 +346,7 @@ module Aspera
345
346
  raise Cli::BadArgument, "Invalid integer: #{result}" if int_result.nil?
346
347
  result = int_result
347
348
  end
348
- Log.log.debug{"#{descr}=#{result}"}
349
+ Log.log.trace1{"#{descr}=#{result}"}
349
350
  result = aliases[result] if aliases&.key?(result)
350
351
  # if value comes from JSON/YAML, it may come as Integer
351
352
  result = result.to_s if result.is_a?(Integer) && validation&.eql?(Allowed::TYPES_STRING)
@@ -486,11 +487,12 @@ module Aspera
486
487
  Log.log.trace1('After parse')
487
488
  rescue OptionParser::InvalidOption => e
488
489
  Log.log.trace1{"InvalidOption #{e}".red}
490
+ # An option like --a.b.c=d does: a={"b":{"c":ext_val(d)}}
489
491
  if (m = e.args.first.match(/^--([a-z\-]+)\.([^=]+)=(.+)$/))
490
492
  option, path, value = m.captures
491
493
  option_sym = self.class.option_line_to_name(option).to_sym
492
494
  if @declared_options.key?(option_sym)
493
- set_option(option_sym, dotted_to_extended(path, value), where: 'dotted')
495
+ set_option(option_sym, DotContainer.dotted_to_container(path, smart_convert(value), get_option(option_sym)), where: 'dotted')
494
496
  retry
495
497
  end
496
498
  end
@@ -561,14 +563,14 @@ module Aspera
561
563
 
562
564
  # Read remaining args and build an Array or Hash
563
565
  # @param value [nil] Argument to `@:` extended value
564
- def args_as_extended(value)
566
+ def args_as_extended(arg)
565
567
  # This extended value does not take args (`@:`)
566
- ExtendedValue.assert_no_value(value, :p)
568
+ ExtendedValue.assert_no_value(arg, :p)
567
569
  result = nil
568
570
  get_next_argument(:args, multiple: true).each do |arg|
569
571
  Aspera.assert(arg.include?(OPTION_VALUE_SEPARATOR)){"Positional argument: #{arg} does not inlude #{OPTION_VALUE_SEPARATOR}"}
570
- path, raw = arg.split(OPTION_VALUE_SEPARATOR, 2)
571
- result = dotted_to_extended(path, raw, result)
572
+ path, value = arg.split(OPTION_VALUE_SEPARATOR, 2)
573
+ result = DotContainer.dotted_to_container(path, smart_convert(value), result)
572
574
  end
573
575
  result
574
576
  end
@@ -590,40 +592,6 @@ module Aspera
590
592
  end
591
593
  end
592
594
 
593
- # Convert `String` to `Integer`, or keep `String` if not `Integer`
594
- def int_or_string(value)
595
- Integer(value, exception: false) || value
596
- end
597
-
598
- def new_hash_or_array_from_key(key)
599
- key.is_a?(Integer) ? [] : {}
600
- end
601
-
602
- def array_requires_integer_index!(container, index)
603
- Aspera.assert(container.is_a?(Hash) || index.is_a?(Integer)){'Using String index when Integer index used previously'}
604
- end
605
-
606
- # Insert extended value `value` into struct `result` at `path`
607
- # @param path [String]
608
- # @param value [String]
609
- # @param result [NilClass, Hash, Array]
610
- # @return [Hash, Array]
611
- def dotted_to_extended(path, value, result = nil)
612
- # Typed keys
613
- keys = path.split(OPTION_DOTTED_SEPARATOR).map{ |k| int_or_string(k)}
614
- # Create, or re-used higher level container
615
- current = (result ||= new_hash_or_array_from_key(keys.first))
616
- # walk the path, and create sub-containers if necessary
617
- keys.each_cons(2) do |k, next_k|
618
- array_requires_integer_index!(current, k)
619
- current = (current[k] ||= new_hash_or_array_from_key(next_k))
620
- end
621
- # Assign value at last index
622
- array_requires_integer_index!(current, keys.last)
623
- current[keys.last] = smart_convert(value)
624
- result
625
- end
626
-
627
595
  # generate command line option from option symbol
628
596
  def symbol_to_option(symbol, opt_val = nil)
629
597
  result = [OPTION_PREFIX, symbol.to_s.gsub(OPTION_SEP_SYMBOL, OPTION_SEP_LINE)].join
@@ -677,15 +645,13 @@ module Aspera
677
645
  OPTION_SEP_SYMBOL = '_'
678
646
  # Option value separator on command line, e.g. in --option-blah=foo, the "="
679
647
  OPTION_VALUE_SEPARATOR = '='
680
- # "." : An option like --a.b.c=d does: a={"b":{"c":ext_val(d)}}
681
- OPTION_DOTTED_SEPARATOR = '.'
682
648
  # Starts an option, e.g. in --option-blah, the two first "--"
683
649
  OPTION_PREFIX = '--'
684
650
  # when this is alone, this stops option processing
685
651
  OPTIONS_STOP = '--'
686
652
  SOURCE_USER = 'cmdline' # cspell:disable-line
687
653
 
688
- private_constant :BOOL_YES, :BOOL_NO, :FALSE_VALUES, :TRUE_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :OPTION_VALUE_SEPARATOR, :OPTION_DOTTED_SEPARATOR, :OPTION_PREFIX, :OPTIONS_STOP, :SOURCE_USER
654
+ private_constant :BOOL_YES, :BOOL_NO, :FALSE_VALUES, :TRUE_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :OPTION_VALUE_SEPARATOR, :OPTION_PREFIX, :OPTIONS_STOP, :SOURCE_USER
689
655
  end
690
656
  end
691
657
  end
@@ -62,6 +62,7 @@ module Aspera
62
62
  'Aspera on Cloud'
63
63
  end
64
64
 
65
+ # @return [Hash,NilClass]
65
66
  def detect(base_url)
66
67
  # no protocol ?
67
68
  base_url = "https://#{base_url}" unless base_url.match?(%r{^[a-z]{1,6}://})
@@ -81,8 +82,10 @@ module Aspera
81
82
  }
82
83
  end
83
84
 
84
- # @param base [String] Base folder path
85
- # @return [String] Folder path that does jot exist, with possible .<number> extension
85
+ # Get folder path that does not exist
86
+ # @param base [String] Base folder path
87
+ # @param always [Boolean] `true` always add number, `false` only if base folder already exists
88
+ # @return [String] Folder path that does not exist, with possible .<number> extension
86
89
  def next_available_folder(base, always: false)
87
90
  counter = always ? 1 : 0
88
91
  loop do
@@ -95,27 +98,26 @@ module Aspera
95
98
  # Get folder path that does not exist
96
99
  # If it exists, an extension is added
97
100
  # or a sequential number if extension == :seq
98
- # @param folder [String] base folder
99
- def unique_folder(folder, extension: nil, always: false)
100
- case extension
101
- when nil
102
- folder
103
- when :seq
104
- # reuse helper
105
- next_available_folder(folder, always: always)
106
- else
107
- if Dir.exist?(folder) || always
108
- # NOTE: it might already exist
109
- "#{folder}.#{Environment.instance.sanitized_filename(extension)}"
110
- else
111
- folder
112
- end
101
+ # @param package_info [Hash] Package information
102
+ # @param destination_folder [String] Base folder
103
+ # @param fld. [Array] List of fields of package
104
+ def unique_folder(package_info, destination_folder, fld: nil, seq: false, opt: false)
105
+ Aspera.assert_array_all(fld, String, type: Cli::BadArgument){'fld'}
106
+ Aspera.assert([1, 2].include?(fld.length)){'fld must have 1 or 2 elements'}
107
+ folder = Environment.instance.sanitized_filename(package_info[fld[0]])
108
+ if seq
109
+ folder = next_available_folder(folder, always: !opt)
110
+ elsif fld[1] && (Dir.exist?(folder) || !opt)
111
+ # NOTE: it might already exist
112
+ folder = "#{folder}.#{Environment.instance.sanitized_filename(fld[1])}"
113
113
  end
114
+ puts("sub= #{folder}")
115
+ File.join(destination_folder, folder)
114
116
  end
115
117
  end
116
118
 
117
119
  # @param wizard [Wizard] The wizard object
118
- # @param app_url [Wizard] The wizard object
120
+ # @param app_url [String] Tested URL
119
121
  # @return [Hash] :preset_value, :test_args
120
122
  def wizard(wizard, app_url)
121
123
  pub_link_info = Api::AoC.link_info(app_url)
@@ -174,7 +176,7 @@ module Aspera
174
176
  auto_set_jwt = true
175
177
  Aspera.error_not_implemented
176
178
  # aoc_api.oauth.grant_method = :web
177
- # aoc_api.oauth.scope = Api::AoC::SCOPE_FILES_ADMIN
179
+ # aoc_api.oauth.scope = Api::AoC::Scope::ADMIN
178
180
  # aoc_api.oauth.specific_parameters[:redirect_uri] = REDIRECT_LOCALHOST
179
181
  end
180
182
  myself = aoc_api.read('self')
@@ -205,48 +207,58 @@ module Aspera
205
207
  @cache_workspace_info = nil
206
208
  @cache_home_node_file = nil
207
209
  @cache_api_aoc = nil
210
+ @scope = Api::AoC::Scope::USER
208
211
  options.declare(:workspace, 'Name of workspace', allowed: [String, NilClass], default: Api::AoC::DEFAULT_WORKSPACE)
209
212
  options.declare(:new_user_option, 'New user creation option for unknown package recipients', allowed: Hash)
210
213
  options.declare(:validate_metadata, 'Validate shared inbox metadata', allowed: Allowed::TYPES_BOOLEAN, default: true)
211
- options.declare(:package_folder, 'Field of package to use as folder name, or @none:', allowed: [String, NilClass])
214
+ options.declare(:package_folder, 'Handling of reception of packages in folders', allowed: Hash, default: {})
212
215
  options.parse_options!
213
216
  # add node plugin options (for manual)
214
217
  Node.declare_options(options)
215
218
  end
216
219
 
220
+ # Change API scope for subsequent calls, re-instantiate API object
221
+ # @param new_scope [String] New scope
222
+ def change_api_scope(new_scope)
223
+ @cache_api_aoc = nil
224
+ @scope = new_scope
225
+ end
226
+
227
+ # create an API object with the same options, but with a different subpath
228
+ # @param aoc_base_path [String] New subpath
229
+ # @return [Api::AoC] API object for AoC (is Rest)
217
230
  def api_from_options(aoc_base_path)
218
- # create an API object with the same options, but with a different subpath
219
231
  return new_with_options(
220
232
  Api::AoC,
221
- base: {
233
+ kwargs: {
234
+ scope: @scope,
222
235
  subpath: aoc_base_path,
223
236
  secret_finder: config
224
237
  },
225
- add: {
226
- scope: Api::AoC::SCOPE_FILES_USER,
238
+ option: {
227
239
  workspace: nil
228
240
  }
229
241
  )
230
242
  end
231
243
 
232
244
  # AoC Rest object
233
- # @return [Rest]
245
+ # @return [Api::AoC] API object for AoC (is Rest)
234
246
  def aoc_api
235
247
  if @cache_api_aoc.nil?
236
248
  @cache_api_aoc = api_from_options(Api::AoC::API_V1)
237
- organization = @cache_api_aoc.read('organization')
238
- if organization['http_gateway_enabled'] && organization['http_gateway_server_url']
239
- transfer.httpgw_url_cb = lambda{organization['http_gateway_server_url']}
249
+ transfer.httpgw_url_cb = lambda do
250
+ organization = @cache_api_aoc.read('organization')
240
251
  # @cache_api_aoc.current_user_info['connect_disabled']
252
+ organization['http_gateway_server_url'] if organization['http_gateway_enabled'] && organization['http_gateway_server_url']
241
253
  end
242
254
  end
243
255
  return @cache_api_aoc
244
256
  end
245
257
 
246
258
  # Generate or update Hash with workspace id and name (option), if not already set
247
- # @param hash [Hash, Nil] set in provided hash
248
- # @param string [Bool] true to set key as string, else as symbol
249
- # @param name [Bool] include name
259
+ # @param hash [Hash,nil] Optional base hash (modified)
260
+ # @param string [Boolean] true to set key as string, else as symbol
261
+ # @param name [Boolean] include name
250
262
  # @return [Hash] with key `workspace_[id,name]` (symbol or string) only if defined
251
263
  def workspace_id_hash(hash: nil, string: false, name: false)
252
264
  info = aoc_api.workspace
@@ -319,8 +331,11 @@ module Aspera
319
331
  NODE4_EXT_COMMANDS = %i[transfer].concat(Node::COMMANDS_GEN4).freeze
320
332
  private_constant :NODE4_EXT_COMMANDS
321
333
 
322
- # @param file_id [String] root file id for the operation (can be AK root, or other, e.g. package, or link)
323
- # @param scope [String] node scope, or nil (admin)
334
+ # Execute a node gen4 command
335
+ # @param command_repo [Symbol] command to execute
336
+ # @param node_id [String] Node identifier
337
+ # @param file_id [String] Root file id for the operation (can be AK root, or other, e.g. package, or link). If `nil` use AK root file id.
338
+ # @param scope [String] node scope (Node::SCOPE_<USER|ADMIN>), or nil (requires secret)
324
339
  def execute_nodegen4_command(command_repo, node_id, file_id: nil, scope: nil)
325
340
  top_node_api = aoc_api.node_api_from(
326
341
  node_id: node_id,
@@ -422,7 +437,7 @@ module Aspera
422
437
  when :client_registration_token then default_fields.push('value', 'data.client_subject_scopes', 'created_at')
423
438
  when :contact
424
439
  default_fields = %w[source_type source_id name email]
425
- default_query = {'include_only_user_personal_contacts' => true} if aoc_api.oauth.scope == Api::AoC::SCOPE_FILES_USER
440
+ default_query = {'include_only_user_personal_contacts' => true} if @scope == Api::AoC::Scope::USER
426
441
  when :node then default_fields.push('name', 'host', 'access_key')
427
442
  when :operation then default_fields = nil
428
443
  when :short_link then default_fields.push('short_url', 'data.url_token_data.purpose')
@@ -455,7 +470,7 @@ module Aspera
455
470
  return Main.result_success
456
471
  when :do
457
472
  command_repo = options.get_next_command(NODE4_EXT_COMMANDS)
458
- return execute_nodegen4_command(command_repo, res_id)
473
+ return execute_nodegen4_command(command_repo, res_id, scope: Api::Node::SCOPE_ADMIN)
459
474
  when :bearer_token
460
475
  node_api = aoc_api.node_api_from(
461
476
  node_id: res_id,
@@ -514,13 +529,15 @@ module Aspera
514
529
  end
515
530
  end
516
531
 
517
- ADMIN_ACTIONS = %i[ats resource usage_reports analytics subscription auth_providers].concat(ADMIN_OBJECTS).freeze
532
+ ADMIN_ACTIONS = %i[ats bearer_token resource usage_reports analytics subscription auth_providers].concat(ADMIN_OBJECTS).freeze
518
533
 
519
534
  def execute_admin_action
520
- # default scope to admin
521
- aoc_api.oauth.scope = Api::AoC::SCOPE_FILES_ADMIN if options.get_option(:scope).nil?
535
+ # change scope to admin
536
+ change_api_scope(Api::AoC::Scope::ADMIN)
522
537
  command_admin = options.get_next_command(ADMIN_ACTIONS)
523
538
  case command_admin
539
+ when :bearer_token
540
+ return Main.result_text(aoc_api.oauth.authorization)
524
541
  when :resource
525
542
  Log.log.warn('resource command is deprecated (4.18), directly use the specific command instead')
526
543
  return execute_resource_action(options.get_next_argument('resource', accept_list: ADMIN_OBJECTS))
@@ -649,13 +666,13 @@ module Aspera
649
666
  when :ats
650
667
  ats_api = Rest.new(**aoc_api.params.deep_merge({
651
668
  base_url: "#{aoc_api.base_url}/admin/ats/pub/v1",
652
- auth: {scope: Api::AoC::SCOPE_FILES_ADMIN_USER}
669
+ auth: {params: {scope: Api::AoC::Scope::ADMIN_USER}}
653
670
  }))
654
- return Ats.new(context: context).execute_action_gen(ats_api)
671
+ return Ats.new(context: context, api: ats_api).execute_action
655
672
  when :analytics
656
673
  analytics_api = Rest.new(**aoc_api.params.deep_merge({
657
674
  base_url: "#{aoc_api.base_url.gsub('/api/v1', '')}/analytics/v2",
658
- auth: {scope: Api::AoC::SCOPE_FILES_ADMIN_USER}
675
+ auth: {params: {scope: Api::AoC::Scope::ADMIN_USER}}
659
676
  }))
660
677
  command_analytics = options.get_next_command(%i[application_events transfers files])
661
678
  case command_analytics
@@ -680,13 +697,13 @@ module Aspera
680
697
  start_date_persistency = PersistencyActionOnce.new(
681
698
  manager: persistency,
682
699
  data: saved_date,
683
- id: IdGenerator.from_list([
700
+ id: IdGenerator.from_list(
684
701
  'aoc_ana_date',
685
702
  options.get_option(:url, mandatory: true),
686
703
  aoc_api.workspace[:name],
687
704
  event_resource_type.to_s,
688
705
  event_resource_id
689
- ])
706
+ )
690
707
  )
691
708
  start_date_time = saved_date.first
692
709
  stop_date_time = Time.now.utc.strftime('%FT%T.%LZ')
@@ -839,9 +856,10 @@ module Aspera
839
856
  manager: persistency,
840
857
  data: [],
841
858
  id: IdGenerator.from_list(
842
- ['aoc_recv',
843
- options.get_option(:url, mandatory: true),
844
- aoc_api.workspace[:id]].concat(aoc_api.additional_persistence_ids)
859
+ 'aoc_recv',
860
+ options.get_option(:url, mandatory: true),
861
+ aoc_api.workspace[:id],
862
+ aoc_api.additional_persistence_ids
845
863
  )
846
864
  )
847
865
  end
@@ -977,13 +995,8 @@ module Aspera
977
995
  end
978
996
  # download all files, or specified list only
979
997
  ts_paths = transfer.ts_source_paths(default: ['.'])
980
- per_package_def = options.get_option(:package_folder)
981
- unless per_package_def.nil?
982
- raise Cli::BadArgument, "Invalid package folder option : #{per_package_def}" unless per_package_def =~ /\A([^+]+)(?:\+([^?]+)(\?)?)?\z/
983
- per_package_field1 = Regexp.last_match(1)
984
- per_package_field2 = Regexp.last_match(2)
985
- per_package_sub_always = Regexp.last_match(3).nil?
986
- end
998
+ per_package_def = options.get_option(:package_folder).symbolize_keys
999
+ save_metadata = per_package_def.delete(:inf)
987
1000
  # get value outside of loop
988
1001
  destination_folder = transfer.destination_folder(Transfer::Spec::DIRECTION_RECEIVE)
989
1002
  result_transfer = []
@@ -999,23 +1012,14 @@ module Aspera
999
1012
  Transfer::Spec::DIRECTION_RECEIVE,
1000
1013
  {'paths'=> ts_paths}
1001
1014
  )
1002
- unless per_package_def.nil?
1003
- # folder based on first field
1004
- folder = File.join(
1005
- destination_folder,
1006
- Environment.instance.sanitized_filename(package_info[per_package_field1])
1007
- )
1008
- transfer.user_transfer_spec['destination_root'] = self.class.unique_folder(
1009
- folder,
1010
- extension: per_package_field2.eql?('seq') ? :seq : package_info[per_package_field2],
1011
- always: per_package_sub_always
1012
- )
1013
- end
1014
- formatter.display_status(%Q{Downloading package: [#{package_info['id']}] "#{package_info['name']}" to [#{destination_folder}]})
1015
+ transfer.user_transfer_spec['destination_root'] = self.class.unique_folder(package_info, destination_folder, **per_package_def) unless per_package_def.empty?
1016
+ dest_folder = transfer.user_transfer_spec['destination_root'] || destination_folder
1017
+ formatter.display_status(%Q{Downloading package: [#{package_info['id']}] "#{package_info['name']}" to [#{dest_folder}]})
1015
1018
  statuses = transfer.start(
1016
1019
  transfer_spec,
1017
1020
  rest_token: package_node_api
1018
1021
  )
1022
+ File.write(File.join(dest_folder, "#{package_id}.info.json"), package_info.to_json) if save_metadata
1019
1023
  result_transfer.push({'package' => package_id, Main::STATUS_FIELD => statuses})
1020
1024
  # update skip list only if all transfer sessions completed
1021
1025
  if skip_ids_persistency && TransferAgent.session_status(statuses).eql?(:success)
@@ -1092,6 +1096,7 @@ module Aspera
1092
1096
  end
1093
1097
  end
1094
1098
  when :automation
1099
+ change_api_scope(Api::AoC::Scope::ADMIN_USER)
1095
1100
  Log.log.warn('BETA: work under progress')
1096
1101
  # automation api is not in the same place
1097
1102
  automation_api = Rest.new(**aoc_api.params, base_url: aoc_api.base_url.gsub('/api/', '/automation/'))
@@ -18,10 +18,10 @@ module Aspera
18
18
  # columns for list of cloud providers
19
19
  CLOUD_TABLE = %w[id name].freeze
20
20
  private_constant :CLOUD_TABLE
21
- def initialize(**_)
22
- super
23
- @ats_api_pub = nil
24
- @ats_api_pub_v1_cache = nil
21
+ def initialize(api: nil, **base_args)
22
+ super(**base_args)
23
+ @ats_api_open = Api::Ats.new
24
+ @ats_api_auth = api
25
25
  options.declare(:ibm_api_key, 'IBM API key, see https://cloud.ibm.com/iam/apikeys')
26
26
  options.declare(:instance, 'ATS instance in ibm cloud')
27
27
  options.declare(:ats_key, 'ATS key identifier (ats_xxx)')
@@ -36,13 +36,13 @@ module Aspera
36
36
  # TODO: provide list ?
37
37
  cloud = options.get_option(:cloud, mandatory: true).upcase
38
38
  region = options.get_option(:region, mandatory: true)
39
- return @ats_api_pub.read("servers/#{cloud}/#{region}")
39
+ return @ats_api_open.read("servers/#{cloud}/#{region}")
40
40
  end
41
41
 
42
42
  # require api key only if needed
43
- def ats_api_pub_v1
44
- return @ats_api_pub_v1_cache unless @ats_api_pub_v1_cache.nil?
45
- @ats_api_pub_v1_cache = Rest.new(
43
+ def ats_api
44
+ return @ats_api_auth unless @ats_api_auth.nil?
45
+ @ats_api_auth = Rest.new(
46
46
  base_url: "#{Api::Ats::SERVICE_BASE_URL}/pub/v1",
47
47
  auth: {
48
48
  type: :basic,
@@ -73,10 +73,10 @@ module Aspera
73
73
  when 'ibm-s3'
74
74
  server_data2 = nil
75
75
  if server_data.nil?
76
- server_data2 = @ats_api_pub.all_servers.find{ |s| s['id'].eql?(params['transfer_server_id'])}
76
+ server_data2 = @ats_api_open.all_servers.find{ |s| s['id'].eql?(params['transfer_server_id'])}
77
77
  raise "no such transfer server id: #{params['transfer_server_id']}" if server_data2.nil?
78
78
  else
79
- server_data2 = @ats_api_pub.all_servers.find do |s|
79
+ server_data2 = @ats_api_open.all_servers.find do |s|
80
80
  s['cloud'].eql?(server_data['cloud']) &&
81
81
  s['region'].eql?(server_data['region']) &&
82
82
  s.key?('s3_authentication_endpoint')
@@ -88,31 +88,31 @@ module Aspera
88
88
  params['storage']['endpoint'] = server_data2['s3_authentication_endpoint'] if !params['storage'].key?('authentication_endpoint')
89
89
  end
90
90
  end
91
- res = ats_api_pub_v1.create('access_keys', params)
91
+ res = ats_api.create('access_keys', params)
92
92
  return Main.result_single_object(res)
93
93
  # TODO : action : modify, with "PUT"
94
94
  when :list
95
95
  params = query_read_delete(default: {'offset' => 0, 'max_results' => 1000})
96
- res = ats_api_pub_v1.read('access_keys', params)
96
+ res = ats_api.read('access_keys', params)
97
97
  return Main.result_object_list(res['data'], fields: ['name', 'id', 'created.at', 'modified.at'])
98
98
  when :show
99
- res = ats_api_pub_v1.read("access_keys/#{access_key_id}")
99
+ res = ats_api.read("access_keys/#{access_key_id}")
100
100
  return Main.result_single_object(res)
101
101
  when :modify
102
102
  params = value_create_modify(command: command)
103
103
  params['id'] = access_key_id
104
- ats_api_pub_v1.update("access_keys/#{access_key_id}", params)
104
+ ats_api.update("access_keys/#{access_key_id}", params)
105
105
  return Main.result_status('modified')
106
106
  when :entitlement
107
- ak = ats_api_pub_v1.read("access_keys/#{access_key_id}")
107
+ ak = ats_api.read("access_keys/#{access_key_id}")
108
108
  api_bss = Api::Alee.new(ak['license']['entitlement_id'], ak['license']['customer_id'])
109
109
  return Main.result_single_object(api_bss.read('entitlement'))
110
110
  when :delete
111
- ats_api_pub_v1.delete("access_keys/#{access_key_id}")
111
+ ats_api.delete("access_keys/#{access_key_id}")
112
112
  return Main.result_status("deleted #{access_key_id}")
113
113
  when :node
114
- ak_data = ats_api_pub_v1.read("access_keys/#{access_key_id}")
115
- server_data = @ats_api_pub.all_servers.find{ |i| i['id'].start_with?(ak_data['transfer_server_id'])}
114
+ ak_data = ats_api.read("access_keys/#{access_key_id}")
115
+ server_data = @ats_api_open.all_servers.find{ |i| i['id'].start_with?(ak_data['transfer_server_id'])}
116
116
  raise Cli::Error, 'no such server found' if server_data.nil?
117
117
  node_url = server_data['transfer_setup_url']
118
118
  api_node = Api::Node.new(
@@ -126,7 +126,7 @@ module Aspera
126
126
  command = options.get_next_command(Node::COMMANDS_GEN4)
127
127
  return Node.new(context: context, api: api_node).execute_command_gen4(command, ak_data['root_file_id'])
128
128
  when :cluster
129
- ats_url = ats_api_pub_v1.base_url
129
+ ats_url = ats_api.base_url
130
130
  api_ak_auth = Rest.new(
131
131
  base_url: ats_url,
132
132
  auth: {
@@ -140,19 +140,19 @@ module Aspera
140
140
  end
141
141
  end
142
142
 
143
- def execute_action_cluster_pub
143
+ def execute_action_cluster_open
144
144
  command = options.get_next_command(%i[clouds list show])
145
145
  case command
146
146
  when :clouds
147
- return Main.result_object_list(@ats_api_pub.cloud_names.map{ |k, v| CLOUD_TABLE.zip([k, v]).to_h})
147
+ return Main.result_object_list(@ats_api_open.cloud_names.map{ |k, v| CLOUD_TABLE.zip([k, v]).to_h})
148
148
  when :list
149
- return Main.result_object_list(@ats_api_pub.all_servers, fields: %w[id cloud region])
149
+ return Main.result_object_list(@ats_api_open.all_servers, fields: %w[id cloud region])
150
150
  when :show
151
151
  if options.get_option(:cloud) || options.get_option(:region)
152
152
  server_data = server_by_cloud_region
153
153
  else
154
154
  server_id = instance_identifier
155
- server_data = @ats_api_pub.all_servers.find{ |i| i['id'].eql?(server_id)}
155
+ server_data = @ats_api_open.all_servers.find{ |i| i['id'].eql?(server_id)}
156
156
  raise BadIdentifier.new('server', server_id) if server_data.nil?
157
157
  end
158
158
  return Main.result_single_object(server_data)
@@ -170,7 +170,9 @@ module Aspera
170
170
  # does not work: base_url: 'https://iam.cloud.ibm.com/identity',
171
171
  grant_type: 'urn:ibm:params:oauth:grant-type:apikey',
172
172
  response_type: 'cloud_iam',
173
- apikey: options.get_option(:ibm_api_key, mandatory: true)
173
+ params: {
174
+ apikey: options.get_option(:ibm_api_key, mandatory: true)
175
+ }
174
176
  }
175
177
  )
176
178
  end
@@ -213,31 +215,23 @@ module Aspera
213
215
  ACTIONS = %i[cluster access_key api_key aws_trust_policy].freeze
214
216
 
215
217
  # called for legacy and AoC
216
- def execute_action_gen(ats_api_arg)
218
+ def execute_action
217
219
  actions = ACTIONS.dup
218
- actions.delete(:api_key) unless ats_api_arg.nil?
220
+ actions.delete(:api_key) unless @ats_api_auth.nil?
219
221
  command = options.get_next_command(actions)
220
- @ats_api_pub_v1_cache = ats_api_arg
221
- # keep as member variable as we may want to use the api in AoC name space
222
- @ats_api_pub = Api::Ats.new
223
222
  case command
224
223
  when :cluster # display general ATS cluster information, this uses public API, no auth
225
- return execute_action_cluster_pub
224
+ return execute_action_cluster_open
226
225
  when :access_key
227
226
  return execute_action_access_key
228
227
  when :api_key # manage credential to access ATS API
229
228
  return execute_action_api_key
230
229
  when :aws_trust_policy
231
- res = ats_api_pub_v1.read('aws/trustpolicy', {region: options.get_option(:region, mandatory: true)})
230
+ res = ats_api.read('aws/trustpolicy', {region: options.get_option(:region, mandatory: true)})
232
231
  return Main.result_single_object(res)
233
232
  else Aspera.error_unexpected_value(command)
234
233
  end
235
234
  end
236
-
237
- # called for legacy ATS only
238
- def execute_action
239
- execute_action_gen(nil)
240
- end
241
235
  end
242
236
  end
243
237
  end
@@ -48,10 +48,15 @@ module Aspera
48
48
  # Global objects
49
49
  attr_reader :context
50
50
 
51
+ # @return [Manager]
51
52
  def options; @context.options; end
53
+ # @return [TransferAgent]
52
54
  def transfer; @context.transfer; end
55
+ # @return [Config]
53
56
  def config; @context.config; end
57
+ # @return [Formatter]
54
58
  def formatter; @context.formatter; end
59
+ # @return [PersistencyFolder]
55
60
  def persistency; @context.persistency; end
56
61
 
57
62
  def add_manual_header(has_options = true)
@@ -131,12 +136,12 @@ module Aspera
131
136
  # @param command [Symbol] command to execute: create show list modify delete
132
137
  # @param display_fields [Array] fields to display by default
133
138
  # @param items_key [String] result is in a sub key of the json
134
- # @param delete_style [String] if set, the delete operation by array in payload
135
- # @param id_as_arg [String] if set, the id is provided as url argument ?<id_as_arg>=<id>
136
- # @param is_singleton [Boolean] if true, entity is the full path to the resource
137
- # @param tclo [Bool] if set, :list use paging with total_count, limit, offset
138
- # @param block [Proc] block to search for identifier based on attribute value
139
- # @return result suitable for CLI result
139
+ # @param delete_style [String] If set, the delete operation by array in payload
140
+ # @param id_as_arg [String] If set, the id is provided as url argument ?<id_as_arg>=<id>
141
+ # @param is_singleton [Boolean] If `true`, entity is the full path to the resource
142
+ # @param tclo [Boolean] If `true`, :list use paging with total_count, limit, offset
143
+ # @param block [Proc] Block to search for identifier based on attribute value
144
+ # @return [Hash] Result suitable for CLI result
140
145
  def entity_execute(
141
146
  api:,
142
147
  entity:,