aspera-cli 4.27.2 → 4.27.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +133 -0
  4. data/CONTRIBUTING.md +5 -2
  5. data/bin/ascli +3 -1
  6. data/docs/README.md +1006 -766
  7. data/lib/aspera/agent/base.rb +7 -2
  8. data/lib/aspera/agent/connect.rb +6 -8
  9. data/lib/aspera/agent/desktop.rb +2 -6
  10. data/lib/aspera/agent/direct.rb +52 -22
  11. data/lib/aspera/agent/node.rb +9 -8
  12. data/lib/aspera/agent/transferd.rb +2 -2
  13. data/lib/aspera/api/alee.rb +1 -1
  14. data/lib/aspera/api/aoc.rb +14 -12
  15. data/lib/aspera/api/ats.rb +1 -1
  16. data/lib/aspera/api/cos_node.rb +2 -2
  17. data/lib/aspera/api/faspex.rb +11 -7
  18. data/lib/aspera/api/httpgw.rb +38 -35
  19. data/lib/aspera/api/node.rb +39 -34
  20. data/lib/aspera/ascmd.rb +3 -1
  21. data/lib/aspera/ascp/installation.rb +63 -28
  22. data/lib/aspera/ascp/management.rb +1 -0
  23. data/lib/aspera/assert.rb +4 -0
  24. data/lib/aspera/cli/ascp_actions.rb +20 -41
  25. data/lib/aspera/cli/async_transfer_store.rb +12 -11
  26. data/lib/aspera/cli/bootstrapper.rb +14 -16
  27. data/lib/aspera/cli/command_line.rb +252 -0
  28. data/lib/aspera/cli/command_registry.rb +215 -37
  29. data/lib/aspera/cli/command_spec.rb +104 -15
  30. data/lib/aspera/cli/completion/ascli.bash +12 -0
  31. data/lib/aspera/cli/completion/ascli.fish +16 -0
  32. data/lib/aspera/cli/completion/ascli.zsh +19 -0
  33. data/lib/aspera/cli/context.rb +3 -0
  34. data/lib/aspera/cli/deprecation.rb +37 -0
  35. data/lib/aspera/cli/extended_value.rb +6 -3
  36. data/lib/aspera/cli/formatter.rb +94 -80
  37. data/lib/aspera/cli/gem_checker.rb +1 -1
  38. data/lib/aspera/cli/hints.rb +7 -6
  39. data/lib/aspera/cli/http.rb +22 -34
  40. data/lib/aspera/cli/info.rb +3 -0
  41. data/lib/aspera/cli/mcp_tool.rb +47 -83
  42. data/lib/aspera/cli/option_declarator.rb +33 -42
  43. data/lib/aspera/cli/option_registry.rb +69 -0
  44. data/lib/aspera/cli/option_types.rb +105 -0
  45. data/lib/aspera/cli/option_value.rb +278 -0
  46. data/lib/aspera/cli/options.schema.yaml +124 -15
  47. data/lib/aspera/cli/parser.rb +333 -862
  48. data/lib/aspera/cli/plugins/alee.rb +7 -4
  49. data/lib/aspera/cli/plugins/aoc.rb +545 -518
  50. data/lib/aspera/cli/plugins/ats.rb +59 -80
  51. data/lib/aspera/cli/plugins/base.rb +221 -265
  52. data/lib/aspera/cli/plugins/basic_auth.rb +2 -10
  53. data/lib/aspera/cli/plugins/config.rb +263 -184
  54. data/lib/aspera/cli/plugins/console.rb +103 -39
  55. data/lib/aspera/cli/plugins/cos.rb +6 -23
  56. data/lib/aspera/cli/plugins/factory.rb +3 -0
  57. data/lib/aspera/cli/plugins/faspex5.rb +204 -182
  58. data/lib/aspera/cli/plugins/faspio.rb +6 -11
  59. data/lib/aspera/cli/plugins/httpgw.rb +8 -11
  60. data/lib/aspera/cli/plugins/mcp.rb +20 -55
  61. data/lib/aspera/cli/plugins/node.rb +300 -327
  62. data/lib/aspera/cli/plugins/orchestrator.rb +152 -110
  63. data/lib/aspera/cli/plugins/preview.rb +96 -105
  64. data/lib/aspera/cli/plugins/server.rb +78 -53
  65. data/lib/aspera/cli/plugins/shares.rb +80 -131
  66. data/lib/aspera/cli/preset_actions.rb +44 -27
  67. data/lib/aspera/cli/preset_manager.rb +44 -19
  68. data/lib/aspera/cli/prompt.rb +36 -0
  69. data/lib/aspera/cli/result.rb +42 -36
  70. data/lib/aspera/cli/runner.rb +32 -59
  71. data/lib/aspera/cli/special_values.rb +5 -0
  72. data/lib/aspera/cli/sync_actions.rb +51 -46
  73. data/lib/aspera/cli/terminal_formatter.rb +9 -3
  74. data/lib/aspera/cli/transfer_actions.rb +14 -9
  75. data/lib/aspera/cli/transfer_agent.rb +34 -38
  76. data/lib/aspera/cli/transfer_progress.rb +290 -55
  77. data/lib/aspera/cli/vault_manager.rb +0 -17
  78. data/lib/aspera/cli/version.rb +1 -1
  79. data/lib/aspera/cli/wizard.rb +5 -3
  80. data/lib/aspera/coverage.rb +1 -1
  81. data/lib/aspera/environment.rb +35 -5
  82. data/lib/aspera/faspex_gw.rb +2 -1
  83. data/lib/aspera/faspex_postproc.rb +1 -0
  84. data/lib/aspera/graphql.rb +5 -5
  85. data/lib/aspera/json_rpc/client.rb +5 -5
  86. data/lib/aspera/keychain/encrypted_hash.rb +2 -2
  87. data/lib/aspera/keychain/factory.rb +2 -1
  88. data/lib/aspera/keychain/one_password_api.rb +1 -1
  89. data/lib/aspera/link_header.rb +2 -2
  90. data/lib/aspera/log.rb +47 -27
  91. data/lib/aspera/markdown.rb +2 -0
  92. data/lib/aspera/mime.rb +25 -0
  93. data/lib/aspera/node_emulator.rb +759 -0
  94. data/lib/aspera/oauth/base.rb +37 -26
  95. data/lib/aspera/oauth/factory.rb +7 -3
  96. data/lib/aspera/oauth/generic.rb +1 -1
  97. data/lib/aspera/oauth/json_credentials.rb +34 -0
  98. data/lib/aspera/oauth/jwt.rb +4 -5
  99. data/lib/aspera/oauth/web.rb +9 -8
  100. data/lib/aspera/oauth.rb +1 -0
  101. data/lib/aspera/persistency_folder.rb +1 -3
  102. data/lib/aspera/preview/file_types.rb +4 -4
  103. data/lib/aspera/preview/generator.rb +11 -1
  104. data/lib/aspera/preview/options.schema.yaml +119 -0
  105. data/lib/aspera/preview/terminal.rb +4 -3
  106. data/lib/aspera/preview/utils.rb +9 -6
  107. data/lib/aspera/products/connect.rb +1 -1
  108. data/lib/aspera/rainbow.rb +7 -0
  109. data/lib/aspera/rest/aspera_errors.rb +72 -0
  110. data/lib/aspera/rest/call_error.rb +27 -0
  111. data/lib/aspera/rest/client.rb +523 -0
  112. data/lib/aspera/rest/error_analyzer.rb +113 -0
  113. data/lib/aspera/rest/list.rb +149 -0
  114. data/lib/aspera/rest/parameters.rb +55 -0
  115. data/lib/aspera/rest/util.rb +176 -0
  116. data/lib/aspera/rest.rb +7 -621
  117. data/lib/aspera/schema/IBM Aspera Console-enhanced.yaml +1125 -0
  118. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +0 -20
  119. data/lib/aspera/schema/IBM Aspera Orchestrator API-v1.yaml +1784 -0
  120. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1230 -137
  121. data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +2395 -0
  122. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +14 -13
  123. data/lib/aspera/schema/documentation.rb +13 -3
  124. data/lib/aspera/schema/reader.rb +12 -18
  125. data/lib/aspera/schema/registry.rb +23 -1
  126. data/lib/aspera/schema/validator.rb +92 -0
  127. data/lib/aspera/secret_hider.rb +36 -25
  128. data/lib/aspera/string_ext.rb +15 -0
  129. data/lib/aspera/temp_file_manager.rb +6 -5
  130. data/lib/aspera/transfer/parameters.rb +2 -0
  131. data/lib/aspera/transfer/spec.rb +1 -0
  132. data/lib/aspera/uri_reader.rb +11 -11
  133. data/lib/aspera/web_auth/index.html +147 -0
  134. data/lib/aspera/web_auth/server.rb +81 -0
  135. data.tar.gz.sig +0 -0
  136. metadata +43 -9
  137. metadata.gz.sig +0 -0
  138. data/lib/aspera/colors.rb +0 -79
  139. data/lib/aspera/node_simulator.rb +0 -344
  140. data/lib/aspera/preview/options.rb +0 -45
  141. data/lib/aspera/rest_call_error.rb +0 -25
  142. data/lib/aspera/rest_error_analyzer.rb +0 -111
  143. data/lib/aspera/rest_errors_aspera.rb +0 -58
  144. data/lib/aspera/rest_list.rb +0 -136
  145. data/lib/aspera/web_auth.rb +0 -211
@@ -11,8 +11,7 @@ require 'aspera/hash_ext'
11
11
  require 'aspera/id_generator'
12
12
  require 'aspera/api/node'
13
13
  require 'aspera/oauth'
14
- require 'aspera/node_simulator'
15
- require 'aspera/rest_list'
14
+ require 'aspera/rest/list'
16
15
  require 'aspera/assert'
