aspera-cli 4.27.0 → 4.27.1

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 (140) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +29 -0
  4. data/CONTRIBUTING.md +1 -7
  5. data/TODO.md +50 -0
  6. data/docs/README.md +708 -488
  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 +297 -313
  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 +16 -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 +12 -12
  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_Shares.yaml +4653 -0
  119. data/lib/aspera/schema/documentation.rb +17 -11
  120. data/lib/aspera/schema/reader.rb +78 -4
  121. data/lib/aspera/schema/registry.rb +26 -7
  122. data/lib/aspera/secret_hider.rb +5 -5
  123. data/lib/aspera/ssh.rb +64 -31
  124. data/lib/aspera/ssl.rb +3 -3
  125. data/lib/aspera/sync/database.rb +2 -2
  126. data/lib/aspera/sync/operations.rb +13 -13
  127. data/lib/aspera/temp_file_manager.rb +24 -4
  128. data/lib/aspera/transfer/faux_file.rb +2 -2
  129. data/lib/aspera/transfer/parameters.rb +15 -15
  130. data/lib/aspera/transfer/resumer.rb +9 -9
  131. data/lib/aspera/transfer/spec.rb +1 -1
  132. data/lib/aspera/uri_reader.rb +52 -15
  133. data/lib/aspera/web_auth.rb +6 -5
  134. data/lib/aspera/web_server_simple.rb +9 -9
  135. data/lib/aspera/yaml.rb +4 -4
  136. data.tar.gz.sig +0 -0
  137. metadata +10 -4
  138. metadata.gz.sig +0 -0
  139. data/lib/aspera/cli/plugins/faspex.rb +0 -576
  140. data/lib/aspera/transfer/uri.rb +0 -56
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # cspell:ignore snid fnid bidi ssync asyncs rund asnodeadmin mkfile mklink asperabrowser asperabrowserurl watchfolders watchfolderd entsrv
4
+ require 'aspera/schema/registry'
4
5
  require 'aspera/cli/plugins/basic_auth'
5
6
  require 'aspera/cli/sync_actions'
6
7
  require 'aspera/cli/special_values'
@@ -22,30 +23,6 @@ module Aspera
22
23
  class Node < BasicAuth
23
24
  include SyncActions
24
25
 
25
- # Processing of paths in arguments and results
26
- # Used only by Faspex4 to browse packages
27
- class NodePathPrefix
28
- def initialize(path)
29
- @root = path
30
- end
31
-
32
- # get next path argument from command line, and add prefix
33
- def add_to_path(path_arg)
34
- File.join(@root, path_arg)
35
- end
36
-
37
- # get remaining path arguments from command line, and add prefix
38
- def add_to_paths!(path_args)
39
- path_args.map!{ |p| add_to_path(p)}
40
- end
41
-
42
- def remove_in_object_list!(obj_list)
43
- obj_list.each do |item|
44
- item['path'] = item['path'].delete_prefix(@root) if item['path'].start_with?(@root)
45
- end
46
- end
47
- end
48
-
49
26
  application_name 'HSTS Node API'
50
27
 
51
28
  SESSION_TIME_FIELDS = %i[start end].freeze
@@ -82,31 +59,12 @@ module Aspera
82
59
  }
83
60
  rescue StandardError => e
84
61
  error = e
85
- Log.log.debug{"detect error: #{e}"}
62
+ Log.log.debug { "detect error: #{e}" }
86
63
  end
87
64
  raise error if error
88
65
  return
89
66
  end
90
67
 
91
- # Called by non-Node plugins (Ats, Cos, Aoc) to declare Node options on their
92
- # own options object. The DSL registry is walked so descriptions stay in one place.
93
- def declare_options(options)
94
- return if options.option_declared?(:root_id)
95
- command_registry.option_specs.each_value do |spec|
96
- next if options.option_declared?(spec.name)
97
- options.declare(
98
- spec.name,
99
- description: spec.description,
100
- short: spec.short,
101
- allowed: spec.allowed,
102
- default: spec.default,
103
- handler: spec.handler,
104
- schema: spec.schema
105
- )
106
- end
107
- options.parse_options!
108
- end
109
-
110
68
  # Using /files/browse: is it a folder (node and shares)
111
69
  def gen3_entry_folder?(entry)
112
70
  FOLDER_TYPES.include?(entry['type'])
@@ -114,7 +72,7 @@ module Aspera
114
72
  end
115
73
 
116
74
  # DSL option declarations - at class level, picked up by Base#initialize via ancestor chain.
117
- # Also exposed via Node.declare_options for non-Node plugins (Ats, Cos, Aoc).
75
+ # Included by other plugins (Ats, Cos, Aoc) via `use_options Node`.
118
76
  option :validator, description: 'Identifier of validator (optional for central)'
119
77
  option :asperabrowserurl, description: 'URL for simple aspera web ui', default: 'https://asperabrowser.mybluemix.net'
120
78
  option :node_api, description: 'Gen4: standard_ports: Use standard FASP ports (true) or get from node API (false). cache: Set to false to force actual file system read',
@@ -149,37 +107,83 @@ module Aspera
149
107
  SEARCH_REMOVE_FIELDS = %w[basename permissions].freeze
150
108
 
151
109
  # Actions in execute_command_gen3
152
- COMMANDS_GEN3 = %i[search space mkdir mklink mkfile rename delete browse upload download cat sync transport spec]
153
-
154
- BASE_ACTIONS = %i[api_details].concat(COMMANDS_GEN3).freeze
110
+ COMMANDS_GEN3 = %i[search space mkdir mklink mkfile rename delete ls upload download cat sync transport spec]
111
+
112
+ # Shared DSL metadata for Gen3 commands (description:, arguments:, transfer_paths:, aliases:).
113
+ # Consumed by commands_under(:files) in shares.rb and commands_under %i[source node]) in faspex.rb.
114
+ # :sync is excluded (intermediate node with sub-commands, handled separately).
115
+ # action: entries are node-specific and intentionally omitted.
116
+ COMMANDS_GEN3_SPEC = {
117
+ search: {description: 'Search for files', arguments: [{name: :search_root, type: String}]},
118
+ space: {description: 'Show space information', arguments: [{name: :path_list, multiple: true}]},
119
+ mkdir: {description: 'Create a folder (Gen3)', arguments: [{name: :path_list, multiple: true}]},
120
+ mklink: {description: 'Create a symbolic link (Gen3)', arguments: [{name: :target, type: String}, {name: :link_path, type: String}]},
121
+ mkfile: {description: 'Create a file (Gen3)', arguments: [{name: :file_path, type: String}, {name: :contents, mandatory: false, default: nil}]},
122
+ rename: {description: 'Rename a file or folder (Gen3)', arguments: [{name: :path_base, type: String}, {name: :path_src, type: String}, {name: :path_dst, type: String}]},
123
+ delete: {description: 'Delete files or folders (Gen3)', arguments: [{name: :paths, multiple: true}]},
124
+ ls: {description: 'List files (Gen3)', arguments: [{name: :path, type: String}], aliases: [:browse]},
125
+ upload: {description: 'Upload files (Gen3)', transfer_paths: :send},
126
+ download: {description: 'Download files (Gen3)', transfer_paths: :receive},
127
+ cat: {description: 'Show file contents (Gen3)', arguments: [{name: :remote_path, type: String}]},
128
+ transport: {description: 'Show transport parameters'},
129
+ spec: {description: 'Show transfer spec base'},
130
+ api_details: {description: 'Show API details'},
131
+ health: {description: 'Check node health'},
132
+ events: {description: 'List events'},
133
+ info: {description: 'Show node info'},
134
+ slash: {description: 'Show root info'},
135
+ license: {description: 'Show license'},
136
+ access_keys: {description: 'Manage access keys'}
137
+ }.freeze
138
+
139
+ BASE_ACTIONS = (%i[api_details] + COMMANDS_GEN3).freeze
155
140
 
156
141
  SPECIAL_ACTIONS = %i[health events info slash license].freeze
157
142
 
158
143
  # commands for : `execute_simple_common`: actions used commonly when a node is involved
159
- COMMON_ACTIONS = %i[access_keys].concat(BASE_ACTIONS).concat(SPECIAL_ACTIONS).freeze
144
+ COMMON_ACTIONS = (%i[access_keys] + BASE_ACTIONS + SPECIAL_ACTIONS).freeze
160
145
 
