aspera-cli 4.16.0 → 4.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +50 -19
  4. data/CONTRIBUTING.md +3 -1
  5. data/README.md +965 -793
  6. data/bin/asession +29 -21
  7. data/lib/aspera/{fasp/agent_alpha.rb → agent/alpha.rb} +26 -25
  8. data/lib/aspera/{fasp/agent_base.rb → agent/base.rb} +15 -12
  9. data/lib/aspera/{fasp/agent_connect.rb → agent/connect.rb} +13 -11
  10. data/lib/aspera/{fasp/agent_direct.rb → agent/direct.rb} +49 -53
  11. data/lib/aspera/{fasp/agent_httpgw.rb → agent/httpgw.rb} +20 -19
  12. data/lib/aspera/{fasp/agent_node.rb → agent/node.rb} +20 -33
  13. data/lib/aspera/{fasp/agent_trsdk.rb → agent/trsdk.rb} +11 -11
  14. data/lib/aspera/api/aoc.rb +586 -0
  15. data/lib/aspera/api/ats.rb +46 -0
  16. data/lib/aspera/api/cos_node.rb +95 -0
  17. data/lib/aspera/api/node.rb +344 -0
  18. data/lib/aspera/ascmd.rb +46 -10
  19. data/lib/aspera/{fasp → ascp}/installation.rb +5 -5
  20. data/lib/aspera/{fasp → ascp}/management.rb +3 -8
  21. data/lib/aspera/{fasp → ascp}/products.rb +1 -1
  22. data/lib/aspera/assert.rb +30 -30
  23. data/lib/aspera/cli/basic_auth_plugin.rb +11 -10
  24. data/lib/aspera/cli/extended_value.rb +1 -1
  25. data/lib/aspera/cli/formatter.rb +13 -13
  26. data/lib/aspera/cli/hints.rb +5 -5
  27. data/lib/aspera/cli/main.rb +35 -28
  28. data/lib/aspera/cli/manager.rb +25 -24
  29. data/lib/aspera/cli/plugin.rb +22 -15
  30. data/lib/aspera/cli/plugin_factory.rb +61 -0
  31. data/lib/aspera/cli/plugins/alee.rb +7 -7
  32. data/lib/aspera/cli/plugins/aoc.rb +83 -77
  33. data/lib/aspera/cli/plugins/ats.rb +32 -33
  34. data/lib/aspera/cli/plugins/bss.rb +3 -4
  35. data/lib/aspera/cli/plugins/config.rb +169 -186
  36. data/lib/aspera/cli/plugins/console.rb +8 -6
  37. data/lib/aspera/cli/plugins/cos.rb +19 -18
  38. data/lib/aspera/cli/plugins/faspex.rb +61 -54
  39. data/lib/aspera/cli/plugins/faspex5.rb +150 -103
  40. data/lib/aspera/cli/plugins/node.rb +68 -73
  41. data/lib/aspera/cli/plugins/orchestrator.rb +34 -44
  42. data/lib/aspera/cli/plugins/preview.rb +31 -31
  43. data/lib/aspera/cli/plugins/server.rb +31 -33
  44. data/lib/aspera/cli/plugins/shares.rb +13 -11
  45. data/lib/aspera/cli/sync_actions.rb +8 -8
  46. data/lib/aspera/cli/transfer_agent.rb +32 -19
  47. data/lib/aspera/cli/transfer_progress.rb +1 -1
  48. data/lib/aspera/cli/version.rb +1 -1
  49. data/lib/aspera/colors.rb +5 -0
  50. data/lib/aspera/command_line_builder.rb +14 -14
  51. data/lib/aspera/coverage.rb +1 -2
  52. data/lib/aspera/data_repository.rb +1 -1
  53. data/lib/aspera/environment.rb +2 -3
  54. data/lib/aspera/faspex_gw.rb +5 -6
  55. data/lib/aspera/faspex_postproc.rb +1 -1
  56. data/lib/aspera/id_generator.rb +2 -2
  57. data/lib/aspera/json_rpc.rb +5 -5
  58. data/lib/aspera/keychain/encrypted_hash.rb +6 -6
  59. data/lib/aspera/keychain/macos_security.rb +27 -22
  60. data/lib/aspera/log.rb +2 -2
  61. data/lib/aspera/nagios.rb +3 -3
  62. data/lib/aspera/node_simulator.rb +5 -6
  63. data/lib/aspera/oauth/base.rb +143 -0
  64. data/lib/aspera/oauth/factory.rb +124 -0
  65. data/lib/aspera/oauth/generic.rb +34 -0
  66. data/lib/aspera/oauth/jwt.rb +51 -0
  67. data/lib/aspera/oauth/url_json.rb +31 -0
  68. data/lib/aspera/oauth/web.rb +50 -0
  69. data/lib/aspera/oauth.rb +5 -331
  70. data/lib/aspera/open_application.rb +7 -7
  71. data/lib/aspera/persistency_action_once.rb +4 -4
  72. data/lib/aspera/persistency_folder.rb +2 -2
  73. data/lib/aspera/preview/generator.rb +5 -5
  74. data/lib/aspera/preview/terminal.rb +3 -2
  75. data/lib/aspera/preview/utils.rb +3 -3
  76. data/lib/aspera/proxy_auto_config.rb +4 -4
  77. data/lib/aspera/rest.rb +175 -144
  78. data/lib/aspera/rest_errors_aspera.rb +3 -3
  79. data/lib/aspera/resumer.rb +77 -0
  80. data/lib/aspera/ssh.rb +6 -1
  81. data/lib/aspera/{fasp → transfer}/error.rb +3 -3
  82. data/lib/aspera/{fasp → transfer}/error_info.rb +1 -1
  83. data/lib/aspera/{fasp → transfer}/faux_file.rb +1 -1
  84. data/lib/aspera/{fasp → transfer}/parameters.rb +58 -89
  85. data/lib/aspera/{fasp/transfer_spec.rb → transfer/spec.rb} +18 -16
  86. data/lib/aspera/{fasp/parameters.yaml → transfer/spec.yaml} +4 -99
  87. data/lib/aspera/{fasp → transfer}/sync.rb +32 -32
  88. data/lib/aspera/{fasp → transfer}/uri.rb +9 -8
  89. data/lib/aspera/web_server_simple.rb +11 -3
  90. data.tar.gz.sig +0 -0
  91. metadata +36 -63
  92. metadata.gz.sig +0 -0
  93. data/lib/aspera/aoc.rb +0 -601
  94. data/lib/aspera/ats_api.rb +0 -47
  95. data/lib/aspera/cos_node.rb +0 -94
  96. data/lib/aspera/fasp/resume_policy.rb +0 -79
  97. data/lib/aspera/node.rb +0 -339
