aspera-cli 4.18.1 → 4.20.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 (85) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +33 -0
  4. data/CONTRIBUTING.md +17 -12
  5. data/README.md +396 -185
  6. data/bin/asession +26 -19
  7. data/examples/build_exec +74 -0
  8. data/examples/{rubyc → build_exec_rubyc} +18 -2
  9. data/examples/get_proto_file.rb +7 -0
  10. data/lib/aspera/agent/alpha.rb +8 -8
  11. data/lib/aspera/agent/base.rb +4 -18
  12. data/lib/aspera/agent/connect.rb +14 -13
  13. data/lib/aspera/agent/direct.rb +123 -120
  14. data/lib/aspera/agent/httpgw.rb +2 -3
  15. data/lib/aspera/agent/node.rb +10 -10
  16. data/lib/aspera/agent/trsdk.rb +17 -20
  17. data/lib/aspera/api/alee.rb +15 -0
  18. data/lib/aspera/api/aoc.rb +128 -99
  19. data/lib/aspera/api/ats.rb +1 -1
  20. data/lib/aspera/api/cos_node.rb +1 -1
  21. data/lib/aspera/api/httpgw.rb +104 -64
  22. data/lib/aspera/api/node.rb +33 -12
  23. data/lib/aspera/ascmd.rb +56 -48
  24. data/lib/aspera/ascp/installation.rb +142 -70
  25. data/lib/aspera/ascp/management.rb +7 -3
  26. data/lib/aspera/ascp/products.rb +13 -7
  27. data/lib/aspera/assert.rb +10 -5
  28. data/lib/aspera/cli/formatter.rb +42 -26
  29. data/lib/aspera/cli/hints.rb +2 -1
  30. data/lib/aspera/cli/info.rb +12 -10
  31. data/lib/aspera/cli/main.rb +16 -13
  32. data/lib/aspera/cli/manager.rb +15 -10
  33. data/lib/aspera/cli/plugin.rb +17 -31
  34. data/lib/aspera/cli/plugin_factory.rb +10 -1
  35. data/lib/aspera/cli/plugins/alee.rb +3 -3
  36. data/lib/aspera/cli/plugins/aoc.rb +222 -194
  37. data/lib/aspera/cli/plugins/ats.rb +16 -14
  38. data/lib/aspera/cli/plugins/config.rb +66 -53
  39. data/lib/aspera/cli/plugins/console.rb +3 -3
  40. data/lib/aspera/cli/plugins/faspex.rb +11 -21
  41. data/lib/aspera/cli/plugins/faspex5.rb +44 -42
  42. data/lib/aspera/cli/plugins/faspio.rb +2 -2
  43. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  44. data/lib/aspera/cli/plugins/node.rb +155 -96
  45. data/lib/aspera/cli/plugins/orchestrator.rb +14 -13
  46. data/lib/aspera/cli/plugins/preview.rb +8 -9
  47. data/lib/aspera/cli/plugins/server.rb +6 -10
  48. data/lib/aspera/cli/plugins/shares.rb +13 -9
  49. data/lib/aspera/cli/sync_actions.rb +72 -31
  50. data/lib/aspera/cli/transfer_agent.rb +13 -14
  51. data/lib/aspera/cli/transfer_progress.rb +36 -18
  52. data/lib/aspera/cli/version.rb +1 -1
  53. data/lib/aspera/command_line_builder.rb +3 -4
  54. data/lib/aspera/coverage.rb +13 -1
  55. data/lib/aspera/environment.rb +59 -10
  56. data/lib/aspera/faspex_gw.rb +3 -3
  57. data/lib/aspera/json_rpc.rb +1 -1
  58. data/lib/aspera/keychain/encrypted_hash.rb +2 -0
  59. data/lib/aspera/keychain/macos_security.rb +7 -12
  60. data/lib/aspera/log.rb +4 -4
  61. data/lib/aspera/node_simulator.rb +1 -1
  62. data/lib/aspera/oauth/base.rb +39 -45
  63. data/lib/aspera/oauth/factory.rb +11 -4
  64. data/lib/aspera/oauth/generic.rb +4 -8
  65. data/lib/aspera/oauth/jwt.rb +4 -4
  66. data/lib/aspera/oauth/url_json.rb +3 -2
  67. data/lib/aspera/oauth/web.rb +10 -6
  68. data/lib/aspera/persistency_action_once.rb +16 -8
  69. data/lib/aspera/preview/utils.rb +5 -16
  70. data/lib/aspera/rest.rb +100 -76
  71. data/lib/aspera/secret_hider.rb +3 -2
  72. data/lib/aspera/ssh.rb +1 -1
  73. data/lib/aspera/transfer/faux_file.rb +7 -5
  74. data/lib/aspera/transfer/parameters.rb +41 -35
  75. data/lib/aspera/transfer/spec.rb +16 -18
  76. data/lib/aspera/transfer/sync.rb +51 -50
  77. data/lib/aspera/transfer/uri.rb +1 -1
  78. data/lib/aspera/uri_reader.rb +1 -1
  79. data/lib/aspera/web_auth.rb +166 -18
  80. data/lib/aspera/web_server_simple.rb +27 -15
  81. data/lib/transfer_pb.rb +84 -0
  82. data/lib/transfer_services_pb.rb +82 -0
  83. data.tar.gz.sig +0 -0
  84. metadata +25 -6
  85. metadata.gz.sig +0 -0
