aspera-cli 4.19.0 → 4.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +46 -0
  4. data/CONTRIBUTING.md +18 -4
  5. data/README.md +886 -510
  6. data/bin/asession +27 -20
  7. data/examples/build_exec +65 -76
  8. data/examples/build_exec_rubyc +40 -0
  9. data/examples/get_proto_file.rb +7 -0
  10. data/lib/aspera/agent/alpha.rb +18 -24
  11. data/lib/aspera/agent/base.rb +2 -18
  12. data/lib/aspera/agent/connect.rb +34 -15
  13. data/lib/aspera/agent/direct.rb +44 -54
  14. data/lib/aspera/agent/httpgw.rb +2 -3
  15. data/lib/aspera/agent/node.rb +11 -21
  16. data/lib/aspera/agent/{trsdk.rb → transferd.rb} +27 -51
  17. data/lib/aspera/api/alee.rb +15 -0
  18. data/lib/aspera/api/aoc.rb +139 -105
  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 +15 -10
  22. data/lib/aspera/api/node.rb +70 -32
  23. data/lib/aspera/ascmd.rb +56 -48
  24. data/lib/aspera/ascp/installation.rb +166 -70
  25. data/lib/aspera/ascp/management.rb +30 -8
  26. data/lib/aspera/assert.rb +10 -5
  27. data/lib/aspera/cli/formatter.rb +166 -162
  28. data/lib/aspera/cli/hints.rb +2 -1
  29. data/lib/aspera/cli/info.rb +12 -10
  30. data/lib/aspera/cli/main.rb +28 -13
  31. data/lib/aspera/cli/manager.rb +7 -2
  32. data/lib/aspera/cli/plugin.rb +17 -31
  33. data/lib/aspera/cli/plugins/alee.rb +3 -3
  34. data/lib/aspera/cli/plugins/aoc.rb +246 -208
  35. data/lib/aspera/cli/plugins/ats.rb +16 -14
  36. data/lib/aspera/cli/plugins/config.rb +154 -94
  37. data/lib/aspera/cli/plugins/console.rb +3 -3
  38. data/lib/aspera/cli/plugins/cos.rb +1 -0
  39. data/lib/aspera/cli/plugins/faspex.rb +15 -23
  40. data/lib/aspera/cli/plugins/faspex5.rb +64 -50
  41. data/lib/aspera/cli/plugins/faspio.rb +2 -2
  42. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  43. data/lib/aspera/cli/plugins/node.rb +174 -109
  44. data/lib/aspera/cli/plugins/orchestrator.rb +14 -13
  45. data/lib/aspera/cli/plugins/preview.rb +8 -9
  46. data/lib/aspera/cli/plugins/server.rb +5 -9
  47. data/lib/aspera/cli/plugins/shares.rb +2 -2
  48. data/lib/aspera/cli/sync_actions.rb +2 -2
  49. data/lib/aspera/cli/transfer_agent.rb +12 -14
  50. data/lib/aspera/cli/transfer_progress.rb +37 -17
  51. data/lib/aspera/cli/version.rb +1 -1
  52. data/lib/aspera/command_line_builder.rb +4 -5
  53. data/lib/aspera/coverage.rb +13 -1
  54. data/lib/aspera/environment.rb +75 -25
  55. data/lib/aspera/faspex_gw.rb +2 -2
  56. data/lib/aspera/json_rpc.rb +1 -1
  57. data/lib/aspera/keychain/macos_security.rb +7 -12
  58. data/lib/aspera/log.rb +3 -4
  59. data/lib/aspera/node_simulator.rb +230 -112
  60. data/lib/aspera/oauth/base.rb +64 -83
  61. data/lib/aspera/oauth/factory.rb +52 -6
  62. data/lib/aspera/oauth/generic.rb +4 -8
  63. data/lib/aspera/oauth/jwt.rb +6 -3
  64. data/lib/aspera/oauth/url_json.rb +1 -2
  65. data/lib/aspera/oauth/web.rb +5 -2
  66. data/lib/aspera/persistency_action_once.rb +16 -8
  67. data/lib/aspera/persistency_folder.rb +20 -2
  68. data/lib/aspera/preview/generator.rb +1 -1
  69. data/lib/aspera/preview/utils.rb +11 -17
  70. data/lib/aspera/products/alpha.rb +30 -0
  71. data/lib/aspera/products/connect.rb +48 -0
  72. data/lib/aspera/products/other.rb +82 -0
  73. data/lib/aspera/products/transferd.rb +54 -0
  74. data/lib/aspera/rest.rb +116 -87
  75. data/lib/aspera/secret_hider.rb +2 -2
  76. data/lib/aspera/ssh.rb +31 -24
  77. data/lib/aspera/transfer/faux_file.rb +4 -4
  78. data/lib/aspera/transfer/parameters.rb +16 -17
  79. data/lib/aspera/transfer/spec.rb +12 -12
  80. data/lib/aspera/transfer/spec.yaml +22 -20
  81. data/lib/aspera/transfer/sync.rb +2 -10
  82. data/lib/aspera/transfer/uri.rb +3 -3
  83. data/lib/aspera/uri_reader.rb +1 -1
  84. data/lib/aspera/web_auth.rb +166 -17
  85. data/lib/aspera/web_server_simple.rb +4 -3
  86. data/lib/transferd_pb.rb +86 -0
  87. data/lib/transferd_services_pb.rb +84 -0
  88. data.tar.gz.sig +0 -0
  89. metadata +58 -22
  90. metadata.gz.sig +0 -0
  91. data/lib/aspera/ascp/products.rb +0 -156
