aspera-cli 4.14.0 → 4.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +29 -3
  4. data/CHANGELOG.md +300 -185
  5. data/CONTRIBUTING.md +74 -23
  6. data/README.md +2346 -1619
  7. data/bin/ascli +16 -25
  8. data/bin/asession +15 -15
  9. data/examples/dascli +2 -2
  10. data/examples/proxy.pac +1 -1
  11. data/lib/aspera/aoc.rb +216 -150
  12. data/lib/aspera/ascmd.rb +25 -18
  13. data/lib/aspera/assert.rb +45 -0
  14. data/lib/aspera/cli/basic_auth_plugin.rb +9 -6
  15. data/lib/aspera/cli/error.rb +17 -0
  16. data/lib/aspera/cli/extended_value.rb +51 -16
  17. data/lib/aspera/cli/formatter.rb +276 -174
  18. data/lib/aspera/cli/hints.rb +81 -0
  19. data/lib/aspera/cli/main.rb +114 -147
  20. data/lib/aspera/cli/manager.rb +181 -136
  21. data/lib/aspera/cli/plugin.rb +82 -64
  22. data/lib/aspera/cli/plugins/alee.rb +0 -1
  23. data/lib/aspera/cli/plugins/aoc.rb +327 -331
  24. data/lib/aspera/cli/plugins/ats.rb +12 -8
  25. data/lib/aspera/cli/plugins/bss.rb +2 -2
  26. data/lib/aspera/cli/plugins/config.rb +575 -439
  27. data/lib/aspera/cli/plugins/console.rb +40 -0
  28. data/lib/aspera/cli/plugins/cos.rb +4 -5
  29. data/lib/aspera/cli/plugins/faspex.rb +111 -92
  30. data/lib/aspera/cli/plugins/faspex5.rb +245 -182
  31. data/lib/aspera/cli/plugins/node.rb +239 -160
  32. data/lib/aspera/cli/plugins/orchestrator.rb +56 -19
  33. data/lib/aspera/cli/plugins/preview.rb +54 -38
  34. data/lib/aspera/cli/plugins/server.rb +63 -20
  35. data/lib/aspera/cli/plugins/shares.rb +64 -38
  36. data/lib/aspera/cli/sync_actions.rb +68 -0
  37. data/lib/aspera/cli/transfer_agent.rb +64 -67
  38. data/lib/aspera/cli/transfer_progress.rb +73 -0
  39. data/lib/aspera/cli/version.rb +1 -1
  40. data/lib/aspera/colors.rb +3 -1
  41. data/lib/aspera/command_line_builder.rb +27 -22
  42. data/lib/aspera/cos_node.rb +6 -4
  43. data/lib/aspera/coverage.rb +22 -0
  44. data/lib/aspera/data_repository.rb +33 -2
  45. data/lib/aspera/environment.rb +21 -8
  46. data/lib/aspera/fasp/agent_alpha.rb +116 -0
  47. data/lib/aspera/fasp/agent_base.rb +40 -76
  48. data/lib/aspera/fasp/agent_connect.rb +21 -22
  49. data/lib/aspera/fasp/agent_direct.rb +169 -179
  50. data/lib/aspera/fasp/agent_httpgw.rb +200 -195
  51. data/lib/aspera/fasp/agent_node.rb +43 -35
  52. data/lib/aspera/fasp/agent_trsdk.rb +124 -41
  53. data/lib/aspera/fasp/error_info.rb +2 -2
  54. data/lib/aspera/fasp/faux_file.rb +52 -0
  55. data/lib/aspera/fasp/installation.rb +89 -191
  56. data/lib/aspera/fasp/management.rb +249 -0
  57. data/lib/aspera/fasp/parameters.rb +86 -47
  58. data/lib/aspera/fasp/parameters.yaml +75 -8
  59. data/lib/aspera/fasp/products.rb +162 -0
  60. data/lib/aspera/fasp/resume_policy.rb +7 -5
  61. data/lib/aspera/fasp/sync.rb +273 -0
  62. data/lib/aspera/fasp/transfer_spec.rb +10 -8
  63. data/lib/aspera/fasp/uri.rb +6 -6
  64. data/lib/aspera/faspex_gw.rb +11 -8
  65. data/lib/aspera/faspex_postproc.rb +8 -7
  66. data/lib/aspera/hash_ext.rb +2 -2
  67. data/lib/aspera/id_generator.rb +3 -1
  68. data/lib/aspera/json_rpc.rb +51 -0
  69. data/lib/aspera/keychain/encrypted_hash.rb +46 -11
  70. data/lib/aspera/keychain/macos_security.rb +15 -13
  71. data/lib/aspera/line_logger.rb +23 -0
  72. data/lib/aspera/log.rb +61 -19
  73. data/lib/aspera/nagios.rb +7 -2
  74. data/lib/aspera/node.rb +105 -21
  75. data/lib/aspera/node_simulator.rb +214 -0
  76. data/lib/aspera/oauth.rb +57 -36
  77. data/lib/aspera/open_application.rb +4 -4
  78. data/lib/aspera/persistency_action_once.rb +13 -14
  79. data/lib/aspera/persistency_folder.rb +5 -4
  80. data/lib/aspera/preview/file_types.rb +56 -268
  81. data/lib/aspera/preview/generator.rb +28 -39
  82. data/lib/aspera/preview/options.rb +2 -0
  83. data/lib/aspera/preview/terminal.rb +36 -16
  84. data/lib/aspera/preview/utils.rb +23 -29
  85. data/lib/aspera/proxy_auto_config.rb +6 -3
  86. data/lib/aspera/rest.rb +127 -80
  87. data/lib/aspera/rest_call_error.rb +1 -1
  88. data/lib/aspera/rest_error_analyzer.rb +16 -14
  89. data/lib/aspera/rest_errors_aspera.rb +39 -34
  90. data/lib/aspera/secret_hider.rb +18 -17
  91. data/lib/aspera/ssh.rb +10 -5
  92. data/lib/aspera/temp_file_manager.rb +11 -4
  93. data/lib/aspera/web_auth.rb +10 -7
  94. data/lib/aspera/web_server_simple.rb +11 -5
  95. data.tar.gz.sig +0 -0
  96. metadata +108 -39
  97. metadata.gz.sig +0 -0
  98. data/lib/aspera/cli/listener/line_dump.rb +0 -19
  99. data/lib/aspera/cli/listener/logger.rb +0 -22
  100. data/lib/aspera/cli/listener/progress.rb +0 -50
  101. data/lib/aspera/cli/listener/progress_multi.rb +0 -84
  102. data/lib/aspera/cli/plugins/sync.rb +0 -44
  103. data/lib/aspera/fasp/listener.rb +0 -13
  104. data/lib/aspera/sync.rb +0 -213
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # spellchecker: ignore workgroups,mypackages
3
+ # spellchecker: ignore workgroups mypackages passcode
4
4
 