@@ -15,10 +15,9 @@ require 'tty-spinner'
15
15
  module Aspera
16
16
  module Cli
17
17
  module Plugins
18
- class Faspex5 < Aspera::Cli::BasicAuthPlugin
18
+ class Faspex5 < Cli::BasicAuthPlugin
19
19
  RECIPIENT_TYPES = %w[user workgroup external_user distribution_list shared_inbox].freeze
20
20
  PACKAGE_TERMINATED = %w[completed failed].freeze
21
- API_DETECT = 'api/v5/configuration/ping'
22
21
  # list of supported mailbox types (to list packages)
23
22
  API_LIST_MAILBOX_TYPES = %w[inbox inbox_history inbox_all inbox_all_history outbox outbox_history pending pending_history all].freeze
24
23
  PACKAGE_SEND_FROM_REMOTE_SOURCE = 'remote_source'
@@ -28,34 +27,49 @@ module Aspera
28
27
  accounts contacts jobs workgroups shared_inboxes nodes oauth_clients registrations saml_configs
29
28
  metadata_profiles email_notifications alternate_addresses
30
29
  ].freeze
30
+ # states for jobs not in final state
31
31
  JOB_RUNNING = %w[queued working].freeze
32
- STANDARD_PATH = '/aspera/faspex'
32
+ PATH_STANDARD_ROOT = '/aspera/faspex'
33
+ PATH_API_V5 = 'api/v5'
34
+ # endpoint for authentication API
35
+ PATH_AUTH = 'auth'
36
+ PATH_HEALTH = 'configuration/ping'
33
37
  PER_PAGE_DEFAULT = 100