161
146
  private_constant :CENTRAL_SOAP_API_TEST, :SEARCH_REMOVE_FIELDS, :BASE_ACTIONS, :SPECIAL_ACTIONS, :COMMON_ACTIONS
162
147
 
163
148
  # used in aoc
164
- NODE4_READ_ACTIONS = %i[bearer_token_node node_info browse find].freeze
149
+ NODE4_READ_ACTIONS = %i[bearer_token_node node_info ls find].freeze
165
150
 
166
151
  # commands for execute_command_gen4
167
- COMMANDS_GEN4 = %i[mkdir mklink mkfile rename delete upload download sync cat show modify permission thumbnail v3].concat(NODE4_READ_ACTIONS).freeze
152
+ COMMANDS_GEN4 = (%i[mkdir mklink mkfile rename delete upload download sync cat show modify permission thumbnail v3] + NODE4_READ_ACTIONS).freeze
153
+
154
+ # Shared DSL metadata for all Gen4 commands (description:, arguments:, transfer_paths:, aliases:).
155
+ # Consumed by commands_under %i[access_keys do]) in node.rb and by aoc.rb.
156
+ # :sync and :permission are excluded: they are intermediate nodes handled separately.
157
+ SINGLE_PATH_ARG = [{name: :path, type: String}].freeze
158
+ COMMANDS_GEN4_SPEC = {
159
+ mkdir: {description: 'Create folder', arguments: SINGLE_PATH_ARG},
160
+ mklink: {description: 'Create symbolic link', arguments: SINGLE_PATH_ARG},
161
+ mkfile: {description: 'Create file', arguments: [{name: :path, type: String}, {name: :contents, mandatory: false, default: nil}]},
162
+ rename: {description: 'Rename entry', arguments: [{name: :source_path, type: String}, {name: :new_name, type: String}]},
163
+ delete: {description: 'Delete entry', arguments: [{name: :paths, type: String, bulk: true}]},
164
+ upload: {description: 'Upload files', transfer_paths: :send},
165
+ download: {description: 'Download files', transfer_paths: :receive},
166
+ modify: {description: 'Modify file', arguments: [{name: :path, type: String}, {name: :update_value, type: Hash, schema: 'node:components.schemas.files-id-put-request'}]},
167
+ cat: {description: 'Show file contents', arguments: SINGLE_PATH_ARG},
168
+ show: {description: 'Show file info', arguments: SINGLE_PATH_ARG},
169
+ thumbnail: {description: 'Show file thumbnail', arguments: SINGLE_PATH_ARG},
170
+ bearer_token_node: {description: 'Show bearer token for file node', arguments: SINGLE_PATH_ARG},
171
+ node_info: {description: 'Show node info for file', arguments: SINGLE_PATH_ARG},
172
+ ls: {description: 'List files', arguments: SINGLE_PATH_ARG, aliases: [:browse]},
173
+ find: {description: 'Find files', arguments: SINGLE_PATH_ARG + FILTER_ARGS},
174
+ v3: {description: 'Legacy v3 commands on files'}
175
+ }.freeze
176
+ private_constant :SINGLE_PATH_ARG
168
177
 
169
178
  # commands supported in ATS for COS
170
179
  COMMANDS_COS = %i[upload download info access_keys api_details transfer].freeze
171
180
  COMMANDS_SHARES = (BASE_ACTIONS - %i[search]).freeze
172
- COMMANDS_FASPEX = COMMON_ACTIONS
173
-
174
181
  # `browse` display fields for gen4
175
182
  GEN4_LS_FIELDS = %w[name type recursive_size size modified_time access_level].freeze
176
183
 
177
184
  # @param api [Rest] an existing API object for the Node API
178
- # @param prefix_path [String,nil] for Faspex 4, allows browsing a package without full path in node (removes storage prefix)
179
- def initialize(context:, api: nil, prefix_path: nil)
180
- @node_path_prefix = prefix_path ? NodePathPrefix.new(prefix_path) : nil
185
+ def initialize(context:, api: nil)
181
186
  super(context: context, basic_options: api.nil?)
182
- Node.declare_options(options)
183
187
  return if context.only_manual?
184
188
  @api_node =
185
189
  if !api.nil?
@@ -207,7 +211,7 @@ module Aspera
207
211
  # Gen3 API
208
212
  # @param path [String] starting path
209
213
  def browse_gen3(path)
210
- folders_to_process = @node_path_prefix.nil? ? path : @node_path_prefix.add_to_path(path)
214
+ folders_to_process = path
211
215
  folders_to_process = [folders_to_process]
212
216
  query = options.get_option(:query) || {}
213
217
  # special parameter: max number of entries in result
@@ -220,7 +224,7 @@ module Aspera
220
224
  single_call = query.key?('skip')
221
225
  # API default is 100, so use 1000 for default
222
226
  query['count'] ||= 1000
223
- Aspera.assert(!(recursive && single_call), type: Cli::BadArgument){'options `recursive` and `skip` cannot be used together'}
227
+ Aspera.assert(!(recursive && single_call), type: Cli::BadArgument) { 'options `recursive` and `skip` cannot be used together' }
224
228
  all_items = []
225
229
  until folders_to_process.empty?
226
230
  path = folders_to_process.shift
@@ -231,10 +235,7 @@ module Aspera
231
235
  # example: send_result={'items'=>[{'file'=>"filename1","permissions"=>[{'name'=>'read'},{'name'=>'write'}]}]}
232
236
  response = @api_node.create('files/browse', query)
233
237
  # 'file','symbolic_link'
234
- if !Node.gen3_entry_folder?(response['self']) || only_path
235
- @node_path_prefix&.remove_in_object_list!([response['self']])
236
- return Result::SingleObject.new(response['self'])
237
- end
238
+ return Result::SingleObject.new(response['self']) if !Node.gen3_entry_folder?(response['self']) || only_path
238
239
  items = response['items']
239
240
  total_count ||= response['total_count']
240
241
  all_items.concat(items)
@@ -242,7 +243,7 @@ module Aspera
242
243
  formatter.display_item_count(response['item_count'], total_count)
243
244
  break
244
245
  end
245
- folders_to_process.concat(items.select{ |i| Node.gen3_entry_folder?(i)}.map{ |i| i['path']}) if recursive
246
+ folders_to_process.concat(items.select { |i| Node.gen3_entry_folder?(i) }.map { |i| i['path'] }) if recursive
246
247
  if !max_items.nil? && (all_items.count >= max_items)
247
248
  all_items = all_items.slice(0, max_items) if all_items.count > max_items
248
249
  break
@@ -254,7 +255,6 @@ module Aspera
254
255
  end
255
256
  query.delete('skip')
256
257
  end
257
- @node_path_prefix&.remove_in_object_list!(all_items)
258
258
  return Result::ObjectList.new(all_items)
259
259
  ensure
260
260
  RestParameters.instance.spinner_cb.call(action: :success)
@@ -297,7 +297,7 @@ module Aspera
297
297
  # @param path [String] plain path or %id:<file_id> selector
298
298
  def apifid_from_path(top_file_id, path)
299
299
  if (m = Parser.percent_selector(path))
300
- Aspera.assert_values(m[:field], ['id'], type: BadArgument){'file id'}
300
+ Aspera.assert_values(m[:field], ['id'], type: BadArgument) { 'file id' }
301
301
  val = m[:value]
302
302
  return Api::NodeFileId.new(@api_node, val.nil? || val.empty? ? top_file_id : val)
303
303
  end
@@ -309,7 +309,7 @@ module Aspera
309
309
  # (intermediate nodes whose path cannot yet be injected via execute_leaf).
310
310
  def apifid_from_next_arg(top_file_id)
311
311
  file_path = options.instance_identifier(description: 'path or %id:<id> or %id:') do |attribute, value|
312
- Aspera.assert_values(attribute, ['id'], type: BadArgument){'file id'}
312
+ Aspera.assert_values(attribute, ['id'], type: BadArgument) { 'file id' }
313
313
  return Api::NodeFileId.new(@api_node, value.nil? || value.empty? ? top_file_id : value)
314
314
  end
315
315
  @api_node.resolve_api_fid(top_file_id, file_path)