5
5
  require 'aspera/cli/basic_auth_plugin'
6
+ require 'aspera/cli/extended_value'
6
7
  require 'aspera/persistency_action_once'
7
8
  require 'aspera/id_generator'
8
9
  require 'aspera/nagios'
9
10
  require 'aspera/environment'
11
+ require 'aspera/assert'
10
12
  require 'securerandom'
11
- require 'ruby-progressbar'
12
13
  require 'tty-spinner'
13
14
 
14
15
  module Aspera
@@ -20,80 +21,132 @@ module Aspera
20
21
  API_DETECT = 'api/v5/configuration/ping'
21
22
  # list of supported mailbox types (to list packages)
22
23
  API_LIST_MAILBOX_TYPES = %w[inbox inbox_history inbox_all inbox_all_history outbox outbox_history pending pending_history all].freeze
23
- PACKAGE_ALL_INIT = 'INIT'
24
24
  PACKAGE_SEND_FROM_REMOTE_SOURCE = 'remote_source'
25
- ADMIN_RESOURCES = %i[accounts contacts jobs workgroups shared_inboxes nodes oauth_clients registrations saml_configs metadata_profiles
26
- email_notifications].freeze
27
- private_constant(*%i[RECIPIENT_TYPES PACKAGE_TERMINATED API_DETECT API_LIST_MAILBOX_TYPES PACKAGE_SEND_FROM_REMOTE_SOURCE])
25
+ # Faspex API v5: get transfer spec for connect
26
+ TRANSFER_CONNECT = 'connect'
27
+ ADMIN_RESOURCES = %i[
28
+ accounts contacts jobs workgroups shared_inboxes nodes oauth_clients registrations saml_configs
29
+ metadata_profiles email_notifications alternate_addresses
30
+ ].freeze
31
+ JOB_RUNNING = %w[queued working].freeze
32
+ STANDARD_PATH = '/aspera/faspex'
33
+ 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])
28
35
  class << self
