aspera-cli 4.27.0 → 4.27.2

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 (142) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +46 -1
  4. data/CONTRIBUTING.md +1 -7
  5. data/TODO.md +50 -0
  6. data/docs/README.md +709 -489
  7. data/lib/aspera/agent/base.rb +10 -2
  8. data/lib/aspera/agent/connect.rb +7 -7
  9. data/lib/aspera/agent/desktop.rb +3 -3
  10. data/lib/aspera/agent/direct.rb +48 -19
  11. data/lib/aspera/agent/factory.rb +3 -3
  12. data/lib/aspera/agent/httpgw.rb +55 -14
  13. data/lib/aspera/agent/node.rb +2 -2
  14. data/lib/aspera/agent/transferd.rb +12 -12
  15. data/lib/aspera/api/aoc.rb +28 -28
  16. data/lib/aspera/api/cos_node.rb +10 -9
  17. data/lib/aspera/api/faspex.rb +2 -2
  18. data/lib/aspera/api/httpgw.rb +20 -20
  19. data/lib/aspera/api/node.rb +31 -53
  20. data/lib/aspera/ascmd.rb +13 -13
  21. data/lib/aspera/ascp/installation.rb +43 -36
  22. data/lib/aspera/ascp/management.rb +6 -6
  23. data/lib/aspera/assert.rb +12 -13
  24. data/lib/aspera/cli/ascp_actions.rb +7 -7
  25. data/lib/aspera/cli/async_transfer_store.rb +49 -5
  26. data/lib/aspera/cli/bootstrapper.rb +9 -9
  27. data/lib/aspera/cli/command_registry.rb +1 -1
  28. data/lib/aspera/cli/command_spec.rb +6 -3
  29. data/lib/aspera/cli/context.rb +3 -3
  30. data/lib/aspera/cli/extended_value.rb +26 -26
  31. data/lib/aspera/cli/formatter.rb +76 -54
  32. data/lib/aspera/cli/gem_checker.rb +1 -1
  33. data/lib/aspera/cli/hints.rb +2 -2
  34. data/lib/aspera/cli/http.rb +22 -29
  35. data/lib/aspera/cli/mailer.rb +7 -7
  36. data/lib/aspera/cli/mcp_tool.rb +31 -4
  37. data/lib/aspera/cli/option_declarator.rb +77 -0
  38. data/lib/aspera/cli/options.schema.yaml +273 -3
  39. data/lib/aspera/cli/parser.rb +539 -280
  40. data/lib/aspera/cli/plugins/alee.rb +1 -1
  41. data/lib/aspera/cli/plugins/aoc.rb +303 -314
  42. data/lib/aspera/cli/plugins/ats.rb +25 -21
  43. data/lib/aspera/cli/plugins/base.rb +357 -186
  44. data/lib/aspera/cli/plugins/basic_auth.rb +0 -12
  45. data/lib/aspera/cli/plugins/config.rb +76 -73
  46. data/lib/aspera/cli/plugins/console.rb +8 -8
  47. data/lib/aspera/cli/plugins/cos.rb +4 -3
  48. data/lib/aspera/cli/plugins/factory.rb +2 -2
  49. data/lib/aspera/cli/plugins/faspex5.rb +259 -248
  50. data/lib/aspera/cli/plugins/faspio.rb +7 -2
  51. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  52. data/lib/aspera/cli/plugins/mcp.rb +11 -11
  53. data/lib/aspera/cli/plugins/node.rb +227 -283
  54. data/lib/aspera/cli/plugins/orchestrator.rb +23 -23
  55. data/lib/aspera/cli/plugins/preview.rb +61 -64
  56. data/lib/aspera/cli/plugins/server.rb +36 -31
  57. data/lib/aspera/cli/plugins/shares.rb +174 -135
  58. data/lib/aspera/cli/preset_actions.rb +39 -19
  59. data/lib/aspera/cli/preset_manager.rb +74 -32
  60. data/lib/aspera/cli/result.rb +18 -16
  61. data/lib/aspera/cli/runner.rb +75 -39
  62. data/lib/aspera/cli/secret_finder.rb +1 -1
  63. data/lib/aspera/cli/sync_actions.rb +36 -35
  64. data/lib/aspera/cli/terminal_formatter.rb +2 -2
  65. data/lib/aspera/cli/transfer_actions.rb +17 -10
  66. data/lib/aspera/cli/transfer_agent.rb +44 -44
  67. data/lib/aspera/cli/transfer_progress.rb +7 -7
  68. data/lib/aspera/cli/vault_manager.rb +34 -12
  69. data/lib/aspera/cli/version.rb +1 -1
  70. data/lib/aspera/cli/wizard.rb +17 -20
  71. data/lib/aspera/colors.rb +3 -3
  72. data/lib/aspera/command_line_builder.rb +17 -16
  73. data/lib/aspera/command_line_converter.rb +1 -1
  74. data/lib/aspera/coverage.rb +1 -2
  75. data/lib/aspera/data_repository.rb +1 -1
  76. data/lib/aspera/dot_container.rb +8 -8
  77. data/lib/aspera/environment.rb +13 -13
  78. data/lib/aspera/faspex_gw.rb +4 -4
  79. data/lib/aspera/faspex_postproc.rb +14 -9
  80. data/lib/aspera/graphql.rb +4 -2
  81. data/lib/aspera/hash_ext.rb +2 -2
  82. data/lib/aspera/json_rpc/client.rb +1 -1
  83. data/lib/aspera/keychain/base.rb +10 -3
  84. data/lib/aspera/keychain/encrypted_hash.rb +92 -17
  85. data/lib/aspera/keychain/factory.rb +17 -5
  86. data/lib/aspera/keychain/hashicorp_vault.rb +1 -1
  87. data/lib/aspera/keychain/macos_security.rb +23 -25
  88. data/lib/aspera/keychain/one_password_api.rb +86 -0
  89. data/lib/aspera/keychain/one_password_base.rb +34 -0
  90. data/lib/aspera/keychain/one_password_cli.rb +98 -0
  91. data/lib/aspera/link_header.rb +2 -2
  92. data/lib/aspera/log.rb +13 -13
  93. data/lib/aspera/markdown.rb +5 -5
  94. data/lib/aspera/nagios.rb +8 -8
  95. data/lib/aspera/node_simulator.rb +33 -27
  96. data/lib/aspera/oauth/base.rb +4 -5
  97. data/lib/aspera/oauth/boot.rb +3 -3
  98. data/lib/aspera/oauth/factory.rb +5 -5
  99. data/lib/aspera/oauth/jwt.rb +5 -7
  100. data/lib/aspera/oauth/web.rb +4 -4
  101. data/lib/aspera/persistency_action_once.rb +4 -4
  102. data/lib/aspera/persistency_folder.rb +8 -8
  103. data/lib/aspera/preview/file_types.rb +4 -4
  104. data/lib/aspera/preview/generator.rb +49 -46
  105. data/lib/aspera/preview/options.rb +3 -2
  106. data/lib/aspera/preview/terminal.rb +5 -5
  107. data/lib/aspera/preview/utils.rb +65 -18
  108. data/lib/aspera/products/connect.rb +2 -2
  109. data/lib/aspera/products/desktop.rb +1 -1
  110. data/lib/aspera/products/other.rb +2 -2
  111. data/lib/aspera/products/transferd.rb +3 -3
  112. data/lib/aspera/proxy_auto_config.rb +10 -10
  113. data/lib/aspera/rest.rb +149 -115
  114. data/lib/aspera/rest_error_analyzer.rb +3 -3
  115. data/lib/aspera/rest_list.rb +10 -9
  116. data/lib/aspera/schema/IBM Aspera Node API-4.4.6.yaml +6232 -0
  117. data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +249 -0
  118. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +39 -0
  119. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
  120. data/lib/aspera/schema/documentation.rb +17 -11
  121. data/lib/aspera/schema/reader.rb +78 -4
  122. data/lib/aspera/schema/registry.rb +26 -7
  123. data/lib/aspera/secret_hider.rb +5 -5
  124. data/lib/aspera/ssh.rb +64 -31
  125. data/lib/aspera/ssl.rb +3 -3
  126. data/lib/aspera/sync/database.rb +2 -2
  127. data/lib/aspera/sync/operations.rb +13 -13
  128. data/lib/aspera/temp_file_manager.rb +24 -4
  129. data/lib/aspera/transfer/faux_file.rb +2 -2
  130. data/lib/aspera/transfer/parameters.rb +15 -15
  131. data/lib/aspera/transfer/resumer.rb +9 -9
  132. data/lib/aspera/transfer/spec.rb +1 -1
  133. data/lib/aspera/transfer/spec.schema.yaml +1 -0
  134. data/lib/aspera/uri_reader.rb +52 -15
  135. data/lib/aspera/web_auth.rb +6 -5
  136. data/lib/aspera/web_server_simple.rb +9 -9
  137. data/lib/aspera/yaml.rb +4 -4
  138. data.tar.gz.sig +0 -0
  139. metadata +10 -4
  140. metadata.gz.sig +0 -0
  141. data/lib/aspera/cli/plugins/faspex.rb +0 -576
  142. data/lib/aspera/transfer/uri.rb +0 -56
