aspera-cli 4.25.3 → 4.25.5

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 (45) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +40 -6
  4. data/CONTRIBUTING.md +122 -111
  5. data/README.md +9 -7
  6. data/lib/aspera/agent/direct.rb +10 -8
  7. data/lib/aspera/agent/factory.rb +3 -3
  8. data/lib/aspera/agent/node.rb +1 -1
  9. data/lib/aspera/api/alee.rb +1 -0
  10. data/lib/aspera/api/aoc.rb +15 -14
  11. data/lib/aspera/api/ats.rb +1 -1
  12. data/lib/aspera/api/cos_node.rb +5 -0
  13. data/lib/aspera/api/faspex.rb +27 -20
  14. data/lib/aspera/api/httpgw.rb +19 -3
  15. data/lib/aspera/api/node.rb +122 -29
  16. data/lib/aspera/ascp/installation.rb +9 -10
  17. data/lib/aspera/cli/error.rb +8 -0
  18. data/lib/aspera/cli/formatter.rb +27 -11
  19. data/lib/aspera/cli/info.rb +2 -1
  20. data/lib/aspera/cli/main.rb +30 -12
  21. data/lib/aspera/cli/manager.rb +43 -31
  22. data/lib/aspera/cli/plugins/aoc.rb +7 -5
  23. data/lib/aspera/cli/plugins/base.rb +1 -88
  24. data/lib/aspera/cli/plugins/config.rb +2 -1
  25. data/lib/aspera/cli/plugins/faspex.rb +6 -6
  26. data/lib/aspera/cli/plugins/faspex5.rb +64 -64
  27. data/lib/aspera/cli/plugins/node.rb +33 -78
  28. data/lib/aspera/cli/plugins/shares.rb +4 -2
  29. data/lib/aspera/cli/special_values.rb +1 -0
  30. data/lib/aspera/cli/transfer_agent.rb +3 -0
  31. data/lib/aspera/cli/version.rb +1 -1
  32. data/lib/aspera/cli/wizard.rb +2 -1
  33. data/lib/aspera/dot_container.rb +10 -10
  34. data/lib/aspera/log.rb +1 -1
  35. data/lib/aspera/markdown.rb +1 -1
  36. data/lib/aspera/persistency_folder.rb +1 -1
  37. data/lib/aspera/rest.rb +39 -54
  38. data/lib/aspera/rest_list.rb +121 -0
  39. data/lib/aspera/sync/operations.rb +1 -1
  40. data/lib/aspera/transfer/parameters.rb +8 -8
  41. data/lib/aspera/transfer/spec.rb +1 -0
  42. data/lib/aspera/yaml.rb +1 -1
  43. data.tar.gz.sig +0 -0
  44. metadata +4 -3
  45. metadata.gz.sig +0 -0
@@ -100,24 +100,18 @@ module Aspera
100
100
  options.parse_options!
101
101
  end
102
102
 
103
- def set_api
104
- # create an API object with the same options, but with a different subpath
105
- @api_v5 = Api::Faspex.new(**Oauth.args_from_options(options))
106
- # in case user wants to use HTTPGW tell transfer agent how to get address
107
- transfer.httpgw_url_cb = lambda{@api_v5.read('account')['gateway_url']}
108
- end
109
-
110
103
  # if recipient is just an email, then convert to expected API hash : name and type
111
- def normalize_recipients(parameters)
112
- return unless parameters.key?('recipients')
113
- Aspera.assert_type(parameters['recipients'], Array){'recipients'}
104
+ def normalize_recipients(parameters, type)
105
+ type = type.to_s
106
+ return unless parameters.key?(type)
107
+ Aspera.assert_type(parameters[type], Array){type}
114
108
  recipient_types = Api::Faspex::RECIPIENT_TYPES
115
109
  if parameters.key?('recipient_types')
116
110
  recipient_types = parameters['recipient_types']
117
111
  parameters.delete('recipient_types')
118
112
  recipient_types = [recipient_types] unless recipient_types.is_a?(Array)
119
113
  end
120
- parameters['recipients'].map! do |recipient_data|
114
+ parameters[type].map! do |recipient_data|
121
115
  # If just a string, make a general lookup and build expected name/type hash
122
116
  if recipient_data.is_a?(String)
123
117
  matched = @api_v5.lookup_by_name('contacts', recipient_data, query: Rest.php_style({context: 'packages', type: recipient_types}))
@@ -157,17 +151,17 @@ module Aspera
157
151
  end
158
152
  end
159
153
 
160
- # @param [Srting] job identifier
154
+ # @param [String] job identifier
161
155
  # @return [Hash] result of API call for job status
162
156
  def wait_for_job(job_id)
163
157
  result = nil
164
158
  loop do
165
159
  result = @api_v5.read("jobs/#{job_id}", {type: :formatted})
166
160
  break unless Api::Faspex::JOB_RUNNING.include?(result['status'])
167
- formatter.long_operation_running(result['status'])
161
+ RestParameters.instance.spinner_cb.call(result['status'])
168
162
  sleep(0.5)
169
163
  end
170
- formatter.long_operation_terminated
164
+ RestParameters.instance.spinner_cb.call(action: :success)
171
165
  return result
172
166
  end
173
167
 
