aspera-cli 4.27.2 → 4.27.3

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 (130) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +50 -0
  4. data/bin/ascli +2 -1
  5. data/docs/README.md +804 -746
  6. data/lib/aspera/agent/connect.rb +6 -4
  7. data/lib/aspera/agent/desktop.rb +2 -2
  8. data/lib/aspera/agent/direct.rb +3 -1
  9. data/lib/aspera/agent/node.rb +3 -3
  10. data/lib/aspera/api/alee.rb +1 -1
  11. data/lib/aspera/api/aoc.rb +14 -12
  12. data/lib/aspera/api/ats.rb +1 -1
  13. data/lib/aspera/api/cos_node.rb +2 -2
  14. data/lib/aspera/api/faspex.rb +9 -7
  15. data/lib/aspera/api/httpgw.rb +37 -33
  16. data/lib/aspera/api/node.rb +38 -33
  17. data/lib/aspera/ascmd.rb +3 -1
  18. data/lib/aspera/ascp/installation.rb +62 -27
  19. data/lib/aspera/ascp/management.rb +1 -0
  20. data/lib/aspera/assert.rb +4 -0
  21. data/lib/aspera/cli/ascp_actions.rb +20 -41
  22. data/lib/aspera/cli/async_transfer_store.rb +2 -2
  23. data/lib/aspera/cli/bootstrapper.rb +11 -15
  24. data/lib/aspera/cli/command_line.rb +252 -0
  25. data/lib/aspera/cli/command_registry.rb +149 -33
  26. data/lib/aspera/cli/command_spec.rb +103 -14
  27. data/lib/aspera/cli/completion/ascli.bash +12 -0
  28. data/lib/aspera/cli/completion/ascli.fish +16 -0
  29. data/lib/aspera/cli/completion/ascli.zsh +19 -0
  30. data/lib/aspera/cli/context.rb +3 -0
  31. data/lib/aspera/cli/deprecation.rb +37 -0
  32. data/lib/aspera/cli/extended_value.rb +2 -0
  33. data/lib/aspera/cli/formatter.rb +87 -75
  34. data/lib/aspera/cli/gem_checker.rb +1 -1
  35. data/lib/aspera/cli/hints.rb +7 -6
  36. data/lib/aspera/cli/http.rb +21 -21
  37. data/lib/aspera/cli/info.rb +3 -0
  38. data/lib/aspera/cli/mcp_tool.rb +47 -83
  39. data/lib/aspera/cli/option_declarator.rb +33 -42
  40. data/lib/aspera/cli/option_registry.rb +69 -0
  41. data/lib/aspera/cli/option_types.rb +103 -0
  42. data/lib/aspera/cli/option_value.rb +281 -0
  43. data/lib/aspera/cli/options.schema.yaml +38 -5
  44. data/lib/aspera/cli/parser.rb +307 -848
  45. data/lib/aspera/cli/plugins/alee.rb +7 -4
  46. data/lib/aspera/cli/plugins/aoc.rb +430 -380
  47. data/lib/aspera/cli/plugins/ats.rb +58 -73
  48. data/lib/aspera/cli/plugins/base.rb +190 -240
  49. data/lib/aspera/cli/plugins/basic_auth.rb +2 -10
  50. data/lib/aspera/cli/plugins/config.rb +244 -178
  51. data/lib/aspera/cli/plugins/console.rb +102 -38
  52. data/lib/aspera/cli/plugins/cos.rb +6 -23
  53. data/lib/aspera/cli/plugins/factory.rb +3 -0
  54. data/lib/aspera/cli/plugins/faspex5.rb +176 -173
  55. data/lib/aspera/cli/plugins/faspio.rb +5 -10
  56. data/lib/aspera/cli/plugins/httpgw.rb +8 -11
  57. data/lib/aspera/cli/plugins/mcp.rb +20 -55
  58. data/lib/aspera/cli/plugins/node.rb +277 -311
  59. data/lib/aspera/cli/plugins/orchestrator.rb +90 -77
  60. data/lib/aspera/cli/plugins/preview.rb +79 -90
  61. data/lib/aspera/cli/plugins/server.rb +76 -50
  62. data/lib/aspera/cli/plugins/shares.rb +68 -116
  63. data/lib/aspera/cli/preset_actions.rb +17 -10
  64. data/lib/aspera/cli/preset_manager.rb +12 -2
  65. data/lib/aspera/cli/prompt.rb +35 -0
  66. data/lib/aspera/cli/result.rb +13 -18
  67. data/lib/aspera/cli/runner.rb +31 -54
  68. data/lib/aspera/cli/special_values.rb +5 -0
  69. data/lib/aspera/cli/sync_actions.rb +41 -37
  70. data/lib/aspera/cli/terminal_formatter.rb +9 -3
  71. data/lib/aspera/cli/transfer_actions.rb +0 -6
  72. data/lib/aspera/cli/transfer_agent.rb +29 -35
  73. data/lib/aspera/cli/vault_manager.rb +0 -17
  74. data/lib/aspera/cli/version.rb +1 -1
  75. data/lib/aspera/cli/wizard.rb +4 -2
  76. data/lib/aspera/coverage.rb +1 -0
  77. data/lib/aspera/environment.rb +6 -0
  78. data/lib/aspera/faspex_gw.rb +2 -1
  79. data/lib/aspera/faspex_postproc.rb +1 -0
  80. data/lib/aspera/graphql.rb +5 -5
  81. data/lib/aspera/json_rpc/client.rb +5 -5
  82. data/lib/aspera/keychain/encrypted_hash.rb +1 -1
  83. data/lib/aspera/keychain/one_password_api.rb +1 -1
  84. data/lib/aspera/link_header.rb +2 -2
  85. data/lib/aspera/log.rb +22 -25
  86. data/lib/aspera/markdown.rb +2 -0
  87. data/lib/aspera/mime.rb +25 -0
  88. data/lib/aspera/node_simulator.rb +1 -0
  89. data/lib/aspera/oauth/base.rb +35 -25
  90. data/lib/aspera/oauth/factory.rb +1 -0
  91. data/lib/aspera/oauth/generic.rb +1 -1
  92. data/lib/aspera/oauth/jwt.rb +1 -1
  93. data/lib/aspera/oauth/web.rb +9 -8
  94. data/lib/aspera/preview/file_types.rb +4 -4
  95. data/lib/aspera/preview/generator.rb +7 -0
  96. data/lib/aspera/preview/options.rb +4 -4
  97. data/lib/aspera/preview/terminal.rb +4 -3
  98. data/lib/aspera/preview/utils.rb +9 -6
  99. data/lib/aspera/products/connect.rb +1 -1
  100. data/lib/aspera/rainbow.rb +7 -0
  101. data/lib/aspera/rest/aspera_errors.rb +60 -0
  102. data/lib/aspera/rest/call_error.rb +27 -0
  103. data/lib/aspera/rest/client.rb +514 -0
  104. data/lib/aspera/rest/error_analyzer.rb +113 -0
  105. data/lib/aspera/rest/list.rb +143 -0
  106. data/lib/aspera/rest/parameters.rb +55 -0
  107. data/lib/aspera/rest/util.rb +176 -0
  108. data/lib/aspera/rest.rb +7 -621
  109. data/lib/aspera/schema/IBM Aspera Console-enhanced.yaml +1125 -0
  110. data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +2395 -0
  111. data/lib/aspera/schema/documentation.rb +13 -3
  112. data/lib/aspera/schema/registry.rb +18 -1
  113. data/lib/aspera/schema/validator.rb +92 -0
  114. data/lib/aspera/secret_hider.rb +36 -25
  115. data/lib/aspera/string_ext.rb +15 -0
  116. data/lib/aspera/temp_file_manager.rb +6 -5
  117. data/lib/aspera/transfer/parameters.rb +2 -0
  118. data/lib/aspera/transfer/spec.rb +1 -0
  119. data/lib/aspera/uri_reader.rb +11 -11
  120. data/lib/aspera/web_auth/index.html +147 -0
  121. data/lib/aspera/web_auth/server.rb +81 -0
  122. data.tar.gz.sig +0 -0
  123. metadata +39 -7
  124. metadata.gz.sig +0 -0
  125. data/lib/aspera/colors.rb +0 -79
  126. data/lib/aspera/rest_call_error.rb +0 -25
  127. data/lib/aspera/rest_error_analyzer.rb +0 -111
  128. data/lib/aspera/rest_errors_aspera.rb +0 -58
  129. data/lib/aspera/rest_list.rb +0 -136
  130. data/lib/aspera/web_auth.rb +0 -211
@@ -18,11 +18,13 @@ require 'aspera/assert'
18
18
  require 'aspera/graphql'
19
19
  require 'securerandom'
20
20
  require 'date'
21
+ require 'aspera/rainbow'
22
+ using Rainbow
21
23
 
22
24
  module Aspera
23
25
  module Cli
24
26
  module Plugins
25
- class Aoc < Oauth
27
+ class Aoc < Oauth # rubocop:disable Metrics/ClassLength
26
28
  # default redirect for AoC web auth
27
29
  REDIRECT_LOCALHOST = 'http://localhost:12345'
28
30
  # admin objects that can be manipulated
@@ -70,7 +72,7 @@ module Aspera
70
72
  base_url = "#{base_url}.#{Api::AoC::SAAS_DOMAIN_PROD}" unless base_url.include?('.')
71
73
  # AoC is only https
72
74
  return unless base_url.start_with?('https://')
73
- location = Rest.new(base_url: base_url, redirect_max: 0).call(operation: 'GET', subpath: 'auth/ping', exception: false, ret: :resp)['Location']
75
+ location = Rest::Client.new(base_url: base_url, redirect_max: 0).call(operation: 'GET', subpath: 'auth/ping', exception: false, ret: :resp)['Location']
74
76
  return if location.nil?
75
77
  redirect_uri = URI.parse(location)
76
78
  od = Api::AoC.split_org_domain(URI.parse(base_url))
@@ -145,25 +147,25 @@ module Aspera
145
147
  def register_short_link_commands(base, parent_path)
146
148
  base.commands_under(parent_path) do
147
149
  base.command(
148
- :create, description: 'Create',
149
- arguments: [{name: :custom_data, type: Hash, mandatory: false, default: {}}]
150
+ :create, description: base.operation_description(:create, 'short link'),
151
+ arguments: [{name: :short_link, type: Hash, mandatory: false, default: {}}]
150
152
  )
151
153
  base.command(
152
- :modify, description: 'Modify',
153
- arguments: [{name: :short_link_id, type: :identifier}, {name: :custom_data, type: Hash, mandatory: false, default: {}}]
154
+ :modify, description: base.operation_description(:modify, 'short link'),
155
+ arguments: [{name: :short_link_id, type: :identifier}, {name: :short_link, type: Hash, mandatory: false, default: {}}]
154
156
  )
155
- base.command(:list, description: 'List short links')
157
+ base.command(:list, description: base.operation_description(:list, 'short link'))
156
158
  base.command(
157
- :show, description: 'Show a short link',
159
+ :show, description: base.operation_description(:show, 'short link'),
158
160
  arguments: [{name: :short_link_id, type: :identifier}]
159
161
  )
160
162
  base.command(
161
- :delete, description: 'Delete a short link',
163
+ :delete, description: base.operation_description(:delete, 'short link'),
162
164
  arguments: [{name: :short_link_id, type: :identifier}]
163
165
  )
164
166
  end