@@ -22,6 +22,10 @@ module Aspera
22
22
  class Node < Cli::BasicAuthPlugin
23
23
  include SyncActions
24
24
  class << self
25
+ # directory: node, container: shares
26
+ FOLDER_TYPES = %w[directory container].freeze
27
+ private_constant :FOLDER_TYPES
28
+
25
29
  def application_name
26
30
  'HSTS Node API'
27
31
  end
@@ -43,9 +47,9 @@ module Aspera
43
47
  test_endpoint = 'ping'
44
48
  result = api.call(operation: 'GET', subpath: test_endpoint)
45
49
  next unless result[:http].body.eql?('')
46
- url_length = -2 - test_endpoint.length
50
+ url_end = -2 - test_endpoint.length
47
51
  return {
48
- url: result[:http].uri.to_s[0..url_length],
52
+ url: result[:http].uri.to_s[0..url_end],
49
53
  version: 'requires authentication'
50
54
  }
51
55
  rescue StandardError => e
@@ -69,16 +73,25 @@ module Aspera
69
73
  end
70
74
 
71
75
  def declare_options(options)
76
+ return if @options_declared
77
+ @options_declared = true
72
78
  options.declare(:validator, 'Identifier of validator (optional for central)')
73
79
  options.declare(:asperabrowserurl, 'URL for simple aspera web ui', default: 'https://asperabrowser.mybluemix.net')
74
80
  options.declare(:sync_name, 'Sync name')
75
81
  options.declare(
76
82
  :default_ports, 'Use standard FASP ports or get from node api (gen4)', values: :bool, default: :yes,
77
83
  handler: {o: Api::Node, m: :use_standard_ports})
78
- options.declare(:root_id, 'File id of top folder if using bearer tokens')
84
+ options.declare(
85
+ :node_cache, 'Set to no to force actual file system read (gen4)', values: :bool,
86
+ handler: {o: Api::Node, m: :use_node_cache})
87
+ options.declare(:root_id, 'File id of top folder when using access key (override AK root id)')
79
88
  SyncActions.declare_options(options)
80
89
  options.parse_options!
81
90
  end
91
+
92
+ def gen3_entry_folder?(entry)
93
+ FOLDER_TYPES.include?(entry['type'])
94
+ end
82
95
  end
83
96
 
84
97
  # spellchecker: disable
@@ -164,7 +177,7 @@ module Aspera
164
177
  def c_result_translate_rem_prefix(response, type, success_msg, path_prefix)
165
178
  errors = []
166
179
  final_result = {type: :object_list, data: [], fields: [type, 'result']}
167
- JSON.parse(response[:http].body)['paths'].each do |p|
180
+ response['paths'].each do |p|
168
181
  result = success_msg
169
182
  if p.key?('error')
170
183
  Log.log.error{"#{p['error']['user_message']} : #{p['path']}"}
@@ -180,9 +193,6 @@ module Aspera
180
193
  return c_result_remove_prefix_path(final_result, type, path_prefix)
181
194
  end
182
195
 
183
- # directory: node, container: shares
184
- FOLDER_TYPE = %w[directory container].freeze
185
-
186
196
  def browse_gen3(prefix_path)
187
197
  folders_to_process = [get_one_argument_with_prefix(prefix_path, 'path')]
188
198
  query = options.get_option(:query, default: {})
@@ -206,26 +216,21 @@ module Aspera
206
216
  result = nil
207
217
  loop do
208
218
  # example: send_result={'items'=>[{'file'=>"filename1","permissions"=>[{'name'=>'read'},{'name'=>'write'}]}]}
209
- response = @api_node.call(
210
- operation: 'POST',
211
- subpath: 'files/browse',
212
- headers: {'Accept' => 'application/json'},
213
- body: query,
214
- body_type: :json)
219
+ response = @api_node.create('files/browse', query)
215
220
  # 'file','symbolic_link'
216
- if only_path || !FOLDER_TYPE.include?(response[:data]['self']['type'])
217
- result = { type: :single_object, data: response[:data]['self']}
221
+ if !Node.gen3_entry_folder?(response['self']) || only_path
222
+ result = { type: :single_object, data: response['self']}
218
223
  break
219
224
  end
220
- items = response[:data]['items']
221
- total_count ||= response[:data]['total_count']
225
+ items = response['items']
226
+ total_count ||= response['total_count']
222
227
  all_items.concat(items)
223
228
  if single_call
224
- formatter.display_item_count(response[:data]['item_count'], total_count)
229
+ formatter.display_item_count(response['item_count'], total_count)
225
230
  break
226
231
  end
227
232
  if recursive
228
- folders_to_process.concat(items.select{|i|FOLDER_TYPE.include?(i['type'])}.map{|i|i['path']})
233
+ folders_to_process.concat(items.select{|i|Node.gen3_entry_folder?(i)}.map{|i|i['path']})
229
234
  end
230
235
  if !max_items.nil? && (all_items.count >= max_items)
231
236
  all_items = all_items.slice(0, max_items) if all_items.count > max_items
