aspera-cli 4.26.2 → 4.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +281 -21
  4. data/CONTRIBUTING.md +98 -7
  5. data/bin/ascli +3 -3
  6. data/docs/README.md +10757 -0
  7. data/docs/test-mcp-with-ai.md +202 -0
  8. data/lib/aspera/agent/base.rb +20 -6
  9. data/lib/aspera/agent/connect.rb +53 -16
  10. data/lib/aspera/agent/desktop.rb +52 -17
  11. data/lib/aspera/agent/direct.rb +26 -24
  12. data/lib/aspera/agent/httpgw.rb +1 -1
  13. data/lib/aspera/agent/node.rb +32 -0
  14. data/lib/aspera/agent/transferd.rb +39 -3
  15. data/lib/aspera/api/alee.rb +1 -1
  16. data/lib/aspera/api/aoc.rb +14 -16
  17. data/lib/aspera/api/cos_node.rb +1 -1
  18. data/lib/aspera/api/faspex.rb +15 -14
  19. data/lib/aspera/api/httpgw.rb +4 -4
  20. data/lib/aspera/api/node.rb +26 -23
  21. data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
  22. data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
  23. data/lib/aspera/ascmd.rb +21 -19
  24. data/lib/aspera/ascp/installation.rb +37 -38
  25. data/lib/aspera/ascp/management.rb +2 -3
  26. data/lib/aspera/cli/ascp_actions.rb +155 -0
  27. data/lib/aspera/cli/async_transfer_store.rb +81 -0
  28. data/lib/aspera/cli/bootstrapper.rb +197 -0
  29. data/lib/aspera/cli/command_registry.rb +145 -0
  30. data/lib/aspera/cli/command_spec.rb +138 -0
  31. data/lib/aspera/cli/context.rb +16 -3
  32. data/lib/aspera/cli/error.rb +16 -0
  33. data/lib/aspera/cli/extended_value.rb +2 -1
  34. data/lib/aspera/cli/formatter.rb +41 -27
  35. data/lib/aspera/cli/gem_checker.rb +65 -0
  36. data/lib/aspera/cli/hints.rb +2 -2
  37. data/lib/aspera/cli/http.rb +70 -19
  38. data/lib/aspera/cli/info.rb +2 -0
  39. data/lib/aspera/cli/mailer.rb +97 -0
  40. data/lib/aspera/cli/mcp_tool.rb +198 -0
  41. data/lib/aspera/cli/options.schema.yaml +364 -7
  42. data/lib/aspera/cli/{manager.rb → parser.rb} +334 -142
  43. data/lib/aspera/cli/plugins/alee.rb +20 -22
  44. data/lib/aspera/cli/plugins/aoc.rb +1166 -853
  45. data/lib/aspera/cli/plugins/ats.rb +200 -161
  46. data/lib/aspera/cli/plugins/base.rb +457 -100
  47. data/lib/aspera/cli/plugins/basic_auth.rb +14 -4
  48. data/lib/aspera/cli/plugins/config.rb +428 -754
  49. data/lib/aspera/cli/plugins/console.rb +106 -64
  50. data/lib/aspera/cli/plugins/cos.rb +44 -32
  51. data/lib/aspera/cli/plugins/factory.rb +4 -4
  52. data/lib/aspera/cli/plugins/faspex.rb +294 -257
  53. data/lib/aspera/cli/plugins/faspex5.rb +585 -357
  54. data/lib/aspera/cli/plugins/faspio.rb +49 -51
  55. data/lib/aspera/cli/plugins/httpgw.rb +18 -25
  56. data/lib/aspera/cli/plugins/mcp.rb +279 -0
  57. data/lib/aspera/cli/plugins/node.rb +998 -794
  58. data/lib/aspera/cli/plugins/oauth.rb +7 -10
  59. data/lib/aspera/cli/plugins/orchestrator.rb +111 -134
  60. data/lib/aspera/cli/plugins/preview.rb +180 -139
  61. data/lib/aspera/cli/plugins/server.rb +124 -79
  62. data/lib/aspera/cli/plugins/shares.rb +301 -107
  63. data/lib/aspera/cli/preset_actions.rb +139 -0
  64. data/lib/aspera/cli/preset_manager.rb +13 -12
  65. data/lib/aspera/cli/result.rb +61 -11
  66. data/lib/aspera/cli/runner.rb +275 -141
  67. data/lib/aspera/cli/secret_finder.rb +40 -0
  68. data/lib/aspera/cli/special_values.rb +1 -0
  69. data/lib/aspera/cli/sync_actions.rb +83 -59
  70. data/lib/aspera/cli/terminal_formatter.rb +1 -1
  71. data/lib/aspera/cli/transfer_actions.rb +83 -0
  72. data/lib/aspera/cli/transfer_agent.rb +116 -51
  73. data/lib/aspera/cli/transfer_progress.rb +3 -3
  74. data/lib/aspera/cli/vault_manager.rb +57 -0
  75. data/lib/aspera/cli/version.rb +1 -1
  76. data/lib/aspera/cli/wizard.rb +21 -20
  77. data/lib/aspera/command_line_builder.rb +22 -22
  78. data/lib/aspera/data_repository.rb +2 -1
  79. data/lib/aspera/dot_container.rb +4 -4
  80. data/lib/aspera/environment.rb +18 -11
  81. data/lib/aspera/exec_spec.rb +13 -0
  82. data/lib/aspera/faspex_gw.rb +1 -1
  83. data/lib/aspera/faspex_postproc.rb +4 -3
  84. data/lib/aspera/graphql.rb +35 -0
  85. data/lib/aspera/hash_ext.rb +6 -0
  86. data/lib/aspera/json_rpc/client.rb +62 -0
  87. data/lib/aspera/json_rpc/version.rb +7 -0
  88. data/lib/aspera/keychain/encrypted_hash.rb +1 -1
  89. data/lib/aspera/keychain/factory.rb +3 -3
  90. data/lib/aspera/keychain/macos_security.rb +3 -3
  91. data/lib/aspera/link_header.rb +82 -0
  92. data/lib/aspera/log.rb +17 -3
  93. data/lib/aspera/markdown.rb +85 -2
  94. data/lib/aspera/node_simulator.rb +3 -1
  95. data/lib/aspera/oauth/base.rb +5 -5
  96. data/lib/aspera/oauth/boot.rb +2 -2
  97. data/lib/aspera/oauth/factory.rb +7 -5
  98. data/lib/aspera/oauth/jwt.rb +4 -4
  99. data/lib/aspera/oauth/web.rb +2 -2
  100. data/lib/aspera/persistency_action_once.rb +8 -8
  101. data/lib/aspera/persistency_folder.rb +10 -3
  102. data/lib/aspera/preview/file_types.rb +3 -3
  103. data/lib/aspera/preview/generator.rb +3 -3
  104. data/lib/aspera/preview/utils.rb +1 -1
  105. data/lib/aspera/products/connect.rb +1 -1
  106. data/lib/aspera/products/other.rb +1 -1
  107. data/lib/aspera/products/transferd.rb +1 -1
  108. data/lib/aspera/proxy_auto_config.rb +7 -6
  109. data/lib/aspera/rest.rb +10 -36
  110. data/lib/aspera/rest_error_analyzer.rb +3 -3
  111. data/lib/aspera/rest_errors_aspera.rb +0 -10
  112. data/lib/aspera/rest_list.rb +3 -3
  113. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1853 -510
  114. data/lib/aspera/schema/documentation.rb +57 -26
  115. data/lib/aspera/schema/reader.rb +59 -10
  116. data/lib/aspera/schema/registry.rb +39 -6
  117. data/lib/aspera/secret_hider.rb +7 -2
  118. data/lib/aspera/ssl.rb +4 -4
  119. data/lib/aspera/sync/conf.schema.yaml +2 -2
  120. data/lib/aspera/sync/operations.rb +10 -13
  121. data/lib/aspera/temp_file_manager.rb +1 -1
  122. data/lib/aspera/timer_limiter.rb +1 -1
  123. data/lib/aspera/transfer/faux_file.rb +24 -11
  124. data/lib/aspera/transfer/parameters.rb +23 -21
  125. data/lib/aspera/transfer/result.rb +74 -0
  126. data/lib/aspera/transfer/resumer.rb +6 -6
  127. data/lib/aspera/transfer/spec.rb +18 -1
  128. data/lib/aspera/transfer/spec.schema.yaml +12 -3
  129. data/lib/aspera/uri_reader.rb +2 -2
  130. data/lib/aspera/web_auth.rb +2 -2
  131. data/lib/aspera/web_server_simple.rb +8 -5
  132. data/lib/aspera/yaml.rb +2 -1
  133. data.tar.gz.sig +0 -0
  134. metadata +25 -3
  135. metadata.gz.sig +0 -0
  136. data/lib/aspera/json_rpc.rb +0 -52
@@ -46,15 +46,16 @@ module Aspera
46
46
  end
47
47
  end
48
48
 
49
+ application_name 'HSTS Node API'
50
+
51
+ SESSION_TIME_FIELDS = %i[start end].freeze
52
+ private_constant :SESSION_TIME_FIELDS
53
+
49
54
  class << self
50
55
  # directory: node, container: shares
51
56
  FOLDER_TYPES = %w[directory container].freeze
52
57
  private_constant :FOLDER_TYPES
53
58
 
54
- def application_name
55
- 'HSTS Node API'
56
- end
57
-
58
59
  # @return [Hash,NilClass]
59
60
  def detect(address_or_url)