@@ -182,16 +176,22 @@ module Aspera
182
176
  when *Api::Faspex::API_LIST_MAILBOX_TYPES then "#{box}/packages"
183
177
  else
184
178
  group_type = options.get_option(:group_type)
185
- "#{group_type}/#{lookup_entity_by_field(api: @api_v5, entity: group_type, value: box)['id']}/packages"
179
+ "#{group_type}/#{@api_v5.lookup_entity_by_field(entity: group_type, value: box)['id']}/packages"
186
180
  end
187
- list, total = list_entities_limit_offset_total_count(
188
- api: @api_v5,
189
- entity: entity,
190
- query: query_read_delete(default: query)
191
- )
181
+ list, total = @api_v5.list_entities_limit_offset_total_count(entity: entity, query: query_read_delete(default: query))
192
182
  return list.select(&filter), total
193
183
  end
194
184
 
185
+ # Build query to get package recipients based on package info in case of shared inbox or workgroup recipient
186
+ # @param package_id [String] the package id to get info from
187
+ def recipient_query(package_id)
188
+ package_info = @api_v5.read("packages/#{package_id}")
189
+ base_query = {}
190
+ base_query['recipient_workgroup_id'] = package_info['recipients'].first['id'] if WORKGROUP_TYPES.include?(package_info['recipients'].first['recipient_type'])
191
+ base_query['recipient_user_id'] = package_info['recipients'].first['id'] if package_info['recipients'].first['recipient_type'].eql?('user')
192
+ base_query
193
+ end
194
+
195
195
  def package_receive(package_ids)
196
196
  # prepare persistency if needed
197
197
  skip_ids_persistency = nil
@@ -241,7 +241,7 @@ module Aspera
241
241
  type: Api::Faspex.box_type(box),
242
242
  transfer_type: Api::Faspex::TRANSFER_CONNECT
243
243
  }
244
- download_params[:recipient_workgroup_id] = lookup_entity_by_field(api: @api_v5, entity: options.get_option(:group_type), value: box)['id'] if !Api::Faspex::API_LIST_MAILBOX_TYPES.include?(box) && box != SpecialValues::ALL
244
+ # download_params[:recipient_workgroup_id] = @api_v5.lookup_entity_by_field(entity: options.get_option(:group_type), value: box)['id'] if !Api::Faspex::API_LIST_MAILBOX_TYPES.include?(box) && box != SpecialValues::ALL
245
245
  packages.each do |package|
246
246
  pkg_id = package['id']
247
247
  formatter.display_status("Receiving package #{pkg_id}")
@@ -249,7 +249,7 @@ module Aspera
249
249
  transfer_spec = @api_v5.call(
250
250
  operation: 'POST',
251
251
  subpath: "packages/#{pkg_id}/transfer_spec/download",
252
- query: download_params,
252
+ query: download_params.merge(recipient_query(pkg_id)),
253
253
  content_type: Mime::JSON,
254
254
  body: param_file_list,
255
255
  headers: {'Accept' => Mime::JSON}
@@ -276,7 +276,7 @@ module Aspera
276
276
  recipient_type: @api_v5.pub_link_context['recipient_type']
277
277
  }]
278
278
  end
279
- normalize_recipients(parameters)
279
+ PACKAGE_RECIPIENT_TYPES.each{ |type| normalize_recipients(parameters, type)}
280
280
  # User specified content prot in tspec, but faspex requires in package creation
281
281
  # `transfer_spec/upload` will set `content_protection`
282
282
  if transfer.user_transfer_spec['content_protection'] && !parameters.key?('ear_enabled')
@@ -298,8 +298,7 @@ module Aspera
298
298
  else
299
299
  # send from remote shared folder
300
300
  if (m = Base.percent_selector(shared_folder))
