aspera-cli 4.25.6 → 4.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +89 -48
  4. data/CONTRIBUTING.md +1 -1
  5. data/lib/aspera/api/aoc.rb +120 -79
  6. data/lib/aspera/api/node.rb +103 -51
  7. data/lib/aspera/ascp/installation.rb +99 -32
  8. data/lib/aspera/assert.rb +17 -13
  9. data/lib/aspera/cli/extended_value.rb +7 -2
  10. data/lib/aspera/cli/formatter.rb +107 -95
  11. data/lib/aspera/cli/main.rb +69 -10
  12. data/lib/aspera/cli/manager.rb +158 -78
  13. data/lib/aspera/cli/options.schema.yaml +82 -0
  14. data/lib/aspera/cli/plugins/aoc.rb +247 -144
  15. data/lib/aspera/cli/plugins/ats.rb +3 -3
  16. data/lib/aspera/cli/plugins/base.rb +60 -76
  17. data/lib/aspera/cli/plugins/config.rb +14 -12
  18. data/lib/aspera/cli/plugins/console.rb +3 -3
  19. data/lib/aspera/cli/plugins/faspex.rb +6 -6
  20. data/lib/aspera/cli/plugins/faspex5.rb +24 -23
  21. data/lib/aspera/cli/plugins/node.rb +67 -71
  22. data/lib/aspera/cli/plugins/oauth.rb +5 -12
  23. data/lib/aspera/cli/plugins/orchestrator.rb +13 -13
  24. data/lib/aspera/cli/plugins/preview.rb +116 -80
  25. data/lib/aspera/cli/plugins/server.rb +2 -10
  26. data/lib/aspera/cli/plugins/shares.rb +7 -7
  27. data/lib/aspera/cli/sync_actions.rb +1 -1
  28. data/lib/aspera/cli/transfer_agent.rb +17 -15
  29. data/lib/aspera/cli/version.rb +1 -1
  30. data/lib/aspera/command_line_builder.rb +22 -18
  31. data/lib/aspera/dot_container.rb +7 -3
  32. data/lib/aspera/environment.rb +6 -5
  33. data/lib/aspera/formatter_interface.rb +14 -0
  34. data/lib/aspera/hash_ext.rb +6 -0
  35. data/lib/aspera/log.rb +5 -4
  36. data/lib/aspera/markdown.rb +4 -1
  37. data/lib/aspera/oauth/factory.rb +1 -1
  38. data/lib/aspera/preview/file_types.rb +1 -1
  39. data/lib/aspera/preview/generator.rb +146 -91
  40. data/lib/aspera/preview/options.rb +4 -1
  41. data/lib/aspera/preview/terminal.rb +50 -20
  42. data/lib/aspera/preview/utils.rb +76 -34
  43. data/lib/aspera/products/transferd.rb +1 -1
  44. data/lib/aspera/proxy_auto_config.rb +3 -0
  45. data/lib/aspera/rest.rb +2 -1
  46. data/lib/aspera/rest_list.rb +23 -16
  47. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +62801 -0
  48. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +8898 -0
  49. data/lib/aspera/schema/documentation.rb +107 -0
  50. data/lib/aspera/schema/reader.rb +75 -0
  51. data/lib/aspera/schema/registry.rb +63 -0
  52. data/lib/aspera/secret_hider.rb +3 -1
  53. data/lib/aspera/sync/conf.schema.yaml +0 -26
  54. data/lib/aspera/sync/operations.rb +9 -5
  55. data/lib/aspera/transfer/faux_file.rb +1 -1
  56. data/lib/aspera/transfer/resumer.rb +1 -1
  57. data/lib/aspera/transfer/spec.rb +3 -3
  58. data/lib/aspera/transfer/spec.schema.yaml +1 -1
  59. data/lib/aspera/uri_reader.rb +17 -2
  60. data/lib/aspera/yaml.rb +4 -2
  61. data.tar.gz.sig +0 -0
  62. metadata +13 -7
  63. metadata.gz.sig +0 -0
  64. data/lib/aspera/transfer/spec_doc.rb +0 -76
@@ -24,26 +24,25 @@ module Aspera
24
24
  REDIRECT_LOCALHOST = 'http://localhost:12345'
25
25
  # admin objects that can be manipulated
26
26
  ADMIN_OBJECTS = %i[
27
- self
28
- organization
29
- user
30
- group
31
- group_membership
32
27
  client
28
+ client_access_key
29
+ client_registration_token
33
30
  contact
34
31
  dropbox
32
+ dropbox_membership
33
+ group
34
+ group_membership
35
+ kms_profile
35
36
  node
36
37
  operation
38
+ organization
37
39
  package
38
40
  saml_configuration
41
+ self
42
+ short_link
43
+ user
39
44
  workspace
40
45
  workspace_membership
41
- dropbox_membership
42
- short_link
43
- application
44
- client_registration_token
45
- client_access_key
46
- kms_profile
47
46
  ].freeze
48
47
  # query to list fully received packages
49
48
  PACKAGE_RECEIVED_BASE_QUERY = {
@@ -53,10 +52,8 @@ module Aspera
53
52
  'completed' => true
54
53
  }.freeze
55
54
  PACKAGE_LIST_DEFAULT_FIELDS = %w[id name created_at files_completed bytes_transferred].freeze
56
- # options and parameters for Api::AoC.new
57
- OPTIONS_NEW = %i[url auth client_id client_secret scope redirect_uri private_key passphrase username password workspace].freeze
58
55
 