@@ -321,21 +321,30 @@ module Aspera
321
321
  # @return [Integer] id of the sync
322
322
  # @raise [Cli::BadArgument] if no such sync, or not by name
323
323
  def async_lookup(field, value)
324
- Aspera.assert_values(field, ['name'], type: Cli::BadArgument){'search field'}
324
+ Aspera.assert_values(field, ['name'], type: Cli::BadArgument) { 'search field' }
325
325
  async_ids = @api_node.read('async/list')['sync_ids']
326
326
  summaries = @api_node.create('async/summary', {'syncs' => async_ids})['sync_summaries']
327
- selected = summaries.find{ |s| s['name'].eql?(value)}
327
+ selected = summaries.find { |s| s['name'].eql?(value) }
328
328
  raise Cli::BadIdentifier.new('sync', value, field: field) if selected.nil?
329
329
  return selected['snid']
330
330
  end
331
331
 
332
+ # Lookup for access_keys: only supports %id:self selector.
333
+ # @param field [String] must be 'id'
334
+ # @param value [String] must be 'self'
335
+ # @return [String] the resolved access key id
336
+ def lookup_access_key_self_id(field, value, **)
337
+ Aspera.assert(field.eql?('id') && value.eql?('self'), type: BadArgument) { 'only selector: %id:self' }
338
+ @api_node.read('access_keys/self')['id']
339
+ end
340
+
332
341
  # Search /asyncs by name
333
342
  # @param field [String] name of the field to search
334
343
  # @param value [String] value of the field to search
335
344
  # @return [Integer] id of the sync
336
345
  # @raise [Cli::BadArgument] if no such sync, or not by name
337
346
  def ssync_lookup(field, value)
338
- Aspera.assert_values(field, ['name'], type: Cli::BadArgument){'search field'}
347
+ Aspera.assert_values(field, ['name'], type: Cli::BadArgument) { 'search field' }
339
348
  @api_node.read('asyncs')['ids'].each do |id|
340
349
  sync_info = @api_node.read("asyncs/#{id}")['configuration']
341
350
  # name is unique, so we can return
@@ -346,106 +355,77 @@ module Aspera
346
355
 
347
356
  # --- DSL command declarations ---
348
357
 
349
- # Gen3 leaf commands
350
- command :search, description: 'Search for files',
351
- arguments: [{name: :search_root, type: String}]
352
- command :space, description: 'Show space information',
353
- arguments: [{name: :path_list, multiple: true}]
354
- command :mkdir, description: 'Create a folder (Gen3)',
355
- arguments: [{name: :path_list, multiple: true}]
356
- command :mklink, description: 'Create a symbolic link (Gen3)',
357
- arguments: [{name: :target, type: String}, {name: :link_path, type: String}]
358
- command :mkfile, description: 'Create a file (Gen3)',
359
- arguments: [{name: :file_path, type: String}, {name: :contents, mandatory: false, default: nil}]
360
- command :rename, description: 'Rename a file or folder (Gen3)',
361
- arguments: [{name: :path_base, type: String}, {name: :path_src, type: String}, {name: :path_dst, type: String}]
362
- command :delete, description: 'Delete files or folders (Gen3)',
363
- arguments: [{name: :paths, multiple: true}]
364
- command :browse, description: 'Browse files (Gen3)',
365
- arguments: [{name: :path, type: String}],
366
- action: ->(path:, **){browse_gen3(path)}
367
- command :upload, description: 'Upload files (Gen3)', transfer_paths: :send
368
- command :download, description: 'Download files (Gen3)', transfer_paths: :receive
369
- command :cat, description: 'Show file contents (Gen3)',
370
- arguments: [{name: :remote_path, type: String}]
371
- command :sync, description: 'Synchronize folders (Gen3)'
372
- commands_under(:sync) do
358
+ # Gen3 leaf commands — metadata from COMMANDS_GEN3_SPEC; action: added where node-specific.
359
+ # :sync is declared separately (intermediate node with sub-commands).
360
+ GEN3_NODE_ACTIONS = {
361
+ ls: ->(path:, **) { browse_gen3(path) },
362
+ transport: ->(**) { Result::SingleObject.new(@api_node.transport_params) },
363
+ spec: ->(**) { Result::SingleObject.new(@api_node.base_spec, fields: Formatter.all_but(Transfer::Spec::SPECIFIC)) },
364
+ api_details: ->(**) { Result::SingleObject.new({base_url: @api_node.base_url}.merge(@api_node.params)) },
365
+ events: ->(**) { Result::ObjectList.new(@api_node.read('events', query_read_delete(schema: Schema::Registry.query_params(Schema::Registry::NODE, 'events'))), fields: ->(f) { !f.start_with?('data') }) },
366
+ info: ->(**) { Result::SingleObject.new(@api_node.read('info')) },
367
+ slash: ->(**) { Result::SingleObject.new(@api_node.read('')) },
368
+ license: ->(**) { Result::SingleObject.new(@api_node.read('license')) }
369
+ }.freeze
370
+ private_constant :GEN3_NODE_ACTIONS
371
+ COMMANDS_GEN3_SPEC.each do |cmd, spec|
372
+ next if cmd.eql?(:sync) # intermediate node with sub-commands
373
+ next if cmd.eql?(:access_keys) # intermediate node declared separately below
374
+ action = GEN3_NODE_ACTIONS[cmd]
375
+ command cmd, **spec, **(action ? {action: action} : {})
376
+ end
377
+ command :sync, description: 'Synchronize folders (Gen3)'
378
+ commands_under :sync do
373
379
  Sync::Operations::DIRECTIONS.each do |dir|
374
- command(dir, description: "#{dir.capitalize}-sync (Gen3)", transfer_paths: :send)
380
+ command dir, description: "#{dir.capitalize}-sync (Gen3)", transfer_paths: :send, arguments: SyncActions::PATH_AND_INFO_ARGS
375
381
  end
376
382
  command :admin, description: 'Manage sync database (admin operations)'
377
- SyncActions.register_sync_admin_commands(self, %i[sync admin])
378
- end
379
- command :transport, description: 'Show transport parameters',
380
- action: ->{Result::SingleObject.new(@api_node.transport_params)}
381
- command :spec, description: 'Show transfer spec base',
382
- action: ->{Result::SingleObject.new(@api_node.base_spec, fields: Formatter.all_but(Transfer::Spec::SPECIFIC))}
383
- # Other common leaf commands
384
- command :api_details, description: 'Show API details',
385
- action: ->{Result::SingleObject.new({base_url: @api_node.base_url}.merge(@api_node.params))}
386
- command :health, description: 'Check node health'
387
- command :events, description: 'List events',
388
- action: ->{Result::ObjectList.new(@api_node.read('events', query_read_delete), fields: ->(f){!f.start_with?('data')})}
389
- command :info, description: 'Show node info',
390
- action: ->{Result::SingleObject.new(@api_node.read('info'))}
391
- command :slash, description: 'Show root info',
392
- action: ->{Result::SingleObject.new(@api_node.read(''))}
393
- command :license, description: 'Show license',
394
- action: ->{Result::SingleObject.new(@api_node.read('license'))}
383
+ SyncActions.register_sync_admin_commands(self, :admin)
384
+ end
395
385
  # access_keys sub-tree
396
386
  command :access_keys, description: 'Manage access keys'
397
- commands_under(:access_keys) do
387
+ commands_under :access_keys do
398
388
  command :do, description: 'Execute Gen4 command via access key',
399
389
  arguments: [{name: :access_key_id, type: :identifier}],
400
390
  setup: :setup_access_key_do
401
391
  command :set_bearer_key, description: 'Set bearer key on access key',
402
392
  arguments: [{name: :access_key_id}, {name: :bearer_key_pem, type: String}]