@@ -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
@@ -114,10 +127,13 @@ module Aspera
114
127
  # commands for execute_command_gen4
115
128
  COMMANDS_GEN4 = %i[mkdir rename delete upload download sync http_node_download show modify permission thumbnail v3].concat(NODE4_READ_ACTIONS).freeze
116
129
 
130
+ # commands supported in ATS for COS
117
131
  COMMANDS_COS = %i[upload download info access_keys api_details transfer].freeze
118
132
  COMMANDS_SHARES = (BASE_ACTIONS - %i[search]).freeze
119
133
  COMMANDS_FASPEX = COMMON_ACTIONS
120
134
 
135
+ GEN4_LS_FIELDS = %w[name type recursive_size size modified_time access_level].freeze
136
+
121
137
  def initialize(api: nil, **env)
122
138
  super(**env, basic_options: api.nil?)
123
139
  Node.declare_options(options) if api.nil?
@@ -164,7 +180,7 @@ module Aspera
164
180
  def c_result_translate_rem_prefix(response, type, success_msg, path_prefix)
165
181
  errors = []
166
182
  final_result = {type: :object_list, data: [], fields: [type, 'result']}
167
- JSON.parse(response[:http].body)['paths'].each do |p|
183
+ response['paths'].each do |p|
168
184
  result = success_msg
169
185
  if p.key?('error')
170
186
  Log.log.error{"#{p['error']['user_message']} : #{p['path']}"}
@@ -180,9 +196,6 @@ module Aspera
180
196
  return c_result_remove_prefix_path(final_result, type, path_prefix)
181
197
  end
182
198
 
183
- # directory: node, container: shares
184
- FOLDER_TYPE = %w[directory container].freeze
185
-
186
199
  def browse_gen3(prefix_path)
187
200
  folders_to_process = [get_one_argument_with_prefix(prefix_path, 'path')]
188
201
  query = options.get_option(:query, default: {})
@@ -206,26 +219,21 @@ module Aspera
206
219
  result = nil
207
220
  loop do
208
221
  # 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)
222
+ response = @api_node.create('files/browse', query)
215
223
  # 'file','symbolic_link'
216
- if only_path || !FOLDER_TYPE.include?(response[:data]['self']['type'])
217
- result = { type: :single_object, data: response[:data]['self']}
224
+ if !Node.gen3_entry_folder?(response['self']) || only_path
225
+ result = { type: :single_object, data: response['self']}
218
226
  break
219
227
  end
220
- items = response[:data]['items']
221
- total_count ||= response[:data]['total_count']
228
+ items = response['items']
229
+ total_count ||= response['total_count']
222
230
  all_items.concat(items)
223
231
  if single_call
224
- formatter.display_item_count(response[:data]['item_count'], total_count)
232
+ formatter.display_item_count(response['item_count'], total_count)
225
233
  break
226
234
  end
227
235
  if recursive
228
- folders_to_process.concat(items.select{|i|FOLDER_TYPE.include?(i['type'])}.map{|i|i['path']})
236
+ folders_to_process.concat(items.select{|i|Node.gen3_entry_folder?(i)}.map{|i|i['path']})
229
237
  end
230
238
  if !max_items.nil? && (all_items.count >= max_items)
231
239
  all_items = all_items.slice(0, max_items) if all_items.count > max_items
@@ -239,6 +247,7 @@ module Aspera
239
247
  query.delete('skip')
240
248
  end
241
249
  result ||= {type: :object_list, data: all_items}
250
+ formatter.long_operation_terminated
242
251
  return c_result_remove_prefix_path(result, 'path', prefix_path)
243
252
  end
244
253
 
@@ -253,19 +262,19 @@ module Aspera
253
262
  when :search
254
263
  search_root = get_one_argument_with_prefix(prefix_path, 'search root')
255
264
  parameters = {'path' => search_root}
256
- other_options = query_option
265
+ other_options = options.get_option(:query)
257
266
  parameters.merge!(other_options) unless other_options.nil?
258
267
  resp = @api_node.create('files/search', parameters)
259
- result = { type: :object_list, data: resp[:data]['items']}
268
+ result = { type: :object_list, data: resp['items']}
260
269
  return Main.result_empty if result[:data].empty?
261
270
  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(',')}")