60
61
  urls = if address_or_url.match?(%r{^[a-z]{1,6}://})
@@ -68,7 +69,7 @@ module Aspera
68
69
  end
69
70
  error = nil
70
71
  urls.each do |base_url|
71
- next unless base_url.match?('https?://')
72
+ next unless base_url.match?(%r{^https?://})
72
73
  api = Rest.new(base_url: base_url)
73
74
  test_endpoint = 'ping'
74
75
  http = api.read(test_endpoint, ret: :resp)
@@ -87,20 +88,22 @@ module Aspera
87
88
  return
88
89
  end
89
90
 
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.
90
93
  def declare_options(options)
91
- return if @options_declared
92
- @options_declared = true
93
- @dynamic_key = nil
94
- options.declare(:validator, 'Identifier of validator (optional for central)')
95
- options.declare(:asperabrowserurl, 'URL for simple aspera web ui', default: 'https://asperabrowser.mybluemix.net')
96
- options.declare(
97
- :node_api, 'Gen4: standard_ports: Use standard FASP ports (true) or get from node API (false). cache: Set to false to force actual file system read',
98
- allowed: Hash,
99
- handler: {o: Api::Node, m: :api_options}
100
- )
101
- options.declare(:root_id, 'Gen4: File id of top folder when using access key (override AK root id)')
102
- options.declare(:dynamic_key, 'Private key PEM to use for dynamic key auth', handler: {o: Api::Node, m: :use_dynamic_key})
103
- SyncActions.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
104
107
  options.parse_options!
105
108
  end
106
109
 
@@ -110,8 +113,17 @@ module Aspera
110
113
  end
111
114
  end
112
115
 
116
+ # 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).
118
+ option :validator, description: 'Identifier of validator (optional for central)'
119
+ option :asperabrowserurl, description: 'URL for simple aspera web ui', default: 'https://asperabrowser.mybluemix.net'
120
+ 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',
121
+ allowed: Hash, handler: {o: Api::Node, m: :api_options}
122
+ option :root_id, description: 'Gen4: File id of top folder when using access key (override AK root id)'
123
+ option :dynamic_key, description: 'Private key PEM to use for dynamic key auth', handler: {o: Api::Node, m: :use_dynamic_key}
124
+
113
125
  # @param wizard [Wizard] The wizard object
114
- # @param app_url [Wizard] The wizard object
126
+ # @param app_url [String] Tested URL
115
127
  # @return [Hash] :preset_value, :test_args
116
128
  def wizard(wizard, app_url)
117
129
  return {
@@ -146,10 +158,7 @@ module Aspera
146
158
  # commands for : `execute_simple_common`: actions used commonly when a node is involved
147
159
  COMMON_ACTIONS = %i[access_keys].concat(BASE_ACTIONS).concat(SPECIAL_ACTIONS).freeze
148
160
 
149
- # actions available in v3 in gen4
150
- V3_IN_V4_ACTIONS = %i[transfer].concat(COMMON_ACTIONS).freeze
151
-
152
- private_constant :CENTRAL_SOAP_API_TEST, :SEARCH_REMOVE_FIELDS, :BASE_ACTIONS, :SPECIAL_ACTIONS, :V3_IN_V4_ACTIONS, :COMMON_ACTIONS
161
+ private_constant :CENTRAL_SOAP_API_TEST, :SEARCH_REMOVE_FIELDS, :BASE_ACTIONS, :SPECIAL_ACTIONS, :COMMON_ACTIONS
153
162
 
154
163
  # used in aoc
155
164
  NODE4_READ_ACTIONS = %i[bearer_token_node node_info browse find].freeze
@@ -196,9 +205,9 @@ module Aspera
196
205
  end
197
206
 
198
207
  # Gen3 API
199
- def browse_gen3
200
- folders_to_process = options.get_next_argument('path', validation: String)
201
- folders_to_process = @node_path_prefix.add_to_path(folders_to_process) unless @node_path_prefix.nil?
208
+ # @param path [String] starting path
209
+ def browse_gen3(path)
210
+ folders_to_process = @node_path_prefix.nil? ? path : @node_path_prefix.add_to_path(path)
202
211
  folders_to_process = [folders_to_process]
203
212
  query = options.get_option(:query) || {}
204
213
  # special parameter: max number of entries in result
@@ -211,7 +220,7 @@ module Aspera
211
220
  single_call = query.key?('skip')
212
221
  # API default is 100, so use 1000 for default
213
222
  query['count'] ||= 1000
214
- raise Cli::BadArgument, 'options `recursive` and `skip` cannot be used together' if recursive && single_call
223
+ Aspera.assert(!(recursive && single_call), type: Cli::BadArgument){'options `recursive` and `skip` cannot be used together'}
215
224
  all_items = []
216
225
  until folders_to_process.empty?
217
226
  path = folders_to_process.shift
@@ -252,9 +261,9 @@ module Aspera
252
261
  end
253
262
 
254
263
  # Create async transfer spec request from direction and folders
255
- # @param sync_direction one of push pull bidi
256
- # @param local_path local folder to sync
257
- # @param remote_path remote folder to sync
264
+ # @param sync_direction [Symbol] one of push pull bidi
265
+ # @param local_path [String] local folder to sync
266
+ # @param remote_path [String] remote folder to sync
258
267
  def sync_spec_request(sync_direction, local_path, remote_path)
259
268
  case sync_direction
260
269
  when :push then {
@@ -282,382 +291,28 @@ module Aspera
282
291
  end
283
292
  end
284
293
 
285
- # Commands based on Gen3 API for file and folder
286
- def execute_command_gen3(command)
287
- case command
288
- when :delete
289
- # TODO: add query for recursive
290
- paths_to_delete = options.get_next_argument('file list', multiple: true)
291
- @node_path_prefix&.add_to_paths!(paths_to_delete)
292
- resp = @api_node.create('files/delete', {paths: paths_to_delete.map{ |i| {'path' => i.start_with?('/') ? i : "/#{i}"}}})
293
- return cli_result_from_paths_response(resp, 'file deleted')
294
- when :search
295
- search_root = options.get_next_argument('search root', validation: String)
296
- search_root = @node_path_prefix.add_to_path(search_root) unless @node_path_prefix.nil?
297
- parameters = {'path' => search_root}
298
- other_options = options.get_option(:query)
299
- parameters.merge!(other_options) unless other_options.nil?
300
- resp = @api_node.create('files/search', parameters)
301
- return Result::Empty.new if resp['items'].empty?
302
- fields = resp['items'].first.keys.reject{ |i| SEARCH_REMOVE_FIELDS.include?(i)}
303
- formatter.display_item_count(resp['item_count'], resp['total_count'])
304
- formatter.display_status("params: #{resp['parameters'].keys.map{ |k| "#{k}:#{resp['parameters'][k]}"}.join(',')}")
305
- @node_path_prefix&.remove_in_object_list!(resp['items'])
306
- return Result::ObjectList.new(resp['items'], fields: fields)
307
- when :space
308
- path_list = options.get_next_argument('folder path or ext.val. list', multiple: true)
309
- @node_path_prefix&.add_to_paths!(path_list)
310
- resp = @api_node.create('space', {'paths' => path_list.map{ |i| {path: i}}})
311
- @node_path_prefix&.remove_in_object_list!(resp['paths'])
312
- return Result::ObjectList.new(resp['paths'])
313
- when :mkdir
314
- path_list = options.get_next_argument('folder path or ext.val. list', multiple: true)
315
- @node_path_prefix&.add_to_paths!(path_list)
316
- resp = @api_node.create('files/create', {'paths' => path_list.map{ |i| {type: :directory, path: i}}})
317
- return cli_result_from_paths_response(resp, 'folder created')
318
- when :mklink
319
- target = options.get_next_argument('target', validation: String)
320
- target = @node_path_prefix.add_to_path(target) unless @node_path_prefix.nil?
321
- one_path = options.get_next_argument('link path', validation: String)
322
- one_path = @node_path_prefix.add_to_path(one_path) unless @node_path_prefix.nil?
323
- resp = @api_node.create('files/create', {'paths' => [{type: :symbolic_link, path: one_path, target: {path: target}}]})
324
- return cli_result_from_paths_response(resp, 'link created')
325
- when :mkfile
326
- one_path = options.get_next_argument('file path', validation: String)
327
- one_path = @node_path_prefix.add_to_path(one_path) unless @node_path_prefix.nil?
328
- contents64 = Base64.strict_encode64(options.get_next_argument('contents'))
329
- resp = @api_node.create('files/create', {'paths' => [{type: :file, path: one_path, contents: contents64}]})
330
- return cli_result_from_paths_response(resp, 'file created')
331
- when :rename
332
- # TODO: multiple ?
333
- path_base = options.get_next_argument('path_base', validation: String)
334
- path_base = @node_path_prefix.add_to_path(path_base) unless @node_path_prefix.nil?
335
- path_src = options.get_next_argument('path_src', validation: String)
336
- path_src = @node_path_prefix.add_to_path(path_src) unless @node_path_prefix.nil?
337
- path_dst = options.get_next_argument('path_dst', validation: String)
338
- path_dst = @node_path_prefix.add_to_path(path_dst) unless @node_path_prefix.nil?
339
- resp = @api_node.create('files/rename', {'paths' => [{'path' => path_base, 'source' => path_src, 'destination' => path_dst}]})
340
- return cli_result_from_paths_response(resp, 'entry moved')
341
- when :browse
342
- return browse_gen3
343
- when :sync
344
- return execute_sync_action do |sync_direction, local_path, remote_path|
345
- # Gen3 API
346
- # empty transfer spec for authorization request
347
- request_transfer_spec = sync_spec_request(sync_direction, local_path, remote_path)
348
- # add fixed parameters if any (for COS)
349
- @api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
350
- # prepare payload for single request
351
- setup_payload = {transfer_requests: [{transfer_request: request_transfer_spec}]}
352
- # only one request, so only one answer
353
- transfer_spec = @api_node.create('files/sync_setup', setup_payload)['transfer_specs'].first['transfer_spec']
354
- # API returns null tag... but async does not like it
355
- transfer_spec.delete_if{ |_k, v| v.nil?}
356
- # delete this part, as the returned value contains only destination, and not sources
357
- # transfer_spec.delete('paths') if command.eql?(:upload)
358
- Log.dump(:ts, transfer_spec)
359
- transfer_spec
360
- end
361
- when :upload, :download
362
- # empty transfer spec for authorization request
363
- request_transfer_spec = {}
364
- # set requested paths depending on direction
365
- request_transfer_spec[:paths] = if command.eql?(:download)
366
- transfer.ts_source_paths
367
- else
368
- [{destination: transfer.destination_folder(Transfer::Spec::DIRECTION_SEND)}]
369
- end
370
- # add fixed parameters if any (for COS)
371
- @api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
372
- Api::Node.add_public_key(request_transfer_spec)
373
- # prepare payload for single request
374
- setup_payload = {transfer_requests: [{transfer_request: request_transfer_spec}]}
375
- # only one request, so only one answer
376
- transfer_spec = @api_node.create("files/#{command}_setup", setup_payload)['transfer_specs'].first['transfer_spec']
377
- Api::Node.add_private_key(transfer_spec)
378
- # delete this part, as the returned value contains only destination, and not sources
379
- transfer_spec.delete('paths') if command.eql?(:upload)
380
- return Runner.result_transfer(transfer.start(transfer_spec))
381
- when :cat
382
- remote_path = options.get_next_argument('remote path', validation: String)
383
- remote_path = @node_path_prefix.add_to_path(remote_path) unless @node_path_prefix.nil?
384
- File.basename(remote_path)
385
- http = @api_node.read("files/#{URI.encode_www_form_component(remote_path)}/contents", ret: :resp)
386
- return Result::Text.new(http.body)
387
- when :transport
388
- return Result::SingleObject.new(@api_node.transport_params)
389
- when :spec
390
- return Result::SingleObject.new(@api_node.base_spec, fields: Formatter.all_but(Transfer::Spec::SPECIFIC))
391
- end
392
- Aspera.error_unreachable_line
393
- end
394
-
395
- # common API to node and Shares
396
- def execute_simple_common(command)
397
- case command
398
- when *COMMANDS_GEN3
399
- execute_command_gen3(command)
400
- when :access_keys
401
- ak_command = options.get_next_command(%i[do set_bearer_key].concat(Operations::ALL))
402
- case ak_command
403
- when *Operations::ALL
404
- return entity_execute(
405
- api: @api_node,
406
- entity: 'access_keys',
407
- command: ak_command
408
- ) do |field, value|
409
- raise BadArgument, 'only selector: %id:self' unless field.eql?('id') && value.eql?('self')
410
- @api_node.read('access_keys/self')['id']
411
- end
412
- when :do
413
- access_key_id = options.get_next_argument('access key id')
414
- root_file_id = options.get_option(:root_id)
415
- if root_file_id.nil?
416
- ak_info = @api_node.read("access_keys/#{access_key_id}")
417
- ak_secret = config.lookup_secret(url: @api_node.base_url, username: ak_info['id'])
418
- # change API credentials if different access key
419
- if !access_key_id.eql?('self')
420
- 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}."}
421
- @api_node.auth_params[:username] = ak_info['id']
422
- @api_node.auth_params[:password] = ak_secret
423
- end
424
- root_file_id = ak_info['root_file_id']
425
- end
426
- command_repo = options.get_next_command(COMMANDS_GEN4)
427
- return execute_command_gen4(command_repo, root_file_id)
428
- when :set_bearer_key
429
- access_key_id = options.get_next_argument('access key id')
430
- access_key_id = @api_node.read('access_keys/self')['id'] if access_key_id.eql?('self')
431
- bearer_key_pem = options.get_next_argument('public or private RSA key PEM value', validation: String)
432
- key = OpenSSL::PKey.read(bearer_key_pem)
433
- key = key.public_key if key.private?
434
- bearer_key_pem = key.to_pem
435
- @api_node.update("access_keys/#{access_key_id}", {token_verification_key: bearer_key_pem})
436
- return Result::Status.new('public key updated')
437
- end
438
- when :health
439
- nagios = Nagios.new
440
- begin
441
- info = @api_node.read('info')
442
- nagios.add_ok('node api', 'accessible')
443
- nagios.check_time_offset(info['current_time'], 'node api')
444
- nagios.check_product_version('node api', 'entsrv', info['version'])
445
- rescue StandardError => e
446
- nagios.add_critical('node api', e.to_s)
447
- end
448
- begin
449
- @api_node.call(
450
- operation: 'POST',
451
- subpath: 'services/soap/Transfer-201210',
452
- content_type: Mime::TEXT,
453
- body: CENTRAL_SOAP_API_TEST,
454
- headers: {'Content-Type' => 'text/xml;charset=UTF-8', 'SOAPAction' => 'FASPSessionNET-200911#GetSessionInfo'},
455
- ret: :resp
456
- ).body
457
- nagios.add_ok('central', 'accessible by node')
458
- rescue StandardError => e
459
- nagios.add_critical('central', e.to_s)
460
- end
461
- Result::ObjectList.new(nagios.status_list)
462
- when :events
463
- events = @api_node.read('events', query_read_delete)
464
- return Result::ObjectList.new(events, fields: ->(f){!f.start_with?('data')})
465
- when :info
466
- nd_info = @api_node.read('info')
467
- return Result::SingleObject.new(nd_info)
468
- when :slash
469
- nd_info = @api_node.read('')
470
- return Result::SingleObject.new(nd_info)
471
- when :license
472
- # requires: asnodeadmin -mu <node user> --acl-add=internal --internal
473
- return Result::SingleObject.new(@api_node.read('license'))
474
- when :api_details
475
- return Result::SingleObject.new({base_url: @api_node.base_url}.merge(@api_node.params))
294
+ # Resolve a NodeFileId from a path argument.
295
+ # Supports %id:<file_id> syntax (returns NodeFileId for that id) or a plain path.
296
+ # @param top_file_id [String] root file id for path resolution
297
+ # @param path [String] plain path or %id:<file_id> selector
298
+ def apifid_from_path(top_file_id, path)
299
+ if (m = Parser.percent_selector(path))
300
+ Aspera.assert_values(m[:field], ['id'], type: BadArgument){'file id'}
301
+ val = m[:value]
302
+ return Api::NodeFileId.new(@api_node, val.nil? || val.empty? ? top_file_id : val)
476
303
  end
304
+ @api_node.resolve_api_fid(top_file_id, path)
477
305
  end
478
306
 
479
- # Allows to specify a file by its path or by its id on the node in command line
480
- # @return [NodeFileId] api and main file id for given path or id in next argument
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).
481
310
  def apifid_from_next_arg(top_file_id)
482
311
  file_path = options.instance_identifier(description: 'path or %id:<id> or %id:') do |attribute, value|
483
- raise BadArgument, 'Only selection "id" is supported (file id)' unless attribute.eql?('id')
484
- # Directly return result for method
485
- return Api::NodeFileId.new(@api_node, value)
486
- end
487
- # There was no selector, so it is a path
488
- return @api_node.resolve_api_fid(top_file_id, file_path)
489
- end
490
-
491
- def execute_command_gen4(command_repo, top_file_id)
492
- override_file_id = options.get_option(:root_id)
493
- top_file_id = override_file_id unless override_file_id.nil?
494
- raise Cli::Error, 'Specify root file id with option root_id' if top_file_id.nil?
495
- case command_repo
496
- when :v3
497
- # NOTE: other common actions are unauthorized with user scope
498
- command_legacy = options.get_next_command(V3_IN_V4_ACTIONS)
499
- # TODO: shall we support all methods here ? what if there is a link ?
500
- apifid = @api_node.resolve_api_fid(top_file_id, '')
501
- return Node.new(context: context, api: apifid.node_api).execute_action(command_legacy)
502
- when :node_info, :bearer_token_node
503
- apifid = apifid_from_next_arg(top_file_id)
504
- result = {
505
- url: apifid.node_api.base_url,
506
- root_id: apifid.file_id
507
- }
508
- case apifid.node_api.auth_params[:type]
509
- when :basic
510
- result[:username] = apifid.node_api.auth_params[:username]
511
- result[:password] = apifid.node_api.auth_params[:password]
512
- when :oauth2
513
- result[:username] = apifid.node_api.params[:headers][Api::Node::HEADER_X_ASPERA_ACCESS_KEY]
514
- result[:password] = apifid.node_api.oauth.authorization
515
- else Aspera.error_unexpected_value(apifid.node_api.auth_params[:type]){'Node API Auth type'}
516
- end
517
- return Result::SingleObject.new(result) if command_repo.eql?(:node_info)
518
- Log.dump(:result, result)
519
- raise BadArgument, "Cannot get bearer token if authenticating with secret (#{apifid.node_api.auth_params[:type]})" unless apifid.node_api.auth_params[:type].eql?(:oauth2)
520
- Aspera.assert(OAuth::Factory.bearer_auth?(result[:password]), 'Not using bearer token auth')
521
- return Result::Text.new(result[:password])
522
- when :browse
523
- apifid = apifid_from_next_arg(top_file_id)
524
- file_info = apifid.node_api.read("files/#{apifid.file_id}", headers: Api::Node.add_cache_control)
525
- # a single file
526
- return Result::ObjectList.new([file_info], fields: GEN4_LS_FIELDS) unless file_info['type'].eql?('folder')
527
- return Result::ObjectList.new(apifid.node_api.list_files(apifid.file_id, query: query_read_delete), fields: GEN4_LS_FIELDS)
528
- when :find
529
- apifid = apifid_from_next_arg(top_file_id)
530
- find_lambda = Api::Node.file_matcher_from_argument(options)
531
- return Result::ObjectList.new(@api_node.find_files(apifid.file_id, find_lambda), fields: ['path'])
532
- when :mkdir, :mklink, :mkfile
533
- containing_folder_path, new_item = Api::Node.split_folder(options.get_next_argument('path'))
534
- apifid = @api_node.resolve_api_fid(top_file_id, containing_folder_path, true)
535
- query = options.get_option(:query)
536
- check_exists = true
537
- payload = {name: new_item}
538
- if query
539
- check_exists = !query.delete('check').eql?(false)
540
- target = query.delete('target')
541
- if target
542
- target_apifid = @api_node.resolve_api_fid(top_file_id, target, true)
543
- payload[:target_id] = target_apifid.file_id
544
- end
545
- payload.merge!(query.symbolize_keys)
546
- end
547
- if check_exists
548
- folder_content = apifid.node_api.read("files/#{apifid.file_id}/files")
549
- link_name = ".#{new_item}.asp-lnk"
550
- found = folder_content.find{ |i| i['name'].eql?(new_item) || i['name'].eql?(link_name)}
551
- raise "A #{found['type']} already exists with name #{new_item}" if found
552
- end
553
- case command_repo
554
- when :mkdir
555
- payload[:type] = :folder
556
- when :mklink
557
- payload[:type] = :link
558
- Aspera.assert(payload[:target_id], 'Missing target_id')
559
- Aspera.assert(payload[:target_node_id], 'Missing target_node_id')
560
- when :mkfile
561
- payload[:type] = :file
562
- payload[:contents] = Base64.strict_encode64(options.get_next_argument('contents'))
563
- end
564
- result = apifid.node_api.create("files/#{apifid.file_id}/files", payload)
565
- return Result::SingleObject.new(result)
566
- when :rename
567
- file_path = options.get_next_argument('source path')
568
- apifid = @api_node.resolve_api_fid(top_file_id, file_path)
569
- newname = options.get_next_argument('new name')
570
- result = apifid.node_api.update("files/#{apifid.file_id}", {name: newname})
571
- return Result::Status.new("renamed to #{newname}")
572
- when :delete
573
- return do_bulk_operation(command: command_repo, descr: 'path', values: String, id_result: 'path') do |l_path|
574
- apifid = if (m = Manager.percent_selector(l_path))
575
- Aspera.assert_values(m[:field], ['id'], type: BadIdentifier)
576
- Api::NodeFileId.new(@api_node, m[:value])
577
- else
578
- @api_node.resolve_api_fid(top_file_id, l_path)
579
- end
580
- result = apifid.node_api.delete("files/#{apifid.file_id}")
581
- {'path' => l_path}
582
- end
583
- when :sync
584
- return execute_sync_action do |sync_direction, _local_path, remote_path|
585
- # Gen4 API
586
- Aspera.assert_values(sync_direction, %i[push pull bidi])
587
- ts_direction = case sync_direction
588
- when :push, :bidi then Transfer::Spec::DIRECTION_SEND
589
- when :pull then Transfer::Spec::DIRECTION_RECEIVE
590
- else Aspera.error_unreachable_line
591
- end
592
- # remote is specified by option: `to_folder`
593
- apifid = @api_node.resolve_api_fid(top_file_id, remote_path)
594
- apifid.node_api.transfer_spec_gen4(apifid.file_id, ts_direction)
595
- end
596
- when :upload
597
- apifid = @api_node.resolve_api_fid(top_file_id, transfer.destination_folder(Transfer::Spec::DIRECTION_SEND), true)
598
- return Runner.result_transfer(transfer.start(apifid.node_api.transfer_spec_gen4(apifid.file_id, Transfer::Spec::DIRECTION_SEND)))
599
- when :download
600
- apifid, source_paths = @api_node.resolve_api_fid_paths(top_file_id, transfer.ts_source_paths)
601
- return Runner.result_transfer(transfer.start(apifid.node_api.transfer_spec_gen4(apifid.file_id, Transfer::Spec::DIRECTION_RECEIVE, {'paths'=>source_paths})))
602
- when :cat
603
- apifid = apifid_from_next_arg(top_file_id)
604
- http = apifid.node_api.read("files/#{apifid.file_id}/content", ret: :resp)
605
- return Result::Text.new(http.body)
606
- when :show
607
- apifid = apifid_from_next_arg(top_file_id)
608
- items = apifid.node_api.read("files/#{apifid.file_id}")
609
- return Result::SingleObject.new(items)
610
- when :modify
611
- apifid = apifid_from_next_arg(top_file_id)
612
- update_param = options.get_next_argument('update data', validation: Hash)
613
- apifid.node_api.update("files/#{apifid.file_id}", update_param)
614
- return Result::Status.new('Done')
615
- when :thumbnail
616
- apifid = apifid_from_next_arg(top_file_id)
617
- http = apifid.node_api.read("files/#{apifid.file_id}/preview", headers: {'Accept' => 'image/png'}, ret: :resp)
618
- return Result::Image.new(http.body)
619
- when :permission
620
- apifid = apifid_from_next_arg(top_file_id)
621
- command_perm = options.get_next_command(%i[list show create delete modify])
622
- case command_perm
623
- when :modify
624
- apifid.node_api.update("permissions/#{options.instance_identifier}", value_create_modify(command: 'permission modify'))
625
- return Result::Status.new('Updated')
626
- when :list
627
- list_query = query_read_delete(default: Rest.php_style({'include' => %w[access_level permission_count]}))
628
- # Specify file to get permissions for unless not specified (then, get all permissions)
629
- list_query['file_id'] = apifid.file_id unless apifid.file_id.to_s.empty?
630
- list_query['inherited'] = false if list_query.key?('file_id') && !list_query.key?('inherited')
631
- items = apifid.node_api.read_with_pages('permissions', list_query)
632
- return Result::ObjectList.new(items)
633
- when :show
634
- return Result::SingleObject.new(apifid.node_api.read("permissions/#{options.instance_identifier}"))
635
- when :delete
636
- return do_bulk_operation(command: command_perm, values: :identifier) do |one_id|
637
- apifid.node_api.delete("permissions/#{one_id}")
638
- # notify application of deletion
639
- the_app = apifid.node_api.app_info
640
- the_app&.api&.permissions_send_event(event_data: {}, app_info: the_app, types: ['permission.deleted'])
641
- {'id' => one_id}
642
- end
643
- when :create
644
- create_param = options.get_next_argument('creation data', validation: Hash)
645
- raise Cli::BadArgument, 'no file_id' if create_param.key?('file_id')
646
- create_param['file_id'] = apifid.file_id
647
- create_param['access_levels'] = Api::Node::ACCESS_LEVELS unless create_param.key?('access_levels')
648
- # add application specific tags (AoC)
649
- the_app = apifid.node_api.app_info
650
- the_app&.api&.permissions_set_create_params(perm_data: create_param, app_info: the_app)
651
- # create permission
652
- created_data = apifid.node_api.create('permissions', create_param)
653
- # notify application of creation
654
- the_app&.api&.permissions_send_event(event_data: created_data, app_info: the_app)
655
- return Result::SingleObject.new(created_data)
656
- else Aspera.error_unreachable_line
657
- end
658
- else Aspera.error_unreachable_line
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)
659
314
  end
660
- Aspera.error_unreachable_line
315
+ @api_node.resolve_api_fid(top_file_id, file_path)
661
316
  end
662
317
 
663
318
  # Search /async by name
@@ -666,7 +321,7 @@ module Aspera
666
321
  # @return [Integer] id of the sync
667
322
  # @raise [Cli::BadArgument] if no such sync, or not by name
668
323
  def async_lookup(field, value)
669
- raise Cli::BadArgument, "Only search by name is supported (#{field})" unless field.eql?('name')
324
+ Aspera.assert_values(field, ['name'], type: Cli::BadArgument){'search field'}
670
325
  async_ids = @api_node.read('async/list')['sync_ids']
671
326
  summaries = @api_node.create('async/summary', {'syncs' => async_ids})['sync_summaries']
672
327
  selected = summaries.find{ |s| s['name'].eql?(value)}
@@ -674,82 +329,13 @@ module Aspera
674
329
  return selected['snid']
675
330
  end
676
331
 
677
- # Node API: /async (stats only)
678
- def execute_async
679
- command = options.get_next_command(%i[list delete files show counters bandwidth])
680
- unless command.eql?(:list)
681
- async_id = options.instance_identifier{ |field, value| async_lookup(field, value)}
682
- if async_id.eql?(SpecialValues::ALL)
683
- raise Cli::BadArgument, 'ALL only for show and delete' unless %i[show delete].include?(command)
684
- async_ids = @api_node.read('async/list')['sync_ids']
685
- else
686
- Integer(async_id) # must be integer
687
- async_ids = [async_id]
688
- end
689
- post_data = {'syncs' => async_ids}
690
- end
691
- case command
692
- when :list
693
- resp = @api_node.read('async/list')['sync_ids']
694
- return Result::ValueList.new(resp)
695
- when :show
696
- resp = @api_node.create('async/summary', post_data)['sync_summaries']
697
- return Result::Empty.new if resp.empty?
698
- return Result::ObjectList.new(resp, fields: %w[snid name local_dir remote_dir]) if async_id.eql?(SpecialValues::ALL)
699
- return Result::SingleObject.new(resp.first)
700
- when :delete
701
- resp = @api_node.create('async/delete', post_data)
702
- return Result::SingleObject.new(resp)
703
- when :bandwidth
704
- post_data['seconds'] = 100 # TODO: as parameter with --query
705
- resp = @api_node.create('async/bandwidth', post_data)
706
- data = resp['bandwidth_data']
707
- return Result::Empty.new if data.empty?
708
- data = data.first[async_id]['data']
709
- return Result::ObjectList.new(data)
710
- when :files
711
- # count int
712
- # filename str
713
- # skip int
714
- # status int
715
- filter = options.get_option(:query)
716
- post_data.merge!(filter) unless filter.nil?
717
- resp = @api_node.create('async/files', post_data)
718
- data = resp['sync_files']
719
- data = data.first[async_id] unless data.empty?
720
- iteration_data = []
721
- skip_ids_persistency = nil
722
- if options.get_option(:once_only, mandatory: true)
723
- skip_ids_persistency = PersistencyActionOnce.new(
724
- manager: persistency,
725
- data: iteration_data,
726
- id: IdGenerator.from_list(
727
- 'sync_files',
728
- options.get_option(:url, mandatory: true),
729
- options.get_option(:username, mandatory: true),
730
- async_id
731
- )
732
- )
733
- data.select!{ |l| l['fnid'].to_i > iteration_data.first} unless iteration_data.first.nil?
734
- iteration_data[0] = data.last['fnid'].to_i unless data.empty?
735
- end
736
- return Result::Empty.new if data.empty?
737
- skip_ids_persistency&.save
738
- return Result::ObjectList.new(data)
739
- when :counters
740
- resp = @api_node.create('async/counters', post_data)['sync_counters'].first[async_id].last
741
- return Result::Empty.new if resp.nil?
742
- return Result::SingleObject.new(resp)
743
- end
744
- end
745
-
746
332
  # Search /asyncs by name
747
333
  # @param field [String] name of the field to search
748
334
  # @param value [String] value of the field to search
749
335
  # @return [Integer] id of the sync
750
336
  # @raise [Cli::BadArgument] if no such sync, or not by name
751
337
  def ssync_lookup(field, value)
752
- raise Cli::BadArgument, "Only search by name is supported (#{field})" unless field.eql?('name')
338
+ Aspera.assert_values(field, ['name'], type: Cli::BadArgument){'search field'}
753
339
  @api_node.read('asyncs')['ids'].each do |id|
754
340
  sync_info = @api_node.read("asyncs/#{id}")['configuration']
755
341
  # name is unique, so we can return
@@ -758,355 +344,973 @@ module Aspera
758
344
  raise Cli::BadIdentifier.new('ssync', value, field: field)
759
345
  end
760
346
 
761
- WATCH_FOLDER_MUL = %i[create list].freeze
762
- WATCH_FOLDER_SING = %i[show modify delete state].freeze
763
- private_constant :WATCH_FOLDER_MUL, :WATCH_FOLDER_SING
347
+ # --- DSL command declarations ---
348
+
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
373
+ Sync::Operations::DIRECTIONS.each do |dir|
374
+ command(dir, description: "#{dir.capitalize}-sync (Gen3)", transfer_paths: :send)
375
+ end
376
+ 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'))}
395
+ # access_keys sub-tree
396
+ command :access_keys, description: 'Manage access keys'
397
+ commands_under(:access_keys) do
398
+ command :do, description: 'Execute Gen4 command via access key',
399
+ arguments: [{name: :access_key_id, type: :identifier}],
400
+ setup: :setup_access_key_do
401
+ command :set_bearer_key, description: 'Set bearer key on access key',
402
+ 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
764
409
 
765
- def watch_folder_action
766
- res_class_path = 'v3/watchfolders'
767
- command = options.get_next_command(WATCH_FOLDER_MUL + WATCH_FOLDER_SING)
768
- if WATCH_FOLDER_SING.include?(command)
769
- one_res_id = options.instance_identifier
770
- one_res_path = "#{res_class_path}/#{one_res_id}"
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)
423
+ end
424
+ command :sync, description: 'Synchronize folders (Gen4)'
425
+ commands_under(%i[access_keys do sync]) do
426
+ Sync::Operations::DIRECTIONS.each do |dir|
427
+ command(dir, description: "#{dir.capitalize}-sync (Gen4)", transfer_paths: :send)
428
+ end
429
+ command :admin, description: 'Manage sync database (admin operations)'
430
+ SyncActions.register_sync_admin_commands(self, %i[access_keys do sync admin])
771
431
  end
772
- # hum, to avoid: Unable to convert 2016_09_14 configuration
432
+ end
433
+ commands_under(%i[access_keys do permission]) do
434
+ command :list, description: 'List permissions on a file'
435
+ command :show, description: 'Show a permission',
436
+ arguments: [{name: :perm_id, type: :identifier}],
437
+ action: ->(apifid:, perm_id:, **){Result::SingleObject.new(apifid.node_api.read("permissions/#{perm_id}"))}
438
+ command :create, description: 'Create a permission',
439
+ arguments: [{name: :data, type: Hash}]
440
+ command :modify, description: 'Modify a permission',
441
+ arguments: [{name: :perm_id, type: :identifier}, {name: :data, type: Hash}]
442
+ command :delete, description: 'Delete permission(s)',
443
+ arguments: [{name: :perm_id, bulk: true}]
444
+ end
445
+ # 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'])}
449
+ command :show, description: 'Show async summary',
450
+ arguments: [{name: :async_id, type: :identifier, lookup: :async_lookup}]
451
+ command :delete, description: 'Delete async',
452
+ arguments: [{name: :async_id, type: :identifier, lookup: :async_lookup}]
453
+ command :bandwidth, description: 'Show async bandwidth',
454
+ arguments: [{name: :async_id, type: :identifier, lookup: :async_lookup}]
455
+ command :files, description: 'List async files',
456
+ arguments: [{name: :async_id, type: :identifier, lookup: :async_lookup}]
457
+ command :counters, description: 'Show async counters',
458
+ arguments: [{name: :async_id, type: :identifier, lookup: :async_lookup}]
459
+ end
460
+ # 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',
472
+ arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
473
+ action: :action_ssync_start
474
+ command :stop, description: 'Stop a sync',
475
+ arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
476
+ action: :action_ssync_stop
477
+ command :bandwidth, description: 'Show sync bandwidth',
478
+ 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',
481
+ 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',
484
+ 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',
487
+ 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',
490
+ arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
491
+ action: ->(ssync_id:, **){Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/summary"))}
492
+ end
493
+ # stream
494
+ 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])}
497
+ command :create, description: 'Create a stream',
498
+ arguments: [{name: :data, type: Hash}],
499
+ action: ->(data:, **){Result::SingleObject.new(@api_node.create('streams', data))}
500
+ command :show, description: 'Show a stream',
501
+ arguments: [{name: :transfer_id, type: :identifier}],
502
+ action: ->(transfer_id:, **){Result::SingleObject.new(@api_node.read("ops/transfers/#{transfer_id}"))}
503
+ 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))}
506
+ command :cancel, description: 'Cancel a stream',
507
+ arguments: [{name: :transfer_id, type: :identifier}],
508
+ action: ->(transfer_id:, **){Result::SingleObject.new(@api_node.cancel("streams/#{transfer_id}"))}
509
+ end
510
+ # transfer
511
+ command :transfer, description: 'Manage transfer operations'
512
+ commands_under(:transfer) do
513
+ command :list, description: 'List transfers'
514
+ command :cancel, description: 'Cancel a transfer',
515
+ arguments: [{name: :transfer_id, type: :identifier}]
516
+ command :show, description: 'Show a transfer',
517
+ arguments: [{name: :transfer_id, type: :identifier}],
518
+ action: ->(transfer_id:, **){Result::SingleObject.new(@api_node.read("ops/transfers/#{transfer_id}"))}
519
+ command :modify, description: 'Modify a transfer',
520
+ arguments: [{name: :transfer_id, type: :identifier}, {name: :update_value, type: Hash}]
521
+ command :bandwidth_average, description: 'Show average bandwidth per period'
522
+ command :sessions, description: 'List transfer sessions'
523
+ end
524
+ # service
525
+ 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'])}
528
+ command :create, description: 'Create a service',
529
+ arguments: [{name: :creation_data, type: Hash}]
530
+ command :delete, description: 'Delete a service',
531
+ arguments: [{name: :service_id, type: :identifier}]
532
+ end
533
+ # watch_folder
534
+ command :watch_folder, description: 'Manage watch folders', setup: :setup_watch_folder
535
+ commands_under(:watch_folder) do
536
+ command :create, description: 'Create a watch folder',
537
+ arguments: [{name: :data, type: Hash}],
538
+ action: ->(data:, **){Result::Status.new("#{@api_node.create('v3/watchfolders', data)['id']} created")}
539
+ command :list, description: 'List watch folders',
540
+ action: ->{Result::ValueList.new(@api_node.read('v3/watchfolders', query_read_delete)['ids'])}
541
+ 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}"))}
544
+ command :modify, description: 'Modify a watch folder',
545
+ arguments: [{name: :res_id, type: :identifier}, {name: :data, type: Hash}]
546
+ command :delete, description: 'Delete a watch folder',
547
+ arguments: [{name: :res_id, type: :identifier}]
548
+ 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"))}
551
+ end
552
+ # central
553
+ command :central, description: 'Query Central service'
554
+ commands_under(:central) do
555
+ command :session, description: 'Query sessions'
556
+ command :file, description: 'Query files'
557
+ end
558
+ commands_under(%i[central session]) do
559
+ command :list, description: 'List sessions',
560
+ arguments: [{name: :request_data, type: Hash, mandatory: false, default: nil}]
561
+ end
562
+ commands_under(%i[central file]) do
563
+ command :list, description: 'List file transfers',
564
+ arguments: [{name: :request_data, type: Hash, mandatory: false, default: nil}]
565
+ command :modify, description: 'Modify file transfer validation',
566
+ arguments: [{name: :request_data, type: Hash, mandatory: false, default: nil}]
567
+ end
568
+ # Standalone leaf commands
569
+ 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)))}
571
+ command :bearer_token, description: 'Generate bearer token',
572
+ arguments: [{name: :private_key_pem, type: String}, {name: :user_group_id, type: Hash}]
573
+ command :simulator, description: 'Start node simulator',
574
+ arguments: [{name: :parameters, type: Hash, mandatory: false, default: {}}]
575
+ command :telemetry, description: 'Report telemetry to external system',
576
+ arguments: [{name: :parameters, type: Hash, mandatory: false, default: {}}]
577
+
578
+ # --- Handler methods (Gen3) ---
579
+
580
+ def action_delete(paths:, **)
581
+ # TODO: add query for recursive
582
+ 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}"}}})
585
+ cli_result_from_paths_response(resp, 'file deleted')
586
+ end
587
+
588
+ def action_search(search_root:, **)
589
+ search_root = @node_path_prefix.nil? ? search_root : @node_path_prefix.add_to_path(search_root)
590
+ parameters = {'path' => search_root}
591
+ other_options = options.get_option(:query)
592
+ parameters.merge!(other_options) unless other_options.nil?
593
+ resp = @api_node.create('files/search', parameters)
594
+ return Result::Empty.new if resp['items'].empty?
595
+ fields = resp['items'].first.keys.reject{ |i| SEARCH_REMOVE_FIELDS.include?(i)}
596
+ 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'])
599
+ Result::ObjectList.new(resp['items'], fields: fields)
600
+ end
601
+
602
+ def action_space(path_list:, **)
603
+ 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'])
607
+ Result::ObjectList.new(resp['paths'])
608
+ end
609
+
610
+ def action_mkdir(path_list:, **)
611
+ 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}}})
614
+ cli_result_from_paths_response(resp, 'folder created')
615
+ end
616
+
617
+ 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}}]})
621
+ cli_result_from_paths_response(resp, 'link created')
622
+ end
623
+
624
+ def action_mkfile(file_path:, contents:, **)
625
+ one_path = @node_path_prefix.nil? ? file_path : @node_path_prefix.add_to_path(file_path)
626
+ contents64 = contents.nil? ? '' : Base64.strict_encode64(contents)
627
+ resp = @api_node.create('files/create', {'paths' => [{type: :file, path: one_path, contents: contents64}]})
628
+ cli_result_from_paths_response(resp, 'file created')
629
+ end
630
+
631
+ def action_rename(path_base:, path_src:, path_dst:, **)
632
+ # 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
+ resp = @api_node.create('files/rename', {'paths' => [{'path' => path_base, 'source' => path_src, 'destination' => path_dst}]})
637
+ cli_result_from_paths_response(resp, 'entry moved')
638
+ end
639
+
640
+ # Obtains a transfer spec via the Node API for the given direction/folders.
641
+ def sync_gen3_block
642
+ lambda do |direction, local_path, remote_path|
643
+ request_transfer_spec = sync_spec_request(direction, local_path, remote_path)
644
+ @api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
645
+ transfer_spec = @api_node.create(
646
+ 'files/sync_setup',
647
+ {transfer_requests: [{transfer_request: request_transfer_spec}]}
648
+ )['transfer_specs'].first['transfer_spec']
649
+ transfer_spec.delete_if{ |_k, v| v.nil?}
650
+ Log.dump(:ts, transfer_spec)
651
+ transfer_spec
652
+ end
653
+ end
654
+
655
+ Sync::Operations::DIRECTIONS.each do |dir|
656
+ define_method(:"action_sync_#{dir}"){ |**| run_sync_transfer(dir, &sync_gen3_block)}
657
+ end
658
+
659
+ def action_upload
660
+ # empty transfer spec for authorization request
661
+ request_transfer_spec = {}
662
+ request_transfer_spec[:paths] = [{destination: transfer.destination_folder(Transfer::Spec::DIRECTION_SEND)}]
663
+ # add fixed parameters if any (for COS)
664
+ @api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
665
+ Api::Node.add_public_key(request_transfer_spec)
666
+ setup_payload = {transfer_requests: [{transfer_request: request_transfer_spec}]}
667
+ transfer_spec = @api_node.create('files/upload_setup', setup_payload)['transfer_specs'].first['transfer_spec']
668
+ Api::Node.add_private_key(transfer_spec)
669
+ transfer_spec.delete('paths')
670
+ Runner.result_transfer(transfer.start(transfer_spec))
671
+ end
672
+
673
+ def action_download
674
+ # empty transfer spec for authorization request
675
+ request_transfer_spec = {}
676
+ request_transfer_spec[:paths] = transfer.ts_source_paths
677
+ # add fixed parameters if any (for COS)
678
+ @api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
679
+ Api::Node.add_public_key(request_transfer_spec)
680
+ setup_payload = {transfer_requests: [{transfer_request: request_transfer_spec}]}
681
+ transfer_spec = @api_node.create('files/download_setup', setup_payload)['transfer_specs'].first['transfer_spec']
682
+ Api::Node.add_private_key(transfer_spec)
683
+ Runner.result_transfer(transfer.start(transfer_spec))
684
+ end
685
+
686
+ def action_cat(remote_path:, **)
687
+ remote_path = @node_path_prefix.nil? ? remote_path : @node_path_prefix.add_to_path(remote_path)
688
+ http = @api_node.read("files/#{URI.encode_www_form_component(remote_path)}/contents", ret: :resp)
689
+ Result::Text.new(http.body)
690
+ end
691
+
692
+ def action_health
693
+ nagios = Nagios.new
694
+ begin
695
+ info = @api_node.read('info')
696
+ nagios.add_ok('node api', 'accessible')
697
+ nagios.check_time_offset(info['current_time'], 'node api')
698
+ nagios.check_product_version('node api', 'entsrv', info['version'])
699
+ rescue StandardError => e
700
+ nagios.add_critical('node api', e.to_s)
701
+ end
702
+ begin
703
+ @api_node.call(
704
+ operation: 'POST',
705
+ subpath: 'services/soap/Transfer-201210',
706
+ content_type: Mime::TEXT,
707
+ body: CENTRAL_SOAP_API_TEST,
708
+ headers: {'Content-Type' => 'text/xml;charset=UTF-8', 'SOAPAction' => 'FASPSessionNET-200911#GetSessionInfo'},
709
+ ret: :resp
710
+ ).body
711
+ nagios.add_ok('central', 'accessible by node')
712
+ rescue StandardError => e
713
+ nagios.add_critical('central', e.to_s)
714
+ end
715
+ Result::ObjectList.new(nagios.status_list)
716
+ end
717
+
718
+ # watch_folder setup: inject required API header (avoids "Unable to convert 2016_09_14 configuration")
719
+ def setup_watch_folder(**)
773
720
  @api_node.params[:headers] ||= {}