403
- Operations::ALL.each{ |op| command(op, description: "#{op.capitalize} access key(s)")}
404
- end
405
-
406
- # Commands requiring a single path argument (Gen4)
407
- COMMANDS_GEN4_SINGLE_PATH = %i[mkdir mklink browse cat show find thumbnail bearer_token_node node_info].freeze
408
- private_constant :COMMANDS_GEN4_SINGLE_PATH
409
-
410
- commands_under(%i[access_keys do]) do
411
- COMMANDS_GEN4.each do |cmd|
412
- next if cmd.eql?(:sync) # sync is an intermediate node declared below
413
- kwargs = {description: "Gen4 #{cmd} command"}
414
- kwargs[:setup] = :setup_access_key_do_permission if cmd.eql?(:permission)
415
- kwargs[:arguments] = [{name: :source_path, type: String}, {name: :new_name, type: String}] if cmd.eql?(:rename)
416
- kwargs[:arguments] = [{name: :paths, type: String, bulk: true}] if cmd.eql?(:delete)
417
- kwargs[:arguments] = [{name: :path, type: String}, {name: :contents, mandatory: false, default: nil}] if cmd.eql?(:mkfile)
418
- kwargs[:arguments] = [{name: :path, type: String}, {name: :update_value, type: Hash}] if cmd.eql?(:modify)
419
- kwargs[:arguments] = [{name: :path, type: String}] if COMMANDS_GEN4_SINGLE_PATH.include?(cmd)
420
- kwargs[:transfer_paths] = :send if cmd.eql?(:upload)
421
- kwargs[:transfer_paths] = :receive if cmd.eql?(:download)
422
- command(cmd, **kwargs)
393
+ crud_commands entity: 'access_keys',
394
+ api: :@api_node,
395
+
396
+ body_component: Schema::Registry::NODE,
397
+ lookup: :lookup_access_key_self_id
398
+ end
399
+
400
+ commands_under %i[access_keys do] do
401
+ COMMANDS_GEN4_SPEC.each do |cmd, spec|
402
+ command cmd, **spec
423
403
  end
424
- command :sync, description: 'Synchronize folders (Gen4)'
425
- commands_under(%i[access_keys do sync]) do
404
+ command :permission, description: 'Manage permissions', setup: :setup_access_key_do_permission
405
+ command :sync, description: 'Synchronize folders'
406
+ commands_under :sync do
426
407
  Sync::Operations::DIRECTIONS.each do |dir|
427
- command(dir, description: "#{dir.capitalize}-sync (Gen4)", transfer_paths: :send)
408
+ command dir, description: "#{dir.capitalize}-sync", transfer_paths: :send, arguments: SyncActions::PATH_AND_INFO_ARGS
428
409
  end
429
410
  command :admin, description: 'Manage sync database (admin operations)'
430
- SyncActions.register_sync_admin_commands(self, %i[access_keys do sync admin])
411
+ SyncActions.register_sync_admin_commands(self, :admin)
431
412
  end
432
413
  end
433
- commands_under(%i[access_keys do permission]) do
414
+ commands_under %i[access_keys do permission] do
434
415
  command :list, description: 'List permissions on a file'
435
416
  command :show, description: 'Show a permission',
436
417
  arguments: [{name: :perm_id, type: :identifier}],
437
- action: ->(apifid:, perm_id:, **){Result::SingleObject.new(apifid.node_api.read("permissions/#{perm_id}"))}
418
+ action: ->(apifid:, perm_id:, **) { Result::SingleObject.new(apifid.node_api.read("permissions/#{perm_id}")) }
438
419
  command :create, description: 'Create a permission',
439
- arguments: [{name: :data, type: Hash}]
420
+ arguments: [{name: :data, type: Hash, schema: 'node:components.schemas.permissions-post-request'}]
440
421
  command :modify, description: 'Modify a permission',
441
- arguments: [{name: :perm_id, type: :identifier}, {name: :data, type: Hash}]
442
- command :delete, description: 'Delete permission(s)',
422
+ arguments: [{name: :perm_id, type: :identifier}, {name: :data, type: Hash, schema: 'node:components.schemas.permissions-id-put-request'}]
423
+ command :delete, description: 'Delete permissions',
443
424
  arguments: [{name: :perm_id, bulk: true}]
444
425
  end
445
426
  # async (legacy /async)
446
- command :async, description: 'Manage async operations (legacy /async)'
447
- commands_under(:async) do
448
- command :list, description: 'List async sync IDs', action: ->{Result::ValueList.new(@api_node.read('async/list')['sync_ids'])}
427
+ commands_under :async, description: 'synchronization (legacy /async)' do
428
+ command :list, description: 'List async sync IDs', action: -> { Result::ValueList.new(@api_node.read('async/list')['sync_ids']) }
449
429
  command :show, description: 'Show async summary',
450
430
  arguments: [{name: :async_id, type: :identifier, lookup: :async_lookup}]
451
431
  command :delete, description: 'Delete async',
@@ -458,73 +438,70 @@ module Aspera
458
438
  arguments: [{name: :async_id, type: :identifier, lookup: :async_lookup}]
459
439
  end
460
440
  # ssync (/asyncs)
461
- command :ssync, description: 'Manage sync operations (/asyncs)'
462
- commands_under(:ssync) do
463
- command :create, description: 'Create ssync', action: ->{entity_execute(api: @api_node, entity: :asyncs, command: :create, items_key: 'ids'){ |f, v| ssync_lookup(f, v)}}
464
- command :list, description: 'List ssync', action: ->{entity_execute(api: @api_node, entity: :asyncs, command: :list, items_key: 'ids'){ |f, v| ssync_lookup(f, v)}}
465
- command :show, description: 'Show ssync',
466
- arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
467
- action: ->(ssync_id:, **){Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}"))}
468
- command :delete, description: 'Delete ssync',
469
- arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
470
- action: :action_ssync_delete
471
- command :start, description: 'Start a sync',
441
+ # command :ssync, description: 'Manage sync operations (/asyncs)'
442
+ commands_under :ssync, description: 'synchronization (/asyncs)' do
443
+ crud_commands entity: 'asyncs',
444
+ api: :@api_node,
445
+ operations: %i[create list show delete],
446
+ items_key: 'ids',
447
+ lookup: :ssync_lookup
448
+ command :start, description: 'Start a sync',
472
449
  arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
473
450
  action: :action_ssync_start
474
- command :stop, description: 'Stop a sync',
451
+ command :stop, description: 'Stop a sync',
475
452
  arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
476
453
  action: :action_ssync_stop
477
454
  command :bandwidth, description: 'Show sync bandwidth',
478
455
  arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
