aspera-cli 4.27.2 → 4.27.4

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