@@ -2,6 +2,7 @@
2
2
 
3
3
  # spellchecker: ignore workgroups mypackages passcode
4
4
 
5
+ require 'aspera/schema/registry'
5
6
  require 'aspera/cli/plugins/oauth'
6
7
  require 'aspera/cli/extended_value'
7
8
  require 'aspera/transfer/result'
@@ -46,7 +47,7 @@ module Aspera
46
47
  }
47
48
  rescue StandardError => e
48
49
  error = e
49
- Log.log.debug{"detect error: #{e}"}
50
+ Log.log.debug { "detect error: #{e}" }
50
51
  end
51
52
  raise error if error
52
53
  return
@@ -80,14 +81,12 @@ module Aspera
80
81
  )
81
82
  return {
82
83
  preset_value: {
83
- url: app_url,
84
- username: wiz_username,
85
- auth: :jwt.to_s,
86
- private_key: "@file:#{private_key_path}",
87
- params: {
88
- client_id: client_id,
89
- client_secret: client_secret
90
- }
84
+ url: app_url,
85
+ username: wiz_username,
86
+ auth: :jwt.to_s,
87
+ private_key: "@file:#{private_key_path}",
88
+ client_id: client_id,
89
+ client_secret: client_secret
91
90
  },
92
91
  test_args: 'user profile show'
93
92
  }
@@ -108,7 +107,7 @@ module Aspera
108
107
  def normalize_recipients(parameters, type)
109
108
  type = type.to_s
110
109
  return unless parameters.key?(type)
111
- Aspera.assert_type(parameters[type], Array){type}
110
+ Aspera.assert_type(parameters[type], Array) { type }
112
111
  recipient_types = Api::Faspex::RECIPIENT_TYPES
113
112
  if parameters.key?('recipient_types')
114
113
  recipient_types = parameters['recipient_types']
@@ -169,9 +168,15 @@ module Aspera
169
168
  return result
170
169
  end
171
170
 
172
- # list all packages with optional filter
173
- def list_packages_with_filter(query: {})
174
- filter = options.get_next_argument('filter', mandatory: false, validation: Proc, default: ->(_x){true})
171
+ # List all packages with optional filter.
172
+ # The special `max` key is extracted *before* the API call and returned separately,
173
+ # so callers that apply a post-API filter (e.g. once_only) can enforce the limit
174
+ # after filtering rather than before.
175
+ # @param filter [Proc, nil] optional filter lambda applied to each package entry
176
+ # @param query [Hash] additional query parameters forwarded to the API
177
+ # @return [Array(Array, Integer|nil, Integer|nil)] [filtered list, max (or nil), total count]
178
+ def list_packages_with_filter(filter: nil, query: {})
179
+ filter ||= ->(_x) { true }
175
180
  box = options.get_option(:box)
176
181
  # Translate box name to API prefix (with ending slash)
177
182
  entity =
@@ -182,8 +187,13 @@ module Aspera
182
187
  group_type = options.get_option(:group_type)
183
188
  "#{group_type}/#{@api_v5.lookup_entity_by_field(entity: group_type, value: box)['id']}/packages"
184
189
  end
185
- list, total = @api_v5.list_entities_limit_offset_total_count(entity: entity, query: query_read_delete(default: query))
186
- return list.select(&filter), total
190
+ # Merge default query with user-provided query: user values take precedence, but defaults are preserved
191
+ user_query = query_read_delete(schema: Schema::Registry.query_params(Schema::Registry::FASPEX, 'packages'))
192
+ merged_query = user_query.nil? ? query.dup : query.merge(user_query)
193
+ # 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)
196
+ return list.select(&filter), max_items, total
187
197
  end
188
198
 
189
199
  # Build query to get package recipients based on package info in case of shared inbox or workgroup recipient
@@ -216,22 +226,24 @@ module Aspera
216
226
  case package_ids
217
227
  when SpecialValues::INIT
218
228
  Aspera.assert(skip_ids_persistency, 'Only with option once_only')
219
- skip_ids_persistency.data.clear.concat(list_packages_with_filter.first.map{ |p| p['id']})
229
+ skip_ids_persistency.data.clear.concat(list_packages_with_filter.first.map { |p| p['id'] }) # no filter: all packages, max ignored
220
230
  skip_ids_persistency.save
221
231
  return Result::Status.new("Initialized skip for #{skip_ids_persistency.data.count} package(s)")
222
232
  when SpecialValues::ALL
223
233
  # TODO: if packages have same name, they will overwrite ?
224
- packages = list_packages_with_filter(query: {'status' => 'completed'}).first
225
- Log.dump(:package_ids, level: :trace1){packages.map{ |p| p['id']}}
234
+ packages, max_items = list_packages_with_filter(query: {'status' => 'completed'}) # no filter: all completed packages
235
+ Log.dump(:package_ids, level: :trace1) { packages.map { |p| p['id'] } }
226
236
  Log.dump(:skip_ids, skip_ids_persistency.data, level: :trace1)
227
- packages.reject!{ |p| skip_ids_persistency.data.include?(p['id'])} if skip_ids_persistency
228
- Log.dump(:package_ids, level: :trace1){packages.map{ |p| p['id']}}
237
+ packages.reject! { |p| skip_ids_persistency.data.include?(p['id']) } if skip_ids_persistency
238
+ # Apply `max` after once_only filtering so we get the N first not-yet-downloaded packages
239
+ packages = packages[0, max_items] if max_items
240
+ Log.dump(:package_ids, level: :trace1) { packages.map { |p| p['id'] } }
229
241
  else