17
16
  require 'base64'
18
17
  require 'zlib'
@@ -47,7 +46,7 @@ module Aspera
47
46
  error = nil
48
47
  urls.each do |base_url|
49
48
  next unless base_url.match?(%r{^https?://})
50
- api = Rest.new(base_url: base_url)
49
+ api = Rest::Client.new(base_url: base_url)
51
50
  test_endpoint = 'ping'
52
51
  http = api.read(test_endpoint, ret: :resp)
53
52
  next unless http.body.eql?('')
@@ -76,9 +75,9 @@ module Aspera
76
75
  option :validator, description: 'Identifier of validator (optional for central)'
77
76
  option :asperabrowserurl, description: 'URL for simple aspera web ui', default: 'https://asperabrowser.mybluemix.net'
78
77
  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',
79
- allowed: Hash, handler: {o: Api::Node, m: :api_options}
78
+ allowed: Hash, on_set: Api::Node.method(:api_options=)
80
79
  option :root_id, description: 'Gen4: File id of top folder when using access key (override AK root id)'
81
- option :dynamic_key, description: 'Private key PEM to use for dynamic key auth', handler: {o: Api::Node, m: :use_dynamic_key}
80
+ option :dynamic_key, description: 'Private key PEM to use for dynamic key auth', on_set: Api::Node.method(:use_dynamic_key=)
82
81
 
83
82
  # @param wizard [Wizard] The wizard object
84
83
  # @param app_url [String] Tested URL
@@ -106,25 +105,21 @@ module Aspera
106
105
  # Fields removed in result of search
107
106
  SEARCH_REMOVE_FIELDS = %w[basename permissions].freeze
108
107
 
109
- # Actions in execute_command_gen3
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.
108
+ # DSL metadata for Gen3 root commands (description:, arguments:, transfer_paths:, aliases:).
109
+ # :access_keys is skipped (intermediate node declared separately), :sync is declared separately.
110
+ # action: entries are in GEN3_NODE_ACTIONS, or implicit.
116
111
  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}]},
112
+ search: {description: 'Search for files', arguments: [{name: :path}]},
113
+ space: {description: 'Show space information', arguments: [{name: :paths, multiple: true}]},
114
+ mkdir: {description: 'Create a folder (Gen3)', arguments: [{name: :paths, multiple: true}]},
115
+ mklink: {description: 'Create a symbolic link (Gen3)', arguments: [{name: :target}, {name: :link_path}]},
116
+ mkfile: {description: 'Create a file (Gen3)', arguments: [{name: :path}, {name: :contents, mandatory: false, default: nil}]},
117
+ rename: {description: 'Rename a file or folder (Gen3)', arguments: [{name: :folder}, {name: :source}, {name: :destination}]},
123
118
  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]},
119
+ ls: {description: 'List files (Gen3)', arguments: [{name: :path}], aliases: [:browse]},
125
120
  upload: {description: 'Upload files (Gen3)', transfer_paths: :send},
126
121
  download: {description: 'Download files (Gen3)', transfer_paths: :receive},
127
- cat: {description: 'Show file contents (Gen3)', arguments: [{name: :remote_path, type: String}]},
122
+ cat: {description: 'Show file contents (Gen3)', arguments: [{name: :path}]},
128
123
  transport: {description: 'Show transport parameters'},
129
124
  spec: {description: 'Show transfer spec base'},
130
125
  api_details: {description: 'Show API details'},
@@ -136,60 +131,52 @@ module Aspera
136
131
  access_keys: {description: 'Manage access keys'}
137
132
  }.freeze
138
133
 
139
- BASE_ACTIONS = (%i[api_details] + COMMANDS_GEN3).freeze
140
-
141
- SPECIAL_ACTIONS = %i[health events info slash license].freeze
142
-
143
- # commands for : `execute_simple_common`: actions used commonly when a node is involved
144
- COMMON_ACTIONS = (%i[access_keys] + BASE_ACTIONS + SPECIAL_ACTIONS).freeze
134
+ private_constant :CENTRAL_SOAP_API_TEST, :SEARCH_REMOVE_FIELDS
145
135
 
146
- private_constant :CENTRAL_SOAP_API_TEST, :SEARCH_REMOVE_FIELDS, :BASE_ACTIONS, :SPECIAL_ACTIONS, :COMMON_ACTIONS
147
-
148
- # used in aoc
136
+ # Gen4 read commands also exposed on AoC packages (`aoc packages ls <id>`)
149
137
  NODE4_READ_ACTIONS = %i[bearer_token_node node_info ls find].freeze
150
138
 
151
- # commands for execute_command_gen4
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
139
+ # DSL metadata for Gen4 commands under `access_keys do` (description:, arguments:, transfer_paths:, aliases:).
140
+ # Other plugins expose this sub-tree with `mount:` (aoc, ats).
141
+ # :sync, :permission and :v3 are excluded: they are intermediate nodes declared separately.
142
+ SINGLE_PATH_ARG = [{name: :path}].freeze
158
143
  COMMANDS_GEN4_SPEC = {
159
144
  mkdir: {description: 'Create folder', arguments: SINGLE_PATH_ARG},
160
145
  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}]},
146
+ mkfile: {description: 'Create file', arguments: [{name: :path}, {name: :contents, mandatory: false, default: nil}]},
147
+ rename: {description: 'Rename entry', arguments: [{name: :source_path}, {name: :new_name}]},
148
+ delete: {description: 'Delete entry', arguments: [{name: :path, bulk: true}]},
164
149
  upload: {description: 'Upload files', transfer_paths: :send},
165
150
  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'}]},
151
+ modify: {description: 'Modify file', arguments: [{name: :path}, {name: :file, type: Hash, schema: 'node:components.schemas.files-id-put-request'}]},
167
152
  cat: {description: 'Show file contents', arguments: SINGLE_PATH_ARG},
168
153
  show: {description: 'Show file info', arguments: SINGLE_PATH_ARG},
169
154
  thumbnail: {description: 'Show file thumbnail', arguments: SINGLE_PATH_ARG},
170
155
  bearer_token_node: {description: 'Show bearer token for file node', arguments: SINGLE_PATH_ARG},
171
156
  node_info: {description: 'Show node info for file', arguments: SINGLE_PATH_ARG},
172
157
  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'}
158
+ find: {description: 'Find files', arguments: SINGLE_PATH_ARG + FILTER_ARGS}
175
159
  }.freeze
176
160
  private_constant :SINGLE_PATH_ARG
177
161
 
178
- # commands supported in ATS for COS
162
+ # Root commands exposed by `cos node` and `shares files` (mount: only:)
179
163
  COMMANDS_COS = %i[upload download info access_keys api_details transfer].freeze
180
- COMMANDS_SHARES = (BASE_ACTIONS - %i[search]).freeze
164
+ COMMANDS_SHARES = %i[api_details space mkdir mklink mkfile rename delete ls upload download cat sync transport spec].freeze
181
165
  # `browse` display fields for gen4
182
166
  GEN4_LS_FIELDS = %w[name type recursive_size size modified_time access_level].freeze
183
167
 
184
- # @param api [Rest] an existing API object for the Node API
168
+ # @param api [Rest::Client] an existing API object for the Node API
185
169
  def initialize(context:, api: nil)
186
- super(context: context, basic_options: api.nil?)
187
- return if context.only_manual?
188
- @api_node =
189
- if !api.nil?
190
- # this can be Api::Node or Rest (Shares)
191
- api
192
- elsif OAuth::Factory.bearer_auth?(options.get_option(:password, mandatory: true))
170
+ super(context: context)
171
+ # Api::Node or Rest::Client (Shares), when provided by the host plugin
172
+ @api_node = api
173
+ end
174
+
175
+ # Node API: provided by the host plugin, or built from CLI options on first use.
176
+ # @return [Api::Node, Rest::Client]
177
+ def api_node
178
+ @api_node ||=
179
+ if OAuth::Factory.bearer_auth?(options.get_option(:password, mandatory: true))
193
180
  # info is provided like node_info of aoc