774
721
  @api_node.params[:headers]['X-aspera-WF-version'] = '2017_10_23'
775
- case command
776
- when :create
777
- resp = @api_node.create(res_class_path, value_create_modify(command: command))
778
- return Result::Status.new("#{resp['id']} created")
779
- when :list
780
- resp = @api_node.read(res_class_path, query_read_delete)
781
- return Result::ValueList.new(resp['ids'])
782
- when :show
783
- return Result::SingleObject.new(@api_node.read(one_res_path))
784
- when :modify
785
- @api_node.update(one_res_path, value_create_modify(command: 'watch_folder'))
786
- return Result::Status.new("#{one_res_id} updated")
787
- when :delete
788
- @api_node.delete(one_res_path)
789
- return Result::Status.new("#{one_res_id} deleted")
790
- when :state
791
- return Result::SingleObject.new(@api_node.read("#{one_res_path}/state"))
722
+ {}
723
+ end
724
+
725
+ def action_access_keys_do_permission_modify(data:, apifid:, perm_id:, **)
726
+ apifid.node_api.update("permissions/#{perm_id}", data)
727
+ Result::Status.new('Updated')
728
+ end
729
+
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")
733
+ end
734
+
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
748
+ end
749
+
750
+ # access_keys > do - setup: resolve access key and root file id
751
+ # access_key_id is resolved by execute_leaf (via instance_arg in CommandSpec)
752
+ # @return [Hash] context hash containing :do_root_file_id
753
+ def setup_access_key_do(access_key_id:, **)
754
+ @do_root_file_id = options.get_option(:root_id)
755
+ if @do_root_file_id.nil?
756
+ ak_info = @api_node.read("access_keys/#{access_key_id}")
757
+ ak_secret = context.secret_finder.lookup(url: @api_node.base_url, username: ak_info['id'])
758
+ 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}."}
760
+ @api_node.auth_params[:username] = ak_info['id']
761
+ @api_node.auth_params[:password] = ak_secret
762
+ end
763
+ @do_root_file_id = ak_info['root_file_id']
792
764
  end