59
- private_constant :REDIRECT_LOCALHOST, :ADMIN_OBJECTS, :PACKAGE_RECEIVED_BASE_QUERY, :OPTIONS_NEW, :PACKAGE_LIST_DEFAULT_FIELDS
56
+ private_constant :REDIRECT_LOCALHOST, :ADMIN_OBJECTS, :PACKAGE_RECEIVED_BASE_QUERY, :PACKAGE_LIST_DEFAULT_FIELDS
60
57
  class << self
61
58
  def application_name
62
59
  'Aspera on Cloud'
@@ -220,21 +217,36 @@ module Aspera
220
217
  # Change API scope for subsequent calls, re-instantiate API object
221
218
  # @param new_scope [String] New scope
222
219
  def change_api_scope(new_scope)
220
+ # Discard cache
223
221
  @cache_api_aoc = nil
224
222
  @scope = new_scope
223
+ nil
225
224
  end
226
225
 
227
226
  # Create an API object with the options from CLI, but with a different subpath
228
- # @param aoc_base_path [String] New subpath
227
+ # @param base_path [String] Base path for APIs.
229
228
  # @return [Api::AoC] API object for AoC (is Rest)
230
- def api_from_options(aoc_base_path)
231
- return Api::AoC.new(**Oauth.args_from_options(
232
- options,
233
- defaults: {workspace: nil},
229
+ def api_from_options(base_path)
230
+ # Get all existing OAuth kwargs from `options`.
231
+ api = Api::AoC.new(
234
232
  scope: @scope,
235
- subpath: aoc_base_path,
236
- secret_finder: config
237
- ))
233
+ subpath: base_path,
234
+ secret_finder: config,
235
+ **Oauth.kwargs_from_options(options)
236
+ )
237
+ # User set a workspace ?
238
+ # @type [String, nil]
239
+ workspace = options.get_option(:workspace)
240
+ if !workspace.nil? && (m = Manager.percent_selector(workspace))
241
+ case m[:field]
242
+ when 'name' then api.ws_ids[:name] = m[:value]
243
+ when 'id' then api.ws_ids[:id] = m[:value]
244
+ else Aspera.error_unexpected_value(m[:field]){'workspace selector: only `name` or `id`'}
245
+ end
246
+ else
247
+ api.ws_ids[:name] = workspace
248
+ end
249
+ api
238
250
  end
239
251
 
240
252
  # AoC Rest object
@@ -260,7 +272,7 @@ module Aspera
260
272
  # * `workspace_name` [String] (optional) the name, included if +name+ is true.
261
273
  # @note The key type (String or Symbol) depends on the +string+ parameter.
262
274
  def workspace_id_hash(hash = nil, string: false, name: false)
263
- info = aoc_api.workspace
275
+ info = aoc_api.workspace_info
264
276
  hash = {} if hash.nil?
265
277
  fields = %i[id]
266
278
  fields.push(:name) if name
@@ -276,9 +288,9 @@ module Aspera
276
288
  # @param resource_class_path url path for resource
277
289
  # @return identifier
278
290
  def get_resource_id_from_args(resource_class_path)
279
- return instance_identifier do |field, value|
291
+ return options.instance_identifier do |field, value|
280
292
  Aspera.assert(field.eql?('name'), type: BadArgument){'only selection by name is supported'}
281
- aoc_api.lookup_by_name(resource_class_path, value)['id']
293
+ aoc_api.lookup_with_q(resource_class_path, value: value)['id']
282
294
  end
283
295
  end
284
296
 
@@ -292,7 +304,7 @@ module Aspera
292
304
  # @param fields fields to display
293
305
  # @param base_query a query applied always
294
306
  # @param default_query default query unless overridden by user
295
- # @param &block (Optional) calls block with user's or default query
307
+ # @yieldparam query [Hash] The user's or default query for modification
296
308
  def result_list(resource_class_path, fields: nil, base_query: {}, default_query: {})
297
309
  Aspera.assert_type(base_query, Hash)
298
310
  Aspera.assert_type(default_query, Hash)
@@ -309,7 +321,7 @@ module Aspera
309
321
  # convenience: specify name instead of id
310
322
  raise BadArgument, 'Use field dropbox_name or dropbox_id, not both' if query.key?('dropbox_id')
311
323
  # TODO : craft a query that looks for dropbox only in current workspace
312
- query['dropbox_id'] = aoc_api.lookup_by_name('dropboxes', query.delete('dropbox_name'))['id']
324
+ query['dropbox_id'] = aoc_api.lookup_with_q('dropboxes', value: query.delete('dropbox_name'))['id']
313
325
  end
314
326
  workspace_id_hash(query, string: true)
315
327
  # by default show dropbox packages only for dropboxes
@@ -327,11 +339,10 @@ module Aspera
327
339
  return aoc_api.read_with_paging('packages', query.compact)
328
340
  end
329
341
 
330
- NODE4_EXT_COMMANDS = %i[transfer].concat(Node::COMMANDS_GEN4).freeze
331
- private_constant :NODE4_EXT_COMMANDS
342
+ FILES_COMMANDS = (Node::COMMANDS_GEN4 + %i[transfer]).freeze
332
343
 
333
- # Execute a node gen4 command
334
- # @param command_repo [Symbol] command to execute
344
+ # Execute a node gen4 command starting at given node and file IDs
345
+ # @param command_repo [Symbol] Command to execute
335
346
  # @param node_id [String] Node identifier
336
347
  # @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.
337
348
  # @param scope [String] node scope (Node::SCOPE_<USER|ADMIN>), or nil (requires secret)
@@ -341,7 +352,7 @@ module Aspera
341
352
  scope: scope,
342
353
  **workspace_id_hash(name: true)
343
354
  )
