aspera-cli 4.26.2 → 4.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +281 -21
  4. data/CONTRIBUTING.md +98 -7
  5. data/bin/ascli +3 -3
  6. data/docs/README.md +10757 -0
  7. data/docs/test-mcp-with-ai.md +202 -0
  8. data/lib/aspera/agent/base.rb +20 -6
  9. data/lib/aspera/agent/connect.rb +53 -16
  10. data/lib/aspera/agent/desktop.rb +52 -17
  11. data/lib/aspera/agent/direct.rb +26 -24
  12. data/lib/aspera/agent/httpgw.rb +1 -1
  13. data/lib/aspera/agent/node.rb +32 -0
  14. data/lib/aspera/agent/transferd.rb +39 -3
  15. data/lib/aspera/api/alee.rb +1 -1
  16. data/lib/aspera/api/aoc.rb +14 -16
  17. data/lib/aspera/api/cos_node.rb +1 -1
  18. data/lib/aspera/api/faspex.rb +15 -14
  19. data/lib/aspera/api/httpgw.rb +4 -4
  20. data/lib/aspera/api/node.rb +26 -23
  21. data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
  22. data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
  23. data/lib/aspera/ascmd.rb +21 -19
  24. data/lib/aspera/ascp/installation.rb +37 -38
  25. data/lib/aspera/ascp/management.rb +2 -3
  26. data/lib/aspera/cli/ascp_actions.rb +155 -0
  27. data/lib/aspera/cli/async_transfer_store.rb +81 -0
  28. data/lib/aspera/cli/bootstrapper.rb +197 -0
  29. data/lib/aspera/cli/command_registry.rb +145 -0
  30. data/lib/aspera/cli/command_spec.rb +138 -0
  31. data/lib/aspera/cli/context.rb +16 -3
  32. data/lib/aspera/cli/error.rb +16 -0
  33. data/lib/aspera/cli/extended_value.rb +2 -1
  34. data/lib/aspera/cli/formatter.rb +41 -27
  35. data/lib/aspera/cli/gem_checker.rb +65 -0
  36. data/lib/aspera/cli/hints.rb +2 -2
  37. data/lib/aspera/cli/http.rb +70 -19
  38. data/lib/aspera/cli/info.rb +2 -0
  39. data/lib/aspera/cli/mailer.rb +97 -0
  40. data/lib/aspera/cli/mcp_tool.rb +198 -0
  41. data/lib/aspera/cli/options.schema.yaml +364 -7
  42. data/lib/aspera/cli/{manager.rb → parser.rb} +334 -142
  43. data/lib/aspera/cli/plugins/alee.rb +20 -22
  44. data/lib/aspera/cli/plugins/aoc.rb +1166 -853
  45. data/lib/aspera/cli/plugins/ats.rb +200 -161
  46. data/lib/aspera/cli/plugins/base.rb +457 -100
  47. data/lib/aspera/cli/plugins/basic_auth.rb +14 -4
  48. data/lib/aspera/cli/plugins/config.rb +428 -754
  49. data/lib/aspera/cli/plugins/console.rb +106 -64
  50. data/lib/aspera/cli/plugins/cos.rb +44 -32
  51. data/lib/aspera/cli/plugins/factory.rb +4 -4
  52. data/lib/aspera/cli/plugins/faspex.rb +294 -257
  53. data/lib/aspera/cli/plugins/faspex5.rb +585 -357
  54. data/lib/aspera/cli/plugins/faspio.rb +49 -51
  55. data/lib/aspera/cli/plugins/httpgw.rb +18 -25
  56. data/lib/aspera/cli/plugins/mcp.rb +279 -0
  57. data/lib/aspera/cli/plugins/node.rb +998 -794
  58. data/lib/aspera/cli/plugins/oauth.rb +7 -10
  59. data/lib/aspera/cli/plugins/orchestrator.rb +111 -134
  60. data/lib/aspera/cli/plugins/preview.rb +180 -139
  61. data/lib/aspera/cli/plugins/server.rb +124 -79
  62. data/lib/aspera/cli/plugins/shares.rb +301 -107
  63. data/lib/aspera/cli/preset_actions.rb +139 -0
  64. data/lib/aspera/cli/preset_manager.rb +13 -12
  65. data/lib/aspera/cli/result.rb +61 -11
  66. data/lib/aspera/cli/runner.rb +275 -141
  67. data/lib/aspera/cli/secret_finder.rb +40 -0
  68. data/lib/aspera/cli/special_values.rb +1 -0
  69. data/lib/aspera/cli/sync_actions.rb +83 -59
  70. data/lib/aspera/cli/terminal_formatter.rb +1 -1
  71. data/lib/aspera/cli/transfer_actions.rb +83 -0
  72. data/lib/aspera/cli/transfer_agent.rb +116 -51
  73. data/lib/aspera/cli/transfer_progress.rb +3 -3
  74. data/lib/aspera/cli/vault_manager.rb +57 -0
  75. data/lib/aspera/cli/version.rb +1 -1
  76. data/lib/aspera/cli/wizard.rb +21 -20
  77. data/lib/aspera/command_line_builder.rb +22 -22
  78. data/lib/aspera/data_repository.rb +2 -1
  79. data/lib/aspera/dot_container.rb +4 -4
  80. data/lib/aspera/environment.rb +18 -11
  81. data/lib/aspera/exec_spec.rb +13 -0
  82. data/lib/aspera/faspex_gw.rb +1 -1
  83. data/lib/aspera/faspex_postproc.rb +4 -3
  84. data/lib/aspera/graphql.rb +35 -0
  85. data/lib/aspera/hash_ext.rb +6 -0
  86. data/lib/aspera/json_rpc/client.rb +62 -0
  87. data/lib/aspera/json_rpc/version.rb +7 -0
  88. data/lib/aspera/keychain/encrypted_hash.rb +1 -1
  89. data/lib/aspera/keychain/factory.rb +3 -3
  90. data/lib/aspera/keychain/macos_security.rb +3 -3
  91. data/lib/aspera/link_header.rb +82 -0
  92. data/lib/aspera/log.rb +17 -3
  93. data/lib/aspera/markdown.rb +85 -2
  94. data/lib/aspera/node_simulator.rb +3 -1
  95. data/lib/aspera/oauth/base.rb +5 -5
  96. data/lib/aspera/oauth/boot.rb +2 -2
  97. data/lib/aspera/oauth/factory.rb +7 -5
  98. data/lib/aspera/oauth/jwt.rb +4 -4
  99. data/lib/aspera/oauth/web.rb +2 -2
  100. data/lib/aspera/persistency_action_once.rb +8 -8
  101. data/lib/aspera/persistency_folder.rb +10 -3
  102. data/lib/aspera/preview/file_types.rb +3 -3
  103. data/lib/aspera/preview/generator.rb +3 -3
  104. data/lib/aspera/preview/utils.rb +1 -1
  105. data/lib/aspera/products/connect.rb +1 -1
  106. data/lib/aspera/products/other.rb +1 -1
  107. data/lib/aspera/products/transferd.rb +1 -1
  108. data/lib/aspera/proxy_auto_config.rb +7 -6
  109. data/lib/aspera/rest.rb +10 -36
  110. data/lib/aspera/rest_error_analyzer.rb +3 -3
  111. data/lib/aspera/rest_errors_aspera.rb +0 -10
  112. data/lib/aspera/rest_list.rb +3 -3
  113. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1853 -510
  114. data/lib/aspera/schema/documentation.rb +57 -26
  115. data/lib/aspera/schema/reader.rb +59 -10
  116. data/lib/aspera/schema/registry.rb +39 -6
  117. data/lib/aspera/secret_hider.rb +7 -2
  118. data/lib/aspera/ssl.rb +4 -4
  119. data/lib/aspera/sync/conf.schema.yaml +2 -2
  120. data/lib/aspera/sync/operations.rb +10 -13
  121. data/lib/aspera/temp_file_manager.rb +1 -1
  122. data/lib/aspera/timer_limiter.rb +1 -1
  123. data/lib/aspera/transfer/faux_file.rb +24 -11
  124. data/lib/aspera/transfer/parameters.rb +23 -21
  125. data/lib/aspera/transfer/result.rb +74 -0
  126. data/lib/aspera/transfer/resumer.rb +6 -6
  127. data/lib/aspera/transfer/spec.rb +18 -1
  128. data/lib/aspera/transfer/spec.schema.yaml +12 -3
  129. data/lib/aspera/uri_reader.rb +2 -2
  130. data/lib/aspera/web_auth.rb +2 -2
  131. data/lib/aspera/web_server_simple.rb +8 -5
  132. data/lib/aspera/yaml.rb +2 -1
  133. data.tar.gz.sig +0 -0
  134. metadata +25 -3
  135. metadata.gz.sig +0 -0
  136. data/lib/aspera/json_rpc.rb +0 -52
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'aspera/schema/registry'
3
4
  require 'aspera/cli/plugins/oauth'
4
5
  require 'aspera/cli/plugins/node'
5
6
  require 'aspera/cli/plugins/ats'
@@ -7,12 +8,14 @@ require 'aspera/cli/transfer_agent'
7
8
  require 'aspera/cli/special_values'
8
9
  require 'aspera/cli/wizard'
9
10
  require 'aspera/agent/node'
11
+ require 'aspera/transfer/result'
10
12
  require 'aspera/transfer/spec'
11
13
  require 'aspera/api/aoc'
12
14
  require 'aspera/api/node'
13
15
  require 'aspera/persistency_action_once'
14
16
  require 'aspera/id_generator'
15
17
  require 'aspera/assert'
18
+ require 'aspera/graphql'
16
19
  require 'securerandom'
17
20
  require 'date'
18
21
 
@@ -56,11 +59,9 @@ module Aspera
56
59
  PACKAGE_LIST_DEFAULT_FIELDS = %w[id name created_at files_completed bytes_transferred].freeze
57
60
 
58
61
  private_constant :REDIRECT_LOCALHOST, :ADMIN_OBJECTS, :PACKAGE_RECEIVED_BASE_QUERY, :PACKAGE_LIST_DEFAULT_FIELDS
59
- class << self
60
- def application_name
61
- 'Aspera on Cloud'
62
- end
62
+ application_name 'Aspera on Cloud'
63
63
 
64
+ class << self
64
65
  # @return [Hash,NilClass]
65
66
  def detect(base_url)
66
67
  # no protocol ?
@@ -99,7 +100,7 @@ module Aspera
99
100
  # or a sequential number if extension == :seq
100
101
  # @param package_info [Hash] Package information
101
102
  # @param destination_folder [String] Base folder
102
- # @param fld. [Array] List of fields of package
103
+ # @param fld [Array] List of fields of package
103
104
  def unique_folder(package_info, destination_folder, fld: nil, seq: false, opt: false)
104
105
  Aspera.assert_array_all(fld, String, type: BadArgument){'fld'}
105
106
  Aspera.assert_values(fld.length, [1, 2]){'fld length'}
@@ -110,7 +111,6 @@ module Aspera
110
111
  # NOTE: it might already exist
111
112
  folder = "#{folder}.#{Environment.instance.sanitized_filename(fld[1])}"
112
113
  end
113
- puts("sub= #{folder}")
114
114
  File.join(destination_folder, folder)
115
115
  end
116
116
  end
@@ -133,7 +133,7 @@ module Aspera
133
133
  test_args: 'organization'
134
134
  }
135
135
  end
136
- options.declare(:use_generic_client, 'Wizard: AoC: use global or org specific jwt client id', allowed: Allowed::TYPES_BOOLEAN, default: Api::AoC.saas_url?(app_url))
136
+ options.declare(:use_generic_client, description: 'Wizard: AoC: use global or org specific jwt client id', allowed: Allowed::TYPES_BOOLEAN, default: Api::AoC.saas_url?(app_url))
137
137
  options.parse_options!
138
138
  # make username mandatory for jwt, this triggers interactive input
139
139
  wiz_username = options.get_option(:username, mandatory: true)
@@ -201,16 +201,17 @@ module Aspera
201
201
  }
202
202
  end
203
203
 
204
+ option :workspace, description: 'Name of workspace', allowed: [String, NilClass], default: Api::AoC::DEFAULT_WORKSPACE
205
+ option :new_user_option, description: 'New user creation option for unknown package recipients', allowed: [Hash, NilClass]
206
+ option :validate_metadata, description: 'Validate shared inbox metadata', allowed: Allowed::TYPES_BOOLEAN, default: true
207
+ option :package_folder, schema: Schema::Registry::PACKAGE_FOLDER_OPTIONS
208
+
204
209
  def initialize(**_)
205
210
  super
206
211
  @cache_workspace_info = nil
207
212
  @cache_home_node_file = nil
208
213
  @cache_api_aoc = nil
209
214
  @scope = Api::AoC::Scope::USER
210
- options.declare(:workspace, 'Name of workspace', allowed: [String, NilClass], default: Api::AoC::DEFAULT_WORKSPACE)
211
- options.declare(:new_user_option, 'New user creation option for unknown package recipients', allowed: Hash)
212
- options.declare(:validate_metadata, 'Validate shared inbox metadata', allowed: Allowed::TYPES_BOOLEAN, default: true)
213
- options.declare(:package_folder, 'Handling of reception of packages in folders', allowed: Hash, default: {})
214
215
  options.parse_options!
215
216
  # add node plugin options (for manual)
216
217
  Node.declare_options(options)
@@ -233,13 +234,13 @@ module Aspera
233
234
  api = Api::AoC.new(
234
235
  scope: @scope,
235
236
  subpath: base_path,
236
- secret_finder: config,
237
+ secret_finder: context.secret_finder,
237
238
  **Oauth.kwargs_from_options(options)
238
239
  )
239
240
  # User set a workspace ?
240
241
  # @type [String, nil]
241
242
  workspace = options.get_option(:workspace)
242
- if !workspace.nil? && (m = Manager.percent_selector(workspace))
243
+ if !workspace.nil? && (m = Parser.percent_selector(workspace))
243
244
  case m[:field]
244
245
  when 'name' then api.ws_ids[:name] = m[:value]
245
246
  when 'id' then api.ws_ids[:id] = m[:value]
@@ -287,8 +288,8 @@ module Aspera
287
288
  end
288
289
 
289
290
  # Get resource identifier from command line, either directly specifying the `id` or from `name` (percent selector).
290
- # @param resource_class_path url path for resource
291
- # @return identifier
291
+ # @param resource_class_path [String] url path for resource
292
+ # @return [String] identifier
292
293
  def get_resource_id_from_args(resource_class_path)
293
294
  return options.instance_identifier do |field, value|
294
295
  Aspera.assert_values(field, ['name'], type: BadArgument){'selector field'}
@@ -302,15 +303,17 @@ module Aspera
302
303
  end
303
304
 
304
305
  # List all entities, given additional, default and user's queries
305
- # @param resource_class_path path to query on API
306
- # @param fields fields to display
307
- # @param base_query a query applied always
308
- # @param default_query default query unless overridden by user
306
+ # @param resource_class_path [String] path to query on API
307
+ # @param fields [Array, nil] fields to display
308
+ # @param base_query [Hash] a query applied always
309
+ # @param default_query [Hash] default query unless overridden by user
310
+ # @param query_component [String, nil] registry component key; when set, --query=help shows filter schema
309
311
  # @yieldparam query [Hash] The user's or default query for modification
310
- def result_list(resource_class_path, fields: nil, base_query: {}, default_query: {})
312
+ def result_list(resource_class_path, fields: nil, base_query: {}, default_query: {}, query_component: nil)
311
313
  Aspera.assert_type(base_query, Hash)