765
+ {do_root_file_id: @do_root_file_id}
766
+ end
767
+
768
+ # access_keys > do > permission - setup: resolve apifid from CLI path argument
769
+ # (path is read from CLI here as 'permission' is an intermediate node)
770
+ # @return [Hash] context hash containing :apifid
771
+ def setup_access_key_do_permission(**)
772
+ {apifid: apifid_from_next_arg(@do_root_file_id)}
773
+ end
774
+
775
+ # access_keys > do > browse
776
+ def action_access_keys_do_browse(path:, do_root_file_id:, **)
777
+ apifid = apifid_from_path(do_root_file_id, path)
778
+ file_info = apifid.node_api.read("files/#{apifid.file_id}", headers: Api::Node.add_cache_control)
779
+ return Result::ObjectList.new([file_info], fields: GEN4_LS_FIELDS) unless file_info['type'].eql?('folder')
780
+ Result::ObjectList.new(apifid.node_api.list_files(apifid.file_id, query: query_read_delete), fields: GEN4_LS_FIELDS)
781
+ end
782
+
783
+ # access_keys > do > find
784
+ def action_access_keys_do_find(path:, do_root_file_id:, **)
785
+ 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'])
793
787
  end
794
788
 
795
- ACTIONS = %i[
796
- async
797
- ssync
798
- stream
799
- transfer
800
- service
801
- watch_folder
802
- central
803
- asperabrowser
804
- basic_token
805
- bearer_token
806
- simulator
807
- telemetry
808
- ].concat(COMMON_ACTIONS).freeze
809
-
810
- def execute_action(command = nil)
811
- command ||= options.get_next_command(ACTIONS)
812
- case command
813
- when *COMMON_ACTIONS then return execute_simple_common(command)
814
- when :async then return execute_async # former API
815
- when :ssync
816
- # Node API: /asyncs (newer)
817
- sync_command = options.get_next_command(%i[start stop bandwidth counters files state summary] + Operations::ALL - %i[modify])
818
- case sync_command
819
- when *Operations::ALL
820
- return entity_execute(
821
- api: @api_node,
822
- entity: :asyncs,
823
- command: sync_command,
824
- items_key: 'ids'
825
- ){ |field, value| ssync_lookup(field, value)}
789
+ # access_keys > do > cat
790
+ def action_access_keys_do_cat(path:, do_root_file_id:, **)
791
+ apifid = apifid_from_path(do_root_file_id, path)
792
+ Result::Text.new(apifid.node_api.read("files/#{apifid.file_id}/content", ret: :resp).body)
793
+ end
794
+
795
+ # access_keys > do > show
796
+ def action_access_keys_do_show(path:, do_root_file_id:, **)
797
+ apifid = apifid_from_path(do_root_file_id, path)
798
+ Result::SingleObject.new(apifid.node_api.read("files/#{apifid.file_id}"))
799
+ end
800
+
801
+ # access_keys > do > modify
802
+ def action_access_keys_do_modify(path:, update_value:, do_root_file_id:, **)
803
+ apifid = apifid_from_path(do_root_file_id, path)
804
+ apifid.node_api.update("files/#{apifid.file_id}", update_value)
805
+ Result::Status.new('Done')
806
+ end
807
+
808
+ # access_keys > do > thumbnail
809
+ def action_access_keys_do_thumbnail(path:, do_root_file_id:, **)
810
+ apifid = apifid_from_path(do_root_file_id, path)
811
+ Result::Image.new(apifid.node_api.read("files/#{apifid.file_id}/preview", headers: {'Accept' => 'image/png'}, ret: :resp).body)
812
+ end
813
+
814
+ # access_keys > do > rename
815
+ def action_access_keys_do_rename(source_path:, new_name:, do_root_file_id:, **)
816
+ apifid = @api_node.resolve_api_fid(do_root_file_id, source_path)
817
+ apifid.node_api.update("files/#{apifid.file_id}", {name: new_name})
818
+ Result::Status.new("renamed to #{new_name}")
819
+ end
820
+
821
+ # access_keys > do > delete
822
+ 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|
825
+ apifid = if (m = Parser.percent_selector(l_path))
826
+ Aspera.assert_values(m[:field], ['id'], type: BadIdentifier)
827
+ Api::NodeFileId.new(@api_node, m[:value])
826
828
  else
