aspera-cli 4.24.2 → 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 (81) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +1070 -758
  4. data/CONTRIBUTING.md +130 -115
  5. data/README.md +961 -623
  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 +104 -67
  10. data/lib/aspera/api/ats.rb +1 -0
  11. data/lib/aspera/api/cos_node.rb +3 -2
  12. data/lib/aspera/api/faspex.rb +17 -10
  13. data/lib/aspera/api/node.rb +10 -12
  14. data/lib/aspera/ascmd.rb +2 -3
  15. data/lib/aspera/ascp/installation.rb +60 -46
  16. data/lib/aspera/ascp/management.rb +9 -5
  17. data/lib/aspera/assert.rb +28 -6
  18. data/lib/aspera/cli/error.rb +4 -2
  19. data/lib/aspera/cli/extended_value.rb +94 -62
  20. data/lib/aspera/cli/formatter.rb +44 -58
  21. data/lib/aspera/cli/main.rb +21 -14
  22. data/lib/aspera/cli/manager.rb +317 -250
  23. data/lib/aspera/cli/plugins/alee.rb +3 -3
  24. data/lib/aspera/cli/plugins/aoc.rb +139 -78
  25. data/lib/aspera/cli/plugins/ats.rb +30 -36
  26. data/lib/aspera/cli/plugins/base.rb +68 -55
  27. data/lib/aspera/cli/plugins/config.rb +90 -100
  28. data/lib/aspera/cli/plugins/console.rb +15 -9
  29. data/lib/aspera/cli/plugins/cos.rb +1 -1
  30. data/lib/aspera/cli/plugins/faspex.rb +39 -30
  31. data/lib/aspera/cli/plugins/faspex5.rb +57 -52
  32. data/lib/aspera/cli/plugins/faspio.rb +10 -7
  33. data/lib/aspera/cli/plugins/httpgw.rb +3 -2
  34. data/lib/aspera/cli/plugins/node.rb +140 -125
  35. data/lib/aspera/cli/plugins/oauth.rb +13 -12
  36. data/lib/aspera/cli/plugins/orchestrator.rb +116 -33
  37. data/lib/aspera/cli/plugins/preview.rb +28 -48
  38. data/lib/aspera/cli/plugins/server.rb +9 -10
  39. data/lib/aspera/cli/plugins/shares.rb +77 -43
  40. data/lib/aspera/cli/sync_actions.rb +49 -38
  41. data/lib/aspera/cli/transfer_agent.rb +16 -35
  42. data/lib/aspera/cli/version.rb +1 -1
  43. data/lib/aspera/cli/wizard.rb +8 -5
  44. data/lib/aspera/command_line_builder.rb +24 -21
  45. data/lib/aspera/coverage.rb +6 -2
  46. data/lib/aspera/dot_container.rb +108 -0
  47. data/lib/aspera/environment.rb +71 -84
  48. data/lib/aspera/faspex_gw.rb +1 -1
  49. data/lib/aspera/faspex_postproc.rb +1 -1
  50. data/lib/aspera/id_generator.rb +7 -10
  51. data/lib/aspera/keychain/factory.rb +1 -2
  52. data/lib/aspera/keychain/macos_security.rb +2 -2
  53. data/lib/aspera/log.rb +2 -1
  54. data/lib/aspera/markdown.rb +31 -0
  55. data/lib/aspera/nagios.rb +6 -5
  56. data/lib/aspera/oauth/base.rb +41 -64
  57. data/lib/aspera/oauth/factory.rb +6 -7
  58. data/lib/aspera/oauth/generic.rb +1 -1
  59. data/lib/aspera/oauth/jwt.rb +1 -1
  60. data/lib/aspera/oauth/url_json.rb +6 -4
  61. data/lib/aspera/oauth/web.rb +2 -2
  62. data/lib/aspera/preview/file_types.rb +24 -38
  63. data/lib/aspera/preview/terminal.rb +95 -29
  64. data/lib/aspera/preview/utils.rb +6 -5
  65. data/lib/aspera/products/connect.rb +3 -3
  66. data/lib/aspera/rest.rb +54 -39
  67. data/lib/aspera/rest_error_analyzer.rb +4 -4
  68. data/lib/aspera/ssh.rb +10 -6
  69. data/lib/aspera/ssl.rb +41 -0
  70. data/lib/aspera/sync/conf.schema.yaml +184 -36
  71. data/lib/aspera/sync/database.rb +2 -1
  72. data/lib/aspera/sync/operations.rb +128 -72
  73. data/lib/aspera/transfer/parameters.rb +9 -10
  74. data/lib/aspera/transfer/spec.rb +2 -3
  75. data/lib/aspera/transfer/spec.schema.yaml +52 -22
  76. data/lib/aspera/transfer/spec_doc.rb +20 -30
  77. data/lib/aspera/uri_reader.rb +18 -4
  78. data/lib/transferd_pb.rb +2 -2
  79. data.tar.gz.sig +0 -0
  80. metadata +34 -6
  81. metadata.gz.sig +0 -0