271
+ formatter.display_item_count(resp['item_count'], resp['total_count'])
272
+ formatter.display_status("params: #{resp['parameters'].keys.map{|k|"#{k}:#{resp['parameters'][k]}"}.join(',')}")
264
273
  return c_result_remove_prefix_path(result, 'path', prefix_path)
265
274
  when :space
266
275
  path_list = get_all_arguments_with_prefix(prefix_path, 'folder path or ext.val. list')
267
276
  resp = @api_node.create('space', { 'paths' => path_list.map {|i| { path: i} } })
268
- result = { type: :object_list, data: resp[:data]['paths']}
277
+ result = { type: :object_list, data: resp['paths']}
269
278
  # return c_result_translate_rem_prefix(resp,'folder','created',prefix_path)
270
279
  return c_result_remove_prefix_path(result, 'path', prefix_path)
271
280
  when :mkdir
@@ -311,7 +320,7 @@ module Aspera
311
320
  # prepare payload for single request
312
321
  setup_payload = {transfer_requests: [{transfer_request: request_transfer_spec}]}
313
322
  # only one request, so only one answer
314
- transfer_spec = @api_node.create('files/sync_setup', setup_payload)[:data]['transfer_specs'].first['transfer_spec']
323
+ transfer_spec = @api_node.create('files/sync_setup', setup_payload)['transfer_specs'].first['transfer_spec']
315
324
  # API returns null tag... but async does not like it
316
325
  transfer_spec.delete_if{ |_k, v| v.nil? }
317
326
  # delete this part, as the returned value contains only destination, and not sources
@@ -333,7 +342,7 @@ module Aspera
333
342
  # prepare payload for single request
334
343
  setup_payload = {transfer_requests: [{transfer_request: request_transfer_spec}]}
335
344
  # only one request, so only one answer
336
- transfer_spec = @api_node.create("files/#{command}_setup", setup_payload)[:data]['transfer_specs'].first['transfer_spec']
345
+ transfer_spec = @api_node.create("files/#{command}_setup", setup_payload)['transfer_specs'].first['transfer_spec']
337
346
  # delete this part, as the returned value contains only destination, and not sources
338
347
  transfer_spec.delete('paths') if command.eql?(:upload)
339
348
  return Main.result_transfer(transfer.start(transfer_spec))
@@ -363,13 +372,13 @@ module Aspera
363
372
  when *Plugin::ALL_OPS
364
373
  return entity_command(ak_command, @api_node, 'access_keys') do |field, value|
365
374
  raise 'only selector: %id:self' unless field.eql?('id') && value.eql?('self')
366
- @api_node.read('access_keys/self')[:data]['id']
375
+ @api_node.read('access_keys/self')['id']
367
376
  end
368
377
  when :do
369
378
  access_key_id = options.get_next_argument('access key id')
370
379
  root_file_id = options.get_option(:root_id)
371
380
  if root_file_id.nil?
372
- ak_info = @api_node.read("access_keys/#{access_key_id}")[:data]
381
+ ak_info = @api_node.read("access_keys/#{access_key_id}")
373
382
  # change API credentials if different access key
374
383
  if !access_key_id.eql?('self')
375
384
  @api_node.auth_params[:username] = ak_info['id']
@@ -381,7 +390,7 @@ module Aspera
381
390
  return execute_command_gen4(command_repo, root_file_id)
382
391
  when :set_bearer_key
383
392
  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')
393
+ access_key_id = @api_node.read('access_keys/self')['id'] if access_key_id.eql?('self')
385
394
  bearer_key_pem = options.get_next_argument('public or private RSA key PEM value', validation: String)
386
395
  key = OpenSSL::PKey.read(bearer_key_pem)
387
396
  key = key.public_key if key.private?
@@ -392,7 +401,7 @@ module Aspera
392
401
  when :health
393
402
  nagios = Nagios.new
394
403
  begin
395
- info = @api_node.read('info')[:data]
404
+ info = @api_node.read('info')
396
405
  nagios.add_ok('node api', 'accessible')
397
406
  nagios.check_time_offset(info['current_time'], 'node api')
398
407
  nagios.check_product_version('node api', 'entsrv', info['version'])
@@ -412,17 +421,17 @@ module Aspera
412
421
  end
413
422
  return nagios.result
414
423
  when :events
415
- events = @api_node.read('events', query_read_delete)[:data]
424
+ events = @api_node.read('events', query_read_delete)
416
425
  return { type: :object_list, data: events, fields: ->(f){!f.start_with?('data')} }
417
426
  when :info
418
- nd_info = @api_node.read('info')[:data]
427
+ nd_info = @api_node.read('info')
419
428
  return { type: :single_object, data: nd_info}
420
429
  when :slash
421
- nd_info = @api_node.read('')[:data]
430
+ nd_info = @api_node.read('')
422
431
  return { type: :single_object, data: nd_info}
423
432
  when :license
424
433
  # requires: asnodeadmin -mu <node user> --acl-add=internal --internal
425
- node_license = @api_node.read('license')[:data]
434
+ node_license = @api_node.read('license')
426
435
  if node_license['failure'].is_a?(String) && node_license['failure'].include?('ACL')
427
436
  Log.log.error('server must have: asnodeadmin -mu <node user> --acl-add=internal --internal')
428
437
  end
@@ -432,8 +441,8 @@ module Aspera
432
441
  end
433
442
  end
434
443
 
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
444
+ # Allows to specify a file by its path or by its id on the node in command line
445
+ # @return [Hash] api and main file id for given path or id in next argument
437
446
  def apifid_from_next_arg(top_file_id)
438
447
  file_path = instance_identifier(description: 'path or %id:<id>') do |attribute, value|
439
448
  raise 'Only selection "id" is supported (file id)' unless attribute.eql?('id')
@@ -445,6 +454,9 @@ module Aspera
445
454
  end
446
455
 
447
456
  def execute_command_gen4(command_repo, top_file_id)
457
+ override_file_id = options.get_option(:root_id)
458
+ top_file_id = override_file_id unless override_file_id.nil?
459
+ raise 'Specify root file id with option root_id' if top_file_id.nil?
448
460
  case command_repo
449
461
  when :v3
450
462
  # NOTE: other common actions are unauthorized with user scope
@@ -453,7 +465,7 @@ module Aspera
453
465
  apifid = @api_node.resolve_api_fid(top_file_id, '')
454
466
  return Node.new(**init_params, api: apifid[:api]).execute_action(command_legacy)
455
467
  when :node_info, :bearer_token_node
456
- apifid = @api_node.resolve_api_fid(top_file_id, options.get_next_argument('path'))
468
+ apifid = apifid_from_next_arg(top_file_id)
457
469
  result = {
458
470
  url: apifid[:api].base_url,
459
471
  root_id: apifid[:file_id]
@@ -473,36 +485,41 @@ module Aspera
473
485
  OAuth::Factory.bearer_extract(result[:password])
474
486
  return Main.result_status(result[:password])
475
487
  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]
478
- if file_info['type'].eql?('folder')
479
- result = apifid[:api].read("files/#{apifid[:file_id]}/files", query_read_delete)
480
- items = result[:data]
481
- formatter.display_item_count(result[:data].length, result[:http]['X-Total-Count'])
482
- else
483
- items = [file_info]
488
+ apifid = apifid_from_next_arg(top_file_id)
489
+ file_info = apifid[:api].read_with_cache("files/#{apifid[:file_id]}")
490
+ unless file_info['type'].eql?('folder')
491
+ # a single file
492
+ return {type: :object_list, data: [file_info], fields: GEN4_LS_FIELDS}
484
493
  end
485
- return {type: :object_list, data: items, fields: %w[name type recursive_size size modified_time access_level]}
494
+ return {type: :object_list, data: apifid[:api].list_files(apifid[:file_id]), fields: GEN4_LS_FIELDS}
486
495
  when :find
487
- apifid = @api_node.resolve_api_fid(top_file_id, options.get_next_argument('path'))
488
- test_block = Api::Node.file_matcher_from_argument(options)
489
- return {type: :object_list, data: @api_node.find_files(apifid[:file_id], test_block), fields: ['path']}
496
+ apifid = apifid_from_next_arg(top_file_id)
497
+ find_lambda = Api::Node.file_matcher_from_argument(options)
498
+ return {type: :object_list, data: @api_node.find_files(apifid[:file_id], find_lambda), fields: ['path']}
490
499
  when :mkdir
491
500
  containing_folder_path = options.get_next_argument('path').split(Api::Node::PATH_SEPARATOR)
492
501
  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]