29
- def detect(base_url)
30
- api = Rest.new(base_url: base_url, redirect_max: 1)
31
- result = api.read(API_DETECT)
32
- if result[:http].code.start_with?('2') && result[:http].body.strip.empty?
33
- suffix_length = -2 - API_DETECT.length
36
+ def application_name
37
+ 'Faspex'
38
+ end
39
+
40
+ def detect(address_or_url)
41
+ address_or_url = "https://#{address_or_url}" unless address_or_url.match?(%r{^[a-z]{1,6}://})
42
+ urls = [address_or_url]
43
+ urls.push("#{address_or_url}#{STANDARD_PATH}") unless address_or_url.end_with?(STANDARD_PATH)
44
+
45
+ urls.each do |base_url|
46
+ next unless base_url.start_with?('https://')
47
+ api = Rest.new(base_url: base_url, redirect_max: 1)
48
+ result = api.read(API_DETECT)
49
+ next unless result[:http].code.start_with?('2') && result[:http].body.strip.empty?
50
+ url_length = -2 - API_DETECT.length
51
+ # take redirect if any
34
52
  return {
35
53
  version: result[:http]['x-ibm-aspera'] || '5',
36
- url: result[:http].uri.to_s[0..suffix_length],
37
- name: 'Faspex 5'
54
+ url: result[:http].uri.to_s[0..url_length]
38
55
  }
56
+ rescue StandardError => e
57
+ Log.log.debug{"detect error: #{e}"}
39
58
  end
40
59
  return nil
41
60
  end
42
- end
43
61
 
44
- # Faspex API v5: get transfer spec for connect
45
- TRANSFER_CONNECT = 'connect'
62
+ def wizard(object:, private_key_path:, pub_key_pem:)
63
+ options = object.options
64
+ formatter = object.formatter
65
+ instance_url = options.get_option(:url, mandatory: true)
66
+ wiz_username = options.get_option(:username, mandatory: true)
67
+ raise "Username shall be an email in Faspex: #{wiz_username}" if !(wiz_username =~ /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i)
68
+ if options.get_option(:client_id).nil? || options.get_option(:client_secret).nil?
69
+ formatter.display_status('Ask the ascli client id and secret to your Administrator.'.red)
70
+ formatter.display_status("Admin should login to: #{instance_url}")
71
+ OpenApplication.instance.uri(instance_url)
72
+ formatter.display_status('Navigate to: 𓃑 → Admin → Configurations → API clients')
73
+ formatter.display_status('Create an API client with:')
74
+ formatter.display_status('- name: ascli')
75
+ formatter.display_status('- JWT: enabled')
76
+ formatter.display_status("Then, logged in as #{wiz_username.red} go to your profile:")
77
+ formatter.display_status('👤 → Account Settings → Preferences -> Public Key in PEM:')
78
+ formatter.display_status(pub_key_pem)
79
+ formatter.display_status('Once set, fill in the parameters:')
80
+ end
81
+ return {
82
+ preset_value: {
83
+ url: instance_url,
84
+ username: wiz_username,
85
+ auth: :jwt.to_s,
86
+ private_key: "@file:#{private_key_path}",
87
+ client_id: options.get_option(:client_id, mandatory: true),
88
+ client_secret: options.get_option(:client_secret, mandatory: true)
89
+ },
90
+ test_args: 'user profile show'
91
+ }
92
+ end
93
+
94
+ def public_link?(url)
95
+ url.include?('/public/')
96
+ end
97
+ end
46
98
 
47
99
  def initialize(env)
48
100
  super(env)
49
101
  options.declare(:client_id, 'OAuth client identifier')
50
102
  options.declare(:client_secret, 'OAuth client secret')
51
103
  options.declare(:redirect_uri, 'OAuth redirect URI for web authentication')
52
- options.declare(:auth, 'OAuth type of authentication', values: %i[boot link].concat(Oauth::STD_AUTH_TYPES), default: :jwt)
104
+ options.declare(:auth, 'OAuth type of authentication', values: %i[boot].concat(Oauth::STD_AUTH_TYPES), default: :jwt)
53
105
  options.declare(:private_key, 'OAuth JWT RSA private key PEM value (prefix file path with @file:)')
54
106
  options.declare(:passphrase, 'OAuth JWT RSA private key passphrase')
55
- options.declare(:link, 'Public link authorization (specific operations)')
56
- options.declare(:box, "Package inbox, either shared inbox name or one of #{API_LIST_MAILBOX_TYPES} or #{VAL_ALL}", default: 'inbox')
107
+ options.declare(:box, "Package inbox, either shared inbox name or one of: #{API_LIST_MAILBOX_TYPES.join(', ')} or #{ExtendedValue::ALL}", default: 'inbox')
57
108
  options.declare(:shared_folder, 'Send package with files from shared folder')
58
- options.declare(:group_type, 'Shared inbox or workgroup', values: %i[shared_inboxes workgroups], default: :shared_inboxes)
109
+ options.declare(:group_type, 'Type of shared box', values: %i[shared_inboxes workgroups], default: :shared_inboxes)
59
110
  options.parse_options!
60
111
  end
61
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"
119
+ end
120
+
62
121
  def set_api
63
- public_link = options.get_option(:link)
64
- unless public_link.nil?
65
- @faspex5_api_base_url = public_link.gsub(%r{/public/.*}, '').gsub(/\?.*/, '')
66
- options.set_option(:auth, :link)
67
- end
68
- @faspex5_api_base_url ||= options.get_option(:url, mandatory: true).gsub(%r{/+$}, '')
69
- @faspex5_api_auth_url = "#{@faspex5_api_base_url}/auth"
70
- faspex5_api_v5_url = "#{@faspex5_api_base_url}/api/v5"
71
- case options.get_option(:auth, mandatory: true)
72
- when :link
73
- uri = URI.parse(public_link)
74
- args = URI.decode_www_form(uri.query).each_with_object({}){|v, h|h[v.first] = v.last; }
75
- Log.dump(:args, args)
76
- context = args['context']
77
- raise 'missing context' if context.nil?
78
- @pub_link_context = JSON.parse(Base64.decode64(context))
79
- Log.dump(:@pub_link_context, @pub_link_context)
122
+ # get endpoint, remove unnecessary trailing slashes
123
+ @faspex5_api_base_url = options.get_option(:url, mandatory: true).gsub(%r{/+$}, '')
124
+ auth_type = self.class.public_link?(@faspex5_api_base_url) ? :public_link : options.get_option(:auth, mandatory: true)
125
+ case auth_type
126
+ when :public_link
127
+ encoded_context = Rest.decode_query(URI.parse(@faspex5_api_base_url).query)['context']
128
+ raise 'Bad faspex5 public link, missing context in query' if encoded_context.nil?
129
+ @pub_link_context = JSON.parse(Base64.decode64(encoded_context))
130
+ Log.log.trace1{Log.dump(:@pub_link_context, @pub_link_context)}
131
+ # ok, we have the additional parameters, get the base url
132
+ @faspex5_api_base_url = @faspex5_api_base_url.gsub(%r{/public/.*}, '').gsub(/\?.*/, '')
80
133
  @api_v5 = Rest.new({
81
- base_url: faspex5_api_v5_url,
134
+ base_url: api_url,
82
135
  headers: {'Passcode' => @pub_link_context['passcode']}
83
136
  })
84
137
  when :boot
85
138
  # the password here is the token copied directly from browser in developer mode
86
139
  @api_v5 = Rest.new({
87
- base_url: faspex5_api_v5_url,
140
+ base_url: api_url,
88
141
  headers: {'Authorization' => options.get_option(:password, mandatory: true)}
89
142
  })
90
143
  when :web
91
144
  # opens a browser and ask user to auth using web
92
145
  @api_v5 = Rest.new({
93
- base_url: faspex5_api_v5_url,
146
+ base_url: api_url,
94
147
  auth: {
95
148
  type: :oauth2,
96
- base_url: @faspex5_api_auth_url,
149
+ base_url: auth_api_url,
97
150
  grant_method: :web,
98
151
  client_id: options.get_option(:client_id, mandatory: true),
99
152
  web: {redirect_uri: options.get_option(:redirect_uri, mandatory: true)}
@@ -101,10 +154,10 @@ module Aspera
101
154
  when :jwt
102
155
  app_client_id = options.get_option(:client_id, mandatory: true)
103
156
  @api_v5 = Rest.new({
104
- base_url: faspex5_api_v5_url,
157
+ base_url: api_url,
105
158
  auth: {
106
159
  type: :oauth2,
107
- base_url: @faspex5_api_auth_url,
160
+ base_url: auth_api_url,
108
161
  grant_method: :jwt,
109
162
  client_id: app_client_id,
110
163
  jwt: {
@@ -117,14 +170,14 @@ module Aspera
117
170
  headers: {typ: 'JWT'}
118
171
  }
119
172
  }})
120
- else raise 'Unexpected case for option: auth'
173
+ else error_unexpected_value(auth_type)
121
174
  end
122
175
  end
123
176
 
124
177
  # if recipient is just an email, then convert to expected API hash : name and type
125
178
  def normalize_recipients(parameters)
126
179
  return unless parameters.key?('recipients')
127
- raise 'Field recipients must be an Array' unless parameters['recipients'].is_a?(Array)
180
+ assert_type(parameters['recipients'], Array){'recipients'}
128
181
  recipient_types = RECIPIENT_TYPES
129
182
  if parameters.key?('recipient_types')
130
183
  recipient_types = parameters['recipient_types']
@@ -147,52 +200,63 @@ module Aspera
147
200
 
148
201
  # wait for package status to be in provided list
149
202
  def wait_package_status(id, status_list: PACKAGE_TERMINATED)
150
- spinner = nil
151
- progress = nil
152
- while true
203
+ total_sent = false
204
+ loop do
153
205
  status = @api_v5.read("packages/#{id}/upload_details")[:data]
206
+ status['id'] = id
154
207
  # user asked to not follow
155
- break if status_list.nil? || status_list.include?(status['upload_status'])
208
+ return status if status_list.nil?
156
209
  if status['upload_status'].eql?('submitted')
157
- if spinner.nil?
158
- spinner = TTY::Spinner.new('[:spinner] :title', format: :classic)
159
- spinner.start
160
- end
161
- spinner.update(title: status['upload_status'])
162
- spinner.spin
163
- elsif progress.nil?
164
- progress = ProgressBar.create(
165
- format: '%a %B %p%% %r Mbps %e',
166
- rate_scale: lambda{|rate|rate / Environment::BYTES_PER_MEBIBIT},
167
- title: 'progress',
168
- total: status['bytes_total'].to_i)
210
+ config.progress_bar&.event(session_id: nil, type: :pre_start, info: status['upload_status'])
211
+ elsif !total_sent
212
+ config.progress_bar&.event(session_id: id, type: :session_start)
213
+ config.progress_bar&.event(session_id: id, type: :session_size, info: status['bytes_total'].to_i)
214
+ total_sent = true
169
215
  else
170
- progress.progress = status['bytes_written'].to_i
216
+ config.progress_bar&.event(session_id: id, type: :transfer, info: status['bytes_written'].to_i)
217
+ end
218
+ if status_list.include?(status['upload_status'])
219
+ # if status['upload_status'].eql?('completed')
220
+ config.progress_bar&.event(session_id: id, type: :end)
221
+ return status
222
+ # end
171
223
  end
224
+ sleep(1.0)
225
+ end
226
+ end
227
+
228
+ def wait_for_job(job_id)
229
+ spinner = nil
230
+ loop do
231
+ status = @api_v5.read("jobs/#{job_id}", {type: :formatted})[:data]
232
+ return status unless JOB_RUNNING.include?(status['status'])
233
+ if spinner.nil?
234
+ spinner = TTY::Spinner.new('[:spinner] :title', format: :classic)
235
+ spinner.start
236
+ end
237
+ spinner.update(title: status['status'])
238
+ spinner.spin
172
239
  sleep(0.5)
173
240
  end
174
- status['id'] = id
175
- return status
241
+ error_unreachable_line
176
242
  end
177
243
 
178
- # get a (full or partial) list of all entities of a given type
244
+ # Get a (full or partial) list of all entities of a given type
179
245
  # @param type [String] the type of entity to list (just a name)
180
246
  # @param query [Hash,nil] additional query parameters
181
- # @param path [String] optional prefix to add to the path (nil or empty string: no prefix)
247
+ # @param real_path [String] real path if it's n ot just the type
182
248
  # @param item_list_key [String] key in the result to get the list of items
183
- def list_entities(type:, path: nil, query: nil, item_list_key: nil)
184
- query = {} if query.nil?
249
+ def list_entities(type:, real_path: nil, query: {}, item_list_key: nil)
185
250
  type = type.to_s if type.is_a?(Symbol)
251
+ assert_type(type, String)
186
252
  item_list_key = type if item_list_key.nil?
187
- raise "internal error: Invalid type #{type.class}" unless type.is_a?(String)
188
- full_path = type
189
- full_path = "#{path}/#{full_path}" unless path.nil? || path.empty?
253
+ full_path = real_path.nil? ? type : real_path
190
254
  result = []
191
255
  offset = 0
192
256
  max_items = query.delete(MAX_ITEMS)
193
257
  remain_pages = query.delete(MAX_PAGES)
194
258
  # merge default parameters, by default 100 per page
195
- query = {'limit'=> 100}.merge(query)
259
+ query = {'limit'=> PER_PAGE_DEFAULT}.merge(query)
196
260
  loop do
197
261
  query['offset'] = offset
198
262
  page_result = @api_v5.read(full_path, query)[:data]
@@ -211,36 +275,39 @@ module Aspera
211
275
  end
212
276
 
213
277
  # lookup an entity id from its name
214
- def lookup_entity_by_field(type:, value:, field: 'name', query: :default, path: nil, item_list_key: nil)
215
- query = {'q'=> value} if query.eql?(:default)
216
- found = list_entities(type: type, path: path, query: query, item_list_key: item_list_key).select{|i|i[field].eql?(value)}
278
+ def lookup_entity_by_field(type:, value:, field: 'name', query: :default, real_path: nil, item_list_key: nil)
279
+ if query.eql?(:default)
280
+ assert(field.eql?('name')){'Default query is on name only'}
281
+ query = {'q'=> value}
282
+ end
283
+ found = list_entities(type: type, real_path: real_path, query: query, item_list_key: item_list_key).select{|i|i[field].eql?(value)}
217
284
  case found.length
218
285
  when 0 then raise "No #{type} with #{field} = #{value}"
219
286
  when 1 then return found.first
220
- else raise "Found #{found.length} #{path} with #{field} = #{value}"
287
+ else raise "Found #{found.length} #{real_path} with #{field} = #{value}"
221
288
  end
222
289
  end
223
290
 
224
291
  # list all packages with optional filter
225
- def list_packages_with_filter
292
+ def list_packages_with_filter(query: {})
226
293
  filter = options.get_next_argument('filter', mandatory: false, type: Proc, default: ->(_x){true})
227
294
  # translate box name to API prefix (with ending slash)
228
295
  box = options.get_option(:box)
229
- api_path =
296
+ real_path =
230
297
  case box
231
- when VAL_ALL then '' # only admin can list all packages globally
232
- when *API_LIST_MAILBOX_TYPES then box
298
+ when ExtendedValue::ALL then 'packages' # only admin can list all packages globally
299
+ when *API_LIST_MAILBOX_TYPES then "#{box}/packages"
233
300
  else
234
301
  group_type = options.get_option(:group_type)
235
- "#{group_type}/#{lookup_entity_by_field(type: group_type, value: box)['id']}"
302
+ "#{group_type}/#{lookup_entity_by_field(type: group_type, value: box)['id']}/packages"
236
303
  end
237
304
  return list_entities(
238
305
  type: 'packages',
239
- query: query_read_delete(default: {}),
240
- path: api_path).select(&filter)
306
+ query: query_read_delete(default: query),
307
+ real_path: real_path).select(&filter)
241
308
  end
242
309
 
243
- def package_receive
310
+ def package_receive(package_ids)
244
311
  # prepare persistency if needed
245
312
  skip_ids_persistency = nil
246
313
  if options.get_option(:once_only, mandatory: true)
@@ -251,52 +318,53 @@ module Aspera
251
318
  id: IdGenerator.from_list([
252
319
  'faspex_recv',
253
320
  options.get_option(:url, mandatory: true),
254
- options.get_option(:username, mandatory: true)]))
321
+ options.get_option(:username, mandatory: true),
322
+ options.get_option(:box, mandatory: true)
323
+ ]))
255
324
  end