230
242
  # a single id was provided, or a list of ids
231
243
  package_ids = [package_ids] unless package_ids.is_a?(Array)
232
- Aspera.assert_array_all(package_ids, String){'Package id(s)'}
244
+ Aspera.assert_array_all(package_ids, String) { 'Package id(s)' }
233
245
  # packages = package_ids.map{|pkg_id|@api_v5.read("packages/#{pkg_id}")}
234
- packages = package_ids.map{ |pkg_id| {'id'=>pkg_id}}
246
+ packages = package_ids.map { |pkg_id| {'id'=>pkg_id} }
235
247
  end
236
248
  result_transfer = []
237
249
  param_file_list = {}
@@ -279,7 +291,7 @@ module Aspera
279
291
  recipient_type: @api_v5.pub_link_context['recipient_type']
280
292
  }]
281
293
  end
282
- PACKAGE_RECIPIENT_TYPES.each{ |type| normalize_recipients(parameters, type)}
294
+ PACKAGE_RECIPIENT_TYPES.each { |type| normalize_recipients(parameters, type) }
283
295
  # User specified content prot in tspec, but faspex requires in package creation
284
296
  # `transfer_spec/upload` will set `content_protection`
285
297
  if transfer.user_transfer_spec['content_protection'] && !parameters.key?('ear_enabled')
@@ -325,13 +337,13 @@ module Aspera
325
337
  def browse_folder(browse_endpoint, base_query = {}, folder_path: '/')
326
338
  folders_to_process = [folder_path]
327
339
  query = base_query.merge(query_read_delete(default: {}))
328
- filters = query.delete('filters'){{}}
340
+ filters = query.delete('filters') { {} }
329
341
  Aspera.assert_type(filters, Hash)
330
342
  filters['basenames'] ||= []
331
- Aspera.assert_type(filters, Hash){'filters'}
343
+ Aspera.assert_type(filters, Hash) { 'filters' }
332
344
  max_items = query.delete(RestList::MAX_ITEMS)
333
345
  recursive = query.delete('recursive')
334
- use_paging = query.delete('paging'){true}
346
+ use_paging = query.delete('paging') { true }
335
347
  if use_paging
336
348
  browse_endpoint = "#{browse_endpoint}/page"
337
349
  query['per_page'] ||= 500
@@ -358,7 +370,7 @@ module Aspera
358
370
  all_items = all_items.slice(0, max_items) if all_items.count > max_items
359
371
  break
360
372
  end
361
- folders_to_process.concat(data['items'].select{ |i| i['type'].eql?('directory')}.map{ |i| i['path']}) if recursive
373
+ folders_to_process.concat(data['items'].select { |i| i['type'].eql?('directory') }.map { |i| i['path'] }) if recursive
362
374
  if use_paging
363
375
  iteration_token = http[Api::Faspex::HEADER_X_NEXT_ITER_TOKEN]
364
376
  break if iteration_token.nil? || iteration_token.empty?
@@ -377,26 +389,27 @@ module Aspera
377
389
  end
378
390
 
379
391
  # Per-resource configuration for admin CRUD sub-trees.
380
- # Keys mirror entity_execute kwargs; extra_commands lists additional leaf commands.
392
+ # Keys mirror entity_list/show/create/modify/delete kwargs; extra_commands lists additional leaf commands.
381
393
  # @return [Hash{Symbol => Hash}]
382
394
  RESOURCE_CONFIG = {
383
395
  accounts: {
384
- display_fields: ->{Formatter.all_but('user_profile_data_attributes')},
396
+ display_fields: -> { Formatter.all_but('user_profile_data_attributes') },
385
397
  extra_commands: [:reset_password],
386
- instance_arg_commands: {reset_password: {arguments: [{name: :contact_id, type: :identifier, lookup: :lookup_accounts_id}]}},
387
- query_component: Schema::Registry::FASPEX
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
388
401
  },
389
- alternate_addresses: {entity: 'configuration/alternate_addresses', query_component: Schema::Registry::FASPEX},
402
+ alternate_addresses: {entity: 'configuration/alternate_addresses', query_component: Schema::Registry::FASPEX, body_component: Schema::Registry::FASPEX},
390
403
  contacts: {query_component: Schema::Registry::FASPEX},
391
- distribution_lists: {entity: 'account/distribution_lists', delete_style: 'ids', query_component: Schema::Registry::FASPEX},
404
+ distribution_lists: {entity: 'account/distribution_lists', delete_style: 'ids', query_component: Schema::Registry::FASPEX, body_component: Schema::Registry::FASPEX},
392
405
  email_notifications: {id_as_arg: 'type', query_component: Schema::Registry::FASPEX},
393
406
  file_processing: {
394
- commands: %i[next modify],
395
- schema: ->{Schema::Registry.req_body(Schema::Registry::FASPEX, 'file_processing.put')},
396
- is_singleton: true
407
+ commands: %i[next modify],
408
+ body_component: Schema::Registry::FASPEX,
409
+ is_singleton: true
397
410
  },
398
411
  jobs: {display_fields: %w[id job_name job_type status], query_component: Schema::Registry::FASPEX},
399
- metadata_profiles: {entity: 'configuration/metadata_profiles', items_key: 'profiles', query_component: Schema::Registry::FASPEX},
412
+ metadata_profiles: {entity: 'configuration/metadata_profiles', items_key: 'profiles', query_component: Schema::Registry::FASPEX, body_component: Schema::Registry::FASPEX},
400
413
  nodes: {
401
414
  extra_commands: %i[browse],
402
415
  instance_arg_commands: {
@@ -405,19 +418,21 @@ module Aspera
405
418
  {name: :folder_path, type: String, mandatory: false, default: '/'}]
406
419
  }
407
420
  },
408
- query_component: Schema::Registry::FASPEX
421
+ query_component: Schema::Registry::FASPEX,
422
+ body_component: Schema::Registry::FASPEX
409
423
  },
410
424
  oauth_clients: {
411
- display_fields: ->{Formatter.all_but('public_key')},
412
- api: ->{Api::Faspex.new(root: Api::Faspex::PATH_AUTH, **Oauth.kwargs_from_options(options))},
425
+ display_fields: -> { Formatter.all_but('public_key') },
426
+ api: -> { Api::Faspex.new(root: Api::Faspex::PATH_AUTH, **Oauth.kwargs_from_options(options)) },
413
427
  list_query: {'expand': true, 'no_api_path': true, 'client_types[]': 'public'},
414
- query_component: Schema::Registry::FASPEX
428
+ query_component: Schema::Registry::FASPEX,
429
+ body_component: Schema::Registry::FASPEX
415
430
  },
416
431
  registrations: {query_component: Schema::Registry::FASPEX},