165
- base.define_action_method(parent_path + [:create]) do |custom_data: {}, **ctx|
166
- sl_exec_create(custom_data, **ctx)
167
+ base.define_action_method(parent_path + [:create]) do |short_link: {}, **ctx|
168
+ sl_exec_create(short_link, **ctx)
167
169
  end
168
170
  base.define_action_method(parent_path + [:list]) do |**ctx|
169
171
  sl_exec_list(**sl_fetch_list(**ctx))
@@ -174,8 +176,8 @@ module Aspera
174
176
  base.define_action_method(parent_path + [:delete]) do |**ctx|
175
177
  sl_exec_delete(**sl_fetch_list(**ctx), **ctx)
176
178
  end
177
- base.define_action_method(parent_path + [:modify]) do |custom_data: {}, **ctx|
178
- sl_exec_modify(custom_data, **sl_fetch_list(**ctx), **ctx)
179
+ base.define_action_method(parent_path + [:modify]) do |short_link: {}, **ctx|
180
+ sl_exec_modify(short_link, **sl_fetch_list(**ctx), **ctx)
179
181
  end
180
182
  end
181
183
  end
@@ -187,7 +189,7 @@ module Aspera
187
189
  pub_link_info = Api::AoC.link_info(app_url)
188
190
  # public link case
189
191
  if pub_link_info.key?(:token)
190
- pub_api = Rest.new(base_url: "https://#{URI.parse(pub_link_info[:url]).host}/api/v1")
192
+ pub_api = Rest::Client.new(base_url: "https://#{URI.parse(pub_link_info[:url]).host}/api/v1")
191
193
  pub_info = pub_api.read('env/url_token_check', {token: pub_link_info[:token]})
192
194
  preset_value = {
193
195
  link: app_url
@@ -198,8 +200,6 @@ module Aspera
198
200
  test_args: 'organization'
199
201
  }
200
202
  end
201
- options.declare(:use_generic_client, description: 'Wizard: AoC: use global or org specific jwt client id', allowed: Type::BOOLEAN, default: Api::AoC.saas_url?(app_url))
202
- options.parse_options!
203
203
  # make username mandatory for jwt, this triggers interactive input
204
204
  wiz_username = options.get_option(:username, mandatory: true)
205
205
  wizard.check_email(wiz_username)
@@ -216,7 +216,10 @@ module Aspera
216
216
  client_id = options.get_option(:client_id)
217
217
  client_secret = options.get_option(:client_secret)
218
218
  if client_id.nil? || client_secret.nil?
219
- if options.get_option(:use_generic_client)
219
+ use_generic_client = options.get_option(:use_generic_client)
220
+ # Default depends on URL
221
+ use_generic_client = Api::AoC.saas_url?(app_url) if use_generic_client.nil?
222
+ if use_generic_client
220
223
  client_id = client_secret = nil
221
224
  formatter.display_status('Using global client_id.')
222
225
  else
@@ -270,6 +273,7 @@ module Aspera
270
273
  option :new_user_option, description: 'New user creation option for unknown package recipients', allowed: [Hash, NilClass]
271
274
  option :validate_metadata, description: 'Validate shared inbox metadata', allowed: Type::BOOLEAN, default: true
272
275
  option :package_folder, schema: Schema::Registry::PACKAGE_FOLDER_OPTIONS
276
+ option :use_generic_client, description: 'Wizard: AoC: use global or org specific jwt client id (default: global for SaaS URL)', allowed: Type::BOOLEAN
273
277
 
274
278
  use_options Node
275
279
 
@@ -279,7 +283,6 @@ module Aspera
279
283
  @cache_home_node_file = nil
280
284
  @cache_api_aoc = nil
281
285
  @scope = Api::AoC::Scope::USER
282
- options.parse_options!
283
286
  end
284
287
 
285
288
  # Change API scope for subsequent calls, re-instantiate API object
@@ -293,7 +296,7 @@ module Aspera
293
296
 
294
297
  # Create an API object with the options from CLI, but with a different subpath
295
298
  # @param base_path [String] Base path for APIs.
296
- # @return [Api::AoC] API object for AoC (is Rest)
299
+ # @return [Api::AoC] API object for AoC (is Rest::Client)
297
300
  def api_from_options(base_path)
298
301
  # Get all existing OAuth kwargs from `options`.
