aspera-cli 4.14.0 → 4.16.0

Sign up to get free protection for your applications and to get access to all the features.
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