344
- file_id = top_node_api.read("access_keys/#{top_node_api.app_info[:node_info]['access_key']}")['root_file_id'] if file_id.nil?
355
+ file_id = top_node_api.read("access_keys/#{top_node_api.app_info.node_info['access_key']}")['root_file_id'] if file_id.nil?
345
356
  node_plugin = Node.new(context: context, api: top_node_api)
346
357
  case command_repo
347
358
  when *Node::COMMANDS_GEN4
@@ -363,23 +374,23 @@ module Aspera
363
374
  server_folder = source_folder
364
375
  else Aspera.error_unreachable_line
365
376
  end
366
- client_apfid = top_node_api.resolve_api_fid(file_id, client_folder)
367
- server_apfid = top_node_api.resolve_api_fid(file_id, server_folder)
377
+ client_apifid = top_node_api.resolve_api_fid(file_id, client_folder)
378
+ server_apifid = top_node_api.resolve_api_fid(file_id, server_folder)
368
379
  # force node as transfer agent
369
380
  transfer.agent_instance = Agent::Node.new(
370
- url: client_apfid[:api].base_url,
371
- username: client_apfid[:api].app_info[:node_info]['access_key'],
372
- password: client_apfid[:api].oauth.authorization,
373
- root_id: client_apfid[:file_id]
381
+ url: client_apifid.node_api.base_url,
382
+ username: client_apifid.node_api.app_info.node_info['access_key'],
383
+ password: client_apifid.node_api.oauth.authorization,
384
+ root_id: client_apifid.file_id
374
385
  )
375
386
  # additional node to node TS info
376
387
  add_ts = {
377
- 'remote_access_key' => server_apfid[:api].app_info[:node_info]['access_key'],
378
- 'destination_root_id' => server_apfid[:file_id],
379
- 'source_root_id' => client_apfid[:file_id]
388
+ 'remote_access_key' => server_apifid.node_api.app_info.node_info['access_key'],
389
+ 'destination_root_id' => server_apifid.file_id,
390
+ 'source_root_id' => client_apifid.file_id
380
391
  }
