aspera-cli 4.27.1 → 4.27.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +67 -1
  4. data/bin/ascli +2 -1
  5. data/docs/README.md +805 -747
  6. data/lib/aspera/agent/connect.rb +6 -4
  7. data/lib/aspera/agent/desktop.rb +2 -2
  8. data/lib/aspera/agent/direct.rb +3 -1
  9. data/lib/aspera/agent/node.rb +3 -3
  10. data/lib/aspera/api/alee.rb +1 -1
  11. data/lib/aspera/api/aoc.rb +14 -12
  12. data/lib/aspera/api/ats.rb +1 -1
  13. data/lib/aspera/api/cos_node.rb +2 -2
  14. data/lib/aspera/api/faspex.rb +9 -7
  15. data/lib/aspera/api/httpgw.rb +37 -33
  16. data/lib/aspera/api/node.rb +38 -33
  17. data/lib/aspera/ascmd.rb +3 -1
  18. data/lib/aspera/ascp/installation.rb +62 -27
  19. data/lib/aspera/ascp/management.rb +1 -0
  20. data/lib/aspera/assert.rb +4 -0
  21. data/lib/aspera/cli/ascp_actions.rb +20 -41
  22. data/lib/aspera/cli/async_transfer_store.rb +2 -2
  23. data/lib/aspera/cli/bootstrapper.rb +11 -15
  24. data/lib/aspera/cli/command_line.rb +252 -0
  25. data/lib/aspera/cli/command_registry.rb +149 -33
  26. data/lib/aspera/cli/command_spec.rb +103 -14
  27. data/lib/aspera/cli/completion/ascli.bash +12 -0
  28. data/lib/aspera/cli/completion/ascli.fish +16 -0
  29. data/lib/aspera/cli/completion/ascli.zsh +19 -0
  30. data/lib/aspera/cli/context.rb +3 -0
  31. data/lib/aspera/cli/deprecation.rb +37 -0
  32. data/lib/aspera/cli/extended_value.rb +2 -0
  33. data/lib/aspera/cli/formatter.rb +87 -75
  34. data/lib/aspera/cli/gem_checker.rb +1 -1
  35. data/lib/aspera/cli/hints.rb +7 -6
  36. data/lib/aspera/cli/http.rb +21 -21
  37. data/lib/aspera/cli/info.rb +3 -0
  38. data/lib/aspera/cli/mcp_tool.rb +47 -83
  39. data/lib/aspera/cli/option_declarator.rb +33 -42
  40. data/lib/aspera/cli/option_registry.rb +69 -0
  41. data/lib/aspera/cli/option_types.rb +103 -0
  42. data/lib/aspera/cli/option_value.rb +281 -0
  43. data/lib/aspera/cli/options.schema.yaml +38 -5
  44. data/lib/aspera/cli/parser.rb +307 -848
  45. data/lib/aspera/cli/plugins/alee.rb +7 -4
  46. data/lib/aspera/cli/plugins/aoc.rb +435 -380
  47. data/lib/aspera/cli/plugins/ats.rb +58 -73
  48. data/lib/aspera/cli/plugins/base.rb +190 -240
  49. data/lib/aspera/cli/plugins/basic_auth.rb +2 -10
  50. data/lib/aspera/cli/plugins/config.rb +244 -178
  51. data/lib/aspera/cli/plugins/console.rb +102 -38
  52. data/lib/aspera/cli/plugins/cos.rb +6 -23
  53. data/lib/aspera/cli/plugins/factory.rb +3 -0
  54. data/lib/aspera/cli/plugins/faspex5.rb +176 -173
  55. data/lib/aspera/cli/plugins/faspio.rb +5 -10
  56. data/lib/aspera/cli/plugins/httpgw.rb +8 -11
  57. data/lib/aspera/cli/plugins/mcp.rb +20 -55
  58. data/lib/aspera/cli/plugins/node.rb +277 -311
  59. data/lib/aspera/cli/plugins/orchestrator.rb +90 -77
  60. data/lib/aspera/cli/plugins/preview.rb +79 -90
  61. data/lib/aspera/cli/plugins/server.rb +76 -50
  62. data/lib/aspera/cli/plugins/shares.rb +68 -116
  63. data/lib/aspera/cli/preset_actions.rb +17 -10
  64. data/lib/aspera/cli/preset_manager.rb +12 -2
  65. data/lib/aspera/cli/prompt.rb +35 -0
  66. data/lib/aspera/cli/result.rb +13 -18
  67. data/lib/aspera/cli/runner.rb +31 -54
  68. data/lib/aspera/cli/special_values.rb +5 -0
  69. data/lib/aspera/cli/sync_actions.rb +41 -37
  70. data/lib/aspera/cli/terminal_formatter.rb +9 -3
  71. data/lib/aspera/cli/transfer_actions.rb +0 -6
  72. data/lib/aspera/cli/transfer_agent.rb +29 -35
  73. data/lib/aspera/cli/vault_manager.rb +0 -17
  74. data/lib/aspera/cli/version.rb +1 -1
  75. data/lib/aspera/cli/wizard.rb +4 -2
  76. data/lib/aspera/command_line_builder.rb +1 -0
  77. data/lib/aspera/coverage.rb +1 -0
  78. data/lib/aspera/environment.rb +7 -1
  79. data/lib/aspera/faspex_gw.rb +2 -1
  80. data/lib/aspera/faspex_postproc.rb +1 -0
  81. data/lib/aspera/graphql.rb +5 -5
  82. data/lib/aspera/json_rpc/client.rb +5 -5
  83. data/lib/aspera/keychain/encrypted_hash.rb +1 -1
  84. data/lib/aspera/keychain/one_password_api.rb +1 -1
  85. data/lib/aspera/link_header.rb +2 -2
  86. data/lib/aspera/log.rb +22 -25
  87. data/lib/aspera/markdown.rb +2 -0
  88. data/lib/aspera/mime.rb +25 -0
  89. data/lib/aspera/node_simulator.rb +1 -0
  90. data/lib/aspera/oauth/base.rb +35 -25
  91. data/lib/aspera/oauth/factory.rb +1 -0
  92. data/lib/aspera/oauth/generic.rb +1 -1
  93. data/lib/aspera/oauth/jwt.rb +1 -1
  94. data/lib/aspera/oauth/web.rb +9 -8
  95. data/lib/aspera/preview/file_types.rb +4 -4
  96. data/lib/aspera/preview/generator.rb +7 -0
  97. data/lib/aspera/preview/options.rb +4 -4
  98. data/lib/aspera/preview/terminal.rb +4 -3
  99. data/lib/aspera/preview/utils.rb +9 -6
  100. data/lib/aspera/products/connect.rb +1 -1
  101. data/lib/aspera/rainbow.rb +7 -0
  102. data/lib/aspera/rest/aspera_errors.rb +60 -0
  103. data/lib/aspera/rest/call_error.rb +27 -0
  104. data/lib/aspera/rest/client.rb +514 -0
  105. data/lib/aspera/rest/error_analyzer.rb +113 -0
  106. data/lib/aspera/rest/list.rb +143 -0
  107. data/lib/aspera/rest/parameters.rb +55 -0
  108. data/lib/aspera/rest/util.rb +176 -0
  109. data/lib/aspera/rest.rb +7 -621
  110. data/lib/aspera/schema/IBM Aspera Console-enhanced.yaml +1125 -0
  111. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +39 -0
  112. data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +2395 -0
  113. data/lib/aspera/schema/documentation.rb +13 -3
  114. data/lib/aspera/schema/registry.rb +18 -1
  115. data/lib/aspera/schema/validator.rb +92 -0
  116. data/lib/aspera/secret_hider.rb +36 -25
  117. data/lib/aspera/string_ext.rb +15 -0
  118. data/lib/aspera/temp_file_manager.rb +6 -5
  119. data/lib/aspera/transfer/parameters.rb +2 -0
  120. data/lib/aspera/transfer/spec.rb +1 -0
  121. data/lib/aspera/transfer/spec.schema.yaml +1 -0
  122. data/lib/aspera/uri_reader.rb +11 -11
  123. data/lib/aspera/web_auth/index.html +147 -0
  124. data/lib/aspera/web_auth/server.rb +81 -0
  125. data.tar.gz.sig +0 -0
  126. metadata +39 -7
  127. metadata.gz.sig +0 -0
  128. data/lib/aspera/colors.rb +0 -79
  129. data/lib/aspera/rest_call_error.rb +0 -25
  130. data/lib/aspera/rest_error_analyzer.rb +0 -111
  131. data/lib/aspera/rest_errors_aspera.rb +0 -58
  132. data/lib/aspera/rest_list.rb +0 -136
  133. data/lib/aspera/web_auth.rb +0 -211