502
+ # add trailing slash to force last link to be resolved
503
+ apifid = @api_node.resolve_api_fid(top_file_id, containing_folder_path.join(Api::Node::PATH_SEPARATOR), true)
504
+ result = apifid[:api].create("files/#{apifid[:file_id]}/files", {name: new_folder, type: :folder})
495
505
  return Main.result_status("created: #{result['name']} (id=#{result['id']})")
496
506
  when :rename
497
507
  file_path = options.get_next_argument('source path')
498
508
  apifid = @api_node.resolve_api_fid(top_file_id, file_path)
499
509
  newname = options.get_next_argument('new name')
500
- result = apifid[:api].update("files/#{apifid[:file_id]}", {name: newname})[:data]
510
+ result = apifid[:api].update("files/#{apifid[:file_id]}", {name: newname})
501
511
  return Main.result_status("renamed to #{newname}")
502
512
  when :delete
503
513
  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]
514
+ apifid = if (m = l_path.match(REGEX_LOOKUP_ID_BY_FIELD))
515
+ {
516
+ api: @api_node,
517
+ file_id: m[2]
518
+ }
519
+ else
520
+ @api_node.resolve_api_fid(top_file_id, l_path)
521
+ end
522
+ result = apifid[:api].delete("files/#{apifid[:file_id]}")
506
523
  {'path' => l_path}
507
524
  end
508
525
  when :sync
@@ -522,27 +539,24 @@ module Aspera
522
539
  transfer_spec
523
540
  end
524
541
  when :upload
525
- apifid = @api_node.resolve_api_fid(top_file_id, transfer.destination_folder(Transfer::Spec::DIRECTION_SEND))
542
+ apifid = @api_node.resolve_api_fid(top_file_id, transfer.destination_folder(Transfer::Spec::DIRECTION_SEND), true)
526
543
  return Main.result_transfer(transfer.start(apifid[:api].transfer_spec_gen4(apifid[:file_id], Transfer::Spec::DIRECTION_SEND)))