256
- package_ids =
257
- if @pub_link_context&.key?('package_id')
258
- @pub_link_context['package_id']
259
- else
260
- # one or several packages
261
- instance_identifier
262
- end
325
+ packages = []
263
326
  case package_ids
264
- when PACKAGE_ALL_INIT
265
- raise 'Only with option once_only' unless skip_ids_persistency
327
+ when ExtendedValue::INIT
328
+ assert(skip_ids_persistency){'Only with option once_only'}
266
329
  skip_ids_persistency.data.clear.concat(list_packages_with_filter.map{|p|p['id']})
267
330
  skip_ids_persistency.save
268
331
  return Main.result_status("Initialized skip for #{skip_ids_persistency.data.count} package(s)")
269
- when VAL_ALL
332
+ when ExtendedValue::ALL
270
333
  # TODO: if packages have same name, they will overwrite ?
271
- package_ids = list_packages_with_filter.map{|p|p['id']}
272
- Log.dump(:package_ids, package_ids)
273
- Log.dump(:package_ids, skip_ids_persistency.data)
274
- package_ids.reject!{|i|skip_ids_persistency.data.include?(i)} if skip_ids_persistency
275
- Log.dump(:package_ids, package_ids)
334
+ packages = list_packages_with_filter(query: {'status' => 'completed'})
335
+ Log.log.trace1{Log.dump(:package_ids, packages.map{|p|p['id']})}
336
+ Log.log.trace1{Log.dump(:skip_ids, skip_ids_persistency.data)}
337
+ packages.reject!{|p|skip_ids_persistency.data.include?(p['id'])} if skip_ids_persistency
338
+ Log.log.trace1{Log.dump(:package_ids, packages.map{|p|p['id']})}
339
+ else
340
+ # a single id was provided, or a list of ids
341
+ 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'}
344
+ # packages = package_ids.map{|pkg_id|@api_v5.read("packages/#{pkg_id}")[:data]}
345
+ packages = package_ids.map{|pkg_id|{'id'=>pkg_id}}
276
346
  end