381
- return Main.result_transfer(transfer.start(server_apfid[:api].transfer_spec_gen4(
382
- server_apfid[:file_id],
392
+ return Main.result_transfer(transfer.start(server_apifid.node_api.transfer_spec_gen4(
393
+ server_apifid.file_id,
383
394
  client_direction,
384
395
  add_ts
385
396
  )))
@@ -388,70 +399,90 @@ module Aspera
388
399
  Aspera.error_unreachable_line
389
400
  end
390
401
 
402
+ # Execute an action on admin resources
391
403
  # @param resource_type [Symbol] One of ADMIN_OBJECTS
392
404
  def execute_resource_action(resource_type)
393
- # get path on API, resource type is singular, but api is plural
394
- resource_class_path =
395
- case resource_type
396
- # special cases: singleton, in admin, with x
397
- when :self, :organization then resource_type
398
- when :client_registration_token, :client_access_key then "admin/#{resource_type}s"
399
- when :application then 'admin/apps_new'
400
- when :dropbox then "#{resource_type}es"
401
- when :kms_profile then "integrations/#{resource_type}s"
402
- else "#{resource_type}s"
403
- end
404
- # build list of supported operations
405
- singleton_object = %i[self organization].include?(resource_type)
406
- global_operations = %i[create list]
407
- supported_operations = %i[show modify]
408
- supported_operations.push(:delete, *global_operations) unless singleton_object
409
- supported_operations.push(:do, :bearer_token) if resource_type.eql?(:node)
410
- supported_operations.push(:set_pub_key) if resource_type.eql?(:client)
411
- supported_operations.push(:shared_folder, :dropbox) if resource_type.eql?(:workspace)
405
+ # Set to `true` is resource creation requires a workspace id
406
+ require_workspace_id = false
407
+ # Default fields to display
408
+ list_default_fields = %w[id name]
409
+ # Default query for `list` action
410
+ list_default_query = {}
411
+ # In result of create, what is the field that contains the id of the created resource
412
+ id_result = 'id'
413
+ supported_operations = Operations::ALL
414
+ # API path
415
+ resource_class_path = "#{resource_type}s"
416
+ # path in openapi where post is located to get creation schema
417
+ create_schema_path = resource_class_path
418
+ case resource_type
419
+ when :client
420
+ supported_operations += %i[set_pub_key]
421
+ # schema_create_modify = Schema::Registry.req_body(Schema::Registry::AOC, "#{resource_class_path}.post")
422
+ when :client_access_key
423
+ resource_class_path = "admin/#{resource_type}s"
424
+ when :client_registration_token
425
+ resource_class_path = "admin/#{resource_type}s"
426
+ list_default_fields = %w[id value data.client_subject_scopes data.name created_at]
427
+ id_result = 'token'
428
+ when :contact
429
+ list_default_fields = %w[source_type source_id name email]
430
+ # list_default_query = {'include_only_user_personal_contacts' => true} if @scope == Api::AoC::Scope::USER
431
+ when :dropbox
432
+ require_workspace_id = true
433
+ resource_class_path = "#{resource_type}es"
434
+ create_schema_path = resource_class_path
435
+ when :group, :saml_configuration
436
+ create_schema_path = nil
437
+ when :group_membership
438
+ list_default_fields = %w[id group_id member_type member_id]
439
+ create_schema_path = nil
440
+ when :kms_profile
441
+ resource_class_path = "integrations/#{resource_type}s"
442
+ create_schema_path = nil
443
+ when :node
444
+ list_default_fields = %w[id name host access_key]
445
+ supported_operations += %i[do bearer_token]
446
+ when :operation
447
+ list_default_fields = %w[id type status created_at updated_at workspace_id user_id workspace_membership_id group_membership_id]
448
+ supported_operations = %i[list show modify]
449
+ when :organization, :self
450
+ supported_operations = Operations::SINGLETON
451
+ resource_instance_path = resource_class_path = resource_type
452
+ when :short_link
453
+ list_default_fields = %w[id short_url data.url_token_data.purpose password_enabled password_protected updated_by_user_id updated_at]
454
+ when :user
455
+ list_default_fields = %w[id name email]
456
+ supported_operations += %i[preferences notifications]
457
+ when :workspace
458
+ supported_operations += %i[shared_folder dropbox]
459
+ when :workspace_membership
460
+ list_default_fields = %w[id workspace_id member_type member_id]
461
+ end
462
+ # Default location of creation payload in schema
463
+ schema_create_modify = Schema::Registry.req_body(Schema::Registry::AOC, "#{create_schema_path}.post") if create_schema_path && supported_operations.include?(:create)
412
464
  command = options.get_next_command(supported_operations)
413
- # require identifier for non global commands
414
- if !singleton_object && !global_operations.include?(command)
465
+ # Require identifier for non global commands
466
+ if (supported_operations != Operations::SINGLETON) && !Operations::GLOBAL.include?(command)
415
467
  res_id = get_resource_id_from_args(resource_class_path)
416
468
  resource_instance_path = "#{resource_class_path}/#{res_id}"
417
469
  end
418
- resource_instance_path = resource_class_path if singleton_object
419
470
  case command
420
471
  when :create
421
- id_result = 'id'
422
- id_result = 'token' if resource_class_path.eql?('admin/client_registration_tokens')
423
472
  # TODO: report inconsistency: creation url is !=, and does not return id.
424
473
  resource_class_path = 'admin/client_registration/token' if resource_class_path.eql?('admin/client_registration_tokens')
425
- return do_bulk_operation(command: command, descr: 'creation data', id_result: id_result) do |params|
474
+ workspace_id = aoc_api.workspace_info[:id] if require_workspace_id
475
+ return do_bulk_operation(command: command, descr: 'creation data', id_result: id_result, schema: schema_create_modify) do |params|
476
+ params['workspace_id'] = workspace_id if require_workspace_id && workspace_id && !params.key?('workspace_id')
426
477
  aoc_api.create(resource_class_path, params)
427
478
  end
428
479
  when :list
429
- default_fields = ['id']
430
- default_query = {}
431
- case resource_type
432
- when :application
433
- default_query = {organization_apps: true}
434
- default_fields.push('app_type', 'app_name', 'available', 'direct_authorizations_allowed', 'workspace_authorizations_allowed')
435
- when :client, :client_access_key, :dropbox, :group, :package, :saml_configuration, :workspace then default_fields.push('name')
436
- when :client_registration_token then default_fields.push('value', 'data.client_subject_scopes', 'created_at')
437
- when :contact
438
- default_fields = %w[source_type source_id name email]
439
- default_query = {'include_only_user_personal_contacts' => true} if @scope == Api::AoC::Scope::USER
440
- when :node then default_fields.push('name', 'host', 'access_key')
441
- when :operation then default_fields = nil
442
- when :short_link then default_fields.push('short_url', 'data.url_token_data.purpose')
443
- when :user then default_fields.push('name', 'email')
444
- when :group_membership then default_fields.push('group_id', 'member_type', 'member_id')
445
- when :workspace_membership then default_fields.push('workspace_id', 'member_type', 'member_id')
446
- end
447
- return result_list(resource_class_path, fields: default_fields, default_query: default_query)
480
+ return result_list(resource_class_path, fields: list_default_fields, default_query: list_default_query)
448
481
  when :show
449
- object = aoc_api.read(resource_instance_path)
450
- # default: show all, but certificate
451
- fields = object.keys.reject{ |k| k.eql?('certificate')}
452
- return Main.result_single_object(object, fields: fields)
482
+ object = aoc_api.read(resource_instance_path, query_read_delete)
483
+ return Main.result_single_object(object, fields: Formatter.all_but('certificate'))
453
484
  when :modify
454
- changes = options.get_next_argument('properties', validation: Hash)
485
+ changes = options.get_next_argument('properties', validation: Hash, schema: schema_create_modify)
455
486
  return do_bulk_operation(command: command, values: res_id) do |one_id|
456
487
  aoc_api.update("#{resource_class_path}/#{one_id}", changes)
457
488
  {'id' => one_id}
@@ -468,7 +499,7 @@ module Aspera
468
499
  aoc_api.update(resource_instance_path, {jwt_grant_enabled: true, public_key: the_public_key})
469
500
  return Main.result_success
470
501
  when :do
471
- command_repo = options.get_next_command(NODE4_EXT_COMMANDS)
502
+ command_repo = options.get_next_command(FILES_COMMANDS)
472
503
  return execute_nodegen4_command(command_repo, res_id, scope: Api::Node::Scope::ADMIN)
473
504
  when :bearer_token
474
505
  node_api = aoc_api.node_api_from(
@@ -488,12 +519,18 @@ module Aspera
488
519
  query = options.get_option(:query) || Api::AoC.workspace_access(res_id).merge({'admin' => true})
489
520
  shared_folders = aoc_api.read_with_paging("#{resource_instance_path}/permissions", query)[:items]
490
521
  # inside a workspace
491
- command_shared = options.get_next_command(%i[list member])
522
+ command_shared = options.get_next_command(%i[list node member])
492
523
  case command_shared
493
524
  when :list
494
525
  return Main.result_object_list(shared_folders, fields: %w[id node_name node_id file_id file.path tags.aspera.files.workspace.share_as])
526
+ when :node
527
+ shared_folder_id = options.instance_identifier(description: 'Shared folder ID')
528
+ shared_folder = shared_folders.find{ |i| i['id'].eql?(shared_folder_id)}
529
+ Aspera.assert(shared_folder)
530
+ command_repo = options.get_next_command(FILES_COMMANDS)
531
+ return execute_nodegen4_command(command_repo, shared_folder['node_id'], file_id: shared_folder['file_id'], scope: Api::Node::Scope::ADMIN)
495
532
  when :member
496
- shared_folder_id = instance_identifier
533
+ shared_folder_id = options.instance_identifier(description: 'Shared folder ID')
497
534
  shared_folder = shared_folders.find{ |i| i['id'].eql?(shared_folder_id)}
498
535
  Aspera.assert(shared_folder)
499
536
  command_shared_member = options.get_next_command(%i[list])
@@ -524,11 +561,75 @@ module Aspera
524
561
  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])
525
562
  end
526
563
  end
564
+ when :preferences, :notifications
565
+ user_preferences_res = "#{resource_instance_path}/#{command.eql?(:preferences) ? 'user_interaction_preferences' : 'notification_preferences'}"
566
+ case options.get_next_command(%i[show modify])
567
+ when :show
568
+ return Main.result_single_object(aoc_api.read(user_preferences_res))
569
+ when :modify
570
+ aoc_api.update(user_preferences_res, options.get_next_argument('properties', validation: Hash))
571
+ return Main.result_status('modified')
572
+ end
527
573
  else Aspera.error_unexpected_value(command)
528
574
  end
529
575
  end
530
576
 
531
- ADMIN_ACTIONS = %i[ats bearer_token resource usage_reports analytics subscription auth_providers].concat(ADMIN_OBJECTS).freeze
577
+ def execute_application_action
578
+ apps_info = aoc_api.read('admin/apps')
579
+ all_app_types = apps_info.map{ |i| i['app_type'].to_sym}
580
+ command_apps = options.get_next_command(%i[types settings instance membership])
581
+ case command_apps
582
+ when :types
583
+ return Main.result_object_list(apps_info)
584
+ when :settings
585
+ app_type = options.get_next_command(all_app_types)
586
+ cmd_path = "/apps/#{app_type}/settings"
587
+ command_app_settings = options.get_next_command(Operations::SINGLETON)
588
+ case command_app_settings
589
+ when :show
590
+ return Main.result_single_object(aoc_api.read(cmd_path))
591
+ when :modify
592
+ aoc_api.update(cmd_path, options.get_next_argument('properties', validation: Hash))
593
+ return Main.result_status('modified')
594
+ end
595
+ when :instance
596
+ list_default_query = {workspace_id: aoc_api.workspace_info[:id]}
597
+ list_default_fields = %w[id app_type available workspace_id]
598
+ command_app_instances = options.get_next_command(%i[list] + Operations::SINGLETON)
599
+ resource_path = 'admin/apps_new'
600
+ if Operations::SINGLETON.include?(command_app_instances)
601
+ app_type = options.get_next_command(all_app_types)
602
+ resource_path = "#{resource_path}/#{app_type}/#{options.instance_identifier(description: "#{app_type} identifier")}"
603
+ end
604
+ case command_app_instances
605
+ when :list
606
+ return result_list(resource_path, fields: list_default_fields, default_query: list_default_query)
607
+ when :show
608
+ return Main.result_single_object(aoc_api.read(resource_path, query_read_delete))
609
+ when :modify
610
+ aoc_api.update(resource_path, options.get_next_argument('properties', validation: Hash))
611
+ return Main.result_status('modified')
612
+ end
613
+ when :membership
614
+ resource_path = 'apps/app_memberships'
615
+ command_app_member = options.get_next_command(%i[create list show delete])
616
+ resource_path = "#{resource_path}/#{options.instance_identifier(description: 'membership id')}" unless Operations::GLOBAL.include?(command_app_member)
617
+ case command_app_member
618
+ when :list
619
+ return result_list(resource_path)
620
+ when :delete
621
+ aoc_api.delete("#{resource_path}}")
622
+ return Main.result_status('deleted')
623
+ when :show
624
+ return Main.result_single_object(aoc_api.read(resource_path, query_read_delete))
625
+ when :create
626
+ aoc_api.update(resource_path, options.get_next_argument('membership properties', validation: Hash))
627
+ return Main.result_status('modified')
628
+ end
629
+ end
630
+ end
631
+
632
+ ADMIN_ACTIONS = %i[bearer_token application ats usage_reports analytics subscription auth_providers].concat(ADMIN_OBJECTS).freeze
532
633
 
533
634
  def execute_admin_action
534
635
  # change scope to admin
@@ -537,11 +638,10 @@ module Aspera
537
638
  case command_admin
538
639
  when :bearer_token
539
640
  return Main.result_text(aoc_api.oauth.authorization)
540
- when :resource
541
- Log.log.warn('resource command is deprecated (4.18), directly use the specific command instead')
542
- return execute_resource_action(options.get_next_argument('resource', accept_list: ADMIN_OBJECTS))
543
641
  when *ADMIN_OBJECTS
544
642
  return execute_resource_action(command_admin)
643
+ when :application
644
+ return execute_application_action
545
645
  when :auth_providers
546
646
  command_auth_prov = options.get_next_command(%i[list update])
547
647
  case command_auth_prov
@@ -699,7 +799,7 @@ module Aspera
699
799
  id: IdGenerator.from_list(
700
800
  'aoc_ana_date',
701
801
  options.get_option(:url, mandatory: true),
702
- aoc_api.workspace[:name],
802
+ aoc_api.workspace_info[:name],
703
803
  event_resource_type.to_s,
704
804
  event_resource_id
705
805
  )
@@ -721,7 +821,7 @@ module Aspera
721
821
  when :files
722
822
  event_type = command_analytics.to_s
723
823
  event_resource_type = options.get_next_argument('resource', accept_list: %i[organizations users nodes])
724
- event_resource_id = instance_identifier(description: "#{event_resource_type} identifier")
824
+ event_resource_id = options.instance_identifier(description: "#{event_resource_type} identifier")
725
825
  event_resource_id =
726
826
  case event_resource_type
727
827
  when :organizations then aoc_api.current_user_info['organization_id']
@@ -729,7 +829,7 @@ module Aspera
729
829
  when :nodes then aoc_api.current_user_info['read_only_home_node_id']
730
830
  else Aspera.error_unreachable_line
731
831
  end if event_resource_id.empty?
732
- event_uuid = instance_identifier(description: 'event uuid')
832
+ event_uuid = options.instance_identifier(description: 'event uuid')
733
833
  filter = query_read_delete(default: {})
734
834
  filter['limit'] ||= 100
735
835
  events = analytics_api.read("#{event_resource_type}/#{event_resource_id}/transfers/#{event_uuid}/#{event_type}", filter)[event_type]
@@ -747,7 +847,10 @@ module Aspera
747
847
  # - a resource id, e.g. `scQ7uXPbvQ`
748
848
  # - a short URL path, e.g. `dxyRpT9`
749
849
  # @param shared_data [Hash] Information for shared data: dropbox_id+name or file_id+node_id
750
- # @param &perm_block [Proc] Optional: create/modify/delete permissions on node
850
+ # @param shared_data [Hash] Shared data for the short link
851
+ # @yieldparam operation [Symbol] Operation type (:create, :update, :delete)
852
+ # @yieldparam resource_id [String] Resource ID for permission management
853
+ # @yieldparam access_levels [Object] Access levels for permissions
751
854
  def short_link_command(**shared_data, &perm_block)
752
855
  link_type = options.get_next_argument('link access (public or private)', accept_list: %i[public private])
753
856
  if shared_data.keys.sort == %i[dropbox_id name]
@@ -818,7 +921,7 @@ module Aspera
818
921
  short_list = aoc_api.read_with_paging('short_links', list_params.merge(query_read_delete(default: {})).compact)
819
922
  case command
820
923
  when :delete
821
- one_id = instance_identifier(description: 'short link id')
924
+ one_id = options.instance_identifier(description: 'short link id')
822
925
  if link_type.eql?(:public)
823
926
  found = short_list[:items].find{ |item| item['id'].eql?(one_id)}
824
927
  raise BadIdentifier.new('Short link', one_id) if found.nil?
@@ -832,12 +935,12 @@ module Aspera
832
935
  when :list
833
936
  return Main.result_object_list(short_list[:items], fields: Formatter.all_but('data'), total: short_list[:total])
834
937
  when :show
835
- one_id = instance_identifier(description: 'short link id')
938
+ one_id = options.instance_identifier(description: 'short link id')
836
939
  found = short_list[:items].find{ |item| item['id'].eql?(one_id)}
837
940
  raise BadIdentifier.new('Short link', one_id) if found.nil?
838
941
  return Main.result_single_object(found, fields: Formatter.all_but('data'))
839
942
  when :modify
840
- one_id = instance_identifier(description: 'short link id')
943
+ one_id = options.instance_identifier(description: 'short link id')
841
944
  node_file = shared_data.slice(:node_id, :file_id)
842
945
  modify_payload = {
843
946
  edit_access: true,
@@ -879,7 +982,7 @@ module Aspera
879
982
  id: IdGenerator.from_list(
880
983
  'aoc_recv',
881
984
  options.get_option(:url, mandatory: true),
882
- aoc_api.workspace[:id],
985
+ aoc_api.workspace_info[:id],
883
986
  aoc_api.additional_persistence_ids
884
987
  )
885
988
  )
@@ -897,8 +1000,7 @@ module Aspera
897
1000
  def execute_action
898
1001
  command = options.get_next_command(ACTIONS)
899
1002
  if %i[files packages].include?(command)
900
- default_flag = ' (default)' if options.get_option(:workspace).eql?(:default)
901
- formatter.display_status("Workspace: #{aoc_api.workspace[:name].to_s.red}#{default_flag}")
1003
+ formatter.display_status("Workspace: #{aoc_api.workspace_info[:name].to_s.red}#{' (default)' if aoc_api.default_workspace?}")
902
1004
  if !aoc_api.private_link.nil?
903
1005
  folder_name = aoc_api.node_api_from(node_id: aoc_api.home[:node_id]).read("files/#{aoc_api.home[:file_id]}")['name']
904
1006
  formatter.display_status("Private Folder: #{folder_name}")
@@ -919,7 +1021,8 @@ module Aspera
919
1021
  when :tier_restrictions
920
1022
  return Main.result_single_object(aoc_api.read('tier_restrictions'))
921
1023
  when :user
922
- case options.get_next_command(%i[workspaces profile preferences contacts])
1024
+ user_cmd = options.get_next_command(%i[workspaces profile preferences notifications contacts])
1025
+ case user_cmd
923
1026
  when :contacts
924
1027
  return execute_resource_action(:contact)
925
1028
  # when :settings
@@ -929,7 +1032,7 @@ module Aspera
929
1032
  when :list
930
1033
  return result_list('workspaces', fields: %w[id name])
931
1034
  when :current
932
- return Main.result_single_object(aoc_api.workspace)
1035
+ return Main.result_single_object(aoc_api.workspace_info)
933
1036
  end
934
1037
  when :profile
935
1038
  case options.get_next_command(%i[show modify])
@@ -939,8 +1042,8 @@ module Aspera
939
1042
  aoc_api.update("users/#{aoc_api.current_user_info(exception: true)['id']}", options.get_next_argument('properties', validation: Hash))
940
1043
  return Main.result_status('modified')
941
1044
  end
942
- when :preferences
943
- user_preferences_res = "users/#{aoc_api.current_user_info(exception: true)['id']}/user_interaction_preferences"
1045
+ when :preferences, :notifications
1046
+ user_preferences_res = "users/#{aoc_api.current_user_info(exception: true)['id']}/#{user_cmd.eql?(:preferences) ? 'user_interaction_preferences' : 'notification_preferences'}"
944
1047
  case options.get_next_command(%i[show modify])
945
1048
  when :show
946
1049
  return Main.result_single_object(aoc_api.read(user_preferences_res))
@@ -965,7 +1068,7 @@ module Aspera
965
1068
  return short_link_command(dropbox_id: get_resource_id_from_args('dropboxes'), name: '')
966
1069
  end
967
1070
  when :send
968
- package_data = value_create_modify(command: package_command)
1071
+ package_data = value_create_modify(command: package_command, schema: Schema::Registry.req_body(Schema::Registry::AOC, 'packages.post'))
969
1072
  new_user_option = options.get_option(:new_user_option)
970
1073
  option_validate = options.get_option(:validate_metadata)
971
1074
  # Works for both normal user auth and link auth.
@@ -991,7 +1094,7 @@ module Aspera
991
1094
  ids_to_download = aoc_api.public_link['data']['package_id']
992
1095
  end
993
1096
  # Get from command line unless it was a public link
994
- ids_to_download ||= instance_identifier
1097
+ ids_to_download ||= options.instance_identifier
995
1098
  skip_ids_persistency = package_persistency
996
1099
  case ids_to_download
997
1100
  when SpecialValues::INIT
@@ -1046,7 +1149,7 @@ module Aspera
1046
1149
  end
1047
1150
  return Main.result_transfer_multiple(result_transfer)
1048
1151
  when :show
1049
- package_id = instance_identifier
1152
+ package_id = options.instance_identifier
1050
1153
  package_info = aoc_api.read("packages/#{package_id}")
1051
1154
  return Main.result_single_object(package_info)
1052
1155
  when :list
@@ -1054,27 +1157,27 @@ module Aspera
1054
1157
  skip_ids_persistency = package_persistency
1055
1158
  reject_packages_from_persistency(result[:items], skip_ids_persistency)
1056
1159
  display_fields = PACKAGE_LIST_DEFAULT_FIELDS
1057
- display_fields += ['workspace_id'] if aoc_api.workspace[:id].nil?
1160
+ display_fields += ['workspace_id'] if aoc_api.workspace_info[:id].nil?
1058
1161
  return Main.result_object_list(result[:items], fields: display_fields, total: result[:total])
1059
1162
  when :delete
1060
- return do_bulk_operation(command: package_command, values: instance_identifier) do |id|
1061
- Aspera.assert_type(id, String, Integer){'identifier'}
1062
- aoc_api.delete("packages/#{id}")
1163
+ return do_bulk_operation(command: package_command, values: options.instance_identifier) do |package_id|
1164
+ Aspera.assert_type(package_id, String, Integer){'identifier'}
1165
+ aoc_api.delete("packages/#{package_id}")
1063
1166
  end
1064
1167
  when :modify
1065
- id = instance_identifier
1168
+ package_id = options.instance_identifier
1066
1169
  package_data = value_create_modify(command: package_command)
1067
- aoc_api.update("packages/#{id}", package_data)
1170
+ aoc_api.update("packages/#{package_id}", package_data)
1068
1171
  return Main.result_status('modified')
1069
1172
  when *Node::NODE4_READ_ACTIONS
1070
- package_id = instance_identifier
1173
+ package_id = options.instance_identifier
1071
1174
  package_info = aoc_api.read("packages/#{package_id}")
1072
1175
  return execute_nodegen4_command(package_command, package_info['node_id'], file_id: package_info['contents_file_id'], scope: Api::Node::Scope::USER)
1073
1176
  end
1074
1177
  when :files
1075
- command_repo = options.get_next_command([:short_link].concat(NODE4_EXT_COMMANDS))
1178
+ command_repo = options.get_next_command([:short_link].concat(FILES_COMMANDS))
1076
1179
  case command_repo
1077
- when *NODE4_EXT_COMMANDS
1180
+ when *FILES_COMMANDS
1078
1181
  return execute_nodegen4_command(command_repo, aoc_api.home[:node_id], file_id: aoc_api.home[:file_id], scope: Api::Node::Scope::USER)
1079
1182
  when :short_link
1080
1183
  folder_dest = options.get_next_argument('path', validation: String)
@@ -1082,16 +1185,16 @@ module Aspera
1082
1185
  node_id: aoc_api.home[:node_id],
1083
1186
  **workspace_id_hash(name: true)
1084
1187
  )
1085
- shared_apfid = home_node_api.resolve_api_fid(aoc_api.home[:file_id], folder_dest)
1188
+ shared_apifid = home_node_api.resolve_api_fid(aoc_api.home[:file_id], folder_dest)
1086
1189
  return short_link_command(
1087
- node_id: shared_apfid[:api].app_info[:node_info]['id'],
1088
- file_id: shared_apfid[:file_id]
1190
+ node_id: shared_apifid.node_api.app_info.node_info['id'],
1191
+ file_id: shared_apifid.file_id
1089
1192
  ) do |op, id, access_levels|
1090
1193
  case op
1091
1194
  when :create
1092
1195
  # `id` is the resource id
1093
1196
  perm_data = {
1094
- 'file_id' => shared_apfid[:file_id],
1197
+ 'file_id' => shared_apifid.file_id,
1095
1198
  'access_id' => id,
1096
1199
  'access_type' => 'user',
1097
1200
  'access_levels' => Api::AoC.expand_access_levels(access_levels),
@@ -1100,23 +1203,23 @@ module Aspera
1100
1203
  'folder_name' => File.basename(folder_dest),
1101
1204
  'created_by_name' => aoc_api.current_user_info['name'],
1102
1205
  'created_by_email' => aoc_api.current_user_info['email'],
1103
- 'access_key' => shared_apfid[:api].app_info[:node_info]['access_key'],
1104
- 'node' => shared_apfid[:api].app_info[:node_info]['name'],
1206
+ 'access_key' => shared_apifid.node_api.app_info.node_info['access_key'],
1207
+ 'node' => shared_apifid.node_api.app_info.node_info['name'],
1105
1208
  **workspace_id_hash(string: true, name: true)
1106
1209
  }
1107
1210
  }