527
544
  when :download
528
545
  source_paths = transfer.ts_source_paths
529
546
  # special case for AoC : all files must be in same folder
530
547
  source_folder = source_paths.shift['source']
531
548
  # if a single file: split into folder and path
532
- apifid = @api_node.resolve_api_fid(top_file_id, source_folder)
549
+ apifid = @api_node.resolve_api_fid(top_file_id, source_folder, true)
533
550
  if source_paths.empty?
534
551
  # get precise info in this element
535
- file_info = apifid[:api].read("files/#{apifid[:file_id]}")[:data]
552
+ file_info = apifid[:api].read("files/#{apifid[:file_id]}")
536
553
  case file_info['type']
537
554
  when 'file'
538
555
  # if the single source is a file, we need to split into folder path and filename
539
556
  src_dir_elements = source_folder.split(Api::Node::PATH_SEPARATOR)
540
557
  # filename is the last one, source folder is what remains
541
558
  source_paths = [{'source' => src_dir_elements.pop}]
542
- # add trailing / so that link is resolved, if it's a shared folder
543
- src_dir_elements.push('')
544
- source_folder = src_dir_elements.join(Api::Node::PATH_SEPARATOR)
545
- apifid = @api_node.resolve_api_fid(top_file_id, source_folder)
559
+ apifid = @api_node.resolve_api_fid(top_file_id, src_dir_elements.join(Api::Node::PATH_SEPARATOR), true)
546
560
  when 'link', 'folder'
547
561
  # single source is 'folder' or 'link'
548
562
  # TODO: add this ? , 'destination'=>file_info['name']
@@ -570,12 +584,12 @@ module Aspera
570
584
  return Main.result_status("downloaded: #{file_name}")
571
585
  when :show
572
586
  apifid = apifid_from_next_arg(top_file_id)
573
- items = apifid[:api].read("files/#{apifid[:file_id]}")[:data]
587
+ items = apifid[:api].read("files/#{apifid[:file_id]}")
574
588
  return {type: :single_object, data: items}
575
589
  when :modify
576
590
  apifid = apifid_from_next_arg(top_file_id)
577
591
  update_param = options.get_next_argument('update data', validation: Hash)
578
- apifid[:api].update("files/#{apifid[:file_id]}", update_param)[:data]
592
+ apifid[:api].update("files/#{apifid[:file_id]}", update_param)
579
593
  return Main.result_status('Done')
580
594
  when :thumbnail
581
595
  apifid = apifid_from_next_arg(top_file_id)
@@ -590,18 +604,19 @@ module Aspera
590
604
  command_perm = options.get_next_command(%i[list create delete])
591
605
  case command_perm
592
606
  when :list
593
- # generic options : TODO: as arg ? query_read_delete
594
- list_options ||= {'include' => Rest.array_params(%w[access_level permission_count])}
595
- # add which one to get
596
- list_options['file_id'] = apifid[:file_id]
597
- list_options['inherited'] ||= false
598
- items = apifid[:api].read('permissions', list_options)[:data]
607
+ list_query = query_read_delete(default: {'include' => Rest.array_params(%w[access_level permission_count])})
608
+ # specify file to get permissions for unless not specified
609
+ list_query['file_id'] = apifid[:file_id] unless apifid[:file_id].to_s.empty?
610
+ list_query['inherited'] = false if list_query.key?('file_id') && !list_query.key?('inherited')
611
+ # NOTE: supports per_page and page and header X-Total-Count
612
+ items = apifid[:api].read('permissions', list_query)
599
613
  return {type: :object_list, data: items}
600
614
  when :delete
601
615
  return do_bulk_operation(command: command_perm, descr: 'identifier', values: :identifier) do |one_id|
602
616
  apifid[:api].delete("permissions/#{one_id}")
603
617
  # notify application of deletion
604
- the_app[:api].permissions_send_event(created_data: created_data, app_info: the_app, types: ['permission.deleted']) unless the_app.nil?
618
+ the_app = apifid[:api].app_info
619
+ the_app&.[](:api)&.permissions_send_event(event_data: {}, app_info: the_app, types: ['permission.deleted'])
605
620
  {'id' => one_id}
606
621
  end
607
622
  when :create
@@ -611,11 +626,11 @@ module Aspera
611
626
  create_param['access_levels'] = Api::Node::ACCESS_LEVELS unless create_param.key?('access_levels')
612
627
  # add application specific tags (AoC)
613
628
  the_app = apifid[:api].app_info
614
- the_app[:api].permissions_set_create_params(create_param: create_param, app_info: the_app) unless the_app.nil?
629
+ the_app&.[](:api)&.permissions_set_create_params(perm_data: create_param, app_info: the_app)
615
630
  # create permission
616
- created_data = apifid[:api].create('permissions', create_param)[:data]
631
+ created_data = apifid[:api].create('permissions', create_param)
617
632
  # notify application of creation
618
- the_app[:api].permissions_send_event(created_data: created_data, app_info: the_app) unless the_app.nil?
633
+ the_app&.[](:api)&.permissions_send_event(event_data: created_data, app_info: the_app)
619
634
  return { type: :single_object, data: created_data}