34
- private_constant(*%i[JOB_RUNNING RECIPIENT_TYPES PACKAGE_TERMINATED API_DETECT API_LIST_MAILBOX_TYPES PACKAGE_SEND_FROM_REMOTE_SOURCE PER_PAGE_DEFAULT])
38
+ # OAuth methods supported
39
+ STD_AUTH_TYPES = %i[web jwt boot].freeze
40
+ HEADER_ITERATION_TOKEN = 'X-Aspera-Next-Iteration-Token'
41
+ private_constant(*%i[JOB_RUNNING RECIPIENT_TYPES PACKAGE_TERMINATED PATH_HEALTH API_LIST_MAILBOX_TYPES PACKAGE_SEND_FROM_REMOTE_SOURCE PER_PAGE_DEFAULT
42
+ STD_AUTH_TYPES])
35
43
  class << self
36
44
  def application_name
37
45
  'Faspex'
38
46
  end
39
47
 
40
48
  def detect(address_or_url)
49
+ # add scheme if missing
41
50
  address_or_url = "https://#{address_or_url}" unless address_or_url.match?(%r{^[a-z]{1,6}://})
42
51
  urls = [address_or_url]
43
- urls.push("#{address_or_url}#{STANDARD_PATH}") unless address_or_url.end_with?(STANDARD_PATH)
44
-
52
+ urls.push("#{address_or_url}#{PATH_STANDARD_ROOT}") unless address_or_url.end_with?(PATH_STANDARD_ROOT)
53
+ error = nil
45
54
  urls.each do |base_url|
55
+ # Faspex is always HTTPS
46
56
  next unless base_url.start_with?('https://')
47
57
  api = Rest.new(base_url: base_url, redirect_max: 1)
48
- result = api.read(API_DETECT)
58
+ path_api_detect = "#{PATH_API_V5}/#{PATH_HEALTH}"
59
+ result = api.read(path_api_detect)
49
60
  next unless result[:http].code.start_with?('2') && result[:http].body.strip.empty?
50
- url_length = -2 - API_DETECT.length
61
+ # end is at -1, and substract 1 for "/"
62
+ url_length = -2 - path_api_detect.length
51
63
  # take redirect if any
52
64
  return {
53
65
  version: result[:http]['x-ibm-aspera'] || '5',
54
66
  url: result[:http].uri.to_s[0..url_length]
55
67
  }
56
68
  rescue StandardError => e
69
+ error = e
57
70
  Log.log.debug{"detect error: #{e}"}
58
71
  end
72
+ raise error if error
59
73
  return nil
60
74
  end
61
75
 
@@ -91,31 +105,25 @@ module Aspera
91
105
  }
92
106
  end
93
107
 
108
+ # @return true if the URL is a public link
94
109
  def public_link?(url)
95
- url.include?('/public/')
110
+ url.include?('?context=')
96
111
  end
97
112
  end
98
113
 
99
- def initialize(env)
100
- super(env)
114
+ def initialize(**env)
115
+ super
101
116
  options.declare(:client_id, 'OAuth client identifier')
102
117
  options.declare(:client_secret, 'OAuth client secret')
103
118
  options.declare(:redirect_uri, 'OAuth redirect URI for web authentication')
104
- options.declare(:auth, 'OAuth type of authentication', values: %i[boot].concat(Oauth::STD_AUTH_TYPES), default: :jwt)
119
+ options.declare(:auth, 'OAuth type of authentication', values: STD_AUTH_TYPES, default: :jwt)
105
120
  options.declare(:private_key, 'OAuth JWT RSA private key PEM value (prefix file path with @file:)')
106
121
  options.declare(:passphrase, 'OAuth JWT RSA private key passphrase')
107
122
  options.declare(:box, "Package inbox, either shared inbox name or one of: #{API_LIST_MAILBOX_TYPES.join(', ')} or #{ExtendedValue::ALL}", default: 'inbox')
108
123
  options.declare(:shared_folder, 'Send package with files from shared folder')
109
124
  options.declare(:group_type, 'Type of shared box', values: %i[shared_inboxes workgroups], default: :shared_inboxes)
110
125
  options.parse_options!
111
- end
112
-
113
- def api_url
114
- return "#{@faspex5_api_base_url}/api/v5"
115
- end
116
-
117
- def auth_api_url
118
- return "#{@faspex5_api_base_url}/auth"
126
+ @pub_link_context = nil
119
127
  end
120
128
 
121
129
  def set_api
@@ -124,60 +132,63 @@ module Aspera
124
132
  auth_type = self.class.public_link?(@faspex5_api_base_url) ? :public_link : options.get_option(:auth, mandatory: true)
125
133
  case auth_type
126
134
  when :public_link
135
+ # resolve any redirect
136
+ @faspex5_api_base_url = Rest.new(base_url: @faspex5_api_base_url, redirect_max: 3).read('')[:http].uri.to_s
127
137
  encoded_context = Rest.decode_query(URI.parse(@faspex5_api_base_url).query)['context']
128
138
  raise 'Bad faspex5 public link, missing context in query' if encoded_context.nil?
139
+ # public link information (allowed usage)
129
140
  @pub_link_context = JSON.parse(Base64.decode64(encoded_context))
130
141
  Log.log.trace1{Log.dump(:@pub_link_context, @pub_link_context)}
131
142
  # ok, we have the additional parameters, get the base url
132
143
  @faspex5_api_base_url = @faspex5_api_base_url.gsub(%r{/public/.*}, '').gsub(/\?.*/, '')
133
- @api_v5 = Rest.new({
134
- base_url: api_url,
144
+ @api_v5 = Rest.new(
145
+ base_url: "#{@faspex5_api_base_url}/#{PATH_API_V5}",
135
146
  headers: {'Passcode' => @pub_link_context['passcode']}
136
- })
147
+ )
137
148
  when :boot
138
149
  # the password here is the token copied directly from browser in developer mode
139
- @api_v5 = Rest.new({
140
- base_url: api_url,
150
+ @api_v5 = Rest.new(
151
+ base_url: "#{@faspex5_api_base_url}/#{PATH_API_V5}",
141
152
  headers: {'Authorization' => options.get_option(:password, mandatory: true)}
142
- })
153
+ )
143
154
  when :web
144
155
  # opens a browser and ask user to auth using web
145
- @api_v5 = Rest.new({
146
- base_url: api_url,
156
+ @api_v5 = Rest.new(
157
+ base_url: "#{@faspex5_api_base_url}/#{PATH_API_V5}",
147
158
  auth: {
148
159
  type: :oauth2,
149
- base_url: auth_api_url,
160
+ base_url: "#{@faspex5_api_base_url}/#{PATH_AUTH}",
150
161
  grant_method: :web,
151
162
  client_id: options.get_option(:client_id, mandatory: true),
152
- web: {redirect_uri: options.get_option(:redirect_uri, mandatory: true)}
153
- }})
163
+ redirect_uri: options.get_option(:redirect_uri, mandatory: true)
164
+ })
154
165
  when :jwt