301
- shared_folder = lookup_entity_by_field(
302
- api: @api_v5,
301
+ shared_folder = @api_v5.lookup_entity_by_field(
303
302
  entity: 'shared_folders',
304
303
  field: m[:field],
305
304
  value: m[:value]
@@ -319,14 +318,14 @@ module Aspera
319
318
 
320
319
  # Browse a folder
321
320
  # @param browse_endpoint [String] the endpoint to browse
322
- def browse_folder(browse_endpoint)
321
+ def browse_folder(browse_endpoint, base_query = {})
323
322
  folders_to_process = [options.get_next_argument('folder path', default: '/')]
324
- query = query_read_delete(default: {})
323
+ query = base_query.merge(query_read_delete(default: {}))
325
324
  filters = query.delete('filters'){{}}
326
325
  Aspera.assert_type(filters, Hash)
327
326
  filters['basenames'] ||= []
328
327
  Aspera.assert_type(filters, Hash){'filters'}
329
- max_items = query.delete(MAX_ITEMS)
328
+ max_items = query.delete(RestList::MAX_ITEMS)
330
329
  recursive = query.delete('recursive')
331
330
  use_paging = query.delete('paging'){true}
332
331
  if use_paging
@@ -357,7 +356,7 @@ module Aspera
357
356
  end
358
357
  folders_to_process.concat(data['items'].select{ |i| i['type'].eql?('directory')}.map{ |i| i['path']}) if recursive
359
358
  if use_paging
360
- iteration_token = http[Api::Faspex::HEADER_ITERATION_TOKEN]
359
+ iteration_token = http[Api::Faspex::HEADER_X_NEXT_ITER_TOKEN]
361
360
  break if iteration_token.nil? || iteration_token.empty?
362
361
  query['iteration_token'] = iteration_token
363
362
  else
@@ -365,12 +364,11 @@ module Aspera
365
364
  break if data['item_count'].eql?(0)
366
365
  query['offset'] += data['item_count']
367
366
  end
368
- formatter.long_operation_running(all_items.count)
367
+ RestParameters.instance.spinner_cb.call(all_items.count)
369
368
  end
370
369
  query.delete('iteration_token')
371
370
  end
372
- formatter.long_operation_terminated
373
-
371
+ RestParameters.instance.spinner_cb.call(action: :success)
374
372
  return Main.result_object_list(all_items, total: total_count)
375
373
  end
376
374
 
@@ -384,7 +382,7 @@ module Aspera
384
382
  when :show
385
383
  return Main.result_single_object(@api_v5.read("packages/#{package_id}"))
386
384
  when :browse
387
- return browse_folder("packages/#{package_id}/files/#{Api::Faspex.box_type(options.get_option(:box))}")
385
+ return browse_folder("packages/#{package_id}/files/#{Api::Faspex.box_type(options.get_option(:box))}", recipient_query(package_id))
388
386
  when :status
389
387
  status_list = options.get_next_argument('list of states, or nothing', mandatory: false, validation: Array)
390
388
  status = wait_package_status(package_id, status_list: status_list)
@@ -418,8 +416,7 @@ module Aspera
418
416
  def execute_resource(res_sym)
419
417
  exec_args = {
420
418
  api: @api_v5,
421
- entity: res_sym.to_s,
422
- tclo: true
419
+ entity: res_sym.to_s
423
420
  }
424
421
  res_id_query = :default
425
422
  available_commands = ALL_OPS
@@ -440,25 +437,30 @@ module Aspera
440
437
  available_commands += [:reset_password]
441
438
  when :oauth_clients
442
439
  exec_args[:display_fields] = Formatter.all_but('public_key')
443
- exec_args[:api] = @api_v5.auth_api
440
+ exec_args[:api] = Api::Faspex.new(root: Api::Faspex::PATH_AUTH, **Oauth.args_from_options(options))
444
441
  exec_args[:list_query] = {'expand': true, 'no_api_path': true, 'client_types[]': 'public'}
445
442
  when :shared_inboxes, :workgroups
446
443
  available_commands += %i[members saml_groups invite_external_collaborator]
447
444
  res_id_query = {'all': true}
448
445
  when :nodes
449
446
  available_commands += %i[shared_folders browse]
447
+ when :jobs
448
+ exec_args[:display_fields] = %w[id job_name job_type status]
450
449
  end
451
450
  res_command = options.get_next_command(available_commands)
452
451
  return Main.result_value_list(Api::Faspex::EMAIL_NOTIF_LIST, name: 'email_id') if res_command.eql?(:list) && res_sym.eql?(:email_notifications)
453
452
  case res_command
454
- when *ALL_OPS
453
+ when :create, :modify, :delete, :show
455
454
  return entity_execute(command: res_command, **exec_args) do |field, value|
456
- lookup_entity_by_field(api: @api_v5, entity: exec_args[:entity], value: value, field: field, items_key: exec_args[:items_key], query: res_id_query)['id']
455
+ @api_v5.lookup_entity_by_field(entity: exec_args[:entity], value: value, field: field, items_key: exec_args[:items_key], query: res_id_query)['id']
457
456
  end
457
+ when :list
458
+ data, total = exec_args[:api].list_entities_limit_offset_total_count(entity: exec_args[:entity], items_key: exec_args[:items_key], query: query_read_delete(default: exec_args[:list_query]))
459
+ return Main.result_object_list(data, total: total, fields: exec_args[:display_fields])
458
460
  when :shared_folders
459
461
  # nodes
460
462
  node_id = instance_identifier do |field, value|
461
- lookup_entity_by_field(api: @api_v5, entity: 'nodes', field: field, value: value)['id']
463
+ @api_v5.lookup_entity_by_field(entity: 'nodes', field: field, value: value)['id']
462
464
  end
463
465
  shfld_entity = "nodes/#{node_id}/shared_folders"
464
466
  sh_command = options.get_next_command(ALL_OPS + [:user])
@@ -469,33 +471,32 @@ module Aspera
469
471
  entity: shfld_entity,
470
472
  command: sh_command
471
473
  ) do |field, value|
472
- lookup_entity_by_field(api: @api_v5, entity: shfld_entity, field: field, value: value)['id']
474
+ @api_v5.lookup_entity_by_field(entity: shfld_entity, field: field, value: value)['id']
473
475
  end
474
476
  when :user
475
477
  sh_id = instance_identifier do |field, value|
476
- lookup_entity_by_field(api: @api_v5, entity: shfld_entity, field: field, value: value)['id']
478
+ @api_v5.lookup_entity_by_field(entity: shfld_entity, field: field, value: value)['id']
477
479
  end
478
480
  user_path = "#{shfld_entity}/#{sh_id}/custom_access_users"
479
481
  return entity_execute(api: @api_v5, entity: user_path, items_key: 'users') do |field, value|
480
- lookup_entity_by_field(api: @api_v5, entity: user_path, items_key: 'users', field: field, value: value)['id']
482
+ @api_v5.lookup_entity_by_field(entity: user_path, items_key: 'users', field: field, value: value)['id']
481
483
  end
482
484
 
483
485
  end
484
486
  when :browse
485
487
  # nodes
486
488
  node_id = instance_identifier do |field, value|
487
- lookup_entity_by_field(api: @api_v5, entity: 'nodes', value: value, field: field)['id']
489
+ @api_v5.lookup_entity_by_field(entity: 'nodes', value: value, field: field)['id']
488
490
  end
489
491
  return browse_folder("nodes/#{node_id}/browse")
490
492
  when :invite_external_collaborator
491
493
  # :shared_inboxes, :workgroups
492
- shared_inbox_id = instance_identifier{ |field, value| lookup_entity_by_field(api: @api_v5, entity: res_sym.to_s, field: field, value: value, query: res_id_query)['id']}
494
+ shared_inbox_id = instance_identifier{ |field, value| @api_v5.lookup_entity_by_field(entity: res_sym.to_s, field: field, value: value, query: res_id_query)['id']}
493
495
  creation_payload = value_create_modify(command: res_command, type: [Hash, String])
494
496
  creation_payload = {'email_address' => creation_payload} if creation_payload.is_a?(String)
495
497
  result = @api_v5.create("#{res_sym}/#{shared_inbox_id}/external_collaborator", creation_payload)
496
498
  formatter.display_status(result['message'])
497
- result = lookup_entity_by_field(
498
- api: @api_v5,
499
+ result = @api_v5.lookup_entity_by_field(
499
500
  entity: "#{res_sym}/#{shared_inbox_id}/members",
500
501
  items_key: 'members',
501
502
  value: creation_payload['email_address'],
@@ -504,7 +505,7 @@ module Aspera
504
505
  return Main.result_single_object(result)
505
506
  when :members, :saml_groups
506
507
  # res_command := :shared_inboxes, :workgroups
507
- res_id = instance_identifier{ |field, value| lookup_entity_by_field(api: @api_v5, entity: res_sym.to_s, field: field, value: value, query: res_id_query)['id']}
508
+ res_id = instance_identifier{ |field, value| @api_v5.lookup_entity_by_field(entity: res_sym.to_s, field: field, value: value, query: res_id_query)['id']}
508
509
  res_path = "#{res_sym}/#{res_id}/#{res_command}"
509
510
  list_key = res_command.to_s
510
511
  list_key = 'groups' if res_command.eql?(:saml_groups)
@@ -515,8 +516,7 @@ module Aspera
515
516
  users = [users] unless users.is_a?(Array)
516
517
  users = users.map do |user|
517
518
  if (m = Base.percent_selector(user))
518
- lookup_entity_by_field(
519
- api: @api_v5,
519
+ @api_v5.lookup_entity_by_field(
520
520
  entity: 'accounts',
521
521
  field: m[:field],
522
522
  value: m[:value],
@@ -537,8 +537,7 @@ module Aspera
537
537
  command: sub_command,
538
538
  items_key: list_key
539
539
  ) do |field, value|
540
- lookup_entity_by_field(
541
- api: @api_v5,
540
+ @api_v5.lookup_entity_by_field(
542
541
  entity: res_path,
543
542
  field: field,
544
543
  value: value,
@@ -547,7 +546,7 @@ module Aspera
547
546
  end
548
547
  when :reset_password
549
548
  # :accounts
550
- contact_id = instance_identifier{ |field, value| lookup_entity_by_field(api: @api_v5, entity: 'accounts', field: field, value: value, query: res_id_query)['id']}
549
+ contact_id = instance_identifier{ |field, value| @api_v5.lookup_entity_by_field(entity: 'accounts', field: field, value: value, query: res_id_query)['id']}
551
550
  @api_v5.create("accounts/#{contact_id}/reset_password", {})
552
551
  return Main.result_status('password reset, user shall check email')
553
552
  end
@@ -568,16 +567,10 @@ module Aspera
568
567
  event_type = options.get_next_command(%i[application webhook])
569
568
  case event_type
570
569
  when :application
571
- list, total = list_entities_limit_offset_total_count(
572
- api: @api_v5,
573
- entity: 'application_events',
574
- query: query_read_delete
575
- )
576
-
570
+ list, total = @api_v5.list_entities_limit_offset_total_count(entity: 'application_events', query: query_read_delete)
577
571
  return Main.result_object_list(list, total: total, fields: %w[event_type created_at application user.name])
578
572
  when :webhook
579
- list, total = list_entities_limit_offset_total_count(
580
- api: @api_v5,
573
+ list, total = @api_v5.list_entities_limit_offset_total_count(
581
574
  entity: 'all_webhooks_events',
582
575
  query: query_read_delete,
583
576
  items_key: 'events'
@@ -622,7 +615,12 @@ module Aspera
622
615
 
623
616
  def execute_action
624
617
  command = options.get_next_command(ACTIONS)
625
- set_api unless %i{postprocessing health}.include?(command)
618
+ unless %i{postprocessing health}.include?(command)
619
+ # create an API object with the same options, but with a different subpath
620
+ @api_v5 = Api::Faspex.new(**Oauth.args_from_options(options))
621
+ # in case user wants to use HTTPGW tell transfer agent how to get address
622
+ transfer.httpgw_url_cb = lambda{@api_v5.read('account')['gateway_url']}
623
+ end
626
624
  case command
627
625
  when :version
628
626
  return Main.result_single_object(@api_v5.read('version'))
@@ -694,7 +692,7 @@ module Aspera
694
692
  items_key: invitation_endpoint,
695
693
  display_fields: %w[id public recipient_type recipient_name email_address]
696
694
  ) do |field, value|
697
- lookup_entity_by_field(api: @api_v5, entity: invitation_endpoint, field: field, value: value, query: {})['id']
695
+ @api_v5.lookup_entity_by_field(entity: invitation_endpoint, field: field, value: value, query: {})['id']
698
696
  end
699
697
  end
700
698
  when :gateway
@@ -719,8 +717,10 @@ module Aspera
719
717
  end
720
718
  SHARED_INBOX_MEMBER_LEVELS = %i[submit_only standard shared_inbox_admin].freeze
721
719
  ACCOUNT_TYPES = %w{local_user saml_user self_registered_user external_user}.freeze
722
- CONTACT_TYPES = %w{workgroup shared_inbox distribution_list user external_user}.freeze
723
- private_constant :SHARED_INBOX_MEMBER_LEVELS, :ACCOUNT_TYPES, :CONTACT_TYPES
720
+ WORKGROUP_TYPES = %w{workgroup shared_inbox}.freeze
721
+ CONTACT_TYPES = (WORKGROUP_TYPES + %w{distribution_list user external_user}).freeze
722
+ PACKAGE_RECIPIENT_TYPES = %i{recipients private_recipients notified_on_upload notified_on_download notified_on_receipt}
723
+ private_constant :SHARED_INBOX_MEMBER_LEVELS, :ACCOUNT_TYPES, :CONTACT_TYPES, :PACKAGE_RECIPIENT_TYPES
724
724
  end
725
725
  end
726
726
  end
@@ -11,6 +11,7 @@ require 'aspera/id_generator'
11
11
  require 'aspera/api/node'
12
12
  require 'aspera/oauth'
13
13
  require 'aspera/node_simulator'
14
+ require 'aspera/rest_list'
14
15
  require 'aspera/assert'
15
16
  require 'base64'
16
17
  require 'zlib'
@@ -93,12 +94,9 @@ module Aspera
93
94
  options.declare(:validator, 'Identifier of validator (optional for central)')
94
95
  options.declare(:asperabrowserurl, 'URL for simple aspera web ui', default: 'https://asperabrowser.mybluemix.net')
95
96
  options.declare(
96
- :default_ports, 'Gen4: Use standard FASP ports (true) or get from node API (false)', allowed: Allowed::TYPES_BOOLEAN, default: true,
97
- handler: {o: Api::Node, m: :use_standard_ports}
98
- )
99
- options.declare(
100
- :node_cache, 'Gen4: Set to no to force actual file system read', allowed: Allowed::TYPES_BOOLEAN,
101
- handler: {o: Api::Node, m: :use_node_cache}
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}
102
100
  )
103
101
  options.declare(:root_id, 'Gen4: File id of top folder when using access key (override AK root id)')
104
102
  options.declare(:dynamic_key, 'Private key PEM to use for dynamic key auth', handler: {o: Api::Node, m: :use_dynamic_key})
@@ -169,7 +167,7 @@ module Aspera
169
167
  # @param api [Rest] an existing API object for the Node API
170
168
  # @param prefix_path [String,nil] for Faspex 4, allows browsing a package without full path in node (removes storage prefix)
171
169
  def initialize(context:, api: nil, prefix_path: nil)
172
- @prefixer = prefix_path ? NodePathPrefix.new(prefix_path) : nil
170
+ @node_path_prefix = prefix_path ? NodePathPrefix.new(prefix_path) : nil
173
171
  super(context: context, basic_options: api.nil?)
174
172
  Node.declare_options(options)
175
173
  return if context.only_manual?
@@ -199,11 +197,11 @@ module Aspera
199
197
  # Gen3 API
200
198
  def browse_gen3
201
199
  folders_to_process = options.get_next_argument('path', validation: String)
202
- folders_to_process = @prefixer.add_to_path(folders_to_process) unless @prefixer.nil?
200
+ folders_to_process = @node_path_prefix.add_to_path(folders_to_process) unless @node_path_prefix.nil?
203
201
  folders_to_process = [folders_to_process]
204
202
  query = options.get_option(:query) || {}
205
203
  # special parameter: max number of entries in result
206
- max_items = query.delete(MAX_ITEMS)
204
+ max_items = query.delete(RestList::MAX_ITEMS)
207
205
  # special parameter: recursive browsing
208
206
  recursive = query.delete('recursive')
209
207
  # special parameter: only return one entry for the path, even if folder
@@ -224,7 +222,7 @@ module Aspera
224
222
  response = @api_node.create('files/browse', query)
225
223
  # 'file','symbolic_link'
226
224
  if !Node.gen3_entry_folder?(response['self']) || only_path
227
- @prefixer&.remove_in_object_list!([response['self']])
225
+ @node_path_prefix&.remove_in_object_list!([response['self']])
228
226
  return Main.result_single_object(response['self'])
229
227
  end
230
228
  items = response['items']
@@ -242,14 +240,14 @@ module Aspera
242
240
  break if all_items.count >= total_count
243
241
  offset += items.count
244
242
  query['skip'] = offset
245
- formatter.long_operation_running(all_items.count)
243
+ RestParameters.instance.spinner_cb.call(all_items.count)
246
244
  end
247
245
  query.delete('skip')
248
246
  end
249
- @prefixer&.remove_in_object_list!(all_items)
247
+ @node_path_prefix&.remove_in_object_list!(all_items)
250
248
  return Main.result_object_list(all_items)
251
249
  ensure
252
- formatter.long_operation_terminated
250
+ RestParameters.instance.spinner_cb.call(action: :success)
253
251
  end
254
252
 
255
253
  # Create async transfer spec request from direction and folders
@@ -289,12 +287,12 @@ module Aspera
289
287
  when :delete
290
288
  # TODO: add query for recursive
291
289
  paths_to_delete = options.get_next_argument('file list', multiple: true)
292
- @prefixer&.add_to_paths!(paths_to_delete)
290
+ @node_path_prefix&.add_to_paths!(paths_to_delete)
293
291
  resp = @api_node.create('files/delete', {paths: paths_to_delete.map{ |i| {'path' => i.start_with?('/') ? i : "/#{i}"}}})
294
292
  return cli_result_from_paths_response(resp, 'file deleted')
295
293
  when :search
296
294
  search_root = options.get_next_argument('search root', validation: String)
297
- search_root = @prefixer.add_to_path(search_root) unless @prefixer.nil?
295
+ search_root = @node_path_prefix.add_to_path(search_root) unless @node_path_prefix.nil?
298
296
  parameters = {'path' => search_root}
299
297
  other_options = options.get_option(:query)
300
298
  parameters.merge!(other_options) unless other_options.nil?
@@ -303,40 +301,40 @@ module Aspera
303
301
  fields = resp['items'].first.keys.reject{ |i| SEARCH_REMOVE_FIELDS.include?(i)}
304
302
  formatter.display_item_count(resp['item_count'], resp['total_count'])
305
303
  formatter.display_status("params: #{resp['parameters'].keys.map{ |k| "#{k}:#{resp['parameters'][k]}"}.join(',')}")
306
- @prefixer&.remove_in_object_list!(resp['items'])
304
+ @node_path_prefix&.remove_in_object_list!(resp['items'])
307
305
  return Main.result_object_list(resp['items'], fields: fields)
308
306
  when :space
309
307
  path_list = options.get_next_argument('folder path or ext.val. list', multiple: true)
310
- @prefixer&.add_to_paths!(path_list)
308
+ @node_path_prefix&.add_to_paths!(path_list)
311
309
  resp = @api_node.create('space', {'paths' => path_list.map{ |i| {path: i}}})
312
- @prefixer&.remove_in_object_list!(resp['paths'])
310
+ @node_path_prefix&.remove_in_object_list!(resp['paths'])
313
311
  return Main.result_object_list(resp['paths'])
314
312
  when :mkdir
315
313
  path_list = options.get_next_argument('folder path or ext.val. list', multiple: true)
316
- @prefixer&.add_to_paths!(path_list)
314
+ @node_path_prefix&.add_to_paths!(path_list)
317
315
  resp = @api_node.create('files/create', {'paths' => path_list.map{ |i| {type: :directory, path: i}}})
318
316
  return cli_result_from_paths_response(resp, 'folder created')
319
317
  when :mklink
320
318
  target = options.get_next_argument('target', validation: String)
321
- target = @prefixer.add_to_path(target) unless @prefixer.nil?
319
+ target = @node_path_prefix.add_to_path(target) unless @node_path_prefix.nil?
322
320
  one_path = options.get_next_argument('link path', validation: String)
323
- one_path = @prefixer.add_to_path(one_path) unless @prefixer.nil?
321
+ one_path = @node_path_prefix.add_to_path(one_path) unless @node_path_prefix.nil?
324
322
  resp = @api_node.create('files/create', {'paths' => [{type: :symbolic_link, path: one_path, target: {path: target}}]})
325
323
  return cli_result_from_paths_response(resp, 'link created')
326
324
  when :mkfile
327
325
  one_path = options.get_next_argument('file path', validation: String)
328
- one_path = @prefixer.add_to_path(one_path) unless @prefixer.nil?
326
+ one_path = @node_path_prefix.add_to_path(one_path) unless @node_path_prefix.nil?
329
327
  contents64 = Base64.strict_encode64(options.get_next_argument('contents'))
330
328
  resp = @api_node.create('files/create', {'paths' => [{type: :file, path: one_path, contents: contents64}]})
331
329
  return cli_result_from_paths_response(resp, 'file created')
332
330
  when :rename
333
331
  # TODO: multiple ?
334
332
  path_base = options.get_next_argument('path_base', validation: String)
335
- path_base = @prefixer.add_to_path(path_base) unless @prefixer.nil?
333
+ path_base = @node_path_prefix.add_to_path(path_base) unless @node_path_prefix.nil?
336
334
  path_src = options.get_next_argument('path_src', validation: String)
337
- path_src = @prefixer.add_to_path(path_src) unless @prefixer.nil?
335
+ path_src = @node_path_prefix.add_to_path(path_src) unless @node_path_prefix.nil?
338
336
  path_dst = options.get_next_argument('path_dst', validation: String)
339
- path_dst = @prefixer.add_to_path(path_dst) unless @prefixer.nil?
337
+ path_dst = @node_path_prefix.add_to_path(path_dst) unless @node_path_prefix.nil?
340
338
  resp = @api_node.create('files/rename', {'paths' => [{'path' => path_base, 'source' => path_src, 'destination' => path_dst}]})
341
339
  return cli_result_from_paths_response(resp, 'entry moved')
342
340
  when :browse
@@ -381,14 +379,14 @@ module Aspera
381
379
  return Main.result_transfer(transfer.start(transfer_spec))
382
380
  when :cat
383
381
  remote_path = options.get_next_argument('remote path', validation: String)
384
- remote_path = @prefixer.add_to_path(remote_path) unless @prefixer.nil?
382
+ remote_path = @node_path_prefix.add_to_path(remote_path) unless @node_path_prefix.nil?
385
383
  File.basename(remote_path)
386
384
  http = @api_node.read("files/#{URI.encode_www_form_component(remote_path)}/contents", ret: :resp)
387
385
  return Main.result_text(http.body)
388
386
  when :transport
389
387
  return Main.result_single_object(@api_node.transport_params)
390
388
  when :spec
391
- return Main.result_single_object(@api_node.base_spec)
389
+ return Main.result_single_object(@api_node.base_spec, fields: Formatter.all_but(Transfer::Spec::SPECIFIC))
392
390
  end
393
391
  Aspera.error_unreachable_line
394
392
  end
@@ -523,7 +521,7 @@ module Aspera
523
521
  return Main.result_text(result[:password])
524
522
  when :browse
525
523
  apifid = apifid_from_next_arg(top_file_id)
526
- file_info = apifid[:api].read("files/#{apifid[:file_id]}", **Api::Node.cache_control)
524
+ file_info = apifid[:api].read("files/#{apifid[:file_id]}", headers: Api::Node.add_cache_control)
527
525
  unless file_info['type'].eql?('folder')
528
526
  # a single file
529
527
  return Main.result_object_list([file_info], fields: GEN4_LS_FIELDS)
@@ -885,10 +883,10 @@ module Aspera
885
883
  iteration_persistency.save
886
884
  return Main.result_status('Persistency reset')
887
885
  end
886
+ else
887
+ Aspera.assert(!transfer_filter.key?('reset'), type: Cli::BadArgument){'reset only with once_only'}
888
888
  end
889
- raise Cli::BadArgument, 'reset only with once_only' if transfer_filter.key?('reset') && iteration_persistency.nil?
890
- max_items = transfer_filter.delete(MAX_ITEMS)
891
- transfers_data = call_with_iteration(api: @api_node, operation: 'GET', subpath: 'ops/transfers', max: max_items, query: transfer_filter, iteration: iteration_persistency&.data)
889
+ transfers_data = @api_node.read_with_paging('ops/transfers', transfer_filter, iteration: iteration_persistency&.data)
892
890
  iteration_persistency&.save
893
891
  return Main.result_object_list(transfers_data, fields: %w[id status start_spec.direction start_spec.remote_user start_spec.remote_host start_spec.destination_path])
894
892
  when :sessions
@@ -931,10 +929,8 @@ module Aspera
931
929
  # do not process last one
932
930
  break if end_date.nil?
933
931
  # init data for this period
934
- period_bandwidth = Transfer::Spec::DIRECTION_ENUM_VALUES.map(&:to_sym).each_with_object({}) do |direction, h|
935
- h[direction] = dir_info.each_with_object({}) do |k2, h2|
936
- h2[k2] = 0
937
- end
932
+ period_bandwidth = Transfer::Spec::DIRECTION_ENUM_VALUES.map(&:to_sym).to_h do |direction|
933
+ [direction, dir_info.to_h{ |k2| [k2, 0]}]
938
934
  end
939
935
  # find all transfers that were active at this time
940
936
  transfers_data.each do |transfer|
@@ -1097,7 +1093,7 @@ module Aspera
1097
1093
  }
1098
1094
  loop do
1099
1095
  timestamp = Time.now
1100
- transfers_data = call_with_iteration(api: @api_node, operation: 'GET', subpath: 'ops/transfers', query: {active_only: true})
1096
+ transfers_data = @api_node.read_with_paging('ops/transfers', {active_only: true})
1101
1097
  datapoint[:asInt] = transfers_data.length
1102
1098
  datapoint[:timeUnixNano] = timestamp.to_i * 1_000_000_000 + timestamp.nsec
1103
1099
  Log.log.info("#{datapoint[:asInt]} active transfers")
@@ -1135,50 +1131,9 @@ module Aspera
1135
1131
  # Translates paths results into CLI result, and removes prefix
1136
1132
  def cli_result_from_paths_response(response, success_msg)
1137
1133
  obj_list = response_to_result(response, success_msg)
1138
- @prefixer&.remove_in_object_list!(obj_list)
1134
+ @node_path_prefix&.remove_in_object_list!(obj_list)
1139
1135
  return Main.result_object_list(obj_list, fields: %w[path result])
1140
1136
  end
1141
-
1142
- # Executes the provided API call in loop
1143
- # @param api [Rest] the API to call
1144
- # @param iteration [Array] a single element array with the iteration token or nil
1145
- # @param max [Integer] maximum number of items to return, or nil for no limit
1146
- # @param query [Hash] query parameters to use for the API call
1147
- # @param call_args [Hash] additional arguments to pass to the API call
1148
- # @return [Array] list of items returned by the API call
1149
- def call_with_iteration(api:, iteration: nil, max: nil, query: nil, **call_args)
1150
- Aspera.assert_type(iteration, Array, NilClass){'iteration'}
1151
- Aspera.assert_type(query, Hash, NilClass){'query'}
1152
- query_token = query&.dup || {}
1153
- item_list = []
1154
- query_token[:iteration_token] = iteration[0] unless iteration.nil?
1155
- loop do
1156
- data, http = api.call(**call_args, query: query_token, ret: :both)
1157
- Aspera.assert_type(data, Array){"Expected data to be an Array, got: #{data.class}"}
1158
- # no data
1159
- break if data.empty?
1160
- # get next iteration token from link
1161
- next_iteration_token = nil
1162
- link_info = http['Link']
1163
- unless link_info.nil?
1164
- m = link_info.match(/<([^>]+)>/)
1165
- Aspera.assert(m){"Cannot parse iteration in Link: #{link_info}"}
1166
- next_iteration_token = Rest.query_to_h(URI.parse(m[1]).query)['iteration_token']
1167
- end
1168
- # same as last iteration: stop
1169
- break if next_iteration_token&.eql?(query_token[:iteration_token])
1170
- query_token[:iteration_token] = next_iteration_token
1171
- item_list.concat(data)
1172
- if max&.<=(item_list.length)
1173
- item_list = item_list.slice(0, max)
1174
- break
1175
- end
1176
- break if next_iteration_token.nil?
1177
- end
1178
- # save iteration token if needed
1179
- iteration[0] = query_token[:iteration_token] unless iteration.nil?
1180
- item_list
1181
- end
1182
1137
  end
1183
1138
  end
1184
1139
  end
@@ -3,6 +3,8 @@
3
3
  require 'aspera/cli/plugins/basic_auth'
4
4
  require 'aspera/cli/plugins/node'
5
5
  require 'aspera/assert'
6
+ require 'aspera/rest_list'
7
+
6
8
  module Aspera
7
9
  module Cli
8
10
  module Plugins
@@ -128,7 +130,7 @@ module Aspera
128
130
  when :admin
129
131
  api_shares_admin = basic_auth_api(ADMIN_API_PATH)
130
132
  admin_command = options.get_next_command(%i[node share transfer_settings user group].freeze)
131
- lookup_share = ->(field, value){lookup_entity_generic(entity: 'share', field: field, value: value){api_shares_admin.read('data/shares')}['id']}
133
+ lookup_share = ->(field, value){RestList.lookup_entity_generic(entity: 'share', field: field, value: value){api_shares_admin.read('data/shares')}['id']}
132
134
  case admin_command
133
135
  when :node
134
136
  return entity_execute(api: api_shares_admin, entity: 'data/nodes')
@@ -177,7 +179,7 @@ module Aspera
177
179
  entity_commands = %i[import].freeze
178
180
  end
179
181
  entity_verb = options.get_next_command(entity_commands)
180
- lookup_block = ->(field, value){lookup_entity_generic(entity: entity_type, field: field, value: value){api_shares_admin.read(entities_path)}['id']}
182
+ lookup_block = ->(field, value){RestList.lookup_entity_generic(entity: entity_type, field: field, value: value){api_shares_admin.read(entities_path)}['id']}
181
183
  case entity_verb
182
184
  when *ALL_OPS # list, show, delete, create, modify
183
185
  display_fields = entity_type.eql?(:user) ? %w[id user_id username first_name last_name email] : nil
@@ -8,6 +8,7 @@ module Aspera
8
8
  INIT = 'INIT'
9
9
  ALL = 'ALL'
10
10
  DEF = 'DEF'
11
+ EOA = 'END'
11
12
  end
12
13
  end
13
14
  end
@@ -33,6 +33,9 @@ module Aspera
33
33
  :DEFAULT_TRANSFER_NOTIFY_TEMPLATE
34
34
 
35
35
  class << self
36
+ # Analyze transfer session statuses and return a global status
37
+ #
38
+ # @param statuses [Array] list of session status, each status is :success or an error message string
36
39
  # @return [:success] if all sessions statuses returned by "start" are success
37
40
  # @return [Exception] if one sessions statuses returned by "start" is failed
38
41
  def session_status(statuses)
@@ -4,6 +4,6 @@ module Aspera
4
4
  module Cli
5
5
  # For beta add extension : .beta1
6
6
  # For dev version add extension : .pre
7
- VERSION = '4.25.3'
7
+ VERSION = '4.25.5'
8
8
  end
9
9
  end