827
- asyncs_id = options.instance_identifier{ |field, value| ssync_lookup(field, value)}
828
- if %i[start stop].include?(sync_command)
829
- @api_node.call(
830
- operation: 'POST',
831
- subpath: "asyncs/#{asyncs_id}/#{sync_command}",
832
- content_type: Mime::TEXT,
833
- body: '',
834
- ret: :resp
835
- ).body
836
- return Result::Status.new('Done')
837
- end
838
- parameters = options.get_option(:query) || {} if %i[bandwidth counters files].include?(sync_command)
839
- return Result::SingleObject.new(@api_node.read("asyncs/#{asyncs_id}/#{sync_command}", parameters))
829
+ @api_node.resolve_api_fid(do_root_file_id, l_path)
830
+ end
831
+ apifid.node_api.delete("files/#{apifid.file_id}")
832
+ {'path' => l_path}
833
+ end
834
+ end
835
+
836
+ # Shared Gen4 sync block: obtains a transfer spec via the Gen4 API for the given direction/remote_path.
837
+ def sync_gen4_block(do_root_file_id)
838
+ lambda do |direction, _local_path, remote_path|
839
+ ts_direction = direction.eql?(:pull) ? Transfer::Spec::DIRECTION_RECEIVE : Transfer::Spec::DIRECTION_SEND
840
+ apifid = @api_node.resolve_api_fid(do_root_file_id, remote_path)
841
+ apifid.node_api.transfer_spec_gen4(apifid.file_id, ts_direction)
842
+ end
843
+ end
844
+
845
+ 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))
848
+ end
849
+ end
850
+
851
+ # access_keys > do > upload
852
+ def action_access_keys_do_upload(do_root_file_id:, **)
853
+ apifid = @api_node.resolve_api_fid(do_root_file_id, transfer.destination_folder(Transfer::Spec::DIRECTION_SEND), true)
854
+ Runner.result_transfer(transfer.start(apifid.node_api.transfer_spec_gen4(apifid.file_id, Transfer::Spec::DIRECTION_SEND)))
855
+ end
856
+
857
+ # access_keys > do > download
858
+ def action_access_keys_do_download(do_root_file_id:, **)
859
+ apifid, source_paths = @api_node.resolve_api_fid_paths(do_root_file_id, transfer.ts_source_paths)
860
+ Runner.result_transfer(transfer.start(apifid.node_api.transfer_spec_gen4(apifid.file_id, Transfer::Spec::DIRECTION_RECEIVE, {'paths'=>source_paths})))
861
+ end
862
+
863
+ # access_keys > do > permission (intermediate node: re-enter DSL at permission sub-tree)
864
+ def action_access_keys_do_permission(do_root_file_id:, **)
865
+ apifid = apifid_from_next_arg(do_root_file_id)
866
+ dispatch_from_registry(%i[access_keys do permission], {apifid: apifid}, skip_setup: true)
867
+ end
868
+
869
+ # access_keys > do > v3
870
+ def action_access_keys_do_v3(do_root_file_id:, **)
871
+ # Build a fresh Node for the v3 root, then let the DSL dispatcher consume the next command.
872
+ v3_node = Node.new(context: context, api: @api_node.resolve_api_fid(do_root_file_id, '').node_api)
873
+ v3_node.execute_action
874
+ end
875
+
876
+ # access_keys > do > node_info / bearer_token_node — shared helper builds the result hash
877
+ def gen4_apifid_info(do_root_file_id, path)
878
+ apifid = apifid_from_path(do_root_file_id, path)
879
+ result = {url: apifid.node_api.base_url, root_id: apifid.file_id}
880
+ case apifid.node_api.auth_params[:type]
881
+ when :basic
882
+ result[:username] = apifid.node_api.auth_params[:username]
883
+ result[:password] = apifid.node_api.auth_params[:password]
884
+ when :oauth2
885
+ result[:username] = apifid.node_api.params[:headers][Api::Node::HEADER_X_ASPERA_ACCESS_KEY]
886
+ result[:password] = apifid.node_api.oauth.authorization
887
+ else Aspera.error_unexpected_value(apifid.node_api.auth_params[:type]){'Node API Auth type'}
888
+ end
889
+ [apifid, result]
890
+ end
891
+
892
+ def action_access_keys_do_node_info(path:, do_root_file_id:, **)
893
+ _apifid, result = gen4_apifid_info(do_root_file_id, path)
894
+ Result::SingleObject.new(result)
895
+ end
896
+
897
+ def action_access_keys_do_bearer_token_node(path:, do_root_file_id:, **)
898
+ apifid, result = gen4_apifid_info(do_root_file_id, path)
899
+ 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]})"}
901
+ Aspera.assert(OAuth::Factory.bearer_auth?(result[:password]), 'Not using bearer token auth')
902
+ Result::Text.new(result[:password])
903
+ end
904
+
905
+ # access_keys > do > mkdir / mklink / mkfile — shared helper: resolve parent folder,
906
+ # build payload from query option, optionally check for name collision.
907
+ # @return [Array(NodeFileId, Hash)] parent apifid and payload with :name set
908
+ # @param path [String] full path (folder/name)
909
+ def gen4_mk_resolve(top_file_id, path)
910
+ containing_folder_path, new_item = Api::Node.split_folder(path)
911
+ apifid = @api_node.resolve_api_fid(top_file_id, containing_folder_path, true)
912
+ query = options.get_option(:query)
913
+ check_exists = true
914
+ payload = {name: new_item}
915
+ if query
916
+ check_exists = !query.delete('check').eql?(false)
917
+ target = query.delete('target')
918
+ if target
919
+ target_apifid = @api_node.resolve_api_fid(top_file_id, target, true)
920
+ payload[:target_id] = target_apifid.file_id
840
921
  end