194
181
  Api::Node.new(
195
182
  base_url: options.get_option(:url, mandatory: true),
@@ -213,9 +200,10 @@ module Aspera
213
200
  def browse_gen3(path)
214
201
  folders_to_process = path
215
202
  folders_to_process = [folders_to_process]
203
+ # not a URL query: merged into the POST body of files/browse
216
204
  query = options.get_option(:query) || {}
217
205
  # special parameter: max number of entries in result
218
- max_items = query.delete(RestList::MAX_ITEMS)
206
+ max_items = query.delete(Rest::List::MAX_ITEMS)
219
207
  # special parameter: recursive browsing
220
208
  recursive = query.delete('recursive')
221
209
  # special parameter: only return one entry for the path, even if folder
@@ -233,7 +221,7 @@ module Aspera
233
221
  total_count = nil
234
222
  loop do
235
223
  # example: send_result={'items'=>[{'file'=>"filename1","permissions"=>[{'name'=>'read'},{'name'=>'write'}]}]}
236
- response = @api_node.create('files/browse', query)
224
+ response = api_node.create('files/browse', query)
237
225
  # 'file','symbolic_link'
238
226
  return Result::SingleObject.new(response['self']) if !Node.gen3_entry_folder?(response['self']) || only_path
239
227
  items = response['items']
@@ -251,13 +239,13 @@ module Aspera
251
239
  break if all_items.count >= total_count
252
240
  offset += items.count
253
241
  query['skip'] = offset
254
- RestParameters.instance.spinner_cb.call(all_items.count)
242
+ Rest::Parameters.instance.spinner_cb.call(all_items.count)
255
243
  end
256
244
  query.delete('skip')
257
245
  end
258
246
  return Result::ObjectList.new(all_items)
259
247
  ensure
260
- RestParameters.instance.spinner_cb.call(action: :success)
248
+ Rest::Parameters.instance.spinner_cb.call(action: :success)
261
249
  end
262
250
 
263
251
  # Create async transfer spec request from direction and folders
@@ -299,20 +287,9 @@ module Aspera
299
287
  if (m = Parser.percent_selector(path))
300
288
  Aspera.assert_values(m[:field], ['id'], type: BadArgument) { 'file id' }
301
289
  val = m[:value]
302
- return Api::NodeFileId.new(@api_node, val.nil? || val.empty? ? top_file_id : val)
290
+ return Api::NodeFileId.new(api_node, val.nil? || val.empty? ? top_file_id : val)
303
291
  end
304
- @api_node.resolve_api_fid(top_file_id, path)
305
- end
306
-
307
- # Legacy helper: resolve a NodeFileId by reading path from the CLI argument stream.
308
- # Used only by setup_access_key_do_permission and action_access_keys_do_permission
309
- # (intermediate nodes whose path cannot yet be injected via execute_leaf).
310
- def apifid_from_next_arg(top_file_id)
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' }
313
- return Api::NodeFileId.new(@api_node, value.nil? || value.empty? ? top_file_id : value)
314
- end
315
- @api_node.resolve_api_fid(top_file_id, file_path)
292
+ api_node.resolve_api_fid(top_file_id, path)
316
293
  end
317
294
 
318
295
  # Search /async by name
@@ -322,8 +299,8 @@ module Aspera
322
299
  # @raise [Cli::BadArgument] if no such sync, or not by name
323
300
  def async_lookup(field, value)
324
301
  Aspera.assert_values(field, ['name'], type: Cli::BadArgument) { 'search field' }
325
- async_ids = @api_node.read('async/list')['sync_ids']
326
- summaries = @api_node.create('async/summary', {'syncs' => async_ids})['sync_summaries']
302
+ async_ids = api_node.read('async/list')['sync_ids']
303
+ summaries = api_node.create('async/summary', {'syncs' => async_ids})['sync_summaries']
327
304
  selected = summaries.find { |s| s['name'].eql?(value) }
328
305
  raise Cli::BadIdentifier.new('sync', value, field: field) if selected.nil?
329
306
  return selected['snid']
@@ -335,7 +312,7 @@ module Aspera
335
312
  # @return [String] the resolved access key id
336
313
  def lookup_access_key_self_id(field, value, **)
337
314
  Aspera.assert(field.eql?('id') && value.eql?('self'), type: BadArgument) { 'only selector: %id:self' }
338
- @api_node.read('access_keys/self')['id']
315
+ api_node.read('access_keys/self')['id']
339
316
  end
340
317
 
341
318
  # Search /asyncs by name
@@ -345,8 +322,8 @@ module Aspera
345
322
  # @raise [Cli::BadArgument] if no such sync, or not by name
346
323
  def ssync_lookup(field, value)
347
324
  Aspera.assert_values(field, ['name'], type: Cli::BadArgument) { 'search field' }
348
- @api_node.read('asyncs')['ids'].each do |id|
349
- sync_info = @api_node.read("asyncs/#{id}")['configuration']
325
+ api_node.read('asyncs')['ids'].each do |id|
326
+ sync_info = api_node.read("asyncs/#{id}")['configuration']
350
327
  # name is unique, so we can return
351
328
  return id if sync_info[field].eql?(value)
352
329
  end
@@ -359,17 +336,16 @@ module Aspera
359
336
  # :sync is declared separately (intermediate node with sub-commands).
360
337
  GEN3_NODE_ACTIONS = {
361
338
  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')) }
339
+ transport: ->(**) { Result::SingleObject.new(api_node.transport_params) },
340
+ spec: ->(**) { Result::SingleObject.new(api_node.base_spec, fields: Formatter.all_but(*Transfer::Spec::SPECIFIC)) },
341
+ api_details: ->(**) { Result::SingleObject.new({base_url: api_node.base_url}.merge(api_node.params)) },
342
+ 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') }) },
343
+ info: ->(**) { Result::SingleObject.new(api_node.read('info')) },
344
+ slash: ->(**) { Result::SingleObject.new(api_node.read('')) },
345
+ license: ->(**) { Result::SingleObject.new(api_node.read('license')) }
369
346
  }.freeze
370
347
  private_constant :GEN3_NODE_ACTIONS
371
348
  COMMANDS_GEN3_SPEC.each do |cmd, spec|
372
- next if cmd.eql?(:sync) # intermediate node with sub-commands
373
349
  next if cmd.eql?(:access_keys) # intermediate node declared separately below
374
350
  action = GEN3_NODE_ACTIONS[cmd]
375
351
  command cmd, **spec, **(action ? {action: action} : {})
@@ -379,8 +355,7 @@ module Aspera
379
355
  Sync::Operations::DIRECTIONS.each do |dir|
380
356
  command dir, description: "#{dir.capitalize}-sync (Gen3)", transfer_paths: :send, arguments: SyncActions::PATH_AND_INFO_ARGS
381
357
  end
382
- command :admin, description: 'Manage sync database (admin operations)'
383
- SyncActions.register_sync_admin_commands(self, :admin)
358
+ sync_admin_commands :admin
384
359
  end
385
360
  # access_keys sub-tree
386
361
  command :access_keys, description: 'Manage access keys'
@@ -389,9 +364,9 @@ module Aspera
389
364
  arguments: [{name: :access_key_id, type: :identifier}],
390
365
  setup: :setup_access_key_do
391
366
  command :set_bearer_key, description: 'Set bearer key on access key',
392
- arguments: [{name: :access_key_id}, {name: :bearer_key_pem, type: String}]
367
+ arguments: [{name: :access_key_id, type: :identifier}, {name: :bearer_key_pem}]
393
368
  crud_commands entity: 'access_keys',
394
- api: :@api_node,
369
+ api: :api_node,
395
370
 
396
371
  body_component: Schema::Registry::NODE,
397
372
  lookup: :lookup_access_key_self_id
@@ -401,35 +376,49 @@ module Aspera
401
376
  COMMANDS_GEN4_SPEC.each do |cmd, spec|
402
377
  command cmd, **spec
403
378
  end
404
- command :permission, description: 'Manage permissions', setup: :setup_access_key_do_permission
379
+ command :v3, description: 'Legacy v3 commands on files', mount: {plugin: self, instance: :v3_node_plugin}
380
+ command :permission, description: 'Manage permissions',
381
+ arguments: [{name: :path, description: 'Path, or %id:<file id>, or %id: for root'}],
382
+ setup: :setup_access_key_do_permission
405
383
  command :sync, description: 'Synchronize folders'
406
384
  commands_under :sync do
407
385
  Sync::Operations::DIRECTIONS.each do |dir|
408
386
  command dir, description: "#{dir.capitalize}-sync", transfer_paths: :send, arguments: SyncActions::PATH_AND_INFO_ARGS
409
387
  end
410
- command :admin, description: 'Manage sync database (admin operations)'
411
- SyncActions.register_sync_admin_commands(self, :admin)
388
+ sync_admin_commands :admin
412
389
  end
413
390
  end
414
391
  commands_under %i[access_keys do permission] do
415
392
  command :list, description: 'List permissions on a file'
416
393
  command :show, description: 'Show a permission',
417
- arguments: [{name: :perm_id, type: :identifier}],
418
- action: ->(apifid:, perm_id:, **) { Result::SingleObject.new(apifid.node_api.read("permissions/#{perm_id}")) }
394
+ arguments: [{name: :permission_id, type: :identifier}],
395
+ action: ->(apifid:, permission_id:, **) { Result::SingleObject.new(apifid.node_api.read("permissions/#{permission_id}")) }
419
396
  command :create, description: 'Create a permission',
420
- arguments: [{name: :data, type: Hash, schema: 'node:components.schemas.permissions-post-request'}]
421
- command :modify, description: 'Modify a permission',
422
- arguments: [{name: :perm_id, type: :identifier}, {name: :data, type: Hash, schema: 'node:components.schemas.permissions-id-put-request'}]
397
+ arguments: [{name: :permission, type: Hash, schema: 'node:components.schemas.permissions-post-request'}]
398
+ command(
399
+ :modify, description: 'Modify a permission',
400
+ arguments: [{name: :permission_id, type: :identifier}, {name: :permission, type: Hash, schema: 'node:components.schemas.permissions-id-put-request'}],
401
+ action: lambda do |permission:, apifid:, permission_id:, **|
402
+ apifid.node_api.update("permissions/#{permission_id}", permission)
403
+ Result::Status.new('Updated')
404
+ end
405
+ )
423
406
  command :delete, description: 'Delete permissions',
424
- arguments: [{name: :perm_id, bulk: true}]
407
+ arguments: [{name: :permission_id, type: :identifier, bulk: true}]
425
408
  end
426
409
  # async (legacy /async)
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']) }
410
+ commands_under :async, description: 'Manage legacy synchronizations (/async)' do
411
+ command :list, description: 'List async sync IDs', action: ->(**) { Result::ValueList.new(api_node.read('async/list')['sync_ids']) }
429
412
  command :show, description: 'Show async summary',
430
413
  arguments: [{name: :async_id, type: :identifier, lookup: :async_lookup}]
431
- command :delete, description: 'Delete async',
432
- arguments: [{name: :async_id, type: :identifier, lookup: :async_lookup}]
414
+ command(
415
+ :delete, description: 'Delete async',
416
+ arguments: [{name: :async_id, type: :identifier, lookup: :async_lookup}],
417
+ action: lambda do |async_id:, **|
418
+ async_ids = async_id.eql?(SpecialValues::ALL) ? api_node.read('async/list')['sync_ids'] : [async_id]
419
+ Result::SingleObject.new(api_node.create('async/delete', {'syncs' => async_ids}))
420
+ end
421
+ )
433
422
  command :bandwidth, description: 'Show async bandwidth',
434
423
  arguments: [{name: :async_id, type: :identifier, lookup: :async_lookup}]
435
424
  command :files, description: 'List async files',
@@ -438,93 +427,138 @@ module Aspera
438
427
  arguments: [{name: :async_id, type: :identifier, lookup: :async_lookup}]
439
428
  end
440
429
  # ssync (/asyncs)
441
- # command :ssync, description: 'Manage sync operations (/asyncs)'
442
- commands_under :ssync, description: 'synchronization (/asyncs)' do
430
+ commands_under :ssync, description: 'Manage synchronizations (/asyncs)' do
443
431
  crud_commands entity: 'asyncs',
444
- api: :@api_node,
432
+ name: 'sync session',
433
+ id_name: :ssync_id,
434
+ api: :api_node,
445
435
  operations: %i[create list show delete],
446
436
  items_key: 'ids',
447
437
  lookup: :ssync_lookup
448
- command :start, description: 'Start a sync',
449
- arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
450
- action: :action_ssync_start
451
- command :stop, description: 'Stop a sync',
452
- arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
453
- action: :action_ssync_stop
438
+ command(
439
+ :start, description: 'Start a sync', arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
440
+ action: lambda do |ssync_id:, **|
441
+ api_node.call(operation: 'POST', subpath: "asyncs/#{ssync_id}/start", content_type: Mime::TEXT, body: '', ret: :resp).body
442
+ Result::Status.new('Done')
443
+ end
444
+ )
445
+ command(
446
+ :stop, description: 'Stop a sync', arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
447
+ action: lambda do |ssync_id:, **|
448
+ api_node.call(operation: 'POST', subpath: "asyncs/#{ssync_id}/stop", content_type: Mime::TEXT, body: '', ret: :resp).body
449
+ Result::Status.new('Done')
450
+ end
451
+ )
454
452
  command :bandwidth, description: 'Show sync bandwidth',
455
453
  arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
456
- action: ->(ssync_id:, **) { Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/bandwidth", options.get_option(:query) || {})) }
454
+ action: ->(ssync_id:, **) { Result::SingleObject.new(api_node.read("asyncs/#{ssync_id}/bandwidth", query_read_delete(default: {}))) }
457
455
  command :counters, description: 'Show sync counters',
458
456
  arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
459
- action: ->(ssync_id:, **) { Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/counters", options.get_option(:query) || {})) }
457
+ action: ->(ssync_id:, **) { Result::SingleObject.new(api_node.read("asyncs/#{ssync_id}/counters", query_read_delete(default: {}))) }
460
458
  command :files, description: 'List sync files',
461
459
  arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
462
- action: ->(ssync_id:, **) { Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/files", options.get_option(:query) || {})) }
460
+ action: ->(ssync_id:, **) { Result::SingleObject.new(api_node.read("asyncs/#{ssync_id}/files", query_read_delete(default: {}))) }
463
461
  command :state, description: 'Show sync state',
464
462
  arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
465
- action: ->(ssync_id:, **) { Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/state")) }
463
+ action: ->(ssync_id:, **) { Result::SingleObject.new(api_node.read("asyncs/#{ssync_id}/state")) }
466
464
  command :summary, description: 'Show sync summary',
467
465
  arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
468
- action: ->(ssync_id:, **) { Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/summary")) }
466
+ action: ->(ssync_id:, **) { Result::SingleObject.new(api_node.read("asyncs/#{ssync_id}/summary")) }
469
467
  end
470
468
  # stream
471
469
  command :stream, description: 'Manage stream operations'
472
470
  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]) }
471
+ command :list, description: 'List streams', action: ->(**) { Result::ObjectList.new(api_node.read('ops/transfers', query_read_delete), fields: %w[id status]) }
474
472
  command :create, description: 'Create a stream',
475
- arguments: [{name: :data, type: Hash, schema: 'node:components.schemas.transferPostRequest'}],
476
- action: ->(data:, **) { Result::SingleObject.new(@api_node.create('streams', data)) }
473
+ arguments: [{name: :stream, type: Hash, schema: 'node:components.schemas.transferPostRequest'}],
474
+ action: ->(stream:, **) { Result::SingleObject.new(api_node.create('streams', stream)) }
477
475
  command :show, description: 'Show a stream',
478
476
  arguments: [{name: :transfer_id, type: :identifier}],
479
- action: ->(transfer_id:, **) { Result::SingleObject.new(@api_node.read("ops/transfers/#{transfer_id}")) }
477
+ action: ->(transfer_id:, **) { Result::SingleObject.new(api_node.read("ops/transfers/#{transfer_id}")) }
480
478
  command :modify, description: 'Modify a stream',
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)) }
479
+ arguments: [{name: :transfer_id, type: :identifier}, {name: :stream, type: Hash, schema: 'node:components.schemas.transferPutRequest'}],
480
+ action: ->(stream:, transfer_id:, **) { Result::SingleObject.new(api_node.update("streams/#{transfer_id}", stream)) }
483
481
  command :cancel, description: 'Cancel a stream',