312
314
  Aspera.assert_type(default_query, Hash)
313
- query = query_read_delete(default: default_query)
315
+ qs_path = query_component ? Schema::Registry.query_params(query_component, resource_class_path) : nil
316
+ query = query_read_delete(default: default_query, schema: qs_path)
314
317
  # caller may add specific modifications or checks to query
315
318
  yield(query) if block_given?
316
319
  result = aoc_api.read_with_paging(resource_class_path, base_query.merge(query).compact)
@@ -321,7 +324,7 @@ module Aspera
321
324
  def resolve_dropbox_name_default_ws_id(query)
322
325
  if query.key?('dropbox_name')
323
326
  # convenience: specify name instead of id
324
- raise BadArgument, 'Use field dropbox_name or dropbox_id, not both' if query.key?('dropbox_id')
327
+ Aspera.assert(!query.key?('dropbox_id'), type: BadArgument){'Use field dropbox_name or dropbox_id, not both'}
325
328
  # TODO : craft a query that looks for dropbox only in current workspace
326
329
  query['dropbox_id'] = aoc_api.lookup_with_q('dropboxes', value: query.delete('dropbox_name'))['id']
327
330
  end
@@ -331,7 +334,6 @@ module Aspera
331
334
  end
332
335
 
333
336
  # List all packages according to `query` option.
334
- # @param <none>
335
337
  # @return [Hash] {items,total} with all packages according to combination of user's query and default query
336
338
  def list_all_packages_with_query
337
339
  query = query_read_delete(default: {})
@@ -344,11 +346,13 @@ module Aspera
344
346
  FILES_COMMANDS = (Node::COMMANDS_GEN4 + %i[transfer]).freeze
345
347
 
346
348
  # Execute a node gen4 command starting at given node and file IDs
347
- # @param command_repo [Symbol] Command to execute
348
- # @param node_id [String] Node identifier
349
- # @param file_id [String] Root file id for the operation (can be AK root, or other, e.g. package, or link). If `nil` use AK root file id.
350
- # @param scope [String] node scope (Node::SCOPE_<USER|ADMIN>), or nil (requires secret)
351
- def execute_nodegen4_command(command_repo, node_id, file_id: nil, scope: nil)
349
+ # @param command_repo [Symbol] Command to execute
350
+ # @param node_id [String] Node identifier
351
+ # @param file_id [String] Root file id for the operation. If nil, uses AK root file id.
352
+ # @param scope [String] node scope (Node::SCOPE_<USER|ADMIN>), or nil (requires secret)
353
+ # @param transfer_direction [Symbol] :push or :pull (only for command_repo == :transfer)
354
+ # @param transfer_source [String] source folder (only for command_repo == :transfer)
355
+ def execute_nodegen4_command(command_repo, node_id, file_id: nil, scope: nil, transfer_direction: nil, transfer_source: nil)
352
356
  top_node_api = aoc_api.node_api_from(
353
357
  node_id: node_id,
354
358
  scope: scope,
@@ -358,13 +362,16 @@ module Aspera
358
362
  node_plugin = Node.new(context: context, api: top_node_api)
359
363
  case command_repo
360
364
  when *Node::COMMANDS_GEN4
361
- return node_plugin.execute_command_gen4(command_repo, file_id)
365
+ # For permission: the handler consumes the path first then re-dispatches to sub-commands.
366
+ # Calling dispatch_from_registry with skip_setup would bypass path consumption and fail.
367
+ return node_plugin.send(:"action_access_keys_do_#{command_repo}", do_root_file_id: file_id) if command_repo.eql?(:permission)
368
+ return node_plugin.dispatch_from_registry([:access_keys, :do, command_repo], {do_root_file_id: file_id}, skip_setup: true)
362
369
  when :transfer
363
370
  # client side is agent
364
371
  # server side is transfer server
365
372
  # in same workspace
366
- push_pull = options.get_next_argument('direction', accept_list: %i[push pull])
367
- source_folder = options.get_next_argument('folder or source files', validation: String)
373
+ push_pull = transfer_direction
374
+ source_folder = transfer_source
368
375
  case push_pull
369
376
  when :push
370
377
  client_direction = Transfer::Spec::DIRECTION_SEND
@@ -403,585 +410,154 @@ module Aspera
403
410
 
404
411
  # Execute an action on admin resources
405
412
  # @param resource_type [Symbol] One of ADMIN_OBJECTS
406
- def execute_resource_action(resource_type)
407
- # Set to `true` is resource creation requires a workspace id
408
- require_workspace_id = false
409
- # Default fields to display
410
- list_default_fields = %w[id name]
411
- # Default query for `list` action
412
- list_default_query = {}
413
- # In result of create, what is the field that contains the id of the created resource
414
- id_result = 'id'
415
- supported_operations = Operations::ALL
416
- # API path
417
- resource_class_path = "#{resource_type}s"
418
- resource_class_path = resource_class_path.gsub(/ys$/, 'ies')
419
- # path in openapi where post is located to get creation schema
420
- create_schema_path = resource_class_path
421
- case resource_type
422
- when :client
423
- supported_operations += %i[set_pub_key]
424
- # schema_create_modify = Schema::Registry.req_body(Schema::Registry::AOC, "#{resource_class_path}.post")
425
- when :client_access_key
426
- resource_class_path = "admin/#{resource_type}s"
427
- when :client_registration_token
428
- resource_class_path = "admin/#{resource_type}s"
429
- list_default_fields = %w[id value data.client_subject_scopes data.name created_at]
430
- id_result = 'token'
431
- when :contact
432
- list_default_fields = %w[source_type source_id name email]
433
- # list_default_query = {'include_only_user_personal_contacts' => true} if @scope == Api::AoC::Scope::USER
434
- when :configuration_policy, :network_policy
435
- list_default_fields = nil
436
- when :dropbox
437
- require_workspace_id = true
438
- resource_class_path = "#{resource_type}es"
439
- create_schema_path = resource_class_path
440
- when :group, :saml_configuration
441
- create_schema_path = nil
442
- when :group_membership
443
- list_default_fields = %w[id group_id member_type member_id]
444
- create_schema_path = nil
445
- when :kms_profile
446
- resource_class_path = "integrations/#{resource_type}s"
447
- create_schema_path = nil
448
- when :node
449
- list_default_fields = %w[id name host access_key]
450
- supported_operations += %i[do bearer_token]
451
- when :operation
452
- list_default_fields = %w[id type status created_at updated_at workspace_id user_id workspace_membership_id group_membership_id]
453
- supported_operations = %i[list show modify]
454
- when :organization, :self
455
- supported_operations = Operations::SINGLETON
456
- resource_instance_path = resource_class_path = resource_type
457
- when :short_link
458
- list_default_fields = %w[id short_url data.url_token_data.purpose password_enabled password_protected updated_by_user_id updated_at]
459
- when :user
460
- list_default_fields = %w[id name email]
461
- supported_operations += %i[preferences notifications]
462
- when :workspace
463
- supported_operations += %i[shared_folder dropbox]
464
- when :workspace_membership
465
- list_default_fields = %w[id workspace_id member_type member_id]
413
+ # Per-resource configuration for admin CRUD resources.
414
+ # Keys: path, list_fields, id_result, require_ws_id, create_schema, extra_ops, singleton, op_setup
415
+ # op_setup: Hash of op => setup method name, used for ops that require consuming an instance identifier.
416
+ # For Operations::INSTANCE ops (show/modify/delete), use the auto-generated :setup_admin_<res>_instance.
417
+ # For extra_ops that are instance ops, specify explicitly (or rely on the auto-generated one).
418
+ ADMIN_OBJECT_CONFIG = {
419
+ client: {extra_ops: %i[set_pub_key], extra_op_args: {set_pub_key: [{name: :private_key_pem, type: String}]}},
420
+ client_access_key: {path: 'admin/client_access_keys'},
421
+ client_registration_token: {path: 'admin/client_registration_tokens', list_fields: %w[id value data.client_subject_scopes data.name created_at], id_result: 'token'},
422
+ configuration_policy: {list_fields: nil},
423
+ contact: {list_fields: %w[source_type source_id name email]},
424
+ dropbox: {path: 'dropboxes', require_ws_id: true},
425
+ dropbox_membership: {},
426
+ group: {create_schema: false},
427
+ group_membership: {list_fields: %w[id group_id member_type member_id], create_schema: false},
428
+ kms_profile: {path: 'integrations/kms_profiles', create_schema: false},
429
+ network_policy: {list_fields: nil},
430
+ node: {list_fields: %w[id name host access_key], extra_ops: %i[do bearer_token], extra_op_args: {bearer_token: [{name: :scope, mandatory: false, default: nil}]}},
431
+ 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]},
432
+ organization: {singleton: true},
433
+ package: {},
434
+ saml_configuration: {create_schema: false},
435
+ self: {singleton: true},
436
+ short_link: {list_fields: %w[id short_url data.url_token_data.purpose password_enabled password_protected updated_by_user_id updated_at]},
437
+ user: {list_fields: %w[id name email], extra_ops: %i[preferences notifications], op_setup: {preferences: :setup_admin_user_instance, notifications: :setup_admin_user_instance}},
438
+ workspace: {extra_ops: %i[shared_folder dropbox], op_setup: {shared_folder: :setup_admin_workspace_shared_folder, dropbox: :setup_admin_workspace_dropbox}},
439
+ workspace_membership: {list_fields: %w[id workspace_id member_type member_id]}
440
+ }.freeze
441
+ private_constant :ADMIN_OBJECT_CONFIG
442
+
443
+ class << self
444
+ # @return [String] AoC REST path for an admin resource type
445
+ def aoc_res_path(res)
446
+ cfg = ADMIN_OBJECT_CONFIG.fetch(res, {})
447
+ return cfg[:path] if cfg[:path]
448
+ "#{res}s".gsub(/ys$/, 'ies')
466
449
  end
467
- # Default location of creation payload in schema
468
- schema_create_modify = Schema::Registry.req_body(Schema::Registry::AOC, "#{create_schema_path}.post") if create_schema_path && supported_operations.include?(:create)
469
- command = options.get_next_command(supported_operations)
470
- # Require identifier for non global commands
471
- if (supported_operations != Operations::SINGLETON) && !Operations::GLOBAL.include?(command)
472
- res_id = get_resource_id_from_args(resource_class_path)
473
- resource_instance_path = "#{resource_class_path}/#{res_id}"
450
+
451
+ # @return [Hash] {path:, ops:, id_result:, require_ws_id:, list_fields:, schema:, query_component:}
452
+ def aoc_res_cfg(res)
453
+ cfg = ADMIN_OBJECT_CONFIG.fetch(res, {})
454
+ path = aoc_res_path(res)
455
+ ops = cfg[:ops] || (Base::Operations::ALL + (cfg[:extra_ops] || []))
456
+ schema = cfg[:create_schema] == false ? nil : Schema::Registry.req_body(Schema::Registry::AOC, "#{path}.post")
457
+ {
458
+ path: path,
459
+ ops: ops,
460
+ id_result: cfg[:id_result] || 'id',
461
+ require_ws_id: cfg[:require_ws_id] || false,
462
+ list_fields: cfg.key?(:list_fields) ? cfg[:list_fields] : %w[id name],
463
+ schema: schema,
464
+ query_component: Schema::Registry::AOC
465
+ }
474
466
  end
475
- case command
476
- when :create
477
- # TODO: report inconsistency: creation url is !=, and does not return id.
478
- resource_class_path = 'admin/client_registration/token' if resource_class_path.eql?('admin/client_registration_tokens')
479
- workspace_id = aoc_api.workspace_info[:id] if require_workspace_id
480
- return do_bulk_operation(command: command, descr: 'creation data', id_result: id_result, schema: schema_create_modify) do |params|
481
- params['workspace_id'] = workspace_id if require_workspace_id && workspace_id && !params.key?('workspace_id')
482
- aoc_api.create(resource_class_path, params)
467
+
468
+ # DSL helper: register the 5 short_link leaf commands under the given parent path
469
+ # and define the corresponding action methods on `base`.
470
+ # @param base [Class] the plugin class
471
+ # @param parent_path [Array<Symbol>] full path ending with :short_link
472
+ def register_short_link_commands(base, parent_path)
473
+ base.commands_under(parent_path) do
474
+ base.command(
475
+ :create, description: 'Create',
476
+ arguments: [{name: :custom_data, type: Hash, mandatory: false, default: {}}]
477
+ )
478
+ base.command(
479
+ :modify, description: 'Modify',
480
+ arguments: [{name: :short_link_id, type: :identifier}, {name: :custom_data, type: Hash, mandatory: false, default: {}}]
481
+ )
482
+ base.command(:list, description: 'List short links')
483
+ base.command(
484
+ :show, description: 'Show a short link',
485
+ arguments: [{name: :short_link_id, type: :identifier}]
486
+ )
487
+ base.command(
488
+ :delete, description: 'Delete a short link',
489
+ arguments: [{name: :short_link_id, type: :identifier}]
490
+ )
483
491
  end
484
- when :list
485
- return result_list(resource_class_path, fields: list_default_fields, default_query: list_default_query)
486
- when :show
487
- object = aoc_api.read(resource_instance_path, query_read_delete)
488
- return Result::SingleObject.new(object, fields: Formatter.all_but('certificate'))
489
- when :modify
490
- changes = options.get_next_argument('properties', validation: Hash, schema: schema_create_modify)
491
- return do_bulk_operation(command: command, values: res_id) do |one_id|
492
- aoc_api.update("#{resource_class_path}/#{one_id}", changes)
493
- {'id' => one_id}
492
+ base.define_action_method(parent_path + [:create]) do |custom_data: {}, **ctx|
493
+ sl_exec_create(custom_data, **ctx)
494
494
  end
495
- when :delete
496
- return do_bulk_operation(command: command, values: res_id) do |one_id|
497
- aoc_api.delete("#{resource_class_path}/#{one_id}")
498
- {'id' => one_id}
495
+ base.define_action_method(parent_path + [:list]) do |**ctx|
496
+ sl_exec_list(**sl_fetch_list(**ctx))
499
497
  end
500
- when :set_pub_key
501
- # special : reads private and generate public
502
- the_private_key = options.get_next_argument('private_key PEM value', validation: String)
503
- the_public_key = OpenSSL::PKey::RSA.new(the_private_key).public_key.to_s
504
- aoc_api.update(resource_instance_path, {jwt_grant_enabled: true, public_key: the_public_key})
505
- return Result::Success.new
506
- when :do
507
- command_repo = options.get_next_command(FILES_COMMANDS)
508
- return execute_nodegen4_command(command_repo, res_id, scope: Api::Node::Scope::ADMIN)
509
- when :bearer_token
510
- node_api = aoc_api.node_api_from(
511
- node_id: res_id,
512
- scope: options.get_next_argument('scope', default: Api::Node::Scope::ADMIN)
513
- )
514
- return Result::Text.new(node_api.oauth.authorization)
515
- when :dropbox
516
- command_shared = options.get_next_command(%i[list])
517
- case command_shared
518
- when :list
519
- query = options.get_option(:query) || {}
520
- res_data = aoc_api.read('dropboxes', query.merge({'workspace_id'=>res_id}))
521
- return Result::ObjectList.new(res_data, fields: %w[id name description])
498
+ base.define_action_method(parent_path + [:show]) do |**ctx|
499
+ sl_exec_show(**sl_fetch_list(**ctx))
522
500
  end