620
635
  else Aspera.error_unreachable_line
621
636
  end
@@ -632,14 +647,14 @@ module Aspera
632
647
  if async_name.nil?
633
648
  async_id = instance_identifier
634
649
  if async_id.eql?(SpecialValues::ALL) && %i[show delete].include?(command)
635
- async_ids = @api_node.read('async/list')[:data]['sync_ids']
650
+ async_ids = @api_node.read('async/list')['sync_ids']
636
651
  else
637
652
  Integer(async_id) # must be integer
638
653
  async_ids = [async_id]
639
654
  end
640
655
  else
641
- async_ids = @api_node.read('async/list')[:data]['sync_ids']
642
- summaries = @api_node.create('async/summary', {'syncs' => async_ids})[:data]['sync_summaries']
656
+ async_ids = @api_node.read('async/list')['sync_ids']
657
+ summaries = @api_node.create('async/summary', {'syncs' => async_ids})['sync_summaries']
643
658
  selected = summaries.find{|s|s['name'].eql?(async_name)}
644
659
  raise "no such sync: #{async_name}" if selected.nil?
645
660
  async_id = selected['snid']
@@ -649,19 +664,19 @@ module Aspera
649
664
  end
650
665
  case command
651
666
  when :list
652
- resp = @api_node.read('async/list')[:data]['sync_ids']
667
+ resp = @api_node.read('async/list')['sync_ids']
653
668
  return { type: :value_list, data: resp, name: 'id' }
654
669
  when :show
655
- resp = @api_node.create('async/summary', post_data)[:data]['sync_summaries']
670
+ resp = @api_node.create('async/summary', post_data)['sync_summaries']
656
671
  return Main.result_empty if resp.empty?
657
672
  return { type: :object_list, data: resp, fields: %w[snid name local_dir remote_dir] } if async_id.eql?(SpecialValues::ALL)
658
673
  return { type: :single_object, data: resp.first }
659
674
  when :delete
660
- resp = @api_node.create('async/delete', post_data)[:data]
675
+ resp = @api_node.create('async/delete', post_data)
661
676
  return { type: :single_object, data: resp, name: 'id' }
662
677
  when :bandwidth
663
678
  post_data['seconds'] = 100 # TODO: as parameter with --value
664
- resp = @api_node.create('async/bandwidth', post_data)[:data]
679
+ resp = @api_node.create('async/bandwidth', post_data)
665
680
  data = resp['bandwidth_data']
666
681
  return Main.result_empty if data.empty?
667
682
  data = data.first[async_id]['data']
@@ -671,9 +686,9 @@ module Aspera
671
686
  # filename str
672
687
  # skip int
673
688
  # status int
674
- filter = query_option
689
+ filter = options.get_option(:query)
675
690
  post_data.merge!(filter) unless filter.nil?
676
- resp = @api_node.create('async/files', post_data)[:data]
691
+ resp = @api_node.create('async/files', post_data)
677
692
  data = resp['sync_files']
678
693
  data = data.first[async_id] unless data.empty?
679
694
  iteration_data = []
@@ -696,7 +711,7 @@ module Aspera
696
711
  skip_ids_persistency&.save
697
712
  return { type: :object_list, data: data, name: 'id' }
698
713
  when :counters
699
- resp = @api_node.create('async/counters', post_data)[:data]['sync_counters'].first[async_id].last
714
+ resp = @api_node.create('async/counters', post_data)['sync_counters'].first[async_id].last
700
715
  return Main.result_empty if resp.nil?
701
716
  return { type: :single_object, data: resp }
702
717
  end
@@ -708,8 +723,8 @@ module Aspera
708
723
  # @param [String] value value of the field to search
709
724
  def ssync_lookup(field, value)
710
725
  raise Cli::BadArgument, "Only search by name is supported (#{field})" unless field.eql?('name')
711
- @api_node.read('asyncs')[:data]['ids'].each do |id|
712
- sync_info = @api_node.read("asyncs/#{id}")[:data]['configuration']
726
+ @api_node.read('asyncs')['ids'].each do |id|
727
+ sync_info = @api_node.read("asyncs/#{id}")['configuration']
713
728
  # name is unique, so we can return
714
729
  return id if sync_info[field].eql?(value)
715
730
  end
@@ -750,27 +765,27 @@ module Aspera
750
765
  return Main.result_status('Done')
751
766
  end
752
767
  parameters = nil
753
- parameters = query_option(default: {}) if %i[bandwidth counters files].include?(sync_command)
754
- return { type: :single_object, data: @api_node.read("asyncs/#{asyncs_id}/#{sync_command}", parameters)[:data] }
768
+ parameters = options.get_option(:query, default: {}) if %i[bandwidth counters files].include?(sync_command)
769
+ return { type: :single_object, data: @api_node.read("asyncs/#{asyncs_id}/#{sync_command}", parameters) }
755
770
  end
756
771
  when :stream
757
772
  command = options.get_next_command(%i[list create show modify cancel])
758
773
  case command
759
774
  when :list
760
775
  resp = @api_node.read('ops/transfers', query_read_delete)