417
- saml_configs: {query_component: Schema::Registry::FASPEX},
418
- shared_inboxes: {res_id_query: {'all': true}, query_component: Schema::Registry::FASPEX},
419
- webhooks: {query_component: Schema::Registry::FASPEX},
420
- workgroups: {res_id_query: {'all': true}, query_component: Schema::Registry::FASPEX}
432
+ saml_configs: {query_component: Schema::Registry::FASPEX, body_component: Schema::Registry::FASPEX},
433
+ shared_inboxes: {res_id_query: {'all': true}, query_component: Schema::Registry::FASPEX, body_component: Schema::Registry::FASPEX},
434
+ webhooks: {query_component: Schema::Registry::FASPEX, body_component: Schema::Registry::FASPEX},
435
+ workgroups: {res_id_query: {'all': true}, query_component: Schema::Registry::FASPEX, body_component: Schema::Registry::FASPEX}
421
436
  }.freeze
422
437
  private_constant :RESOURCE_CONFIG
423
438
 
@@ -427,7 +442,7 @@ module Aspera
427
442
  v.is_a?(Proc) ? instance_exec(&v) : v
428
443
  end
429
444
 
430
- # Build exec_args hash for entity_execute from RESOURCE_CONFIG for the given resource.
445
+ # Build args hash from RESOURCE_CONFIG for use with entity_list/show/create/modify/delete.
431
446
  def res_exec_args(res_sym)
432
447
  cfg = RESOURCE_CONFIG.fetch(res_sym, {})
433
448
  {
@@ -439,33 +454,16 @@ module Aspera
439
454
  display_fields: resource_config_value(cfg, :display_fields),
440
455
  list_query: resource_config_value(cfg, :list_query),
441
456
  is_singleton: resource_config_value(cfg, :is_singleton) || false,
442
- schema: resource_config_value(cfg, :schema),
443
- query_component: resource_config_value(cfg, :query_component)
457
+ query_component: resource_config_value(cfg, :query_component),
458
+ body_component: resource_config_value(cfg, :body_component)
444
459
  }.compact
445
460
  end
446
461
 
447
- # Lookup methods for arguments: + lookup: on specific nodes
448
-
449
462
  # admin > nodes — lookup node id by field/value
450
463
  def lookup_node_id(field, value, **)
451
464
  @api_v5.lookup_entity_by_field(entity: 'nodes', field: field, value: value)['id']
452
465
  end
453
466
 
454
- # admin > shared_inboxes — lookup id by field/value
455
- def lookup_shared_inboxes_id(field, value, **)
456
- @api_v5.lookup_entity_by_field(entity: 'shared_inboxes', field: field, value: value, query: {'all': true})['id']
457
- end
458
-
459
- # admin > workgroups — lookup id by field/value
460
- def lookup_workgroups_id(field, value, **)
461
- @api_v5.lookup_entity_by_field(entity: 'workgroups', field: field, value: value, query: {'all': true})['id']
462
- end
463
-
464
- # admin > accounts — lookup id by field/value (used for reset_password arguments:)
465
- def lookup_accounts_id(field, value, **)
466
- res_lookup_id(:accounts, field, value)
467
- end
468
-
469
467
  # Lookup id for a RESOURCE_CONFIG resource by field/value.
470
468
  def res_lookup_id(res_sym, field, value)
471
469
  cfg = RESOURCE_CONFIG.fetch(res_sym, {})
@@ -475,32 +473,38 @@ module Aspera
475
473
  @api_v5.lookup_entity_by_field(entity: entity, value: value, field: field, items_key: items_key, query: res_id_query)['id']
476
474
  end
477
475
 
476
+ # Dynamically define lookup methods for admin resources (used by crud_commands)
477
+ Api::Faspex::ADMIN_RESOURCES.each do |res|
478
+ define_method(:"lookup_admin_#{res}_id") do |field, value, **|
479
+ res_lookup_id(res, field, value)
480
+ end
481
+ end
482
+
478
483
  # --- DSL ---
479
484
 
480
485
  # Commands that need @api_v5 carry setup: :setup_api_v5.
481
486
  # :health and :postprocessing work without authentication, so they have no setup.
482
487
  command :health, description: 'Check Faspex 5 health'
483
- command :version, description: 'Show Faspex 5 version', setup: :setup_api_v5, action: ->{Result::SingleObject.new(@api_v5.read('version'))}
484
- command :bearer_token, description: 'Show OAuth bearer token', setup: :setup_api_v5, action: ->{Result::Text.new(@api_v5.oauth.authorization)}
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) }
485
490
  command :packages, description: 'Manage packages', setup: :setup_api_v5
486
- commands_under(:packages) do
487
- command :list, description: 'List packages'
491
+ commands_under :packages do
492
+ command :list, description: 'List packages',
493
+ arguments: [{name: :filter, mandatory: false, default: nil, type: Proc}]
488
494
  command :send, description: 'Send a package', transfer_paths: :send,
489
495
  arguments: [{name: :data, type: Hash, schema: Schema::Registry.req_body(Schema::Registry::FASPEX, 'packages.post')}],
490
- action: ->(data:, **){package_send(data)}
496
+ action: ->(data:, **) { package_send(data) }
491
497
  command :show, description: 'Show a package', setup: :setup_package_id,
492
- action: ->(package_id:, **){Result::SingleObject.new(@api_v5.read("packages/#{package_id}"))}
498
+ action: ->(package_id:, **) { Result::SingleObject.new(@api_v5.read("packages/#{package_id}")) }
493
499
  command :browse, description: 'Browse package files', setup: :setup_package_id,
494
500
  arguments: [{name: :folder_path, type: String, mandatory: false, default: '/'}],
495
- 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)}
496
- command(
497
- :status, description: 'Wait for package status', setup: :setup_package_id,
501
+ 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
+ command :status, description: 'Wait for package status', setup: :setup_package_id,
498
503
  arguments: [{name: :status_list, type: Array, mandatory: false, default: nil}],
499
- action: ->(status_list:, package_id:, **){Result::SingleObject.new(wait_package_status(package_id, status_list: status_list))}
500
- )
501
- command :delete, description: 'Delete package(s)', setup: :setup_package_id
504
+ 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
502
506
  command :receive, description: 'Receive a package', setup: :setup_package_id, transfer_paths: :receive,