@@ -17,13 +17,13 @@ module Aspera
17
17
  nagios = Nagios.new
18
18
  begin
19
19
  api = Api::Alee.new(nil, nil, version: 'ping')
20
- result = api.call(operation: 'GET')
21
- raise "unexpected response: #{result[:http].body}" unless result[:http].body.eql?('pong')
20
+ http = api.read(nil, ret: :resp)
21
+ raise "unexpected response: #{http.body}" unless http.body.eql?('pong')
22
22
  nagios.add_ok('api', 'answered ok')
23
23
  rescue StandardError => e
24
24
  nagios.add_critical('api', e.to_s)
25
25
  end
26
- return nagios.result
26
+ Main.result_object_list(nagios.status_list)
27
27
  when :entitlement
28
28
  entitlement_id = options.get_option(:username, mandatory: true)
29
29
  customer_id = options.get_option(:password, mandatory: true)
@@ -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}://})
@@ -69,9 +70,9 @@ module Aspera
69
70
  base_url = "#{base_url}.#{Api::AoC::SAAS_DOMAIN_PROD}" unless base_url.include?('.')
70
71
  # AoC is only https
71
72
  return unless base_url.start_with?('https://')
72
- res_http = Rest.new(base_url: base_url, redirect_max: 0).call(operation: 'GET', subpath: 'auth/ping', exception: false)[:http]
73
- return if res_http['Location'].nil?
74
- redirect_uri = URI.parse(res_http['Location'])
73
+ location = Rest.new(base_url: base_url, redirect_max: 0).call(operation: 'GET', subpath: 'auth/ping', exception: false, ret: :resp)['Location']
74
+ return if location.nil?
75
+ redirect_uri = URI.parse(location)
75
76
  od = Api::AoC.split_org_domain(URI.parse(base_url))
76
77
  return unless redirect_uri.path.end_with?("oauth2/#{od[:organization]}/login")
77
78
  # either in standard domain, or product name in page
@@ -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)
@@ -132,7 +134,7 @@ module Aspera
132
134
  test_args: 'organization'
133
135
  }
134
136
  end
135
- options.declare(:use_generic_client, 'Wizard: AoC: use global or org specific jwt client id', values: :bool, default: Api::AoC.saas_url?(app_url))
137
+ options.declare(:use_generic_client, 'Wizard: AoC: use global or org specific jwt client id', allowed: Allowed::TYPES_BOOLEAN, default: Api::AoC.saas_url?(app_url))
136
138
  options.parse_options!
137
139
  # make username mandatory for jwt, this triggers interactive input
138
140
  wiz_username = options.get_option(:username, mandatory: true)
@@ -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
208
- options.declare(:workspace, 'Name of workspace', types: [String, NilClass], default: Api::AoC::DEFAULT_WORKSPACE)
209
- options.declare(:new_user_option, 'New user creation option for unknown package recipients', types: Hash)
210
- options.declare(:validate_metadata, 'Validate shared inbox metadata', values: :bool, default: true)
211
- options.declare(:package_folder, 'Field of package to use as folder name, or @none:', types: [String, NilClass])
210
+ @scope = Api::AoC::Scope::USER
211
+ options.declare(:workspace, 'Name of workspace', allowed: [String, NilClass], default: Api::AoC::DEFAULT_WORKSPACE)
212
+ options.declare(:new_user_option, 'New user creation option for unknown package recipients', allowed: Hash)
213
+ options.declare(:validate_metadata, 'Validate shared inbox metadata', allowed: Allowed::TYPES_BOOLEAN, default: true)
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
@@ -288,7 +300,7 @@ module Aspera
288
300
  query = query_read_delete(default: default_query)