479
- action: ->(ssync_id:, **){Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/bandwidth", options.get_option(:query) || {}))}
480
- command :counters, description: 'Show sync counters',
456
+ action: ->(ssync_id:, **) { Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/bandwidth", options.get_option(:query) || {})) }
457
+ command :counters, description: 'Show sync counters',
481
458
  arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
482
- action: ->(ssync_id:, **){Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/counters", options.get_option(:query) || {}))}
483
- command :files, description: 'List sync files',
459
+ action: ->(ssync_id:, **) { Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/counters", options.get_option(:query) || {})) }
460
+ command :files, description: 'List sync files',
484
461
  arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
485
- action: ->(ssync_id:, **){Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/files", options.get_option(:query) || {}))}
486
- command :state, description: 'Show sync state',
462
+ action: ->(ssync_id:, **) { Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/files", options.get_option(:query) || {})) }
463
+ command :state, description: 'Show sync state',
487
464
  arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
488
- action: ->(ssync_id:, **){Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/state"))}
489
- command :summary, description: 'Show sync summary',
465
+ action: ->(ssync_id:, **) { Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/state")) }
466
+ command :summary, description: 'Show sync summary',
490
467
  arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
491
- action: ->(ssync_id:, **){Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/summary"))}
468
+ action: ->(ssync_id:, **) { Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/summary")) }
492
469
  end
493
470
  # stream
494
471
  command :stream, description: 'Manage stream operations'
495
- commands_under(:stream) do
496
- command :list, description: 'List streams', action: ->{Result::ObjectList.new(@api_node.read('ops/transfers', query_read_delete), fields: %w[id status])}
472
+ commands_under :stream do
473
+ command :list, description: 'List streams', action: -> { Result::ObjectList.new(@api_node.read('ops/transfers', query_read_delete), fields: %w[id status]) }
497
474
  command :create, description: 'Create a stream',
498
- arguments: [{name: :data, type: Hash}],
499
- action: ->(data:, **){Result::SingleObject.new(@api_node.create('streams', data))}
475
+ arguments: [{name: :data, type: Hash, schema: 'node:components.schemas.transferPostRequest'}],
476
+ action: ->(data:, **) { Result::SingleObject.new(@api_node.create('streams', data)) }
500
477
  command :show, description: 'Show a stream',
501
478
  arguments: [{name: :transfer_id, type: :identifier}],
502
- action: ->(transfer_id:, **){Result::SingleObject.new(@api_node.read("ops/transfers/#{transfer_id}"))}
479
+ action: ->(transfer_id:, **) { Result::SingleObject.new(@api_node.read("ops/transfers/#{transfer_id}")) }
503
480
  command :modify, description: 'Modify a stream',
504
- arguments: [{name: :transfer_id, type: :identifier}, {name: :data, type: Hash}],
505
- action: ->(data:, transfer_id:, **){Result::SingleObject.new(@api_node.update("streams/#{transfer_id}", data))}
481
+ arguments: [{name: :transfer_id, type: :identifier}, {name: :data, type: Hash, schema: 'node:components.schemas.transferPutRequest'}],
482
+ action: ->(data:, transfer_id:, **) { Result::SingleObject.new(@api_node.update("streams/#{transfer_id}", data)) }
506
483
  command :cancel, description: 'Cancel a stream',
507
484
  arguments: [{name: :transfer_id, type: :identifier}],
508
- action: ->(transfer_id:, **){Result::SingleObject.new(@api_node.cancel("streams/#{transfer_id}"))}
485
+ action: ->(transfer_id:, **) { Result::SingleObject.new(@api_node.cancel("streams/#{transfer_id}")) }
509
486
  end
510
487
  # transfer
511
488
  command :transfer, description: 'Manage transfer operations'
512
- commands_under(:transfer) do
513
- command :list, description: 'List transfers'
514
- command :cancel, description: 'Cancel a transfer',
489
+ commands_under :transfer do
490
+ command :list, description: 'List transfers'
491
+ command :cancel, description: 'Cancel a transfer',
515
492
  arguments: [{name: :transfer_id, type: :identifier}]
516
- command :show, description: 'Show a transfer',
493
+ command :show, description: 'Show a transfer',
517
494
  arguments: [{name: :transfer_id, type: :identifier}],
518
- action: ->(transfer_id:, **){Result::SingleObject.new(@api_node.read("ops/transfers/#{transfer_id}"))}
495
+ action: ->(transfer_id:, **) { Result::SingleObject.new(@api_node.read("ops/transfers/#{transfer_id}")) }
519
496
  command :modify, description: 'Modify a transfer',
520
- arguments: [{name: :transfer_id, type: :identifier}, {name: :update_value, type: Hash}]
497
+ arguments: [{name: :transfer_id, type: :identifier}, {name: :update_value, type: Hash, schema: 'node:components.schemas.transferPutRequest'}]
521
498
  command :bandwidth_average, description: 'Show average bandwidth per period'
522
499
  command :sessions, description: 'List transfer sessions'
523
500
  end
524
501
  # service
525
502
  command :service, description: 'Manage services'
526
- commands_under(:service) do
527
- command :list, description: 'List services', action: ->{Result::ObjectList.new(@api_node.read('rund/services')['services'])}
503
+ commands_under :service do
504
+ command :list, description: 'List services', action: -> { Result::ObjectList.new(@api_node.read('rund/services')['services']) }
528
505
  command :create, description: 'Create a service',
529
506
  arguments: [{name: :creation_data, type: Hash}]
530
507
  command :delete, description: 'Delete a service',
@@ -532,34 +509,34 @@ module Aspera
532
509
  end
533
510
  # watch_folder
534
511
  command :watch_folder, description: 'Manage watch folders', setup: :setup_watch_folder
535
- commands_under(:watch_folder) do
512
+ commands_under :watch_folder do
536
513
  command :create, description: 'Create a watch folder',
537
514
  arguments: [{name: :data, type: Hash}],
538
- action: ->(data:, **){Result::Status.new("#{@api_node.create('v3/watchfolders', data)['id']} created")}
515
+ action: ->(data:, **) { Result::Status.new("#{@api_node.create('v3/watchfolders', data)['id']} created") }
539
516
  command :list, description: 'List watch folders',
540
- action: ->{Result::ValueList.new(@api_node.read('v3/watchfolders', query_read_delete)['ids'])}
517
+ action: -> { Result::ValueList.new(@api_node.read('v3/watchfolders', query_read_delete)['ids']) }
541
518
  command :show, description: 'Show a watch folder',
542
- arguments: [{name: :res_id, type: :identifier}],
543
- action: ->(res_id:, **){Result::SingleObject.new(@api_node.read("v3/watchfolders/#{res_id}"))}
519
+ arguments: [{name: :watch_folder_id, type: :identifier}],
520
+ action: ->(watch_folder_id:, **) { Result::SingleObject.new(@api_node.read("v3/watchfolders/#{watch_folder_id}")) }
544
521
  command :modify, description: 'Modify a watch folder',
545
- arguments: [{name: :res_id, type: :identifier}, {name: :data, type: Hash}]
522
+ arguments: [{name: :watch_folder_id, type: :identifier}, {name: :data, type: Hash}]
546
523
  command :delete, description: 'Delete a watch folder',
547
- arguments: [{name: :res_id, type: :identifier}]
524
+ arguments: [{name: :watch_folder_id, type: :identifier}]
548
525
  command :state, description: 'Show watch folder state',
549
- arguments: [{name: :res_id, type: :identifier}],
550
- action: ->(res_id:, **){Result::SingleObject.new(@api_node.read("v3/watchfolders/#{res_id}/state"))}
526
+ arguments: [{name: :watch_folder_id, type: :identifier}],
527
+ action: ->(watch_folder_id:, **) { Result::SingleObject.new(@api_node.read("v3/watchfolders/#{watch_folder_id}/state")) }
551
528
  end
552
529
  # central
553
530
  command :central, description: 'Query Central service'
554
- commands_under(:central) do
531
+ commands_under :central do
555
532
  command :session, description: 'Query sessions'
556
533
  command :file, description: 'Query files'
557
534
  end
558
- commands_under(%i[central session]) do
535
+ commands_under %i[central session] do
559
536
  command :list, description: 'List sessions',
560
537
  arguments: [{name: :request_data, type: Hash, mandatory: false, default: nil}]
561
538
  end
562
- commands_under(%i[central file]) do
539
+ commands_under %i[central file] do
563
540
  command :list, description: 'List file transfers',
564
541
  arguments: [{name: :request_data, type: Hash, mandatory: false, default: nil}]
565
542
  command :modify, description: 'Modify file transfer validation',
@@ -567,72 +544,60 @@ module Aspera
567
544
  end
568
545
  # Standalone leaf commands
569
546
  command :asperabrowser, description: 'Open Aspera browser'
570
- command :basic_token, description: 'Generate basic auth token', action: ->{Result::Text.new(Rest.basic_authorization(options.get_option(:username, mandatory: true), options.get_option(:password, mandatory: true)))}
547
+ command :basic_token, description: 'Generate basic auth token', action: -> { Result::Text.new(Rest.basic_authorization(options.get_option(:username, mandatory: true), options.get_option(:password, mandatory: true))) }
571
548
  command :bearer_token, description: 'Generate bearer token',
572
- arguments: [{name: :private_key_pem, type: String}, {name: :user_group_id, type: Hash}]
549
+ arguments: [{name: :private_key_pem, type: String}, {name: :user_group_id, type: Hash, schema: 'opts:components.schemas.NodeBearerTokenOptions'}]
573
550
  command :simulator, description: 'Start node simulator',
574
- arguments: [{name: :parameters, type: Hash, mandatory: false, default: {}}]
551
+ arguments: [{name: :parameters, type: Hash, mandatory: false, default: {}, schema: 'opts:components.schemas.NodeSimulatorOptions'}]
575
552
  command :telemetry, description: 'Report telemetry to external system',
576
- arguments: [{name: :parameters, type: Hash, mandatory: false, default: {}}]
553
+ arguments: [{name: :parameters, type: Hash, mandatory: false, default: {}, schema: 'opts:components.schemas.NodeTelemetryOptions'}]
577
554
 
578
555
  # --- Handler methods (Gen3) ---
579
556
 
580
557
  def action_delete(paths:, **)
581
558
  # TODO: add query for recursive
582
559
  paths_to_delete = Array(paths)
583
- @node_path_prefix&.add_to_paths!(paths_to_delete)
584
- resp = @api_node.create('files/delete', {paths: paths_to_delete.map{ |i| {'path' => i.start_with?('/') ? i : "/#{i}"}}})
560
+ resp = @api_node.create('files/delete', {paths: paths_to_delete.map { |i| {'path' => i.start_with?('/') ? i : "/#{i}"} }})
585
561
  cli_result_from_paths_response(resp, 'file deleted')
586
562
  end
587
563
 
588
564
  def action_search(search_root:, **)
589
- search_root = @node_path_prefix.nil? ? search_root : @node_path_prefix.add_to_path(search_root)
590
565
  parameters = {'path' => search_root}
591
566
  other_options = options.get_option(:query)
592
567
  parameters.merge!(other_options) unless other_options.nil?
593
568
  resp = @api_node.create('files/search', parameters)
594
569
  return Result::Empty.new if resp['items'].empty?
595
- fields = resp['items'].first.keys.reject{ |i| SEARCH_REMOVE_FIELDS.include?(i)}
570
+ fields = resp['items'].first.keys.reject { |i| SEARCH_REMOVE_FIELDS.include?(i) }
596
571
  formatter.display_item_count(resp['item_count'], resp['total_count'])
597
- formatter.display_status("params: #{resp['parameters'].keys.map{ |k| "#{k}:#{resp['parameters'][k]}"}.join(',')}")
598
- @node_path_prefix&.remove_in_object_list!(resp['items'])
572
+ formatter.display_status("params: #{resp['parameters'].keys.map { |k| "#{k}:#{resp['parameters'][k]}" }.join(',')}")
599
573
  Result::ObjectList.new(resp['items'], fields: fields)
600
574
  end
601
575
 
602
576
  def action_space(path_list:, **)
603
577
  path_list = Array(path_list)
604
- @node_path_prefix&.add_to_paths!(path_list)
605
- resp = @api_node.create('space', {'paths' => path_list.map{ |i| {path: i}}})
606
- @node_path_prefix&.remove_in_object_list!(resp['paths'])
578
+ resp = @api_node.create('space', {'paths' => path_list.map { |i| {path: i} }})
607
579
  Result::ObjectList.new(resp['paths'])
608
580
  end
609
581
 
610
582
  def action_mkdir(path_list:, **)
611
583
  path_list = Array(path_list)
612
- @node_path_prefix&.add_to_paths!(path_list)
613
- resp = @api_node.create('files/create', {'paths' => path_list.map{ |i| {type: :directory, path: i}}})
584
+ resp = @api_node.create('files/create', {'paths' => path_list.map { |i| {type: :directory, path: i} }})
614
585
  cli_result_from_paths_response(resp, 'folder created')
615
586
  end
616
587
 
617
588
  def action_mklink(target:, link_path:, **)
618
- target = @node_path_prefix.nil? ? target : @node_path_prefix.add_to_path(target)
619
- one_path = @node_path_prefix.nil? ? link_path : @node_path_prefix.add_to_path(link_path)
620
- resp = @api_node.create('files/create', {'paths' => [{type: :symbolic_link, path: one_path, target: {path: target}}]})
589
+ resp = @api_node.create('files/create', {'paths' => [{type: :symbolic_link, path: link_path, target: {path: target}}]})
621
590
  cli_result_from_paths_response(resp, 'link created')
622
591
  end
623
592
 
624
593
  def action_mkfile(file_path:, contents:, **)
625
- one_path = @node_path_prefix.nil? ? file_path : @node_path_prefix.add_to_path(file_path)
626
594
  contents64 = contents.nil? ? '' : Base64.strict_encode64(contents)
627
- resp = @api_node.create('files/create', {'paths' => [{type: :file, path: one_path, contents: contents64}]})
595
+ resp = @api_node.create('files/create', {'paths' => [{type: :file, path: file_path, contents: contents64}]})
628
596
  cli_result_from_paths_response(resp, 'file created')
629
597
  end
630
598
 
631
599
  def action_rename(path_base:, path_src:, path_dst:, **)
632
600
  # TODO: multiple ?
633
- path_base = @node_path_prefix.nil? ? path_base : @node_path_prefix.add_to_path(path_base)
634
- path_src = @node_path_prefix.nil? ? path_src : @node_path_prefix.add_to_path(path_src)
635
- path_dst = @node_path_prefix.nil? ? path_dst : @node_path_prefix.add_to_path(path_dst)
636
601
  resp = @api_node.create('files/rename', {'paths' => [{'path' => path_base, 'source' => path_src, 'destination' => path_dst}]})
637
602
  cli_result_from_paths_response(resp, 'entry moved')
638
603
  end
@@ -646,14 +611,14 @@ module Aspera
646
611
  'files/sync_setup',
647
612
  {transfer_requests: [{transfer_request: request_transfer_spec}]}
648
613
  )['transfer_specs'].first['transfer_spec']
649
- transfer_spec.delete_if{ |_k, v| v.nil?}
614
+ transfer_spec.delete_if { |_k, v| v.nil? }
650
615
  Log.dump(:ts, transfer_spec)
651
616
  transfer_spec
652
617
  end
653
618
  end
654
619
 
655
620
  Sync::Operations::DIRECTIONS.each do |dir|
656
- define_method(:"action_sync_#{dir}"){ |**| run_sync_transfer(dir, &sync_gen3_block)}
621
+ define_method(:"action_sync_#{dir}") { |path:, sync_info: {}, **| run_sync_transfer(dir, path: path, sync_info: sync_info, &sync_gen3_block) }
657
622
  end
658
623
 
659
624
  def action_upload
@@ -684,7 +649,6 @@ module Aspera
684
649
  end
685
650
 
686
651
  def action_cat(remote_path:, **)
687
- remote_path = @node_path_prefix.nil? ? remote_path : @node_path_prefix.add_to_path(remote_path)
688
652
  http = @api_node.read("files/#{URI.encode_www_form_component(remote_path)}/contents", ret: :resp)
689
653
  Result::Text.new(http.body)
690
654
  end
@@ -727,24 +691,14 @@ module Aspera
727
691
  Result::Status.new('Updated')
728
692
  end
729
693
 
730
- def action_watch_folder_modify(data:, res_id:, **)
731
- @api_node.update("v3/watchfolders/#{res_id}", data)
732
- Result::Status.new("#{res_id} updated")
694
+ def action_watch_folder_modify(data:, watch_folder_id:, **)
695
+ @api_node.update("v3/watchfolders/#{watch_folder_id}", data)
696
+ Result::Status.new("#{watch_folder_id} updated")
733
697
  end
734
698
 
735
- def action_watch_folder_delete(res_id:, **)
736
- @api_node.delete("v3/watchfolders/#{res_id}")
737
- Result::Status.new("#{res_id} deleted")
738
- end
739
-
740
- # access_keys > CRUD
741
- Operations::ALL.each do |op|
742
- define_action_method([:access_keys, op]) do
743
- entity_execute(api: @api_node, entity: 'access_keys', command: op) do |field, value|
744
- Aspera.assert(field.eql?('id') && value.eql?('self'), type: BadArgument){'only selector: %id:self'}
745
- @api_node.read('access_keys/self')['id']
746
- end
747
- end
699
+ def action_watch_folder_delete(watch_folder_id:, **)
700
+ @api_node.delete("v3/watchfolders/#{watch_folder_id}")
701
+ Result::Status.new("#{watch_folder_id} deleted")
748
702
  end
749
703
 
750
704
  # access_keys > do - setup: resolve access key and root file id
@@ -756,7 +710,7 @@ module Aspera
756
710
  ak_info = @api_node.read("access_keys/#{access_key_id}")
757
711
  ak_secret = context.secret_finder.lookup(url: @api_node.base_url, username: ak_info['id'])
758
712
  if !access_key_id.eql?('self')
759
- Aspera.assert(ak_secret, type: Cli::MissingArgument){"Please provide secret for #{ak_info['id']} using option: secret or by setting a preset for #{ak_info['id']}@#{@api_node.base_url}."}
713
+ Aspera.assert(ak_secret, type: Cli::MissingArgument) { "Please provide secret for #{ak_info['id']} using option: secret or by setting a preset for #{ak_info['id']}@#{@api_node.base_url}." }
760
714
  @api_node.auth_params[:username] = ak_info['id']
761
715
  @api_node.auth_params[:password] = ak_secret
762
716
  end
@@ -772,8 +726,8 @@ module Aspera
772
726
  {apifid: apifid_from_next_arg(@do_root_file_id)}
773
727
  end
774
728
 
775
- # access_keys > do > browse
776
- def action_access_keys_do_browse(path:, do_root_file_id:, **)
729
+ # access_keys > do > ls
730
+ def action_access_keys_do_ls(path:, do_root_file_id:, **)
777
731
  apifid = apifid_from_path(do_root_file_id, path)
778
732
  file_info = apifid.node_api.read("files/#{apifid.file_id}", headers: Api::Node.add_cache_control)
779
733
  return Result::ObjectList.new([file_info], fields: GEN4_LS_FIELDS) unless file_info['type'].eql?('folder')
@@ -781,9 +735,9 @@ module Aspera
781
735
  end
782
736
 
783
737
  # access_keys > do > find
784
- def action_access_keys_do_find(path:, do_root_file_id:, **)
738
+ def action_access_keys_do_find(path:, filter: nil, do_root_file_id:, **)
785
739
  apifid = apifid_from_path(do_root_file_id, path)
786
- Result::ObjectList.new(@api_node.find_files(apifid.file_id, Api::Node.file_matcher_from_argument(options)), fields: ['path'])
740
+ Result::ObjectList.new(@api_node.find_files(apifid.file_id, Base.file_matcher(filter)), fields: ['path'])
787
741
  end
788
742
 
789
743
  # access_keys > do > cat
@@ -820,8 +774,7 @@ module Aspera
820
774
 
821
775
  # access_keys > do > delete
822
776
  def action_access_keys_do_delete(paths:, do_root_file_id:, **)
823
- is_bulk = options.get_option(:bulk)
824
- Result.bulk(paths, is_bulk: is_bulk, command: :delete, id_result: 'path', bfail: options.get_option(:bfail)) do |l_path|
777
+ bulk_result(paths, command: :delete, id_result: 'path') do |l_path|
825
778
  apifid = if (m = Parser.percent_selector(l_path))
826
779
  Aspera.assert_values(m[:field], ['id'], type: BadIdentifier)
827
780
  Api::NodeFileId.new(@api_node, m[:value])
@@ -843,8 +796,8 @@ module Aspera
843
796
  end
844
797
 
845
798
  Sync::Operations::DIRECTIONS.each do |dir|
846
- define_method(:"action_access_keys_do_sync_#{dir}") do |do_root_file_id:, **|
847
- run_sync_transfer(dir, &sync_gen4_block(do_root_file_id))
799
+ define_method(:"action_access_keys_do_sync_#{dir}") do |path:, sync_info: {}, do_root_file_id:, **|
800
+ run_sync_transfer(dir, path: path, sync_info: sync_info, &sync_gen4_block(do_root_file_id))
848
801
  end
849
802
  end
850
803
 
@@ -884,7 +837,7 @@ module Aspera
884
837
  when :oauth2
885
838
  result[:username] = apifid.node_api.params[:headers][Api::Node::HEADER_X_ASPERA_ACCESS_KEY]
886
839
  result[:password] = apifid.node_api.oauth.authorization
887
- else Aspera.error_unexpected_value(apifid.node_api.auth_params[:type]){'Node API Auth type'}
840
+ else Aspera.error_unexpected_value(apifid.node_api.auth_params[:type]) { 'Node API Auth type' }
888
841
  end
889
842
  [apifid, result]
890
843
  end
@@ -897,7 +850,7 @@ module Aspera
897
850
  def action_access_keys_do_bearer_token_node(path:, do_root_file_id:, **)
898
851
  apifid, result = gen4_apifid_info(do_root_file_id, path)
899
852
  Log.dump(:result, result)
900
- Aspera.assert(apifid.node_api.auth_params[:type].eql?(:oauth2), type: BadArgument){"Cannot get bearer token if authenticating with secret (#{apifid.node_api.auth_params[:type]})"}
853
+ Aspera.assert(apifid.node_api.auth_params[:type].eql?(:oauth2), type: BadArgument) { "Cannot get bearer token if authenticating with secret (#{apifid.node_api.auth_params[:type]})" }
901
854
  Aspera.assert(OAuth::Factory.bearer_auth?(result[:password]), 'Not using bearer token auth')
902
855
  Result::Text.new(result[:password])
903
856
  end
@@ -924,8 +877,8 @@ module Aspera
924
877
  if check_exists
925
878
  folder_content = apifid.node_api.read("files/#{apifid.file_id}/files")
926
879
  link_name = ".#{new_item}.asp-lnk"
927
- found = folder_content.find{ |i| i['name'].eql?(new_item) || i['name'].eql?(link_name)}
928
- Aspera.assert(!found, type: Cli::Error){"A #{found['type']} already exists with name #{new_item}"}
880
+ found = folder_content.find { |i| i['name'].eql?(new_item) || i['name'].eql?(link_name) }
881
+ Aspera.assert(!found, type: Cli::Error) { "A #{found['type']} already exists with name #{new_item}" }
929
882
  end
930
883
  [apifid, payload]
931
884
  end
@@ -961,9 +914,7 @@ module Aspera
961
914
  end
962
915
 
963
916
  def action_access_keys_do_permission_delete(perm_id:, apifid:, **)
964
- is_bulk = options.get_option(:bulk)
965
- items = perm_id.is_a?(Array) ? perm_id : [perm_id]
966
- Result.bulk(items, is_bulk: is_bulk, command: :delete, bfail: options.get_option(:bfail)) do |one_id|
917
+ bulk_result(perm_id, command: :delete) do |one_id|
967
918
  apifid.node_api.delete("permissions/#{one_id}")
968
919
  the_app = apifid.node_api.app_info
969
920
  the_app&.api&.permissions_send_event(event_data: {}, app_info: the_app, types: ['permission.deleted'])
@@ -973,7 +924,7 @@ module Aspera
973
924
 
974
925
  def action_access_keys_do_permission_create(data:, apifid:, **)
975
926
  create_param = data
976
- Aspera.assert(!create_param.key?('file_id'), type: Cli::BadArgument){'no file_id'}
927
+ Aspera.assert(!create_param.key?('file_id'), type: Cli::BadArgument) { 'no file_id' }
977
928
  create_param['file_id'] = apifid.file_id
978
929
  create_param['access_levels'] = Api::Node::ACCESS_LEVELS unless create_param.key?('access_levels')
979
930
  the_app = apifid.node_api.app_info
@@ -1036,7 +987,7 @@ module Aspera
1036
987
  data: iteration_data,
1037
988
  id: IdGenerator.from_list('sync_files', options.get_option(:url, mandatory: true), options.get_option(:username, mandatory: true), async_id)
1038
989
  )
1039
- data.select!{ |l| l['fnid'].to_i > iteration_data.first} unless iteration_data.first.nil?
990
+ data.select! { |l| l['fnid'].to_i > iteration_data.first } unless iteration_data.first.nil?
1040
991
  iteration_data[0] = data.last['fnid'].to_i unless data.empty?
1041
992
  end
1042
993
  return Result::Empty.new if data.empty?
@@ -1051,15 +1002,6 @@ module Aspera
1051
1002
  Result::SingleObject.new(resp)
1052
1003
  end
1053
1004
 
1054
- def action_ssync_delete(ssync_id:, **)
1055
- is_bulk = options.get_option(:bulk)
1056
- items = ssync_id.is_a?(Array) ? ssync_id : [ssync_id]
1057
- Result.bulk(items, is_bulk: is_bulk, command: :delete, bfail: options.get_option(:bfail)) do |one_id|
1058
- @api_node.delete("asyncs/#{one_id}", query_read_delete)
1059
- {'id' => one_id}
1060
- end
1061
- end
1062
-
1063
1005
  def action_ssync_start(ssync_id:, **)
1064
1006
  @api_node.call(operation: 'POST', subpath: "asyncs/#{ssync_id}/start", content_type: Mime::TEXT, body: '', ret: :resp).body
1065
1007
  Result::Status.new('Done')
@@ -1072,7 +1014,7 @@ module Aspera
1072
1014
 
1073
1015
  # transfer sub-commands
1074
1016
  def action_transfer_list
1075
- transfer_filter = query_read_delete(default: {})
1017
+ transfer_filter = query_read_delete(default: {}, schema: Schema::Registry.query_params(Schema::Registry::NODE, 'ops/transfers'))
1076
1018
  iteration_persistency = nil
1077
1019
  if options.get_option(:once_only, mandatory: true)
1078
1020
  iteration_persistency = PersistencyActionOnce.new(
@@ -1094,8 +1036,8 @@ module Aspera
1094
1036
  end
1095
1037
 
1096
1038
  def action_transfer_sessions
1097
- transfers_data = @api_node.read('ops/transfers', query_read_delete)
1098
- sessions = transfers_data.flat_map{ |t| t['sessions']}
1039
+ transfers_data = @api_node.read('ops/transfers', query_read_delete(schema: Schema::Registry.query_params(Schema::Registry::NODE, 'ops/transfers')))
1040
+ sessions = transfers_data.flat_map { |t| t['sessions'] }
1099
1041
  sessions.each do |session|
1100
1042
  SESSION_TIME_FIELDS.each do |what|
1101
1043
  session["#{what}_time"] = session["#{what}_time_usec"] ? Time.at(session["#{what}_time_usec"] / 1_000_000.0).utc.iso8601(0) : nil
@@ -1115,7 +1057,7 @@ module Aspera
1115
1057
  end
1116
1058
 
1117
1059
  def action_transfer_bandwidth_average
1118
- transfers_data = @api_node.read('ops/transfers', query_read_delete)
1060
+ transfers_data = @api_node.read('ops/transfers', query_read_delete(schema: Schema::Registry.query_params(Schema::Registry::NODE, 'ops/transfers')))
1119
1061
  bandwidth_period = {}
1120
1062
  dir_info = %i[avg_kbps sessions].freeze
1121
1063
  transfers_data.each do |t|
@@ -1129,7 +1071,7 @@ module Aspera
1129
1071
  end_date = all_dates[index + 1]
1130
1072
  break if end_date.nil?
1131
1073
  period_bandwidth = Transfer::Spec::DIRECTION_ENUM_VALUES.map(&:to_sym).to_h do |dir|
1132
- [dir, dir_info.to_h{ |k2| [k2, 0]}]
1074
+ [dir, dir_info.to_h { |k2| [k2, 0] }]
1133
1075
  end
1134
1076
  transfers_data.each do |t|
1135
1077
  next if t['avg_rate_kbps'].zero?
@@ -1138,7 +1080,7 @@ module Aspera
1138
1080
  info[:avg_kbps] += t['avg_rate_kbps']
1139
1081
  info[:sessions] += 1
1140
1082
  end
1141
- next if Transfer::Spec::DIRECTION_ENUM_VALUES.map(&:to_sym).all?{ |dir| period_bandwidth[dir][:sessions].zero?}
1083
+ next if Transfer::Spec::DIRECTION_ENUM_VALUES.map(&:to_sym).all? { |dir| period_bandwidth[dir][:sessions].zero? }
1142
1084
  result.push({start: Time.at(start_date / 1_000_000), end: Time.at(end_date / 1_000_000)}.merge(period_bandwidth))
1143
1085
  end
1144
1086
  Result::ObjectList.new(result)
@@ -1211,7 +1153,7 @@ module Aspera
1211
1153
  def action_simulator(parameters: {}, **)
1212
1154
  require 'aspera/node_simulator'
1213
1155
  parameters = parameters.symbolize_keys
1214
- uri = URI.parse(parameters.delete(:url){WebServerSimple::DEFAULT_URL})
1156
+ uri = URI.parse(parameters.delete(:url) { WebServerSimple::DEFAULT_URL })
1215
1157
  server = WebServerSimple.new(uri, **parameters.slice(*WebServerSimple::PARAMS))
1216
1158
  server.mount(uri.path, NodeSimulatorServlet, parameters.except(*WebServerSimple::PARAMS), NodeSimulator.new)
1217
1159
  server.start
@@ -1221,13 +1163,13 @@ module Aspera
1221
1163
  def action_telemetry(parameters: {}, **)
1222
1164
  parameters = parameters.symbolize_keys
1223
1165
  %i[url key].each do |psym|
1224
- Aspera.assert(parameters.key?(psym), type: Cli::BadArgument){"Missing parameter: #{psym}"}
1166
+ Aspera.assert(parameters.key?(psym), type: Cli::BadArgument) { "Missing parameter: #{psym}" }
1225
1167
  end
1226
1168
  require 'socket'
1227
1169
  parameters[:interval] = 10 unless parameters.key?(:interval)
1228
1170
  parameters[:hostname] = Socket.gethostname unless parameters.key?(:hostname)
1229
1171
  interval = parameters[:interval].to_f
1230
- Aspera.assert(interval > 0, type: Cli::BadArgument){'Interval must be a positive number in seconds'}
1172
+ Aspera.assert(interval > 0, type: Cli::BadArgument) { 'Interval must be a positive number in seconds' }
1231
1173
  otel_api = Rest.new(
1232
1174
  base_url: "#{parameters[:url]}/v1",
1233
1175
  headers: {
@@ -1294,16 +1236,19 @@ module Aspera
1294
1236
  end
1295
1237
  end
1296
1238
 
1297
- # Dispatch a command that has already been read from the argument stream.
1298
- # Used when a new Node instance is created and the command was already consumed
1299
- # (e.g., :v3 delegation, shares, cos, faspex).
1300
- # Re-enters the DSL registry for all commands so Proc handlers and sub-trees
1239
+ # Dispatch a Gen3 command that has already been read from the argument stream.
1240
+ # Used when a new Node instance is created by an external plugin (shares, faspex, cos, :v3)
1241
+ # and the command symbol was already consumed before the Node was instantiated.
1242
+ # Re-enters the DSL registry so Proc actions and intermediate nodes (sync, access_keys)
1301
1243
  # are resolved normally.
1244
+ # Pre-resolved CLI arguments (e.g. path:) can be forwarded via +ctx+ to avoid
1245
+ # re-consuming tokens that were already resolved by the calling plugin's DSL.
1302
1246
  # @param command [Symbol] command already consumed from the argument stream
1247
+ # @param ctx [Hash] pre-resolved context (keyword args forwarded to dispatch)
1303
1248
  # @return [Object] CLI result
1304
- def dispatch_v3_command(command)
1305
- Aspera.assert_values(command, COMMANDS_GEN3 + %i[health events info slash license api_details access_keys transfer]){'v3 command'}
1306
- dispatch_from_registry([command], {})
1249
+ def dispatch_v3_command(command, **ctx)
1250
+ Aspera.assert_values(command, COMMANDS_GEN3 + %i[health events info slash license api_details access_keys transfer]) { 'v3 command' }
1251
+ dispatch_from_registry([command], ctx)
1307
1252
  end
1308
1253
 
1309
1254
  private
@@ -1317,21 +1262,20 @@ module Aspera
1317
1262
  response['paths'].each do |p|
1318
1263
  result = success_msg
1319
1264
  if p.key?('error')
1320
- Log.log.error{"#{p['error']['user_message']} : #{p['path']}"}
1265
+ Log.log.error { "#{p['error']['user_message']} : #{p['path']}" }
1321
1266
  result = p['error']['user_message']
1322
1267
  errors.push([p['path'], p['error']['user_message']])
1323
1268
  end
1324
1269
  obj_list.push({'path' => p['path'], 'result' => result})
1325
1270
  end
1326
1271
  # one error make all fail
1327
- raise errors.map{ |i| "#{i.first}: #{i.last}"}.join(', ') unless errors.empty?
1272
+ raise errors.map { |i| "#{i.first}: #{i.last}" }.join(', ') unless errors.empty?
1328
1273
  obj_list
1329
1274
  end
1330
1275
 
1331
1276
  # Translates paths results into CLI result, and removes prefix
1332
1277
  def cli_result_from_paths_response(response, success_msg)
1333
1278
  obj_list = response_to_result(response, success_msg)
1334
- @node_path_prefix&.remove_in_object_list!(obj_list)
1335
1279
  return Result::ObjectList.new(obj_list, fields: %w[path result])
1336
1280
  end
1337
1281
  end