155
166
  app_client_id = options.get_option(:client_id, mandatory: true)
156
- @api_v5 = Rest.new({
157
- base_url: api_url,
167
+ @api_v5 = Rest.new(
168
+ base_url: "#{@faspex5_api_base_url}/#{PATH_API_V5}",
158
169
  auth: {
159
- type: :oauth2,
160
- base_url: auth_api_url,
161
- grant_method: :jwt,
162
- client_id: app_client_id,
163
- jwt: {
164
- payload: {
165
- iss: app_client_id, # issuer
166
- aud: app_client_id, # audience (this field is not clear...)
167
- sub: "user:#{options.get_option(:username, mandatory: true)}" # subject is a user
168
- },
169
- private_key_obj: OpenSSL::PKey::RSA.new(options.get_option(:private_key, mandatory: true), options.get_option(:passphrase)),
170
- headers: {typ: 'JWT'}
171
- }
172
- }})
173
- else error_unexpected_value(auth_type)
170
+ type: :oauth2,
171
+ grant_method: :jwt,
172
+ base_url: "#{@faspex5_api_base_url}/#{PATH_AUTH}",
173
+ client_id: app_client_id,
174
+ payload: {
175
+ iss: app_client_id, # issuer
176
+ aud: app_client_id, # audience (this field is not clear...)
177
+ sub: "user:#{options.get_option(:username, mandatory: true)}" # subject is a user
178
+ },
179
+ private_key_obj: OpenSSL::PKey::RSA.new(options.get_option(:private_key, mandatory: true), options.get_option(:passphrase)),
180
+ headers: {typ: 'JWT'}
181
+ })
182
+ else Aspera.error_unexpected_value(auth_type)
174
183
  end