277
- # a single id was provided
278
- # TODO: check package_ids is a list of strings
279
- package_ids = [package_ids] if package_ids.is_a?(String)
280
347
  result_transfer = []
281
- package_ids.each do |pkg_id|
348
+ param_file_list = {}
349
+ begin
350
+ param_file_list['paths'] = transfer.source_list.map{|source|{'path'=>source}}
351
+ rescue Cli::BadArgument
352
+ # paths is optional
353
+ end
354
+ download_params = {
355
+ type: 'received',
356
+ transfer_type: TRANSFER_CONNECT
357
+ }
358
+ box = options.get_option(:box)
359
+ case box
360
+ when /outbox/ then download_params[:type] = 'sent'
361
+ when *API_LIST_MAILBOX_TYPES then nil # nothing to do
362
+ else # shared inbox / workgroup
363
+ download_params[:recipient_workgroup_id] = lookup_entity_by_field(type: options.get_option(:group_type), value: box)['id']
364
+ end
365
+ packages.each do |package|
366
+ pkg_id = package['id']
282
367
  formatter.display_status("Receiving package #{pkg_id}")
283
- param_file_list = {}
284
- begin
285
- param_file_list['paths'] = transfer.source_list.map{|source|{'path'=>source}}
286
- rescue Aspera::Cli::CliBadArgument
287
- # paths is optional
288
- end
289
- download_params = {
290
- type: 'received',
291
- transfer_type: TRANSFER_CONNECT
292
- }
293
- box = options.get_option(:box)
294
- case box
295
- when /outbox/ then download_params[:type] = 'sent'
296
- when *API_LIST_MAILBOX_TYPES then nil # nothing to do
297
- else # shared inbox / workgroup
298
- download_params[:recipient_workgroup_id] = lookup_entity_by_field(type: options.get_option(:group_type), value: box)['id']
299
- end
300
368
  # TODO: allow from sent as well ?