1108
- created_data = shared_apfid[:api].create('permissions', perm_data)
1109
- aoc_api.permissions_send_event(event_data: created_data, app_info: shared_apfid[:api].app_info)
1211
+ created_data = shared_apifid.node_api.create('permissions', perm_data)
1212
+ aoc_api.permissions_send_event(event_data: created_data, app_info: shared_apifid.node_api.app_info)
1110
1213
  when :update
1111
1214
  # `id` is the permission_id
1112
- found = shared_apfid[:api].read('permissions', {file_id: shared_apfid[:file_id], inherited: false, access_type: 'user', access_id: id}).find{ |i| i['access_id'].eql?(id)}
1113
- raise Error, 'Short link not found: #{id}' if found.nil?
1114
- shared_apfid[:api].update("permissions/#{found['id']}", {access_levels: Api::AoC.expand_access_levels(access_levels)})
1215
+ found = shared_apifid.node_api.read('permissions', {file_id: shared_apifid.file_id, inherited: false, access_type: 'user', access_id: id}).find{ |i| i['access_id'].eql?(id)}
1216
+ raise Error, "Short link not found: #{id}" if found.nil?
1217
+ shared_apifid.node_api.update("permissions/#{found['id']}", {access_levels: Api::AoC.expand_access_levels(access_levels)})
1115
1218
  when :delete