503
- action: ->(package_id:, **){package_receive(package_id)}
507
+ action: ->(package_id:, **) { package_receive(package_id) }
504
508
  command(:file_processing, description: 'Show file processing status', setup: :setup_package_id, action: lambda do |package_id:, **|
505
509
  result, count = @api_v5.list_entities_limit_offset_total_count(entity: "packages/#{package_id}/file_statuses", items_key: 'files')
506
510
  Result::ObjectList.new(result, total: count)
@@ -515,25 +519,28 @@ module Aspera
515
519
  arguments: [{name: :parameters, type: Hash, mandatory: false, default: {}}]
516
520
  command :invitations, description: 'Manage invitations', setup: :setup_api_v5
517
521
 
518
- commands_under(:invitations) do
522
+ commands_under :invitations do
519
523
  command :create, description: 'Create an invitation',
520
524
  arguments: [{name: :input_data, type: Hash, bulk: true}]
521
525
  command :resend, description: 'Resend an invitation',
522
526
  arguments: [{name: :invitation_id, type: :identifier}]
523
- Operations::ALL.reject{ |op| op == :create}.each do |op|
524
- command(op, description: "#{op.capitalize} invitation(s)")
525
- end
527
+ crud_commands entity: 'invitations',
528
+ api: :@api_v5,
529
+ operations: Operations::ALL - %i[create],
530
+ items_key: 'invitations',
531
+ display_fields: %w[id public recipient_type recipient_name email_address]
526
532
  end
527
533
 
528
- commands_under(:user) do
529
- command :account, description: 'Show account information', action: ->{Result::SingleObject.new(@api_v5.read('account', query_read_delete))}
534
+ commands_under :user do
535
+ command :account, description: 'Show account information', action: -> { Result::SingleObject.new(@api_v5.read('account', query_read_delete)) }
530
536
  command :profile, description: 'Manage user profile'
531
537
  end
532
538
 
533
- commands_under(%i[user profile]) do
534
- command :show, description: 'Show user profile', action: ->{Result::SingleObject.new(@api_v5.read('account/preferences'))}
539
+ commands_under %i[user profile] do
540
+ command :show, description: 'Show user profile', action: -> { Result::SingleObject.new(@api_v5.read('account/preferences')) }
535
541
  command(
536
- :modify, description: 'Modify user profile',
542
+ :modify,
543
+ description: 'Modify user profile',
537
544
  arguments: [{name: :properties, type: Hash}],
538
545
  action: lambda do |properties:, **|
539
546
  @api_v5.update('account/preferences', properties)
@@ -542,15 +549,15 @@ module Aspera
542
549
  )
543
550
  end
544
551
 
545
- commands_under(:shared_folders) do
546
- command :list, description: 'List shared folders', action: ->{Result::ObjectList.new(@api_v5.read('shared_folders')['shared_folders'])}
552
+ commands_under :shared_folders do
553
+ command :list, description: 'List shared folders', action: -> { Result::ObjectList.new(@api_v5.read('shared_folders')['shared_folders']) }
547
554
  command :browse, description: 'Browse a shared folder',
548
555
  arguments: [{name: :shared_folder_id, type: :identifier, lookup: :lookup_shared_folder_id},
549
556
  {name: :folder_path, type: String, mandatory: false, default: '/'}]
550
557
  end
551
558
 
552
559
  # admin sub-tree: fixed commands + all ADMIN_RESOURCES with their sub-commands
553
- commands_under(:admin) do
560
+ commands_under :admin do
554
561
  command :configuration, description: 'Manage Faspex 5 configuration'
555
562
  command :smtp, description: 'Manage SMTP configuration'
556
563
  command :events, description: 'List events'
@@ -559,127 +566,155 @@ module Aspera
559
566
  Api::Faspex::ADMIN_RESOURCES.each do |res|
560
567
  cfg = RESOURCE_CONFIG.fetch(res, {})
561
568
  extra = cfg[:extra_commands] || []
562
- cmds = cfg[:commands] || (Operations::ALL + extra)
563
569
  ia_cmds = cfg[:instance_arg_commands] || {}
564
570
  is_singleton = cfg[:is_singleton] || false
565
- schema_val = cfg[:schema]
566
- command(res, description: "Manage #{res.to_s.tr('_', ' ')}")
567
- commands_under([:admin, res]) do
568
- cmds.each do |c|
571
+ entity_path = cfg[:entity] || res.to_s
572
+ crud_ops = ((cfg[:commands] || Operations::ALL) - %i[list]) & Operations::ALL
573
+
574
+ command res, description: "Manage #{res.to_s.tr('_', ' ')}"
575
+ commands_under(res) do
576
+ # List is handled with Faspex 5 pagination (item_list_with_total)
577
+ unless is_singleton
578
+ list_kwargs = {description: 'List'}
579
+ list_kwargs[:query_schema] = Schema::Registry.query_params(cfg[:query_component], entity_path) if cfg[:query_component]
580
+ command :list, **list_kwargs
581
+ end
582
+
583
+ # Standard CRUD operations handled via crud_commands
584
+ crud_commands(
585
+ api: -> { res_exec_args(res)[:api] },
586
+ entity: entity_path,
587
+ operations: crud_ops,
588
+ lookup: :"lookup_admin_#{res}_id",
589
+ is_singleton: is_singleton,
590
+ body_component: cfg[:body_component],
591
+ display_fields: cfg[:display_fields],
592
+ delete_style: cfg[:delete_style],
593
+ id_as_arg: cfg[:id_as_arg] || false
594
+ )
595
+
596
+ # Extra commands (e.g. browse, reset_password, next)
597
+ extra.each do |c|
569
598
  ia = ia_cmds[c] || {}
570
- extra_args =
571
- if !is_singleton && c.eql?(:create)
572
- [{name: :input_data, type: Hash, bulk: true, schema: schema_val}]
573
- elsif c.eql?(:modify)
574
- [{name: :input_data, type: Hash, schema: schema_val}]
575
- else
576
- []
577
- end
578
- # Merge arguments: from ia (e.g. identifier spec) with extra_args for this operation
579
- merged_args = Array(ia[:arguments]) + extra_args
580
- spec_kwargs = merged_args.empty? ? ia.except(:arguments) : ia.except(:arguments).merge(arguments: merged_args)
581
- # Attach query_schema (full path) to :list/:delete CommandSpec so --help shows the tip.
582
- # cfg[:query_component] is always a String constant so direct read is safe here (no Proc).
583
- if QUERY_SCHEMA_COMMANDS.include?(c) && cfg[:query_component]
584
- entity_path = cfg[:entity] || res.to_s
585
- spec_kwargs = spec_kwargs.merge(
586
- query_schema: Schema::Registry.query_params(cfg[:query_component], entity_path)
587
- )
588
- end
589
- command(c, description: c.to_s.tr('_', ' ').capitalize, **spec_kwargs)
599
+ command c, description: c.to_s.tr('_', ' ').capitalize, **ia
590
600
  end
591
601
  end
592
602
  end
593
603
  end
594
604
 
595
- commands_under(%i[admin nodes]) do
605
+ commands_under %i[admin nodes] do
596
606
  command :shared_folders, description: 'Manage shared folders',
597
607
  arguments: [{name: :node_id, type: :identifier, lookup: :lookup_node_id}],
598
608
  setup: :setup_admin_nodes_shared_folders
599
609
  end
600
610
 
601
611
  # admin > nodes > shared_folders sub-tree
602
- commands_under(%i[admin nodes shared_folders]) do
603
- Operations::ALL.each{ |c| command(c, description: c.to_s.tr('_', ' ').capitalize)}
612
+ # entity: :sf_entity is resolved at runtime from ctx (injected by setup_admin_nodes_shared_folders).
613
+ commands_under %i[admin nodes shared_folders] do
614
+ crud_commands entity: :sf_entity,
615
+ api: :@api_v5,
616
+ name: 'shared folder',
617
+ lookup: :lookup_sf_id,
618
+ items_key: 'shared_folders'
604
619
  command :user, description: 'Custom access users',
605
620
  arguments: [{name: :sf_id, type: :identifier, lookup: :lookup_sf_id}],
606
621
  setup: :setup_admin_nodes_shared_folders_user
607
622
  end
608
623
 
609
624
  # admin > nodes > shared_folders > user sub-tree (custom access users)
610
- commands_under(%i[admin nodes shared_folders user]) do
611
- Operations::ALL.each{ |c| command(c, description: c.to_s.tr('_', ' ').capitalize)}
625
+ # entity: :user_path is resolved at runtime from ctx (injected by setup_admin_nodes_shared_folders_user).
626
+ 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'
612
628
  end
613
629
 
614
- MEMBER_SUBS = %i[members saml_groups].freeze
630
+ # `members` and `saml_groups`
615
631
  CRUD_NO_SHOW = %i[create list modify delete].freeze
616
632
  CRUD_NO_LIST = %i[create modify delete show].freeze
617
633
 
634
+ RES_SINGULAR = {shared_inboxes: :shared_inbox, workgroups: :workgroup}.freeze
635
+ private_constant :RES_SINGULAR
636
+
618
637
  # admin > shared_inboxes|workgroups > members|saml_groups|invite_external_collaborator:
619
638
  # res_id consumed via arguments:(:identifier) + lookup:, builds res_instance_path for all children
620
639
  %i[shared_inboxes workgroups].each do |res|
621
- MEMBER_SUBS.each do |sub|
622
- commands_under([:admin, res]) do
623
- command sub, description: sub.to_s.tr('_', ' ').capitalize,
624
- arguments: [{name: :res_id, type: :identifier, lookup: :"lookup_#{res}_id"}],
625
- setup: :"setup_admin_#{res}_instance"
626
- end
627
- commands_under([:admin, res, sub]) do
628
- CRUD_NO_SHOW.each do |c|
629
- args = if c.eql?(:create) && sub.eql?(:members)
640
+ lookup_res_id = ->(field, value, **) { @api_v5.lookup_entity_by_field(entity: res.to_s, field: field, value: value, query: {'all': true})['id'] }
641
+ commands_under [:admin, res] do
642
+ command :members, description: 'Members',
643
+ arguments: [{name: :"#{RES_SINGULAR[res]}_id", type: :identifier, lookup: lookup_res_id}],
644
+ setup: :"setup_admin_#{res}_instance"
645
+ command :saml_groups, description: 'Saml groups',
646
+ arguments: [{name: :"#{RES_SINGULAR[res]}_id", type: :identifier, lookup: lookup_res_id}],
647
+ setup: :"setup_admin_#{res}_instance"
648
+ command :invite_external_collaborator, description: 'Invite external collaborator',
649
+ arguments: [{name: :"#{RES_SINGULAR[res]}_id", type: :identifier, lookup: lookup_res_id},
650
+ {name: :input_data, type: Hash}]
651
+ end
652
+
653
+ commands_under [:admin, res, :members] do
654
+ CRUD_NO_SHOW.each do |c|
655
+ args =
656
+ if c.eql?(:create)
630
657
  {arguments: [{name: :users, bulk: true}, {name: :access, mandatory: false, default: :standard}]}
658
+ elsif Operations::INSTANCE.include?(c)
659
+ {arguments: [{name: :member_id, type: :identifier, lookup: :"lookup_#{res}_members_id"}]}
631
660
  else
632
661
  {}
633
662
  end
634
- command(c, description: c.to_s.capitalize, **args)
635
- end
663
+ command c, description: c.to_s.capitalize, **args
636
664
  end
637
665
  end
638
- commands_under([:admin, res]) do
639
- command :invite_external_collaborator, description: 'Invite external collaborator',
640
- arguments: [{name: :res_id, type: :identifier, lookup: :"lookup_#{res}_id"},
641
- {name: :input_data, type: Hash}]
666
+
667
+ commands_under [:admin, res, :saml_groups] do
668
+ crud_commands entity: :saml_groups_path,
669
+ api: :@api_v5,
670
+ name: 'SAML group',
671
+ operations: CRUD_NO_SHOW,
672
+ lookup: :"lookup_#{res}_saml_groups_id",
673
+ items_key: 'groups'
642
674
  end
643
675
  end
644
676
 
645
- commands_under(%i[admin configuration]) do
646
- command :show, description: 'Show configuration', action: ->{Result::SingleObject.new(@api_v5.read('configuration'))}
647
- command(
648
- :modify, description: 'Modify configuration',
677
+ commands_under %i[admin configuration] do
678
+ command :show, description: 'Show configuration', action: -> { Result::SingleObject.new(@api_v5.read('configuration')) }
679
+ command :modify, description: 'Modify configuration',
649
680
  arguments: [{name: :input_data, type: Hash}],
650
- action: ->(input_data:, **){Result::SingleObject.new(@api_v5.update('configuration', input_data))}
651
- )
681
+ action: ->(input_data:, **) { Result::SingleObject.new(@api_v5.update('configuration', input_data)) }
652
682
  end
653
683
 
654
- commands_under(%i[admin smtp]) do
655
- command :show, description: 'Show SMTP configuration', action: ->{Result::SingleObject.new(@api_v5.read('configuration/smtp'))}
656
- command(
657
- :create, description: 'Create SMTP configuration',
684
+ commands_under %i[admin smtp] do
685
+ command :show, description: 'Show SMTP configuration', action: -> { Result::SingleObject.new(@api_v5.read('configuration/smtp')) }
686
+ command :create, description: 'Create SMTP configuration',
658
687
  arguments: [{name: :input_data, type: Hash}],
659
- action: ->(input_data:, **){Result::SingleObject.new(@api_v5.create('configuration/smtp', input_data))}
660
- )
661
- command(
662
- :modify, description: 'Modify SMTP configuration',
688
+ action: ->(input_data:, **) { Result::SingleObject.new(@api_v5.create('configuration/smtp', input_data)) }
689
+ command :modify, description: 'Modify SMTP configuration',
663
690
  arguments: [{name: :input_data, type: Hash}],
664
- action: ->(input_data:, **){Result::SingleObject.new(@api_v5.update('configuration/smtp', input_data))}
691
+ action: ->(input_data:, **) { Result::SingleObject.new(@api_v5.update('configuration/smtp', input_data)) }
692
+ command(
693
+ :delete, description: 'Delete SMTP configuration',
694
+ action: lambda do
695
+ @api_v5.delete('configuration/smtp')
696
+ Result::Status.new('SMTP configuration deleted')
697
+ end
665
698
  )
666
- command(:delete, description: 'Delete SMTP configuration', action: lambda do
667
- @api_v5.delete('configuration/smtp')
668
- Result::Status.new('SMTP configuration deleted')
669
- end)
670
699
  command :test, description: 'Test SMTP configuration',
671
700
  arguments: [{name: :test_data, type: nil}]
672
701
  end
673
702
 
674
- commands_under(%i[admin events]) do
675
- command(:application, description: 'List application events', action: lambda do
676
- list, total = @api_v5.list_entities_limit_offset_total_count(entity: 'application_events', query: query_read_delete)
677
- Result::ObjectList.new(list, total: total, fields: %w[event_type created_at application user.name])
678
- end)
679
- command(:webhook, description: 'List webhook events', action: lambda do
680
- list, total = @api_v5.list_entities_limit_offset_total_count(entity: 'all_webhooks_events', query: query_read_delete, items_key: 'events')
681
- Result::ObjectList.new(list, total: total)
682
- end)
703
+ commands_under %i[admin events] do
704
+ command(
705
+ :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')))
708
+ Result::ObjectList.new(list, total: total, fields: %w[event_type created_at application user.name])
709
+ end
710
+ )
711
+ command(
712
+ :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')
715
+ Result::ObjectList.new(list, total: total)
716
+ end
717
+ )
683
718
  end
684
719
 
685
720
  # admin > clean_deleted handler (leaf, no sub-commands)
@@ -705,19 +740,6 @@ module Aspera
705
740
  end
706
741
  end
707
742
 
708
- # admin > <resource> > create / modify / delete / show
709
- Api::Faspex::ADMIN_RESOURCES.each do |res|
710
- CRUD_NO_LIST.each do |op|
711
- define_action_method([:admin, res, op]) do |input_data: nil, res_id: nil, **|
712
- args = res_exec_args(res)
713
- is_bulk = options.get_option(:bulk)
714
- items = input_data
715
- items = [items] if items && !items.is_a?(Array)
716
- entity_execute(command: op, input_data: items, res_id: res_id, is_bulk: is_bulk, bfail: options.get_option(:bfail), **args){ |f, v| res_lookup_id(res, f, v)}
717
- end
718
- end
719
- end
720
-
721
743
  def action_admin_smtp_test(test_data:, **)
722
744
  test_data = {test_email_recipient: test_data} if test_data.is_a?(String)
723
745
  creation = @api_v5.create('configuration/smtp/test', test_data)
@@ -754,17 +776,15 @@ module Aspera
754
776
  end
755
777
 
756
778
  # Lookup shared folder id by field/value within a node's shared_folders entity.
757
- # Used as lookup: on the :user command under admin > nodes > shared_folders.
758
- # sf_entity is available in ctx because setup_admin_nodes_shared_folders ran first (Phase A of parent).
779
+ # sf_entity is in ctx from setup_admin_nodes_shared_folders (Phase A of parent).
759
780
  def lookup_sf_id(field, value, sf_entity:, **)
760
781
  @api_v5.lookup_entity_by_field(entity: sf_entity, items_key: 'shared_folders', field: field, value: value)['id']
761
782
  end
762
783
 
763
- # admin > nodes > shared_folders > create/modify/delete/show/list
764
- Operations::ALL.each do |op|
765
- define_action_method([:admin, :nodes, :shared_folders, op]) do |sf_entity:, **|
766
- entity_execute(api: @api_v5, entity: sf_entity, items_key: 'shared_folders', command: op){ |f, v| @api_v5.lookup_entity_by_field(entity: sf_entity, items_key: 'shared_folders', field: f, value: v)['id']}
767
- end
784
+ # Lookup custom access user id by field/value within a shared folder's users entity.
785
+ # user_path is in ctx from setup_admin_nodes_shared_folders_user (Phase A of parent).
786
+ 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']
768
788
  end
769
789
 
770
790
  # admin > nodes > shared_folders > user — sf_id: already in ctx via arguments: on the :user command
@@ -772,17 +792,14 @@ module Aspera
772
792
  {user_path: "#{sf_entity}/#{sf_id}/custom_access_users"}
773
793
  end
774
794
 
775
- # admin > nodes > shared_folders > user > create/modify/delete/show/list (custom access users)
776
- Operations::ALL.each do |op|
777
- define_action_method([:admin, :nodes, :shared_folders, :user, op]) do |user_path:, **|
778
- entity_execute(api: @api_v5, entity: user_path, items_key: 'users', command: op){ |f, v| @api_v5.lookup_entity_by_field(entity: user_path, items_key: 'users', field: f, value: v)['id']}
779
- end
780
- end
781
-
782
795
  # admin > shared_inboxes|workgroups — res_id: already in ctx via arguments: on the :members|:saml_groups command
783
796
  %i[shared_inboxes workgroups].each do |res|
784
- define_method(:"setup_admin_#{res}_instance") do |res_id:, **|
785
- {res_instance_path: "#{res}/#{res_id}"}
797
+ define_method(:"setup_admin_#{res}_instance") do |**kwargs|
798
+ res_id = kwargs[:"#{RES_SINGULAR[res]}_id"]
799
+ {
800
+ res_instance_path: "#{res}/#{res_id}",
801
+ saml_groups_path: "#{res}/#{res_id}/saml_groups"
802
+ }
786
803
  end
787
804
  end
788
805
 
@@ -800,37 +817,46 @@ module Aspera
800
817
  end
801
818
  end
802
819
 
803
- # admin > shared_inboxes|workgroups > members|saml_groups > create/list/modify/delete
820
+ # Lookup methods for members/saml_groups identity resolution (used by arguments: lookup:)
821
+ # res_instance_path is in ctx from setup_admin_{res}_instance (Phase A of parent).
804
822
  %i[shared_inboxes workgroups].each do |res|
805
- MEMBER_SUBS.each do |sub|
806
- CRUD_NO_SHOW.each do |op|
807
- if op.eql?(:create) && sub.eql?(:members)
808
- define_action_method([:admin, res, sub, op]) do |users:, access:, res_instance_path:, **|
809
- res_path = "#{res_instance_path}/#{sub}"
810
- list_key = sub.eql?(:saml_groups) ? 'groups' : sub.to_s
811
- resolved = resolve_member_user_ids(users)
812
- input_data = [{user: resolved.map{ |u| {id: u, access: access}}}]
813
- entity_execute(api: @api_v5, entity: res_path, command: op, input_data: input_data, items_key: list_key) do |f, v|
814
- @api_v5.lookup_entity_by_field(entity: res_path, field: f, value: v, query: Rest.php_style({type: %w[user]}))['user_id']
815
- end
816
- end
817
- else
818
- define_action_method([:admin, res, sub, op]) do |res_instance_path:, **|
819
- res_path = "#{res_instance_path}/#{sub}"
820
- list_key = sub.eql?(:saml_groups) ? 'groups' : sub.to_s
821
- entity_execute(api: @api_v5, entity: res_path, command: op, items_key: list_key) do |f, v|
822
- @api_v5.lookup_entity_by_field(entity: res_path, field: f, value: v, query: Rest.php_style({type: %w[user]}))['user_id']
823
- end
824
- end
825
- end
826
- end
823
+ define_method(:"lookup_#{res}_members_id") do |field, value, res_instance_path:, **|
824
+ 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']
826
+ end
827
+ define_method(:"lookup_#{res}_saml_groups_id") do |field, value, res_instance_path:, **|
828
+ 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']
830
+ end
831
+ end
832
+
833
+ # admin > shared_inboxes|workgroups > members > create/list/modify/delete
834
+ # (saml_groups CRUD is handled by crud_commands)
835
+ %i[shared_inboxes workgroups].each do |res|
836
+ 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')
838
+ end
839
+
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])
842
+ end
843
+
844
+ 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])
846
+ end
847
+
848
+ define_action_method([:admin, res, :members, :create]) do |users:, access:, res_instance_path:, **|
849
+ res_path = "#{res_instance_path}/members"
850
+ 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)
827
853
  end