299
302
  api = Api::AoC.new(
@@ -318,7 +321,7 @@ module Aspera
318
321
  end
319
322
 
320
323
  # AoC Rest object
321
- # @return [Api::AoC] API object for AoC (is Rest)
324
+ # @return [Api::AoC] API object for AoC (is Rest::Client)
322
325
  def aoc_api
323
326
  if @cache_api_aoc.nil?
324
327
  @cache_api_aoc = api_from_options(Api::AoC::API_V1)
@@ -337,8 +340,8 @@ module Aspera
337
340
  # @param name [Boolean] Include name
338
341
  # @return [Hash{Symbol, String => String}] the modified hash containing:
339
342
  # * `workspace_id` [String] the unique identifier.
340
- # * `workspace_name` [String] (optional) the name, included if +name+ is true.
341
- # @note The key type (String or Symbol) depends on the +string+ parameter.
343
+ # * `workspace_name` [String] (optional) the name, included if `name` is true.
344
+ # @note The key type (String or Symbol) depends on the `string` parameter.
342
345
  def workspace_id_hash(hash = nil, string: false, name: false)
343
346
  info = aoc_api.workspace_info
344
347
  hash = {} if hash.nil?
@@ -352,21 +355,6 @@ module Aspera
352
355
  return hash
353
356
  end
354
357
 
355
- # Get resource identifier from command line, either directly specifying the `id` or from `name` (percent selector).
356
- # @param resource_class_path [String] url path for resource
357
- # @return [String] identifier
358
- def get_resource_id_from_args(resource_class_path)
359
- return options.instance_identifier do |field, value|
360
- Aspera.assert_values(field, ['name'], type: BadArgument) { 'selector field' }
361
- aoc_api.lookup_with_q(resource_class_path, value: value)['id']
362
- end
363
- end
364
-
365
- # Get resource path from command line
366
- def get_resource_path_from_args(resource_class_path)
367
- return "#{resource_class_path}/#{get_resource_id_from_args(resource_class_path)}"
368
- end
369
-
370
358
  # List all entities, given additional, default and user's queries
371
359
  # @param resource_class_path [String] path to query on API
372
360
  # @param fields [Array, nil] fields to display
@@ -409,87 +397,98 @@ module Aspera
409
397
  PACKAGE_RECEIVED_BASE_QUERY.each { |k, v| query[k] = v unless query.key?(k) }
410
398
  resolve_dropbox_name_default_ws_id(query)
411
399
  # Extract `max` before paging so callers can apply it after post-API filtering
412
- max_items = query.delete(RestList::MAX_ITEMS)&.to_i
400
+ max_items = query.delete(Rest::List::MAX_ITEMS)&.to_i
413
401
  return aoc_api.read_with_paging('packages', query.compact), max_items
414
402
  end
415
403
 
416
- FILES_COMMANDS = (Node::COMMANDS_GEN4 + %i[transfer]).freeze
417
-
418
- # Execute a node gen4 command starting at given node and file IDs.
419
- # Arguments already resolved by the DSL (e.g. path:) are forwarded via +resolved_args+
420
- # and injected into the dispatch context so node.rb does not re-consume them from the CLI.
421
- # @param command_repo [Symbol] Command to execute (from Node::COMMANDS_GEN4 or :transfer)
422
- # @param node_id [String] Node identifier
423
- # @param file_id [String, nil] Root file id; if nil, the AK root file id is used
424
- # @param scope [String, nil] node scope (Node::Scope::USER/ADMIN), or nil (requires secret)
425
- # @param transfer_direction [Symbol, nil] :push or :pull (only for command_repo == :transfer)
426
- # @param transfer_source [String, nil] source folder (only for command_repo == :transfer)
427
- # @param resolved_args [Hash] already-resolved CLI arguments (e.g. path:) forwarded to dispatch
428
- def execute_nodegen4_command(command_repo, node_id, file_id: nil, scope: nil, transfer_direction: nil, transfer_source: nil, **resolved_args)
429
- top_node_api = aoc_api.node_api_from(
404
+ # Arguments of the node-to-node `transfer` command (files, admin node do, shared folder node)
405
+ TRANSFER_ARGS = [{name: :direction, allowed: %i[push pull]}, {name: :source_folder}].freeze
406
+ # Mount of the Node plugin Gen4 commands (`node access_keys do <id> ...`), instance: set per mount point
407
+ NODE_GEN4_MOUNT = {plugin: Node, at: %i[access_keys do]}.freeze
408
+ # Package identifier argument, `%name:` selector (lookup method shared with `admin package`)
409
+ PACKAGE_ID_ARG = {name: :package_id, type: :identifier, lookup: :lookup_aoc_package_id}.freeze
410
+ # Resource of analytics events (`admin analytics transfers|files`)
411
+ EVENT_RESOURCE_TYPE_ARG = {name: :event_resource_type, allowed: %i[organizations users nodes]}.freeze
412
+ EVENT_RESOURCE_ID_ARG = {name: :event_resource_id, description: 'Resource identifier, empty for the one of the current user'}.freeze
413
+ private_constant :TRANSFER_ARGS, :NODE_GEN4_MOUNT, :PACKAGE_ID_ARG, :EVENT_RESOURCE_TYPE_ARG, :EVENT_RESOURCE_ID_ARG
414
+
415
+ # Node API on a Gen4 node, and its root file id.
416
+ # @param node_id [String] Node identifier
417
+ # @param file_id [String, nil] Root file id; if nil, the access key root file id is used
418
+ # @param scope [String, nil] node scope (Api::Node::Scope::USER/ADMIN), or nil (requires secret)
419
+ # @return [Array(Api::Node, String)]
420
+ def nodegen4_root(node_id, file_id: nil, scope: nil)
421
+ node_api = aoc_api.node_api_from(
430
422
  node_id: node_id,
431
423
  scope: scope,
432
424
  **workspace_id_hash(name: true)
433
425
  )
434
- file_id = top_node_api.read("access_keys/#{top_node_api.app_info.node_info['access_key']}")['root_file_id'] if file_id.nil?
435
- node_plugin = Node.new(context: context, api: top_node_api)
436
- case command_repo
437
- when *Node::COMMANDS_GEN4
438
- # For permission: the handler consumes the path first then re-dispatches to sub-commands.
439
- # Calling dispatch_from_registry with skip_setup would bypass path consumption and fail.
440
- return node_plugin.send(:"action_access_keys_do_#{command_repo}", do_root_file_id: file_id, **resolved_args) if command_repo.eql?(:permission)
441
- return node_plugin.dispatch_from_registry([:access_keys, :do, command_repo], {do_root_file_id: file_id, **resolved_args}, skip_setup: true)
442
- when :transfer
443
- # client side is agent
444
- # server side is transfer server
445
- # in same workspace
446
- push_pull = transfer_direction
447
- source_folder = transfer_source
448
- case push_pull
449
- when :push
450
- client_direction = Transfer::Spec::DIRECTION_SEND
451
- client_folder = source_folder
452
- server_folder = transfer.destination_folder(client_direction)
453
- when :pull
454
- client_direction = Transfer::Spec::DIRECTION_RECEIVE
455
- client_folder = transfer.destination_folder(client_direction)
456
- server_folder = source_folder
457
- else Aspera.error_unreachable_line
458
- end
459
- client_apifid = top_node_api.resolve_api_fid(file_id, client_folder)
460
- server_apifid = top_node_api.resolve_api_fid(file_id, server_folder)
461
- # force node as transfer agent
462
- transfer.agent_instance = Agent::Node.new(
463
- url: client_apifid.node_api.base_url,
464
- username: client_apifid.node_api.app_info.node_info['access_key'],
465
- password: client_apifid.node_api.oauth.authorization,
466
- root_id: client_apifid.file_id
467
- )
468
- # additional node to node TS info
469
- add_ts = {
470
- 'remote_access_key' => server_apifid.node_api.app_info.node_info['access_key'],
471
- 'destination_root_id' => server_apifid.file_id,
472
- 'source_root_id' => client_apifid.file_id
473
- }
474
- return Runner.result_transfer(transfer.start(server_apifid.node_api.transfer_spec_gen4(
475
- server_apifid.file_id,
476
- client_direction,
477
- add_ts
478
- )))
479
- else Aspera.error_unexpected_value(command_repo) { 'command' }
480
- end
481
- Aspera.error_unreachable_line
426
+ file_id = node_api.read("access_keys/#{node_api.app_info.node_info['access_key']}")['root_file_id'] if file_id.nil?
427
+ [node_api, file_id]
428
+ end
429
+
430
+ # Node plugin for Gen4 commands on a node, with the seed ctx of its `access_keys do` sub-tree.
431
+ # Used as `instance:` of mounts of NODE_GEN4_MOUNT.
432
+ # @return [Array(Node, Hash)]
433
+ def nodegen4_plugin(node_id, file_id: nil, scope: nil)
434
+ node_api, file_id = nodegen4_root(node_id, file_id: file_id, scope: scope)
435
+ [Node.new(context: context, api: node_api), {do_root_file_id: file_id}]
436
+ end
437
+
438
+ # Node-to-node transfer: client side is agent, server side is transfer server, in same workspace.
439
+ # @param direction [Symbol] :push or :pull
440
+ # @param source_folder [String] source folder
441
+ # @return [Result]
442
+ def nodegen4_transfer(node_id, direction:, source_folder:, file_id: nil, scope: nil)
443
+ top_node_api, file_id = nodegen4_root(node_id, file_id: file_id, scope: scope)
444
+ case direction
445
+ when :push
446
+ client_direction = Transfer::Spec::DIRECTION_SEND
447
+ client_folder = source_folder
448
+ server_folder = transfer.destination_folder(client_direction)
449
+ when :pull
450
+ client_direction = Transfer::Spec::DIRECTION_RECEIVE
451
+ client_folder = transfer.destination_folder(client_direction)
452
+ server_folder = source_folder
453
+ else Aspera.error_unexpected_value(direction) { 'direction' }
454
+ end
455
+ client_apifid = top_node_api.resolve_api_fid(file_id, client_folder)
456
+ server_apifid = top_node_api.resolve_api_fid(file_id, server_folder)
457
+ # force node as transfer agent
458
+ transfer.agent_instance = Agent::Node.new(
459
+ url: client_apifid.node_api.base_url,
460
+ username: client_apifid.node_api.app_info.node_info['access_key'],
461
+ password: client_apifid.node_api.oauth.authorization,
462
+ root_id: client_apifid.file_id
463
+ )
464
+ # additional node to node TS info
465
+ add_ts = {
466
+ 'remote_access_key' => server_apifid.node_api.app_info.node_info['access_key'],
467
+ 'destination_root_id' => server_apifid.file_id,
468
+ 'source_root_id' => client_apifid.file_id
469
+ }
470
+ Runner.result_transfer(transfer.start(server_apifid.node_api.transfer_spec_gen4(
471
+ server_apifid.file_id,
472
+ client_direction,
473
+ add_ts
474
+ )))
482
475
  end
483
476
 
484
477
  # Execute an action on admin resources
485
478
  # @param resource_type [Symbol] One of ADMIN_OBJECTS
486
479
  # Per-resource configuration for admin CRUD resources.
487
- # Keys: path, list_fields, id_result, require_ws_id, create_schema, extra_ops, singleton, op_setup
480
+ # Keys: path, list_fields, id_result, require_ws_id, create_schema, extra_ops, singleton, op_setup, op_mount, op_descriptions
488
481
  # op_setup: Hash of op => setup method name, used for ops that require consuming an instance identifier.
489
482
  # For Operations::INSTANCE ops (show/modify/delete), use the auto-generated :setup_admin_<res>_instance.
490
483
  # For extra_ops that are instance ops, specify explicitly (or rely on the auto-generated one).
484
+ # op_mount: Hash of op => mount: of that op's node.
485
+ # op_descriptions: Hash of op => description, for ops other than standard ones.
491
486
  ADMIN_OBJECT_CONFIG = {
492
- client: {extra_ops: %i[set_pub_key], extra_op_args: {set_pub_key: [{name: :private_key_pem, type: String}]}},
487
+ client: {
488
+ extra_ops: %i[set_pub_key],
489
+ extra_op_args: {set_pub_key: [{name: :private_key_pem}]},
490
+ op_descriptions: {set_pub_key: 'Set public key of client from a private key'}
491
+ },
493
492
  client_access_key: {path: 'admin/client_access_keys'},
494
493
  client_registration_token: {path: 'admin/client_registration_tokens', list_fields: %w[id value data.client_subject_scopes data.name created_at], id_result: 'token'},
495
494
  configuration_policy: {list_fields: nil},
@@ -500,15 +499,30 @@ module Aspera
500
499
  group_membership: {list_fields: %w[id group_id member_type member_id], create_schema: false},
501
500
  kms_profile: {path: 'integrations/kms_profiles', create_schema: false},
502
501
  network_policy: {list_fields: nil},
503
- node: {list_fields: %w[id name host access_key], extra_ops: %i[do bearer_token update_status], extra_op_args: {bearer_token: [{name: :scope, mandatory: false, default: nil}]}},
502
+ node: {
503
+ list_fields: %w[id name host access_key],
504
+ extra_ops: %i[do bearer_token update_status],
505
+ extra_op_args: {bearer_token: [{name: :scope, mandatory: false, default: nil}]},
506
+ op_mount: {do: NODE_GEN4_MOUNT.merge(instance: :admin_node_do_plugin)},
507
+ op_descriptions: {do: 'Execute command on node', bearer_token: 'Generate bearer token for node', update_status: 'Ask AoC to scan node and update its status'}
508
+ },
504
509
  operation: {list_fields: %w[id type status created_at updated_at workspace_id user_id workspace_membership_id group_membership_id], ops: %i[list show modify]},
505
510
  organization: {singleton: true},
506
511
  package: {},
507
512
  saml_configuration: {create_schema: false},
508
- self: {singleton: true},
513
+ self: {singleton: true, op_descriptions: {show: 'Show current user'}},
509
514
  short_link: {list_fields: %w[id short_url data.url_token_data.purpose password_enabled password_protected updated_by_user_id updated_at]},
510
- user: {list_fields: %w[id name email], extra_ops: %i[preferences notifications], op_setup: {preferences: :setup_admin_user_instance, notifications: :setup_admin_user_instance}},
511
- workspace: {extra_ops: %i[shared_folder dropbox], op_setup: {shared_folder: :setup_admin_workspace_shared_folder, dropbox: :setup_admin_workspace_dropbox}},
515
+ user: {
516
+ list_fields: %w[id name email],
517
+ extra_ops: %i[preferences notifications],
518
+ op_setup: {preferences: :setup_admin_user_instance, notifications: :setup_admin_user_instance},
519
+ op_descriptions: {preferences: 'Manage user preferences', notifications: 'Manage user notification preferences'}
520
+ },
521
+ workspace: {
522
+ extra_ops: %i[shared_folder dropbox],
523
+ op_setup: {shared_folder: :setup_admin_workspace_shared_folder, dropbox: :setup_admin_workspace_dropbox},
524
+ op_descriptions: {shared_folder: 'Manage shared folders of workspace', dropbox: 'Manage shared inboxes of workspace'}
525
+ },
512
526
  workspace_membership: {list_fields: %w[id workspace_id member_type member_id]}
513
527
  }.freeze
514
528
  private_constant :ADMIN_OBJECT_CONFIG
@@ -524,14 +538,14 @@ module Aspera
524
538
 
525
539
  # Build analytics REST API (shared by action_admin_analytics_*)
526
540
  def build_analytics_api
527
- Rest.new(**aoc_api.params.deep_merge({
541
+ Rest::Client.new(**aoc_api.params.deep_merge({
528
542
  base_url: "#{aoc_api.base_url.gsub('/api/v1', '')}/analytics/v2",
529
543
  auth: {params: {scope: Api::AoC::Scope::ADMIN_USER}}
530
544
  }))
531
545
  end
532
546
 
533
547
  # Compute short-link purposes from shared_data keys and link_type.
534
- # @param shared_data [Hash] :dropbox_id+:name or :file_id+:node_id
548
+ # @param shared_data [Hash] :dropbox_id + :name or :file_id + :node_id
535
549
  # @param link_type [Symbol] :public or :private
536
550
  # @return [Array(String,String)] [token_purpose, short_link_purpose]
537
551
  def short_link_purposes(shared_data, link_type)
@@ -591,35 +605,39 @@ module Aspera
591
605
  command :reminder, description: 'Send reminder email with list of orgs'
592
606
  command(
593
607
  :servers, description: 'List AoC servers (no auth)',
594
- action: lambda do
608
+ action: lambda do |**|
595
609
  no_auth_api = Api::AoC.new(url: options.get_option(:url), auth: :none)
596
610
  Result::ObjectList.new(no_auth_api.read('servers'))
597
611
  end
598
612
  )
599
- command :bearer_token, description: 'Display bearer token',
600
- action: -> { Result::Text.new(aoc_api.oauth.authorization) }
613
+ command :bearer_token, description: 'Show bearer token',
614
+ action: ->(**) { Result::Text.new(aoc_api.oauth.authorization) }
601
615
  command :organization, description: 'Show organization info',
602
- action: -> { Result::SingleObject.new(aoc_api.read('organization')) }
616
+ action: ->(**) { Result::SingleObject.new(aoc_api.read('organization')) }
603
617
  command :tier_restrictions, description: 'Show tier restrictions',
604
- action: -> { Result::SingleObject.new(aoc_api.read('tier_restrictions')) }
605
- command :user, description: 'User commands'
606
- command :packages, description: 'Package commands', setup: :setup_workspace_display
607
- command :files, description: 'Files commands (workspace-aware)', setup: :setup_workspace_display
608
- command :admin, description: 'Administration commands', setup: :setup_admin_scope
618
+ action: ->(**) { Result::SingleObject.new(aoc_api.read('tier_restrictions')) }
619
+ command :user, description: 'Manage current user'
620
+ # Node Gen4 read-only commands on packages: `packages <command> <package_id> ...`
621
+ command :packages, description: 'Manage packages', setup: :setup_workspace_display,
622
+ mount: NODE_GEN4_MOUNT.merge(instance: :package_node_plugin, only: Node::NODE4_READ_ACTIONS, arguments: [PACKAGE_ID_ARG])
623
+ command :files, description: 'Manage files (workspace-aware)', setup: :setup_workspace_display,
624
+ mount: NODE_GEN4_MOUNT.merge(instance: :files_node_plugin)
625
+ command :admin, description: 'Administer AoC', setup: :setup_admin_scope
609
626
  commands_under :admin do
610
627
  command :bearer_token, description: 'Show admin bearer token',
611
- action: -> { Result::Text.new(aoc_api.oauth.authorization) }
628
+ action: ->(**) { Result::Text.new(aoc_api.oauth.authorization) }
612
629
  command :application, description: 'Manage applications'
613
630
  command :ats, description: 'Manage ATS (Aspera Transfer Service)',
614
- delegate_instance: :build_ats_plugin, delegates_to: []
631
+ mount: {plugin: Ats, instance: :build_ats_plugin}
615
632
  command :usage_reports, description: 'List usage reports',
616
- action: -> { result_list('usage_reports', base_query: workspace_id_hash) }
633
+ action: ->(**) { result_list('usage_reports', base_query: workspace_id_hash) }
617
634
  command :auth_providers, description: 'Manage auth providers'
618
635
  command :subscription, description: 'Show subscription info'
619
636
  command :analytics, description: 'Query analytics'
620
637
  ADMIN_OBJECTS.each do |res|
621
638
  cfg = ADMIN_OBJECT_CONFIG.fetch(res, {})
622
639
  op_setup = cfg[:op_setup] || {}
640
+ op_mount = cfg[:op_mount] || {}
623
641
  extra_op_args = cfg[:extra_op_args] || {}
624
642
  is_singleton = cfg[:singleton]
625
643
  id_arg_spec = is_singleton ? [] : [{name: :"#{res}_id", type: :identifier, lookup: :"lookup_aoc_#{res}_id"}]
@@ -630,18 +648,19 @@ module Aspera
630
648
  else
631
649
  Operations::ALL + (cfg[:extra_ops] || [])
632
650
  end
633
- command res, description: "Manage #{res.to_s.tr('_', ' ')}"
651
+ command res, description: "Manage #{entity_noun(res, singular: false)}"
634
652
  commands_under res do
635
653
  ops.each do |op|
636
654
  extra_setup = op_setup[op]
637
655
  base_attrs = extra_setup ? {setup: extra_setup} : {}
656
+ base_attrs[:mount] = op_mount[op] if op_mount.key?(op)
638
657
  extra_arg_list =
639
658
  if !is_singleton && op.eql?(:create)
640
659
  c = aoc_res_cfg(res)
641
- [{name: :data, type: Hash, bulk: true, schema: c[:schema]}]
660
+ [{name: res, type: Hash, bulk: true, schema: c[:schema]}]
642
661
  elsif !is_singleton && op.eql?(:modify)
643
662
  c = aoc_res_cfg(res)
644
- [{name: :data, type: Hash, schema: c[:schema]}]
663
+ [{name: res, type: Hash, schema: c[:schema]}]
645
664
  elsif extra_op_args.key?(op)
646
665
  extra_op_args[op]
647
666
  else
@@ -655,62 +674,71 @@ module Aspera
655
674
  c = aoc_res_cfg(res)
656
675
  merged = merged.merge(query_schema: Schema::Registry.query_params(c[:query_component], c[:path]))
657
676
  end
658
- command op, description: op.to_s.tr('_', ' ').capitalize, **merged
677
+ description = cfg.dig(:op_descriptions, op) ||
678
+ (Operations::ALL.include?(op) ? operation_description(op, entity_noun(res, singular: false)) : op.to_s.tr('_', ' ').capitalize)
679
+ command op, description: description, **merged
659
680
  end
660
681
  end
661
682
  end
662
683
  end
663
684
  # admin > workspace > shared_folder sub-tree
664
685
  commands_under %i[admin workspace shared_folder] do
665
- command :list, description: 'List shared folders'
686
+ command :list, description: 'List shared folders',
687
+ action: ->(shared_folders:, **) { Result::ObjectList.new(shared_folders, fields: %w[id node_name node_id file_id file.path tags.aspera.files.workspace.share_as]) }
666
688
  command :node, description: 'Execute node command on shared folder',
667
- arguments: [{name: :sf_id, type: :identifier}],
668
- setup: :setup_admin_workspace_shared_folder_node
689
+ arguments: [{name: :shared_folder_id, type: :identifier}],
690
+ setup: :setup_admin_workspace_shared_folder_node,
691
+ mount: NODE_GEN4_MOUNT.merge(instance: :admin_workspace_shared_folder_node_plugin)
669
692
  command :member, description: 'Show folder members',
670
- arguments: [{name: :sf_id, type: :identifier}],
693
+ arguments: [{name: :shared_folder_id, type: :identifier}],
671
694
  setup: :setup_admin_workspace_shared_folder_member
672
695
  end
673
- # admin > workspace > shared_folder > node sub-tree (Gen4 commands)
696
+ # admin > workspace > shared_folder > node: Gen4 commands are mounted, plus node-to-node transfer
674
697
  commands_under %i[admin workspace shared_folder node] do
675
- command :transfer, description: 'Transfer files (node-to-node)'
676
- command :permission, description: 'Manage permissions'
677
- command :sync, description: 'Synchronize folders'
678
- Node::COMMANDS_GEN4_SPEC.each do |cmd, spec|
679
- command cmd, **spec
680
- end
698
+ command :transfer, description: 'Transfer files (node-to-node)', arguments: TRANSFER_ARGS,
699
+ action: ->(sf_item:, direction:, source_folder:, **) { nodegen4_transfer(sf_item['node_id'], file_id: sf_item['file_id'], scope: Api::Node::Scope::ADMIN, direction: direction, source_folder: source_folder) }
681
700
  end
682
701
  commands_under %i[admin workspace shared_folder member] do
683
702
  command :list, description: 'List members of a shared folder'
684
703
  end
685
704
  # admin > workspace > dropbox sub-tree
686
705
  commands_under %i[admin workspace dropbox] do
687
- command :list, description: 'List dropboxes in workspace'
706
+ command(
707
+ :list, description: 'List dropboxes in workspace',
708
+ action: lambda do |ws_res_id:, **|
709
+ query = query_read_delete(default: {}, schema: Schema::Registry.query_params(Schema::Registry::AOC, 'dropboxes'))
710
+ Result::ObjectList.new(aoc_api.read('dropboxes', query.merge({'workspace_id' => ws_res_id})), fields: %w[id name description])
711
+ end
712
+ )
688
713
  end
689
- # admin > node > do sub-tree (Gen4 commands)
714
+ # admin > node > do: Gen4 commands are mounted (op_mount), plus node-to-node transfer
690
715
  commands_under %i[admin node do] do
691
- command :transfer, description: 'Transfer files (node-to-node)'
692
- command :permission, description: 'Manage permissions'
693
- command :sync, description: 'Synchronize folders'
694
- Node::COMMANDS_GEN4_SPEC.each do |cmd, spec|
695
- command cmd, **spec
696
- end
716
+ command :transfer, description: 'Transfer files (node-to-node)', arguments: TRANSFER_ARGS,
717
+ action: ->(node_id:, direction:, source_folder:, **) { nodegen4_transfer(node_id, scope: Api::Node::Scope::ADMIN, direction: direction, source_folder: source_folder) }
697
718
  end
698
719
  # admin > user > preferences|notifications sub-trees
699
720
  %i[preferences notifications].each do |pref|
700
- commands_under([:admin, :user, pref]) do
701
- command :show, description: "Show #{pref}"
702
- command :modify, description: "Modify #{pref}",
703
- arguments: [{name: :properties, type: Hash}]
721
+ commands_under [:admin, :user, pref] do
722
+ command :show, description: "Show user #{pref}"
723
+ command :modify, description: "Modify user #{pref}",
724
+ arguments: [{name: pref, type: Hash}]
704
725
  end
705
726
  end
706
727
  commands_under %i[admin auth_providers] do
707
728
  command :list, description: 'List auth providers',
708
- action: -> { result_list('admin/auth_providers') }
709
- command :update, description: 'Update auth provider', action: -> { Aspera.error_not_implemented }
729
+ action: ->(**) { result_list('admin/auth_providers') }
730
+ command :update, description: 'Update auth provider', action: ->(**) { Aspera.error_not_implemented }
710
731
  end
711
732
  commands_under %i[admin subscription] do
712
- command :account, description: 'Show subscription account'
713
- command :usage, description: 'Show subscription usage',
733
+ command(
734
+ :account, description: 'Show subscription account',
735
+ action: lambda do |**|
736
+ org = aoc_api.read('organization')
737
+ result = GraphQL.execute(api_from_options('bss/platform/graphql'), 'bss_subscription_account', {organization_id: org['id']})
738
+ Result::SingleObject.new(result['aoc']['bssSubscription'])
739
+ end
740
+ )
741
+ command :usage, description: 'Show subscription usage',
714
742
  arguments: [
715
743
  {name: :aggregate, mandatory: false, default: :ALL, allowed: %i[ALL MONTHLY]},
716
744
  {name: :start_date, mandatory: false, default: nil},
@@ -718,23 +746,29 @@ module Aspera
718
746
  ]
719
747
  end
720
748
  commands_under %i[admin analytics] do
721
- command :application_events, description: 'List application events'
749
+ command(
750
+ :application_events, description: 'List application events',
751
+ action: lambda do |**|
752
+ events = build_analytics_api.read("organizations/#{aoc_api.current_user_info['organization_id']}/application_events")['application_events']
753
+ Result::ObjectList.new(events)
754
+ end
755
+ )
722
756
  command :transfers, description: 'List transfer events',
723
757
  arguments: [
724
- {name: :event_resource_type, mandatory: true, allowed: %i[organizations users nodes]},
725
- {name: :event_resource_id, mandatory: false, default: nil}
758
+ EVENT_RESOURCE_TYPE_ARG,
759
+ EVENT_RESOURCE_ID_ARG.merge(mandatory: false, default: nil)
726
760
  ]
727
761
  command :files, description: 'List file events',
728
762
  arguments: [
729
- {name: :event_resource_type, mandatory: true, allowed: %i[organizations users nodes]},
730
- {name: :event_resource_id, mandatory: true},
731
- {name: :event_uuid, mandatory: true}
763
+ EVENT_RESOURCE_TYPE_ARG,
764
+ EVENT_RESOURCE_ID_ARG,
765
+ {name: :event_uuid, type: :identifier}
732
766
  ]
733
767
  end
734
768
  # application sub-commands
735
769
  commands_under %i[admin application] do
736
770
  command :types, description: 'List application types',
737
- action: -> { Result::ObjectList.new(aoc_api.read('admin/apps')) }
771
+ action: ->(**) { Result::ObjectList.new(aoc_api.read('admin/apps')) }
738
772
  command :settings, description: 'Manage per-app-type settings'
739
773
  command :instance, description: 'Manage app instances'
740
774
  command :membership, description: 'Manage app memberships'
@@ -744,15 +778,15 @@ module Aspera
744
778
  private_constant :APP_SETTINGS_PATH, :APP_INSTANCE_PATH
745
779
  commands_under APP_SETTINGS_PATH do
746
780
  APP_TYPES.each do |app_type|
747
- command app_type, description: "Settings for #{app_type} app"
781
+ command app_type, description: "Manage settings of #{app_type} app"
748
782
  commands_under app_type do
749
783
  command :show, description: "Show #{app_type} settings",
750
- action: -> { Result::SingleObject.new(aoc_api.read("/apps/#{app_type}/settings")) }
784
+ action: ->(**) { Result::SingleObject.new(aoc_api.read("/apps/#{app_type}/settings")) }
751
785
  command(
752
786
  :modify, description: "Modify #{app_type} settings",
753
- arguments: [{name: :properties, type: Hash}],
754
- action: lambda do |properties:, **|
755
- aoc_api.update("/apps/#{app_type}/settings", properties)
787
+ arguments: [{name: :settings, type: Hash}],
788
+ action: lambda do |settings:, **|
789
+ aoc_api.update("/apps/#{app_type}/settings", settings)
756
790
  Result::Status.new('modified')
757
791
  end
758
792
  )
@@ -760,57 +794,63 @@ module Aspera
760
794
  end
761
795
  end
762
796
  commands_under APP_INSTANCE_PATH do
763
- command(:list, description: 'List app instances', action: lambda do
764
- result_list(
765
- 'admin/apps_new',
766
- fields: %w[id app_type available workspace_id],
767
- default_query: {workspace_id: aoc_api.workspace_info[:id]}
768
- )
769
- end)
797
+ command :list, description: 'List app instances'
770
798
  APP_TYPES.each do |app_type|
771
- command app_type, description: "Show or modify a #{app_type} instance"
799
+ command app_type, description: "Manage #{app_type} app instance"
772
800
  commands_under app_type do
773
- command :show, description: "Show a #{app_type} instance",
774
- arguments: [{name: :"#{app_type}_id", type: :identifier}]
775
- command :modify, description: "Modify a #{app_type} instance",
776
- arguments: [{name: :"#{app_type}_id", type: :identifier}, {name: :properties, type: Hash}]
801
+ command :show, description: "Show #{app_type} instance",
802
+ arguments: [{name: :instance_id, type: :identifier}]
803
+ command :modify, description: "Modify #{app_type} instance",
804
+ arguments: [{name: :instance_id, type: :identifier}, {name: :instance, type: Hash}]
777
805
  end
778
806
  end
779
807
  end
780
808
  commands_under %i[admin application membership] do
781
- command :list, description: 'List app memberships', action: -> { result_list('apps/app_memberships') }
782
- command :show, description: 'Show an app membership', arguments: [{name: :membership_id, type: :identifier}]
783
- command :delete, description: 'Delete an app membership', arguments: [{name: :membership_id, type: :identifier}]
809
+ command :list, description: 'List app memberships', action: ->(**) { result_list('apps/app_memberships') }
810
+ command :show, description: 'Show an app membership', arguments: [{name: :membership_id, type: :identifier}],
811
+ action: ->(membership_id:, **) { Result::SingleObject.new(aoc_api.read("apps/app_memberships/#{membership_id}", query_read_delete)) }
812
+ command(
813
+ :delete, description: 'Delete an app membership', arguments: [{name: :membership_id, type: :identifier}],
814
+ action: lambda do |membership_id:, **|
815
+ aoc_api.delete("apps/app_memberships/#{membership_id}")
816
+ Result::Status.new('deleted')
817
+ end
818
+ )
784
819
  command :create, description: 'Create an app membership', arguments: [{name: :membership, type: Hash}]
785
820
  end
786
- command :automation, description: 'Automation commands (BETA)', setup: :setup_automation_api
821
+ command :automation, description: 'Manage automation (BETA)', setup: :setup_automation_api
787
822
  command :gateway, description: 'Start AoC Faspex4 gateway',
788
823
  arguments: [{name: :parameters, type: Hash, mandatory: false, default: {}}]
789
824
 
790
825
  # user sub-commands
791
826
  commands_under :user do
792
- commands_under :workspaces, description: "User's workspaces" do
793
- command :list, description: 'List workspaces', action: -> { result_list('workspaces', fields: %w[id name]) }
794
- command :current, description: 'Show current workspace', action: -> { Result::SingleObject.new(aoc_api.workspace_info) }
827
+ commands_under :workspaces, description: "Manage user's workspaces" do
828
+ command :list, description: 'List workspaces', action: ->(**) { result_list('workspaces', fields: %w[id name]) }
829
+ command :current, description: 'Show current workspace', action: ->(**) { Result::SingleObject.new(aoc_api.workspace_info) }
795
830
  end
796
- # command :profile, description: 'User profile commands'
797
- commands_under :profile, description: "Manager user's profile" do
798
- command :show, description: 'Show user profile', action: -> { Result::SingleObject.new(aoc_api.current_user_info(exception: true)) }
831
+ commands_under :profile, description: "Manage user's profile" do
832
+ command :show, description: 'Show user profile', action: ->(**) { Result::SingleObject.new(aoc_api.current_user_info(exception: true)) }
799
833
  command(
800
834
  :modify, description: 'Modify user profile',
801
- arguments: [{name: :properties, type: Hash}],
802
- action: lambda do |properties:, **|
803
- aoc_api.update("users/#{aoc_api.current_user_info(exception: true)['id']}", properties)
835
+ arguments: [{name: :profile, type: Hash}],
836
+ action: lambda do |profile:, **|
837
+ aoc_api.update("users/#{aoc_api.current_user_info(exception: true)['id']}", profile)
804
838
  Result::Status.new('modified')
805
839
  end
806
840
  )
807
841
  end
808
- command :preferences, description: 'User interaction preferences'
809
- command :notifications, description: 'Notification preferences'
842
+ command :preferences, description: 'Manage interaction preferences'
843
+ command :notifications, description: 'Manage notification preferences'
810
844
  command :contacts, description: 'Manage contacts'
811
845
  # user > contacts sub-commands (same CRUD as admin > contact)
812
846
  commands_under %i[contacts] do
813
- Operations::ALL.each { |op| command op, description: op.to_s.capitalize }
847
+ contact_id = {name: :contact_id, type: :identifier, lookup: :lookup_aoc_contact_id}
848
+ contact_schema = aoc_res_cfg(:contact)[:schema]
849
+ command :list, description: operation_description(:list, 'contact')
850
+ command :show, description: operation_description(:show, 'contact'), arguments: [contact_id]
851
+ command :create, description: operation_description(:create, 'contact'), arguments: [{name: :contact, type: Hash, bulk: true, schema: contact_schema}]
852
+ command :modify, description: operation_description(:modify, 'contact'), arguments: [contact_id, {name: :contact, type: Hash, schema: contact_schema}]
853
+ command :delete, description: operation_description(:delete, 'contact'), arguments: [contact_id]
814
854
  end
815
855
  command :settings, description: 'Manage client settings'
816
856
  commands_under %i[settings] do
@@ -821,17 +861,17 @@ module Aspera
821
861
  commands_under %i[user preferences] do
822
862
  command(
823
863
  :show, description: 'Show user preferences',
824
- action: lambda do
864
+ action: lambda do |**|
825
865
  user_id = aoc_api.current_user_info(exception: true)['id']
826
866
  Result::SingleObject.new(aoc_api.read("users/#{user_id}/user_interaction_preferences"))
827
867
  end
828
868
  )
829
869
  command(
830
870
  :modify, description: 'Modify user preferences',
831
- arguments: [{name: :properties, type: Hash}],
832
- action: lambda do |properties:, **|
871
+ arguments: [{name: :preferences, type: Hash}],
872
+ action: lambda do |preferences:, **|
833
873
  user_id = aoc_api.current_user_info(exception: true)['id']
834
- aoc_api.update("users/#{user_id}/user_interaction_preferences", properties)
874
+ aoc_api.update("users/#{user_id}/user_interaction_preferences", preferences)
835
875
  Result::Status.new('modified')
836
876
  end
837
877
  )
@@ -840,100 +880,146 @@ module Aspera
840
880
  commands_under %i[user notifications] do
841
881
  command(
842
882
  :show, description: 'Show notification preferences',
843
- action: lambda do
883
+ action: lambda do |**|
844
884
  user_id = aoc_api.current_user_info(exception: true)['id']
845
885
  Result::SingleObject.new(aoc_api.read("users/#{user_id}/notification_preferences"))
846
886
  end
847
887
  )
848
888
  command(
849
889
  :modify, description: 'Modify notification preferences',
850
- arguments: [{name: :properties, type: Hash}],
851
- action: lambda do |properties:, **|
890
+ arguments: [{name: :notifications, type: Hash}],
891
+ action: lambda do |notifications:, **|
852
892
  user_id = aoc_api.current_user_info(exception: true)['id']
853
- aoc_api.update("users/#{user_id}/notification_preferences", properties)
893
+ aoc_api.update("users/#{user_id}/notification_preferences", notifications)
854
894
  Result::Status.new('modified')
855
895
  end
856
896
  )
857
897
  end
858
898
 
859
899
  # packages sub-commands — instance commands consume package_id
900
+ # Not `crud_commands`: `list` has its own query and `once_only` persistency.
860
901
  commands_under :packages do
861
- command :shared_inboxes, description: 'Shared inbox commands'
902
+ command :shared_inboxes, description: 'Manage shared inboxes'
862
903
  command :send, description: 'Send a package', transfer_paths: :send,
863
- arguments: [{name: :data, type: Hash, schema: Schema::Registry.req_body(Schema::Registry::AOC, 'packages.post')}]
904
+ arguments: [{name: :package, type: Hash, schema: Schema::Registry.req_body(Schema::Registry::AOC, 'packages.post')}]
864
905
  command :receive, description: 'Receive packages', aliases: [:recv], transfer_paths: :receive,
865
- arguments: [{name: :package_id, type: :identifier}]
906
+ arguments: [PACKAGE_ID_ARG]
866
907
  command :list, description: 'List packages'
867
908
  command :show, description: 'Show a package',
868
- arguments: [{name: :package_id, type: :identifier}]
909
+ arguments: [PACKAGE_ID_ARG],
910
+ action: ->(package_id:, **) { Result::SingleObject.new(aoc_api.read("packages/#{package_id}")) }
869
911
  command :delete, description: 'Delete packages',
870
- arguments: [{name: :package_id, type: :identifier}]
871
- command :modify, description: 'Modify a package',
872
- arguments: [{name: :package_id, type: :identifier}, {name: :data, type: Hash}]
873
- # Node Gen4 read-only actions on packages
874
- command :bearer_token_node, description: 'Show bearer token for package node',
875
- arguments: [{name: :package_id, type: :identifier}]
876
- command :node_info, description: 'Show node info for package',
877
- arguments: [{name: :package_id, type: :identifier}]
878
- command :ls, description: 'List package contents', aliases: [:browse],
879
- arguments: [{name: :package_id, type: :identifier}]
880
- command :find, description: 'Find files in package',
881
- arguments: [{name: :package_id, type: :identifier}]
912
+ arguments: [PACKAGE_ID_ARG.merge(bulk: true)]
913
+ command(
914
+ :modify, description: 'Modify a package',
915
+ arguments: [PACKAGE_ID_ARG, {name: :package, type: Hash}],
916
+ action: lambda do |package:, package_id:, **|
917
+ aoc_api.update("packages/#{package_id}", package)
918
+ Result::Status.new('modified')
919
+ end
920
+ )
882
921
  end
883
922
 
884
923
  commands_under %i[packages shared_inboxes] do
885
- command :list, description: 'List shared inboxes',
886
- action: (lambda do
887
- result_list(
888
- 'dropbox_memberships',
889
- fields: %w[dropbox_id dropbox.name],
890
- default_query: workspace_id_hash({'embed[]' => 'dropbox', 'aggregate_permissions_by_dropbox' => true, 'sort' => 'dropbox_name'}, string: true)
891
- )
892
- end)
924
+ command :list, description: 'List shared inboxes'
893
925
  command :show, description: 'Show a shared inbox',
894
926
  arguments: [{name: :dropbox_id, type: :identifier, lookup: :lookup_aoc_dropbox_id}],
895
927
  action: ->(dropbox_id:, **) { Result::SingleObject.new(aoc_api.read("dropboxes/#{dropbox_id}")) }
896
928
  command :short_link, description: 'Manage shared inbox short links',
897
- arguments: [{name: :link_type, allowed: %i[public private]}],
929
+ arguments: [{name: :link_type, allowed: %i[public private]}, {name: :dropbox_id, type: :identifier, lookup: :lookup_aoc_dropbox_id}],
898
930
  setup: :setup_packages_short_link
899
931
  end
900
932
  # packages > shared_inboxes > short_link sub-commands
901
933
  register_short_link_commands(self, %i[packages shared_inboxes short_link])
902
934
 
903
- # files sub-commands: AoC-specific commands + all Gen4 commands from COMMANDS_GEN4_SPEC
935
+ # files sub-commands: Gen4 commands are mounted, plus AoC-specific commands
904
936
  commands_under :files do
905
937
  command :short_link, description: 'Manage file short link',
906
- arguments: [{name: :folder_dest, type: String}, {name: :link_type, allowed: %i[public private]}],
938
+ arguments: [{name: :folder}, {name: :link_type, allowed: %i[public private]}],
907
939
  setup: :setup_files_short_link
908
- command :transfer, description: 'Transfer files (node-to-node)',
909
- arguments: [{name: :direction, allowed: %i[push pull]}, {name: :source_folder, type: String}]
910
- command :permission, description: 'Manage permissions'
911
- command :sync, description: 'Synchronize folders'
912
- Node::COMMANDS_GEN4_SPEC.each do |cmd, spec|
913
- command cmd, **spec
914
- end
940
+ command :transfer, description: 'Transfer files (node-to-node)', arguments: TRANSFER_ARGS,
941
+ action: ->(direction:, source_folder:, **) { nodegen4_transfer(aoc_api.home[:node_id], file_id: aoc_api.home[:file_id], scope: Api::Node::Scope::USER, direction: direction, source_folder: source_folder) }
915
942
  end
916
943
  # files > short_link sub-commands
917
944
  register_short_link_commands(self, %i[files short_link])
918
945
 
919
946
  # automation sub-commands
920
- commands_under :automation do
921
- commands_under :instances do
922
- crud_commands api: :aoc_api, entity: 'workflow_instances', name: 'workflow instance'
923
- end
924
-
947
+ # Automation API: a workflow has ordered steps (step_order), a step has ordered actions (action_order)
948
+ AUTOMATION_CRUD = {api: :@automation_api, body_component: Schema::Registry::AUTOMATION}.freeze
949
+ private_constant :AUTOMATION_CRUD
950
+ commands_under :automation do
925
951
  commands_under :workflows do
926
- crud_commands api: :@automation_api, entity: 'workflows'
952
+ crud_commands(**AUTOMATION_CRUD, entity: 'workflows', items_key: 'workflows', query_component: Schema::Registry::AUTOMATION)
927
953
  command :launch, description: 'Launch a workflow',
928
- arguments: [{name: :wf_id, type: :identifier}],
929
- action: ->(wf_id:, **) { Result::SingleObject.new(@automation_api.create("workflows/#{wf_id}/launch", {})) }
930
- commands_under :action, description: 'Add action to workflow (TODO)' do
931
- %i[list create show].each do |cmd|
932
- command cmd,
933
- description: "#{cmd.capitalize} action (TODO)",
934
- arguments: [{name: :wf_id, type: :identifier}]
954
+ arguments: [{name: :workflow_id, type: :identifier}],
955
+ action: ->(workflow_id:, **) { Result::SingleObject.new(@automation_api.create("workflows/#{workflow_id}/launch", {})) }
956
+ command :update_state, description: 'Update state of workflow',
957
+ arguments: [{name: :workflow_id, type: :identifier},
958
+ {name: :state, type: Hash, schema: Schema::Registry.req_body(Schema::Registry::AUTOMATION, 'workflows/{id}/update_state.put')}],
959
+ action: ->(workflow_id:, state:, **) { Result::SingleObject.new(@automation_api.update("workflows/#{workflow_id}/update_state", state)) }
960
+ command(
961
+ :cancel_instances, description: 'Cancel all jobs of workflow',
962
+ arguments: [{name: :workflow_id, type: :identifier}],
963
+ action: lambda do |workflow_id:, **|
964
+ @automation_api.update("workflows/#{workflow_id}/cancel_instances", {})
965
+ Result::Status.new('canceled')
935
966
  end
936
- end
967
+ )
968
+ command(
969
+ :delete_instances, description: 'Delete all jobs of workflow',
970
+ arguments: [{name: :workflow_id, type: :identifier}],
971
+ action: lambda do |workflow_id:, **|
972
+ @automation_api.delete("workflows/#{workflow_id}/delete_instances")
973
+ Result::Status.new('deleted')
974
+ end
975
+ )
976
+ command :action, description: 'Manage actions of workflow',
977
+ arguments: [{name: :workflow_id, type: :identifier}]
978
+ command :permissions, description: 'Manage permissions of workflow',
979
+ arguments: [{name: :workflow_id, type: :identifier}]
980
+ end
981
+ # workflow_id is resolved by the parent command
982
+ commands_under %i[workflows action] do
983
+ command :list, description: 'List actions of all steps of workflow'
984
+ command :create, description: 'Add a step with one action at the end of workflow',
985
+ arguments: [{name: :action, type: Hash, mandatory: false, default: {}, schema: Schema::Registry.req_body(Schema::Registry::AUTOMATION, 'actions.post')}]
986
+ end
987
+ commands_under %i[workflows permissions] do
988
+ # API requires query parameter workflow_id
989
+ command(
990
+ :list, description: 'List permissions of workflow',
991
+ query_schema: Schema::Registry.query_params(Schema::Registry::AUTOMATION, 'workflow_permissions'),
992
+ action: lambda do |workflow_id:, **|
993
+ schema = Schema::Registry.query_params(Schema::Registry::AUTOMATION, 'workflow_permissions')
994
+ query = (query_read_delete(schema: schema) || {}).merge('workflow_id' => workflow_id)
995
+ Result::ObjectList.new(@automation_api.read('workflow_permissions', query))
996
+ end
997
+ )
998
+ command(
999
+ :create, description: 'Create a permission on workflow',
1000
+ arguments: [{name: :workflow_permission, type: Hash, bulk: true, schema: Schema::Registry.req_body(Schema::Registry::AUTOMATION, 'workflow_permissions.post')}],
1001
+ action: lambda do |workflow_id:, workflow_permission:, **|
1002
+ bulk_result(workflow_permission, command: :create) do |params|
1003
+ @automation_api.create('workflow_permissions', params.merge('workflow_id' => workflow_id))
1004
+ end
1005
+ end
1006
+ )
1007
+ crud_commands(**AUTOMATION_CRUD, entity: 'workflow_permissions', name: 'workflow permission', operations: %i[modify delete])
1008
+ end
1009
+ commands_under :instances do
1010
+ crud_commands(
1011
+ **AUTOMATION_CRUD, entity: 'workflow_instances', name: 'workflow instance', operations: %i[list show delete],
1012
+ items_key: 'workflow_instances', query_component: Schema::Registry::AUTOMATION
1013
+ )
1014
+ command :cancel, description: 'Cancel workflow instance',
1015
+ arguments: [{name: :workflow_instance_id, type: :identifier}],
1016
+ action: ->(workflow_instance_id:, **) { Result::SingleObject.new(@automation_api.update("workflow_instances/#{workflow_instance_id}", {'status' => 'canceled'})) }
1017
+ end
1018
+ commands_under :steps do
1019
+ crud_commands(**AUTOMATION_CRUD, entity: 'steps', operations: %i[create show modify delete])
1020
+ end
1021
+ commands_under :actions do
1022
+ crud_commands(**AUTOMATION_CRUD, entity: 'actions', operations: %i[create show modify delete])
937
1023
  end
938
1024
  end
939
1025
 
@@ -954,13 +1040,13 @@ module Aspera
954
1040
  def setup_automation_api(**)
955
1041
  change_api_scope(Api::AoC::Scope::ADMIN_USER)
956
1042
  Log.log.warn('BETA: work under progress')
957
- @automation_api = Rest.new(**aoc_api.params, base_url: aoc_api.base_url.gsub('/api/', '/automation/'))
1043
+ @automation_api = Rest::Client.new(**aoc_api.params, base_url: aoc_api.base_url.gsub('/api/', '/automation/'))
958
1044
  {}
959
1045
  end
960
1046
 
961
1047
  # --- handler methods ---
962
1048
 
963
- def action_reminder
1049
+ def action_reminder(**)
964
1050
  user_email = options.get_option(:username, mandatory: true)
965
1051
  no_auth_api = Api::AoC.new(url: options.get_option(:url), auth: :none)
966
1052
  no_auth_api.create('organization_reminders', {email: user_email})
@@ -968,8 +1054,8 @@ module Aspera
968
1054
  end
969
1055
 
970
1056
  # packages > send
971
- def action_packages_send(data:, **)
972
- package_data = data
1057
+ def action_packages_send(package:, **)
1058
+ package_data = package
973
1059
  new_user_option = options.get_option(:new_user_option)
974
1060
  option_validate = options.get_option(:validate_metadata)
975
1061
  workspace_id_hash(package_data, string: true) unless package_data.key?('workspace_id')
@@ -1042,7 +1128,7 @@ module Aspera
1042
1128
  end
1043
1129
 
1044
1130
  # packages > list
1045
- def action_packages_list
1131
+ def action_packages_list(**)
1046
1132
  result, max_items = list_all_packages_with_query
1047
1133
  skip_ids_persistency = package_persistency
1048
1134
  reject_packages_from_persistency(result[:items], skip_ids_persistency)
@@ -1052,11 +1138,6 @@ module Aspera
1052
1138
  Result::ObjectList.new(result[:items], fields: display_fields, total: result[:total])
1053
1139
  end
1054
1140
 
1055
- # packages > show
1056
- def action_packages_show(package_id:, **)
1057
- Result::SingleObject.new(aoc_api.read("packages/#{package_id}"))
1058
- end
1059
-
1060
1141
  # packages > delete
1061
1142
  def action_packages_delete(package_id:, **)
1062
1143
  bulk_result(package_id, command: :delete) do |one_id|
@@ -1065,30 +1146,24 @@ module Aspera
1065
1146
  end
1066
1147
  end
1067
1148
 
1068
- # packages > modify
1069
- def action_packages_modify(data:, package_id:, **)
1070
- aoc_api.update("packages/#{package_id}", data)
1071
- Result::Status.new('modified')
1072
- end
1073
-
1074
- # packages > bearer_token_node / node_info / ls / find
1075
- # (NODE4_READ_ACTIONS dispatched by full path: action_packages_bearer_token_node, etc.)
1076
- Node::NODE4_READ_ACTIONS.each do |action|
1077
- define_action_method([:packages, action]) do |package_id:, **|
1078
- package_info = aoc_api.read("packages/#{package_id}")
1079
- execute_nodegen4_command(action, package_info['node_id'], file_id: package_info['contents_file_id'], scope: Api::Node::Scope::USER)
1080
- end
1149
+ # Used as `instance:` of the mount on packages: node of the package contents
1150
+ # @return [Array(Node, Hash)]
1151
+ def package_node_plugin(package_id:, **)
1152
+ package_info = aoc_api.read("packages/#{package_id}")
1153
+ # An id that is not a single path segment (e.g. `/`) reads the list of packages
1154
+ Aspera.assert(package_info.is_a?(Hash), type: Cli::BadArgument) { "invalid package id: #{package_id}" }
1155
+ nodegen4_plugin(package_info['node_id'], file_id: package_info['contents_file_id'], scope: Api::Node::Scope::USER)
1081
1156
  end
1082
1157
 
1083
1158
  # setup: files > short_link
1084
1159
  # Resolves the target folder, consumes link_type argument, computes purposes.
1085
1160
  # @return [Hash] ctx keys: sl_shared_data, sl_link_type, sl_token_purpose, sl_short_link_purpose, sl_perm_block, sl_shared_apifid, sl_folder_dest
1086
- def setup_files_short_link(folder_dest:, link_type:, **)
1161
+ def setup_files_short_link(folder:, link_type:, **)
1087
1162
  home_node_api = aoc_api.node_api_from(
1088
1163
  node_id: aoc_api.home[:node_id],
1089
1164
  **workspace_id_hash(name: true)
1090
1165
  )
1091
- shared_apifid = home_node_api.resolve_api_fid(aoc_api.home[:file_id], folder_dest)
1166
+ shared_apifid = home_node_api.resolve_api_fid(aoc_api.home[:file_id], folder)
1092
1167
  shared_data = {
1093
1168
  node_id: shared_apifid.node_api.app_info.node_info['id'],
1094
1169
  file_id: shared_apifid.file_id
@@ -1104,7 +1179,7 @@ module Aspera
1104
1179
  'access_levels' => Api::AoC.expand_access_levels(access_levels),
1105
1180
  'tags' => {
1106
1181
  'url_token' => true,
1107
- 'folder_name' => File.basename(folder_dest),
1182
+ 'folder_name' => File.basename(folder),
1108
1183
  'created_by_name' => aoc_api.current_user_info['name'],
1109
1184
  'created_by_email' => aoc_api.current_user_info['email'],
1110
1185
  'access_key' => shared_apifid.node_api.app_info.node_info['access_key'],
@@ -1135,10 +1210,9 @@ module Aspera
1135
1210
  end
1136
1211
 
1137
1212
  # setup: packages > shared_inboxes > short_link
1138
- # Reads dropbox_id, consumes link_type argument, computes purposes.
1213
+ # link_type: and dropbox_id: resolved via arguments: on the node, computes purposes.
1139
1214
  # @return [Hash] ctx keys: sl_shared_data, sl_link_type, sl_token_purpose, sl_short_link_purpose
1140
- def setup_packages_short_link(link_type:, **)
1141
- dropbox_id = get_resource_id_from_args('dropboxes')
1215
+ def setup_packages_short_link(link_type:, dropbox_id:, **)
1142
1216
  shared_data = {dropbox_id: dropbox_id, name: ''}
1143
1217
  token_purpose, short_link_purpose = short_link_purposes(shared_data, link_type)
1144
1218
  {
@@ -1167,7 +1241,7 @@ module Aspera
1167
1241
  url_token_data: {data: shared_data, purpose: sl_token_purpose}
1168
1242
  }
1169
1243
  end
1170
- custom_data = {}
1244
+ custom_data = custom_data.dup
1171
1245
  access_levels = custom_data.delete('access_levels')
1172
1246
  if (pass = custom_data.delete('password'))
1173
1247
  create_payload[:data][:url_token_data][:password] = pass
@@ -1224,7 +1298,7 @@ module Aspera
1224
1298
  one_id = short_link_id
1225
1299
  node_file = sl_shared_data.slice(:node_id, :file_id)
1226
1300
  modify_payload = {edit_access: true, json_query: node_file}
1227
- custom_data = {}
1301
+ custom_data = custom_data.dup
1228
1302
  if (pass = custom_data.delete('password'))
1229
1303
  modify_payload[:password_enabled] = true
1230
1304
  modify_payload[:data] = {url_token_data: {password: pass, data: node_file}}
@@ -1241,38 +1315,41 @@ module Aspera
1241
1315
  Result::Status.new('modified')
1242
1316
  end
1243
1317
 
1244
- # files > transfer
1245
- def action_files_transfer(direction:, source_folder:, **)
1246
- execute_nodegen4_command(
1247
- :transfer, aoc_api.home[:node_id],
1248
- file_id: aoc_api.home[:file_id],
1249
- scope: Api::Node::Scope::USER,
1250
- transfer_direction: direction,
1251
- transfer_source: source_folder
1252
- )
1253
- end
1254
-
1255
- # files > FILES_COMMANDS (all Gen4 node commands except :transfer, handled above)
1256
- FILES_COMMANDS.reject { |a| a.eql?(:transfer) }.each do |action|
1257
- define_action_method([:files, action]) do |**ctx|
1258
- execute_nodegen4_command(action, aoc_api.home[:node_id], file_id: aoc_api.home[:file_id], scope: Api::Node::Scope::USER, **ctx)
1259
- end
1318
+ # files - mount target: Gen4 commands on the user's home folder
1319
+ def files_node_plugin(**)
1320
+ nodegen4_plugin(aoc_api.home[:node_id], file_id: aoc_api.home[:file_id], scope: Api::Node::Scope::USER)
1260
1321
  end
1261
1322
 
1262
1323
  # admin > application > instance > <type> > show|modify
1263
1324
  APP_TYPES.each do |app_type|
1264
1325
  define_action_method([:admin, :application, :instance, app_type, :show]) do |**kwargs|
1265
- app_id = kwargs[:"#{app_type}_id"]
1326
+ app_id = kwargs[:instance_id]
1266
1327
  Result::SingleObject.new(aoc_api.read("admin/apps_new/#{app_type}/#{app_id}", query_read_delete))
1267
1328
  end
1268
1329
 
1269
- define_action_method([:admin, :application, :instance, app_type, :modify]) do |properties:, **kwargs|
1270
- app_id = kwargs[:"#{app_type}_id"]
1271
- aoc_api.update("admin/apps_new/#{app_type}/#{app_id}", properties)
1330
+ define_action_method([:admin, :application, :instance, app_type, :modify]) do |instance:, **kwargs|
1331
+ app_id = kwargs[:instance_id]
1332
+ aoc_api.update("admin/apps_new/#{app_type}/#{app_id}", instance)
1272
1333
  Result::Status.new('modified')
1273
1334
  end
1274
1335
  end
1275
1336
 
1337
+ def action_admin_application_instance_list(**)
1338
+ result_list(
1339
+ 'admin/apps_new',
1340
+ fields: %w[id app_type available workspace_id],
1341
+ default_query: {workspace_id: aoc_api.workspace_info[:id]}
1342
+ )
1343
+ end
1344
+
1345
+ def action_packages_shared_inboxes_list(**)
1346
+ result_list(
1347
+ 'dropbox_memberships',
1348
+ fields: %w[dropbox_id dropbox.name],
1349
+ default_query: workspace_id_hash({'embed[]' => 'dropbox', 'aggregate_permissions_by_dropbox' => true, 'sort' => 'dropbox_name'}, string: true)
1350
+ )
1351
+ end
1352
+
1276
1353
  def action_admin_application_membership_create(membership:, **)
1277
1354
  data = membership.dup
1278
1355
  app_type = data.delete('app_type')
@@ -1281,29 +1358,12 @@ module Aspera
1281
1358
  Result::SingleObject.new(aoc_api.create("apps/#{app_type}/app_memberships", data))
1282
1359
  end
1283
1360
 
1284
- # admin > application > membership > show|delete
1285
- def action_admin_application_membership_show(membership_id:, **)
1286
- Result::SingleObject.new(aoc_api.read("apps/app_memberships/#{membership_id}", query_read_delete))
1287
- end
1288
-
1289
- def action_admin_application_membership_delete(membership_id:, **)
1290
- aoc_api.delete("apps/app_memberships/#{membership_id}")
1291
- Result::Status.new('deleted')
1292
- end
1293
-
1294
1361
  # admin - setup: change API scope to admin once
1295
1362
  def setup_admin_scope(**)
1296
1363
  change_api_scope(Api::AoC::Scope::ADMIN)
1297
1364
  {}
1298
1365
  end
1299
1366
 
1300
- # admin > subscription > account
1301
- def action_admin_subscription_account
1302
- org = aoc_api.read('organization')
1303
- result = GraphQL.execute(api_from_options('bss/platform/graphql'), 'bss_subscription_account', {organization_id: org['id']})
1304
- Result::SingleObject.new(result['aoc']['bssSubscription'])
1305
- end
1306
-
1307
1367
  # admin > subscription > usage
1308
1368
  def action_admin_subscription_usage(aggregate:, start_date:, end_date:, **)
1309
1369
  today = Date.today
@@ -1318,21 +1378,20 @@ module Aspera
1318
1378
  Result::SingleObject.new(result['aoc'])
1319
1379
  end
1320
1380
 
1321
- # admin > analytics > application_events
1322
- def action_admin_analytics_application_events
1323
- events = build_analytics_api.read("organizations/#{aoc_api.current_user_info['organization_id']}/application_events")['application_events']
1324
- Result::ObjectList.new(events)
1381
+ # @return [String] the given resource id, or the one of the current user if empty
1382
+ def analytics_resource_id(event_resource_type, event_resource_id)
1383
+ return event_resource_id unless event_resource_id.to_s.empty?
1384
+ case event_resource_type
1385
+ when :organizations then aoc_api.current_user_info['organization_id']
1386
+ when :users then aoc_api.current_user_info['id']
1387
+ when :nodes then aoc_api.current_user_info['read_only_home_node_id']
1388
+ else Aspera.error_unreachable_line
1389
+ end
1325
1390
  end
1326
1391
 
1327
1392
  # admin > analytics > transfers
1328
1393
  def action_admin_analytics_transfers(event_resource_type:, event_resource_id:, **)
1329
- event_resource_id ||=
1330
- case event_resource_type
1331
- when :organizations then aoc_api.current_user_info['organization_id']
1332
- when :users then aoc_api.current_user_info['id']
1333
- when :nodes then aoc_api.current_user_info['read_only_home_node_id']
1334
- else Aspera.error_unreachable_line
1335
- end
1394
+ event_resource_id = analytics_resource_id(event_resource_type, event_resource_id)
1336
1395
  filter = query_read_delete(default: {})
1337
1396
  filter['limit'] ||= 100
1338
1397
  if options.get_option(:once_only, mandatory: true)
@@ -1356,13 +1415,7 @@ module Aspera
1356
1415
 
1357
1416
  # admin > analytics > files
1358
1417
  def action_admin_analytics_files(event_resource_type:, event_resource_id:, event_uuid:, **)
1359
- event_resource_id =
1360
- case event_resource_type
1361
- when :organizations then aoc_api.current_user_info['organization_id']
1362
- when :users then aoc_api.current_user_info['id']
1363
- when :nodes then aoc_api.current_user_info['read_only_home_node_id']
1364
- else Aspera.error_unreachable_line
1365
- end if event_resource_id.to_s.empty?
1418
+ event_resource_id = analytics_resource_id(event_resource_type, event_resource_id)
1366
1419
  filter = query_read_delete(default: {})
1367
1420
  filter['limit'] ||= 100
1368
1421
  events = build_analytics_api.read("#{event_resource_type}/#{event_resource_id}/transfers/#{event_uuid}/files", filter)['files']
@@ -1370,7 +1423,7 @@ module Aspera
1370
1423
  end
1371
1424
 
1372
1425
  # Lookup methods for arguments:(:identifier) + lookup: on admin resources.
1373
- # One method per non-singleton resource; each delegates to get_resource_id_from_args.
1426
+ # One method per non-singleton resource: percent selector searches with `q=<value>`.
1374
1427
  ADMIN_OBJECTS.reject { |r| ADMIN_OBJECT_CONFIG.dig(r, :singleton) }.each do |res|
1375
1428
  define_method(:"lookup_aoc_#{res}_id") do |_field, value, **|
1376
1429
  aoc_api.lookup_with_q(aoc_res_path(res), value: value)['id']
@@ -1379,7 +1432,7 @@ module Aspera
1379
1432
 
1380
1433
  # admin > <res> > list
1381
1434
  ADMIN_OBJECTS.each do |res|
1382
- define_action_method([:admin, res, :list]) do
1435
+ define_action_method([:admin, res, :list]) do |**|
1383
1436
  c = aoc_res_cfg(res)
1384
1437
  result_list(c[:path], fields: c[:list_fields], query_component: c[:query_component])
1385
1438
  end
@@ -1396,14 +1449,15 @@ module Aspera
1396
1449
 
1397
1450
  # admin > organization|self > show (singleton)
1398
1451
  %i[organization self].each do |res|
1399
- define_action_method([:admin, res, :show]) do
1452
+ define_action_method([:admin, res, :show]) do |**|
1400
1453
  Result::SingleObject.new(aoc_api.read(res.to_s, query_read_delete), fields: Formatter.all_but('certificate'))
1401
1454
  end
1402
1455
  end
1403
1456
 
1404
1457
  # admin > <res> > create
1405
1458
  ADMIN_OBJECTS.reject { |r| ADMIN_OBJECT_CONFIG.dig(r, :singleton) }.each do |res|
1406
- define_action_method([:admin, res, :create]) do |data:, **|
1459
+ define_action_method([:admin, res, :create]) do |**kwargs|
1460
+ data = kwargs.fetch(res)
1407
1461
  c = aoc_res_cfg(res)
1408
1462
  path = c[:path]
1409
1463
  # Special case: client_registration_token has a different creation URL
@@ -1418,7 +1472,8 @@ module Aspera
1418
1472
 
1419
1473
  # admin > <res> > modify
1420
1474
  ADMIN_OBJECTS.reject { |r| ADMIN_OBJECT_CONFIG.dig(r, :singleton) || ADMIN_OBJECT_CONFIG.dig(r, :ops)&.then { |o| !o.include?(:modify) } }.each do |res|
1421
- define_action_method([:admin, res, :modify]) do |data:, **kwargs|
1475
+ define_action_method([:admin, res, :modify]) do |**kwargs|
1476
+ data = kwargs.fetch(res)
1422
1477
  res_id = kwargs[:"#{res}_id"]
1423
1478
  c = aoc_res_cfg(res)
1424
1479
  aoc_api.update("#{c[:path]}/#{res_id}", data)
@@ -1448,6 +1503,28 @@ module Aspera
1448
1503
  end
1449
1504
  end
1450
1505
 
1506
+ # automation > workflows > action > list
1507
+ # A workflow has ordered steps (step_order), a step has ordered actions (action_order)
1508
+ def action_automation_workflows_action_list(workflow_id:, **)
1509
+ workflow = @automation_api.read("workflows/#{workflow_id}")
1510
+ actions = Array(workflow['step_order']).flat_map do |step_id|
1511
+ step = @automation_api.read("steps/#{step_id}")
1512
+ Array(step['action_order']).map { |action_id| @automation_api.read("actions/#{action_id}") }
1513
+ end
1514
+ Result::ObjectList.new(actions)
1515
+ end
1516
+
1517
+ # automation > workflows > action > create
1518
+ # Default action type is `manual`
1519
+ def action_automation_workflows_action_create(workflow_id:, action:, **)
1520
+ workflow = @automation_api.read("workflows/#{workflow_id}")
1521
+ step = @automation_api.create('steps', {'workflow_id' => workflow_id})
1522
+ @automation_api.update("workflows/#{workflow_id}", {'step_order' => Array(workflow['step_order']) + [step['id']]})
1523
+ new_action = @automation_api.create('actions', {'type' => 'manual'}.merge(action).merge('step_id' => step['id']))
1524
+ @automation_api.update("steps/#{step['id']}", {'action_order' => [new_action['id']]})
1525
+ Result::SingleObject.new(new_action)
1526
+ end
1527
+
1451
1528
  # admin > client > set_pub_key
1452
1529
  def action_admin_client_set_pub_key(private_key_pem:, client_id:, **)
1453
1530
  c = aoc_res_cfg(:client)
@@ -1457,10 +1534,10 @@ module Aspera
1457
1534
  end
1458
1535
 
1459
1536
  # admin > ats — build and return an Ats plugin instance wired to the AoC ATS API.
1460
- # Used as delegate_instance: target so --help traverses the Ats registry.
1537
+ # Mount target of `admin ats`.
1461
1538
  # @return [Ats] configured Ats plugin instance
1462
- def build_ats_plugin
1463
- ats_api = Rest.new(**aoc_api.params.deep_merge({
1539
+ def build_ats_plugin(**)
1540
+ ats_api = Rest::Client.new(**aoc_api.params.deep_merge({
1464
1541
  base_url: "#{aoc_api.base_url}/admin/ats/pub/v1",
1465
1542
  auth: {params: {scope: Api::AoC::Scope::ADMIN_USER}}
1466
1543
  }))
@@ -1470,11 +1547,9 @@ module Aspera
1470
1547
  # admin > node > do | bearer_token — setup reuses the generic instance setup
1471
1548
  # (setup_admin_node_instance is auto-generated above, providing res_id:)
1472
1549
 
1473
- # admin > node > do > <FILES_COMMAND>
1474
- FILES_COMMANDS.each do |cmd|
1475
- define_action_method([:admin, :node, :do, cmd]) do |node_id:, **ctx|
1476
- execute_nodegen4_command(cmd, node_id, scope: Api::Node::Scope::ADMIN, **ctx)
1477
- end
1550
+ # admin > node > do - mount target: Gen4 commands on the node, admin scope
1551
+ def admin_node_do_plugin(node_id:, **)
1552
+ nodegen4_plugin(node_id, scope: Api::Node::Scope::ADMIN)
1478
1553
  end
1479
1554
 
1480
1555
  # admin > node > bearer_token
@@ -1494,28 +1569,17 @@ module Aspera
1494
1569
  {ws_res_id: workspace_id}
1495
1570
  end
1496
1571
 
1497
- # admin > workspace > dropbox > list
1498
- def action_admin_workspace_dropbox_list(ws_res_id:, **)
1499
- query = options.get_option(:query) || {}
1500
- Result::ObjectList.new(aoc_api.read('dropboxes', query.merge({'workspace_id' => ws_res_id})), fields: %w[id name description])
1501
- end
1502
-
1503
1572
  # admin > workspace > shared_folder — res_id: already in ctx via arguments:(:identifier)
1504
1573
  def setup_admin_workspace_shared_folder(workspace_id:, **)
1505
1574
  resource_instance_path = "#{aoc_res_path(:workspace)}/#{workspace_id}"
1506
- query = options.get_option(:query) || Api::AoC.workspace_access(workspace_id).merge({'admin' => true})
1575
+ query = query_read_delete(default: Api::AoC.workspace_access(workspace_id).merge({'admin' => true}))
1507
1576
  shared_folders = aoc_api.read_with_paging("#{resource_instance_path}/permissions", query)[:items]
1508
1577
  {ws_res_id: workspace_id, shared_folders: shared_folders}
1509
1578
  end
1510
1579
 
1511
- # admin > workspace > shared_folder > list
1512
- def action_admin_workspace_shared_folder_list(shared_folders:, **)
1513
- Result::ObjectList.new(shared_folders, fields: %w[id node_name node_id file_id file.path tags.aspera.files.workspace.share_as])
1514
- end
1515
-
1516
- # admin > workspace > shared_folder > node|member — sf_id: already in ctx via arguments:(:identifier)
1517
- def resolve_sf_item(shared_folders:, sf_id:, **)
1518
- sf_item = shared_folders.find { |i| i['id'].eql?(sf_id) }
1580
+ # admin > workspace > shared_folder > node|member — shared_folder_id: already in ctx via arguments:(:identifier)
1581
+ def resolve_sf_item(shared_folders:, shared_folder_id:, **)
1582
+ sf_item = shared_folders.find { |i| i['id'].eql?(shared_folder_id) }
1519
1583
  Aspera.assert(sf_item, 'shared folder not found')
1520
1584
  {sf_item: sf_item}
1521
1585
  end
@@ -1523,11 +1587,9 @@ module Aspera
1523
1587
  alias_method :setup_admin_workspace_shared_folder_node, :resolve_sf_item
1524
1588
  alias_method :setup_admin_workspace_shared_folder_member, :resolve_sf_item
1525
1589
 
1526
- # admin > workspace > shared_folder > node > <FILES_COMMAND>
1527
- FILES_COMMANDS.each do |cmd|
1528
- define_action_method([:admin, :workspace, :shared_folder, :node, cmd]) do |sf_item:, **ctx|
1529
- execute_nodegen4_command(cmd, sf_item['node_id'], file_id: sf_item['file_id'], scope: Api::Node::Scope::ADMIN, **ctx)
1530
- end
1590
+ # admin > workspace > shared_folder > node - mount target: Gen4 commands on the shared folder, admin scope
1591
+ def admin_workspace_shared_folder_node_plugin(sf_item:, **)
1592
+ nodegen4_plugin(sf_item['node_id'], file_id: sf_item['file_id'], scope: Api::Node::Scope::ADMIN)
1531
1593
  end
1532
1594
 
1533
1595
  # admin > workspace > shared_folder > member > list
@@ -1561,24 +1623,12 @@ module Aspera
1561
1623
  define_action_method([:admin, :user, pref, :show]) do |user_id:, **|
1562
1624
  Result::SingleObject.new(aoc_api.read("#{aoc_res_path(:user)}/#{user_id}/#{pref_path}"))
1563
1625
  end
1564
- define_action_method([:admin, :user, pref, :modify]) do |properties:, user_id:, **|
1565
- aoc_api.update("#{aoc_res_path(:user)}/#{user_id}/#{pref_path}", properties)
1626
+ define_action_method([:admin, :user, pref, :modify]) do |user_id:, **kwargs|
1627
+ aoc_api.update("#{aoc_res_path(:user)}/#{user_id}/#{pref_path}", kwargs.fetch(pref))
1566
1628
  Result::Status.new('modified')
1567
1629
  end
1568
1630
  end
1569
1631
 
1570
- # automation > workflows > action > * (TODO: not fully implemented)
1571
- %i[list create show].each do |cmd|
1572
- define_action_method([:automation, :workflows, :action, cmd]) do |wf_id:, **|
1573
- Log.log.warn { "Not implemented: #{cmd}" }
1574
- step = @automation_api.create('steps', {'workflow_id' => wf_id})
1575
- @automation_api.update("workflows/#{wf_id}", {'step_order' => [step['id']]})
1576
- action = @automation_api.create('actions', {'step_id' => step['id'], 'type' => 'manual'})
1577
- @automation_api.update("steps/#{step['id']}", {'action_order' => [action['id']]})
1578
- Result::SingleObject.new(@automation_api.read("workflows/#{wf_id}"))
1579
- end
1580
- end
1581
-
1582
1632
  def action_gateway(parameters: {}, **)
1583
1633
  require 'aspera/faspex_gw'
1584
1634
  parameters = parameters.symbolize_keys