484
482
  arguments: [{name: :transfer_id, type: :identifier}],
485
- action: ->(transfer_id:, **) { Result::SingleObject.new(@api_node.cancel("streams/#{transfer_id}")) }
483
+ action: ->(transfer_id:, **) { Result::SingleObject.new(api_node.cancel("streams/#{transfer_id}")) }
486
484
  end
487
485
  # transfer
488
486
  command :transfer, description: 'Manage transfer operations'
489
487
  commands_under :transfer do
490
488
  command :list, description: 'List transfers'
491
- command :cancel, description: 'Cancel a transfer',
492
- arguments: [{name: :transfer_id, type: :identifier}]
489
+ command(
490
+ :cancel, description: 'Cancel a transfer',
491
+ arguments: [{name: :transfer_id, type: :identifier}],
492
+ action: lambda do |transfer_id:, **|
493
+ api_node.cancel("ops/transfers/#{transfer_id}")
494
+ Result::Status.new('Cancelled')
495
+ end
496
+ )
493
497
  command :show, description: 'Show a transfer',
494
498
  arguments: [{name: :transfer_id, type: :identifier}],
495
- action: ->(transfer_id:, **) { Result::SingleObject.new(@api_node.read("ops/transfers/#{transfer_id}")) }
496
- command :modify, description: 'Modify a transfer',
497
- arguments: [{name: :transfer_id, type: :identifier}, {name: :update_value, type: Hash, schema: 'node:components.schemas.transferPutRequest'}]
499
+ action: ->(transfer_id:, **) { Result::SingleObject.new(api_node.read("ops/transfers/#{transfer_id}")) }
500
+ command(
501
+ :modify, description: 'Modify a transfer',
502
+ arguments: [{name: :transfer_id, type: :identifier}, {name: :transfer, type: Hash, schema: 'node:components.schemas.transferPutRequest'}],
503
+ action: lambda do |transfer:, transfer_id:, **|
504
+ api_node.update("ops/transfers/#{transfer_id}", transfer)
505
+ Result::Status.new('Modified')
506
+ end
507
+ )
498
508
  command :bandwidth_average, description: 'Show average bandwidth per period'
499
509
  command :sessions, description: 'List transfer sessions'
500
510
  end
501
511
  # service
502
512
  command :service, description: 'Manage services'
503
513
  commands_under :service do
504
- command :list, description: 'List services', action: -> { Result::ObjectList.new(@api_node.read('rund/services')['services']) }
505
- command :create, description: 'Create a service',
506
- arguments: [{name: :creation_data, type: Hash}]
507
- command :delete, description: 'Delete a service',
508
- arguments: [{name: :service_id, type: :identifier}]
514
+ command :list, description: 'List services', action: ->(**) { Result::ObjectList.new(api_node.read('rund/services')['services']) }
515
+ command(
516
+ :create, description: 'Create a service',
517
+ arguments: [{name: :service, type: Hash}],
518
+ action: lambda do |service:, **|
519
+ resp = api_node.create('rund/services', service)
520
+ Result::Status.new("#{resp['id']} created")
521
+ end
522
+ )
523
+ command(
524
+ :delete, description: 'Delete a service',
525
+ arguments: [{name: :service_id, type: :identifier}],
526
+ action: lambda do |service_id:, **|
527
+ api_node.delete("rund/services/#{service_id}")
528
+ Result::Status.new("#{service_id} deleted")
529
+ end
530
+ )
509
531
  end
510
532
  # watch_folder
511
533
  command :watch_folder, description: 'Manage watch folders', setup: :setup_watch_folder
512
534
  commands_under :watch_folder do
513
535
  command :create, description: 'Create a watch folder',
514
- arguments: [{name: :data, type: Hash}],
515
- action: ->(data:, **) { Result::Status.new("#{@api_node.create('v3/watchfolders', data)['id']} created") }
536
+ arguments: [{name: :watch_folder, type: Hash}],
537
+ action: ->(watch_folder:, **) { Result::Status.new("#{api_node.create('v3/watchfolders', watch_folder)['id']} created") }
516
538
  command :list, description: 'List watch folders',
517
- action: -> { Result::ValueList.new(@api_node.read('v3/watchfolders', query_read_delete)['ids']) }
539
+ action: ->(**) { Result::ValueList.new(api_node.read('v3/watchfolders', query_read_delete)['ids']) }
518
540
  command :show, description: 'Show a watch folder',
519
541
  arguments: [{name: :watch_folder_id, type: :identifier}],