@@ -5,6 +5,7 @@
5
5
  require 'aspera/schema/registry'
6
6
  require 'aspera/cli/plugins/oauth'
7
7
  require 'aspera/cli/extended_value'
8
+ require 'aspera/cli/formatter'
8
9
  require 'aspera/transfer/result'
9
10
  require 'aspera/cli/special_values'
10
11
  require 'aspera/cli/wizard'
@@ -15,6 +16,8 @@ require 'aspera/nagios'
15
16
  require 'aspera/environment'
16
17
  require 'aspera/assert'
17
18
  require 'securerandom'
19
+ require 'aspera/rainbow'
20
+ using Rainbow
18
21
 
19
22
  module Aspera
20
23
  module Cli
@@ -35,7 +38,7 @@ module Aspera
35
38
  urls.each do |base_url|
36
39
  # Faspex is always HTTPS
37
40
  next unless base_url.start_with?('https://')
38
- api = Rest.new(base_url: base_url, redirect_max: 1)
41
+ api = Rest::Client.new(base_url: base_url, redirect_max: 1)
39
42
  response = api.read(Api::Faspex::PATH_API_DETECT, ret: :resp)
40
43
  next unless response.code.start_with?('2') && response.body.strip.empty?
41
44
  # end is at -1, and subtract 1 for "/"
@@ -96,13 +99,6 @@ module Aspera
96
99
  option :shared_folder, description: 'Send package with files from shared folder'
97
100
  option :group_type, description: 'Type of shared box', allowed: %i[shared_inboxes workgroups], default: :shared_inboxes
98
101
 
99
- def initialize(**_)
100
- super
101
- options.parse_options!
102
- # [Aspera::Api::Faspex]
103
- @api_v5 = nil
104
- end
105
-
106
102
  # if recipient is just an email, then convert to expected API hash : name and type
107
103
  def normalize_recipients(parameters, type)
108
104
  type = type.to_s
@@ -117,7 +113,7 @@ module Aspera
117
113
  parameters[type].map! do |recipient_data|
118
114
  # If just a string, make a general lookup and build expected name/type hash
119
115
  if recipient_data.is_a?(String)
120
- matched = @api_v5.lookup_with_q('contacts', value: recipient_data, query: Rest.php_style({context: 'packages', type: recipient_types}))
116
+ matched = api_v5.lookup_with_q('contacts', value: recipient_data, query: Rest.php_style({context: 'packages', type: recipient_types}))
121
117
  recipient_data = {
122
118
  name: matched['name'],
123
119
  recipient_type: matched['type']
@@ -132,7 +128,7 @@ module Aspera
132
128
  def wait_package_status(id, status_list: Api::Faspex::PACKAGE_TERMINATED)
133
129
  total_sent = false
134
130
  loop do
135
- status = @api_v5.read("packages/#{id}/upload_details")
131
+ status = api_v5.read("packages/#{id}/upload_details")
136
132
  status['id'] = id
137
133
  # user asked to not follow
138
134
  return status if status_list.nil?
@@ -159,12 +155,12 @@ module Aspera
159
155
  def wait_for_job(job_id)
160
156
  result = nil
161
157
  loop do
162
- result = @api_v5.read("jobs/#{job_id}", {type: :formatted})
158
+ result = api_v5.read("jobs/#{job_id}", {type: :formatted})
163
159
  break unless Api::Faspex::JOB_RUNNING.include?(result['status'])
164
- RestParameters.instance.spinner_cb.call(result['status'])
160
+ Rest::Parameters.instance.spinner_cb.call(result['status'])
165
161
  sleep(0.5)
166
162
  end
167
- RestParameters.instance.spinner_cb.call(action: :success)
163
+ Rest::Parameters.instance.spinner_cb.call(action: :success)
168
164
  return result
169
165
  end
170
166
 
@@ -185,21 +181,21 @@ module Aspera
185
181
  when *Api::Faspex::API_LIST_MAILBOX_TYPES then "#{box}/packages"
186
182
  else
187
183
  group_type = options.get_option(:group_type)
188
- "#{group_type}/#{@api_v5.lookup_entity_by_field(entity: group_type, value: box)['id']}/packages"
184
+ "#{group_type}/#{api_v5.lookup_entity_by_field(entity: group_type, value: box)['id']}/packages"
189
185
  end
190
186
  # Merge default query with user-provided query: user values take precedence, but defaults are preserved
191
187
  user_query = query_read_delete(schema: Schema::Registry.query_params(Schema::Registry::FASPEX, 'packages'))
192
188
  merged_query = user_query.nil? ? query.dup : query.merge(user_query)
193
189
  # Extract `max` before the API call so callers can apply it after post-API filtering
194
- max_items = merged_query.delete(RestList::MAX_ITEMS)&.to_i
195
- list, total = @api_v5.list_entities_limit_offset_total_count(entity: entity, query: merged_query)
190
+ max_items = merged_query.delete(Rest::List::MAX_ITEMS)&.to_i
191
+ list, total = api_v5.list_entities_limit_offset_total_count(entity: entity, query: merged_query)
196
192
  return list.select(&filter), max_items, total
197
193
  end
198
194
 
199
195
  # Build query to get package recipients based on package info in case of shared inbox or workgroup recipient
200
196
  # @param package_id [String] the package id to get info from
201
197
  def recipient_query(package_id)
202
- package_info = @api_v5.read("packages/#{package_id}")
198
+ package_info = api_v5.read("packages/#{package_id}")
203
199
  base_query = {}
204
200
  base_query['recipient_workgroup_id'] = package_info['recipients'].first['id'] if WORKGROUP_TYPES.include?(package_info['recipients'].first['recipient_type'])
205
201
  base_query['recipient_user_id'] = package_info['recipients'].first['id'] if package_info['recipients'].first['recipient_type'].eql?('user')
@@ -242,7 +238,7 @@ module Aspera
242
238
  # a single id was provided, or a list of ids
243
239
  package_ids = [package_ids] unless package_ids.is_a?(Array)
244
240
  Aspera.assert_array_all(package_ids, String) { 'Package id(s)' }
245
- # packages = package_ids.map{|pkg_id|@api_v5.read("packages/#{pkg_id}")}
241
+ # packages = package_ids.map{|pkg_id|api_v5.read("packages/#{pkg_id}")}
246
242
  packages = package_ids.map { |pkg_id| {'id'=>pkg_id} }
247
243
  end
248
244
  result_transfer = []
@@ -257,12 +253,12 @@ module Aspera
257
253
  type: Api::Faspex.box_type(box),
258
254
  transfer_type: Api::Faspex::TRANSFER_CONNECT
259
255
  }