523
- when :shared_folder
524
- query = options.get_option(:query) || Api::AoC.workspace_access(res_id).merge({'admin' => true})
525
- shared_folders = aoc_api.read_with_paging("#{resource_instance_path}/permissions", query)[:items]
526
- # inside a workspace
527
- command_shared = options.get_next_command(%i[list node member])
528
- case command_shared
529
- when :list
530
- return Result::ObjectList.new(shared_folders, fields: %w[id node_name node_id file_id file.path tags.aspera.files.workspace.share_as])
531
- when :node
532
- shared_folder_id = options.instance_identifier(description: 'Shared folder ID')
533
- shared_folder = shared_folders.find{ |i| i['id'].eql?(shared_folder_id)}
534
- Aspera.assert(shared_folder, 'shared folder not found')
535
- command_repo = options.get_next_command(FILES_COMMANDS)
536
- return execute_nodegen4_command(command_repo, shared_folder['node_id'], file_id: shared_folder['file_id'], scope: Api::Node::Scope::ADMIN)
537
- when :member
538
- shared_folder_id = options.instance_identifier(description: 'Shared folder ID')
539
- shared_folder = shared_folders.find{ |i| i['id'].eql?(shared_folder_id)}
540
- Aspera.assert(shared_folder, 'shared folder not found')
541
- command_shared_member = options.get_next_command(%i[list])
542
- case command_shared_member
543
- when :list
544
- node_api = aoc_api.node_api_from(
545
- node_id: shared_folder['node_id'],
546
- workspace_id: res_id,
547
- workspace_name: nil,
548
- scope: Api::Node::Scope::USER
549
- )
550
- result = node_api.read(
551
- 'permissions',
552
- {'file_id' => shared_folder['file_id'], 'tag' => "aspera.files.workspace.id=#{res_id}"}
553
- )
554
- result.each do |item|
555
- item['member'] = begin
556
- if Api::AoC.workspace_access?(item)
557
- {'name'=>'[Internal permission]'}
558
- else
559
- aoc_api.read("admin/#{item['access_type']}s/#{item['access_id']}") rescue {'name': 'not found'}
560
- end
561
- rescue => e
562
- {'name'=>e.to_s}
563
- end
564
- end
565
- # TODO : read users and group name and add, if query "include_members"
566
- return Result::ObjectList.new(result, fields: %w[access_type access_id access_level last_updated_at member.name member.email member.system_group_type member.system_group])
567
- end
501
+ base.define_action_method(parent_path + [:delete]) do |**ctx|
502
+ sl_exec_delete(**sl_fetch_list(**ctx), **ctx)
568
503
  end
569
- when :preferences, :notifications
570
- user_preferences_res = "#{resource_instance_path}/#{command.eql?(:preferences) ? 'user_interaction_preferences' : 'notification_preferences'}"
571
- case options.get_next_command(%i[show modify])
572
- when :show
573
- return Result::SingleObject.new(aoc_api.read(user_preferences_res))
574
- when :modify
575
- aoc_api.update(user_preferences_res, options.get_next_argument('properties', validation: Hash))
576
- return Result::Status.new('modified')
504
+ base.define_action_method(parent_path + [:modify]) do |custom_data: {}, **ctx|
505
+ sl_exec_modify(custom_data, **sl_fetch_list(**ctx), **ctx)
577
506
  end
578
- else Aspera.error_unexpected_value(command)
579
507
  end
580
508
  end
581
509
 
582
- def execute_application_action
583
- apps_info = aoc_api.read('admin/apps')
584
- # List of allowed symbols for app type
585
- all_app_syms = apps_info.map{ |i| i['app_type'].to_sym}
586
- command_apps = options.get_next_command(%i[types settings instance membership])
587
- case command_apps
588
- when :types
589
- return Result::ObjectList.new(apps_info)
590
- when :settings
591
- app_type = options.get_next_command(all_app_syms)
592
- cmd_path = "/apps/#{app_type}/settings"
593
- command_app_settings = options.get_next_command(Operations::SINGLETON)
594
- case command_app_settings
595
- when :show
596
- return Result::SingleObject.new(aoc_api.read(cmd_path))
597
- when :modify
598
- aoc_api.update(cmd_path, options.get_next_argument('properties', validation: Hash))
599
- return Result::Status.new('modified')
600
- end
601
- when :instance
602
- list_default_query = {workspace_id: aoc_api.workspace_info[:id]}
603
- list_default_fields = %w[id app_type available workspace_id]
604
- command_app_instances = options.get_next_command(%i[list] + Operations::SINGLETON)
605
- resource_path = 'admin/apps_new'
606
- if Operations::SINGLETON.include?(command_app_instances)
607
- app_type = options.get_next_command(all_app_syms)
608
- resource_path = "#{resource_path}/#{app_type}/#{options.instance_identifier(description: "#{app_type} identifier")}"
609
- end
610
- case command_app_instances
611
- when :list
612
- return result_list(resource_path, fields: list_default_fields, default_query: list_default_query)
613
- when :show
614
- return Result::SingleObject.new(aoc_api.read(resource_path, query_read_delete))
615
- when :modify
616
- aoc_api.update(resource_path, options.get_next_argument('properties', validation: Hash))
617
- return Result::Status.new('modified')
618
- end
619
- when :membership
620
- resource_path = 'apps/app_memberships'
621
- command_app_member = options.get_next_command(%i[create list show delete])
622
- resource_path = "#{resource_path}/#{options.instance_identifier(description: 'membership id')}" unless Operations::GLOBAL.include?(command_app_member)
623
- case command_app_member
624
- when :list
625
- return result_list(resource_path)
626
- when :delete
627
- aoc_api.delete(resource_path)
628
- return Result::Status.new('deleted')
629
- when :show
630
- return Result::SingleObject.new(aoc_api.read(resource_path, query_read_delete))
631
- when :create
632
- data = options.get_next_argument('membership properties', validation: Hash)
633
- app_type = data.delete('app_type')
634
- Aspera.assert_type(app_type, String){'app_type'}
635
- Aspera.assert_values(app_type.to_sym, all_app_syms){'app_type'}
636
- return Result::SingleObject.new(aoc_api.create("apps/#{app_type}/app_memberships", data))
637
- end
638
- end
639
- end
510
+ # Instance delegators so instance methods can call aoc_res_path/aoc_res_cfg without self.class.
511
+ def aoc_res_path(res) = self.class.aoc_res_path(res)
512
+ def aoc_res_cfg(res) = self.class.aoc_res_cfg(res)
513
+
514
+ # Known fixed set of AoC application types (verified against API: activity, automation, files, packages)
515
+ APP_TYPES = %i[activity automation files packages].freeze
640
516
 
641
517
  ADMIN_ACTIONS = %i[bearer_token application ats usage_reports analytics subscription auth_providers].concat(ADMIN_OBJECTS).freeze
642
518
 
643
- def execute_admin_action
644
- # change scope to admin
645
- change_api_scope(Api::AoC::Scope::ADMIN)
646
- command_admin = options.get_next_command(ADMIN_ACTIONS)
647
- case command_admin
648
- when :bearer_token
649
- return Result::Text.new(aoc_api.oauth.authorization)
650
- when *ADMIN_OBJECTS
651
- return execute_resource_action(command_admin)
652
- when :application
653
- return execute_application_action
654
- when :auth_providers
655
- command_auth_prov = options.get_next_command(%i[list update])
656
- case command_auth_prov
657
- when :list
658
- return result_list('admin/auth_providers')
659
- when :update
660
- Aspera.error_not_implemented
661
- end
662
- when :subscription
663
- org = aoc_api.read('organization')
664
- bss_graphql = api_from_options('bss/platform/graphql')
665
- command_subscription = options.get_next_command(%i[account usage])
666
- case command_subscription
667
- when :account
668
- # cspell:disable
669
- graphql_query = <<-GRAPHQL
670
- query ($organization_id: ID!) {
671
- aoc (organization_id: $organization_id) {
672
- bssSubscription {
673
- aocVersion
674
- endDate
675
- startDate
676
- termMonths
677
- plan
678
- trial
679
- termType
680
- aocOrganizations {
681
- id
682
- }
683
- additionalStorageVolumeGb
684
- additionalEgressVolumeGb
685
- term {
686
- startDate
687
- endDate
688
- transferVolumeGb
689
- egressVolumeGb
690
- storageVolumeGb
691
- transferVolumeOffsetGb
692
- }
693
- paygoRate {
694
- transferRate
695
- storageRate
696
- currency
697
- }
698
- aocPlanData {
699
- tier
700
- trial
701
- workspaces { max }
702
- users {
703
- planAmount
704
- max
705
- }
706
- samlIntegration
707
- activity
708
- sharedInboxes
709
- uniqueUrls
710
- support
711
- watermarking
712
- byok
713
- automation { planAmount, max }
714
- }
715
- }
716
- }
717
- }
718
- GRAPHQL
719
- # cspell:enable
720
- result = bss_graphql.create(nil, {query: graphql_query, variables: {organization_id: org['id']}})['data']
721
- return Result::SingleObject.new(result['aoc']['bssSubscription'])
722
- when :usage
723
- # cspell:disable
724
- graphql_query = <<-GRAPHQL
725
- query ($organization_id: ID!, $startDate: Date!, $endDate: Date!, $aggregate: TransferUsageAggregateOption!) {
726
- aoc (organization_id: $organization_id) {
727
- bssSubscription {
728
- aocOrganizations { id }
729
- additionalStorageVolumeGb
730
- additionalEgressVolumeGb
731
- aocPlanData {
732
- tier
733
- trial
734
- }
735
- term {
736
- transferVolumeGb
737
- egressVolumeGb
738
- storageVolumeGb
739
- startDate
740
- endDate
741
- transferVolumeOffsetGb
742
- }
743
- termMonths
744
- transferUsages (startDate: $startDate, endDate: $endDate, aggregate: $aggregate) { mbTotal }
745
- egressUsages (startDate: $startDate, endDate: $endDate, aggregate: $aggregate) { usageMb }
746
- }
747
- subscriptionEntitlements {
748
- id
749
- transferUsages (startDate: $startDate, endDate: $endDate, aggregate: $aggregate) { mbTotal }
750
- egressUsages (startDate: $startDate, endDate: $endDate, aggregate: $aggregate) { usageMb }
751
- }
752
- }
753
- }
754
- GRAPHQL
755
- aggregate = options.get_next_argument('aggregation', accept_list: %i[ALL MONTHLY], default: :ALL)
756
- today = Date.today
757
- start_date = options.get_next_argument('start date', mandatory: false, default: today.prev_year.strftime('%Y-%m-%d'))
758
- end_date = options.get_next_argument('end date', mandatory: false, default: today.strftime('%Y-%m-%d'))
759
- # cspell:enable
760
- result = bss_graphql.create(
761
- nil,
762
- {
763
- query: graphql_query,
764
- variables: {
765
- organization_id: org['id'],
766
- aggregate: aggregate,
767
- startDate: start_date,
768
- endDate: end_date
769
- }
770
- }
771
- )['data']
772
- return Result::SingleObject.new(result['aoc'])
773
- end
774
- when :ats
775
- ats_api = Rest.new(**aoc_api.params.deep_merge({
776
- base_url: "#{aoc_api.base_url}/admin/ats/pub/v1",
777
- auth: {params: {scope: Api::AoC::Scope::ADMIN_USER}}
778
- }))
779
- return Ats.new(context: context, api: ats_api).execute_action
780
- when :analytics
781
- analytics_api = Rest.new(**aoc_api.params.deep_merge({
782
- base_url: "#{aoc_api.base_url.gsub('/api/v1', '')}/analytics/v2",
783
- auth: {params: {scope: Api::AoC::Scope::ADMIN_USER}}
784
- }))
785
- command_analytics = options.get_next_command(%i[application_events transfers files])
786
- case command_analytics
787
- when :application_events
788
- event_type = command_analytics.to_s
789
- events = analytics_api.read("organizations/#{aoc_api.current_user_info['organization_id']}/#{event_type}")[event_type]
790
- return Result::ObjectList.new(events)
791
- when :transfers
792
- event_type = command_analytics.to_s
793
- event_resource_type = options.get_next_argument('resource', accept_list: %i[organizations users nodes])
794
- event_resource_id = options.get_next_argument("#{event_resource_type} identifier", mandatory: false) ||
795
- case event_resource_type
796
- when :organizations then aoc_api.current_user_info['organization_id']
797
- when :users then aoc_api.current_user_info['id']
798
- when :nodes then aoc_api.current_user_info['read_only_home_node_id']
799
- else Aspera.error_unreachable_line
800
- end
801
- filter = query_read_delete(default: {})
802
- filter['limit'] ||= 100
803
- if options.get_option(:once_only, mandatory: true)
804
- saved_date = []
805
- start_date_persistency = PersistencyActionOnce.new(
806
- manager: persistency,
807
- data: saved_date,
808
- id: IdGenerator.from_list(
809
- 'aoc_ana_date',
810
- options.get_option(:url, mandatory: true),
811
- aoc_api.workspace_info[:name],
812
- event_resource_type.to_s,
813
- event_resource_id
814
- )
815
- )
816
- start_date_time = saved_date.first
817
- stop_date_time = Time.now.utc.strftime('%FT%T.%LZ')
818
- saved_date[0] = stop_date_time
819
- filter['start_time'] = start_date_time unless start_date_time.nil?
820
- filter['stop_time'] = stop_date_time
821
- end
822
- events = analytics_api.read("#{event_resource_type}/#{event_resource_id}/#{event_type}", filter)[event_type]
823
- start_date_persistency&.save
824
- if !options.get_option(:notify_to).nil?
825
- events.each do |tr_event|
826
- config.send_email_template(values: {ev: tr_event})
827
- end
828
- end
829
- return Result::ObjectList.new(events)
830
- when :files
831
- event_type = command_analytics.to_s
832
- event_resource_type = options.get_next_argument('resource', accept_list: %i[organizations users nodes])
833
- event_resource_id = options.instance_identifier(description: "#{event_resource_type} identifier")
834
- event_resource_id =
835
- case event_resource_type
836
- when :organizations then aoc_api.current_user_info['organization_id']
837
- when :users then aoc_api.current_user_info['id']
838
- when :nodes then aoc_api.current_user_info['read_only_home_node_id']
839
- else Aspera.error_unreachable_line
840
- end if event_resource_id.empty?
841
- event_uuid = options.instance_identifier(description: 'event uuid')
842
- filter = query_read_delete(default: {})
843
- filter['limit'] ||= 100
844
- events = analytics_api.read("#{event_resource_type}/#{event_resource_id}/transfers/#{event_uuid}/#{event_type}", filter)[event_type]
845
- return Result::ObjectList.new(events)
846
- end
847
- when :usage_reports
848
- return result_list('usage_reports', base_query: workspace_id_hash)
849
- end
519
+ # Build analytics REST API (shared by action_admin_analytics_*)
520
+ def build_analytics_api
521
+ Rest.new(**aoc_api.params.deep_merge({
522
+ base_url: "#{aoc_api.base_url.gsub('/api/v1', '')}/analytics/v2",
523
+ auth: {params: {scope: Api::AoC::Scope::ADMIN_USER}}
524
+ }))
850
525
  end
851
526
 