520
- action: ->(watch_folder_id:, **) { Result::SingleObject.new(@api_node.read("v3/watchfolders/#{watch_folder_id}")) }
521
- command :modify, description: 'Modify a watch folder',
522
- arguments: [{name: :watch_folder_id, type: :identifier}, {name: :data, type: Hash}]
523
- command :delete, description: 'Delete a watch folder',
524
- arguments: [{name: :watch_folder_id, type: :identifier}]
525
- command :state, description: 'Show watch folder state',
542
+ action: ->(watch_folder_id:, **) { Result::SingleObject.new(api_node.read("v3/watchfolders/#{watch_folder_id}")) }
543
+ command(
544
+ :modify, description: 'Modify a watch folder',
545
+ arguments: [{name: :watch_folder_id, type: :identifier}, {name: :watch_folder, type: Hash}],
546
+ action: lambda do |watch_folder:, watch_folder_id:, **|
547
+ api_node.update("v3/watchfolders/#{watch_folder_id}", watch_folder)
548
+ Result::Status.new("#{watch_folder_id} updated")
549
+ end
550
+ )
551
+ command(
552
+ :delete, description: 'Delete a watch folder',
553
+ arguments: [{name: :watch_folder_id, type: :identifier}],
554
+ action: lambda do |watch_folder_id:, **|
555
+ api_node.delete("v3/watchfolders/#{watch_folder_id}")
556
+ Result::Status.new("#{watch_folder_id} deleted")
557
+ end
558
+ )
559
+ command :state, description: 'Show watch folder state',
526
560
  arguments: [{name: :watch_folder_id, type: :identifier}],
527
- action: ->(watch_folder_id:, **) { Result::SingleObject.new(@api_node.read("v3/watchfolders/#{watch_folder_id}/state")) }
561
+ action: ->(watch_folder_id:, **) { Result::SingleObject.new(api_node.read("v3/watchfolders/#{watch_folder_id}/state")) }
528
562
  end
529
563
  # central
530
564
  command :central, description: 'Query Central service'
@@ -534,21 +568,29 @@ module Aspera
534
568
  end
535
569
  commands_under %i[central session] do
536
570
  command :list, description: 'List sessions',
537
- arguments: [{name: :request_data, type: Hash, mandatory: false, default: nil}]
571
+ arguments: [{name: :criteria, type: Hash, mandatory: false, default: nil}]
538
572
  end
539
573
  commands_under %i[central file] do
540
574
  command :list, description: 'List file transfers',
541
- arguments: [{name: :request_data, type: Hash, mandatory: false, default: nil}]
575
+ arguments: [{name: :criteria, type: Hash, mandatory: false, default: nil}]
542
576
  command :modify, description: 'Modify file transfer validation',
543
- arguments: [{name: :request_data, type: Hash, mandatory: false, default: nil}]
577
+ arguments: [{name: :file, type: Hash, mandatory: false, default: nil}]
544
578
  end
545
579
  # Standalone leaf commands
546
580
  command :asperabrowser, description: 'Open Aspera browser'
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))) }
548
- command :bearer_token, description: 'Generate bearer token',
549
- arguments: [{name: :private_key_pem, type: String}, {name: :user_group_id, type: Hash, schema: 'opts:components.schemas.NodeBearerTokenOptions'}]
550
- command :simulator, description: 'Start node simulator',
551
- arguments: [{name: :parameters, type: Hash, mandatory: false, default: {}, schema: 'opts:components.schemas.NodeSimulatorOptions'}]
581
+ 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))) }
582
+ command(
583
+ :bearer_token, description: 'Generate bearer token',
584
+ arguments: [{name: :private_key_pem}, {name: :token, type: Hash, schema: 'opts:components.schemas.NodeBearerTokenOptions'}],
585
+ action: lambda do |private_key_pem:, token:, **|
586
+ private_key = OpenSSL::PKey::RSA.new(private_key_pem)
587
+ access_key = options.get_option(:username, mandatory: true)
588
+ Result::Text.new(Api::Node.bearer_token(payload: token, access_key: access_key, private_key: private_key))
589
+ end
590
+ )
591
+ command :emulator, description: 'Start node emulator',
592
+ aliases: [:simulator],
593
+ arguments: [{name: :parameters, type: Hash, mandatory: false, default: {}, schema: Schema::Registry::NODE_EMULATOR_OPTIONS}]
552
594
  command :telemetry, description: 'Report telemetry to external system',
553
595
  arguments: [{name: :parameters, type: Hash, mandatory: false, default: {}, schema: 'opts:components.schemas.NodeTelemetryOptions'}]
554
596
 
@@ -557,15 +599,16 @@ module Aspera
557
599
  def action_delete(paths:, **)
558
600
  # TODO: add query for recursive
559
601
  paths_to_delete = Array(paths)
560
- resp = @api_node.create('files/delete', {paths: paths_to_delete.map { |i| {'path' => i.start_with?('/') ? i : "/#{i}"} }})
602
+ resp = api_node.create('files/delete', {paths: paths_to_delete.map { |i| {'path' => i.start_with?('/') ? i : "/#{i}"} }})
561
603
  cli_result_from_paths_response(resp, 'file deleted')
562
604
  end
563
605
 
564
- def action_search(search_root:, **)
565
- parameters = {'path' => search_root}
606
+ def action_search(path:, **)
607
+ parameters = {'path' => path}
608
+ # not a URL query: merged into the POST body of files/search
566
609
  other_options = options.get_option(:query)
567
610
  parameters.merge!(other_options) unless other_options.nil?
568
- resp = @api_node.create('files/search', parameters)
611
+ resp = api_node.create('files/search', parameters)
569
612
  return Result::Empty.new if resp['items'].empty?
570
613
  fields = resp['items'].first.keys.reject { |i| SEARCH_REMOVE_FIELDS.include?(i) }
571
614
  formatter.display_item_count(resp['item_count'], resp['total_count'])
@@ -573,32 +616,32 @@ module Aspera
573
616
  Result::ObjectList.new(resp['items'], fields: fields)
574
617
  end
575
618
 
576
- def action_space(path_list:, **)
577
- path_list = Array(path_list)
578
- resp = @api_node.create('space', {'paths' => path_list.map { |i| {path: i} }})
619
+ def action_space(paths:, **)
620
+ paths = Array(paths)
621
+ resp = api_node.create('space', {'paths' => paths.map { |i| {path: i} }})
579
622
  Result::ObjectList.new(resp['paths'])
580
623
  end
581
624
 
582
- def action_mkdir(path_list:, **)
583
- path_list = Array(path_list)
584
- resp = @api_node.create('files/create', {'paths' => path_list.map { |i| {type: :directory, path: i} }})
625
+ def action_mkdir(paths:, **)
626
+ paths = Array(paths)
627
+ resp = api_node.create('files/create', {'paths' => paths.map { |i| {type: :directory, path: i} }})
585
628
  cli_result_from_paths_response(resp, 'folder created')
586
629
  end
587
630
 
588
631
  def action_mklink(target:, link_path:, **)
589
- resp = @api_node.create('files/create', {'paths' => [{type: :symbolic_link, path: link_path, target: {path: target}}]})
632
+ resp = api_node.create('files/create', {'paths' => [{type: :symbolic_link, path: link_path, target: {path: target}}]})
590
633
  cli_result_from_paths_response(resp, 'link created')
591
634
  end
592
635
 
593
- def action_mkfile(file_path:, contents:, **)
636
+ def action_mkfile(path:, contents:, **)
594
637
  contents64 = contents.nil? ? '' : Base64.strict_encode64(contents)
595
- resp = @api_node.create('files/create', {'paths' => [{type: :file, path: file_path, contents: contents64}]})
638
+ resp = api_node.create('files/create', {'paths' => [{type: :file, path: path, contents: contents64}]})
596
639
  cli_result_from_paths_response(resp, 'file created')
597
640
  end
598
641
 
599
- def action_rename(path_base:, path_src:, path_dst:, **)
642
+ def action_rename(folder:, source:, destination:, **)
600
643
  # TODO: multiple ?
601
- resp = @api_node.create('files/rename', {'paths' => [{'path' => path_base, 'source' => path_src, 'destination' => path_dst}]})
644
+ resp = api_node.create('files/rename', {'paths' => [{'path' => folder, 'source' => source, 'destination' => destination}]})
602
645
  cli_result_from_paths_response(resp, 'entry moved')
603
646
  end
604
647
 
@@ -606,11 +649,11 @@ module Aspera
606
649
  def sync_gen3_block
607
650
  lambda do |direction, local_path, remote_path|
608
651
  request_transfer_spec = sync_spec_request(direction, local_path, remote_path)
609
- @api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
610
- transfer_spec = @api_node.create(
652
+ api_node.add_tspec_info(request_transfer_spec) if api_node.respond_to?(:add_tspec_info)
653
+ transfer_spec = api_node.create(
611
654
  'files/sync_setup',
612
655
  {transfer_requests: [{transfer_request: request_transfer_spec}]}
613
- )['transfer_specs'].first['transfer_spec']
656
+ )['transfer_specs'].first&.fetch('transfer_spec')
614
657
  transfer_spec.delete_if { |_k, v| v.nil? }
615
658
  Log.dump(:ts, transfer_spec)
616
659
  transfer_spec
@@ -618,45 +661,45 @@ module Aspera
618
661
  end
619
662
 
620
663
  Sync::Operations::DIRECTIONS.each do |dir|
621
- define_method(:"action_sync_#{dir}") { |path:, sync_info: {}, **| run_sync_transfer(dir, path: path, sync_info: sync_info, &sync_gen3_block) }
664
+ define_action_method([:sync, dir]) { |path:, sync_info: {}, **| run_sync_transfer(dir, path: path, sync_info: sync_info, &sync_gen3_block) }
622
665
  end
623
666
 
624
- def action_upload
667
+ def action_upload(**)
625
668
  # empty transfer spec for authorization request
626
669
  request_transfer_spec = {}
627
670
  request_transfer_spec[:paths] = [{destination: transfer.destination_folder(Transfer::Spec::DIRECTION_SEND)}]
