aspera-cli 4.13.0 → 4.15.0

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