289
301
  # caller may add specific modifications or checks to query
290
302
  yield(query) if block_given?
291
- result = aoc_api.read_with_paging(resource_class_path, query: base_query.merge(query).compact, formatter: formatter)
303
+ result = aoc_api.read_with_paging(resource_class_path, base_query.merge(query).compact, formatter: formatter)
292
304
  return Main.result_object_list(result[:items], fields: fields, total: result[:total])
293
305
  end
294
306
 
@@ -313,14 +325,17 @@ module Aspera
313
325
  Aspera.assert_type(query, Hash){'query'}
314
326
  PACKAGE_RECEIVED_BASE_QUERY.each{ |k, v| query[k] = v unless query.key?(k)}
315
327
  resolve_dropbox_name_default_ws_id(query)
316
- return aoc_api.read_with_paging('packages', query: query.compact, formatter: formatter)
328
+ return aoc_api.read_with_paging('packages', query.compact, formatter: formatter)
317
329
  end
318
330
 
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,
@@ -374,6 +389,7 @@ module Aspera
374
389
  Aspera.error_unreachable_line
375
390
  end
376
391
 
392
+ # @param resource_type [Symbol] One of ADMIN_OBJECTS
377
393
  def execute_resource_action(resource_type)
378
394
  # get path on API, resource type is singular, but api is plural
379
395
  resource_class_path =
@@ -391,8 +407,9 @@ module Aspera
391
407
  global_operations = %i[create list]
392
408
  supported_operations = %i[show modify]
393
409
  supported_operations.push(:delete, *global_operations) unless singleton_object
394
- supported_operations.push(:do) if resource_type.eql?(:node)
410
+ supported_operations.push(:do, :bearer_token) if resource_type.eql?(:node)
395
411
  supported_operations.push(:set_pub_key) if resource_type.eql?(:client)
412
+ supported_operations.push(:shared_folder, :dropbox) if resource_type.eql?(:workspace)
396
413
  command = options.get_next_command(supported_operations)
397
414
  # require identifier for non global commands
398
415
  if !singleton_object && !global_operations.include?(command)
@@ -420,7 +437,7 @@ module Aspera
420
437
  when :client_registration_token then default_fields.push('value', 'data.client_subject_scopes', 'created_at')
421
438
  when :contact
422
439
  default_fields = %w[source_type source_id name email]
423
- 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
424
441
  when :node then default_fields.push('name', 'host', 'access_key')
425
442
  when :operation then default_fields = nil
426
443
  when :short_link then default_fields.push('short_url', 'data.url_token_data.purpose')
@@ -453,18 +470,74 @@ module Aspera
453
470
  return Main.result_success
454
471
  when :do
455
472
  command_repo = options.get_next_command(NODE4_EXT_COMMANDS)