628
671
  # add fixed parameters if any (for COS)
629
- @api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
672
+ api_node.add_tspec_info(request_transfer_spec) if api_node.respond_to?(:add_tspec_info)
630
673
  Api::Node.add_public_key(request_transfer_spec)
631
674
  setup_payload = {transfer_requests: [{transfer_request: request_transfer_spec}]}
632
- transfer_spec = @api_node.create('files/upload_setup', setup_payload)['transfer_specs'].first['transfer_spec']
675
+ transfer_spec = api_node.create('files/upload_setup', setup_payload)['transfer_specs'].first&.fetch('transfer_spec')
633
676
  Api::Node.add_private_key(transfer_spec)
634
677
  transfer_spec.delete('paths')
635
678
  Runner.result_transfer(transfer.start(transfer_spec))
636
679
  end
637
680
 
638
- def action_download
681
+ def action_download(**)
639
682
  # empty transfer spec for authorization request
640
683
  request_transfer_spec = {}
641
684
  request_transfer_spec[:paths] = transfer.ts_source_paths
642
685
  # add fixed parameters if any (for COS)
643
- @api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
686
+ api_node.add_tspec_info(request_transfer_spec) if api_node.respond_to?(:add_tspec_info)
644
687
  Api::Node.add_public_key(request_transfer_spec)
645
688
  setup_payload = {transfer_requests: [{transfer_request: request_transfer_spec}]}
646
- transfer_spec = @api_node.create('files/download_setup', setup_payload)['transfer_specs'].first['transfer_spec']
689
+ transfer_spec = api_node.create('files/download_setup', setup_payload)['transfer_specs'].first&.fetch('transfer_spec')
647
690
  Api::Node.add_private_key(transfer_spec)
648
691
  Runner.result_transfer(transfer.start(transfer_spec))
649
692
  end
650
693
 
651
- def action_cat(remote_path:, **)
652
- http = @api_node.read("files/#{URI.encode_www_form_component(remote_path)}/contents", ret: :resp)
694
+ def action_cat(path:, **)
695
+ http = api_node.read("files/#{URI.encode_www_form_component(path)}/contents", ret: :resp)
653
696
  Result::Text.new(http.body)
654
697
  end
655
698
 
656
- def action_health
699
+ def action_health(**)
657
700
  nagios = Nagios.new
658
701
  begin
659
- info = @api_node.read('info')
702
+ info = api_node.read('info')
660
703
  nagios.add_ok('node api', 'accessible')
661
704
  nagios.check_time_offset(info['current_time'], 'node api')
662
705
  nagios.check_product_version('node api', 'entsrv', info['version'])
@@ -664,7 +707,7 @@ module Aspera
664
707
  nagios.add_critical('node api', e.to_s)
665
708
  end
666
709
  begin
667
- @api_node.call(
710
+ api_node.call(
668
711
  operation: 'POST',
669
712
  subpath: 'services/soap/Transfer-201210',
670
713
  content_type: Mime::TEXT,
@@ -681,49 +724,32 @@ module Aspera
681
724
 
682
725
  # watch_folder setup: inject required API header (avoids "Unable to convert 2016_09_14 configuration")
683
726
  def setup_watch_folder(**)
684
- @api_node.params[:headers] ||= {}
685
- @api_node.params[:headers]['X-aspera-WF-version'] = '2017_10_23'
727
+ api_node.headers['X-aspera-WF-version'] = '2017_10_23'
686
728
  {}
687
729
  end
688
730
 
689
- def action_access_keys_do_permission_modify(data:, apifid:, perm_id:, **)
690
- apifid.node_api.update("permissions/#{perm_id}", data)
691
- Result::Status.new('Updated')
692
- end
693
-
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")
697
- end
698
-
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")
702
- end
703
-
704
731
  # access_keys > do - setup: resolve access key and root file id
705
- # access_key_id is resolved by execute_leaf (via instance_arg in CommandSpec)
706
732
  # @return [Hash] context hash containing :do_root_file_id
707
733
  def setup_access_key_do(access_key_id:, **)
708
734
  @do_root_file_id = options.get_option(:root_id)
709
735
  if @do_root_file_id.nil?
710
- ak_info = @api_node.read("access_keys/#{access_key_id}")
711
- ak_secret = context.secret_finder.lookup(url: @api_node.base_url, username: ak_info['id'])
736
+ ak_info = api_node.read("access_keys/#{access_key_id}")
737
+ ak_secret = context.secret_finder.lookup(url: api_node.base_url, username: ak_info['id'])
712
738
  if !access_key_id.eql?('self')
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}." }
714
- @api_node.auth_params[:username] = ak_info['id']
715
- @api_node.auth_params[:password] = ak_secret
739
+ 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}." }
740
+ api_node.auth_params[:username] = ak_info['id']
741
+ api_node.auth_params[:password] = ak_secret
716
742
  end
717
743
  @do_root_file_id = ak_info['root_file_id']
718
744
  end
719
745
  {do_root_file_id: @do_root_file_id}
720
746
  end
721
747
 
722
- # access_keys > do > permission - setup: resolve apifid from CLI path argument
723
- # (path is read from CLI here as 'permission' is an intermediate node)
748
+ # access_keys > do > permission - setup: resolve apifid from the path argument of the node
749
+ # do_root_file_id: comes from ctx (setup_access_key_do, or seed of a mount)
724
750
  # @return [Hash] context hash containing :apifid
725
- def setup_access_key_do_permission(**)
726
- {apifid: apifid_from_next_arg(@do_root_file_id)}
751
+ def setup_access_key_do_permission(do_root_file_id:, path:, **)
752
+ {apifid: apifid_from_path(do_root_file_id, path)}
727
753
  end
728
754
 
729
755
  # access_keys > do > ls
@@ -737,7 +763,7 @@ module Aspera
737
763
  # access_keys > do > find
738
764
  def action_access_keys_do_find(path:, filter: nil, do_root_file_id:, **)
739
765
  apifid = apifid_from_path(do_root_file_id, path)
740
- Result::ObjectList.new(@api_node.find_files(apifid.file_id, Base.file_matcher(filter)), fields: ['path'])
766
+ Result::ObjectList.new(api_node.find_files(apifid.file_id, Base.file_matcher(filter)), fields: ['path'])
741
767
  end
742
768
 
743
769
  # access_keys > do > cat
@@ -753,9 +779,9 @@ module Aspera
753
779
  end
754
780
 
755
781
  # access_keys > do > modify
756
- def action_access_keys_do_modify(path:, update_value:, do_root_file_id:, **)
782
+ def action_access_keys_do_modify(path:, file:, do_root_file_id:, **)
757
783
  apifid = apifid_from_path(do_root_file_id, path)
758
- apifid.node_api.update("files/#{apifid.file_id}", update_value)
784
+ apifid.node_api.update("files/#{apifid.file_id}", file)
759
785
  Result::Status.new('Done')
760
786
  end
761
787
 
@@ -767,19 +793,19 @@ module Aspera
767
793
 
768
794
  # access_keys > do > rename
769
795
  def action_access_keys_do_rename(source_path:, new_name:, do_root_file_id:, **)
770
- apifid = @api_node.resolve_api_fid(do_root_file_id, source_path)
796
+ apifid = api_node.resolve_api_fid(do_root_file_id, source_path)
771
797
  apifid.node_api.update("files/#{apifid.file_id}", {name: new_name})
772
798
  Result::Status.new("renamed to #{new_name}")
773
799
  end
774
800
 
775
801
  # access_keys > do > delete
776
- def action_access_keys_do_delete(paths:, do_root_file_id:, **)
777
- bulk_result(paths, command: :delete, id_result: 'path') do |l_path|
802
+ def action_access_keys_do_delete(path:, do_root_file_id:, **)
803
+ bulk_result(path, command: :delete, id_result: 'path') do |l_path|
778
804
  apifid = if (m = Parser.percent_selector(l_path))
779
805
  Aspera.assert_values(m[:field], ['id'], type: BadIdentifier)
780
- Api::NodeFileId.new(@api_node, m[:value])
806
+ Api::NodeFileId.new(api_node, m[:value])
781
807
  else
782
- @api_node.resolve_api_fid(do_root_file_id, l_path)
808
+ api_node.resolve_api_fid(do_root_file_id, l_path)
783
809
  end
784
810
  apifid.node_api.delete("files/#{apifid.file_id}")
785
811
  {'path' => l_path}
@@ -790,40 +816,33 @@ module Aspera
790
816
  def sync_gen4_block(do_root_file_id)
791
817
  lambda do |direction, _local_path, remote_path|
792
818
  ts_direction = direction.eql?(:pull) ? Transfer::Spec::DIRECTION_RECEIVE : Transfer::Spec::DIRECTION_SEND
793
- apifid = @api_node.resolve_api_fid(do_root_file_id, remote_path)
819
+ apifid = api_node.resolve_api_fid(do_root_file_id, remote_path)
794
820
  apifid.node_api.transfer_spec_gen4(apifid.file_id, ts_direction)
795
821
  end
796
822
  end
797
823
 
798
824
  Sync::Operations::DIRECTIONS.each do |dir|
799
- define_method(:"action_access_keys_do_sync_#{dir}") do |path:, sync_info: {}, do_root_file_id:, **|
825
+ define_action_method([:access_keys, :do, :sync, dir]) do |path:, sync_info: {}, do_root_file_id:, **|
800
826
  run_sync_transfer(dir, path: path, sync_info: sync_info, &sync_gen4_block(do_root_file_id))
801
827
  end
802
828
  end
803
829
 
804
830
  # access_keys > do > upload
805
831
  def action_access_keys_do_upload(do_root_file_id:, **)