184
+ # in case user wants to use HTTPGW tell transfer agent how to get address
185
+ transfer.httpgw_url_cb = lambda { @api_v5.read('account')[:data]['gateway_url'] }
175
186
  end
176
187
 
177
188
  # if recipient is just an email, then convert to expected API hash : name and type
178
189
  def normalize_recipients(parameters)
179
190
  return unless parameters.key?('recipients')
180
- assert_type(parameters['recipients'], Array){'recipients'}
191
+ Aspera.assert_type(parameters['recipients'], Array){'recipients'}
181
192
  recipient_types = RECIPIENT_TYPES
182
193
  if parameters.key?('recipient_types')
183
194
  recipient_types = parameters['recipient_types']
@@ -185,7 +196,7 @@ module Aspera
185
196
  recipient_types = [recipient_types] unless recipient_types.is_a?(Array)
186
197
  end
187
198
  parameters['recipients'].map! do |recipient_data|
188
- # if just a string, assume it is the name
199
+ # if just a string, make a general lookup and build expected name/type hash
189
200
  if recipient_data.is_a?(String)
190
201
  matched = @api_v5.lookup_by_name('contacts', recipient_data, {context: 'packages', type: Rest.array_params(recipient_types)})
191
202
  recipient_data = {
@@ -238,7 +249,7 @@ module Aspera
238
249
  spinner.spin
239
250
  sleep(0.5)
240
251
  end
241
- error_unreachable_line
252
+ Aspera.error_unreachable_line
242
253
  end
243
254
 
244
255
  # Get a (full or partial) list of all entities of a given type
@@ -248,7 +259,7 @@ module Aspera
248
259
  # @param item_list_key [String] key in the result to get the list of items
249
260
  def list_entities(type:, real_path: nil, query: {}, item_list_key: nil)
250
261
  type = type.to_s if type.is_a?(Symbol)
251
- assert_type(type, String)
262
+ Aspera.assert_type(type, String)
252
263
  item_list_key = type if item_list_key.nil?
253
264
  full_path = real_path.nil? ? type : real_path
254
265
  result = []
@@ -277,7 +288,7 @@ module Aspera
277
288
  # lookup an entity id from its name
278
289
  def lookup_entity_by_field(type:, value:, field: 'name', query: :default, real_path: nil, item_list_key: nil)
279
290
  if query.eql?(:default)
280
- assert(field.eql?('name')){'Default query is on name only'}
291
+ Aspera.assert(field.eql?('name')){'Default query is on name only'}
281
292
  query = {'q'=> value}
282
293
  end
283
294
  found = list_entities(type: type, real_path: real_path, query: query, item_list_key: item_list_key).select{|i|i[field].eql?(value)}
@@ -313,7 +324,7 @@ module Aspera
313
324
  if options.get_option(:once_only, mandatory: true)
314
325
  # read ids from persistency
315
326
  skip_ids_persistency = PersistencyActionOnce.new(
316
- manager: @agents[:persistency],
327
+ manager: persistency,
317
328
  data: [],
318
329
  id: IdGenerator.from_list([
319
330
  'faspex_recv',
@@ -325,7 +336,7 @@ module Aspera
325
336
  packages = []
326
337
  case package_ids
327
338
  when ExtendedValue::INIT
328
- assert(skip_ids_persistency){'Only with option once_only'}
339
+ Aspera.assert(skip_ids_persistency){'Only with option once_only'}
329
340
  skip_ids_persistency.data.clear.concat(list_packages_with_filter.map{|p|p['id']})
330
341
  skip_ids_persistency.save
331
342
  return Main.result_status("Initialized skip for #{skip_ids_persistency.data.count} package(s)")
@@ -339,8 +350,8 @@ module Aspera
339
350
  else
340
351
  # a single id was provided, or a list of ids
341
352
  package_ids = [package_ids] unless package_ids.is_a?(Array)
342
- assert_type(package_ids, Array){'Expecting a single package id or a list of ids'}
343
- assert(package_ids.all?(String)){'Package id shall be String'}
353
+ Aspera.assert_type(package_ids, Array){'Expecting a single package id or a list of ids'}
354
+ Aspera.assert(package_ids.all?(String)){'Package id shall be String'}
344
355
  # packages = package_ids.map{|pkg_id|@api_v5.read("packages/#{pkg_id}")[:data]}
345
356
  packages = package_ids.map{|pkg_id|{'id'=>pkg_id}}
346
357
  end
@@ -386,6 +397,45 @@ module Aspera
386
397
  return Main.result_transfer_multiple(result_transfer)
387
398
  end
388
399
 
400
+ # browse a folder
401
+ # @param browse_endpoint [String] the endpoint to browse
402
+ def browse_folder(browse_endpoint)
403
+ path = options.get_next_argument('folder path', mandatory: false, default: '/')
404
+ query = query_read_delete(default: {})
405
+ query['filters'] = {} unless query.key?('filters')
406
+ filters = query.delete('filters')
407
+ filters['basenames'] = [] unless filters.key?('basenames')
408
+ Aspera.assert_type(filters, Hash){'filters'}
409
+ max_items = query.delete('max')
410
+ recursive = query.delete('recursive')
411
+ all_items = []
412
+ folders_to_process = [path]
413
+ until folders_to_process.empty?
414
+ path = folders_to_process.shift
415
+ query.delete('iteration_token')
416
+ loop do
417
+ response = @api_v5.call(
418
+ operation: 'POST',
419
+ subpath: browse_endpoint,
420
+ headers: {'Accept' => 'application/json', 'Content-Type' => 'application/json'},
421
+ url_params: query,
422
+ json_params: {'path' => path, 'filters' => filters})
423
+ all_items.concat(response[:data]['items'])
424
+ if recursive
425
+ folders_to_process.concat(response[:data]['items'].select{|i|i['type'].eql?('directory')}.map{|i|i['path']})
426
+ end
427
+ if !max_items.nil? && (all_items.count >= max_items)
428
+ all_items = all_items.slice(0, max_items) if all_items.count > max_items
429
+ break
430
+ end
431
+ iteration_token = response[:http][HEADER_ITERATION_TOKEN]
432
+ break if iteration_token.nil? || iteration_token.empty?
433
+ query['iteration_token'] = iteration_token
434
+ end
435
+ end
436
+ return {type: :object_list, data: all_items}
437
+ end
438
+
389
439
  def package_action
390
440
  command = options.get_next_command(%i[show browse status delete receive send list])
391
441
  package_id =
@@ -396,36 +446,34 @@ module Aspera
396
446
  when :show
397
447
  return {type: :single_object, data: @api_v5.read("packages/#{package_id}")[:data]}
398
448
  when :browse
399
- path = options.get_next_argument('path', expected: :single, mandatory: false) || '/'
400
- # TODO: support multi-page listing ?
401
- params = {
402
- # recipient_user_id: 25,
403
- # offset: 0,
404
- # limit: 25
405
- }
406
- result = @api_v5.call({
407
- operation: 'POST',
408
- subpath: "packages/#{package_id}/files/received",
409
- headers: {'Accept' => 'application/json'},
410
- url_params: params,
411
- json_params: {'path' => path, 'filters' => {'basenames'=>[]}}})[:data]
412
- formatter.display_item_count(result['item_count'], result['total_count'])
413
- return {type: :object_list, data: result['items']}
449
+ location = case options.get_option(:box)
450
+ when 'inbox' then 'received'
451
+ when 'outbox' then 'sent'
452
+ else raise 'Browse only available for inbox and outbox'
453
+ end
454
+ return browse_folder("packages/#{package_id}/files/#{location}")
414
455
  when :status
415
456
  status = wait_package_status(package_id, status_list: nil)
416
457
  return {type: :single_object, data: status}
417
458
  when :delete
418
459
  ids = package_id
419
460
  ids = [ids] unless ids.is_a?(Array)
420
- assert_type(ids, Array){'Package identifier'}
421
- assert(ids.all?(String)){'Package id shall be String'}
461
+ Aspera.assert_type(ids, Array){'Package identifier'}
462
+ Aspera.assert(ids.all?(String)){'Package id shall be String'}
422
463
  # API returns 204, empty on success
423
- @api_v5.call({operation: 'DELETE', subpath: 'packages', headers: {'Accept' => 'application/json'}, json_params: {ids: ids}})
464
+ @api_v5.call(operation: 'DELETE', subpath: 'packages', headers: {'Accept' => 'application/json'}, json_params: {ids: ids})
424
465
  return Main.result_status('Package(s) deleted')
425
466
  when :receive
426
467
  return package_receive(package_id)
427
468
  when :send
428
469
  parameters = value_create_modify(command: command)
470
+ # autofill recipient for public url
471
+ if @pub_link_context&.key?('recipient_type') && !parameters.key?('recipients')
472
+ parameters['recipients'] = [{
473
+ name: @pub_link_context['name'],
474
+ recipient_type: @pub_link_context['recipient_type']
475
+ }]
476
+ end
429
477
  normalize_recipients(parameters)
430
478
  package = @api_v5.create('packages', parameters)[:data]
431
479
  shared_folder = options.get_option(:shared_folder)
@@ -465,7 +513,7 @@ module Aspera
465
513
  end # case package
466
514
  end
467
515
 
468
- ACTIONS = %i[health version user bearer_token packages shared_folders admin gateway postprocessing].freeze
516
+ ACTIONS = %i[health version user bearer_token packages shared_folders admin gateway postprocessing invitations].freeze
469
517
 
470
518
  def execute_action
471
519
  command = options.get_next_command(ACTIONS)
@@ -485,7 +533,9 @@ module Aspera
485
533
  end
486
534
  return nagios.result
487
535
  when :user
488
- case options.get_next_command(%i[profile])
536
+ case options.get_next_command(%i[account profile])
537
+ when :account
538
+ return { type: :single_object, data: @api_v5.read('account')[:data] }
489
539
  when :profile
490
540
  case options.get_next_command(%i[show modify])
491
541
  when :show
@@ -511,21 +561,9 @@ module Aspera
511
561
  raise "multiple matches for #{field} = #{value}" if matches.length > 1
512
562
  matches.first['id']
513
563
  end
514
- path = options.get_next_argument('folder path', mandatory: false) || '/'
515
564
  node = all_shared_folders.find{|i|i['id'].eql?(shared_folder_id)}
516
565
  raise "No such shared folder id #{shared_folder_id}" if node.nil?
517
- result = @api_v5.call({
518
- operation: 'POST',
519
- subpath: "nodes/#{node['node_id']}/shared_folders/#{shared_folder_id}/browse",
520
- headers: {'Accept' => 'application/json', 'Content-Type' => 'application/json'},
521
- json_params: {'path': path, 'filters': {'basenames': []}},
522
- url_params: {offset: 0, limit: 100}
523
- })[:data]
524
- if result.key?('items')
525
- return {type: :object_list, data: result['items']}
526
- else
527
- return {type: :single_object, data: result['self']}
528
- end
566
+ return browse_folder("nodes/#{node['node_id']}/shared_folders/#{shared_folder_id}/browse")
529
567
  end
530
568
  when :admin
531
569
  case options.get_next_command(%i[resource smtp].freeze)
@@ -550,12 +588,12 @@ module Aspera
550
588
  display_fields = Formatter.all_but('user_profile_data_attributes')
551
589
  when :oauth_clients
552
590
  display_fields = Formatter.all_but('public_key')
553
- adm_api = Rest.new(@api_v5.params.merge({base_url: auth_api_url}))
591
+ adm_api = Rest.new(**@api_v5.params.merge(base_url: "#{@faspex5_api_base_url}/#{PATH_AUTH}"))
554
592
  when :shared_inboxes, :workgroups
555
593
  available_commands.push(:members, :saml_groups, :invite_external_collaborator)
556
594
  special_query = {'all': true}
557
595
  when :nodes
558
- available_commands.push(:shared_folders)
596
+ available_commands.push(:shared_folders, :browse)
559
597
  end
560
598
  res_command = options.get_next_command(available_commands)
561
599
  case res_command
@@ -573,6 +611,8 @@ module Aspera
573
611
  lookup_entity_by_field(
574
612
  type: 'shared_folders', real_path: sh_path, field: field, value: value)['id']
575
613
  end
614
+ when :browse
615
+ return browse_folder("#{res_path}/#{instance_identifier}/browse")
576
616
  when :invite_external_collaborator
577
617
  shared_inbox_id = instance_identifier { |field, value| lookup_entity_by_field(type: res_type.to_s, field: field, value: value)['id']}
578
618
  creation_payload = value_create_modify(command: res_command, type: [Hash, String])
@@ -637,34 +677,41 @@ module Aspera
637
677
  return { type: :single_object, data: result }
638
678
  end
639
679
  end
680
+ when :invitations
681
+ invitation_endpoint = 'invitations'
682
+ invitation_command = options.get_next_command(%i[resend].concat(Plugin::ALL_OPS))
683
+ case invitation_command
684
+ when :create
685
+ return do_bulk_operation(command: invitation_command, descr: 'data') do |params|
686
+ invitation_endpoint = params.key?('recipient_name') ? 'public_invitations' : 'invitations'
687
+ @api_v5.create(invitation_endpoint, params)[:data]
688
+ end
689
+ when :resend
690
+ @api_v5.create("#{invitation_endpoint}/#{instance_identifier}/resend")
691
+ return Main.result_status('Invitation resent')
692
+ else
693
+ return entity_command(
694
+ invitation_command, @api_v5, invitation_endpoint, item_list_key: invitation_endpoint,
695
+ display_fields: %w[id public recipient_type recipient_name email_address])
696
+ end
640
697
  when :gateway
641
698
  require 'aspera/faspex_gw'
642
699
  url = value_create_modify(command: command, description: 'listening url (e.g. https://localhost:12345)', type: String)
643
700
  uri = URI.parse(url)
644
701
  server = WebServerSimple.new(uri)
645
702
  server.mount(uri.path, Faspex4GWServlet, @api_v5, nil)
646
- # on ctrl-c, tell server main loop to exit
647
- trap('INT') { server.shutdown }
648
- formatter.display_status("Gateway for Faspex 4-style API listening on #{url}")
649
- Log.log.info("Listening on #{url}")
650
- # this is blocking until server exits
651
703
  server.start
652
704
  return Main.result_status('Gateway terminated')
653
705
  when :postprocessing
654
706
  require 'aspera/faspex_postproc' # cspell:disable-line
655
707
  parameters = value_create_modify(command: command)
656
708
  parameters = parameters.symbolize_keys
657
- assert(parameters.key?(:url)){'Missing key: url'}
709
+ Aspera.assert(parameters.key?(:url)){'Missing key: url'}
658
710
  uri = URI.parse(parameters[:url])
659
711
  parameters[:processing] ||= {}
660
712
  parameters[:processing][:root] = uri.path
661
713
  server = WebServerSimple.new(uri, certificate: parameters[:certificate])
662
714
  server.mount(uri.path, Faspex4PostProcServlet, parameters[:processing])
663
- # on ctrl-c, tell server main loop to exit
664
- trap('INT') { server.shutdown }
665
- formatter.display_status("Web-hook for Faspex 4-style post processing listening on #{uri.port}")
666
- Log.log.info("Listening on #{uri.port}")
667
- # this is blocking until server exits
668
715
  server.start
669
716
  return Main.result_status('Gateway terminated')
670
717
  end # case command