aspera-cli 4.27.5 → 4.28.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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +160 -4
- data/CONTRIBUTING.md +4 -2
- data/README.md +1 -1
- data/bin/asession +1 -1
- data/docs/README.md +1086 -331
- data/docs/test-mcp-with-ai.md +5 -3
- data/lib/aspera/agent/direct.rb +15 -4
- data/lib/aspera/agent/factory.rb +2 -1
- data/lib/aspera/agent/node.rb +1 -1
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/faspex.rb +1 -1
- data/lib/aspera/api/node.rb +6 -2
- data/lib/aspera/ascp/installation.rb +47 -11
- data/lib/aspera/ascp/management.rb +89 -43
- data/lib/aspera/cli/ascp_actions.rb +5 -19
- data/lib/aspera/cli/bootstrapper.rb +1 -2
- data/lib/aspera/cli/command_registry.rb +43 -3
- data/lib/aspera/cli/command_spec.rb +3 -0
- data/lib/aspera/cli/completion/ascli.bash +39 -5
- data/lib/aspera/cli/completion/ascli.fish +16 -8
- data/lib/aspera/cli/completion/ascli.ps1 +41 -0
- data/lib/aspera/cli/completion/ascli.zsh +20 -4
- data/lib/aspera/cli/completion_actions.rb +360 -0
- data/lib/aspera/cli/error.rb +42 -0
- data/lib/aspera/cli/extended_value.rb +30 -5
- data/lib/aspera/cli/formatter.rb +118 -56
- data/lib/aspera/cli/hints.rb +0 -1
- data/lib/aspera/cli/http.rb +6 -5
- data/lib/aspera/cli/info.rb +10 -4
- data/lib/aspera/cli/mailer.rb +1 -1
- data/lib/aspera/cli/mcp/http_servlet.rb +156 -0
- data/lib/aspera/cli/mcp/secrets.rb +143 -0
- data/lib/aspera/cli/mcp/tool.rb +231 -0
- data/lib/aspera/cli/option_value.rb +9 -1
- data/lib/aspera/cli/options.schema.yaml +390 -39
- data/lib/aspera/cli/parser.rb +14 -5
- data/lib/aspera/cli/plugins/aoc.rb +8 -11
- data/lib/aspera/cli/plugins/base.rb +36 -15
- data/lib/aspera/cli/plugins/config.rb +15 -44
- data/lib/aspera/cli/plugins/console.rb +3 -1
- data/lib/aspera/cli/plugins/factory.rb +1 -1
- data/lib/aspera/cli/plugins/faspex5.rb +9 -19
- data/lib/aspera/cli/plugins/faspio.rb +36 -5
- data/lib/aspera/cli/plugins/httpgw.rb +1 -0
- data/lib/aspera/cli/plugins/mcp.rb +177 -118
- data/lib/aspera/cli/plugins/node.rb +14 -8
- data/lib/aspera/cli/plugins/orchestrator.rb +351 -38
- data/lib/aspera/cli/plugins/server.rb +3 -1
- data/lib/aspera/cli/plugins/shares.rb +205 -35
- data/lib/aspera/cli/preset_actions.rb +15 -7
- data/lib/aspera/cli/preset_manager.rb +24 -2
- data/lib/aspera/cli/prompt.rb +3 -1
- data/lib/aspera/cli/result.rb +17 -4
- data/lib/aspera/cli/runner.rb +133 -43
- data/lib/aspera/cli/security.rb +134 -0
- data/lib/aspera/cli/table_fitter.rb +116 -0
- data/lib/aspera/cli/terminal_formatter.rb +23 -34
- data/lib/aspera/cli/transfer_agent.rb +4 -1
- data/lib/aspera/cli/vault_manager.rb +6 -5
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/web_secret.html.erb +61 -0
- data/lib/aspera/cli/web_secret.rb +220 -0
- data/lib/aspera/cli/wizard.rb +19 -9
- data/lib/aspera/command_line_builder.rb +26 -0
- data/lib/aspera/data_repository.rb +38 -24
- data/lib/aspera/duration.rb +42 -0
- data/lib/aspera/environment.rb +88 -24
- data/lib/aspera/exec_spec.rb +6 -5
- data/lib/aspera/faspex_gw.rb +98 -32
- data/lib/aspera/faspex_postproc.rb +187 -44
- data/lib/aspera/formatter_interface.rb +0 -1
- data/lib/aspera/id_generator.rb +2 -1
- data/lib/aspera/json_rpc/client.rb +51 -32
- data/lib/aspera/json_rpc/version.rb +1 -0
- data/lib/aspera/keychain/base.rb +5 -0
- data/lib/aspera/keychain/encrypted_hash.rb +6 -2
- data/lib/aspera/keychain/factory.rb +2 -3
- data/lib/aspera/keychain/hashicorp_vault.rb +7 -4
- data/lib/aspera/keychain/macos_security.rb +119 -13
- data/lib/aspera/keychain/one_password_api.rb +8 -3
- data/lib/aspera/keychain/one_password_base.rb +14 -9
- data/lib/aspera/keychain/one_password_cli.rb +4 -2
- data/lib/aspera/log.rb +3 -6
- data/lib/aspera/markdown.rb +152 -115
- data/lib/aspera/mime.rb +6 -0
- data/lib/aspera/nagios.rb +3 -1
- data/lib/aspera/node_emulator.rb +98 -101
- data/lib/aspera/oauth/{base.rb → client/base.rb} +5 -4
- data/lib/aspera/oauth/{boot.rb → client/boot.rb} +3 -2
- data/lib/aspera/oauth/{factory.rb → client/factory.rb} +7 -0
- data/lib/aspera/oauth/{generic.rb → client/generic.rb} +1 -1
- data/lib/aspera/oauth/{json_credentials.rb → client/json_credentials.rb} +1 -1
- data/lib/aspera/oauth/{jwt.rb → client/jwt.rb} +1 -1
- data/lib/aspera/oauth/{url_json.rb → client/url_json.rb} +1 -1
- data/lib/aspera/oauth/{web.rb → client/web.rb} +2 -2
- data/lib/aspera/oauth/server/authorize.html.erb +34 -0
- data/lib/aspera/oauth/server/pkce.rb +426 -0
- data/lib/aspera/orchestrator_workflow_checker.rb +561 -0
- data/lib/aspera/preview/terminal.rb +3 -2
- data/lib/aspera/rainbow.rb +27 -1
- data/lib/aspera/rest/aspera_errors.rb +51 -59
- data/lib/aspera/rest/client.rb +21 -14
- data/lib/aspera/rest/error_analyzer.rb +9 -0
- data/lib/aspera/rest/list.rb +6 -6
- data/lib/aspera/rest/util.rb +7 -1
- data/lib/aspera/rest.rb +0 -1
- data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +0 -2
- data/lib/aspera/schema/IBM Aspera Orchestrator API-v1-enhanced.yaml +4001 -0
- data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0-enhanced.yaml +1097 -0
- data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +35 -35
- data/lib/aspera/schema/IBM_Aspera_Shares.yaml +819 -7
- data/lib/aspera/schema/documentation.rb +50 -78
- data/lib/aspera/schema/reader.rb +39 -11
- data/lib/aspera/schema/registry.rb +8 -2
- data/lib/aspera/secret_hider.rb +94 -15
- data/lib/aspera/secret_strength.rb +114 -0
- data/lib/aspera/servlet_helpers.rb +111 -0
- data/lib/aspera/sync/args.schema.yaml +3 -1
- data/lib/aspera/sync/conf.schema.yaml +16 -6
- data/lib/aspera/sync/operations.rb +21 -1
- data/lib/aspera/temp_file_manager.rb +79 -20
- data/lib/aspera/transfer/parameters.rb +5 -0
- data/lib/aspera/transfer/spec.rb +52 -12
- data/lib/aspera/transfer/spec.schema.yaml +8 -5
- data/lib/aspera/web_auth/index.html.erb +16 -0
- data/lib/aspera/web_auth/server.rb +18 -10
- data/lib/aspera/web_page.html.erb +262 -0
- data/lib/aspera/web_page.rb +61 -0
- data/lib/aspera/web_server_simple.rb +76 -10
- data.tar.gz.sig +0 -0
- metadata +47 -32
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/mcp_tool.rb +0 -189
- data/lib/aspera/oauth.rb +0 -9
- data/lib/aspera/schema/IBM Aspera Orchestrator API-v1.yaml +0 -1784
- data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +0 -249
- data/lib/aspera/web_auth/index.html +0 -147
data/lib/aspera/cli/runner.rb
CHANGED
|
@@ -4,24 +4,29 @@ require 'aspera/cli/command_spec'
|
|
|
4
4
|
require 'aspera/cli/context'
|
|
5
5
|
require 'aspera/cli/parser'
|
|
6
6
|
require 'aspera/cli/formatter'
|
|
7
|
+
require 'aspera/cli/table_fitter'
|
|
7
8
|
require 'aspera/cli/plugins/factory'
|
|
8
9
|
require 'aspera/cli/bootstrapper'
|
|
10
|
+
require 'aspera/rest/aspera_errors' # registers error handlers of Aspera REST APIs
|
|
9
11
|
require 'aspera/cli/plugins/config'
|
|
10
12
|
require 'aspera/cli/mailer'
|
|
11
13
|
require 'aspera/cli/secret_finder'
|
|
12
14
|
require 'aspera/cli/extended_value'
|
|
15
|
+
require 'aspera/cli/security'
|
|
13
16
|
require 'aspera/cli/transfer_agent'
|
|
14
17
|
require 'aspera/cli/version'
|
|
15
18
|
require 'aspera/cli/info'
|
|
16
19
|
require 'aspera/cli/hints'
|
|
17
20
|
require 'aspera/cli/result'
|
|
18
21
|
require 'aspera/transfer/result'
|
|
22
|
+
require 'aspera/duration'
|
|
19
23
|
require 'aspera/secret_hider'
|
|
20
24
|
require 'aspera/log'
|
|
21
25
|
require 'aspera/assert'
|
|
22
26
|
require 'aspera/schema/documentation'
|
|
23
27
|
require 'aspera/schema/registry'
|
|
24
28
|
require 'net/ssh/errors'
|
|
29
|
+
require 'net/protocol'
|
|
25
30
|
require 'openssl'
|
|
26
31
|
|
|
27
32
|
module Aspera
|
|
@@ -49,6 +54,17 @@ module Aspera
|
|
|
49
54
|
end
|
|
50
55
|
end
|
|
51
56
|
|
|
57
|
+
# Result of a command that creates an object and transfers its files, e.g. `packages send`.
|
|
58
|
+
# @param object [Hash] Created object
|
|
59
|
+
# @param transfer_result [Transfer::Result] Typed result from TransferAgent#start
|
|
60
|
+
# @raise [StandardError] if the transfer failed
|
|
61
|
+
# @return [Result::SingleObject] The object, with field `transfer` (`job_id`) if the transfer is asynchronous
|
|
62
|
+
def result_object_transfer(object, transfer_result)
|
|
63
|
+
status = result_transfer(transfer_result)
|
|
64
|
+
object = object.merge('transfer' => status.data) if status.is_a?(Result::SingleObject)
|
|
65
|
+
Result::SingleObject.new(object)
|
|
66
|
+
end
|
|
67
|
+
|
|
52
68
|
# Used when one command executes several transfer jobs (each job being possibly multi session)
|
|
53
69
|
# @param status_table [Array] [{STATUS_FIELD=>Transfer::Result,...},...]
|
|
54
70
|
# @return [Result] a status object suitable as command result
|
|
@@ -94,7 +110,8 @@ module Aspera
|
|
|
94
110
|
Plugins::Factory.instance.add_plugins_from_lookup_folders
|
|
95
111
|
# Help requested without command? Show global options + plugin list
|
|
96
112
|
return result_usage if @option_help && @context.options.command_or_arg_empty?
|
|
97
|
-
|
|
113
|
+
# Shell completion is executed on each `Tab`: no network access
|
|
114
|
+
@context.config.periodic_check_newer_gem_version unless @context.options.next_arguments?(COMPLETION_COMMAND)
|
|
98
115
|
command_sym =
|
|
99
116
|
if @option_show_config && @context.options.command_or_arg_empty?
|
|
100
117
|
COMMAND_CONFIG
|
|
@@ -158,18 +175,18 @@ module Aspera
|
|
|
158
175
|
begin
|
|
159
176
|
result = run_with_result
|
|
160
177
|
@context.formatter.display_results(result) if result
|
|
161
|
-
rescue Net::SSH::AuthenticationFailed => e;
|
|
162
|
-
rescue OpenSSL::SSL::SSLError => e;
|
|
163
|
-
rescue Cli::BadArgument => e;
|
|
164
|
-
rescue Cli::MissingArgument => e;
|
|
165
|
-
rescue Cli::BadIdentifier => e;
|
|
166
|
-
rescue Cli::SchemaRequest => e;
|
|
167
|
-
rescue Cli::Error => e;
|
|
168
|
-
rescue Transfer::Error => e;
|
|
169
|
-
rescue Rest::CallError => e;
|
|
170
|
-
rescue
|
|
171
|
-
rescue StandardError => e;
|
|
172
|
-
rescue Interrupt => e;
|
|
178
|
+
rescue Net::SSH::AuthenticationFailed => e; exception_info = {e: e, t: 'SSH', security: true, exit: ExitCode::AUTH}
|
|
179
|
+
rescue OpenSSL::SSL::SSLError => e; exception_info = {e: e, t: 'SSL', exit: ExitCode::NETWORK}
|
|
180
|
+
rescue Cli::BadArgument => e; exception_info = {e: e, t: 'Argument', usage: true, exit: ExitCode::USAGE}
|
|
181
|
+
rescue Cli::MissingArgument => e; exception_info = {e: e, t: 'Missing', exit: ExitCode::USAGE}
|
|
182
|
+
rescue Cli::BadIdentifier, EntityNotFound => e; exception_info = {e: e, t: 'Identifier', exit: ExitCode::NOT_FOUND}
|
|
183
|
+
rescue Cli::SchemaRequest => e; exception_info = {e: e, t: 'Schema', exit: ExitCode::SUCCESS}
|
|
184
|
+
rescue Cli::Error => e; exception_info = {e: e, t: 'Tool', usage: true, exit: ExitCode::ERROR}
|
|
185
|
+
rescue Transfer::Error => e; exception_info = {e: e, t: 'Transfer', exit: ExitCode::TRANSFER}
|
|
186
|
+
rescue Rest::CallError => e; exception_info = {e: e, t: 'Rest', exit: ExitCode.for_call_error(e)}
|
|
187
|
+
rescue *NETWORK_ERRORS => e; exception_info = {e: e, t: 'Network', exit: ExitCode::NETWORK}
|
|
188
|
+
rescue StandardError => e; exception_info = {e: e, t: "Other(#{e.class.name})", debug: true, exit: ExitCode::ERROR}
|
|
189
|
+
rescue Interrupt => e; exception_info = {e: e, t: 'Interruption', debug: true, exit: ExitCode::INTERRUPTED}
|
|
173
190
|
end
|
|
174
191
|
# 1- processing of error condition
|
|
175
192
|
unless exception_info.nil?
|
|
@@ -180,35 +197,28 @@ module Aspera
|
|
|
180
197
|
Hints.hint_for(exception_info[:e], @context.formatter)
|
|
181
198
|
if exception_info[:e].is_a?(Cli::SchemaRequest)
|
|
182
199
|
Log.log.info { "#{exception_info[:t]}: #{exception_info[:e].message}" }
|
|
183
|
-
|
|
184
|
-
if schema_path.nil?
|
|
185
|
-
Log.log.warn { 'Sorry, no schema provided yet. Please refer to the manual or API.' }
|
|
186
|
-
else
|
|
187
|
-
reader = Schema::Registry.instance.reader(schema_path)
|
|
188
|
-
rows = reader.to_rows.map do |row|
|
|
189
|
-
if row.key?('enum')
|
|
190
|
-
row = row.dup
|
|
191
|
-
row['type'] = 'enum'
|
|
192
|
-
row['description'] = "#{row['description']}\nAllowed: #{row['enum'].join(', ')}"
|
|
193
|
-
end
|
|
194
|
-
row
|
|
195
|
-
end
|
|
196
|
-
@context.formatter.display_results(Result::ObjectList.new(rows, fields: %w[name type required description]))
|
|
197
|
-
end
|
|
200
|
+
display_schema(exception_info[:e].path)
|
|
198
201
|
end
|
|
199
202
|
end
|
|
200
203
|
# 2- processing of unprocessed arguments (skip when help was displayed: sub-commands are not consumed)
|
|
201
204
|
unless @option_help
|
|
202
205
|
@context.options&.final_errors&.each do |msg|
|
|
203
206
|
Log.log.error { "Argument: #{msg}" }
|
|
204
|
-
exception_info = {e: Exception.new(msg), t: 'UnusedArg'} if exception_info.nil?
|
|
207
|
+
exception_info = {e: Exception.new(msg), t: 'UnusedArg', exit: ExitCode::USAGE} if exception_info.nil?
|
|
205
208
|
end
|
|
206
209
|
end
|
|
207
210
|
# 3- exit on error
|
|
208
211
|
unless exception_info.nil?
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
+
if Log.log.debug?
|
|
213
|
+
# Same display as an uncaught exception, but keeps the exit code
|
|
214
|
+
$stderr.puts(exception_info[:e].full_message) # rubocop:disable Style/StderrPuts
|
|
215
|
+
elsif exception_info[:debug]
|
|
216
|
+
@context.formatter.display_message(:error, 'Use --log-level=debug to get more details.')
|
|
217
|
+
end
|
|
218
|
+
exit_code = exception_info[:exit]
|
|
219
|
+
# Nagios plugins report the failure of the check itself as UNKNOWN
|
|
220
|
+
exit_code = Nagios::UNKNOWN if !exit_code.eql?(ExitCode::SUCCESS) && @context.formatter&.format_type.eql?(:nagios)
|
|
221
|
+
Process.exit(exit_code)
|
|
212
222
|
end
|
|
213
223
|
return
|
|
214
224
|
end
|
|
@@ -228,6 +238,28 @@ module Aspera
|
|
|
228
238
|
Result::Text.new(usage_text(plugin: plugin))
|
|
229
239
|
end
|
|
230
240
|
|
|
241
|
+
# `on_set` callback of the `ui` option (dot-notation sub-properties): `mode`, `command`
|
|
242
|
+
# A plain value is the `mode`. Values are validated against schema `UiOptions`.
|
|
243
|
+
# @param value [Hash] sub-properties to set
|
|
244
|
+
def option_ui=(value)
|
|
245
|
+
Aspera.assert_type(value, Hash)
|
|
246
|
+
value.each do |k, v|
|
|
247
|
+
case k.to_sym
|
|
248
|
+
when :mode
|
|
249
|
+
Environment.instance.url_method =
|
|
250
|
+
case v.to_s
|
|
251
|
+
when 'auto' then Environment.instance.default_gui_mode
|
|
252
|
+
when 'terminal', 'text' then :text
|
|
253
|
+
when 'browser', 'graphical' then :graphical
|
|
254
|
+
else Aspera.error_unexpected_value(v, type: BadArgument) { 'ui.mode' }
|
|
255
|
+
end
|
|
256
|
+
when :command
|
|
257
|
+
Environment.instance.browser_command = v.nil? || v.empty? ? nil : v.map(&:to_s)
|
|
258
|
+
else Aspera.error_unexpected_value(k, type: BadArgument) { 'ui sub-option (mode, command)' }
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
231
263
|
# `on_set` callback of the `log` option (dot-notation sub-properties): `level`, `type`, `format`, `secrets`
|
|
232
264
|
# @param value [Hash] sub-properties to set
|
|
233
265
|
def option_log=(value)
|
|
@@ -243,6 +275,29 @@ module Aspera
|
|
|
243
275
|
end
|
|
244
276
|
end
|
|
245
277
|
|
|
278
|
+
# `on_set` callback of the `temp` option (dot-notation sub-properties): `folder`, `clean`, `expiry`
|
|
279
|
+
# A plain value is the `folder`. Values are validated against schema `TempOptions`.
|
|
280
|
+
# @param value [Hash] sub-properties to set
|
|
281
|
+
def option_temp=(value)
|
|
282
|
+
Aspera.assert_type(value, Hash)
|
|
283
|
+
value.each do |k, v|
|
|
284
|
+
case k.to_sym
|
|
285
|
+
when :folder then TempFileManager.instance.global_temp = v
|
|
286
|
+
when :clean then TempFileManager.instance.cleanup_on_exit = Parser.get_from_list(v.to_s, 'temp clean', BoolValue::ALL)
|
|
287
|
+
when :expiry
|
|
288
|
+
TempFileManager.instance.expiry_sec =
|
|
289
|
+
case v
|
|
290
|
+
when nil, false then nil
|
|
291
|
+
when Numeric then v
|
|
292
|
+
else Duration.to_unit(v, 's')
|
|
293
|
+
end
|
|
294
|
+
else Aspera.error_unexpected_value(k, type: BadArgument) { 'temp sub-option (folder, clean, expiry)' }
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
rescue ParameterError => e
|
|
298
|
+
raise BadArgument, "Option temp: #{e.message}"
|
|
299
|
+
end
|
|
300
|
+
|
|
246
301
|
private
|
|
247
302
|
|
|
248
303
|
# Build the usage/help text.
|
|
@@ -327,16 +382,36 @@ module Aspera
|
|
|
327
382
|
# @param arg [ArgumentSpec]
|
|
328
383
|
# @return [Array<String>]
|
|
329
384
|
def schema_help_lines(arg)
|
|
330
|
-
builder =
|
|
331
|
-
rows = builder.rows.
|
|
332
|
-
builder.columns.map { |column| row[column].
|
|
385
|
+
builder = schema_documentation(arg.schema)
|
|
386
|
+
rows = builder.rows.map do |row|
|
|
387
|
+
builder.columns.map { |column| Array(row[column]).join("\n") }
|
|
333
388
|
end
|
|
334
389
|
style = {}
|
|
335
390
|
style[:border] = :unicode_round if Environment.terminal_supports_unicode?
|
|
336
|
-
table =
|
|
391
|
+
table = TableFitter.table(headings: builder.columns, rows: rows, style: style, width: Environment.terminal_columns&.-(4)).to_s
|
|
337
392
|
["\n SCHEMA: #{arg.name}", table.lines.map { |line| " #{line.chomp}" }.join("\n")]
|
|
338
393
|
end
|
|
339
394
|
|
|
395
|
+
# Display the documentation of a schema: value `help` given to an option or argument
|
|
396
|
+
# @param schema_path [String, nil] Path of the schema in the registry, `nil` if none
|
|
397
|
+
# @return [nil]
|
|
398
|
+
def display_schema(schema_path)
|
|
399
|
+
if schema_path.nil?
|
|
400
|
+
Log.log.warn { 'Sorry, no schema provided yet. Please refer to the manual or API.' }
|
|
401
|
+
return
|
|
402
|
+
end
|
|
403
|
+
builder = schema_documentation(schema_path)
|
|
404
|
+
@context.formatter.display_results(Result::ObjectList.new(builder.rows, fields: builder.columns))
|
|
405
|
+
nil
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# @param schema_path [String] Path of the schema in the registry
|
|
409
|
+
# @return [Schema::Documentation] Documentation of the schema, for the terminal
|
|
410
|
+
def schema_documentation(schema_path)
|
|
411
|
+
options = schema_path.eql?(Schema::Registry::TRANSFER_SPEC) ? Transfer::Spec.doc_options : {}
|
|
412
|
+
Schema::Documentation.new(TerminalFormatter, Schema::Registry.instance.reader(schema_path), **options).build
|
|
413
|
+
end
|
|
414
|
+
|
|
340
415
|
# Initialize agents and options
|
|
341
416
|
# This can throw exception if there is a problem with the environment, needs to be caught by execute method
|
|
342
417
|
# @raise [StandardError] if there is a problem with the environment
|
|
@@ -413,6 +488,9 @@ module Aspera
|
|
|
413
488
|
|
|
414
489
|
ARGS
|
|
415
490
|
#{t}Some commands require mandatory arguments, e.g. a path.
|
|
491
|
+
|
|
492
|
+
EXIT STATUS
|
|
493
|
+
#{ExitCode::DESCRIPTIONS.map { |code, description| "#{t}#{code.to_s.ljust(5)}#{description}" }.join("\n")}
|
|
416
494
|
END_OF_BANNER
|
|
417
495
|
end
|
|
418
496
|
|
|
@@ -427,9 +505,9 @@ module Aspera
|
|
|
427
505
|
@context.options.declare(:show_config, description: 'Display parameters used for the provided action', allowed: Type::NONE) { @option_show_config = true }
|
|
428
506
|
@context.options.declare(:version, description: 'Display version', allowed: Type::NONE, short: 'v') { @context.formatter.display_message(:data, Cli::VERSION); Process.exit(0) } # rubocop:disable Style/Semicolon
|
|
429
507
|
@context.options.declare(
|
|
430
|
-
:ui, description: '
|
|
431
|
-
allowed:
|
|
432
|
-
on_set:
|
|
508
|
+
:ui, description: 'User interface (dot-notation: mode, command)',
|
|
509
|
+
allowed: [Hash, String], shorthand: 'mode', schema: Schema::Registry::UI_OPTIONS,
|
|
510
|
+
on_set: method(:option_ui=)
|
|
433
511
|
)
|
|
434
512
|
@context.options.declare(
|
|
435
513
|
:invalid_characters, description: 'Replacement character and invalid filename characters',
|
|
@@ -440,11 +518,17 @@ module Aspera
|
|
|
440
518
|
@context.options.declare(:log_format, description: 'Log formatter', allowed: [Proc, Logger::Formatter, String], on_set: Log.instance.method(:formatter=))
|
|
441
519
|
@context.options.declare(:logger, description: 'Logging method', allowed: Log::LOG_TYPES, on_set: Log.instance.method(:logger_type=))
|
|
442
520
|
@context.options.declare(:log, description: 'Logging options (dot-notation: level, type, format, secrets)', on_set: method(:option_log=), schema: Schema::Registry::LOG_OPTIONS)
|
|
521
|
+
@context.options.declare(:security, description: 'Security restrictions (dot-notation: level, allowed_paths)', on_set: Security.instance.method(:options=), schema: Schema::Registry::SECURITY_OPTIONS)
|
|
443
522
|
@context.options.declare(:lock_port, description: 'Prevent dual execution of a command, e.g. in cron', allowed: Type::INTEGER)
|
|
444
523
|
@context.options.declare(:once_only, description: 'Process only new items (some commands)', allowed: Type::BOOLEAN, default: false)
|
|
445
524
|
@context.options.declare(:log_secrets, description: 'Show passwords in logs', allowed: Type::BOOLEAN, default: SecretHider.instance.log_secrets, on_set: SecretHider.instance.method(:log_secrets=))
|
|
446
|
-
@context.options.declare(
|
|
447
|
-
|
|
525
|
+
@context.options.declare(
|
|
526
|
+
:temp, description: 'Temporary files (dot-notation: folder, clean, expiry)',
|
|
527
|
+
allowed: [Hash, String], shorthand: 'folder', schema: Schema::Registry::TEMP_OPTIONS,
|
|
528
|
+
on_set: method(:option_temp=)
|
|
529
|
+
)
|
|
530
|
+
@context.options.declare(:clean_temp, description: 'Cleanup temporary files on exit', allowed: Type::BOOLEAN, on_set: TempFileManager.instance.method(:cleanup_on_exit=), deprecation: {last: '4.27.5', message: 'use --temp.clean'})
|
|
531
|
+
@context.options.declare(:temp_folder, description: 'Temporary folder', on_set: TempFileManager.instance.method(:global_temp=), deprecation: {last: '4.27.5', message: 'use --temp.folder'})
|
|
448
532
|
@context.options.declare(:pid_file, description: 'Write process identifier to file, delete on exit')
|
|
449
533
|
@context.options.declare(
|
|
450
534
|
:parser, description: 'Default parser for structured parameters and options',
|
|
@@ -471,12 +555,18 @@ module Aspera
|
|
|
471
555
|
end
|
|
472
556
|
COMMAND_CONFIG = :config
|
|
473
557
|
COMMAND_HELP = :help
|
|
558
|
+
# Shell completion commands (scripts and words)
|
|
559
|
+
COMPLETION_COMMAND = %w[config completion].freeze
|
|
474
560
|
# Types that go to result of type = text
|
|
475
561
|
SCALAR_TYPES = [String, Integer, Symbol].freeze
|
|
476
|
-
USER_INTERFACES = %i[text graphical].freeze
|
|
477
562
|
EXTEND_ARGS = :''
|
|
563
|
+
# Errors of name resolution, connection and timeout
|
|
564
|
+
NETWORK_ERRORS = [
|
|
565
|
+
SocketError, Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout,
|
|
566
|
+
Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::ETIMEDOUT
|
|
567
|
+
].freeze
|
|
478
568
|
|
|
479
|
-
private_constant :COMMAND_CONFIG, :COMMAND_HELP, :SCALAR_TYPES, :
|
|
569
|
+
private_constant :COMMAND_CONFIG, :COMMAND_HELP, :COMPLETION_COMMAND, :SCALAR_TYPES, :EXTEND_ARGS, :NETWORK_ERRORS
|
|
480
570
|
end
|
|
481
571
|
end
|
|
482
572
|
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'singleton'
|
|
4
|
+
require 'aspera/assert'
|
|
5
|
+
require 'aspera/cli/error'
|
|
6
|
+
|
|
7
|
+
module Aspera
|
|
8
|
+
module Cli
|
|
9
|
+
# Security level: restricts the features that access local resources (files, environment, network) or execute code.
|
|
10
|
+
# On the command line, the default level is `low`: the user can do anything that the account is allowed to do.
|
|
11
|
+
# It can be raised, for example in MCP server mode, where commands come from a client.
|
|
12
|
+
#
|
|
13
|
+
# @!method self.instance
|
|
14
|
+
# Returns the singleton instance of Security
|
|
15
|
+
# @return [Security] the singleton instance
|
|
16
|
+
class Security
|
|
17
|
+
include Singleton
|
|
18
|
+
|
|
19
|
+
# Security levels, from least to most restrictive
|
|
20
|
+
LEVELS = %i[low medium high].freeze
|
|
21
|
+
# Extended-value handlers restricted by security level, with the levels that allow them
|
|
22
|
+
EXTENDED_VALUE_LEVELS = {
|
|
23
|
+
ruby: %i[low],
|
|
24
|
+
uri: %i[low],
|
|
25
|
+
env: %i[low],
|
|
26
|
+
file: %i[low medium]
|
|
27
|
+
}.freeze
|
|
28
|
+
# Levels that allow processing a secret (e.g. by an extended value): the result could reveal it, as only declared values are hidden
|
|
29
|
+
SECRET_PROCESSING_LEVELS = %i[low].freeze
|
|
30
|
+
private_constant :EXTENDED_VALUE_LEVELS, :SECRET_PROCESSING_LEVELS
|
|
31
|
+
|
|
32
|
+
def initialize
|
|
33
|
+
reset
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Back to default: level `low`, no allowed path, not locked
|
|
37
|
+
# @return [nil]
|
|
38
|
+
def reset
|
|
39
|
+
@level = nil
|
|
40
|
+
@default_level = LEVELS.first
|
|
41
|
+
@allowed_paths = []
|
|
42
|
+
@locked = nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [Symbol] Level set by option `security`, else default level
|
|
46
|
+
def level = @level || @default_level
|
|
47
|
+
|
|
48
|
+
# Set by option `security`.
|
|
49
|
+
# When locked, the value can only be set again to the current one.
|
|
50
|
+
# @param value [Hash] `level` and `allowed_paths`, see schema `SecurityOptions`
|
|
51
|
+
# @return [nil]
|
|
52
|
+
def options=(value)
|
|
53
|
+
Aspera.assert_type(value, Hash, type: BadArgument) { 'security' }
|
|
54
|
+
# When locked, each command starts with the empty value of a Hash option: not a change
|
|
55
|
+
return if @locked && value.empty?
|
|
56
|
+
value = value.transform_keys(&:to_sym)
|
|
57
|
+
new_level = value[:level]&.to_sym
|
|
58
|
+
Aspera.assert_values(new_level, LEVELS, type: BadArgument) { 'security.level' } unless new_level.nil?
|
|
59
|
+
new_paths = Array(value[:allowed_paths]).map do |path|
|
|
60
|
+
path = File.expand_path(path)
|
|
61
|
+
Aspera.assert(File.exist?(path), type: BadArgument) { "security.allowed_paths: no such file or folder: #{path}" }
|
|
62
|
+
File.realpath(path)
|
|
63
|
+
end
|
|
64
|
+
if @locked
|
|
65
|
+
Aspera.assert((new_level || @default_level).eql?(level) && new_paths.eql?(@allowed_paths), type: BadArgument) do
|
|
66
|
+
"security cannot be changed #{@locked}"
|
|
67
|
+
end
|
|
68
|
+
return
|
|
69
|
+
end
|
|
70
|
+
@level = new_level
|
|
71
|
+
@allowed_paths = new_paths
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Prevent further changes, e.g. by commands of an MCP client
|
|
76
|
+
# @param reason [String] For error messages, e.g. `in MCP server mode`
|
|
77
|
+
# @param default [Symbol, nil] Level if not set by option
|
|
78
|
+
# @return [nil]
|
|
79
|
+
def lock(reason, default: nil)
|
|
80
|
+
Aspera.assert_values(default, LEVELS) unless default.nil?
|
|
81
|
+
@default_level = default unless default.nil?
|
|
82
|
+
@locked = reason
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @param handler [Symbol] Extended-value handler
|
|
87
|
+
# @raise [BadArgument] If the handler is refused at current level, with the level that allows it and where to set it
|
|
88
|
+
def check_extended_value(handler)
|
|
89
|
+
allowed = EXTENDED_VALUE_LEVELS[handler]
|
|
90
|
+
return if allowed.nil? || allowed.include?(level)
|
|
91
|
+
# the least permissive level that allows it
|
|
92
|
+
needed = handler.eql?(:file) ? "level #{allowed.last} with the file in security.allowed_paths" : "level #{allowed.last}"
|
|
93
|
+
raise BadArgument, "@#{handler}: extended value is disabled by security level #{level}: use #{needed} (#{where_to_set}), or give the value itself"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# A secret (e.g. read by `@vault:`, `@secret:`, `@preset:`) is hidden in results only as declared: the result of processing it
|
|
97
|
+
# could reveal it (e.g. split by `@list:`, in an error message of `@json:`, short value in a text by `@extend:`, converted as name of parameter).
|
|
98
|
+
# @param what [String] What processes the value, e.g. `@list`
|
|
99
|
+
# @param hint [String] What to do instead
|
|
100
|
+
# @yieldreturn [Boolean] `true` if the value contains a secret, evaluated only if the level refuses it
|
|
101
|
+
# @raise [BadArgument] If the value contains a secret and processing it is refused at current level
|
|
102
|
+
def check_secret_processing(what, hint: 'give the secret as the whole value')
|
|
103
|
+
return if SECRET_PROCESSING_LEVELS.include?(level) || !yield
|
|
104
|
+
raise BadArgument, "#{what}: cannot process a secret at security level #{level}, as the result could reveal it: #{hint}, or use level low (#{where_to_set})"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# At level `medium`, the real path is checked, so that neither `..` nor a symbolic link leads outside of allowed paths.
|
|
108
|
+
# A missing file gets the same error as a file outside allowed paths: no information on other files.
|
|
109
|
+
# @param path [String] Path of a file to read
|
|
110
|
+
# @return [String] Path to read
|
|
111
|
+
# @raise [BadArgument] If reading the file is refused at current level
|
|
112
|
+
def readable_path(path)
|
|
113
|
+
path = File.expand_path(path)
|
|
114
|
+
case level
|
|
115
|
+
when :low then path
|
|
116
|
+
when :medium
|
|
117
|
+
real_path = File.realpath(path) rescue nil
|
|
118
|
+
Aspera.assert(!real_path.nil? && @allowed_paths.any? { |allowed| real_path.eql?(allowed) || real_path.start_with?(File.join(allowed, '')) }, type: BadArgument) do
|
|
119
|
+
"no such file in security.allowed_paths: #{path}"
|
|
120
|
+
end
|
|
121
|
+
real_path
|
|
122
|
+
else raise BadArgument, "reading files is disabled by security level #{level}"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
# @return [String] Where to set the level, for error messages
|
|
129
|
+
def where_to_set
|
|
130
|
+
@locked.nil? ? 'option security' : "option security when starting, as it cannot be changed #{@locked}"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'terminal-table'
|
|
4
|
+
require 'unicode/display_width'
|
|
5
|
+
|
|
6
|
+
module Aspera
|
|
7
|
+
module Cli
|
|
8
|
+
# Build a terminal table that fits in a given width, by wrapping cells.
|
|
9
|
+
# Columns with text (containing spaces, e.g. `description`) are wrapped first, the widest first.
|
|
10
|
+
# If not enough, other columns (names, identifiers) are wrapped too.
|
|
11
|
+
# If the table cannot fit, columns are narrowed to their minimum width.
|
|
12
|
+
module TableFitter
|
|
13
|
+
# Minimum width of a wrapped column with text
|
|
14
|
+
MIN_TEXT_WIDTH = 20
|
|
15
|
+
# Minimum width of other wrapped columns
|
|
16
|
+
MIN_WIDTH = 10
|
|
17
|
+
# ANSI style (zero width)
|
|
18
|
+
STYLE = /\e\[[0-9;]*m/
|
|
19
|
+
# Styles that reset all styles
|
|
20
|
+
RESETS = ["\e[0m", "\e[m"].freeze
|
|
21
|
+
# ANSI style or character
|
|
22
|
+
TOKEN = /#{STYLE}|\X/
|
|
23
|
+
# Long words are preferably broken after one of these characters
|
|
24
|
+
BREAK_AFTER = %r{[-_./,=]}
|
|
25
|
+
private_constant :MIN_TEXT_WIDTH, :MIN_WIDTH, :STYLE, :RESETS, :TOKEN, :BREAK_AFTER
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
# @param headings [Array] Column names
|
|
29
|
+
# @param rows [Array<Array<String>>] Rows of cells
|
|
30
|
+
# @param style [Hash] `Terminal::Table` style
|
|
31
|
+
# @param width [Integer, nil] Maximum width of the table, or `nil` for natural width (not a `style` parameter)
|
|
32
|
+
# @return [Terminal::Table]
|
|
33
|
+
def table(headings:, rows:, style:, width:)
|
|
34
|
+
headings, rows = fit(headings, rows, width - overhead(headings.length, style)) unless width.nil?
|
|
35
|
+
Terminal::Table.new(headings: headings, rows: rows, style: style)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# @return [Integer] Width taken by borders and padding for a table with that number of columns
|
|
41
|
+
def overhead(column_count, style)
|
|
42
|
+
Terminal::Table.new(headings: [''] * column_count, style: style).to_s.lines.map { |line| display_width(line.chomp) }.max
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param available [Integer] Width available for the content of all columns
|
|
46
|
+
# @return [Array] Headings and rows, wrapped to fit in available width, or as narrow as possible
|
|
47
|
+
def fit(headings, rows, available)
|
|
48
|
+
lines = [headings, *rows].map { |row| row.map(&:to_s) }
|
|
49
|
+
columns = lines.transpose
|
|
50
|
+
natural = columns.map { |cells| cells.flat_map { |cell| cell.split("\n") }.map { |line| display_width(line) }.max || 0 }
|
|
51
|
+
return [headings, rows] if natural.sum <= available
|
|
52
|
+
text = columns.map { |cells| cells.any? { |cell| cell.match?(/[ \t]/) } }
|
|
53
|
+
# Minimum width of a column, not narrower than its heading
|
|
54
|
+
floor = ->(i, min) { min.clamp(display_width(columns[i].first), natural[i]) }
|
|
55
|
+
# First wrap only columns with text, if not enough wrap all columns
|
|
56
|
+
text_only = natural.each_index.map { |i| text[i] ? floor.call(i, MIN_TEXT_WIDTH) : natural[i] }
|
|
57
|
+
all = natural.each_index.map { |i| floor.call(i, text[i] ? MIN_TEXT_WIDTH : MIN_WIDTH) }
|
|
58
|
+
minimum = text_only.sum <= available ? text_only : all
|
|
59
|
+
# Columns wider than `limit` are narrowed to it, but not below their minimum (`limit` is `-1` if the table cannot fit)
|
|
60
|
+
widths_for = ->(limit) { natural.zip(minimum).map { |nat, min| limit.clamp(min, nat) } }
|
|
61
|
+
limit = (0..natural.max).bsearch { |l| widths_for.call(l).sum > available } - 1
|
|
62
|
+
widths = widths_for.call(limit)
|
|
63
|
+
wrapped = lines.map { |row| row.each_with_index.map { |cell, i| wrap(cell, widths[i]) } }
|
|
64
|
+
[wrapped.first, wrapped.drop(1)]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @return [String] Text with lines longer than width wrapped on spaces, and long words broken
|
|
68
|
+
def wrap(text, width)
|
|
69
|
+
text.split("\n").map do |line|
|
|
70
|
+
next line if display_width(line) <= width
|
|
71
|
+
wrapped = line.split.flat_map { |word| break_word(word, width) }.each_with_object([]) do |word, result|
|
|
72
|
+
if !result.empty? && display_width(result.last) + 1 + display_width(word) <= width
|
|
73
|
+
result[-1] = "#{result.last} #{word}"
|
|
74
|
+
else
|
|
75
|
+
result.push(word)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
restyle(wrapped).join("\n")
|
|
79
|
+
end.join("\n")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [Array<String>] Word split in pieces not wider than width, preferably after a punctuation
|
|
83
|
+
def break_word(word, width)
|
|
84
|
+
pieces = []
|
|
85
|
+
while display_width(word) > width
|
|
86
|
+
piece = +''
|
|
87
|
+
word.scan(TOKEN) do |token|
|
|
88
|
+
break if display_width(piece + token) > width && display_width(piece).positive?
|
|
89
|
+
piece << token
|
|
90
|
+
end
|
|
91
|
+
cut = piece.rindex(BREAK_AFTER)
|
|
92
|
+
piece = piece[0..cut] if cut && display_width(piece[0..cut]) > width / 2
|
|
93
|
+
pieces.push(piece)
|
|
94
|
+
word = word[piece.length..]
|
|
95
|
+
end
|
|
96
|
+
pieces.push(word)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @return [Array<String>] Lines with ANSI styles closed at end of line, and open again on next line
|
|
100
|
+
def restyle(lines)
|
|
101
|
+
active = []
|
|
102
|
+
lines.map do |line|
|
|
103
|
+
result = active.join + line
|
|
104
|
+
line.scan(STYLE) { |style| RESETS.include?(style) ? active.clear : active.push(style) }
|
|
105
|
+
active.empty? ? result : "#{result}#{RESETS.first}"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @return [Integer] Number of terminal columns used to display the string, ignoring ANSI escape sequences
|
|
110
|
+
def display_width(string)
|
|
111
|
+
Unicode::DisplayWidth.of(Terminal::Table::Util.ansi_escape(string))
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -20,52 +20,41 @@ module Aspera
|
|
|
20
20
|
'HINT:'.bg(:green).white.blink
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
# Format boolean with colored
|
|
23
|
+
# Format boolean with colored check mark, or Y and space without Unicode
|
|
24
24
|
def tick(yes)
|
|
25
|
-
result =
|
|
26
|
-
if Environment.terminal_supports_unicode?
|
|
27
|
-
yes ? "\u2713" : "\u2717"
|
|
28
|
-
else
|
|
29
|
-
yes ? 'Y' : ' '
|
|
30
|
-
end
|
|
25
|
+
result = Environment.char(yes ? '✓' : '✗')
|
|
31
26
|
return result.green if yes
|
|
32
|
-
|
|
27
|
+
result.red
|
|
33
28
|
end
|
|
34
29
|
|
|
35
30
|
# Format special values with colors (dim for empty, reverse for others)
|
|
36
31
|
def special_format(what)
|
|
37
32
|
result = "<#{what}>"
|
|
38
|
-
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# Prepare table row for terminal display (word wrap arrays)
|
|
42
|
-
def check_row(row)
|
|
43
|
-
row.each_key do |k|
|
|
44
|
-
row[k] = row[k].map { |i| WordWrap.ww(i.to_s, 120).chomp }.join("\n") if row[k].is_a?(Array)
|
|
45
|
-
end
|
|
33
|
+
%w[null empty].any? { |s| what.include?(s) } ? result.faint : result.inverse
|
|
46
34
|
end
|
|
47
35
|
|
|
48
|
-
# Convert Markdown to terminal format (**bold** -> blue, `code` -> bold)
|
|
49
|
-
# @param
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
36
|
+
# Convert Markdown inline markup to terminal format (**bold** -> blue, `code` -> bold)
|
|
37
|
+
# @param text [String] text with Markdown inline markup
|
|
38
|
+
# @return [String] text with terminal formatting
|
|
39
|
+
def markdown_text(text)
|
|
40
|
+
text.to_s.gsub(Markdown::INLINE_MARKUP_RE) do
|
|
41
|
+
match = Regexp.last_match
|
|
42
|
+
if match[:entity]
|
|
43
|
+
case match[:entity]
|
|
44
|
+
when 'bsol' then '\\'
|
|
45
|
+
else Aspera.error_unexpected_value(match[:entity]) { 'HTML entity' }
|
|
46
|
+
end
|
|
47
|
+
elsif match[:bold]
|
|
48
|
+
match[:bold].to_s.blue
|
|
49
|
+
elsif match[:code]
|
|
50
|
+
match[:code].to_s.bold
|
|
51
|
+
else
|
|
52
|
+
Aspera.error_unexpected_value(match.to_s)
|
|
53
|
+
end
|
|
65
54
|
end
|
|
66
55
|
end
|
|
67
56
|
|
|
68
|
-
module_function :hint, :tick, :special_format, :
|
|
57
|
+
module_function :hint, :tick, :special_format, :markdown_text
|
|
69
58
|
end
|
|
70
59
|
end
|
|
71
60
|
end
|
|
@@ -9,6 +9,7 @@ require 'aspera/cli/info'
|
|
|
9
9
|
require 'aspera/cli/async_transfer_store'
|
|
10
10
|
require 'aspera/log'
|
|
11
11
|
require 'aspera/assert'
|
|
12
|
+
require 'aspera/secret_hider'
|
|
12
13
|
require 'securerandom'
|
|
13
14
|
|
|
14
15
|
module Aspera
|
|
@@ -252,11 +253,13 @@ module Aspera
|
|
|
252
253
|
# updated transfer spec with command line
|
|
253
254
|
transfer_spec.deep_merge!(@user_transfer_spec)
|
|
254
255
|
# resolve pseudo-parameter: target_rate -> target_rate_kbps (overrides target_rate_kbps if both are present)
|
|
255
|
-
|
|
256
|
+
Transfer::Spec.resolve_target_rate(transfer_spec)
|
|
256
257
|
# recursively remove values that are nil (user wants to delete)
|
|
257
258
|
transfer_spec.deep_do { |hash, key, value, _unused| hash.delete(key) if value.nil? }
|
|
258
259
|
# if TS from app has content_protection (e.g. F5), that means content is protected: ask password if not provided
|
|
259
260
|
transfer_spec['content_protection_password'] = @context.options.prompt_user_input('content protection password', sensitive: true) if transfer_spec['content_protection'].eql?('decrypt') && !transfer_spec.key?('content_protection_password')
|
|
261
|
+
# Secrets of transfer spec (e.g. `token` from the application) are hidden wherever they appear (logs, results)
|
|
262
|
+
SecretHider.instance.add_secrets_in(transfer_spec)
|
|
260
263
|
# create transfer agent
|
|
261
264
|
agent_instance.start_transfer(transfer_spec, token_regenerator: rest_token)
|
|
262
265
|
# --- async mode ---
|