761
- return { type: :object_list, data: resp[:data], fields: %w[id status] } # TODO: useful?
776
+ return { type: :object_list, data: resp, fields: %w[id status] } # TODO: useful?
762
777
  when :create
763
778
  resp = @api_node.create('streams', value_create_modify(command: command))
764
- return { type: :single_object, data: resp[:data] }
779
+ return { type: :single_object, data: resp }
765
780
  when :show
766
781
  resp = @api_node.read("ops/transfers/#{options.get_next_argument('transfer id')}")
767
- return { type: :other_struct, data: resp[:data] }
782
+ return { type: :other_struct, data: resp }
768
783
  when :modify
769
784
  resp = @api_node.update("streams/#{options.get_next_argument('transfer id')}", value_create_modify(command: command))
770
- return { type: :other_struct, data: resp[:data] }
785
+ return { type: :other_struct, data: resp }
771
786
  when :cancel
772
787
  resp = @api_node.cancel("streams/#{options.get_next_argument('transfer id')}")
773
- return { type: :other_struct, data: resp[:data] }
788
+ return { type: :other_struct, data: resp }
774
789
  else
775
790
  raise 'error'
776
791
  end
@@ -783,14 +798,61 @@ module Aspera
783
798
  end
784
799
  case command
785
800
  when :list
786
- transfers_data = @api_node.read(res_class_path, query_read_delete)[:data]
801
+ transfer_filter = query_read_delete(default: {})
802
+ last_iteration_token = nil
803
+ iteration_persistency = nil
804
+ if options.get_option(:once_only, mandatory: true)
805
+ iteration_persistency = PersistencyActionOnce.new(
806
+ manager: persistency,
807
+ data: [],
808
+ id: IdGenerator.from_list([
809
+ 'node_transfers',
810
+ options.get_option(:url, mandatory: true),
811
+ options.get_option(:username, mandatory: true)
812
+ ]))
813
+ if transfer_filter.delete('reset')
814
+ iteration_persistency.data.clear
815
+ iteration_persistency.save
816
+ return Main.result_status('Persistency reset')
817
+ end
818
+ last_iteration_token = iteration_persistency.data.first
819
+ end
820
+ raise 'reset only with once_only' if transfer_filter.key?('reset') && iteration_persistency.nil?
821
+ max_items = transfer_filter.delete(MAX_ITEMS)
822
+ transfers_data = []
823
+ loop do
824
+ transfer_filter['iteration_token'] = last_iteration_token unless last_iteration_token.nil?
825
+ result = @api_node.call(operation: 'GET', subpath: res_class_path, query: transfer_filter)
826
+ # no data
827
+ break if result[:data].empty?
828
+ # get next iteration token from link
829
+ next_iteration_token = nil
830
+ link_info = result[:http]['Link']
831
+ unless link_info.nil?
832
+ m = link_info.match(/<([^>]+)>/)
833
+ raise "Cannot parse iteration in Link: #{link_info}" if m.nil?
834
+ next_iteration_token = CGI.parse(URI.parse(m[1]).query)['iteration_token']&.first
835
+ end
836
+ # same as last iteration: stop
837
+ break if next_iteration_token&.eql?(last_iteration_token)
838
+ last_iteration_token = next_iteration_token
839
+ transfers_data.concat(result[:data])
840
+ if max_items&.<=(transfers_data.length)
841
+ # if !max_items.nil? && (transfers_data.length >= max_items)
842
+ transfers_data = transfers_data.slice(0, max_items)
843
+ break
844
+ end
845
+ break if last_iteration_token.nil?
846
+ end
847
+ iteration_persistency&.data&.[]=(0, last_iteration_token)
848
+ iteration_persistency&.save
787
849
  return {
788
850
  type: :object_list,
789
851
  data: transfers_data,
790
852
  fields: %w[id status start_spec.direction start_spec.remote_user start_spec.remote_host start_spec.destination_path]
791
853
  }
792
854
  when :sessions
793
- transfers_data = @api_node.read(res_class_path, query_read_delete)[:data]
855
+ transfers_data = @api_node.read(res_class_path, query_read_delete)
794
856
  sessions = transfers_data.map{|t|t['sessions']}.flatten
795
857
  sessions.each do |session|
796
858
  session['start_time'] = Time.at(session['start_time_usec'] / 1_000_000.0).utc.iso8601(0)
@@ -803,15 +865,15 @@ module Aspera
803
865
  }
804
866
  when :cancel
805
867
  resp = @api_node.cancel(one_res_path)
806
- return { type: :other_struct, data: resp[:data] }
868
+ return { type: :other_struct, data: resp }
807
869
  when :show
808
870
  resp = @api_node.read(one_res_path)
809
- return { type: :other_struct, data: resp[:data] }
871
+ return { type: :other_struct, data: resp }
810
872
  when :modify
811
873
  resp = @api_node.update(one_res_path, options.get_next_argument('update value', validation: Hash))
812
- return { type: :other_struct, data: resp[:data] }
874
+ return { type: :other_struct, data: resp }
813
875
  when :bandwidth_average