852
- # Create a short link for the given entity: Shared folder or Shared Inbox
853
- # Short link entity: `short_links` have:
854
- # - a numerical id, e.g. `764412`
855
- # - a resource type, e.g. `UrlToken`
856
- # - a resource id, e.g. `scQ7uXPbvQ`
857
- # - a short URL path, e.g. `dxyRpT9`
858
- # @param shared_data [Hash] Information for shared data: dropbox_id+name or file_id+node_id
859
- # @param shared_data [Hash] Shared data for the short link
860
- # @yieldparam operation [Symbol] Operation type (:create, :update, :delete)
861
- # @yieldparam resource_id [String] Resource ID for permission management
862
- # @yieldparam access_levels [Object] Access levels for permissions
863
- def short_link_command(**shared_data, &perm_block)
864
- link_type = options.get_next_argument('link access (public or private)', accept_list: %i[public private])
527
+ # Compute short-link purposes from shared_data keys and link_type.
528
+ # @param shared_data [Hash] :dropbox_id+:name or :file_id+:node_id
529
+ # @param link_type [Symbol] :public or :private
530
+ # @return [Array(String,String)] [token_purpose, short_link_purpose]
531
+ def short_link_purposes(shared_data, link_type)
865
532
  if shared_data.keys.sort == %i[dropbox_id name]
866
- # Packages app, Shared inbox
867
533
  token_purpose = 'send_package_to_dropbox'
868
534
  short_link_purpose = link_type.eql?(:public) ? 'send_package_to_dropbox' : 'shared_folder_auth_link'
869
535
  elsif shared_data.keys.sort == %i[file_id node_id]
870
- # Files app, Shared folder
871
536
  token_purpose = 'view_shared_file'
872
537
  short_link_purpose = link_type.eql?(:public) ? 'token_auth_redirection' : 'shared_folder_auth_link'
873
538
  else
874
539
  Aspera.error_unexpected_value(shared_data.keys)
875
540
  end
876
- command = options.get_next_command(%i[create delete list show] + (link_type.eql?(:public) ? %i[modify] : []))
877
- case command
878
- when :create
879
- # Add workspace id
880
- workspace_id_hash(shared_data)
881
- create_payload = {
882
- purpose: short_link_purpose,
883
- user_selected_name: nil
884
- }
885
- case link_type
886
- when :private
887
- create_payload[:data] = shared_data
888
- when :public
889
- create_payload[:expires_at] = nil
890
- create_payload[:password_enabled] = false
891
- shared_data[:name] = ''
892
- create_payload[:data] = {
893
- aoc: true,
894
- url_token_data: {
895
- data: shared_data,
896
- purpose: token_purpose
897
- }
898
- }
899
- end
900
- custom_data = value_create_modify(command: command, default: {})
901
- access_levels = custom_data.delete('access_levels')
902
- if (pass = custom_data.delete('password'))
903
- create_payload[:data][:url_token_data][:password] = pass
904
- create_payload[:password_enabled] = true
905
- end
906
- create_payload.deep_merge!(custom_data)
907
- result_create_short_link = aoc_api.create('short_links', create_payload)
908
- # Creation: perm_block: permission on node
909
- yield(:create, result_create_short_link['resource_id'], access_levels) if block_given? && link_type.eql?(:public)
910
- return Result::SingleObject.new(result_create_short_link)
911
- when :delete, :list, :show, :modify
912
- workspace_id_hash(shared_data)
913
- query = if link_type.eql?(:private)
914
- shared_data
915
- else
916
- {
917
- url_token_data: {
918
- data: shared_data,
919
- purpose: token_purpose
920
- }
921
- }
922
- end
923
- list_params = {
924
- json_query: query.to_json,
925
- purpose: short_link_purpose,
926
- edit_access: true,
927
- # embed: 'updated_by_user',
928
- sort: '-created_at'
929
- }
930
- short_list = aoc_api.read_with_paging('short_links', list_params.merge(query_read_delete(default: {})).compact)
931
- case command
932
- when :delete
933
- one_id = options.instance_identifier(description: 'short link id')
934
- if link_type.eql?(:public)
935
- found = short_list[:items].find{ |item| item['id'].eql?(one_id)}
936
- raise BadIdentifier.new('Short link', one_id) if found.nil?
937
- yield(:delete, found['resource_id'], nil)
938
- end
939
- aoc_api.delete("short_links/#{one_id}", {
940
- edit_access: true,
941
- json_query: shared_data.to_json
942
- })
943
- return Result::Status.new('deleted')
944
- when :list
945
- return Result::ObjectList.new(short_list[:items], fields: Formatter.all_but('data'), total: short_list[:total])
946
- when :show
947
- one_id = options.instance_identifier(description: 'short link id')
948
- found = short_list[:items].find{ |item| item['id'].eql?(one_id)}
949
- raise BadIdentifier.new('Short link', one_id) if found.nil?
950
- return Result::SingleObject.new(found, fields: Formatter.all_but('data'))
951
- when :modify
952
- one_id = options.instance_identifier(description: 'short link id')
953
- node_file = shared_data.slice(:node_id, :file_id)
954
- modify_payload = {
955
- edit_access: true,
956
- json_query: node_file
957
- }
958
- custom_data = value_create_modify(command: command)
959
- if (pass = custom_data.delete('password'))
960
- modify_payload[:password_enabled] = true
961
- modify_payload[:data] = {
962
- url_token_data: {
963
- password: pass,
964
- data: node_file
965
- }
966
- }
967
- else
968
- modify_payload[:password_enabled] = false
969
- end
970
- if custom_data.delete('access_levels')
971
- # Modification: perm_block: permission on node
972
- found = short_list[:items].find{ |item| item['id'].eql?(one_id)}
973
- raise BadIdentifier.new('Short link', one_id) if found.nil?
974
- yield(:update, found['resource_id'], access_levels)
975
- end
976
- modify_payload.deep_merge!(custom_data)
977
- aoc_api.update("short_links/#{one_id}", modify_payload)
978
- return Result::Status.new('modified')
979
- end
980
- else Aspera.error_unexpected_value(command)
541
+ [token_purpose, short_link_purpose]
542
+ end
543
+
544
+ # Build the list_params hash used by delete/list/show/modify short link operations.
545
+ # @return [Hash]
546
+ def short_link_list_params(shared_data:, link_type:, token_purpose:, short_link_purpose:, **)
547
+ query = if link_type.eql?(:private)
548
+ shared_data
549
+ else
550
+ {url_token_data: {data: shared_data, purpose: token_purpose}}
981
551
  end
552
+ {
553
+ json_query: query.to_json,
554
+ purpose: short_link_purpose,
555
+ edit_access: true,
556
+ sort: '-created_at'
557
+ }
982
558
  end
983
559
 
984
- # @return persistency object if option `once_only` is used.
560
+ # @return [PersistencyActionOnce, nil] persistency object if option `once_only` is used.
985
561
  def package_persistency
986
562
  return unless options.get_option(:once_only, mandatory: true)
987
563
  # TODO: add query info to id
@@ -1003,290 +579,1027 @@ module Aspera
1003
579
  all_packages.reject!{ |pkg| skip_package[pkg['id']]}
1004
580
  end
1005
581
 
1006
- # must be public
1007
- ACTIONS = %i[reminder servers bearer_token organization tier_restrictions user packages files admin automation gateway].freeze
582
+ # --- DSL command declarations ---
1008
583
 