260
- # download_params[:recipient_workgroup_id] = @api_v5.lookup_entity_by_field(entity: options.get_option(:group_type), value: box)['id'] if !Api::Faspex::API_LIST_MAILBOX_TYPES.include?(box) && box != SpecialValues::ALL
256
+ # download_params[:recipient_workgroup_id] = api_v5.lookup_entity_by_field(entity: options.get_option(:group_type), value: box)['id'] if !Api::Faspex::API_LIST_MAILBOX_TYPES.include?(box) && box != SpecialValues::ALL
261
257
  packages.each do |package|
262
258
  pkg_id = package['id']
263
259
  formatter.display_status("Receiving package #{pkg_id}")
264
260
  # TODO: allow from sent as well ?
265
- transfer_spec = @api_v5.call(
261
+ transfer_spec = api_v5.call(
266
262
  operation: 'POST',
267
263
  subpath: "packages/#{pkg_id}/transfer_spec/download",
268
264
  query: download_params.merge(recipient_query(pkg_id)),
@@ -285,10 +281,10 @@ module Aspera
285
281
 
286
282
  def package_send(parameters)
287
283
  # autofill recipient for public url
288
- if @api_v5.pub_link_context&.key?('recipient_type') && !parameters.key?('recipients')
284
+ if api_v5.pub_link_context&.key?('recipient_type') && !parameters.key?('recipients')
289
285
  parameters['recipients'] = [{
290
- name: @api_v5.pub_link_context['name'],
291
- recipient_type: @api_v5.pub_link_context['recipient_type']
286
+ name: api_v5.pub_link_context['name'],
287
+ recipient_type: api_v5.pub_link_context['recipient_type']
292
288
  }]
293
289
  end
294
290
  PACKAGE_RECIPIENT_TYPES.each { |type| normalize_recipients(parameters, type) }
@@ -298,11 +294,11 @@ module Aspera
298
294
  transfer.user_transfer_spec.delete('content_protection')
299
295
  parameters['ear_enabled'] = true
300
296
  end
301
- package = @api_v5.create('packages', parameters)
297
+ package = api_v5.create('packages', parameters)
302
298
  shared_folder = options.get_option(:shared_folder)
303
299
  if shared_folder.nil?
304
300
  # send from local files
305
- transfer_spec = @api_v5.create(
301
+ transfer_spec = api_v5.create(
306
302
  "packages/#{package['id']}/transfer_spec/upload",
307
303
  {paths: transfer.source_list},
308
304
  query: {transfer_type: Api::Faspex::TRANSFER_CONNECT}
@@ -313,7 +309,7 @@ module Aspera
313
309
  else
314
310
  # send from remote shared folder
315
311
  if (m = Parser.percent_selector(shared_folder))
316
- shared_folder = @api_v5.lookup_entity_by_field(
312
+ shared_folder = api_v5.lookup_entity_by_field(
317
313
  entity: 'shared_folders',
318
314
  field: m[:field],
319
315
  value: m[:value]
@@ -321,7 +317,7 @@ module Aspera
321
317
  end
322
318
  transfer_request = {shared_folder_id: shared_folder, paths: transfer.source_list}
323
319
  # start remote transfer and get first status
324
- result = @api_v5.create("packages/#{package['id']}/remote_transfer", transfer_request)
320
+ result = api_v5.create("packages/#{package['id']}/remote_transfer", transfer_request)
325
321
  result['id'] = package['id']
326
322
  unless result['status'].eql?('completed')
327
323
  formatter.display_status("Package #{package['id']}")
@@ -341,7 +337,7 @@ module Aspera
341
337
  Aspera.assert_type(filters, Hash)
342
338
  filters['basenames'] ||= []
343
339
  Aspera.assert_type(filters, Hash) { 'filters' }
344
- max_items = query.delete(RestList::MAX_ITEMS)
340
+ max_items = query.delete(Rest::List::MAX_ITEMS)
345
341
  recursive = query.delete('recursive')
346
342
  use_paging = query.delete('paging') { true }
347
343
  if use_paging
@@ -356,7 +352,7 @@ module Aspera
356
352
  until folders_to_process.empty?
357
353
  path = folders_to_process.shift
358
354
  loop do
359
- data, http = @api_v5.call(
355
+ data, http = api_v5.call(
360
356
  operation: 'POST',
361
357
  subpath: browse_endpoint,
362
358
  query: query,
@@ -380,49 +376,49 @@ module Aspera
380
376
  break if data['item_count'].eql?(0)
381
377
  query['offset'] += data['item_count']
382
378
  end
383
- RestParameters.instance.spinner_cb.call(all_items.count)
379
+ Rest::Parameters.instance.spinner_cb.call(all_items.count)
384
380
  end
385
381
  query.delete('iteration_token')
386
382
  end
387
- RestParameters.instance.spinner_cb.call(action: :success)
383
+ Rest::Parameters.instance.spinner_cb.call(action: :success)
388
384
  return Result::ObjectList.new(all_items, total: total_count)
389
385
  end
390
386
 
391
387
  # Per-resource configuration for admin CRUD sub-trees.
392
- # Keys mirror entity_list/show/create/modify/delete kwargs; extra_commands lists additional leaf commands.
388
+ # Keys mirror entity_list/show/create/modify/delete kwargs; extra_commands declares additional leaf commands: {name => command attributes}.
393
389
  # @return [Hash{Symbol => Hash}]
394
390
  RESOURCE_CONFIG = {
395
391
  accounts: {
396
- display_fields: -> { Formatter.all_but('user_profile_data_attributes') },
397
- extra_commands: [:reset_password],
398
- instance_arg_commands: {reset_password: {arguments: [{name: :contact_id, type: :identifier, lookup: ->(field, value, **) { res_lookup_id(:accounts, field, value) }}]}},
399
- query_component: Schema::Registry::FASPEX,
400
- body_component: Schema::Registry::FASPEX
392
+ display_fields: Formatter.all_but('user_profile_data_attributes'),
393
+ extra_commands: {reset_password: {description: 'Send password reset email to account', arguments: [{name: :account_id, type: :identifier, lookup: ->(field, value, **) { res_lookup_id(:accounts, field, value) }}]}},
394
+ query_component: Schema::Registry::FASPEX,
395
+ body_component: Schema::Registry::FASPEX
401
396
  },
402
397
  alternate_addresses: {entity: 'configuration/alternate_addresses', query_component: Schema::Registry::FASPEX, body_component: Schema::Registry::FASPEX},
403
398
  contacts: {query_component: Schema::Registry::FASPEX},
404
399
  distribution_lists: {entity: 'account/distribution_lists', delete_style: 'ids', query_component: Schema::Registry::FASPEX, body_component: Schema::Registry::FASPEX},
405
400
  email_notifications: {id_as_arg: 'type', query_component: Schema::Registry::FASPEX},
406
401
  file_processing: {
407
- commands: %i[next modify],
402
+ commands: %i[modify],
403
+ extra_commands: {next: {description: 'List next files to process'}},
408
404
  body_component: Schema::Registry::FASPEX,
409
405
  is_singleton: true
410
406
  },
411
407
  jobs: {display_fields: %w[id job_name job_type status], query_component: Schema::Registry::FASPEX},
412
408
  metadata_profiles: {entity: 'configuration/metadata_profiles', items_key: 'profiles', query_component: Schema::Registry::FASPEX, body_component: Schema::Registry::FASPEX},
413
409
  nodes: {
414
- extra_commands: %i[browse],
415
- instance_arg_commands: {
410
+ extra_commands: {
416
411
  browse: {
417
- arguments: [{name: :node_id, type: :identifier, lookup: :lookup_node_id},
418
- {name: :folder_path, type: String, mandatory: false, default: '/'}]
412
+ description: 'Browse files of node',
413
+ arguments: [{name: :node_id, type: :identifier, lookup: :lookup_node_id},
414
+ {name: :folder_path, mandatory: false, default: '/'}]
419
415
  }
420
416
  },
421
- query_component: Schema::Registry::FASPEX,
422
- body_component: Schema::Registry::FASPEX
417
+ query_component: Schema::Registry::FASPEX,
418
+ body_component: Schema::Registry::FASPEX
423
419
  },
424
420
  oauth_clients: {
425
- display_fields: -> { Formatter.all_but('public_key') },
421
+ display_fields: Formatter.all_but('public_key'),
426
422
  api: -> { Api::Faspex.new(root: Api::Faspex::PATH_AUTH, **Oauth.kwargs_from_options(options)) },
427
423
  list_query: {'expand': true, 'no_api_path': true, 'client_types[]': 'public'},
428
424
  query_component: Schema::Registry::FASPEX,
@@ -446,7 +442,7 @@ module Aspera
446
442
  def res_exec_args(res_sym)
447
443
  cfg = RESOURCE_CONFIG.fetch(res_sym, {})
448
444
  {
449
- api: resource_config_value(cfg, :api) || @api_v5,
445
+ api: resource_config_value(cfg, :api) || api_v5,
450
446
  entity: resource_config_value(cfg, :entity) || res_sym.to_s,
451
447
  items_key: resource_config_value(cfg, :items_key),
452
448
  delete_style: resource_config_value(cfg, :delete_style),
@@ -461,7 +457,7 @@ module Aspera
461
457
 
462
458
  # admin > nodes — lookup node id by field/value
463
459
  def lookup_node_id(field, value, **)
464
- @api_v5.lookup_entity_by_field(entity: 'nodes', field: field, value: value)['id']
460
+ api_v5.lookup_entity_by_field(entity: 'nodes', field: field, value: value)['id']
465
461
  end
466
462
 
467
463
  # Lookup id for a RESOURCE_CONFIG resource by field/value.
@@ -470,7 +466,7 @@ module Aspera
470
466
  entity = resource_config_value(cfg, :entity) || res_sym.to_s
471
467
  items_key = resource_config_value(cfg, :items_key)
472
468
  res_id_query = resource_config_value(cfg, :res_id_query) || :default
473
- @api_v5.lookup_entity_by_field(entity: entity, value: value, field: field, items_key: items_key, query: res_id_query)['id']
469
+ api_v5.lookup_entity_by_field(entity: entity, value: value, field: field, items_key: items_key, query: res_id_query)['id']
474
470
  end
475
471
 
476
472
  # Dynamically define lookup methods for admin resources (used by crud_commands)
@@ -482,78 +478,86 @@ module Aspera
482
478
 
483
479
  # --- DSL ---
484
480
 
485
- # Commands that need @api_v5 carry setup: :setup_api_v5.
486
- # :health and :postprocessing work without authentication, so they have no setup.
481
+ # :health and :postprocessing work without authentication: they do not use api_v5.
487
482
  command :health, description: 'Check Faspex 5 health'
488
- command :version, description: 'Show Faspex 5 version', setup: :setup_api_v5, action: -> { Result::SingleObject.new(@api_v5.read('version')) }
489
- command :bearer_token, description: 'Show OAuth bearer token', setup: :setup_api_v5, action: -> { Result::Text.new(@api_v5.oauth.authorization) }
490
- command :packages, description: 'Manage packages', setup: :setup_api_v5
483
+ command :version, description: 'Show Faspex 5 version', action: ->(**) { Result::SingleObject.new(api_v5.read('version')) }
484
+ command :bearer_token, description: 'Show OAuth bearer token', action: ->(**) { Result::Text.new(api_v5.oauth.authorization) }
485
+ # Package id argument; with a public link to a package, setup_package_id provides it instead
486
+ PACKAGE_ID_ARG = [{name: :package_id, type: :identifier}].freeze
487
+ private_constant :PACKAGE_ID_ARG
488
+ command :packages, description: 'Manage packages'
491
489
  commands_under :packages do
492
490
  command :list, description: 'List packages',
493
491
  arguments: [{name: :filter, mandatory: false, default: nil, type: Proc}]
494
492
  command :send, description: 'Send a package', transfer_paths: :send,
495
- arguments: [{name: :data, type: Hash, schema: Schema::Registry.req_body(Schema::Registry::FASPEX, 'packages.post')}],
496
- action: ->(data:, **) { package_send(data) }
497
- command :show, description: 'Show a package', setup: :setup_package_id,
498
- action: ->(package_id:, **) { Result::SingleObject.new(@api_v5.read("packages/#{package_id}")) }
493
+ arguments: [{name: :package, type: Hash, schema: Schema::Registry.req_body(Schema::Registry::FASPEX, 'packages.post')}],
494
+ action: ->(package:, **) { package_send(package) }
495
+ command :show, description: 'Show a package', setup: :setup_package_id, arguments: PACKAGE_ID_ARG,
496
+ action: ->(package_id:, **) { Result::SingleObject.new(api_v5.read("packages/#{package_id}")) }
499
497
  command :browse, description: 'Browse package files', setup: :setup_package_id,
500
- arguments: [{name: :folder_path, type: String, mandatory: false, default: '/'}],
498
+ arguments: PACKAGE_ID_ARG + [{name: :folder_path, mandatory: false, default: '/'}],
501
499
  action: ->(folder_path:, package_id:, **) { browse_folder("packages/#{package_id}/files/#{Api::Faspex.box_type(options.get_option(:box))}", recipient_query(package_id), folder_path: folder_path) }
502
500
  command :status, description: 'Wait for package status', setup: :setup_package_id,
503
- arguments: [{name: :status_list, type: Array, mandatory: false, default: nil}],
501
+ arguments: PACKAGE_ID_ARG + [{name: :status_list, type: Array, mandatory: false, default: nil}],
504
502
  action: ->(status_list:, package_id:, **) { Result::SingleObject.new(wait_package_status(package_id, status_list: status_list)) }
505
- command :delete, description: 'Delete packages', setup: :setup_package_id
506
- command :receive, description: 'Receive a package', setup: :setup_package_id, transfer_paths: :receive,
503
+ command :delete, description: 'Delete packages', setup: :setup_package_id, arguments: PACKAGE_ID_ARG
504
+ command :receive, description: 'Receive a package', setup: :setup_package_id, transfer_paths: :receive, arguments: PACKAGE_ID_ARG,
507
505
  action: ->(package_id:, **) { package_receive(package_id) }
508
- command(:file_processing, description: 'Show file processing status', setup: :setup_package_id, action: lambda do |package_id:, **|
509
- result, count = @api_v5.list_entities_limit_offset_total_count(entity: "packages/#{package_id}/file_statuses", items_key: 'files')
506
+ command(:file_processing, description: 'Show file processing status', setup: :setup_package_id, arguments: PACKAGE_ID_ARG, action: lambda do |package_id:, **|
507
+ result, count = api_v5.list_entities_limit_offset_total_count(entity: "packages/#{package_id}/file_statuses", items_key: 'files')
510
508
  Result::ObjectList.new(result, total: count)
511
509
  end)
512
510
  end
513
- command :admin, description: 'Administer Faspex 5', setup: :setup_api_v5
514
- command :user, description: 'Manage current user', setup: :setup_api_v5
515
- command :shared_folders, description: 'Browse shared folders', setup: :setup_api_v5
516
- command :gateway, description: 'Start Faspex 4 gateway emulation', setup: :setup_api_v5,
511
+ command :admin, description: 'Administer Faspex 5'
512
+ command :user, description: 'Manage current user'
513
+ command :shared_folders, description: 'Browse shared folders'
514
+ command :gateway, description: 'Start Faspex 4 gateway emulation',
517
515
  arguments: [{name: :parameters, type: Hash, mandatory: false, default: {}}]
518
516
  command :postprocessing, description: 'Start Faspex 4 post-processing server',
519
517
  arguments: [{name: :parameters, type: Hash, mandatory: false, default: {}}]
520
- command :invitations, description: 'Manage invitations', setup: :setup_api_v5
518
+ command :invitations, description: 'Manage invitations'
521
519
 
522
520
  commands_under :invitations do
523
521
  command :create, description: 'Create an invitation',
524
- arguments: [{name: :input_data, type: Hash, bulk: true}]
525
- command :resend, description: 'Resend an invitation',
526
- arguments: [{name: :invitation_id, type: :identifier}]
522
+ arguments: [{name: :invitation, type: Hash, bulk: true}]
523
+ command(
524
+ :resend, description: 'Resend an invitation',
525
+ arguments: [{name: :invitation_id, type: :identifier}],
526
+ action: lambda do |invitation_id:, **|
527
+ api_v5.create("invitations/#{invitation_id}/resend", nil)
528
+ Result::Status.new('Invitation resent')
529
+ end
530
+ )
527
531
  crud_commands entity: 'invitations',
528
- api: :@api_v5,
532
+ api: :api_v5,
529
533
  operations: Operations::ALL - %i[create],
530
534
  items_key: 'invitations',
531
535
  display_fields: %w[id public recipient_type recipient_name email_address]
532
536
  end
533
537
 
534
538
  commands_under :user do
535
- command :account, description: 'Show account information', action: -> { Result::SingleObject.new(@api_v5.read('account', query_read_delete)) }
539
+ command :account, description: 'Show account information', action: ->(**) { Result::SingleObject.new(api_v5.read('account', query_read_delete)) }
536
540
  command :profile, description: 'Manage user profile'
537
541
  end
538
542
 
539
543
  commands_under %i[user profile] do
540
- command :show, description: 'Show user profile', action: -> { Result::SingleObject.new(@api_v5.read('account/preferences')) }
544
+ command :show, description: 'Show user profile', action: ->(**) { Result::SingleObject.new(api_v5.read('account/preferences')) }
541
545
  command(
542
546
  :modify,
543
547
  description: 'Modify user profile',
544
- arguments: [{name: :properties, type: Hash}],
545
- action: lambda do |properties:, **|
546
- @api_v5.update('account/preferences', properties)
548
+ arguments: [{name: :profile, type: Hash}],
549
+ action: lambda do |profile:, **|
550
+ api_v5.update('account/preferences', profile)
547
551
  Result::Status.new('modified')
548
552
  end
549
553
  )
550
554
  end
551
555
 
552
556
  commands_under :shared_folders do
553
- command :list, description: 'List shared folders', action: -> { Result::ObjectList.new(@api_v5.read('shared_folders')['shared_folders']) }
557
+ command :list, description: 'List shared folders', action: ->(**) { Result::ObjectList.new(api_v5.read('shared_folders')['shared_folders']) }
554
558
  command :browse, description: 'Browse a shared folder',
555
559
  arguments: [{name: :shared_folder_id, type: :identifier, lookup: :lookup_shared_folder_id},
556
- {name: :folder_path, type: String, mandatory: false, default: '/'}]
560
+ {name: :folder_path, mandatory: false, default: '/'}]
557
561
  end
558
562
 
559
563
  # admin sub-tree: fixed commands + all ADMIN_RESOURCES with their sub-commands
@@ -561,21 +565,26 @@ module Aspera
561
565
  command :configuration, description: 'Manage Faspex 5 configuration'
562
566
  command :smtp, description: 'Manage SMTP configuration'
563
567
  command :events, description: 'List events'
564
- command :clean_deleted, description: 'Clean deleted packages',
565
- arguments: [{name: :input_data, type: Hash, mandatory: false, default: {}}]
568
+ command(
569
+ :clean_deleted, description: 'Clean deleted packages',
570
+ arguments: [{name: :parameters, type: Hash, mandatory: false, default: {}}],
571
+ action: lambda do |parameters: {}, **|
572
+ parameters = api_v5.read('configuration').slice('days_before_deleting_package_records') if parameters.empty?
573
+ Result::SingleObject.new(api_v5.create('internal/packages/clean_deleted', parameters))
574
+ end
575
+ )
566
576
  Api::Faspex::ADMIN_RESOURCES.each do |res|
567
577
  cfg = RESOURCE_CONFIG.fetch(res, {})
568
- extra = cfg[:extra_commands] || []
569
- ia_cmds = cfg[:instance_arg_commands] || {}
578
+ extra = cfg[:extra_commands] || {}
570
579
  is_singleton = cfg[:is_singleton] || false
571
580
  entity_path = cfg[:entity] || res.to_s
572
581
  crud_ops = ((cfg[:commands] || Operations::ALL) - %i[list]) & Operations::ALL
573
582
 
574
- command res, description: "Manage #{res.to_s.tr('_', ' ')}"
575
- commands_under(res) do
583
+ command res, description: "Manage #{entity_noun(res, singular: false)}"
584
+ commands_under res do
576
585
  # List is handled with Faspex 5 pagination (item_list_with_total)
577
586
  unless is_singleton
578
- list_kwargs = {description: 'List'}
587
+ list_kwargs = {description: operation_description(:list, entity_noun(entity_path))}
579
588
  list_kwargs[:query_schema] = Schema::Registry.query_params(cfg[:query_component], entity_path) if cfg[:query_component]
580
589
  command :list, **list_kwargs
581
590
  end
@@ -594,9 +603,8 @@ module Aspera
594
603
  )
595
604
 
596
605
  # Extra commands (e.g. browse, reset_password, next)
597
- extra.each do |c|
598
- ia = ia_cmds[c] || {}
599
- command c, description: c.to_s.tr('_', ' ').capitalize, **ia
606
+ extra.each do |name, attrs|
607
+ command name, **attrs
600
608
  end
601
609
  end
602
610
  end
@@ -612,19 +620,19 @@ module Aspera
612
620
  # entity: :sf_entity is resolved at runtime from ctx (injected by setup_admin_nodes_shared_folders).
613
621
  commands_under %i[admin nodes shared_folders] do
614
622
  crud_commands entity: :sf_entity,
615
- api: :@api_v5,
623
+ api: :api_v5,
616
624
  name: 'shared folder',
617
625
  lookup: :lookup_sf_id,
618
626
  items_key: 'shared_folders'
619
- command :user, description: 'Custom access users',
620
- arguments: [{name: :sf_id, type: :identifier, lookup: :lookup_sf_id}],
627
+ command :user, description: 'Manage custom access users',
628
+ arguments: [{name: :shared_folder_id, type: :identifier, lookup: :lookup_sf_id}],
621
629
  setup: :setup_admin_nodes_shared_folders_user
622
630
  end
623
631
 
624
632
  # admin > nodes > shared_folders > user sub-tree (custom access users)
625
633
  # entity: :user_path is resolved at runtime from ctx (injected by setup_admin_nodes_shared_folders_user).
626
634
  commands_under %i[admin nodes shared_folders user] do
627
- crud_commands entity: :user_path, api: :@api_v5, name: 'custom access user', lookup: :lookup_sf_user_id, items_key: 'users'
635
+ crud_commands entity: :user_path, api: :api_v5, name: 'custom access user', lookup: :lookup_sf_user_id, items_key: 'users'
628
636
  end
629
637
 
630
638
  # `members` and `saml_groups`
@@ -632,41 +640,48 @@ module Aspera
632
640
  CRUD_NO_LIST = %i[create modify delete show].freeze
633
641
 
634
642
  RES_SINGULAR = {shared_inboxes: :shared_inbox, workgroups: :workgroup}.freeze
635
- private_constant :RES_SINGULAR
643
+ # Access levels of members, per resource
644
+ MEMBER_ACCESS = {
645
+ shared_inboxes: %i[submit_only standard shared_inbox_admin].freeze,
646
+ workgroups: %i[standard workgroup_admin].freeze
647
+ }.freeze
648
+ private_constant :RES_SINGULAR, :MEMBER_ACCESS
636
649
 
637
650
  # admin > shared_inboxes|workgroups > members|saml_groups|invite_external_collaborator:
638
651
  # res_id consumed via arguments:(:identifier) + lookup:, builds res_instance_path for all children
639
652
  %i[shared_inboxes workgroups].each do |res|
640
- lookup_res_id = ->(field, value, **) { @api_v5.lookup_entity_by_field(entity: res.to_s, field: field, value: value, query: {'all': true})['id'] }
653
+ lookup_res_id = ->(field, value, **) { api_v5.lookup_entity_by_field(entity: res.to_s, field: field, value: value, query: {'all': true})['id'] }
641
654
  commands_under [:admin, res] do
642
- command :members, description: 'Members',
655
+ command :members, description: 'Manage members',
643
656
  arguments: [{name: :"#{RES_SINGULAR[res]}_id", type: :identifier, lookup: lookup_res_id}],
644
657
  setup: :"setup_admin_#{res}_instance"
645
- command :saml_groups, description: 'Saml groups',
658
+ command :saml_groups, description: 'Manage SAML groups',
646
659
  arguments: [{name: :"#{RES_SINGULAR[res]}_id", type: :identifier, lookup: lookup_res_id}],
647
660
  setup: :"setup_admin_#{res}_instance"
648
661
  command :invite_external_collaborator, description: 'Invite external collaborator',
649
662
  arguments: [{name: :"#{RES_SINGULAR[res]}_id", type: :identifier, lookup: lookup_res_id},
650
- {name: :input_data, type: Hash}]
663
+ {name: :collaborator, type: Hash}]
651
664
  end
652
665
 
653
666
  commands_under [:admin, res, :members] do
654
667
  CRUD_NO_SHOW.each do |c|
655
668
  args =
656
669
  if c.eql?(:create)
657
- {arguments: [{name: :users, bulk: true}, {name: :access, mandatory: false, default: :standard}]}
670
+ {arguments: [{name: :users, bulk: true}, {name: :access, allowed: MEMBER_ACCESS[res], mandatory: false, default: :standard}]}
671
+ elsif c.eql?(:modify)
672
+ {arguments: [{name: :member_id, type: :identifier, lookup: :"lookup_#{res}_members_id"}, {name: :member, type: Hash}]}
658
673
  elsif Operations::INSTANCE.include?(c)
659
674
  {arguments: [{name: :member_id, type: :identifier, lookup: :"lookup_#{res}_members_id"}]}
660
675
  else
661
676
  {}
662
677
  end
663
- command c, description: c.to_s.capitalize, **args
678
+ command c, description: operation_description(c, 'member'), **args
664
679
  end
665
680
  end
666
681
 
667
682
  commands_under [:admin, res, :saml_groups] do
668
683
  crud_commands entity: :saml_groups_path,
669
- api: :@api_v5,
684
+ api: :api_v5,
670
685
  name: 'SAML group',
671
686
  operations: CRUD_NO_SHOW,
672
687
  lookup: :"lookup_#{res}_saml_groups_id",
@@ -675,57 +690,51 @@ module Aspera
675
690
  end
676
691
 
677
692
  commands_under %i[admin configuration] do
678
- command :show, description: 'Show configuration', action: -> { Result::SingleObject.new(@api_v5.read('configuration')) }
693
+ command :show, description: 'Show configuration', action: ->(**) { Result::SingleObject.new(api_v5.read('configuration')) }
679
694
  command :modify, description: 'Modify configuration',
680
- arguments: [{name: :input_data, type: Hash}],
681
- action: ->(input_data:, **) { Result::SingleObject.new(@api_v5.update('configuration', input_data)) }
695
+ arguments: [{name: :configuration, type: Hash}],
696
+ action: ->(configuration:, **) { Result::SingleObject.new(api_v5.update('configuration', configuration)) }
682
697
  end
683
698
 
684
699
  commands_under %i[admin smtp] do
685
- command :show, description: 'Show SMTP configuration', action: -> { Result::SingleObject.new(@api_v5.read('configuration/smtp')) }
700
+ command :show, description: 'Show SMTP configuration', action: ->(**) { Result::SingleObject.new(api_v5.read('configuration/smtp')) }
686
701
  command :create, description: 'Create SMTP configuration',
687
- arguments: [{name: :input_data, type: Hash}],
688
- action: ->(input_data:, **) { Result::SingleObject.new(@api_v5.create('configuration/smtp', input_data)) }
702
+ arguments: [{name: :smtp, type: Hash}],
703
+ action: ->(smtp:, **) { Result::SingleObject.new(api_v5.create('configuration/smtp', smtp)) }
689
704
  command :modify, description: 'Modify SMTP configuration',
690
- arguments: [{name: :input_data, type: Hash}],
691
- action: ->(input_data:, **) { Result::SingleObject.new(@api_v5.update('configuration/smtp', input_data)) }
705
+ arguments: [{name: :smtp, type: Hash}],
706
+ action: ->(smtp:, **) { Result::SingleObject.new(api_v5.update('configuration/smtp', smtp)) }
692
707
  command(
693
708
  :delete, description: 'Delete SMTP configuration',
694
- action: lambda do
695
- @api_v5.delete('configuration/smtp')
709
+ action: lambda do |**|
710
+ api_v5.delete('configuration/smtp')
696
711
  Result::Status.new('SMTP configuration deleted')
697
712
  end
698
713
  )
699
714
  command :test, description: 'Test SMTP configuration',
700
- arguments: [{name: :test_data, type: nil}]
715
+ arguments: [{name: :recipient}]
701
716
  end
702
717
 
703
718
  commands_under %i[admin events] do
704
719
  command(
705
720
  :application, description: 'List application events',
706
- action: lambda do
707
- list, total = @api_v5.list_entities_limit_offset_total_count(entity: 'application_events', query: query_read_delete(schema: Schema::Registry.query_params(Schema::Registry::FASPEX, 'application_events')))
721
+ action: lambda do |**|
722
+ list, total = api_v5.list_entities_limit_offset_total_count(entity: 'application_events', query: query_read_delete(schema: Schema::Registry.query_params(Schema::Registry::FASPEX, 'application_events')))
708
723
  Result::ObjectList.new(list, total: total, fields: %w[event_type created_at application user.name])
709
724
  end
710
725
  )
711
726
  command(
712
727
  :webhook, description: 'List webhook events',
713
- action: lambda do
714
- list, total = @api_v5.list_entities_limit_offset_total_count(entity: 'all_webhooks_events', query: query_read_delete(schema: Schema::Registry.query_params(Schema::Registry::FASPEX, 'all_webhooks_events')), items_key: 'events')
728
+ action: lambda do |**|
729
+ list, total = api_v5.list_entities_limit_offset_total_count(entity: 'all_webhooks_events', query: query_read_delete(schema: Schema::Registry.query_params(Schema::Registry::FASPEX, 'all_webhooks_events')), items_key: 'events')
715
730
  Result::ObjectList.new(list, total: total)
716
731
  end
717
732
  )
718
733
  end
719
734
 
720
- # admin > clean_deleted handler (leaf, no sub-commands)
721
- define_action_method(%i[admin clean_deleted]) do |input_data: {}, **|
722
- input_data = @api_v5.read('configuration').slice('days_before_deleting_package_records') if input_data.empty?
723
- Result::SingleObject.new(@api_v5.create('internal/packages/clean_deleted', input_data))
724
- end
725
-
726
735
  # admin > <resource> > list
727
736
  Api::Faspex::ADMIN_RESOURCES.each do |res|
728
- define_action_method([:admin, res, :list]) do
737
+ define_action_method([:admin, res, :list]) do |**|
729
738
  args = res_exec_args(res)
730
739
  # Special case: email_notifications list returns a fixed value list
731
740
  next Result::ValueList.new(Api::Faspex::EMAIL_NOTIF_LIST, name: 'email_id') if res.eql?(:email_notifications)
@@ -740,9 +749,9 @@ module Aspera
740
749
  end
741
750
  end
742
751
 
743
- def action_admin_smtp_test(test_data:, **)
744
- test_data = {test_email_recipient: test_data} if test_data.is_a?(String)
745
- creation = @api_v5.create('configuration/smtp/test', test_data)
752
+ def action_admin_smtp_test(recipient:, **)
753
+ recipient = {test_email_recipient: recipient} if recipient.is_a?(String)
754
+ creation = api_v5.create('configuration/smtp/test', recipient)
746
755
  result = wait_for_job(creation['job_id'])
747
756
  begin
748
757
  result['serialized_args'] = JSON.parse(result['serialized_args'])
@@ -753,15 +762,15 @@ module Aspera
753
762
  end
754
763
 
755
764
  # admin > accounts > reset_password
756
- def action_admin_accounts_reset_password(contact_id:, **)
757
- @api_v5.create("accounts/#{contact_id}/reset_password", {})
765
+ def action_admin_accounts_reset_password(account_id:, **)
766
+ api_v5.create("accounts/#{account_id}/reset_password", {})
758
767
  Result::Status.new('password reset, user shall check email')
759
768
  end
760
769
 
761
770
  # admin > file_processing > next
762
- def action_admin_file_processing_next
771
+ def action_admin_file_processing_next(**)
763
772
  args = res_exec_args(:file_processing)
764
- result, count = @api_v5.list_entities_limit_offset_total_count(entity: args[:entity], operation: 'POST', items_key: 'files')
773
+ result, count = api_v5.list_entities_limit_offset_total_count(entity: args[:entity], operation: 'POST', items_key: 'files')
765
774
  Result::ObjectList.new(result, total: count)
766
775
  end
767
776
 
@@ -778,18 +787,18 @@ module Aspera
778
787
  # Lookup shared folder id by field/value within a node's shared_folders entity.
779
788
  # sf_entity is in ctx from setup_admin_nodes_shared_folders (Phase A of parent).
780
789
  def lookup_sf_id(field, value, sf_entity:, **)
781
- @api_v5.lookup_entity_by_field(entity: sf_entity, items_key: 'shared_folders', field: field, value: value)['id']
790
+ api_v5.lookup_entity_by_field(entity: sf_entity, items_key: 'shared_folders', field: field, value: value)['id']
782
791
  end
783
792
 
784
793
  # Lookup custom access user id by field/value within a shared folder's users entity.
785
794
  # user_path is in ctx from setup_admin_nodes_shared_folders_user (Phase A of parent).
786
795
  def lookup_sf_user_id(field, value, user_path:, **)
787
- @api_v5.lookup_entity_by_field(entity: user_path, items_key: 'users', field: field, value: value)['id']
796
+ api_v5.lookup_entity_by_field(entity: user_path, items_key: 'users', field: field, value: value)['id']
788
797
  end
789
798
 
790
- # admin > nodes > shared_folders > user — sf_id: already in ctx via arguments: on the :user command
791
- def setup_admin_nodes_shared_folders_user(sf_entity:, sf_id:, **)
792
- {user_path: "#{sf_entity}/#{sf_id}/custom_access_users"}
799
+ # admin > nodes > shared_folders > user — shared_folder_id: already in ctx via arguments: on the :user command
800
+ def setup_admin_nodes_shared_folders_user(sf_entity:, shared_folder_id:, **)
801
+ {user_path: "#{sf_entity}/#{shared_folder_id}/custom_access_users"}
793
802
  end
794
803
 
795
804
  # admin > shared_inboxes|workgroups — res_id: already in ctx via arguments: on the :members|:saml_groups command
@@ -810,7 +819,7 @@ module Aspera
810
819
  users = [users] unless users.is_a?(Array)
811
820
  users.map do |user|
812
821
  if (m = Parser.percent_selector(user))
813
- @api_v5.lookup_entity_by_field(entity: 'accounts', field: m[:field], value: m[:value], query: Rest.php_style({type: ACCOUNT_TYPES}))['id']
822
+ api_v5.lookup_entity_by_field(entity: 'accounts', field: m[:field], value: m[:value], query: Rest.php_style({type: ACCOUNT_TYPES}))['id']
814
823
  else
815
824
  user
816
825
  end
@@ -822,11 +831,11 @@ module Aspera
822
831
  %i[shared_inboxes workgroups].each do |res|
823
832
  define_method(:"lookup_#{res}_members_id") do |field, value, res_instance_path:, **|
824
833
  res_path = "#{res_instance_path}/members"
825
- @api_v5.lookup_entity_by_field(entity: res_path, field: field, value: value, query: Rest.php_style({type: %w[user]}))['user_id']
834
+ api_v5.lookup_entity_by_field(entity: res_path, field: field, value: value, query: Rest.php_style({type: %w[user]}))['user_id']
826
835
  end
827
836
  define_method(:"lookup_#{res}_saml_groups_id") do |field, value, res_instance_path:, **|
828
837
  res_path = "#{res_instance_path}/saml_groups"
829
- @api_v5.lookup_entity_by_field(entity: res_path, field: field, value: value, query: Rest.php_style({type: %w[user]}))['user_id']
838
+ api_v5.lookup_entity_by_field(entity: res_path, field: field, value: value, query: Rest.php_style({type: %w[user]}))['user_id']
830
839
  end
831
840
  end
832
841
 
@@ -834,52 +843,52 @@ module Aspera
834
843
  # (saml_groups CRUD is handled by crud_commands)
835
844
  %i[shared_inboxes workgroups].each do |res|
836
845
  define_action_method([:admin, res, :members, :list]) do |res_instance_path:, **|
837
- entity_list(api: @api_v5, entity: "#{res_instance_path}/members", items_key: 'members')
846
+ entity_list(api: api_v5, entity: "#{res_instance_path}/members", items_key: 'members')
838
847
  end
839
848
 
840
- define_action_method([:admin, res, :members, :modify]) do |res_instance_path:, **kwargs|
841
- entity_modify(api: @api_v5, entity: "#{res_instance_path}/members", id: kwargs[:member_id])
849
+ define_action_method([:admin, res, :members, :modify]) do |res_instance_path:, member_id:, member:, **|
850
+ entity_modify(api: api_v5, entity: "#{res_instance_path}/members", id: member_id, data: member)
842
851
  end
843
852
 
844
853
  define_action_method([:admin, res, :members, :delete]) do |res_instance_path:, **kwargs|
845
- entity_delete(api: @api_v5, entity: "#{res_instance_path}/members", id: kwargs[:member_id])
854
+ entity_delete(api: api_v5, entity: "#{res_instance_path}/members", id: kwargs[:member_id])
846
855
  end
847
856
 
848
857
  define_action_method([:admin, res, :members, :create]) do |users:, access:, res_instance_path:, **|
849
858
  res_path = "#{res_instance_path}/members"
850
859
  resolved = resolve_member_user_ids(users)
851
- input_data = [{user: resolved.map { |u| {id: u, access: access} }}]
852
- entity_create(api: @api_v5, entity: res_path, input_data: input_data)
860
+ entity_create(api: api_v5, entity: res_path, data: {user: resolved.map { |u| {id: u, access: access} }})
853
861
  end
854
862
  end
855
863
 
856
864
  # admin > shared_inboxes|workgroups > invite_external_collaborator
857
865
  %i[shared_inboxes workgroups].each do |res|
858
- define_action_method([:admin, res, :invite_external_collaborator]) do |input_data:, **kwargs|
866
+ define_action_method([:admin, res, :invite_external_collaborator]) do |collaborator:, **kwargs|
859
867
  res_instance_path = "#{res}/#{kwargs[:"#{RES_SINGULAR[res]}_id"]}"
860
- result = @api_v5.create("#{res_instance_path}/external_collaborator", input_data)
868
+ result = api_v5.create("#{res_instance_path}/external_collaborator", collaborator)
861
869
  formatter.display_status(result['message'])
862
- Result::SingleObject.new(@api_v5.lookup_entity_by_field(entity: "#{res_instance_path}/members", items_key: 'members', value: input_data['email_address'], query: {}))
870
+ Result::SingleObject.new(api_v5.lookup_entity_by_field(entity: "#{res_instance_path}/members", items_key: 'members', value: collaborator['email_address'], query: {}))
863
871
  end
864
872
  end
865
873
 
866
- # --- root setup ---
874
+ # --- API ---
867
875
 
868
- # Build @api_v5 for all commands that need it (all except :health and :postprocessing).
869
- # @return [Hash] empty ctx (state stored in @api_v5)
870
- def setup_api_v5(**)
871
- return {} if @api_v5
876
+ # Faspex 5 API, built from CLI options on first use.
877
+ # @return [Api::Faspex]
878
+ def api_v5
879
+ return @api_v5 if @api_v5
872
880
  @api_v5 = Api::Faspex.new(**Oauth.kwargs_from_options(options))
873
881
  # in case user wants to use HTTPGW tell transfer agent how to get address
874
882
  transfer.httpgw_url_cb = lambda { @api_v5.read('account')['gateway_url'] }
875
- {}
883
+ @api_v5
876
884
  end
877
885
 
878
- # Setup for package sub-commands that need an id: resolves package_id from pub_link or argument.
879
- # @return [Hash] ctx key: package_id
886
+ # Setup for package sub-commands that need an id: with a public link to a package, the id comes from the link.
887
+ # Leaf setup runs before argument resolution: the declared package_id argument is then not read.
888
+ # @return [Hash] ctx key: package_id, or empty (package_id is read from the command line)
880
889
  def setup_package_id(**)
881
- package_id = @api_v5.pub_link_context&.key?('package_id') ? @api_v5.pub_link_context['package_id'] : options.instance_identifier
882
- {package_id: package_id}
890
+ return {} unless api_v5.pub_link_context&.key?('package_id')
891
+ {package_id: api_v5.pub_link_context['package_id']}
883
892
  end
884
893
 
885
894
  def action_packages_list(filter: nil, **)
@@ -895,7 +904,7 @@ module Aspera
895
904
  ids = package_id.is_a?(Array) ? package_id : [package_id]
896
905
  Aspera.assert_array_all(ids, String) { 'Package id(s)' }
897
906
  # API returns 204, empty on success
898
- @api_v5.call(
907
+ api_v5.call(
899
908
  operation: 'DELETE',
900
909
  subpath: 'packages',
901
910
  content_type: Mime::JSON,
@@ -907,10 +916,10 @@ module Aspera
907
916
 
908
917
  # --- handlers ---
909
918
 
910
- def action_health
919
+ def action_health(**)
911
920
  nagios = Nagios.new
912
921
  begin
913
- data, http = Rest.new(base_url: options.get_option(:url, mandatory: true))
922
+ data, http = Rest::Client.new(base_url: options.get_option(:url, mandatory: true))
914
923
  .read('health', ret: :both)
915
924
  data.each do |k, v|
916
925
  nagios.add_ok(k, v.to_s)
@@ -925,7 +934,7 @@ module Aspera
925
934
  # Lookup a shared folder id by field/value.
926
935
  # Called via lookup: :lookup_shared_folder_id on the shared_folders > browse command.
927
936
  def lookup_shared_folder_id(field, value, **)
928
- all = @api_v5.read('shared_folders')['shared_folders']
937
+ all = api_v5.read('shared_folders')['shared_folders']
929
938
  matches = all.select { |i| i[field].eql?(value) }
930
939
  Aspera.assert(!matches.empty?) { "no match for #{field} = #{value}" }
931
940
  Aspera.assert(matches.length == 1) { "multiple matches for #{field} = #{value}" }
@@ -933,7 +942,7 @@ module Aspera
933
942
  end
934
943
 
935
944
  def action_shared_folders_browse(folder_path:, shared_folder_id:, **)
936
- all_shared_folders = @api_v5.read('shared_folders')['shared_folders']
945
+ all_shared_folders = api_v5.read('shared_folders')['shared_folders']
937
946
  node = all_shared_folders.find { |i| i['id'].eql?(shared_folder_id) }
938
947
  Aspera.assert(!node.nil?) { "No such shared folder id #{shared_folder_id}" }
939
948
  browse_folder("nodes/#{node['node_id']}/shared_folders/#{shared_folder_id}/browse", {}, folder_path: folder_path)
@@ -941,15 +950,10 @@ module Aspera
941
950
 
942
951
  # invitations sub-handlers
943
952
 
944
- def action_invitations_resend(invitation_id:, **)
945
- @api_v5.create("invitations/#{invitation_id}/resend", nil)
946
- Result::Status.new('Invitation resent')
947
- end
948
-
949
- def action_invitations_create(input_data:, **)
950
- bulk_result(input_data, command: :create) do |params|
953
+ def action_invitations_create(invitation:, **)
954
+ bulk_result(invitation, command: :create) do |params|
951
955
  endpoint = params.key?('recipient_name') ? 'public_invitations' : 'invitations'
952
- @api_v5.create(endpoint, params)
956
+ api_v5.create(endpoint, params)
953
957
  end
954
958
  end
955
959
 
@@ -959,7 +963,7 @@ module Aspera
959
963
  uri = URI.parse(parameters.delete(:url) { WebServerSimple::DEFAULT_URL })
960
964
  server = WebServerSimple.new(uri, **parameters.slice(*WebServerSimple::PARAMS))
961
965
  Aspera.assert(parameters.except(*WebServerSimple::PARAMS).empty?) { "unexpected parameters: #{parameters.except(*WebServerSimple::PARAMS).keys}" }
962
- server.mount(uri.path, Faspex4GWServlet, @api_v5, nil)
966
+ server.mount(uri.path, Faspex4GWServlet, api_v5, nil)
963
967
  server.start
964
968
  Result::Status.new('Gateway terminated')
965
969
  end
@@ -974,12 +978,11 @@ module Aspera
974
978
  server.start
975
979
  Result::Status.new('Gateway terminated')
976
980
  end
977
- SHARED_INBOX_MEMBER_LEVELS = %i[submit_only standard shared_inbox_admin].freeze
978
981
  ACCOUNT_TYPES = %w{local_user saml_user self_registered_user external_user}.freeze
979
982
  WORKGROUP_TYPES = %w{workgroup shared_inbox}.freeze
980
983
  CONTACT_TYPES = (WORKGROUP_TYPES + %w{distribution_list user external_user}).freeze
981
984
  PACKAGE_RECIPIENT_TYPES = %i{recipients private_recipients notified_on_upload notified_on_download notified_on_receipt}
982
- private_constant :SHARED_INBOX_MEMBER_LEVELS, :ACCOUNT_TYPES, :CONTACT_TYPES, :PACKAGE_RECIPIENT_TYPES,
985
+ private_constant :ACCOUNT_TYPES, :CONTACT_TYPES, :PACKAGE_RECIPIENT_TYPES,
983
986
  :CRUD_NO_SHOW, :CRUD_NO_LIST
984
987
  end
985
988
  end