1116
1219
  # `id` is the resource id, i.e. `access_id`
1117
- found = shared_apfid[:api].read('permissions', {file_id: shared_apfid[:file_id], inherited: false, access_type: 'user', access_id: id}).first
1118
- raise Error, 'Short link not found: #{id}' if found.nil?
1119
- shared_apfid[:api].delete("permissions/#{found['id']}")
1220
+ found = shared_apifid.node_api.read('permissions', {file_id: shared_apifid.file_id, inherited: false, access_type: 'user', access_id: id}).first
1221
+ raise Error, "Short link not found: #{id}" if found.nil?
1222
+ shared_apifid.node_api.delete("permissions/#{found['id']}")
1120
1223
  else Aspera.error_unexpected_value(op)
1121
1224
  end
1122
1225
  end
@@ -1131,21 +1234,21 @@ module Aspera
1131
1234
  when :instances
1132
1235
  return entity_execute(api: aoc_api, entity: 'workflow_instances')
1133
1236
  when :workflows
1134
- wf_command = options.get_next_command(%i[action launch].concat(ALL_OPS))
1237
+ wf_command = options.get_next_command(%i[action launch].concat(Operations::ALL))
1135
1238
  case wf_command
1136
- when *ALL_OPS
1239
+ when *Operations::ALL
1137
1240
  return entity_execute(
1138
1241
  api: automation_api,
1139
1242
  entity: 'workflows',
1140
1243
  command: wf_command
1141
1244
  )