828
854
  end
829
855
 
830
856
  # admin > shared_inboxes|workgroups > invite_external_collaborator
831
857
  %i[shared_inboxes workgroups].each do |res|
832
- define_action_method([:admin, res, :invite_external_collaborator]) do |input_data:, res_id:, **|
833
- res_instance_path = "#{res}/#{res_id}"
858
+ define_action_method([:admin, res, :invite_external_collaborator]) do |input_data:, **kwargs|
859
+ res_instance_path = "#{res}/#{kwargs[:"#{RES_SINGULAR[res]}_id"]}"
834
860
  result = @api_v5.create("#{res_instance_path}/external_collaborator", input_data)
835
861
  formatter.display_status(result['message'])
836
862
  Result::SingleObject.new(@api_v5.lookup_entity_by_field(entity: "#{res_instance_path}/members", items_key: 'members', value: input_data['email_address'], query: {}))
@@ -845,7 +871,7 @@ module Aspera
845
871
  return {} if @api_v5
846
872
  @api_v5 = Api::Faspex.new(**Oauth.kwargs_from_options(options))
847
873
  # in case user wants to use HTTPGW tell transfer agent how to get address
848
- transfer.httpgw_url_cb = lambda{@api_v5.read('account')['gateway_url']}
874
+ transfer.httpgw_url_cb = lambda { @api_v5.read('account')['gateway_url'] }
849
875
  {}