456
- return execute_nodegen4_command(command_repo, res_id)
473
+ return execute_nodegen4_command(command_repo, res_id, scope: Api::Node::SCOPE_ADMIN)
474
+ when :bearer_token
475
+ node_api = aoc_api.node_api_from(
476
+ node_id: res_id,
477
+ scope: options.get_next_argument('scope')
478
+ )
479
+ return Main.result_text(node_api.oauth.authorization)
480
+ when :dropbox
481
+ command_shared = options.get_next_command(%i[list])
482
+ case command_shared
483
+ when :list
484
+ query = options.get_option(:query) || {}
485
+ res_data = aoc_api.read('dropboxes', query.merge({'workspace_id'=>res_id}))
486
+ return Main.result_object_list(res_data, fields: %w[id name description])
487
+ end
488
+ when :shared_folder
489
+ query = options.get_option(:query) || Api::AoC.workspace_access(res_id).merge({'admin' => true})
490
+ shared_folders = aoc_api.read_with_paging("#{resource_instance_path}/permissions", query)[:items]
491
+ # inside a workspace
492
+ command_shared = options.get_next_command(%i[list member])
493
+ case command_shared
494
+ when :list
495
+ return Main.result_object_list(shared_folders, fields: %w[id node_name node_id file_id file.path tags.aspera.files.workspace.share_as])
496
+ when :member
497
+ shared_folder_id = instance_identifier
498
+ shared_folder = shared_folders.find{ |i| i['id'].eql?(shared_folder_id)}
499
+ Aspera.assert(shared_folder)
500
+ command_shared_member = options.get_next_command(%i[list])
501
+ case command_shared_member
502
+ when :list
503
+ node_api = aoc_api.node_api_from(
504
+ node_id: shared_folder['node_id'],
505
+ workspace_id: res_id,
506
+ workspace_name: nil,
507
+ scope: Api::Node::SCOPE_USER
508
+ )
509
+ result = node_api.read(
510
+ 'permissions',
511
+ {'file_id' => shared_folder['file_id'], 'tag' => "aspera.files.workspace.id=#{res_id}"}
512
+ )
513
+ result.each do |item|
514
+ item['member'] = begin
515
+ if Api::AoC.workspace_access?(item)
516
+ {'name'=>'[Internal permission]'}
517
+ else
518
+ aoc_api.read("admin/#{item['access_type']}s/#{item['access_id']}") rescue {'name': 'not found'}
519
+ end
520
+ rescue => e
521
+ {'name'=>e.to_s}
522
+ end
523
+ end
524
+ # TODO : read users and group name and add, if query "include_members"
525
+ return Main.result_object_list(result, fields: %w[access_type access_id access_level last_updated_at member.name member.email member.system_group_type member.system_group])
526
+ end
527
+ end
457
528
  else Aspera.error_unexpected_value(command)
458
529
  end
459
530
  end
460
531
 
461
- 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
462
533
 
463
534
  def execute_admin_action
464
- # default scope to admin
465
- 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)
466
537
  command_admin = options.get_next_command(ADMIN_ACTIONS)
467
538
  case command_admin
539
+ when :bearer_token
540
+ return Main.result_text(aoc_api.oauth.authorization)
468
541
  when :resource
469
542
  Log.log.warn('resource command is deprecated (4.18), directly use the specific command instead')
470
543
  return execute_resource_action(options.get_next_argument('resource', accept_list: ADMIN_OBJECTS))
@@ -593,13 +666,13 @@ module Aspera
593
666
  when :ats
594
667
  ats_api = Rest.new(**aoc_api.params.deep_merge({
595
668
  base_url: "#{aoc_api.base_url}/admin/ats/pub/v1",
596
- auth: {scope: Api::AoC::SCOPE_FILES_ADMIN_USER}
669
+ auth: {params: {scope: Api::AoC::Scope::ADMIN_USER}}
597
670
  }))
598
- return Ats.new(context: context).execute_action_gen(ats_api)
671
+ return Ats.new(context: context, api: ats_api).execute_action
599
672
  when :analytics
600
673
  analytics_api = Rest.new(**aoc_api.params.deep_merge({
601
674
  base_url: "#{aoc_api.base_url.gsub('/api/v1', '')}/analytics/v2",
602
- auth: {scope: Api::AoC::SCOPE_FILES_ADMIN_USER}
675
+ auth: {params: {scope: Api::AoC::Scope::ADMIN_USER}}
603
676
  }))
604
677
  command_analytics = options.get_next_command(%i[application_events transfers files])
605
678
  case command_analytics
@@ -624,13 +697,13 @@ module Aspera
624
697
  start_date_persistency = PersistencyActionOnce.new(
625
698
  manager: persistency,
626
699
  data: saved_date,
627
- id: IdGenerator.from_list([
700
+ id: IdGenerator.from_list(
628
701
  'aoc_ana_date',
629
702
  options.get_option(:url, mandatory: true),
630
703
  aoc_api.workspace[:name],
631
704
  event_resource_type.to_s,
632
705
  event_resource_id
633
- ])
706
+ )
634
707
  )
635
708
  start_date_time = saved_date.first
636
709
  stop_date_time = Time.now.utc.strftime('%FT%T.%LZ')
@@ -736,7 +809,7 @@ module Aspera
736
809
  }