@@ -239,6 +244,7 @@ module Aspera
239
244
  query.delete('skip')
240
245
  end
241
246
  result ||= {type: :object_list, data: all_items}
247
+ formatter.long_operation_terminated
242
248
  return c_result_remove_prefix_path(result, 'path', prefix_path)
243
249
  end
244
250
 
@@ -253,19 +259,19 @@ module Aspera
253
259
  when :search
254
260
  search_root = get_one_argument_with_prefix(prefix_path, 'search root')
255
261
  parameters = {'path' => search_root}
256
- other_options = query_option
262
+ other_options = options.get_option(:query)
257
263
  parameters.merge!(other_options) unless other_options.nil?
258
264
  resp = @api_node.create('files/search', parameters)
259
- result = { type: :object_list, data: resp[:data]['items']}
265
+ result = { type: :object_list, data: resp['items']}
260
266
  return Main.result_empty if result[:data].empty?
261
267
  result[:fields] = result[:data].first.keys.reject{|i|SEARCH_REMOVE_FIELDS.include?(i)}
262
- formatter.display_item_count(resp[:data]['item_count'], resp[:data]['total_count'])
263
- formatter.display_status("params: #{resp[:data]['parameters'].keys.map{|k|"#{k}:#{resp[:data]['parameters'][k]}"}.join(',')}")
268
+ formatter.display_item_count(resp['item_count'], resp['total_count'])
269
+ formatter.display_status("params: #{resp['parameters'].keys.map{|k|"#{k}:#{resp['parameters'][k]}"}.join(',')}")
264
270
  return c_result_remove_prefix_path(result, 'path', prefix_path)
265
271
  when :space
266
272
  path_list = get_all_arguments_with_prefix(prefix_path, 'folder path or ext.val. list')
267
273
  resp = @api_node.create('space', { 'paths' => path_list.map {|i| { path: i} } })
268
- result = { type: :object_list, data: resp[:data]['paths']}
274
+ result = { type: :object_list, data: resp['paths']}
269
275
  # return c_result_translate_rem_prefix(resp,'folder','created',prefix_path)
270
276
  return c_result_remove_prefix_path(result, 'path', prefix_path)
271
277
  when :mkdir
@@ -311,7 +317,7 @@ module Aspera
311
317
  # prepare payload for single request
312
318
  setup_payload = {transfer_requests: [{transfer_request: request_transfer_spec}]}
313
319
  # only one request, so only one answer
314
- transfer_spec = @api_node.create('files/sync_setup', setup_payload)[:data]['transfer_specs'].first['transfer_spec']
320
+ transfer_spec = @api_node.create('files/sync_setup', setup_payload)['transfer_specs'].first['transfer_spec']
315
321
  # API returns null tag... but async does not like it
316
322
  transfer_spec.delete_if{ |_k, v| v.nil? }
317
323
  # delete this part, as the returned value contains only destination, and not sources
@@ -333,7 +339,7 @@ module Aspera
333
339
  # prepare payload for single request
334
340
  setup_payload = {transfer_requests: [{transfer_request: request_transfer_spec}]}
335
341
  # only one request, so only one answer
336
- transfer_spec = @api_node.create("files/#{command}_setup", setup_payload)[:data]['transfer_specs'].first['transfer_spec']
342
+ transfer_spec = @api_node.create("files/#{command}_setup", setup_payload)['transfer_specs'].first['transfer_spec']
337
343
  # delete this part, as the returned value contains only destination, and not sources
338
344
  transfer_spec.delete('paths') if command.eql?(:upload)
339
345
  return Main.result_transfer(transfer.start(transfer_spec))
@@ -363,13 +369,13 @@ module Aspera
363
369
  when *Plugin::ALL_OPS
364
370
  return entity_command(ak_command, @api_node, 'access_keys') do |field, value|
365
371
  raise 'only selector: %id:self' unless field.eql?('id') && value.eql?('self')
366
- @api_node.read('access_keys/self')[:data]['id']
372
+ @api_node.read('access_keys/self')['id']
367
373
  end
368
374
  when :do
369
375
  access_key_id = options.get_next_argument('access key id')
370
376
  root_file_id = options.get_option(:root_id)
371
377
  if root_file_id.nil?
372
- ak_info = @api_node.read("access_keys/#{access_key_id}")[:data]
378
+ ak_info = @api_node.read("access_keys/#{access_key_id}")
373
379
  # change API credentials if different access key
374
380
  if !access_key_id.eql?('self')
375
381
  @api_node.auth_params[:username] = ak_info['id']
@@ -381,7 +387,7 @@ module Aspera
381
387
  return execute_command_gen4(command_repo, root_file_id)
382
388
  when :set_bearer_key
383
389
  access_key_id = options.get_next_argument('access key id')
384
- access_key_id = @api_node.read('access_keys/self')[:data]['id'] if access_key_id.eql?('self')
390
+ access_key_id = @api_node.read('access_keys/self')['id'] if access_key_id.eql?('self')
385
391
  bearer_key_pem = options.get_next_argument('public or private RSA key PEM value', validation: String)
386
392
  key = OpenSSL::PKey.read(bearer_key_pem)
387
393
  key = key.public_key if key.private?
@@ -392,7 +398,7 @@ module Aspera
392
398
  when :health
393
399
  nagios = Nagios.new
394
400
  begin
395
- info = @api_node.read('info')[:data]
401
+ info = @api_node.read('info')
396
402
  nagios.add_ok('node api', 'accessible')
397
403
  nagios.check_time_offset(info['current_time'], 'node api')
398
404
  nagios.check_product_version('node api', 'entsrv', info['version'])
@@ -412,17 +418,17 @@ module Aspera
412
418
  end
413
419
  return nagios.result
414
420
  when :events
415
- events = @api_node.read('events', query_read_delete)[:data]
421
+ events = @api_node.read('events', query_read_delete)
416
422
  return { type: :object_list, data: events, fields: ->(f){!f.start_with?('data')} }
417
423
  when :info
418
- nd_info = @api_node.read('info')[:data]
424
+ nd_info = @api_node.read('info')
419
425
  return { type: :single_object, data: nd_info}
420
426
  when :slash
421
- nd_info = @api_node.read('')[:data]
427
+ nd_info = @api_node.read('')
422
428
  return { type: :single_object, data: nd_info}
423
429
  when :license
424
430
  # requires: asnodeadmin -mu <node user> --acl-add=internal --internal
425
- node_license = @api_node.read('license')[:data]
431
+ node_license = @api_node.read('license')
426
432
  if node_license['failure'].is_a?(String) && node_license['failure'].include?('ACL')
427
433
  Log.log.error('server must have: asnodeadmin -mu <node user> --acl-add=internal --internal')
428
434
  end
@@ -432,8 +438,8 @@ module Aspera
432
438
  end
433
439
  end
434
440
 
435
- # @return [Hash] api and main file id for given path or id
436
- # Allows to specify a file by its path or by its id on the node
441
+ # Allows to specify a file by its path or by its id on the node in command line
442
+ # @return [Hash] api and main file id for given path or id in next argument
437
443
  def apifid_from_next_arg(top_file_id)
438
444
  file_path = instance_identifier(description: 'path or %id:<id>') do |attribute, value|
439
445
  raise 'Only selection "id" is supported (file id)' unless attribute.eql?('id')
@@ -445,6 +451,9 @@ module Aspera
445
451
  end
446
452
 
447
453
  def execute_command_gen4(command_repo, top_file_id)
454
+ override_file_id = options.get_option(:root_id)
455
+ top_file_id = override_file_id unless override_file_id.nil?
456
+ raise 'Specify root file id with option root_id' if top_file_id.nil?
448
457
  case command_repo
449
458
  when :v3
450
459
  # NOTE: other common actions are unauthorized with user scope
@@ -453,7 +462,7 @@ module Aspera
453
462
  apifid = @api_node.resolve_api_fid(top_file_id, '')
454
463
  return Node.new(**init_params, api: apifid[:api]).execute_action(command_legacy)
455
464
  when :node_info, :bearer_token_node
456
- apifid = @api_node.resolve_api_fid(top_file_id, options.get_next_argument('path'))
465
+ apifid = apifid_from_next_arg(top_file_id)
457
466
  result = {
458
467
  url: apifid[:api].base_url,
459
468
  root_id: apifid[:file_id]
@@ -473,10 +482,14 @@ module Aspera
473
482
  OAuth::Factory.bearer_extract(result[:password])
474
483
  return Main.result_status(result[:password])
475
484
  when :browse
476
- apifid = @api_node.resolve_api_fid(top_file_id, options.get_next_argument('path'))
477
- file_info = apifid[:api].read("files/#{apifid[:file_id]}")[:data]
485
+ apifid = apifid_from_next_arg(top_file_id)
486
+ file_info = apifid[:api].read_with_cache("files/#{apifid[:file_id]}")
478
487
  if file_info['type'].eql?('folder')
479
- result = apifid[:api].read("files/#{apifid[:file_id]}/files", query_read_delete)
488
+ result = apifid[:api].call(
489
+ operation: 'GET',
490
+ subpath: "files/#{apifid[:file_id]}/files",
491
+ headers: Api::Node.cache_control_headers,
492
+ query: query_read_delete)
480
493
  items = result[:data]
481
494
  formatter.display_item_count(result[:data].length, result[:http]['X-Total-Count'])
482
495
  else
@@ -484,25 +497,33 @@ module Aspera
484
497
  end
485
498
  return {type: :object_list, data: items, fields: %w[name type recursive_size size modified_time access_level]}
486
499
  when :find
487
- apifid = @api_node.resolve_api_fid(top_file_id, options.get_next_argument('path'))
500
+ apifid = apifid_from_next_arg(top_file_id)
488
501
  test_block = Api::Node.file_matcher_from_argument(options)
489
502
  return {type: :object_list, data: @api_node.find_files(apifid[:file_id], test_block), fields: ['path']}
490
503
  when :mkdir
491
504
  containing_folder_path = options.get_next_argument('path').split(Api::Node::PATH_SEPARATOR)
492
505
  new_folder = containing_folder_path.pop
493
- apifid = @api_node.resolve_api_fid(top_file_id, containing_folder_path.join(Api::Node::PATH_SEPARATOR))
494
- result = apifid[:api].create("files/#{apifid[:file_id]}/files", {name: new_folder, type: :folder})[:data]
506
+ # add trailing slash to force last link to be resolved
507
+ apifid = @api_node.resolve_api_fid(top_file_id, containing_folder_path.join(Api::Node::PATH_SEPARATOR), true)
508
+ result = apifid[:api].create("files/#{apifid[:file_id]}/files", {name: new_folder, type: :folder})
495
509
  return Main.result_status("created: #{result['name']} (id=#{result['id']})")
496
510
  when :rename
497
511
  file_path = options.get_next_argument('source path')
498
512
  apifid = @api_node.resolve_api_fid(top_file_id, file_path)
499
513
  newname = options.get_next_argument('new name')
500
- result = apifid[:api].update("files/#{apifid[:file_id]}", {name: newname})[:data]
514
+ result = apifid[:api].update("files/#{apifid[:file_id]}", {name: newname})
501
515
  return Main.result_status("renamed to #{newname}")
502
516
  when :delete
503
517
  return do_bulk_operation(command: command_repo, descr: 'path', values: String, id_result: 'path') do |l_path|
504
- apifid = @api_node.resolve_api_fid(top_file_id, l_path)
505
- result = apifid[:api].delete("files/#{apifid[:file_id]}")[:data]
518
+ apifid = if (m = l_path.match(REGEX_LOOKUP_ID_BY_FIELD))
519
+ {
520
+ api: @api_node,
521
+ file_id: m[2]
522
+ }
523
+ else
524
+ @api_node.resolve_api_fid(top_file_id, l_path)
525
+ end
526
+ result = apifid[:api].delete("files/#{apifid[:file_id]}")
506
527
  {'path' => l_path}
507
528
  end
508
529
  when :sync
@@ -522,26 +543,24 @@ module Aspera
522
543
  transfer_spec
523
544
  end
524
545
  when :upload
525
- apifid = @api_node.resolve_api_fid(top_file_id, transfer.destination_folder(Transfer::Spec::DIRECTION_SEND))
546
+ apifid = @api_node.resolve_api_fid(top_file_id, transfer.destination_folder(Transfer::Spec::DIRECTION_SEND), true)
526
547
  return Main.result_transfer(transfer.start(apifid[:api].transfer_spec_gen4(apifid[:file_id], Transfer::Spec::DIRECTION_SEND)))
527
548
  when :download
528
549
  source_paths = transfer.ts_source_paths
529
550
  # special case for AoC : all files must be in same folder
530
551
  source_folder = source_paths.shift['source']
531
552
  # if a single file: split into folder and path
532
- apifid = @api_node.resolve_api_fid(top_file_id, source_folder)
553
+ apifid = @api_node.resolve_api_fid(top_file_id, source_folder, true)
533
554
  if source_paths.empty?
534
- file_info = apifid[:api].read("files/#{apifid[:file_id]}")[:data]
555
+ # get precise info in this element
556
+ file_info = apifid[:api].read("files/#{apifid[:file_id]}")
535
557
  case file_info['type']
536
558
  when 'file'
537
559
  # if the single source is a file, we need to split into folder path and filename
538
560
  src_dir_elements = source_folder.split(Api::Node::PATH_SEPARATOR)
539
- # filename is the last one
561
+ # filename is the last one, source folder is what remains
540
562
  source_paths = [{'source' => src_dir_elements.pop}]
541
- # source folder is what remains
542
- source_folder = src_dir_elements.join(Api::Node::PATH_SEPARATOR)
543
- # TODO: instead of creating a new object, use the same, and change file id with parent folder id ? possible ?
544
- apifid = @api_node.resolve_api_fid(top_file_id, source_folder)
563
+ apifid = @api_node.resolve_api_fid(top_file_id, src_dir_elements.join(Api::Node::PATH_SEPARATOR), true)
545
564
  when 'link', 'folder'
546
565
  # single source is 'folder' or 'link'
547
566
  # TODO: add this ? , 'destination'=>file_info['name']
@@ -569,12 +588,12 @@ module Aspera
569
588
  return Main.result_status("downloaded: #{file_name}")
570
589
  when :show
571
590
  apifid = apifid_from_next_arg(top_file_id)
572
- items = apifid[:api].read("files/#{apifid[:file_id]}")[:data]
591
+ items = apifid[:api].read("files/#{apifid[:file_id]}")
573
592
  return {type: :single_object, data: items}
574
593
  when :modify
575
594
  apifid = apifid_from_next_arg(top_file_id)
576
595
  update_param = options.get_next_argument('update data', validation: Hash)
577
- apifid[:api].update("files/#{apifid[:file_id]}", update_param)[:data]
596
+ apifid[:api].update("files/#{apifid[:file_id]}", update_param)
578
597
  return Main.result_status('Done')
579
598
  when :thumbnail
580
599
  apifid = apifid_from_next_arg(top_file_id)
@@ -594,13 +613,14 @@ module Aspera
594
613
  # add which one to get
595
614
  list_options['file_id'] = apifid[:file_id]
596
615
  list_options['inherited'] ||= false
597
- items = apifid[:api].read('permissions', list_options)[:data]
616
+ items = apifid[:api].read('permissions', list_options)
598
617
  return {type: :object_list, data: items}
599
618
  when :delete
600
619
  return do_bulk_operation(command: command_perm, descr: 'identifier', values: :identifier) do |one_id|
601
620
  apifid[:api].delete("permissions/#{one_id}")
602
621
  # notify application of deletion
603
- the_app[:api].permissions_send_event(created_data: created_data, app_info: the_app, types: ['permission.deleted']) unless the_app.nil?
622
+ the_app = apifid[:api].app_info
623
+ the_app&.[](:api)&.permissions_send_event(event_data: {}, app_info: the_app, types: ['permission.deleted'])
604
624
  {'id' => one_id}
605
625
  end
606
626
  when :create
@@ -610,11 +630,11 @@ module Aspera
610
630
  create_param['access_levels'] = Api::Node::ACCESS_LEVELS unless create_param.key?('access_levels')
611
631
  # add application specific tags (AoC)
612
632
  the_app = apifid[:api].app_info
613
- the_app[:api].permissions_set_create_params(create_param: create_param, app_info: the_app) unless the_app.nil?
633
+ the_app&.[](:api)&.permissions_set_create_params(perm_data: create_param, app_info: the_app)
614
634
  # create permission
615
- created_data = apifid[:api].create('permissions', create_param)[:data]
635
+ created_data = apifid[:api].create('permissions', create_param)
616
636
  # notify application of creation
617
- the_app[:api].permissions_send_event(created_data: created_data, app_info: the_app) unless the_app.nil?
637
+ the_app&.[](:api)&.permissions_send_event(event_data: created_data, app_info: the_app)
618
638
  return { type: :single_object, data: created_data}
619
639
  else Aspera.error_unreachable_line
620
640
  end
@@ -631,14 +651,14 @@ module Aspera
631
651
  if async_name.nil?
632
652
  async_id = instance_identifier
633
653
  if async_id.eql?(SpecialValues::ALL) && %i[show delete].include?(command)
634
- async_ids = @api_node.read('async/list')[:data]['sync_ids']
654
+ async_ids = @api_node.read('async/list')['sync_ids']
635
655
  else
636
656
  Integer(async_id) # must be integer
637
657
  async_ids = [async_id]
638
658
  end
639
659
  else
640
- async_ids = @api_node.read('async/list')[:data]['sync_ids']
641
- summaries = @api_node.create('async/summary', {'syncs' => async_ids})[:data]['sync_summaries']
660
+ async_ids = @api_node.read('async/list')['sync_ids']
661
+ summaries = @api_node.create('async/summary', {'syncs' => async_ids})['sync_summaries']
642
662
  selected = summaries.find{|s|s['name'].eql?(async_name)}
643
663
  raise "no such sync: #{async_name}" if selected.nil?
644
664
  async_id = selected['snid']
@@ -648,19 +668,19 @@ module Aspera
648
668
  end
649
669
  case command
650
670
  when :list
651
- resp = @api_node.read('async/list')[:data]['sync_ids']
671
+ resp = @api_node.read('async/list')['sync_ids']
652
672
  return { type: :value_list, data: resp, name: 'id' }
653
673
  when :show
654
- resp = @api_node.create('async/summary', post_data)[:data]['sync_summaries']
674
+ resp = @api_node.create('async/summary', post_data)['sync_summaries']
655
675
  return Main.result_empty if resp.empty?
656
676
  return { type: :object_list, data: resp, fields: %w[snid name local_dir remote_dir] } if async_id.eql?(SpecialValues::ALL)
657
677
  return { type: :single_object, data: resp.first }
658
678
  when :delete
659
- resp = @api_node.create('async/delete', post_data)[:data]
679
+ resp = @api_node.create('async/delete', post_data)
660
680
  return { type: :single_object, data: resp, name: 'id' }
661
681
  when :bandwidth
662
682
  post_data['seconds'] = 100 # TODO: as parameter with --value
663
- resp = @api_node.create('async/bandwidth', post_data)[:data]
683
+ resp = @api_node.create('async/bandwidth', post_data)
664
684
  data = resp['bandwidth_data']
665
685
  return Main.result_empty if data.empty?
666
686
  data = data.first[async_id]['data']
@@ -670,9 +690,9 @@ module Aspera
670
690
  # filename str
671
691
  # skip int
672
692
  # status int
673
- filter = query_option
693
+ filter = options.get_option(:query)
674
694
  post_data.merge!(filter) unless filter.nil?
675
- resp = @api_node.create('async/files', post_data)[:data]
695
+ resp = @api_node.create('async/files', post_data)
676
696
  data = resp['sync_files']
677
697
  data = data.first[async_id] unless data.empty?
678
698
  iteration_data = []
@@ -695,7 +715,7 @@ module Aspera
695
715
  skip_ids_persistency&.save
696
716
  return { type: :object_list, data: data, name: 'id' }
697
717
  when :counters
698
- resp = @api_node.create('async/counters', post_data)[:data]['sync_counters'].first[async_id].last
718
+ resp = @api_node.create('async/counters', post_data)['sync_counters'].first[async_id].last
699
719
  return Main.result_empty if resp.nil?
700
720
  return { type: :single_object, data: resp }
701
721
  end
@@ -707,8 +727,8 @@ module Aspera
707
727
  # @param [String] value value of the field to search
708
728
  def ssync_lookup(field, value)
709
729
  raise Cli::BadArgument, "Only search by name is supported (#{field})" unless field.eql?('name')
710
- @api_node.read('asyncs')[:data]['ids'].each do |id|
711
- sync_info = @api_node.read("asyncs/#{id}")[:data]['configuration']
730
+ @api_node.read('asyncs')['ids'].each do |id|
731
+ sync_info = @api_node.read("asyncs/#{id}")['configuration']
712
732
  # name is unique, so we can return
713
733
  return id if sync_info[field].eql?(value)
714
734
  end
@@ -749,27 +769,27 @@ module Aspera
749
769
  return Main.result_status('Done')
750
770
  end
751
771
  parameters = nil
752
- parameters = query_option(default: {}) if %i[bandwidth counters files].include?(sync_command)
753
- return { type: :single_object, data: @api_node.read("asyncs/#{asyncs_id}/#{sync_command}", parameters)[:data] }
772
+ parameters = options.get_option(:query, default: {}) if %i[bandwidth counters files].include?(sync_command)
773
+ return { type: :single_object, data: @api_node.read("asyncs/#{asyncs_id}/#{sync_command}", parameters) }
754
774
  end
755
775
  when :stream
756
776
  command = options.get_next_command(%i[list create show modify cancel])
757
777
  case command
758
778
  when :list
759
779
  resp = @api_node.read('ops/transfers', query_read_delete)
760
- return { type: :object_list, data: resp[:data], fields: %w[id status] } # TODO: useful?
780
+ return { type: :object_list, data: resp, fields: %w[id status] } # TODO: useful?
761
781
  when :create
762
782
  resp = @api_node.create('streams', value_create_modify(command: command))
763
- return { type: :single_object, data: resp[:data] }
783
+ return { type: :single_object, data: resp }
764
784
  when :show
765
785
  resp = @api_node.read("ops/transfers/#{options.get_next_argument('transfer id')}")
766
- return { type: :other_struct, data: resp[:data] }
786
+ return { type: :other_struct, data: resp }
767
787
  when :modify
768
788
  resp = @api_node.update("streams/#{options.get_next_argument('transfer id')}", value_create_modify(command: command))
769
- return { type: :other_struct, data: resp[:data] }
789
+ return { type: :other_struct, data: resp }
770
790
  when :cancel
771
791
  resp = @api_node.cancel("streams/#{options.get_next_argument('transfer id')}")
772
- return { type: :other_struct, data: resp[:data] }
792
+ return { type: :other_struct, data: resp }
773
793
  else
774
794
  raise 'error'
775
795
  end
@@ -782,14 +802,50 @@ module Aspera
782
802
  end
783
803
  case command
784
804
  when :list
785
- transfers_data = @api_node.read(res_class_path, query_read_delete)[:data]
805
+ iteration_persistency = nil
806
+ iteration_data = []
807
+ if options.get_option(:once_only, mandatory: true)
808
+ iteration_persistency = PersistencyActionOnce.new(
809
+ manager: persistency,
810
+ data: iteration_data,
811
+ id: IdGenerator.from_list([
812
+ 'node_transfers',
813
+ options.get_option(:url, mandatory: true),
814
+ options.get_option(:username, mandatory: true)
815
+ ]))
816
+ end
817
+ transfer_filter = query_read_delete(default: {})
818
+ if transfer_filter.delete('reset')
819
+ iteration_data.clear
820
+ iteration_persistency&.save
821
+ return Main.result_status('Persistency reset')
822
+ end
823
+ max_items = transfer_filter.delete(MAX_ITEMS)
824
+ transfer_filter['iteration_token'] = iteration_persistency.data[0] unless iteration_data.empty?
825
+ transfers_data = []
826
+ loop do
827
+ result = @api_node.call(operation: 'GET', subpath: res_class_path, query: transfer_filter)
828
+ data = result[:data]
829
+ transfers_data.concat(data)
830
+ if !max_items.nil? && (transfers_data.length >= max_items)
831
+ transfers_data = transfers_data.slice(0, max_items)
832
+ break
833
+ end
834
+ link_info = result[:http]['Link']
835
+ break if iteration_persistency.nil? || data.empty? || link_info.nil?
836
+ m = link_info.match(/<([^>]+)>/)
837
+ raise "Problem with iteration: #{link_info}" if m.nil?
838
+ iteration_token = CGI.parse(URI.parse(m[1]).query)['iteration_token']&.first
839
+ iteration_data[0] = transfer_filter['iteration_token'] = iteration_token
840
+ end
841
+ iteration_persistency&.save
786
842
  return {
787
843
  type: :object_list,
788
844
  data: transfers_data,
789
845
  fields: %w[id status start_spec.direction start_spec.remote_user start_spec.remote_host start_spec.destination_path]
790
846
  }
791
847
  when :sessions
792
- transfers_data = @api_node.read(res_class_path, query_read_delete)[:data]
848
+ transfers_data = @api_node.read(res_class_path, query_read_delete)
793
849
  sessions = transfers_data.map{|t|t['sessions']}.flatten
794
850
  sessions.each do |session|
795
851
  session['start_time'] = Time.at(session['start_time_usec'] / 1_000_000.0).utc.iso8601(0)
@@ -802,15 +858,15 @@ module Aspera
802
858
  }
803
859
  when :cancel
804
860
  resp = @api_node.cancel(one_res_path)
805
- return { type: :other_struct, data: resp[:data] }
861
+ return { type: :other_struct, data: resp }
806
862
  when :show
807
863
  resp = @api_node.read(one_res_path)
808
- return { type: :other_struct, data: resp[:data] }
864
+ return { type: :other_struct, data: resp }
809
865
  when :modify
810
866
  resp = @api_node.update(one_res_path, options.get_next_argument('update value', validation: Hash))
811
- return { type: :other_struct, data: resp[:data] }
867
+ return { type: :other_struct, data: resp }
812
868
  when :bandwidth_average
813
- transfers_data = @api_node.read(res_class_path, query_read_delete)[:data]
869
+ transfers_data = @api_node.read(res_class_path, query_read_delete)
814
870
  # collect all key dates
815
871
  bandwidth_period = {}
816
872
  dir_info = %i[avg_kbps sessions].freeze
@@ -865,12 +921,12 @@ module Aspera
865
921
  case command
866
922
  when :list
867
923
  resp = @api_node.read('rund/services')
868
- return { type: :object_list, data: resp[:data]['services'] }
924
+ return { type: :object_list, data: resp['services'] }
869
925
  when :create
870
926
  # @json:'{"type":"WATCHFOLDERD","run_as":{"user":"user1"}}'
871
927
  params = options.get_next_argument('creation data', validation: Hash)
872
928
  resp = @api_node.create('rund/services', params)
873
- return Main.result_status("#{resp[:data]['id']} created")
929
+ return Main.result_status("#{resp['id']} created")
874
930
  when :delete
875
931
  @api_node.delete("rund/services/#{service_id}")
876
932
  return Main.result_status("#{service_id} deleted")
@@ -888,36 +944,37 @@ module Aspera
888
944
  case command
889
945
  when :create
890
946
  resp = @api_node.create(res_class_path, value_create_modify(command: command))
891
- return Main.result_status("#{resp[:data]['id']} created")
947
+ return Main.result_status("#{resp['id']} created")
892
948
  when :list
893
949
  resp = @api_node.read(res_class_path, query_read_delete)
894
- return { type: :value_list, data: resp[:data]['ids'], name: 'id' }
950
+ return { type: :value_list, data: resp['ids'], name: 'id' }
895
951
  when :show
896
- return { type: :single_object, data: @api_node.read(one_res_path)[:data]}
952
+ return { type: :single_object, data: @api_node.read(one_res_path)}
897
953
  when :modify
898
- @api_node.update(one_res_path, query_option(mandatory: true))
954
+ @api_node.update(one_res_path, options.get_option(:query, mandatory: true))
899
955
  return Main.result_status("#{one_res_id} updated")
900
956
  when :delete
901
957
  @api_node.delete(one_res_path)
902
958
  return Main.result_status("#{one_res_id} deleted")
903
959
  when :state
904
- return { type: :single_object, data: @api_node.read("#{one_res_path}/state")[:data] }
960
+ return { type: :single_object, data: @api_node.read("#{one_res_path}/state") }
905
961
  end
906
962
  when :central
907
963
  command = options.get_next_command(%i[session file])
908
964
  validator_id = options.get_option(:validator)
909
965
  validation = {'validator_id' => validator_id} unless validator_id.nil?
910
- request_data = query_option(default: {})
966
+ request_data = options.get_option(:query, default: {})
911
967
  case command
912
968
  when :session
913
969
  command = options.get_next_command([:list])
914
970
  case command
915
971
  when :list
972
+ request_data = options.get_next_argument('request data', mandatory: false, validation: Hash, default: {})
916
973
  request_data.deep_merge!({'validation' => validation}) unless validation.nil?
917
974
  resp = @api_node.create('services/rest/transfers/v1/sessions', request_data)
918
975
  return {
919
976
  type: :object_list,
920
- data: resp[:data]['session_info_result']['session_info'],
977
+ data: resp['session_info_result']['session_info'],
921
978
  fields: %w[session_uuid status transport direction bytes_transferred]
922
979
  }
923
980
  end
@@ -925,12 +982,14 @@ module Aspera
925
982
  command = options.get_next_command(%i[list modify])
926
983
  case command
927
984
  when :list
985
+ request_data = options.get_next_argument('request data', mandatory: false, validation: Hash, default: {})
928
986
  request_data.deep_merge!({'validation' => validation}) unless validation.nil?
929
- resp = @api_node.create('services/rest/transfers/v1/files', request_data)[:data]
987
+ resp = @api_node.create('services/rest/transfers/v1/files', request_data)
930
988
  resp = JSON.parse(resp) if resp.is_a?(String)
931
989
  Log.log.debug{Log.dump(:resp, resp)}
932
990
  return { type: :object_list, data: resp['file_transfer_info_result']['file_transfer_info'], fields: %w[session_uuid file_id status path]}
933
991
  when :modify
992
+ request_data = options.get_next_argument('request data', mandatory: false, validation: Hash, default: {})
934
993
  request_data.deep_merge!(validation) unless validation.nil?
935
994
  @api_node.update('services/rest/transfers/v1/files', request_data)
936
995
  return Main.result_status('updated')