1142
1245
  when :launch
1143
- wf_id = instance_identifier
1246
+ wf_id = options.instance_identifier
1144
1247
  data = automation_api.create("workflows/#{wf_id}/launch", {})
1145
1248
  return Main.result_single_object(data)
1146
1249
  when :action
1147
1250
  # TODO: not complete
1148
- wf_id = instance_identifier
1251
+ wf_id = options.instance_identifier
1149
1252
  wf_action_cmd = options.get_next_command(%i[list create show])
1150
1253
  Log.log.warn{"Not implemented: #{wf_action_cmd}"}
1151
1254
  step = automation_api.create('steps', {'workflow_id' => wf_id})
@@ -1164,7 +1267,7 @@ module Aspera
1164
1267
  uri = URI.parse(parameters.delete(:url){WebServerSimple::DEFAULT_URL})
1165
1268
  server = WebServerSimple.new(uri, **parameters.slice(*WebServerSimple::PARAMS))
1166
1269
  Aspera.assert(parameters.except(*WebServerSimple::PARAMS).empty?)
1167
- server.mount(uri.path, Faspex4GWServlet, aoc_api, aoc_api.workspace[:id])
1270
+ server.mount(uri.path, Faspex4GWServlet, aoc_api, aoc_api.workspace_info[:id])
1168
1271
  server.start
1169
1272
  return Main.result_status('Gateway terminated')
1170
1273
  else Aspera.error_unreachable_line