1009
- def execute_action
1010
- command = options.get_next_command(ACTIONS)
1011
- if %i[files packages].include?(command)
1012
- formatter.display_status("Workspace: #{aoc_api.workspace_info[:name].to_s.red}#{' (default)' if aoc_api.default_workspace?}")
1013
- if !aoc_api.private_link.nil?
1014
- folder_name = aoc_api.node_api_from(node_id: aoc_api.home[:node_id]).read("files/#{aoc_api.home[:file_id]}")['name']
1015
- formatter.display_status("Private Folder: #{folder_name}")
1016
- end
1017
- end
1018
- case command
1019
- when :reminder
1020
- # send an email reminder with list of orgs
1021
- user_email = options.get_option(:username, mandatory: true)
1022
- no_auth_api = Api::AoC.new(url: options.get_option(:url), auth: :none)
1023
- no_auth_api.create('organization_reminders', {email: user_email})
1024
- return Result::Status.new("List of organizations user is member of, has been sent by e-mail to #{user_email}")
1025
- when :servers
584
+ # Root-level commands
585
+ command :reminder, description: 'Send reminder email with list of orgs'
586
+ command(
587
+ :servers, description: 'List AoC servers (no auth)',
588
+ action: lambda do
1026
589
  no_auth_api = Api::AoC.new(url: options.get_option(:url), auth: :none)
1027
- return Result::ObjectList.new(no_auth_api.read('servers'))
1028
- when :bearer_token
1029
- return Result::Text.new(aoc_api.oauth.authorization)
1030
- when :organization
1031
- return Result::SingleObject.new(aoc_api.read('organization'))
1032
- when :tier_restrictions
1033
- return Result::SingleObject.new(aoc_api.read('tier_restrictions'))
1034
- when :user
1035
- user_cmd = options.get_next_command(%i[workspaces profile preferences notifications contacts settings])
1036
- case user_cmd
1037
- when :contacts
1038
- return execute_resource_action(:contact)
1039
- when :settings
1040
- return entity_execute(api: aoc_api, entity: 'client_settings')
1041
- when :workspaces
1042
- case options.get_next_command(%i[list current])
1043
- when :list
1044
- return result_list('workspaces', fields: %w[id name])
1045
- when :current
1046
- return Result::SingleObject.new(aoc_api.workspace_info)
1047
- end
1048
- when :profile
1049
- case options.get_next_command(%i[show modify])
1050
- when :show
1051
- return Result::SingleObject.new(aoc_api.current_user_info(exception: true))
1052
- when :modify
1053
- aoc_api.update("users/#{aoc_api.current_user_info(exception: true)['id']}", options.get_next_argument('properties', validation: Hash))
1054
- return Result::Status.new('modified')
1055
- end
1056
- when :preferences, :notifications
1057
- user_preferences_res = "users/#{aoc_api.current_user_info(exception: true)['id']}/#{user_cmd.eql?(:preferences) ? 'user_interaction_preferences' : 'notification_preferences'}"
1058
- case options.get_next_command(%i[show modify])
1059
- when :show
1060
- return Result::SingleObject.new(aoc_api.read(user_preferences_res))
1061
- when :modify
1062
- aoc_api.update(user_preferences_res, options.get_next_argument('properties', validation: Hash))
1063
- return Result::Status.new('modified')
1064
- end
590
+ Result::ObjectList.new(no_auth_api.read('servers'))
591
+ end
592
+ )
593
+ command :bearer_token, description: 'Display bearer token',
594
+ action: ->{Result::Text.new(aoc_api.oauth.authorization)}
595
+ command :organization, description: 'Show organization info',
596
+ action: ->{Result::SingleObject.new(aoc_api.read('organization'))}
597
+ command :tier_restrictions, description: 'Show tier restrictions',
598
+ action: ->{Result::SingleObject.new(aoc_api.read('tier_restrictions'))}
599
+ command :user, description: 'User commands'
600
+ command :packages, description: 'Package commands', setup: :setup_workspace_display
601
+ command :files, description: 'Files commands (workspace-aware)', setup: :setup_workspace_display
602
+ command :admin, description: 'Administration commands', setup: :setup_admin_scope
603
+ commands_under(:admin) do
604
+ command :bearer_token, description: 'Show admin bearer token',
605
+ action: ->{Result::Text.new(aoc_api.oauth.authorization)}
606
+ command :application, description: 'Manage applications'
607
+ command(
608
+ :ats, description: 'Manage ATS (Aspera Transfer Service)',
609
+ action: lambda do
610
+ ats_api = Rest.new(**aoc_api.params.deep_merge({
611
+ base_url: "#{aoc_api.base_url}/admin/ats/pub/v1",
612
+ auth: {params: {scope: Api::AoC::Scope::ADMIN_USER}}
613
+ }))
614
+ Ats.new(context: context, api: ats_api).execute_action
1065
615
  end
1066
- when :packages
1067
- package_command = options.get_next_command(%i[shared_inboxes send receive list show delete modify].concat(Node::NODE4_READ_ACTIONS), aliases: {recv: :receive})
1068
- case package_command
1069
- when :shared_inboxes
1070
- case options.get_next_command(%i[list show short_link])
1071
- when :list
1072
- default_query = {'embed[]' => 'dropbox', 'aggregate_permissions_by_dropbox' => true, 'sort' => 'dropbox_name'}
1073
- workspace_id_hash(default_query, string: true)
1074
- return result_list('dropbox_memberships', fields: %w[dropbox_id dropbox.name], default_query: default_query)
1075
- when :show
1076
- return Result::SingleObject.new(aoc_api.read(get_resource_path_from_args('dropboxes')))
1077
- when :short_link
1078
- # TODO: check name
1079
- return short_link_command(dropbox_id: get_resource_id_from_args('dropboxes'), name: '')
1080
- end
1081
- when :send
1082
- package_data = value_create_modify(command: package_command, schema: Schema::Registry.req_body(Schema::Registry::AOC, 'packages.post'))
1083
- new_user_option = options.get_option(:new_user_option)
1084
- option_validate = options.get_option(:validate_metadata)
1085
- # Works for both normal user auth and link auth.
1086
- workspace_id_hash(package_data, string: true) unless package_data.key?('workspace_id')
1087
- if !aoc_api.public_link.nil?
1088
- aoc_api.assert_public_link_types(%w[send_package_to_user send_package_to_dropbox])
1089
- box_type = aoc_api.public_link['purpose'].split('_').last
1090
- package_data['recipients'] = [{'id' => aoc_api.public_link['data']["#{box_type}_id"], 'type' => box_type}]
1091
- # enforce workspace id from link (should be already ok, but in case user wanted to override)
1092
- package_data['workspace_id'] = aoc_api.public_link['data']['workspace_id']
1093
- end
1094
- package_data['encryption_at_rest'] = true if transfer.user_transfer_spec['content_protection'].eql?('encrypt')
1095
- # transfer may raise an error
1096
- created_package = aoc_api.create_package_simple(package_data, option_validate, new_user_option)
1097
- Runner.result_transfer(transfer.start(created_package[:spec], rest_token: created_package[:node]))
1098
- # return all info on package (especially package id)
1099
- return Result::SingleObject.new(created_package[:info])
1100
- when :receive
1101
- ids_to_download = nil
1102
- if !aoc_api.public_link.nil?
1103
- aoc_api.assert_public_link_types(['view_received_package'])
1104
- # Set the package id from link
1105
- ids_to_download = aoc_api.public_link['data']['package_id']
1106
- end
1107
- # Get from command line unless it was a public link
1108
- ids_to_download ||= options.instance_identifier
1109
- skip_ids_persistency = package_persistency
1110
- case ids_to_download
1111
- when SpecialValues::INIT
1112
- all_packages = list_all_packages_with_query[:items]
1113
- Aspera.assert(skip_ids_persistency, 'INIT requires option once_only')
1114
- skip_ids_persistency.data.clear.concat(all_packages.map{ |e| e['id']})
1115
- skip_ids_persistency.save
1116
- return Result::Status.new("Initialized skip for #{skip_ids_persistency.data.count} package(s)")
1117
- when SpecialValues::ALL
1118
- all_packages = list_all_packages_with_query[:items]
1119
- # remove from list the ones already downloaded
1120
- reject_packages_from_persistency(all_packages, skip_ids_persistency)
1121
- ids_to_download = all_packages.map{ |e| e['id']}
1122
- formatter.display_status("Found #{ids_to_download.length} package(s).")
1123
- else
1124
- # single id to array
1125
- ids_to_download = [ids_to_download] unless ids_to_download.is_a?(Array)
1126
- end
1127
- # download all files, or specified list only
1128
- ts_paths = transfer.ts_source_paths(default: ['.'])
1129
- per_package_def = options.get_option(:package_folder).symbolize_keys
1130
- save_metadata = per_package_def.delete(:inf)
1131
- # get value outside of loop
1132
- destination_folder = transfer.destination_folder(Transfer::Spec::DIRECTION_RECEIVE)
1133
- result_transfer = []
1134
- ids_to_download.each do |package_id|
1135
- package_info = aoc_api.read("packages/#{package_id}")
1136
- package_node_api = aoc_api.node_api_from(
1137
- node_id: package_info['node_id'],
1138
- package_info: package_info,
1139
- **workspace_id_hash(name: true)
1140
- )
1141
- transfer_spec = package_node_api.transfer_spec_gen4(
1142
- package_info['contents_file_id'],
1143
- Transfer::Spec::DIRECTION_RECEIVE,
1144
- {'paths'=> ts_paths}
1145
- )
1146
- transfer.user_transfer_spec['destination_root'] = self.class.unique_folder(package_info, destination_folder, **per_package_def) unless per_package_def.empty?
1147
- dest_folder = transfer.user_transfer_spec['destination_root'] || destination_folder
1148
- formatter.display_status(%Q{Downloading package: [#{package_info['id']}] "#{package_info['name']}" to [#{dest_folder}]})
1149
- statuses = transfer.start(
1150
- transfer_spec,
1151
- rest_token: package_node_api
1152
- )
1153
- File.write(File.join(dest_folder, "#{package_id}.info.json"), package_info.to_json) if save_metadata
1154
- result_transfer.push({'package' => package_id, Runner::STATUS_FIELD => statuses})
1155
- # update skip list only if all transfer sessions completed
1156
- if skip_ids_persistency && TransferAgent.session_status(statuses).eql?(:success)
1157
- skip_ids_persistency.data.push(package_id)
1158
- skip_ids_persistency.save
616
+ )
617
+ command :usage_reports, description: 'List usage reports',
618
+ action: ->{result_list('usage_reports', base_query: workspace_id_hash)}
619
+ command :auth_providers, description: 'Manage auth providers'
620
+ command :subscription, description: 'Show subscription info'
621
+ command :analytics, description: 'Query analytics'
622
+ ADMIN_OBJECTS.each do |res|
623
+ cfg = ADMIN_OBJECT_CONFIG.fetch(res, {})
624
+ op_setup = cfg[:op_setup] || {}
625
+ extra_op_args = cfg[:extra_op_args] || {}
626
+ is_singleton = cfg[:singleton]
627
+ id_arg_spec = is_singleton ? [] : [{name: :res_id, type: :identifier, lookup: :"lookup_aoc_#{res}_id"}]
628
+ ops = if cfg[:ops]
629
+ cfg[:ops]
630
+ elsif is_singleton
631
+ %i[show]
632
+ else
633
+ Operations::ALL + (cfg[:extra_ops] || [])
634
+ end
635
+ command(res, description: "Manage #{res.to_s.tr('_', ' ')}")
636
+ commands_under([:admin, res]) do
637
+ ops.each do |op|
638
+ extra_setup = op_setup[op]
639
+ base_attrs = extra_setup ? {setup: extra_setup} : {}
640
+ extra_arg_list =
641
+ if !is_singleton && op.eql?(:create)
642
+ c = aoc_res_cfg(res)
643
+ [{name: :data, type: Hash, bulk: true, schema: c[:schema]}]
644
+ elsif !is_singleton && op.eql?(:modify)
645
+ c = aoc_res_cfg(res)
646
+ [{name: :data, type: Hash, schema: c[:schema]}]
647
+ elsif extra_op_args.key?(op)
648
+ extra_op_args[op]
649
+ else
650
+ []
651
+ end
652
+ # For non-global ops, prepend the identifier spec
653
+ arg_list = (Operations::GLOBAL.include?(op) ? [] : id_arg_spec) + extra_arg_list
654
+ merged = arg_list.empty? ? base_attrs : base_attrs.merge(arguments: arg_list)
655
+ # Attach query_schema (full path) to :list CommandSpec so --help shows the tip
656
+ if op.eql?(:list)
657
+ c = aoc_res_cfg(res)
658
+ merged = merged.merge(query_schema: Schema::Registry.query_params(c[:query_component], c[:path]))
1159
659
  end
660
+ command(op, description: op.to_s.tr('_', ' ').capitalize, **merged)
1160
661
  end
1161
- return Runner.result_transfer_multiple(result_transfer)
1162
- when :show
1163
- package_id = options.instance_identifier
1164
- package_info = aoc_api.read("packages/#{package_id}")
1165
- return Result::SingleObject.new(package_info)
1166
- when :list
1167
- result = list_all_packages_with_query
1168
- skip_ids_persistency = package_persistency
1169
- reject_packages_from_persistency(result[:items], skip_ids_persistency)
1170
- display_fields = PACKAGE_LIST_DEFAULT_FIELDS
1171
- display_fields += ['workspace_id'] if aoc_api.workspace_info[:id].nil?
1172
- return Result::ObjectList.new(result[:items], fields: display_fields, total: result[:total])
1173
- when :delete
1174
- return do_bulk_operation(command: package_command, values: options.instance_identifier) do |package_id|
1175
- Aspera.assert_type(package_id, String, Integer){'identifier'}
1176
- aoc_api.delete("packages/#{package_id}")
1177
- end
1178
- when :modify
1179
- package_id = options.instance_identifier
1180
- package_data = value_create_modify(command: package_command)
1181
- aoc_api.update("packages/#{package_id}", package_data)
1182
- return Result::Status.new('modified')
1183
- when *Node::NODE4_READ_ACTIONS
1184
- package_id = options.instance_identifier
1185
- package_info = aoc_api.read("packages/#{package_id}")
1186
- return execute_nodegen4_command(package_command, package_info['node_id'], file_id: package_info['contents_file_id'], scope: Api::Node::Scope::USER)
1187
662
  end
1188
- when :files
1189
- command_repo = options.get_next_command([:short_link].concat(FILES_COMMANDS))
1190
- case command_repo
1191
- when *FILES_COMMANDS
1192
- return execute_nodegen4_command(command_repo, aoc_api.home[:node_id], file_id: aoc_api.home[:file_id], scope: Api::Node::Scope::USER)
1193
- when :short_link
1194
- folder_dest = options.get_next_argument('path', validation: String)
1195
- home_node_api = aoc_api.node_api_from(
1196
- node_id: aoc_api.home[:node_id],
1197
- **workspace_id_hash(name: true)
1198
- )
1199
- shared_apifid = home_node_api.resolve_api_fid(aoc_api.home[:file_id], folder_dest)
1200
- return short_link_command(
1201
- node_id: shared_apifid.node_api.app_info.node_info['id'],
1202
- file_id: shared_apifid.file_id
1203
- ) do |op, id, access_levels|
1204
- case op
1205
- when :create
1206
- # `id` is the resource id
1207
- perm_data = {
1208
- 'file_id' => shared_apifid.file_id,
1209
- 'access_id' => id,
1210
- 'access_type' => 'user',
1211
- 'access_levels' => Api::AoC.expand_access_levels(access_levels),
1212
- 'tags' => {
1213
- 'url_token' => true,
1214
- 'folder_name' => File.basename(folder_dest),
1215
- 'created_by_name' => aoc_api.current_user_info['name'],
1216
- 'created_by_email' => aoc_api.current_user_info['email'],
1217
- 'access_key' => shared_apifid.node_api.app_info.node_info['access_key'],
1218
- 'node' => shared_apifid.node_api.app_info.node_info['name'],
1219
- **workspace_id_hash(string: true, name: true)
1220
- }
1221
- }
1222
- created_data = shared_apifid.node_api.create('permissions', perm_data)
1223
- aoc_api.permissions_send_event(event_data: created_data, app_info: shared_apifid.node_api.app_info)
1224
- when :update
1225
- # `id` is the permission_id
1226
- 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)}
1227
- raise Error, "Short link not found: #{id}" if found.nil?
1228
- shared_apifid.node_api.update("permissions/#{found['id']}", {access_levels: Api::AoC.expand_access_levels(access_levels)})
1229
- when :delete
1230
- # `id` is the resource id, i.e. `access_id`
1231
- found = shared_apifid.node_api.read('permissions', {file_id: shared_apifid.file_id, inherited: false, access_type: 'user', access_id: id}).first
1232
- raise Error, "Short link not found: #{id}" if found.nil?
1233
- shared_apifid.node_api.delete("permissions/#{found['id']}")
1234
- else Aspera.error_unexpected_value(op)
663
+ end
664
+ end
665
+ # admin > workspace > shared_folder sub-tree
666
+ commands_under(%i[admin workspace shared_folder]) do
667
+ command :list, description: 'List shared folders'
668
+ command :node, description: 'Execute node command on shared folder',
669
+ arguments: [{name: :sf_id, type: :identifier}],
670
+ setup: :setup_admin_workspace_shared_folder_node
671
+ command :member, description: 'Show folder members',
672
+ arguments: [{name: :sf_id, type: :identifier}],
673
+ setup: :setup_admin_workspace_shared_folder_member
674
+ end
675
+ # admin > workspace > shared_folder > node sub-tree (FILES_COMMANDS)
676
+ commands_under(%i[admin workspace shared_folder node]) do
677
+ FILES_COMMANDS.each{ |c| command(c, description: c.to_s.tr('_', ' ').capitalize)}
678
+ end
679
+ commands_under(%i[admin workspace shared_folder member]) do
680
+ command :list, description: 'List members of a shared folder'
681
+ end
682
+ # admin > workspace > dropbox sub-tree
683
+ commands_under(%i[admin workspace dropbox]) do
684
+ command :list, description: 'List dropboxes in workspace'
685
+ end
686
+ # admin > node > do sub-tree (FILES_COMMANDS)
687
+ commands_under(%i[admin node do]) do
688
+ FILES_COMMANDS.each{ |c| command(c, description: c.to_s.tr('_', ' ').capitalize)}
689
+ end
690
+ # admin > user > preferences|notifications sub-trees
691
+ %i[preferences notifications].each do |pref|
692
+ commands_under([:admin, :user, pref]) do
693
+ command :show, description: "Show #{pref}"
694
+ command :modify, description: "Modify #{pref}",
695
+ arguments: [{name: :properties, type: Hash}]
696
+ end
697
+ end
698
+ commands_under(%i[admin auth_providers]) do
699
+ command :list, description: 'List auth providers',
700
+ action: ->{result_list('admin/auth_providers')}
701
+ command :update, description: 'Update auth provider', action: ->{Aspera.error_not_implemented}
702
+ end
703
+ commands_under(%i[admin subscription]) do
704
+ command :account, description: 'Show subscription account'
705
+ command :usage, description: 'Show subscription usage',
706
+ arguments: [
707
+ {name: :aggregate, mandatory: false, default: :ALL, allowed: %i[ALL MONTHLY]},
708
+ {name: :start_date, mandatory: false, default: nil},
709
+ {name: :end_date, mandatory: false, default: nil}
710
+ ]
711
+ end
712
+ commands_under(%i[admin analytics]) do
713
+ command :application_events, description: 'List application events'
714
+ command :transfers, description: 'List transfer events',
715
+ arguments: [
716
+ {name: :event_resource_type, mandatory: true, allowed: %i[organizations users nodes]},
717
+ {name: :event_resource_id, mandatory: false, default: nil}
718
+ ]
719
+ command :files, description: 'List file events',
720
+ arguments: [
721
+ {name: :event_resource_type, mandatory: true, allowed: %i[organizations users nodes]},
722
+ {name: :event_resource_id, mandatory: true},
723
+ {name: :event_uuid, mandatory: true}
724
+ ]
725
+ end
726
+ # application sub-commands
727
+ commands_under(%i[admin application]) do
728
+ command :types, description: 'List application types',
729
+ action: ->{Result::ObjectList.new(aoc_api.read('admin/apps'))}
730
+ command :settings, description: 'Manage per-app-type settings'
731
+ command :instance, description: 'Manage app instances'
732
+ command :membership, description: 'Manage app memberships'
733
+ end
734
+ APP_SETTINGS_PATH = %i[admin application settings].freeze
735
+ APP_INSTANCE_PATH = %i[admin application instance].freeze
736
+ private_constant :APP_SETTINGS_PATH, :APP_INSTANCE_PATH
737
+ commands_under(APP_SETTINGS_PATH) do
738
+ APP_TYPES.each do |app_type|
739
+ command(app_type, description: "Settings for #{app_type} app")
740
+ commands_under(APP_SETTINGS_PATH + [app_type]) do
741
+ command :show, description: "Show #{app_type} settings",
742
+ action: ->{Result::SingleObject.new(aoc_api.read("/apps/#{app_type}/settings"))}
743
+ command(
744
+ :modify, description: "Modify #{app_type} settings",
745
+ arguments: [{name: :properties, type: Hash}],
746
+ action: lambda do |properties:, **|
747
+ aoc_api.update("/apps/#{app_type}/settings", properties)
748
+ Result::Status.new('modified')
1235
749
  end
1236
- end
750
+ )
751
+ end
752
+ end
753
+ end
754
+ commands_under(APP_INSTANCE_PATH) do
755
+ command(:list, description: 'List app instances', action: lambda do
756
+ result_list(
757
+ 'admin/apps_new',
758
+ fields: %w[id app_type available workspace_id],
759
+ default_query: {workspace_id: aoc_api.workspace_info[:id]}
760
+ )
761
+ end)
762
+ APP_TYPES.each do |app_type|
763
+ command(app_type, description: "Show or modify a #{app_type} instance")
764
+ commands_under(APP_INSTANCE_PATH + [app_type]) do
765
+ command :show, description: "Show a #{app_type} instance",
766
+ arguments: [{name: :res_id, type: :identifier}]
767
+ command :modify, description: "Modify a #{app_type} instance",
768
+ arguments: [{name: :res_id, type: :identifier}, {name: :properties, type: Hash}]
769
+ end
770
+ end
771
+ end
772
+ commands_under(%i[admin application membership]) do
773
+ command :list, description: 'List app memberships',
774
+ action: ->{result_list('apps/app_memberships')}
775
+ command :show, description: 'Show an app membership',
776
+ arguments: [{name: :res_id, type: :identifier}]
777
+ command :delete, description: 'Delete an app membership',
778
+ arguments: [{name: :res_id, type: :identifier}]
779
+ command :create, description: 'Create an app membership',
780
+ arguments: [{name: :membership, type: Hash}]
781
+ end
782
+ command :automation, description: 'Automation commands (BETA)', setup: :setup_automation_api
783
+ command :gateway, description: 'Start AoC Faspex4 gateway',
784
+ arguments: [{name: :parameters, type: Hash, mandatory: false, default: {}}]
785
+
786
+ # user sub-commands
787
+ commands_under(:user) do
788
+ command :workspaces, description: 'Workspace commands'
789
+ command :profile, description: 'User profile commands'
790
+ command :preferences, description: 'User interaction preferences'
791
+ command :notifications, description: 'Notification preferences'
792
+ command :contacts, description: 'Manage contacts'
793
+ entity_command :settings, api: :aoc_api, entity: 'client_settings', description: 'Manage client settings'
794
+ end
795
+ # user > contacts sub-commands (same CRUD as admin > contact)
796
+ commands_under(%i[user contacts]) do
797
+ Operations::ALL.each{ |op| command(op, description: op.to_s.capitalize)}
798
+ end
799
+
800
+ commands_under(%i[user workspaces]) do
801
+ command :list, description: 'List workspaces',
802
+ action: ->{result_list('workspaces', fields: %w[id name])}
803
+ command :current, description: 'Show current workspace',
804
+ action: ->{Result::SingleObject.new(aoc_api.workspace_info)}
805
+ end
806
+
807
+ commands_under(%i[user profile]) do
808
+ command :show, description: 'Show user profile',
809
+ action: ->{Result::SingleObject.new(aoc_api.current_user_info(exception: true))}
810
+ command(
811
+ :modify, description: 'Modify user profile',
812
+ arguments: [{name: :properties, type: Hash}],
813
+ action: lambda do |properties:, **|
814
+ aoc_api.update("users/#{aoc_api.current_user_info(exception: true)['id']}", properties)
815
+ Result::Status.new('modified')
816
+ end
817
+ )
818
+ end
819
+
820
+ commands_under(%i[user preferences]) do
821
+ command(
822
+ :show, description: 'Show user preferences',
823
+ action: lambda do
824
+ user_id = aoc_api.current_user_info(exception: true)['id']
825
+ Result::SingleObject.new(aoc_api.read("users/#{user_id}/user_interaction_preferences"))
826
+ end
827
+ )
828
+ command(
829
+ :modify, description: 'Modify user preferences',
830
+ arguments: [{name: :properties, type: Hash}],
831
+ action: lambda do |properties:, **|
832
+ user_id = aoc_api.current_user_info(exception: true)['id']
833
+ aoc_api.update("users/#{user_id}/user_interaction_preferences", properties)
834
+ Result::Status.new('modified')
835
+ end
836
+ )
837
+ end
838
+
839
+ commands_under(%i[user notifications]) do
840
+ command(
841
+ :show, description: 'Show notification preferences',
842
+ action: lambda do
843
+ user_id = aoc_api.current_user_info(exception: true)['id']
844
+ Result::SingleObject.new(aoc_api.read("users/#{user_id}/notification_preferences"))
845
+ end
846
+ )
847
+ command(
848
+ :modify, description: 'Modify notification preferences',
849
+ arguments: [{name: :properties, type: Hash}],
850
+ action: lambda do |properties:, **|
851
+ user_id = aoc_api.current_user_info(exception: true)['id']
852
+ aoc_api.update("users/#{user_id}/notification_preferences", properties)
853
+ Result::Status.new('modified')
854
+ end
855
+ )
856
+ end
857
+
858
+ # packages sub-commands — instance commands consume package_id
859
+ commands_under(:packages) do
860
+ command :shared_inboxes, description: 'Shared inbox commands'
861
+ command :send, description: 'Send a package', transfer_paths: :send,
862
+ arguments: [{name: :data, type: Hash, schema: Schema::Registry.req_body(Schema::Registry::AOC, 'packages.post')}]
863
+ command :receive, description: 'Receive package(s)', aliases: [:recv], transfer_paths: :receive,
864
+ arguments: [{name: :package_id, type: :identifier}]
865
+ command :list, description: 'List packages'
866
+ command :show, description: 'Show a package',
867
+ arguments: [{name: :package_id, type: :identifier}]
868
+ command :delete, description: 'Delete package(s)',
869
+ arguments: [{name: :package_id, type: :identifier}]
870
+ command :modify, description: 'Modify a package',
871
+ arguments: [{name: :package_id, type: :identifier}, {name: :data, type: Hash}]
872
+ # Node Gen4 read-only actions on packages
873
+ command :bearer_token_node, description: 'Show bearer token for package node',
874
+ arguments: [{name: :package_id, type: :identifier}]
875
+ command :node_info, description: 'Show node info for package',
876
+ arguments: [{name: :package_id, type: :identifier}]
877
+ command :browse, description: 'Browse package contents',
878
+ arguments: [{name: :package_id, type: :identifier}]
879
+ command :find, description: 'Find files in package',
880
+ arguments: [{name: :package_id, type: :identifier}]
881
+ end
882
+
883
+ commands_under(%i[packages shared_inboxes]) do
884
+ command :list, description: 'List shared inboxes',
885
+ action: (lambda do
886
+ result_list(
887
+ 'dropbox_memberships',
888
+ fields: %w[dropbox_id dropbox.name],
889
+ default_query: workspace_id_hash({'embed[]' => 'dropbox', 'aggregate_permissions_by_dropbox' => true, 'sort' => 'dropbox_name'}, string: true)
890
+ )
891
+ end)
892
+ command :show, description: 'Show a shared inbox',
893
+ arguments: [{name: :dropbox_id, type: :identifier, lookup: :lookup_aoc_dropbox_id}],
894
+ action: ->(dropbox_id:, **){Result::SingleObject.new(aoc_api.read("dropboxes/#{dropbox_id}"))}
895
+ command :short_link, description: 'Manage shared inbox short links',
896
+ arguments: [{name: :link_type, allowed: %i[public private]}],
897
+ setup: :setup_packages_short_link
898
+ end
899
+ # packages > shared_inboxes > short_link sub-commands
900
+ register_short_link_commands(self, %i[packages shared_inboxes short_link])
901
+
902
+ # files sub-commands: all FILES_COMMANDS + :short_link
903
+ # Declared dynamically after FILES_COMMANDS is available (class body evaluated after constants)
904
+ commands_under(:files) do
905
+ command :short_link, description: 'Manage file short link',
906
+ arguments: [{name: :folder_dest, type: String}, {name: :link_type, allowed: %i[public private]}],
907
+ 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 :mkdir, description: 'Create folder'
911
+ command :mklink, description: 'Create symbolic link'
912
+ command :mkfile, description: 'Create file'
913
+ command :rename, description: 'Rename entry'
914
+ command :delete, description: 'Delete entry'
915
+ command :upload, description: 'Upload files', transfer_paths: :send
916
+ command :download, description: 'Download files', transfer_paths: :receive
917
+ command :sync, description: 'Synchronize folders'
918
+ command :cat, description: 'Show file contents'
919
+ command :show, description: 'Show file info'
920
+ command :modify, description: 'Modify file'
921
+ command :permission, description: 'Manage permissions'
922
+ command :thumbnail, description: 'Show file thumbnail'
923
+ command :v3, description: 'Legacy v3 commands on files'
924
+ command :bearer_token_node, description: 'Show bearer token for file node'
925
+ command :node_info, description: 'Show node info for file'
926
+ command :browse, description: 'Browse files'
927
+ command :find, description: 'Find files'
928
+ end
929
+ # files > short_link sub-commands
930
+ register_short_link_commands(self, %i[files short_link])
931
+
932
+ # automation sub-commands
933
+ commands_under(:automation) do
934
+ entity_command :instances, description: 'Manage workflow instances', api: :aoc_api, entity: 'workflow_instances'
935
+ command :workflows, description: 'Manage workflows'
936
+ end
937
+
938
+ commands_under(%i[automation workflows]) do
939
+ command :create, description: 'Create a workflow'
940
+ command :list, description: 'List workflows'
941
+ command :show, description: 'Show a workflow'
942
+ command :modify, description: 'Modify a workflow'
943
+ command :delete, description: 'Delete a workflow'
944
+ command :launch, description: 'Launch a workflow',
945
+ arguments: [{name: :wf_id, type: :identifier}],
946
+ action: ->(wf_id:, **){Result::SingleObject.new(@automation_api.create("workflows/#{wf_id}/launch", {}))}
947
+ command :action, description: 'Add action to workflow (TODO)'
948
+ end
949
+
950
+ commands_under(%i[automation workflows action]) do
951
+ command :list, description: 'List actions (TODO)',
952
+ arguments: [{name: :wf_id, type: :identifier}]
953
+ command :create, description: 'Create an action (TODO)',
954
+ arguments: [{name: :wf_id, type: :identifier}]
955
+ command :show, description: 'Show an action (TODO)',
956
+ arguments: [{name: :wf_id, type: :identifier}]
957
+ end
958
+
959
+ # --- setup methods ---
960
+
961
+ # Display workspace info before dispatching files/packages sub-commands.
962
+ # Returns {} so it does not inject anything into ctx.
963
+ def setup_workspace_display(**)
964
+ formatter.display_status("Workspace: #{aoc_api.workspace_info[:name].to_s.red}#{' (default)' if aoc_api.default_workspace?}")
965
+ if !aoc_api.private_link.nil?
966
+ folder_name = aoc_api.node_api_from(node_id: aoc_api.home[:node_id]).read("files/#{aoc_api.home[:file_id]}")['name']
967
+ formatter.display_status("Private Folder: #{folder_name}")
968
+ end
969
+ {}
970
+ end
971
+
972
+ # Build automation API and store in @automation_api ivar.
973
+ def setup_automation_api(**)
974
+ change_api_scope(Api::AoC::Scope::ADMIN_USER)
975
+ Log.log.warn('BETA: work under progress')
976
+ @automation_api = Rest.new(**aoc_api.params, base_url: aoc_api.base_url.gsub('/api/', '/automation/'))
977
+ {}
978
+ end
979
+
980
+ # --- handler methods ---
981
+
982
+ def action_reminder
983
+ user_email = options.get_option(:username, mandatory: true)
984
+ no_auth_api = Api::AoC.new(url: options.get_option(:url), auth: :none)
985
+ no_auth_api.create('organization_reminders', {email: user_email})
986
+ return Result::Status.new("List of organizations user is member of, has been sent by e-mail to #{user_email}")
987
+ end
988
+
989
+ # packages > send
990
+ def action_packages_send(data:, **)
991
+ package_data = data
992
+ new_user_option = options.get_option(:new_user_option)
993
+ option_validate = options.get_option(:validate_metadata)
994
+ workspace_id_hash(package_data, string: true) unless package_data.key?('workspace_id')
995
+ if !aoc_api.public_link.nil?
996
+ aoc_api.assert_public_link_types(%w[send_package_to_user send_package_to_dropbox])
997
+ box_type = aoc_api.public_link['purpose'].split('_').last
998
+ package_data['recipients'] = [{'id' => aoc_api.public_link['data']["#{box_type}_id"], 'type' => box_type}]
999
+ package_data['workspace_id'] = aoc_api.public_link['data']['workspace_id']
1000
+ end
1001
+ package_data['encryption_at_rest'] = true if transfer.user_transfer_spec['content_protection'].eql?('encrypt')
1002
+ created_package = aoc_api.create_package_simple(package_data, option_validate, new_user_option)
1003
+ Runner.result_transfer(transfer.start(created_package[:spec], rest_token: created_package[:node]))
1004
+ return Result::SingleObject.new(created_package[:info])
1005
+ end
1006
+
1007
+ # packages > receive — package_id: from arguments:(:identifier) (or overridden by public_link)
1008
+ def action_packages_receive(package_id:, **)
1009
+ ids_to_download = if aoc_api.public_link.nil?
1010
+ package_id
1011
+ else
1012
+ aoc_api.assert_public_link_types(['view_received_package'])
1013
+ aoc_api.public_link['data']['package_id']
1014
+ end
1015
+ skip_ids_persistency = package_persistency
1016
+ case ids_to_download
1017
+ when SpecialValues::INIT
1018
+ all_packages = list_all_packages_with_query[:items]
1019
+ Aspera.assert(skip_ids_persistency, 'INIT requires option once_only')
1020
+ skip_ids_persistency.data.clear.concat(all_packages.map{ |e| e['id']})
1021
+ skip_ids_persistency.save
1022
+ return Result::Status.new("Initialized skip for #{skip_ids_persistency.data.count} package(s)")
1023
+ when SpecialValues::ALL
1024
+ all_packages = list_all_packages_with_query[:items]
1025
+ reject_packages_from_persistency(all_packages, skip_ids_persistency)
1026
+ ids_to_download = all_packages.map{ |e| e['id']}
1027
+ formatter.display_status("Found #{ids_to_download.length} package(s).")
1028
+ else
1029
+ ids_to_download = [ids_to_download] unless ids_to_download.is_a?(Array)
1030
+ end
1031
+ ts_paths = transfer.ts_source_paths(default: ['.'])
1032
+ per_package_def = options.get_option(:package_folder).symbolize_keys
1033
+ save_metadata = per_package_def.delete(:inf)
1034
+ destination_folder = transfer.destination_folder(Transfer::Spec::DIRECTION_RECEIVE)
1035
+ result_transfer = []
1036
+ ids_to_download.each do |package_id|
1037
+ package_info = aoc_api.read("packages/#{package_id}")
1038
+ package_node_api = aoc_api.node_api_from(
1039
+ node_id: package_info['node_id'],
1040
+ package_info: package_info,
1041
+ **workspace_id_hash(name: true)
1042
+ )
1043
+ transfer_spec = package_node_api.transfer_spec_gen4(
1044
+ package_info['contents_file_id'],
1045
+ Transfer::Spec::DIRECTION_RECEIVE,
1046
+ {'paths'=> ts_paths}
1047
+ )
1048
+ transfer.user_transfer_spec['destination_root'] = self.class.unique_folder(package_info, destination_folder, **per_package_def) unless per_package_def.empty?
1049
+ dest_folder = transfer.user_transfer_spec['destination_root'] || destination_folder
1050
+ formatter.display_status(%Q{Downloading package: [#{package_info['id']}] "#{package_info['name']}" to [#{dest_folder}]})
1051
+ statuses = transfer.start(transfer_spec, rest_token: package_node_api)
1052
+ File.write(File.join(dest_folder, "#{package_id}.info.json"), package_info.to_json) if save_metadata
1053
+ result_transfer.push({'package' => package_id, Runner::STATUS_FIELD => statuses})
1054
+ if skip_ids_persistency && statuses.is_a?(Transfer::Result::Success)
1055
+ skip_ids_persistency.data.push(package_id)
1056
+ skip_ids_persistency.save
1057
+ end
1058
+ end
1059
+ return Runner.result_transfer_multiple(result_transfer)
1060
+ end
1061
+
1062
+ # packages > list
1063
+ def action_packages_list
1064
+ result = list_all_packages_with_query
1065
+ skip_ids_persistency = package_persistency
1066
+ reject_packages_from_persistency(result[:items], skip_ids_persistency)
1067
+ display_fields = PACKAGE_LIST_DEFAULT_FIELDS
1068
+ display_fields += ['workspace_id'] if aoc_api.workspace_info[:id].nil?
1069
+ Result::ObjectList.new(result[:items], fields: display_fields, total: result[:total])
1070
+ end
1071
+
1072
+ # packages > show
1073
+ def action_packages_show(package_id:, **)
1074
+ Result::SingleObject.new(aoc_api.read("packages/#{package_id}"))
1075
+ end
1076
+
1077
+ # packages > delete
1078
+ def action_packages_delete(package_id:, **)
1079
+ is_bulk = options.get_option(:bulk)
1080
+ items = package_id.is_a?(Array) ? package_id : [package_id]
1081
+ Result.bulk(items, is_bulk: is_bulk, command: :delete, bfail: options.get_option(:bfail)) do |one_id|
1082
+ Aspera.assert_type(one_id, String, Integer){'identifier'}
1083
+ aoc_api.delete("packages/#{one_id}")
1084
+ end
1085
+ end
1086
+
1087
+ # packages > modify
1088
+ def action_packages_modify(data:, package_id:, **)
1089
+ aoc_api.update("packages/#{package_id}", data)
1090
+ Result::Status.new('modified')
1091
+ end
1092
+
1093
+ # packages > bearer_token_node / node_info / browse / find
1094
+ # (NODE4_READ_ACTIONS dispatched by full path: action_packages_bearer_token_node, etc.)
1095
+ Node::NODE4_READ_ACTIONS.each do |action|
1096
+ define_action_method([:packages, action]) do |package_id:, **|
1097
+ package_info = aoc_api.read("packages/#{package_id}")
1098
+ execute_nodegen4_command(action, package_info['node_id'], file_id: package_info['contents_file_id'], scope: Api::Node::Scope::USER)
1099
+ end
1100
+ end
1101
+
1102
+ # setup: files > short_link
1103
+ # Resolves the target folder, consumes link_type argument, computes purposes.
1104
+ # @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
1105
+ def setup_files_short_link(folder_dest:, link_type:, **)
1106
+ home_node_api = aoc_api.node_api_from(
1107
+ node_id: aoc_api.home[:node_id],
1108
+ **workspace_id_hash(name: true)
1109
+ )
1110
+ shared_apifid = home_node_api.resolve_api_fid(aoc_api.home[:file_id], folder_dest)
1111
+ shared_data = {
1112
+ node_id: shared_apifid.node_api.app_info.node_info['id'],
1113
+ file_id: shared_apifid.file_id
1114
+ }
1115
+ token_purpose, short_link_purpose = short_link_purposes(shared_data, link_type)
1116
+ perm_block = lambda do |op, id, access_levels|
1117
+ case op
1118
+ when :create
1119
+ perm_data = {
1120
+ 'file_id' => shared_apifid.file_id,
1121
+ 'access_id' => id,
1122
+ 'access_type' => 'user',
1123
+ 'access_levels' => Api::AoC.expand_access_levels(access_levels),
1124
+ 'tags' => {
1125
+ 'url_token' => true,
1126
+ 'folder_name' => File.basename(folder_dest),
1127
+ 'created_by_name' => aoc_api.current_user_info['name'],
1128
+ 'created_by_email' => aoc_api.current_user_info['email'],
1129
+ 'access_key' => shared_apifid.node_api.app_info.node_info['access_key'],
1130
+ 'node' => shared_apifid.node_api.app_info.node_info['name'],
1131
+ **workspace_id_hash(string: true, name: true)
1132
+ }
1133
+ }
1134
+ created_data = shared_apifid.node_api.create('permissions', perm_data)
1135
+ aoc_api.permissions_send_event(event_data: created_data, app_info: shared_apifid.node_api.app_info)
1136
+ when :update
1137
+ 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)}
1138
+ Aspera.assert(!found.nil?, type: Error){"Short link not found: #{id}"}
1139
+ shared_apifid.node_api.update("permissions/#{found['id']}", {access_levels: Api::AoC.expand_access_levels(access_levels)})
1140
+ when :delete
1141
+ found = shared_apifid.node_api.read('permissions', {file_id: shared_apifid.file_id, inherited: false, access_type: 'user', access_id: id}).first
1142
+ Aspera.assert(!found.nil?, type: Error){"Short link not found: #{id}"}
1143
+ shared_apifid.node_api.delete("permissions/#{found['id']}")
1144
+ else Aspera.error_unexpected_value(op)
1145
+ end
1146
+ end
1147
+ {
1148
+ sl_shared_data: shared_data,
1149
+ sl_link_type: link_type,
1150
+ sl_token_purpose: token_purpose,
1151
+ sl_short_link_purpose: short_link_purpose,
1152
+ sl_perm_block: perm_block
1153
+ }
1154
+ end
1155
+
1156
+ # setup: packages > shared_inboxes > short_link
1157
+ # Reads dropbox_id, consumes link_type argument, computes purposes.
1158
+ # @return [Hash] ctx keys: sl_shared_data, sl_link_type, sl_token_purpose, sl_short_link_purpose
1159
+ def setup_packages_short_link(link_type:, **)
1160
+ dropbox_id = get_resource_id_from_args('dropboxes')
1161
+ shared_data = {dropbox_id: dropbox_id, name: ''}
1162
+ token_purpose, short_link_purpose = short_link_purposes(shared_data, link_type)
1163
+ {
1164
+ sl_shared_data: shared_data,
1165
+ sl_link_type: link_type,
1166
+ sl_token_purpose: token_purpose,
1167
+ sl_short_link_purpose: short_link_purpose,
1168
+ sl_perm_block: nil
1169
+ }
1170
+ end
1171
+
1172
+ # Shared implementation for short_link > create
1173
+ def sl_exec_create(custom_data = {}, sl_shared_data:, sl_link_type:, sl_token_purpose:, sl_short_link_purpose:, sl_perm_block:, **)
1174
+ shared_data = sl_shared_data.dup
1175
+ workspace_id_hash(shared_data)
1176
+ create_payload = {purpose: sl_short_link_purpose, user_selected_name: nil}
1177
+ case sl_link_type
1178
+ when :private
1179
+ create_payload[:data] = shared_data
1180
+ when :public
1181
+ create_payload[:expires_at] = nil
1182
+ create_payload[:password_enabled] = false
1183
+ shared_data[:name] = ''
1184
+ create_payload[:data] = {
1185
+ aoc: true,
1186
+ url_token_data: {data: shared_data, purpose: sl_token_purpose}
1187
+ }
1188
+ end
1189
+ custom_data = {}
1190
+ access_levels = custom_data.delete('access_levels')
1191
+ if (pass = custom_data.delete('password'))
1192
+ create_payload[:data][:url_token_data][:password] = pass
1193
+ create_payload[:password_enabled] = true
1194
+ end
1195
+ create_payload.deep_merge!(custom_data)
1196
+ result_create_short_link = aoc_api.create('short_links', create_payload)
1197
+ sl_perm_block&.call(:create, result_create_short_link['resource_id'], access_levels) if sl_link_type.eql?(:public)
1198
+ Result::SingleObject.new(result_create_short_link)
1199
+ end
1200
+
1201
+ # Shared implementation for short_link > delete|list|show|modify: fetch the short_list
1202
+ def sl_fetch_list(sl_shared_data:, sl_link_type:, sl_token_purpose:, sl_short_link_purpose:, **)
1203
+ shared_data = sl_shared_data.dup
1204
+ workspace_id_hash(shared_data)
1205
+ list_params = short_link_list_params(
1206
+ shared_data: shared_data, link_type: sl_link_type,
1207
+ token_purpose: sl_token_purpose, short_link_purpose: sl_short_link_purpose
1208
+ )
1209
+ {
1210
+ sl_short_list: aoc_api.read_with_paging('short_links', list_params.merge(query_read_delete(default: {})).compact),
1211
+ sl_shared_data_ws: shared_data
1212
+ }
1213
+ end
1214
+
1215
+ # Shared implementation for short_link > delete
1216
+ def sl_exec_delete(sl_shared_data_ws:, sl_short_list:, sl_link_type:, sl_perm_block:, short_link_id: nil, **)
1217
+ one_id = short_link_id
1218
+ if sl_link_type.eql?(:public)
1219
+ found = sl_short_list[:items].find{ |item| item['id'].eql?(one_id)}
1220
+ raise BadIdentifier.new('Short link', one_id) if found.nil?
1221
+ sl_perm_block&.call(:delete, found['resource_id'], nil)
1222
+ end
1223
+ aoc_api.delete("short_links/#{one_id}", {edit_access: true, json_query: sl_shared_data_ws.to_json})
1224
+ Result::Status.new('deleted')
1225
+ end
1226
+
1227
+ # Shared implementation for short_link > list
1228
+ def sl_exec_list(sl_short_list:, **)
1229
+ Result::ObjectList.new(sl_short_list[:items], fields: Formatter.all_but('data'), total: sl_short_list[:total])
1230
+ end
1231
+
1232
+ # Shared implementation for short_link > show
1233
+ def sl_exec_show(sl_short_list:, short_link_id: nil, **)
1234
+ one_id = short_link_id
1235
+ found = sl_short_list[:items].find{ |item| item['id'].eql?(one_id)}
1236
+ raise BadIdentifier.new('Short link', one_id) if found.nil?
1237
+ Result::SingleObject.new(found, fields: Formatter.all_but('data'))
1238
+ end
1239
+
1240
+ # Shared implementation for short_link > modify
1241
+ def sl_exec_modify(custom_data = {}, sl_shared_data:, sl_short_list:, sl_link_type:, sl_perm_block:, short_link_id: nil, **)
1242
+ Aspera.assert_values(sl_link_type, [:public], type: Cli::BadArgument){'link_type'}
1243
+ one_id = short_link_id
1244
+ node_file = sl_shared_data.slice(:node_id, :file_id)
1245
+ modify_payload = {edit_access: true, json_query: node_file}
1246
+ custom_data = {}
1247
+ if (pass = custom_data.delete('password'))
1248
+ modify_payload[:password_enabled] = true
1249
+ modify_payload[:data] = {url_token_data: {password: pass, data: node_file}}
1250
+ else
1251
+ modify_payload[:password_enabled] = false
1252
+ end
1253
+ if custom_data.delete('access_levels')
1254
+ found = sl_short_list[:items].find{ |item| item['id'].eql?(one_id)}
1255
+ raise BadIdentifier.new('Short link', one_id) if found.nil?
1256
+ sl_perm_block&.call(:update, found['resource_id'], nil)
1257
+ end
1258
+ modify_payload.deep_merge!(custom_data)
1259
+ aoc_api.update("short_links/#{one_id}", modify_payload)
1260
+ Result::Status.new('modified')
1261
+ end
1262
+
1263
+ # files > transfer
1264
+ def action_files_transfer(direction:, source_folder:, **)
1265
+ execute_nodegen4_command(
1266
+ :transfer, aoc_api.home[:node_id],
1267
+ file_id: aoc_api.home[:file_id],
1268
+ scope: Api::Node::Scope::USER,
1269
+ transfer_direction: direction,
1270
+ transfer_source: source_folder
1271
+ )
1272
+ end
1273
+
1274
+ # files > FILES_COMMANDS (all Gen4 node commands except :transfer, handled above)
1275
+ FILES_COMMANDS.reject{ |a| a.eql?(:transfer)}.each do |action|
1276
+ define_action_method([:files, action]) do
1277
+ execute_nodegen4_command(action, aoc_api.home[:node_id], file_id: aoc_api.home[:file_id], scope: Api::Node::Scope::USER)
1278
+ end
1279
+ end
1280
+
1281
+ # admin > application > instance > <type> > show|modify
1282
+ APP_TYPES.each do |app_type|
1283
+ define_action_method([:admin, :application, :instance, app_type, :show]) do |res_id:, **|
1284
+ Result::SingleObject.new(aoc_api.read("admin/apps_new/#{app_type}/#{res_id}", query_read_delete))
1285
+ end
1286
+
1287
+ define_action_method([:admin, :application, :instance, app_type, :modify]) do |properties:, res_id:, **|
1288
+ aoc_api.update("admin/apps_new/#{app_type}/#{res_id}", properties)
1289
+ Result::Status.new('modified')
1290
+ end
1291
+ end
1292
+
1293
+ def action_admin_application_membership_create(membership:, **)
1294
+ data = membership.dup
1295
+ app_type = data.delete('app_type')
1296
+ Aspera.assert_type(app_type, String){'app_type'}
1297
+ Aspera.assert_values(app_type.to_sym, APP_TYPES){'app_type'}
1298
+ Result::SingleObject.new(aoc_api.create("apps/#{app_type}/app_memberships", data))
1299
+ end
1300
+
1301
+ # admin > application > membership > show|delete
1302
+ def action_admin_application_membership_show(res_id:, **)
1303
+ Result::SingleObject.new(aoc_api.read("apps/app_memberships/#{res_id}", query_read_delete))
1304
+ end
1305
+
1306
+ def action_admin_application_membership_delete(res_id:, **)
1307
+ aoc_api.delete("apps/app_memberships/#{res_id}")
1308
+ Result::Status.new('deleted')
1309
+ end
1310
+
1311
+ # admin - setup: change API scope to admin once
1312
+ def setup_admin_scope(**)
1313
+ change_api_scope(Api::AoC::Scope::ADMIN)
1314
+ {}
1315
+ end
1316
+
1317
+ # admin > subscription > account
1318
+ def action_admin_subscription_account
1319
+ org = aoc_api.read('organization')
1320
+ result = GraphQL.execute(api_from_options('bss/platform/graphql'), 'bss_subscription_account', {organization_id: org['id']})
1321
+ Result::SingleObject.new(result['aoc']['bssSubscription'])
1322
+ end
1323
+
1324
+ # admin > subscription > usage
1325
+ def action_admin_subscription_usage(aggregate:, start_date:, end_date:, **)
1326
+ today = Date.today
1327
+ aggregate = :ALL if aggregate.nil?
1328
+ start_date = today.prev_year.strftime('%Y-%m-%d') if start_date.nil?
1329
+ end_date = today.strftime('%Y-%m-%d') if end_date.nil?
1330
+ org = aoc_api.read('organization')
1331
+ result = GraphQL.execute(
1332
+ api_from_options('bss/platform/graphql'), 'bss_subscription_usage',
1333
+ {organization_id: org['id'], aggregate: aggregate, startDate: start_date, endDate: end_date}
1334
+ )
1335
+ Result::SingleObject.new(result['aoc'])
1336
+ end
1337
+
1338
+ # admin > analytics > application_events
1339
+ def action_admin_analytics_application_events
1340
+ analytics_api = build_analytics_api
1341
+ events = analytics_api.read("organizations/#{aoc_api.current_user_info['organization_id']}/application_events")['application_events']
1342
+ Result::ObjectList.new(events)
1343
+ end
1344
+
1345
+ # admin > analytics > transfers
1346
+ def action_admin_analytics_transfers(event_resource_type:, event_resource_id:, **)
1347
+ analytics_api = build_analytics_api
1348
+ event_resource_id ||=
1349
+ case event_resource_type
1350
+ when :organizations then aoc_api.current_user_info['organization_id']
1351
+ when :users then aoc_api.current_user_info['id']
1352
+ when :nodes then aoc_api.current_user_info['read_only_home_node_id']
1353
+ else Aspera.error_unreachable_line
1237
1354
  end
1238
- when :automation
1239
- change_api_scope(Api::AoC::Scope::ADMIN_USER)
1240
- Log.log.warn('BETA: work under progress')
1241
- # automation api is not in the same place
1242
- automation_api = Rest.new(**aoc_api.params, base_url: aoc_api.base_url.gsub('/api/', '/automation/'))
1243
- command_automation = options.get_next_command(%i[workflows instances])
1244
- case command_automation
1245
- when :instances
1246
- return entity_execute(api: aoc_api, entity: 'workflow_instances')
1247
- when :workflows
1248
- wf_command = options.get_next_command(%i[action launch].concat(Operations::ALL))
1249
- case wf_command
1250
- when *Operations::ALL
1251
- return entity_execute(
1252
- api: automation_api,
1253
- entity: 'workflows',
1254
- command: wf_command
1255
- )
1256
- when :launch
1257
- wf_id = options.instance_identifier
1258
- data = automation_api.create("workflows/#{wf_id}/launch", {})
1259
- return Result::SingleObject.new(data)
1260
- when :action
1261
- # TODO: not complete
1262
- wf_id = options.instance_identifier
1263
- wf_action_cmd = options.get_next_command(%i[list create show])
1264
- Log.log.warn{"Not implemented: #{wf_action_cmd}"}
1265
- step = automation_api.create('steps', {'workflow_id' => wf_id})
1266
- automation_api.update("workflows/#{wf_id}", {'step_order' => [step['id']]})
1267
- action = automation_api.create('actions', {'step_id' => step['id'], 'type' => 'manual'})
1268
- automation_api.update("steps/#{step['id']}", {'action_order' => [action['id']]})
1269
- wf = automation_api.read("workflows/#{wf_id}")
1270
- return Result::SingleObject.new(wf)
1355
+ filter = query_read_delete(default: {})
1356
+ filter['limit'] ||= 100
1357
+ if options.get_option(:once_only, mandatory: true)
1358
+ saved_date = []
1359
+ start_date_persistency = PersistencyActionOnce.new(
1360
+ manager: persistency,
1361
+ data: saved_date,
1362
+ id: IdGenerator.from_list('aoc_ana_date', options.get_option(:url, mandatory: true), aoc_api.workspace_info[:name], event_resource_type.to_s, event_resource_id)
1363
+ )
1364
+ start_date_time = saved_date.first
1365
+ stop_date_time = Time.now.utc.strftime('%FT%T.%LZ')
1366
+ saved_date[0] = stop_date_time
1367
+ filter['start_time'] = start_date_time unless start_date_time.nil?
1368
+ filter['stop_time'] = stop_date_time
1369
+ end
1370
+ events = analytics_api.read("#{event_resource_type}/#{event_resource_id}/transfers", filter)['transfers']
1371
+ start_date_persistency&.save
1372
+ events.each{ |tr_event| context.mailer.send_email_template(values: {ev: tr_event})} if !options.get_option(:notify_to).nil?
1373
+ Result::ObjectList.new(events)
1374
+ end
1375
+
1376
+ # admin > analytics > files
1377
+ def action_admin_analytics_files(event_resource_type:, event_resource_id:, event_uuid:, **)
1378
+ analytics_api = build_analytics_api
1379
+ event_resource_id =
1380
+ case event_resource_type
1381
+ when :organizations then aoc_api.current_user_info['organization_id']
1382
+ when :users then aoc_api.current_user_info['id']
1383
+ when :nodes then aoc_api.current_user_info['read_only_home_node_id']
1384
+ else Aspera.error_unreachable_line
1385
+ end if event_resource_id.to_s.empty?
1386
+ filter = query_read_delete(default: {})
1387
+ filter['limit'] ||= 100
1388
+ events = analytics_api.read("#{event_resource_type}/#{event_resource_id}/transfers/#{event_uuid}/files", filter)['files']
1389
+ Result::ObjectList.new(events)
1390
+ end
1391
+
1392
+ # Lookup methods for arguments:(:identifier) + lookup: on admin resources.
1393
+ # One method per non-singleton resource; each delegates to get_resource_id_from_args.
1394
+ ADMIN_OBJECTS.reject{ |r| ADMIN_OBJECT_CONFIG.dig(r, :singleton)}.each do |res|
1395
+ define_method(:"lookup_aoc_#{res}_id") do |_field, value, **|
1396
+ aoc_api.lookup_with_q(aoc_res_path(res), value: value)['id']
1397
+ end
1398
+ end
1399
+
1400
+ # admin > <res> > list
1401
+ ADMIN_OBJECTS.each do |res|
1402
+ define_action_method([:admin, res, :list]) do
1403
+ c = aoc_res_cfg(res)
1404
+ result_list(c[:path], fields: c[:list_fields], query_component: c[:query_component])
1405
+ end
1406
+ end
1407
+
1408
+ # admin > <res> > show
1409
+ ADMIN_OBJECTS.reject{ |r| ADMIN_OBJECT_CONFIG.dig(r, :singleton)}.each do |res|
1410
+ define_action_method([:admin, res, :show]) do |res_id:, **|
1411
+ c = aoc_res_cfg(res)
1412
+ Result::SingleObject.new(aoc_api.read("#{c[:path]}/#{res_id}", query_read_delete), fields: Formatter.all_but('certificate'))
1413
+ end
1414
+ end
1415
+
1416
+ # admin > organization|self > show (singleton)
1417
+ %i[organization self].each do |res|
1418
+ define_action_method([:admin, res, :show]) do
1419
+ Result::SingleObject.new(aoc_api.read(res.to_s, query_read_delete), fields: Formatter.all_but('certificate'))
1420
+ end
1421
+ end
1422
+
1423
+ # admin > <res> > create
1424
+ ADMIN_OBJECTS.reject{ |r| ADMIN_OBJECT_CONFIG.dig(r, :singleton)}.each do |res|
1425
+ define_action_method([:admin, res, :create]) do |data:, **|
1426
+ c = aoc_res_cfg(res)
1427
+ path = c[:path]
1428
+ # Special case: client_registration_token has a different creation URL
1429
+ path = 'admin/client_registration/token' if path.eql?('admin/client_registration_tokens')
1430
+ workspace_id = aoc_api.workspace_info[:id] if c[:require_ws_id]
1431
+ is_bulk = options.get_option(:bulk)
1432
+ Result.bulk(data, is_bulk: is_bulk, command: :create, id_result: c[:id_result], bfail: options.get_option(:bfail)) do |params|
1433
+ params['workspace_id'] = workspace_id if c[:require_ws_id] && workspace_id && !params.key?('workspace_id')
1434
+ aoc_api.create(path, params)
1435
+ end
1436
+ end
1437
+ end
1438
+
1439
+ # admin > <res> > modify
1440
+ ADMIN_OBJECTS.reject{ |r| ADMIN_OBJECT_CONFIG.dig(r, :singleton) || ADMIN_OBJECT_CONFIG.dig(r, :ops)&.then{ |o| !o.include?(:modify)}}.each do |res|
1441
+ define_action_method([:admin, res, :modify]) do |data:, res_id:, **|
1442
+ c = aoc_res_cfg(res)
1443
+ aoc_api.update("#{c[:path]}/#{res_id}", data)
1444
+ Result::Status.new('modified')
1445
+ end
1446
+ end
1447
+
1448
+ # admin > <res> > delete
1449
+ ADMIN_OBJECTS.reject do |r|
1450
+ cfg = ADMIN_OBJECT_CONFIG.fetch(r, {})
1451
+ cfg[:singleton] || (cfg[:ops] && !cfg[:ops].include?(:delete))
1452
+ end.each do |res|
1453
+ define_action_method([:admin, res, :delete]) do |res_id:, **|
1454
+ c = aoc_res_cfg(res)
1455
+ is_bulk = options.get_option(:bulk)
1456
+ items = res_id.is_a?(Array) ? res_id : [res_id]
1457
+ Result.bulk(items, is_bulk: is_bulk, command: :delete, bfail: options.get_option(:bfail)) do |one_id|
1458
+ aoc_api.delete("#{c[:path]}/#{one_id}")
1459
+ {'id' => one_id}
1460
+ end
1461
+ end
1462
+ end
1463
+
1464
+ # user > contacts > list|show|create|modify|delete (same API path as admin > contact)
1465
+ Operations::ALL.each do |op|
1466
+ define_action_method([:user, :contacts, op]) do |**ctx|
1467
+ send(CommandSpec.action_method([:admin, :contact, op]), **ctx)
1468
+ end
1469
+ end
1470
+
1471
+ # admin > client > set_pub_key
1472
+ def action_admin_client_set_pub_key(private_key_pem:, res_id:, **)
1473
+ c = aoc_res_cfg(:client)
1474
+ the_public_key = OpenSSL::PKey::RSA.new(private_key_pem).public_key.to_s
1475
+ aoc_api.update("#{c[:path]}/#{res_id}", {jwt_grant_enabled: true, public_key: the_public_key})
1476
+ Result::Success.new
1477
+ end
1478
+
1479
+ # admin > node > do | bearer_token — setup reuses the generic instance setup
1480
+ # (setup_admin_node_instance is auto-generated above, providing res_id:)
1481
+
1482
+ # admin > node > do > <FILES_COMMAND>
1483
+ FILES_COMMANDS.each do |cmd|
1484
+ define_action_method([:admin, :node, :do, cmd]) do |res_id:, **|
1485
+ execute_nodegen4_command(cmd, res_id, scope: Api::Node::Scope::ADMIN)
1486
+ end
1487
+ end
1488
+
1489
+ # admin > node > bearer_token
1490
+ def action_admin_node_bearer_token(scope:, res_id:, **)
1491
+ scope ||= Api::Node::Scope::ADMIN
1492
+ node_api = aoc_api.node_api_from(node_id: res_id, scope: scope)
1493
+ Result::Text.new(node_api.oauth.authorization)
1494
+ end
1495
+
1496
+ # admin > workspace > dropbox — res_id: already in ctx via arguments:(:identifier)
1497
+ def setup_admin_workspace_dropbox(res_id:, **)
1498
+ {ws_res_id: res_id}
1499
+ end
1500
+
1501
+ # admin > workspace > dropbox > list
1502
+ def action_admin_workspace_dropbox_list(ws_res_id:, **)
1503
+ query = options.get_option(:query) || {}
1504
+ Result::ObjectList.new(aoc_api.read('dropboxes', query.merge({'workspace_id' => ws_res_id})), fields: %w[id name description])
1505
+ end
1506
+
1507
+ # admin > workspace > shared_folder — res_id: already in ctx via arguments:(:identifier)
1508
+ def setup_admin_workspace_shared_folder(res_id:, **)
1509
+ resource_instance_path = "#{aoc_res_path(:workspace)}/#{res_id}"
1510
+ query = options.get_option(:query) || Api::AoC.workspace_access(res_id).merge({'admin' => true})
1511
+ shared_folders = aoc_api.read_with_paging("#{resource_instance_path}/permissions", query)[:items]
1512
+ {ws_res_id: res_id, shared_folders: shared_folders}
1513
+ end
1514
+
1515
+ # admin > workspace > shared_folder > list
1516
+ def action_admin_workspace_shared_folder_list(shared_folders:, **)
1517
+ Result::ObjectList.new(shared_folders, fields: %w[id node_name node_id file_id file.path tags.aspera.files.workspace.share_as])
1518
+ end
1519
+
1520
+ # admin > workspace > shared_folder > node|member — sf_id: already in ctx via arguments:(:identifier)
1521
+ def resolve_sf_item(shared_folders:, sf_id:, **)
1522
+ sf_item = shared_folders.find{ |i| i['id'].eql?(sf_id)}
1523
+ Aspera.assert(sf_item, 'shared folder not found')
1524
+ {sf_item: sf_item}
1525
+ end
1526
+
1527
+ alias_method :setup_admin_workspace_shared_folder_node, :resolve_sf_item
1528
+ alias_method :setup_admin_workspace_shared_folder_member, :resolve_sf_item
1529
+
1530
+ # admin > workspace > shared_folder > node > <FILES_COMMAND>
1531
+ FILES_COMMANDS.each do |cmd|
1532
+ define_action_method([:admin, :workspace, :shared_folder, :node, cmd]) do |sf_item:, **|
1533
+ execute_nodegen4_command(cmd, sf_item['node_id'], file_id: sf_item['file_id'], scope: Api::Node::Scope::ADMIN)
1534
+ end
1535
+ end
1536
+
1537
+ # admin > workspace > shared_folder > member > list
1538
+ def action_admin_workspace_shared_folder_member_list(ws_res_id:, sf_item:, **)
1539
+ node_api = aoc_api.node_api_from(
1540
+ node_id: sf_item['node_id'],
1541
+ workspace_id: ws_res_id,
1542
+ workspace_name: nil,
1543
+ scope: Api::Node::Scope::USER
1544
+ )
1545
+ result = node_api.read('permissions', {'file_id' => sf_item['file_id'], 'tag' => "aspera.files.workspace.id=#{ws_res_id}"})
1546
+ result.each do |item|
1547
+ item['member'] = begin
1548
+ if Api::AoC.workspace_access?(item)
1549
+ {'name' => '[Internal permission]'}
1550
+ else
1551
+ aoc_api.read("admin/#{item['access_type']}s/#{item['access_id']}") rescue {'name' => 'not found'}
1271
1552
  end
1553
+ rescue => e
1554
+ {'name' => e.to_s}
1272
1555
  end
1273
- when :admin
1274
- return execute_admin_action
1275
- when :gateway
1276
- require 'aspera/faspex_gw'
1277
- parameters = value_create_modify(command: command, default: {}).symbolize_keys
1278
- uri = URI.parse(parameters.delete(:url){WebServerSimple::DEFAULT_URL})
1279
- server = WebServerSimple.new(uri, **parameters.slice(*WebServerSimple::PARAMS))
1280
- Aspera.assert(parameters.except(*WebServerSimple::PARAMS).empty?){"unexpected parameters: #{parameters.except(*WebServerSimple::PARAMS).keys}"}
1281
- server.mount(uri.path, Faspex4GWServlet, aoc_api, aoc_api.workspace_info[:id])
1282
- server.start
1283
- return Result::Status.new('Gateway terminated')
1284
- else Aspera.error_unreachable_line
1285
1556
  end
1286
- Aspera.error_unreachable_line
1557
+ # TODO : read users and group name and add, if query "include_members"
1558
+ Result::ObjectList.new(result, fields: %w[access_type access_id access_level last_updated_at member.name member.email member.system_group_type member.system_group])
1287
1559
  end
1288
1560
 
1289
- private :execute_admin_action
1561
+ # admin > user > preferences|notifications > show|modify
1562
+ # (setup_admin_user_instance is auto-generated, providing res_id:)
1563
+ %i[preferences notifications].each do |pref|
1564
+ pref_path = pref.eql?(:preferences) ? 'user_interaction_preferences' : 'notification_preferences'
1565
+ define_action_method([:admin, :user, pref, :show]) do |res_id:, **|
1566
+ Result::SingleObject.new(aoc_api.read("#{aoc_res_path(:user)}/#{res_id}/#{pref_path}"))
1567
+ end
1568
+ define_action_method([:admin, :user, pref, :modify]) do |properties:, res_id:, **|
1569
+ aoc_api.update("#{aoc_res_path(:user)}/#{res_id}/#{pref_path}", properties)
1570
+ Result::Status.new('modified')
1571
+ end
1572
+ end
1573
+
1574
+ # automation > workflows > CRUD operations
1575
+ Operations::ALL.each do |op|
1576
+ define_action_method([:automation, :workflows, op]) do
1577
+ entity_execute(api: @automation_api, entity: 'workflows', command: op)
1578
+ end
1579
+ end
1580
+
1581
+ # automation > workflows > action > * (TODO: not fully implemented)
1582
+ %i[list create show].each do |cmd|
1583
+ define_action_method([:automation, :workflows, :action, cmd]) do |wf_id:, **|
1584
+ Log.log.warn{"Not implemented: #{cmd}"}
1585
+ step = @automation_api.create('steps', {'workflow_id' => wf_id})
1586
+ @automation_api.update("workflows/#{wf_id}", {'step_order' => [step['id']]})
1587
+ action = @automation_api.create('actions', {'step_id' => step['id'], 'type' => 'manual'})
1588
+ @automation_api.update("steps/#{step['id']}", {'action_order' => [action['id']]})
1589
+ Result::SingleObject.new(@automation_api.read("workflows/#{wf_id}"))
1590
+ end
1591
+ end
1592
+
1593
+ def action_gateway(parameters: {}, **)
1594
+ require 'aspera/faspex_gw'
1595
+ parameters = parameters.symbolize_keys
1596
+ uri = URI.parse(parameters.delete(:url){WebServerSimple::DEFAULT_URL})
1597
+ server = WebServerSimple.new(uri, **parameters.slice(*WebServerSimple::PARAMS))
1598
+ Aspera.assert(parameters.except(*WebServerSimple::PARAMS).empty?){"unexpected parameters: #{parameters.except(*WebServerSimple::PARAMS).keys}"}
1599
+ server.mount(uri.path, Faspex4GWServlet, aoc_api, aoc_api.workspace_info[:id])
1600
+ server.start
1601
+ return Result::Status.new('Gateway terminated')
1602
+ end
1290
1603
  end
1291
1604
  end
1292
1605
  end