aspera-cli 4.13.0 → 4.15.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 (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