301
369
  transfer_spec = @api_v5.call(
302
370
  operation: 'POST',
@@ -319,21 +387,15 @@ module Aspera
319
387
  end
320
388
 
321
389
  def package_action
322
- command = options.get_next_command(%i[list show browse status delete send receive])
390
+ command = options.get_next_command(%i[show browse status delete receive send list])
391
+ package_id =
392
+ if %i[receive show browse status delete].include?(command)
393
+ @pub_link_context&.key?('package_id') ? @pub_link_context['package_id'] : instance_identifier
394
+ end
323
395
  case command
324
- when :list
325
- return {
326
- type: :object_list,
327
- data: list_packages_with_filter,
328
- fields: %w[id title release_date total_bytes total_files created_time state]
329
- }
330
396
  when :show
331
- id = @pub_link_context['package_id'] if @pub_link_context&.key?('package_id')
332
- id ||= instance_identifier
333
- return {type: :single_object, data: @api_v5.read("packages/#{id}")[:data]}
397
+ return {type: :single_object, data: @api_v5.read("packages/#{package_id}")[:data]}
334
398
  when :browse
335
- id = @pub_link_context['package_id'] if @pub_link_context&.key?('package_id')
336
- id ||= instance_identifier
337
399
  path = options.get_next_argument('path', expected: :single, mandatory: false) || '/'
338
400
  # TODO: support multi-page listing ?
339
401
  params = {
@@ -343,29 +405,32 @@ module Aspera
343
405
  }
344
406
  result = @api_v5.call({
345
407
  operation: 'POST',
346
- subpath: "packages/#{id}/files/received",
408
+ subpath: "packages/#{package_id}/files/received",
347
409
  headers: {'Accept' => 'application/json'},
348
410
  url_params: params,
349
411
  json_params: {'path' => path, 'filters' => {'basenames'=>[]}}})[:data]
350
412
  formatter.display_item_count(result['item_count'], result['total_count'])
351
413
  return {type: :object_list, data: result['items']}
352
414
  when :status
353
- status = wait_package_status(instance_identifier, status_list: nil)
415
+ status = wait_package_status(package_id, status_list: nil)
354
416
  return {type: :single_object, data: status}
355
417
  when :delete
356
- ids = instance_identifier
418
+ ids = package_id
357
419
  ids = [ids] unless ids.is_a?(Array)
358
- raise 'Package identifier must be a single id or an Array' unless ids.is_a?(Array) && ids.all?(String)
420
+ assert_type(ids, Array){'Package identifier'}
421
+ assert(ids.all?(String)){'Package id shall be String'}
359
422
  # API returns 204, empty on success
360
423
  @api_v5.call({operation: 'DELETE', subpath: 'packages', headers: {'Accept' => 'application/json'}, json_params: {ids: ids}})
361
424
  return Main.result_status('Package(s) deleted')
425
+ when :receive
426
+ return package_receive(package_id)
362
427
  when :send
363
- parameters = value_create_modify(command: command, type: Hash)
428
+ parameters = value_create_modify(command: command)
364
429
  normalize_recipients(parameters)
365
430
  package = @api_v5.create('packages', parameters)[:data]
366
431
  shared_folder = options.get_option(:shared_folder)
367
432
  if shared_folder.nil?
368
- # TODO: option to send from remote source or httpgw
433
+ # send from local files
369
434
  transfer_spec = @api_v5.call(
370
435
  operation: 'POST',
371
436
  subpath: "packages/#{package['id']}/transfer_spec/upload",
@@ -377,6 +442,7 @@ module Aspera
377
442
  transfer_spec.delete('authentication')
378
443
  return Main.result_transfer(transfer.start(transfer_spec))
379
444
  else
445
+ # send from remote shared folder
380
446
  if (m = shared_folder.match(REGEX_LOOKUP_ID_BY_FIELD))
381
447
  shared_folder = lookup_entity_by_field(type: 'shared_folders', value: m[2])['id']
382
448
  end
@@ -390,8 +456,12 @@ module Aspera
390
456
  end
391
457
  return {type: :single_object, data: result}
392
458
  end
393
- when :receive
394
- return package_receive
459
+ when :list
460
+ return {
461
+ type: :object_list,
462
+ data: list_packages_with_filter,
463
+ fields: %w[id title release_date total_bytes total_files created_time state]
464
+ }
395
465
  end # case package
396
466
  end
397
467
 
@@ -465,26 +535,44 @@ module Aspera
465
535
  id_as_arg = false
466
536
  display_fields = nil
467
537
  adm_api = @api_v5
538
+ special_query = :default
468
539
  available_commands = [].concat(Plugin::ALL_OPS)
469
540
  case res_type
470
541
  when :metadata_profiles
471
542
  res_path = 'configuration/metadata_profiles'
472
543
  list_key = 'profiles'
544
+ when :alternate_addresses
545
+ res_path = 'configuration/alternate_addresses'
473
546
  when :email_notifications
474
547
  list_key = false
475
548
  id_as_arg = 'type'
476
549
  when :accounts
477
- display_fields = [:all_but, 'user_profile_data_attributes']
550
+ display_fields = Formatter.all_but('user_profile_data_attributes')
478
551
  when :oauth_clients
479
- display_fields = [:all_but, 'public_key']
480
- adm_api = Rest.new(@api_v5.params.merge({base_url: @faspex5_api_auth_url}))
552
+ display_fields = Formatter.all_but('public_key')
553
+ adm_api = Rest.new(@api_v5.params.merge({base_url: auth_api_url}))
481
554
  when :shared_inboxes, :workgroups
482
555
  available_commands.push(:members, :saml_groups, :invite_external_collaborator)
556
+ special_query = {'all': true}
557
+ when :nodes
558
+ available_commands.push(:shared_folders)
483
559
  end
484
560
  res_command = options.get_next_command(available_commands)
485
561
  case res_command
486
562
  when *Plugin::ALL_OPS
487
- return entity_command(res_command, adm_api, res_path, item_list_key: list_key, display_fields: display_fields, id_as_arg: id_as_arg)
563
+ return entity_command(res_command, adm_api, res_path, item_list_key: list_key, display_fields: display_fields, id_as_arg: id_as_arg) do |field, value|
564
+ lookup_entity_by_field(
565
+ type: res_type, real_path: res_path, field: field, value: value, query: special_query)['id']
566
+ end
567
+ when :shared_folders
568
+ node_id = instance_identifier do |field, value|
569
+ lookup_entity_by_field(type: res_type.to_s, field: field, value: value)['id']
570
+ end
571
+ sh_path = "#{res_path}/#{node_id}/shared_folders"
572
+ return entity_action(adm_api, sh_path, item_list_key: 'shared_folders') do |field, value|
573
+ lookup_entity_by_field(
574
+ type: 'shared_folders', real_path: sh_path, field: field, value: value)['id']
575
+ end
488
576
  when :invite_external_collaborator
489
577
  shared_inbox_id = instance_identifier { |field, value| lookup_entity_by_field(type: res_type.to_s, field: field, value: value)['id']}
490
578
  creation_payload = value_create_modify(command: res_command, type: [Hash, String])
@@ -492,7 +580,9 @@ module Aspera
492
580
  res_path = "#{res_type}/#{shared_inbox_id}/external_collaborator"
493
581
  result = adm_api.create(res_path, creation_payload)[:data]
494
582
  formatter.display_status(result['message'])
495
- result = lookup_entity_by_field(type: 'members', path: "#{res_type}/#{shared_inbox_id}", value: creation_payload['email_address'], query: {})
583
+ result = lookup_entity_by_field(
584
+ type: 'members', real_path: "#{res_type}/#{shared_inbox_id}/members", value: creation_payload['email_address'],
585
+ query: {})
496
586
  return {type: :single_object, data: result}
497
587
  when :members, :saml_groups
498
588
  res_id = instance_identifier { |field, value| lookup_entity_by_field(type: res_type.to_s, field: field, value: value)['id']}
@@ -533,20 +623,23 @@ module Aspera
533
623
  when :show
534
624
  return { type: :single_object, data: @api_v5.read(smtp_path)[:data] }
535
625
  when :create
536
- return { type: :single_object, data: @api_v5.create(smtp_path, value_create_modify(command: smtp_cmd, type: Hash))[:data] }
626
+ return { type: :single_object, data: @api_v5.create(smtp_path, value_create_modify(command: smtp_cmd))[:data] }
537
627
  when :modify
538
- return { type: :single_object, data: @api_v5.modify(smtp_path, value_create_modify(command: smtp_cmd, type: Hash))[:data] }
628
+ return { type: :single_object, data: @api_v5.update(smtp_path, value_create_modify(command: smtp_cmd))[:data] }
539
629
  when :delete
540
630
  return { type: :single_object, data: @api_v5.delete(smtp_path)[:data] }
541
631
  when :test
542
632
  test_data = options.get_next_argument('Email or test data, see API')
543
633
  test_data = {test_email_recipient: test_data} if test_data.is_a?(String)
544
- return { type: :single_object, data: @api_v5.create(File.join(smtp_path, 'test'), test_data)[:data] }
634
+ creation = @api_v5.create(File.join(smtp_path, 'test'), test_data)[:data]
635
+ result = wait_for_job(creation['job_id'])
636
+ result['serialized_args'] = JSON.parse(result['serialized_args']) rescue result['serialized_args']
637
+ return { type: :single_object, data: result }
545
638
  end
546
639
  end
547
640
  when :gateway
548
641
  require 'aspera/faspex_gw'
549
- url = value_create_modify(type: String)
642
+ url = value_create_modify(command: command, description: 'listening url (e.g. https://localhost:12345)', type: String)
550
643
  uri = URI.parse(url)
551
644
  server = WebServerSimple.new(uri)
552
645
  server.mount(uri.path, Faspex4GWServlet, @api_v5, nil)
@@ -559,9 +652,9 @@ module Aspera
559
652
  return Main.result_status('Gateway terminated')
560
653
  when :postprocessing
561
654
  require 'aspera/faspex_postproc' # cspell:disable-line
562
- parameters = value_create_modify(type: Hash)
655
+ parameters = value_create_modify(command: command)
563
656
  parameters = parameters.symbolize_keys
564
- raise 'Missing key: url' unless parameters.key?(:url)
657
+ assert(parameters.key?(:url)){'Missing key: url'}
565
658
  uri = URI.parse(parameters[:url])
566
659
  parameters[:processing] ||= {}
567
660
  parameters[:processing][:root] = uri.path
@@ -576,36 +669,6 @@ module Aspera
576
669
  return Main.result_status('Gateway terminated')
577
670
  end # case command
578
671
  end # action
579
-
580
- def wizard(params)
581
- if params[:prepare]
582
- # if not defined by user, generate unique name
583
- params[:preset_name] ||= [params[:plugin_sym]].concat(URI.parse(params[:instance_url]).host.gsub(/[^a-z0-9.]/, '').split('.')).join('_')
584
- params[:need_private_key] = true
585
- return
586
- end
587
- formatter.display_status('Ask the ascli client id and secret to your Administrator, or ask them to go to:'.red)
588
- OpenApplication.instance.uri(params[:instance_url])
589
- formatter.display_status('Then: 𓃑 → Admin → Configurations → API clients')
590
- formatter.display_status('Create an API client with:')
591
- formatter.display_status('- name: ascli')
592
- formatter.display_status('- JWT: enabled')
593
- formatter.display_status('Then, logged in as user go to your profile:')
594
- formatter.display_status('👤 → Account Settings → Preferences -> Public Key in PEM:')
595
- formatter.display_status(params[:pub_key_pem])
596
- formatter.display_status('Once set, fill in the parameters:')
597
- return {
598
- preset_value: {
599
- url: params[:instance_url],
600
- username: options.get_option(:username, mandatory: true),
601
- auth: :jwt.to_s,
602
- private_key: '@file:' + params[:private_key_path],
603
- client_id: options.get_option(:client_id, mandatory: true),
604
- client_secret: options.get_option(:client_secret, mandatory: true)
605
- },
606
- test_args: "#{params[:plugin_sym]} user profile show"
607
- }
608
- end
609
672
  end # Faspex5
610
673
  end # Plugins
611
674
  end # Cli