737
810
  return result_list('short_links', fields: Formatter.all_but('data'), base_query: list_params) if command.eql?(:list)
738
811
  one_id = instance_identifier
739
- found = aoc_api.read_with_paging('short_links', query: list_params, formatter: formatter)[:items].find{ |item| item['id'].eql?(one_id)}
812
+ found = aoc_api.read_with_paging('short_links', list_params, formatter: formatter)[:items].find{ |item| item['id'].eql?(one_id)}
740
813
  raise Cli::BadIdentifier.new('Short link', one_id) if found.nil?
741
814
  return Main.result_single_object(found, fields: Formatter.all_but('data'))
742
815
  when :modify
@@ -783,9 +856,10 @@ module Aspera
783
856
  manager: persistency,
784
857
  data: [],
785
858
  id: IdGenerator.from_list(
786
- ['aoc_recv',
787
- options.get_option(:url, mandatory: true),
788
- 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
789
863
  )
790
864
  )
791
865
  end
@@ -886,7 +960,7 @@ module Aspera
886
960
  # enforce workspace id from link (should be already ok, but in case user wanted to override)
887
961
  package_data['workspace_id'] = aoc_api.public_link['data']['workspace_id']
888
962
  end
889
- package_data['encryption_at_rest'] = true if transfer.option_transfer_spec['content_protection'].eql?('encrypt')
963
+ package_data['encryption_at_rest'] = true if transfer.user_transfer_spec['content_protection'].eql?('encrypt')
890
964
  # transfer may raise an error
891
965
  created_package = aoc_api.create_package_simple(package_data, option_validate, new_user_option)
892
966
  Main.result_transfer(transfer.start(created_package[:spec], rest_token: created_package[:node]))
@@ -921,13 +995,8 @@ module Aspera
921
995
  end
922
996
  # download all files, or specified list only
923
997
  ts_paths = transfer.ts_source_paths(default: ['.'])
924
- per_package_def = options.get_option(:package_folder)
925
- unless per_package_def.nil?
926
- raise Cli::BadArgument, "Invalid package folder option : #{per_package_def}" unless per_package_def =~ /\A([^+]+)(?:\+([^?]+)(\?)?)?\z/
927
- per_package_field1 = Regexp.last_match(1)
928
- per_package_field2 = Regexp.last_match(2)
929
- per_package_sub_always = Regexp.last_match(3).nil?
930
- end
998
+ per_package_def = options.get_option(:package_folder).symbolize_keys
999
+ save_metadata = per_package_def.delete(:inf)
931
1000
  # get value outside of loop
932
1001
  destination_folder = transfer.destination_folder(Transfer::Spec::DIRECTION_RECEIVE)
933
1002
  result_transfer = []
@@ -943,23 +1012,14 @@ module Aspera
943
1012
  Transfer::Spec::DIRECTION_RECEIVE,
944
1013
  {'paths'=> ts_paths}
945
1014
  )
946
- unless per_package_def.nil?
947
- # folder based on first field
948
- folder = File.join(
949
- destination_folder,
950
- Environment.instance.sanitized_filename(package_info[per_package_field1])
951
- )
952
- transfer.option_transfer_spec['destination_root'] = self.class.unique_folder(
953
- folder,
954
- extension: per_package_field2.eql?('seq') ? :seq : package_info[per_package_field2],
955
- always: per_package_sub_always
956
- )
957
- end
958
- 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}]})
959
1018
  statuses = transfer.start(
960
1019
  transfer_spec,
961
1020
  rest_token: package_node_api
962
1021
  )
1022
+ File.write(File.join(dest_folder, "#{package_id}.info.json"), package_info.to_json) if save_metadata
963
1023
  result_transfer.push({'package' => package_id, Main::STATUS_FIELD => statuses})
964
1024
  # update skip list only if all transfer sessions completed
965
1025
  if skip_ids_persistency && TransferAgent.session_status(statuses).eql?(:success)
@@ -1036,6 +1096,7 @@ module Aspera
1036
1096
  end
1037
1097
  end
1038
1098
  when :automation
1099
+ change_api_scope(Api::AoC::Scope::ADMIN_USER)
1039
1100
  Log.log.warn('BETA: work under progress')
1040
1101
  # automation api is not in the same place
1041
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