806
- apifid = @api_node.resolve_api_fid(do_root_file_id, transfer.destination_folder(Transfer::Spec::DIRECTION_SEND), true)
832
+ apifid = api_node.resolve_api_fid(do_root_file_id, transfer.destination_folder(Transfer::Spec::DIRECTION_SEND), true)
807
833
  Runner.result_transfer(transfer.start(apifid.node_api.transfer_spec_gen4(apifid.file_id, Transfer::Spec::DIRECTION_SEND)))
808
834
  end
809
835
 
810
836
  # access_keys > do > download
811
837
  def action_access_keys_do_download(do_root_file_id:, **)
812
- apifid, source_paths = @api_node.resolve_api_fid_paths(do_root_file_id, transfer.ts_source_paths)
838
+ apifid, source_paths = api_node.resolve_api_fid_paths(do_root_file_id, transfer.ts_source_paths)
813
839
  Runner.result_transfer(transfer.start(apifid.node_api.transfer_spec_gen4(apifid.file_id, Transfer::Spec::DIRECTION_RECEIVE, {'paths'=>source_paths})))
814
840
  end
815
841
 
816
- # access_keys > do > permission (intermediate node: re-enter DSL at permission sub-tree)
817
- def action_access_keys_do_permission(do_root_file_id:, **)
818
- apifid = apifid_from_next_arg(do_root_file_id)
819
- dispatch_from_registry(%i[access_keys do permission], {apifid: apifid}, skip_setup: true)
820
- end
821
-
822
- # access_keys > do > v3
823
- def action_access_keys_do_v3(do_root_file_id:, **)
824
- # Build a fresh Node for the v3 root, then let the DSL dispatcher consume the next command.
825
- v3_node = Node.new(context: context, api: @api_node.resolve_api_fid(do_root_file_id, '').node_api)
826
- v3_node.execute_action
842
+ # access_keys > do > v3 - mount target: Node plugin on the node hosting the root file
843
+ # @return [Node]
844
+ def v3_node_plugin(do_root_file_id:, **)
845
+ Node.new(context: context, api: api_node.resolve_api_fid(do_root_file_id, '').node_api)
827
846
  end
828
847
 
829
848
  # access_keys > do > node_info / bearer_token_node — shared helper builds the result hash
@@ -861,7 +880,8 @@ module Aspera
861
880
  # @param path [String] full path (folder/name)
862
881
  def gen4_mk_resolve(top_file_id, path)
863
882
  containing_folder_path, new_item = Api::Node.split_folder(path)
864
- apifid = @api_node.resolve_api_fid(top_file_id, containing_folder_path, true)
883
+ apifid = api_node.resolve_api_fid(top_file_id, containing_folder_path, true)
884
+ # not a URL query: special keys `check` and `target`, rest merged into the POST body
865
885
  query = options.get_option(:query)
866
886
  check_exists = true
867
887
  payload = {name: new_item}
@@ -869,7 +889,7 @@ module Aspera
869
889
  check_exists = !query.delete('check').eql?(false)
870
890
  target = query.delete('target')
871
891
  if target
872
- target_apifid = @api_node.resolve_api_fid(top_file_id, target, true)
892
+ target_apifid = api_node.resolve_api_fid(top_file_id, target, true)
873
893
  payload[:target_id] = target_apifid.file_id
874
894
  end
875
895
  payload.merge!(query.symbolize_keys)
@@ -913,8 +933,8 @@ module Aspera
913
933
  Result::ObjectList.new(apifid.node_api.read_with_pages('permissions', list_query))
914
934
  end
915
935
 
916
- def action_access_keys_do_permission_delete(perm_id:, apifid:, **)
917
- bulk_result(perm_id, command: :delete) do |one_id|
936
+ def action_access_keys_do_permission_delete(permission_id:, apifid:, **)
937
+ bulk_result(permission_id, command: :delete) do |one_id|
918
938
  apifid.node_api.delete("permissions/#{one_id}")
919
939
  the_app = apifid.node_api.app_info
920
940
  the_app&.api&.permissions_send_event(event_data: {}, app_info: the_app, types: ['permission.deleted'])
@@ -922,8 +942,8 @@ module Aspera
922
942
  end
923
943
  end
924
944
 
925
- def action_access_keys_do_permission_create(data:, apifid:, **)
926
- create_param = data
945
+ def action_access_keys_do_permission_create(permission:, apifid:, **)
946
+ create_param = permission
927
947
  Aspera.assert(!create_param.key?('file_id'), type: Cli::BadArgument) { 'no file_id' }
928
948
  create_param['file_id'] = apifid.file_id
929
949
  create_param['access_levels'] = Api::Node::ACCESS_LEVELS unless create_param.key?('access_levels')
@@ -936,36 +956,31 @@ module Aspera
936
956
 
937
957
  # access_keys > set_bearer_key
938
958
  def action_access_keys_set_bearer_key(access_key_id:, bearer_key_pem:, **)
939
- access_key_id = @api_node.read('access_keys/self')['id'] if access_key_id.eql?('self')
959
+ access_key_id = api_node.read('access_keys/self')['id'] if access_key_id.eql?('self')
940
960
  key = OpenSSL::PKey.read(bearer_key_pem)
941
961
  key = key.public_key if key.private?
942
- @api_node.update("access_keys/#{access_key_id}", {token_verification_key: key.to_pem})
962
+ api_node.update("access_keys/#{access_key_id}", {token_verification_key: key.to_pem})
943
963
  Result::Status.new('public key updated')
944
964
  end
945
965
 
946
966
  # async sub-commands: individual handlers
947
967
  def action_async_show(async_id:, **)
948
- async_ids = @api_node.read('async/list')['sync_ids']
968
+ async_ids = api_node.read('async/list')['sync_ids']
949
969
  if async_id.eql?(SpecialValues::ALL)
950
- resp = @api_node.create('async/summary', {'syncs' => async_ids})['sync_summaries']
970
+ resp = api_node.create('async/summary', {'syncs' => async_ids})['sync_summaries']
951
971
  return Result::Empty.new if resp.empty?
952
972
  return Result::ObjectList.new(resp, fields: %w[snid name local_dir remote_dir])
953
973
  end
954
974
  Integer(async_id)
955
- resp = @api_node.create('async/summary', {'syncs' => [async_id]})['sync_summaries']
975
+ resp = api_node.create('async/summary', {'syncs' => [async_id]})['sync_summaries']
956
976
  return Result::Empty.new if resp.empty?
957
977
  Result::SingleObject.new(resp.first)
958
978
  end
959
979
 
960
- def action_async_delete(async_id:, **)
961
- async_ids = async_id.eql?(SpecialValues::ALL) ? @api_node.read('async/list')['sync_ids'] : [async_id]
962
- Result::SingleObject.new(@api_node.create('async/delete', {'syncs' => async_ids}))
963
- end
964
-
965
980
  def action_async_bandwidth(async_id:, **)
966
981
  Integer(async_id)
967
982
  post_data = {'syncs' => [async_id], 'seconds' => 100}
968
- resp = @api_node.create('async/bandwidth', post_data)
983
+ resp = api_node.create('async/bandwidth', post_data)
969
984
  data = resp['bandwidth_data']
970
985
  return Result::Empty.new if data.empty?
971
986
  Result::ObjectList.new(data.first[async_id]['data'])
@@ -974,9 +989,10 @@ module Aspera
974
989
  def action_async_files(async_id:, **)
975
990
  Integer(async_id)
976
991
  post_data = {'syncs' => [async_id]}
992
+ # not a URL query: merged into the POST body of async/files
977
993
  filter = options.get_option(:query)
978
994
  post_data.merge!(filter) unless filter.nil?
979
- resp = @api_node.create('async/files', post_data)
995
+ resp = api_node.create('async/files', post_data)
980
996
  data = resp['sync_files']
981
997
  data = data.first[async_id] unless data.empty?
982
998
  iteration_data = []
@@ -997,23 +1013,13 @@ module Aspera
997
1013
 
998
1014
  def action_async_counters(async_id:, **)
999
1015
  Integer(async_id)
1000
- resp = @api_node.create('async/counters', {'syncs' => [async_id]})['sync_counters'].first[async_id].last
1016
+ resp = api_node.create('async/counters', {'syncs' => [async_id]})['sync_counters'].first&.[](async_id)&.last
1001
1017
  return Result::Empty.new if resp.nil?
1002
1018
  Result::SingleObject.new(resp)
1003
1019
  end
1004
1020
 
1005
- def action_ssync_start(ssync_id:, **)
1006
- @api_node.call(operation: 'POST', subpath: "asyncs/#{ssync_id}/start", content_type: Mime::TEXT, body: '', ret: :resp).body
1007
- Result::Status.new('Done')
1008
- end
1009
-
1010
- def action_ssync_stop(ssync_id:, **)
1011
- @api_node.call(operation: 'POST', subpath: "asyncs/#{ssync_id}/stop", content_type: Mime::TEXT, body: '', ret: :resp).body
1012
- Result::Status.new('Done')
1013
- end
1014
-
1015
1021
  # transfer sub-commands
1016
- def action_transfer_list
1022
+ def action_transfer_list(**)
1017
1023
  transfer_filter = query_read_delete(default: {}, schema: Schema::Registry.query_params(Schema::Registry::NODE, 'ops/transfers'))
1018
1024
  iteration_persistency = nil
1019
1025
  if options.get_option(:once_only, mandatory: true)
@@ -1030,13 +1036,13 @@ module Aspera
1030
1036
  else
1031
1037
  Aspera.assert(!transfer_filter.key?('reset'), 'reset only with once_only', type: Cli::BadArgument)
1032
1038
  end
1033
- transfers_data = @api_node.read_with_paging('ops/transfers', transfer_filter, iteration: iteration_persistency&.data)
1039
+ transfers_data = api_node.read_with_paging('ops/transfers', transfer_filter, iteration: iteration_persistency&.data)
1034
1040
  iteration_persistency&.save