841
- when :stream
842
- command = options.get_next_command(%i[list create show modify cancel])
843
- case command
844
- when :list
845
- resp = @api_node.read('ops/transfers', query_read_delete)
846
- return Result::ObjectList.new(resp, fields: %w[id status]) # TODO: useful?
847
- when :create
848
- resp = @api_node.create('streams', value_create_modify(command: command))
849
- return Result::SingleObject.new(resp)
850
- when :show
851
- resp = @api_node.read("ops/transfers/#{options.get_next_argument('transfer id')}")
852
- return Result::SingleObject.new(resp)
853
- when :modify
854
- resp = @api_node.update("streams/#{options.get_next_argument('transfer id')}", value_create_modify(command: command))
855
- return Result::SingleObject.new(resp)
856
- when :cancel
857
- resp = @api_node.cancel("streams/#{options.get_next_argument('transfer id')}")
858
- return Result::SingleObject.new(resp)
859
- else Aspera.error_unexpected_value(command)
922
+ payload.merge!(query.symbolize_keys)
923
+ end
924
+ if check_exists
925
+ folder_content = apifid.node_api.read("files/#{apifid.file_id}/files")
926
+ 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}"}
929
+ end
930
+ [apifid, payload]
931
+ end
932
+
933
+ def action_access_keys_do_mkdir(path:, do_root_file_id:, **)
934
+ apifid, payload = gen4_mk_resolve(do_root_file_id, path)
935
+ payload[:type] = :folder
936
+ Result::SingleObject.new(apifid.node_api.create("files/#{apifid.file_id}/files", payload))
937
+ end
938
+
939
+ def action_access_keys_do_mklink(path:, do_root_file_id:, **)
940
+ apifid, payload = gen4_mk_resolve(do_root_file_id, path)
941
+ payload[:type] = :link
942
+ Aspera.assert(payload[:target_id], 'Missing target_id')
943
+ Aspera.assert(payload[:target_node_id], 'Missing target_node_id')
944
+ Result::SingleObject.new(apifid.node_api.create("files/#{apifid.file_id}/files", payload))
945
+ end
946
+
947
+ def action_access_keys_do_mkfile(path:, contents:, do_root_file_id:, **)
948
+ apifid, payload = gen4_mk_resolve(do_root_file_id, path)
949
+ payload[:type] = :file
950
+ payload[:contents] = contents.nil? ? '' : Base64.strict_encode64(contents)
951
+ Result::SingleObject.new(apifid.node_api.create("files/#{apifid.file_id}/files", payload))
952
+ end
953
+
954
+ # access_keys > do > permission > list/show/create/modify/delete
955
+ def action_access_keys_do_permission_list(apifid:, **)
956
+ list_query = query_read_delete(default: Rest.php_style({'include' => %w[access_level permission_count]}))
957
+ # Specify file to get permissions for unless not specified (then, get all permissions)
958
+ list_query['file_id'] = apifid.file_id unless apifid.file_id.to_s.empty?
959
+ list_query['inherited'] = false if list_query.key?('file_id') && !list_query.key?('inherited')
960
+ Result::ObjectList.new(apifid.node_api.read_with_pages('permissions', list_query))
961
+ end
962
+
963
+ 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|
967
+ apifid.node_api.delete("permissions/#{one_id}")
968
+ the_app = apifid.node_api.app_info
969
+ the_app&.api&.permissions_send_event(event_data: {}, app_info: the_app, types: ['permission.deleted'])
970
+ {'id' => one_id}
971
+ end
972
+ end
973
+
974
+ def action_access_keys_do_permission_create(data:, apifid:, **)
975
+ create_param = data
976
+ Aspera.assert(!create_param.key?('file_id'), type: Cli::BadArgument){'no file_id'}
977
+ create_param['file_id'] = apifid.file_id
978
+ create_param['access_levels'] = Api::Node::ACCESS_LEVELS unless create_param.key?('access_levels')
979
+ the_app = apifid.node_api.app_info
980
+ the_app&.api&.permissions_set_create_params(perm_data: create_param, app_info: the_app)
981
+ created_data = apifid.node_api.create('permissions', create_param)
982
+ the_app&.api&.permissions_send_event(event_data: created_data, app_info: the_app)
983
+ Result::SingleObject.new(created_data)
984
+ end
985
+
986
+ # access_keys > set_bearer_key
987
+ def action_access_keys_set_bearer_key(access_key_id:, bearer_key_pem:, **)
988
+ access_key_id = @api_node.read('access_keys/self')['id'] if access_key_id.eql?('self')
989
+ key = OpenSSL::PKey.read(bearer_key_pem)
990
+ key = key.public_key if key.private?
991
+ @api_node.update("access_keys/#{access_key_id}", {token_verification_key: key.to_pem})
992
+ Result::Status.new('public key updated')
993
+ end
994
+
995
+ # async sub-commands: individual handlers
996
+ def action_async_show(async_id:, **)
997
+ async_ids = @api_node.read('async/list')['sync_ids']
998
+ if async_id.eql?(SpecialValues::ALL)
999
+ resp = @api_node.create('async/summary', {'syncs' => async_ids})['sync_summaries']
1000
+ return Result::Empty.new if resp.empty?
1001
+ return Result::ObjectList.new(resp, fields: %w[snid name local_dir remote_dir])
1002
+ end
1003
+ Integer(async_id)
1004
+ resp = @api_node.create('async/summary', {'syncs' => [async_id]})['sync_summaries']
1005
+ return Result::Empty.new if resp.empty?
1006
+ Result::SingleObject.new(resp.first)
1007
+ end
1008
+
1009
+ def action_async_delete(async_id:, **)
1010
+ async_ids = async_id.eql?(SpecialValues::ALL) ? @api_node.read('async/list')['sync_ids'] : [async_id]
1011
+ Result::SingleObject.new(@api_node.create('async/delete', {'syncs' => async_ids}))
1012
+ end
1013
+
1014
+ def action_async_bandwidth(async_id:, **)
1015
+ Integer(async_id)
1016
+ post_data = {'syncs' => [async_id], 'seconds' => 100}
1017
+ resp = @api_node.create('async/bandwidth', post_data)
1018
+ data = resp['bandwidth_data']
1019
+ return Result::Empty.new if data.empty?
1020
+ Result::ObjectList.new(data.first[async_id]['data'])
1021
+ end
1022
+
1023
+ def action_async_files(async_id:, **)
1024
+ Integer(async_id)
1025
+ post_data = {'syncs' => [async_id]}
1026
+ filter = options.get_option(:query)
1027
+ post_data.merge!(filter) unless filter.nil?
1028
+ resp = @api_node.create('async/files', post_data)
1029
+ data = resp['sync_files']
1030
+ data = data.first[async_id] unless data.empty?
1031
+ iteration_data = []
1032
+ skip_ids_persistency = nil
1033
+ if options.get_option(:once_only, mandatory: true)
1034
+ skip_ids_persistency = PersistencyActionOnce.new(
1035
+ manager: persistency,
1036
+ data: iteration_data,
1037
+ id: IdGenerator.from_list('sync_files', options.get_option(:url, mandatory: true), options.get_option(:username, mandatory: true), async_id)
1038
+ )
1039
+ data.select!{ |l| l['fnid'].to_i > iteration_data.first} unless iteration_data.first.nil?
1040
+ iteration_data[0] = data.last['fnid'].to_i unless data.empty?
1041
+ end
1042
+ return Result::Empty.new if data.empty?
1043
+ skip_ids_persistency&.save
1044
+ Result::ObjectList.new(data)
1045
+ end
1046
+
1047
+ def action_async_counters(async_id:, **)
1048
+ Integer(async_id)
1049
+ resp = @api_node.create('async/counters', {'syncs' => [async_id]})['sync_counters'].first[async_id].last
1050
+ return Result::Empty.new if resp.nil?
1051
+ Result::SingleObject.new(resp)
1052
+ end
1053
+
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
+ def action_ssync_start(ssync_id:, **)
1064
+ @api_node.call(operation: 'POST', subpath: "asyncs/#{ssync_id}/start", content_type: Mime::TEXT, body: '', ret: :resp).body
1065
+ Result::Status.new('Done')
1066
+ end
1067
+
1068
+ def action_ssync_stop(ssync_id:, **)
1069
+ @api_node.call(operation: 'POST', subpath: "asyncs/#{ssync_id}/stop", content_type: Mime::TEXT, body: '', ret: :resp).body
1070
+ Result::Status.new('Done')
1071
+ end
1072
+
1073
+ # transfer sub-commands
1074
+ def action_transfer_list
1075
+ transfer_filter = query_read_delete(default: {})
1076
+ iteration_persistency = nil
1077
+ if options.get_option(:once_only, mandatory: true)
1078
+ iteration_persistency = PersistencyActionOnce.new(
1079
+ manager: persistency,
1080
+ data: [],
1081
+ id: IdGenerator.from_list('node_transfers', options.get_option(:url, mandatory: true), options.get_option(:username, mandatory: true))
1082
+ )
1083
+ if transfer_filter.delete('reset')
1084
+ iteration_persistency.data.clear
1085
+ iteration_persistency.save
1086
+ return Result::Status.new('Persistency reset')
860
1087
  end