850
876
  end
851
877
 
@@ -856,8 +882,9 @@ module Aspera
856
882
  {package_id: package_id}
857
883
  end
858
884
 
859
- def action_packages_list
860
- list, total = list_packages_with_filter
885
+ def action_packages_list(filter: nil, **)
886
+ list, max_items, total = list_packages_with_filter(filter: filter)
887
+ list = list[0, max_items] if max_items
861
888
  fields = %w[id title status sender.name recipients.0.name release_date total_bytes total_files]
862
889
  fields.delete('recipients.0.name') if %w[inbox inbox_history].include?(options.get_option(:box))
863
890
  fields.delete('sender.name') if %w[outbox outbox_history].include?(options.get_option(:box))
@@ -866,7 +893,7 @@ module Aspera
866
893
 
867
894
  def action_packages_delete(package_id:, **)
868
895
  ids = package_id.is_a?(Array) ? package_id : [package_id]
869
- Aspera.assert_array_all(ids, String){'Package id(s)'}
896
+ Aspera.assert_array_all(ids, String) { 'Package id(s)' }
870
897
  # API returns 204, empty on success
871
898
  @api_v5.call(
872
899
  operation: 'DELETE',
@@ -899,16 +926,16 @@ module Aspera
899
926
  # Called via lookup: :lookup_shared_folder_id on the shared_folders > browse command.
900
927
  def lookup_shared_folder_id(field, value, **)
901
928
  all = @api_v5.read('shared_folders')['shared_folders']
902
- matches = all.select{ |i| i[field].eql?(value)}
903
- Aspera.assert(!matches.empty?){"no match for #{field} = #{value}"}
904
- Aspera.assert(matches.length == 1){"multiple matches for #{field} = #{value}"}
929
+ matches = all.select { |i| i[field].eql?(value) }
930
+ Aspera.assert(!matches.empty?) { "no match for #{field} = #{value}" }
931
+ Aspera.assert(matches.length == 1) { "multiple matches for #{field} = #{value}" }
905
932
  matches.first['id']
906
933
  end
907
934
 
908
935
  def action_shared_folders_browse(folder_path:, shared_folder_id:, **)
909
936
  all_shared_folders = @api_v5.read('shared_folders')['shared_folders']
910
- node = all_shared_folders.find{ |i| i['id'].eql?(shared_folder_id)}
911
- Aspera.assert(!node.nil?){"No such shared folder id #{shared_folder_id}"}
937
+ node = all_shared_folders.find { |i| i['id'].eql?(shared_folder_id) }
938
+ Aspera.assert(!node.nil?) { "No such shared folder id #{shared_folder_id}" }
912
939
  browse_folder("nodes/#{node['node_id']}/shared_folders/#{shared_folder_id}/browse", {}, folder_path: folder_path)
913
940
  end
914
941
 
@@ -920,34 +947,18 @@ module Aspera
920
947
  end
921
948
 
922
949
  def action_invitations_create(input_data:, **)
923
- is_bulk = options.get_option(:bulk)
924
- Result.bulk(input_data, is_bulk: is_bulk, command: :create, bfail: options.get_option(:bfail)) do |params|
950
+ bulk_result(input_data, command: :create) do |params|
925
951
  endpoint = params.key?('recipient_name') ? 'public_invitations' : 'invitations'
926
952
  @api_v5.create(endpoint, params)
927
953
  end
928
954
  end
929
955
 
930
- # CRUD handlers for invitations (list, show, modify, delete)
931
- Operations::ALL.reject{ |op| op == :create}.each do |op|
932
- define_action_method([:invitations, op]) do
933
- entity_execute(
934
- api: @api_v5,
935
- entity: 'invitations',
936
- command: op,
937
- items_key: 'invitations',
938
- display_fields: %w[id public recipient_type recipient_name email_address]
939
- ) do |field, value|
940
- @api_v5.lookup_entity_by_field(entity: 'invitations', field: field, value: value, query: {})['id']
941
- end
942
- end
943
- end
944
-
945
956
  def action_gateway(parameters: {}, **)
946
957
  require 'aspera/faspex_gw'
947
958
  parameters = parameters.symbolize_keys
948
- uri = URI.parse(parameters.delete(:url){WebServerSimple::DEFAULT_URL})
959
+ uri = URI.parse(parameters.delete(:url) { WebServerSimple::DEFAULT_URL })
949
960
  server = WebServerSimple.new(uri, **parameters.slice(*WebServerSimple::PARAMS))
950
- Aspera.assert(parameters.except(*WebServerSimple::PARAMS).empty?){"unexpected parameters: #{parameters.except(*WebServerSimple::PARAMS).keys}"}
961
+ Aspera.assert(parameters.except(*WebServerSimple::PARAMS).empty?) { "unexpected parameters: #{parameters.except(*WebServerSimple::PARAMS).keys}" }
951
962
  server.mount(uri.path, Faspex4GWServlet, @api_v5, nil)
952
963
  server.start
953
964
  Result::Status.new('Gateway terminated')
@@ -956,7 +967,7 @@ module Aspera
956
967
  def action_postprocessing(parameters: {}, **)
957
968
  require 'aspera/faspex_postproc' # cspell:disable-line
958
969
  parameters = parameters.symbolize_keys
959
- uri = URI.parse(parameters.delete(:url){WebServerSimple::DEFAULT_URL})
970
+ uri = URI.parse(parameters.delete(:url) { WebServerSimple::DEFAULT_URL })
960
971
  parameters[:root] = uri.path
961
972
  server = WebServerSimple.new(uri, **parameters.slice(*WebServerSimple::PARAMS))
962
973
  server.mount(uri.path, Faspex4PostProcServlet, parameters.except(*WebServerSimple::PARAMS))
@@ -969,7 +980,7 @@ module Aspera
969
980
  CONTACT_TYPES = (WORKGROUP_TYPES + %w{distribution_list user external_user}).freeze
970
981
  PACKAGE_RECIPIENT_TYPES = %i{recipients private_recipients notified_on_upload notified_on_download notified_on_receipt}
971
982
  private_constant :SHARED_INBOX_MEMBER_LEVELS, :ACCOUNT_TYPES, :CONTACT_TYPES, :PACKAGE_RECIPIENT_TYPES,
972
- :MEMBER_SUBS, :CRUD_NO_SHOW, :CRUD_NO_LIST
983
+ :CRUD_NO_SHOW, :CRUD_NO_LIST
973
984
  end
974
985
  end
975
986
  end