1035
1041
  Result::ObjectList.new(transfers_data, fields: %w[id status start_spec.direction start_spec.remote_user start_spec.remote_host start_spec.destination_path])
1036
1042
  end
1037
1043
 
1038
- def action_transfer_sessions
1039
- transfers_data = @api_node.read('ops/transfers', query_read_delete(schema: Schema::Registry.query_params(Schema::Registry::NODE, 'ops/transfers')))
1044
+ def action_transfer_sessions(**)
1045
+ transfers_data = api_node.read('ops/transfers', query_read_delete(schema: Schema::Registry.query_params(Schema::Registry::NODE, 'ops/transfers')))
1040
1046
  sessions = transfers_data.flat_map { |t| t['sessions'] }
1041
1047
  sessions.each do |session|
1042
1048
  SESSION_TIME_FIELDS.each do |what|
@@ -1046,18 +1052,8 @@ module Aspera
1046
1052
  Result::ObjectList.new(sessions, fields: %w[id status start_time end_time target_rate_kbps])
1047
1053
  end
1048
1054
 
1049
- def action_transfer_cancel(transfer_id:, **)
1050
- @api_node.cancel("ops/transfers/#{transfer_id}")
1051
- Result::Status.new('Cancelled')
1052
- end
1053
-
1054
- def action_transfer_modify(update_value:, transfer_id:, **)
1055
- @api_node.update("ops/transfers/#{transfer_id}", update_value)
1056
- Result::Status.new('Modified')
1057
- end
1058
-
1059
- def action_transfer_bandwidth_average
1060
- transfers_data = @api_node.read('ops/transfers', query_read_delete(schema: Schema::Registry.query_params(Schema::Registry::NODE, 'ops/transfers')))
1055
+ def action_transfer_bandwidth_average(**)
1056
+ transfers_data = api_node.read('ops/transfers', query_read_delete(schema: Schema::Registry.query_params(Schema::Registry::NODE, 'ops/transfers')))
1061
1057
  bandwidth_period = {}
1062
1058
  dir_info = %i[avg_kbps sessions].freeze
1063
1059
  transfers_data.each do |t|
@@ -1086,17 +1082,6 @@ module Aspera
1086
1082
  Result::ObjectList.new(result)
1087
1083
  end
1088
1084
 
1089
- # service sub-commands
1090
- def action_service_create(creation_data:, **)
1091
- resp = @api_node.create('rund/services', creation_data)
1092
- Result::Status.new("#{resp['id']} created")
1093
- end
1094
-
1095
- def action_service_delete(service_id:, **)
1096
- @api_node.delete("rund/services/#{service_id}")
1097
- Result::Status.new("#{service_id} deleted")
1098
- end
1099
-
1100
1085
  # central: shared helper
1101
1086
  def central_validation
1102
1087
  validator_id = options.get_option(:validator)
@@ -1104,35 +1089,35 @@ module Aspera
1104
1089
  end
1105
1090
 
1106
1091
  # central > session > list
1107
- def action_central_session_list(request_data:, **)
1108
- request_data ||= {}
1092
+ def action_central_session_list(criteria:, **)
1093
+ criteria ||= {}
1109
1094
  validation = central_validation
1110
- request_data.deep_merge!({'validation' => validation}) unless validation.nil?
1111
- resp = @api_node.create('services/rest/transfers/v1/sessions', request_data)
1095
+ criteria.deep_merge!({'validation' => validation}) unless validation.nil?
1096
+ resp = api_node.create('services/rest/transfers/v1/sessions', criteria)
1112
1097
  Result::ObjectList.new(resp['session_info_result']['session_info'], fields: %w[session_uuid status transport direction bytes_transferred])
1113
1098
  end
1114
1099
 
1115
1100
  # central > file > list
1116
- def action_central_file_list(request_data:, **)
1117
- request_data ||= {}
1101
+ def action_central_file_list(criteria:, **)
1102
+ criteria ||= {}
1118
1103
  validation = central_validation
1119
- request_data.deep_merge!({'validation' => validation}) unless validation.nil?
1120
- resp = @api_node.create('services/rest/transfers/v1/files', request_data)
1104
+ criteria.deep_merge!({'validation' => validation}) unless validation.nil?
1105
+ resp = api_node.create('services/rest/transfers/v1/files', criteria)
1121
1106
  resp = JSON.parse(resp) if resp.is_a?(String)
1122
1107
  Log.dump(:resp, resp)
1123
1108
  Result::ObjectList.new(resp['file_transfer_info_result']['file_transfer_info'], fields: %w[session_uuid file_id status path])
1124
1109
  end
1125
1110
 
1126
1111
  # central > file > modify
1127
- def action_central_file_modify(request_data:, **)
1128
- request_data ||= {}
1112
+ def action_central_file_modify(file:, **)
1113
+ file ||= {}
1129
1114
  validation = central_validation
1130
- request_data.deep_merge!(validation) unless validation.nil?
1131
- @api_node.update('services/rest/transfers/v1/files', request_data)
1115
+ file.deep_merge!(validation) unless validation.nil?
1116
+ api_node.update('services/rest/transfers/v1/files', file)
1132
1117
  Result::Status.new('updated')
1133
1118
  end
1134
1119
 
1135
- def action_asperabrowser
1120
+ def action_asperabrowser(**)
1136
1121
  browse_params = {
1137
1122
  'nodeUser' => options.get_option(:username, mandatory: true),
1138
1123
  'nodePW' => options.get_option(:password, mandatory: true),
@@ -1144,20 +1129,23 @@ module Aspera
1144
1129
  return Result::Status.new('done')
1145
1130
  end
1146
1131
 
1147
- def action_bearer_token(private_key_pem:, user_group_id:, **)
1148
- private_key = OpenSSL::PKey::RSA.new(private_key_pem)
1149
- access_key = options.get_option(:username, mandatory: true)
1150
- Result::Text.new(Api::Node.bearer_token(payload: user_group_id, access_key: access_key, private_key: private_key))
1151
- end
1152
-
1153
- def action_simulator(parameters: {}, **)
1154
- require 'aspera/node_simulator'
1155
- parameters = parameters.symbolize_keys
1156
- uri = URI.parse(parameters.delete(:url) { WebServerSimple::DEFAULT_URL })
1132
+ def action_emulator(parameters: {}, **)
1133
+ require 'aspera/node_emulator'
1134
+ # missing parameters take the default of the schema
1135
+ defaults = Schema::Registry.instance.reader(Schema::Registry::NODE_EMULATOR_OPTIONS).current['properties'].each_with_object({}) do |(name, property), result|
1136
+ result[name.to_sym] = property['default'] if property.key?('default')
1137
+ end
1138
+ parameters = defaults.merge(parameters.symbolize_keys)
1139
+ uri = URI.parse(parameters.delete(:url))
1140
+ config = parameters.except(*WebServerSimple::PARAMS)
1141
+ docroot = config.delete(:docroot)
1142
+ retention_sec = config.delete(:retention_sec)
1143
+ Aspera.assert(config[:username].nil? == config[:password].nil?, type: Cli::BadArgument) { 'Parameters username and password must be set together' }
1144
+ Log.log.warn('No username and password: emulator accepts requests without authentication') if config[:username].nil?
1157
1145
  server = WebServerSimple.new(uri, **parameters.slice(*WebServerSimple::PARAMS))
1158
- server.mount(uri.path, NodeSimulatorServlet, parameters.except(*WebServerSimple::PARAMS), NodeSimulator.new)
1146
+ server.mount(uri.path, NodeEmulatorServlet, config, NodeEmulator.new(docroot: docroot, retention_sec: retention_sec))
1159
1147
  server.start
1160
- return Result::Status.new('Simulator terminated')
1148
+ return Result::Status.new('Emulator terminated')
1161
1149
  end
1162
1150
 
1163
1151
  def action_telemetry(parameters: {}, **)
@@ -1169,8 +1157,8 @@ module Aspera
1169
1157
  parameters[:interval] = 10 unless parameters.key?(:interval)
1170
1158
  parameters[:hostname] = Socket.gethostname unless parameters.key?(:hostname)
1171
1159
  interval = parameters[:interval].to_f
1172
- Aspera.assert(interval > 0, type: Cli::BadArgument) { 'Interval must be a positive number in seconds' }
1173
- otel_api = Rest.new(
1160
+ Aspera.assert(interval >= 0, type: Cli::BadArgument) { 'Interval must be a positive number in seconds, or 0 for single shot' }
1161
+ otel_api = Rest::Client.new(
1174
1162
  base_url: "#{parameters[:url]}/v1",
1175
1163
  headers: {
1176
1164
  # 'Authorization' => "apiToken #{parameters[:key]}",
@@ -1225,7 +1213,7 @@ module Aspera
1225
1213
  }
1226
1214
  loop do
1227
1215
  timestamp = Time.now
1228
- transfers_data = @api_node.read_with_paging('ops/transfers', {active_only: true})
1216
+ transfers_data = api_node.read_with_paging('ops/transfers', {active_only: true})
1229
1217
  datapoint[:asInt] = transfers_data.length
1230
1218
  datapoint[:timeUnixNano] = timestamp.to_i * 1_000_000_000 + timestamp.nsec
1231
1219
  Log.log.info("#{datapoint[:asInt]} active transfers")
@@ -1236,21 +1224,6 @@ module Aspera
1236
1224
  end
1237
1225
  end
1238
1226
 
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)
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.
1246
- # @param command [Symbol] command already consumed from the argument stream
1247
- # @param ctx [Hash] pre-resolved context (keyword args forwarded to dispatch)
1248
- # @return [Object] CLI result
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)
1252
- end
1253
-
1254
1227
  private
1255
1228
 
1256
1229
  # Response has key `paths`.