861
- when :transfer
862
- command = options.get_next_command(%i[list cancel show modify bandwidth_average sessions])
863
- case command
864
- when :list
865
- transfer_filter = query_read_delete(default: {})
866
- iteration_persistency = nil
867
- if options.get_option(:once_only, mandatory: true)
868
- iteration_persistency = PersistencyActionOnce.new(
869
- manager: persistency,
870
- data: [],
871
- id: IdGenerator.from_list(
872
- 'node_transfers',
873
- options.get_option(:url, mandatory: true),
874
- options.get_option(:username, mandatory: true)
875
- )
876
- )
877
- if transfer_filter.delete('reset')
878
- iteration_persistency.data.clear
879
- iteration_persistency.save
880
- return Result::Status.new('Persistency reset')
881
- end
882
- else
883
- Aspera.assert(!transfer_filter.key?('reset'), 'reset only with once_only', type: Cli::BadArgument)
884
- end
885
- transfers_data = @api_node.read_with_paging('ops/transfers', transfer_filter, iteration: iteration_persistency&.data)
886
- iteration_persistency&.save
887
- return Result::ObjectList.new(transfers_data, fields: %w[id status start_spec.direction start_spec.remote_user start_spec.remote_host start_spec.destination_path])
888
- when :sessions
889
- transfers_data = @api_node.read('ops/transfers', query_read_delete)
890
- sessions = transfers_data.flat_map{ |t| t['sessions']}
891
- start_end = %i[start end].freeze
892
- sessions.each do |session|
893
- start_end.each do |what|
894
- session["#{what}_time"] = session["#{what}_time_usec"] ? Time.at(session["#{what}_time_usec"] / 1_000_000.0).utc.iso8601(0) : nil
895
- end
896
- end
897
- return Result::ObjectList.new(sessions, fields: %w[id status start_time end_time target_rate_kbps])
898
- when :cancel
899
- @api_node.cancel("ops/transfers/#{options.instance_identifier}")
900
- return Result::Status.new('Cancelled')
901
- when :show
902
- resp = @api_node.read("ops/transfers/#{options.instance_identifier}")
903
- return Result::SingleObject.new(resp)
904
- when :modify
905
- @api_node.update("ops/transfers/#{options.instance_identifier}", options.get_next_argument('update value', validation: Hash))
906
- return Result::Status.new('Modified')
907
- when :bandwidth_average
908
- transfers_data = @api_node.read('ops/transfers', query_read_delete)
909
- # collect all key dates
910
- bandwidth_period = {}
911
- dir_info = %i[avg_kbps sessions].freeze
912
- transfers_data.each do |transfer|
913
- session = transfer
914
- # transfer['sessions'].each do |session|
915
- next if session['avg_rate_kbps'].zero?
916
- bandwidth_period[session['start_time_usec']] = 0
917
- bandwidth_period[session['end_time_usec']] = 0
918
- # end
919
- end
920
- result = []
921
- # all dates sorted numerically
922
- all_dates = bandwidth_period.keys.sort
923
- all_dates.each_with_index do |start_date, index|
924
- end_date = all_dates[index + 1]
925
- # do not process last one
926
- break if end_date.nil?
927
- # init data for this period
928
- period_bandwidth = Transfer::Spec::DIRECTION_ENUM_VALUES.map(&:to_sym).to_h do |direction|
929
- [direction, dir_info.to_h{ |k2| [k2, 0]}]
930
- end
931
- # find all transfers that were active at this time
932
- transfers_data.each do |transfer|
933
- session = transfer
934
- # transfer['sessions'].each do |session|
935
- # skip if not information for this period
936
- next if session['avg_rate_kbps'].zero?
937
- # skip if not in this period
938
- next if session['start_time_usec'] >= end_date || session['end_time_usec'] <= start_date
939
- info = period_bandwidth[transfer['start_spec']['direction'].to_sym]
940
- info[:avg_kbps] += session['avg_rate_kbps']
941
- info[:sessions] += 1
942
- # end
943
- end
944
- next if Transfer::Spec::DIRECTION_ENUM_VALUES.map(&:to_sym).all? do |dir|
945
- period_bandwidth[dir][:sessions].zero?
946
- end
947
- result.push({start: Time.at(start_date / 1_000_000), end: Time.at(end_date / 1_000_000)}.merge(period_bandwidth))
948
- end
949
- return Result::ObjectList.new(result)
950
- else Aspera.error_unexpected_value(command)
1088
+ else
1089
+ Aspera.assert(!transfer_filter.key?('reset'), 'reset only with once_only', type: Cli::BadArgument)
1090
+ end
1091
+ transfers_data = @api_node.read_with_paging('ops/transfers', transfer_filter, iteration: iteration_persistency&.data)
1092
+ iteration_persistency&.save
1093
+ Result::ObjectList.new(transfers_data, fields: %w[id status start_spec.direction start_spec.remote_user start_spec.remote_host start_spec.destination_path])
1094
+ end
1095
+
1096
+ def action_transfer_sessions
1097
+ transfers_data = @api_node.read('ops/transfers', query_read_delete)
1098
+ sessions = transfers_data.flat_map{ |t| t['sessions']}
1099
+ sessions.each do |session|
1100
+ SESSION_TIME_FIELDS.each do |what|
1101
+ session["#{what}_time"] = session["#{what}_time_usec"] ? Time.at(session["#{what}_time_usec"] / 1_000_000.0).utc.iso8601(0) : nil
951
1102
  end
952
- when :service
953
- command = options.get_next_command(%i[list create delete])
954
- service_id = options.instance_identifier if [:delete].include?(command)
955
- case command
956
- when :list
957
- resp = @api_node.read('rund/services')
958
- return Result::ObjectList.new(resp['services'])
959
- when :create
960
- # @json:'{"type":"WATCHFOLDERD","run_as":{"user":"user1"}}'
961
- params = options.get_next_argument('creation data', validation: Hash)
962
- resp = @api_node.create('rund/services', params)
963
- return Result::Status.new("#{resp['id']} created")
964
- when :delete
965
- @api_node.delete("rund/services/#{service_id}")
966
- return Result::Status.new("#{service_id} deleted")
1103
+ end
1104
+ Result::ObjectList.new(sessions, fields: %w[id status start_time end_time target_rate_kbps])
1105
+ end
1106
+
1107
+ def action_transfer_cancel(transfer_id:, **)
1108
+ @api_node.cancel("ops/transfers/#{transfer_id}")
1109
+ Result::Status.new('Cancelled')
1110
+ end
1111
+
1112
+ def action_transfer_modify(update_value:, transfer_id:, **)
1113
+ @api_node.update("ops/transfers/#{transfer_id}", update_value)
1114
+ Result::Status.new('Modified')
1115
+ end
1116
+
1117
+ def action_transfer_bandwidth_average
1118
+ transfers_data = @api_node.read('ops/transfers', query_read_delete)
1119
+ bandwidth_period = {}
1120
+ dir_info = %i[avg_kbps sessions].freeze
1121
+ transfers_data.each do |t|
1122
+ next if t['avg_rate_kbps'].zero?
1123
+ bandwidth_period[t['start_time_usec']] = 0
1124
+ bandwidth_period[t['end_time_usec']] = 0
1125
+ end
1126
+ result = []
1127
+ all_dates = bandwidth_period.keys.sort
1128
+ all_dates.each_with_index do |start_date, index|
1129
+ end_date = all_dates[index + 1]
1130
+ break if end_date.nil?
1131
+ period_bandwidth = Transfer::Spec::DIRECTION_ENUM_VALUES.map(&:to_sym).to_h do |dir|
1132
+ [dir, dir_info.to_h{ |k2| [k2, 0]}]
967
1133
  end
968
- when :watch_folder
969
- return watch_folder_action
970
- when :central
971
- command = options.get_next_command(%i[session file])
972
- validator_id = options.get_option(:validator)
973
- validation = {'validator_id' => validator_id} unless validator_id.nil?
974
- request_data = options.get_option(:query) || {}
975
- case command
976
- when :session
977
- command = options.get_next_command([:list])
978
- case command
979
- when :list
980
- request_data = options.get_next_argument('request data', mandatory: false, validation: Hash, default: {})
981
- request_data.deep_merge!({'validation' => validation}) unless validation.nil?
982
- resp = @api_node.create('services/rest/transfers/v1/sessions', request_data)
983
- return Result::ObjectList.new(resp['session_info_result']['session_info'], fields: %w[session_uuid status transport direction bytes_transferred])
984
- end
985
- when :file
986
- command = options.get_next_command(%i[list modify])
987
- case command
988
- when :list
989
- request_data = options.get_next_argument('request data', mandatory: false, validation: Hash, default: {})
990
- request_data.deep_merge!({'validation' => validation}) unless validation.nil?
991
- resp = @api_node.create('services/rest/transfers/v1/files', request_data)
992
- resp = JSON.parse(resp) if resp.is_a?(String)
993
- Log.dump(:resp, resp)
994
- return Result::ObjectList.new(resp['file_transfer_info_result']['file_transfer_info'], fields: %w[session_uuid file_id status path])
995
- when :modify
996
- request_data = options.get_next_argument('request data', mandatory: false, validation: Hash, default: {})
997
- request_data.deep_merge!(validation) unless validation.nil?
998
- @api_node.update('services/rest/transfers/v1/files', request_data)
999
- return Result::Status.new('updated')
1000
- end
1134
+ transfers_data.each do |t|
1135
+ next if t['avg_rate_kbps'].zero?
1136
+ next if t['start_time_usec'] >= end_date || t['end_time_usec'] <= start_date
1137
+ info = period_bandwidth[t['start_spec']['direction'].to_sym]
1138
+ info[:avg_kbps] += t['avg_rate_kbps']
1139
+ info[:sessions] += 1
1001
1140
  end