814
- transfers_data = @api_node.read(res_class_path, query_read_delete)[:data]
876
+ transfers_data = @api_node.read(res_class_path, query_read_delete)
815
877
  # collect all key dates
816
878
  bandwidth_period = {}
817
879
  dir_info = %i[avg_kbps sessions].freeze
@@ -866,12 +928,12 @@ module Aspera
866
928
  case command
867
929
  when :list
868
930
  resp = @api_node.read('rund/services')
869
- return { type: :object_list, data: resp[:data]['services'] }
931
+ return { type: :object_list, data: resp['services'] }
870
932
  when :create
871
933
  # @json:'{"type":"WATCHFOLDERD","run_as":{"user":"user1"}}'
872
934
  params = options.get_next_argument('creation data', validation: Hash)
873
935
  resp = @api_node.create('rund/services', params)
874
- return Main.result_status("#{resp[:data]['id']} created")
936
+ return Main.result_status("#{resp['id']} created")
875
937
  when :delete
876
938
  @api_node.delete("rund/services/#{service_id}")
877
939
  return Main.result_status("#{service_id} deleted")
@@ -889,36 +951,37 @@ module Aspera
889
951
  case command
890
952
  when :create
891
953
  resp = @api_node.create(res_class_path, value_create_modify(command: command))
892
- return Main.result_status("#{resp[:data]['id']} created")
954
+ return Main.result_status("#{resp['id']} created")
893
955
  when :list
894
956
  resp = @api_node.read(res_class_path, query_read_delete)
895
- return { type: :value_list, data: resp[:data]['ids'], name: 'id' }
957
+ return { type: :value_list, data: resp['ids'], name: 'id' }
896
958
  when :show
897
- return { type: :single_object, data: @api_node.read(one_res_path)[:data]}
959
+ return { type: :single_object, data: @api_node.read(one_res_path)}
898
960
  when :modify
899
- @api_node.update(one_res_path, query_option(mandatory: true))
961
+ @api_node.update(one_res_path, options.get_option(:query, mandatory: true))
900
962
  return Main.result_status("#{one_res_id} updated")
901
963
  when :delete
902
964
  @api_node.delete(one_res_path)
903
965
  return Main.result_status("#{one_res_id} deleted")
904
966
  when :state
905
- return { type: :single_object, data: @api_node.read("#{one_res_path}/state")[:data] }
967
+ return { type: :single_object, data: @api_node.read("#{one_res_path}/state") }
906
968
  end
907
969
  when :central
908
970
  command = options.get_next_command(%i[session file])
909
971
  validator_id = options.get_option(:validator)
910
972
  validation = {'validator_id' => validator_id} unless validator_id.nil?
911
- request_data = query_option(default: {})
973
+ request_data = options.get_option(:query, default: {})
912
974
  case command
913
975
  when :session
914
976
  command = options.get_next_command([:list])
915
977
  case command
916
978
  when :list
979
+ request_data = options.get_next_argument('request data', mandatory: false, validation: Hash, default: {})
917
980
  request_data.deep_merge!({'validation' => validation}) unless validation.nil?
918
981
  resp = @api_node.create('services/rest/transfers/v1/sessions', request_data)
919
982
  return {
920
983
  type: :object_list,
921
- data: resp[:data]['session_info_result']['session_info'],
984
+ data: resp['session_info_result']['session_info'],
922
985
  fields: %w[session_uuid status transport direction bytes_transferred]
923
986
  }
924
987
  end
@@ -926,12 +989,14 @@ module Aspera
926
989
  command = options.get_next_command(%i[list modify])
927
990
  case command
928
991
  when :list
992
+ request_data = options.get_next_argument('request data', mandatory: false, validation: Hash, default: {})
929
993
  request_data.deep_merge!({'validation' => validation}) unless validation.nil?
930
- resp = @api_node.create('services/rest/transfers/v1/files', request_data)[:data]
994
+ resp = @api_node.create('services/rest/transfers/v1/files', request_data)
931
995
  resp = JSON.parse(resp) if resp.is_a?(String)
932
996
  Log.log.debug{Log.dump(:resp, resp)}
933
997
  return { type: :object_list, data: resp['file_transfer_info_result']['file_transfer_info'], fields: %w[session_uuid file_id status path]}
934
998
  when :modify
999
+ request_data = options.get_next_argument('request data', mandatory: false, validation: Hash, default: {})
935
1000
  request_data.deep_merge!(validation) unless validation.nil?
936
1001
  @api_node.update('services/rest/transfers/v1/files', request_data)
937
1002
  return Main.result_status('updated')
@@ -961,7 +1026,7 @@ module Aspera
961
1026
  raise 'Missing key: url' unless parameters.key?(:url)
962
1027
  uri = URI.parse(parameters[:url])
963
1028
  server = WebServerSimple.new(uri, certificate: parameters[:certificate])
964
- server.mount(uri.path, NodeSimulatorServlet, parameters[:credentials], transfer)
1029
+ server.mount(uri.path, NodeSimulatorServlet, parameters[:credentials], NodeSimulator.new)
965
1030
  server.start
966
1031
  return Main.result_status('Simulator terminated')
967
1032
  end