1002
- when :asperabrowser
1003
- browse_params = {
1004
- 'nodeUser' => options.get_option(:username, mandatory: true),
1005
- 'nodePW' => options.get_option(:password, mandatory: true),
1006
- 'nodeURL' => options.get_option(:url, mandatory: true)
1141
+ next if Transfer::Spec::DIRECTION_ENUM_VALUES.map(&:to_sym).all?{ |dir| period_bandwidth[dir][:sessions].zero?}
1142
+ result.push({start: Time.at(start_date / 1_000_000), end: Time.at(end_date / 1_000_000)}.merge(period_bandwidth))
1143
+ end
1144
+ Result::ObjectList.new(result)
1145
+ end
1146
+
1147
+ # service sub-commands
1148
+ def action_service_create(creation_data:, **)
1149
+ resp = @api_node.create('rund/services', creation_data)
1150
+ Result::Status.new("#{resp['id']} created")
1151
+ end
1152
+
1153
+ def action_service_delete(service_id:, **)
1154
+ @api_node.delete("rund/services/#{service_id}")
1155
+ Result::Status.new("#{service_id} deleted")
1156
+ end
1157
+
1158
+ # central: shared helper
1159
+ def central_validation
1160
+ validator_id = options.get_option(:validator)
1161
+ validator_id ? {'validator_id' => validator_id} : nil
1162
+ end
1163
+
1164
+ # central > session > list
1165
+ def action_central_session_list(request_data:, **)
1166
+ request_data ||= {}
1167
+ validation = central_validation
1168
+ request_data.deep_merge!({'validation' => validation}) unless validation.nil?
1169
+ resp = @api_node.create('services/rest/transfers/v1/sessions', request_data)
1170
+ Result::ObjectList.new(resp['session_info_result']['session_info'], fields: %w[session_uuid status transport direction bytes_transferred])
1171
+ end
1172
+
1173
+ # central > file > list
1174
+ def action_central_file_list(request_data:, **)
1175
+ request_data ||= {}
1176
+ validation = central_validation
1177
+ request_data.deep_merge!({'validation' => validation}) unless validation.nil?
1178
+ resp = @api_node.create('services/rest/transfers/v1/files', request_data)
1179
+ resp = JSON.parse(resp) if resp.is_a?(String)
1180
+ Log.dump(:resp, resp)
1181
+ Result::ObjectList.new(resp['file_transfer_info_result']['file_transfer_info'], fields: %w[session_uuid file_id status path])
1182
+ end
1183
+
1184
+ # central > file > modify
1185
+ def action_central_file_modify(request_data:, **)
1186
+ request_data ||= {}
1187
+ validation = central_validation
1188
+ request_data.deep_merge!(validation) unless validation.nil?
1189
+ @api_node.update('services/rest/transfers/v1/files', request_data)
1190
+ Result::Status.new('updated')
1191
+ end
1192
+
1193
+ def action_asperabrowser
1194
+ browse_params = {
1195
+ 'nodeUser' => options.get_option(:username, mandatory: true),
1196
+ 'nodePW' => options.get_option(:password, mandatory: true),
1197
+ 'nodeURL' => options.get_option(:url, mandatory: true)
1198
+ }
1199
+ # encode parameters so that it looks good in url
1200
+ encoded_params = Base64.strict_encode64(Zlib::Deflate.deflate(JSON.generate(browse_params))).gsub(/=+$/, '').tr('+/', '-_').reverse
1201
+ Environment.instance.open_uri("#{options.get_option(:asperabrowserurl)}?goto=#{encoded_params}")
1202
+ return Result::Status.new('done')
1203
+ end
1204
+
1205
+ def action_bearer_token(private_key_pem:, user_group_id:, **)
1206
+ private_key = OpenSSL::PKey::RSA.new(private_key_pem)
1207
+ access_key = options.get_option(:username, mandatory: true)
1208
+ Result::Text.new(Api::Node.bearer_token(payload: user_group_id, access_key: access_key, private_key: private_key))
1209
+ end
1210
+
1211
+ def action_simulator(parameters: {}, **)
1212
+ require 'aspera/node_simulator'
1213
+ parameters = parameters.symbolize_keys
1214
+ uri = URI.parse(parameters.delete(:url){WebServerSimple::DEFAULT_URL})
1215
+ server = WebServerSimple.new(uri, **parameters.slice(*WebServerSimple::PARAMS))
1216
+ server.mount(uri.path, NodeSimulatorServlet, parameters.except(*WebServerSimple::PARAMS), NodeSimulator.new)
1217
+ server.start
1218
+ return Result::Status.new('Simulator terminated')
1219
+ end
1220
+
1221
+ def action_telemetry(parameters: {}, **)
1222
+ parameters = parameters.symbolize_keys
1223
+ %i[url key].each do |psym|
1224
+ Aspera.assert(parameters.key?(psym), type: Cli::BadArgument){"Missing parameter: #{psym}"}
1225
+ end
1226
+ require 'socket'
1227
+ parameters[:interval] = 10 unless parameters.key?(:interval)
1228
+ parameters[:hostname] = Socket.gethostname unless parameters.key?(:hostname)
1229
+ interval = parameters[:interval].to_f
1230
+ Aspera.assert(interval > 0, type: Cli::BadArgument){'Interval must be a positive number in seconds'}
1231
+ otel_api = Rest.new(
1232
+ base_url: "#{parameters[:url]}/v1",
1233
+ headers: {
1234
+ # 'Authorization' => "apiToken #{parameters[:key]}",
1235
+ 'x-instana-key' => parameters[:key],
1236
+ 'x-instana-host' => parameters[:hostname]
1007
1237
  }
1008
- # encode parameters so that it looks good in url
1009
- encoded_params = Base64.strict_encode64(Zlib::Deflate.deflate(JSON.generate(browse_params))).gsub(/=+$/, '').tr('+/', '-_').reverse
1010
- Environment.instance.open_uri("#{options.get_option(:asperabrowserurl)}?goto=#{encoded_params}")
1011
- return Result::Status.new('done')
1012
- when :basic_token
1013
- return Result::Text.new(Rest.basic_authorization(options.get_option(:username, mandatory: true), options.get_option(:password, mandatory: true)))
1014
- when :bearer_token
1015
- private_key = OpenSSL::PKey::RSA.new(options.get_next_argument('private RSA key PEM value', validation: String))
1016
- token_info = options.get_next_argument('user and group identification', validation: Hash)
1017
- access_key = options.get_option(:username, mandatory: true)
1018
- return Result::Text.new(Api::Node.bearer_token(payload: token_info, access_key: access_key, private_key: private_key))
1019
- when :simulator
1020
- require 'aspera/node_simulator'
1021
- parameters = value_create_modify(command: command, default: {}).symbolize_keys
1022
- uri = URI.parse(parameters.delete(:url){WebServerSimple::DEFAULT_URL})
1023
- server = WebServerSimple.new(uri, **parameters.slice(*WebServerSimple::PARAMS))
1024
- server.mount(uri.path, NodeSimulatorServlet, parameters.except(*WebServerSimple::PARAMS), NodeSimulator.new)
1025
- server.start
1026
- return Result::Status.new('Simulator terminated')
1027
- when :telemetry
1028
- parameters = value_create_modify(command: command, default: {}).symbolize_keys
1029
- %i[url key].each do |psym|
1030
- raise Cli::BadArgument, "Missing parameter: #{psym}" unless parameters.key?(psym)
1031
- end
1032
- require 'socket'
1033
- parameters[:interval] = 10 unless parameters.key?(:interval)
1034
- parameters[:hostname] = Socket.gethostname unless parameters.key?(:hostname)
1035
- interval = parameters[:interval].to_f
1036
- raise Cli::BadArgument, 'Interval must be a positive number in seconds' if interval <= 0
1037
- otel_api = Rest.new(
1038
- base_url: "#{parameters[:url]}/v1",
1039
- headers: {
1040
- # 'Authorization' => "apiToken #{parameters[:key]}",
1041
- 'x-instana-key' => parameters[:key],
1042
- 'x-instana-host' => parameters[:hostname]
1043
- }
1044
- )
1045
- datapoint = {
1046
- attributes: [
1047
- {
1048
- key: 'server.name',
1049
- value: {
1050
- stringValue: 'HSTS1'
1051
- }
1238
+ )
1239
+ datapoint = {
1240
+ attributes: [
1241
+ {
1242
+ key: 'server.name',
1243
+ value: {
1244
+ stringValue: 'HSTS1'
1052
1245
  }
1053
- ],
1054
- asInt: nil,
1055
- timeUnixNano: nil
1056
- }
1057
- # https://opentelemetry.io/docs/specs/otel/metrics/data-model/#gauge
1058
- metrics = {
1059
- resourceMetrics: [
1060
- {
1061
- resource: {
1062
- attributes: [
1246
+ }
1247
+ ],
1248
+ asInt: nil,
1249
+ timeUnixNano: nil
1250
+ }
1251
+ # https://opentelemetry.io/docs/specs/otel/metrics/data-model/#gauge
1252
+ metrics = {
1253
+ resourceMetrics: [
1254
+ {
1255
+ resource: {
1256
+ attributes: [
1257
+ {
1258
+ key: 'service.name',
1259
+ value: {
1260
+ stringValue: 'IBMAspera'
1261
+ }
1262
+ }
1263
+ ]
1264
+ },
1265
+ scopeMetrics: [
1266
+ {
1267
+ metrics: [
1063
1268
  {
1064
- key: 'service.name',
1065
- value: {
1066
- stringValue: 'IBMAspera'
1269
+ name: 'active.transfers',
1270
+ description: 'Number of active transfers',
1271
+ unit: '1',
1272
+ gauge: {
1273
+ dataPoints: [
1274
+ datapoint
1275
+ ]
1067
1276
  }
1068
1277
  }
1069
1278
  ]
1070
- },
1071
- scopeMetrics: [
1072
- {
1073
- metrics: [
1074
- {
1075
- name: 'active.transfers',
1076
- description: 'Number of active transfers',
1077
- unit: '1',
1078
- gauge: {
1079
- dataPoints: [
1080
- datapoint
1081
- ]
1082
- }
1083
- }
1084
- ]
1085
- }
1086
- ]
1087
- }
1088
- ]
1089
- }
1090
- loop do
1091
- timestamp = Time.now
1092
- transfers_data = @api_node.read_with_paging('ops/transfers', {active_only: true})
1093
- datapoint[:asInt] = transfers_data.length
1094
- datapoint[:timeUnixNano] = timestamp.to_i * 1_000_000_000 + timestamp.nsec
1095
- Log.log.info("#{datapoint[:asInt]} active transfers")
1096
- # https://www.ibm.com/docs/en/instana-observability/current?topic=instana-backend
1097
- otel_api.create('metrics', metrics)
1098
- break if interval.eql?(0.0)
1099
- sleep([0.0, interval - (Time.now - timestamp)].max)
1100
- end
1279
+ }
1280
+ ]
1281
+ }
1282
+ ]
1283
+ }
1284
+ loop do
1285
+ timestamp = Time.now
1286
+ transfers_data = @api_node.read_with_paging('ops/transfers', {active_only: true})
1287
+ datapoint[:asInt] = transfers_data.length
1288
+ datapoint[:timeUnixNano] = timestamp.to_i * 1_000_000_000 + timestamp.nsec
1289
+ Log.log.info("#{datapoint[:asInt]} active transfers")
1290
+ # https://www.ibm.com/docs/en/instana-observability/current?topic=instana-backend
1291
+ otel_api.create('metrics', metrics)
1292
+ break if interval.eql?(0.0)
1293
+ sleep([0.0, interval - (Time.now - timestamp)].max)
1101
1294
  end
1102
- Aspera.error_unreachable_line
1295
+ end
1296
+
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
1301
+ # are resolved normally.
1302
+ # @param command [Symbol] command already consumed from the argument stream
1303
+ # @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], {})
1103
1307
  end
1104
1308
 
1105
1309
  private
1106
1310
 
1107
1311
  # Response has key `paths`.
1108
1312
  # From those, check if there is an error
1109
- # @return [Array] of Hash with 2 keys: `path` and `result`
1313
+ # @return [Array<Hash>] list of hashes with 2 keys: `path` and `result`
1110
1314
  def response_to_result(response, success_msg)
1111
1315
  errors = []
1112
1316
  obj_list = []