aspera-cli 4.26.1 → 4.27.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 +300 -22
- data/CONTRIBUTING.md +100 -12
- data/bin/ascli +5 -5
- data/docs/README.md +10757 -0
- data/docs/test-mcp-with-ai.md +202 -0
- data/lib/aspera/agent/base.rb +20 -6
- data/lib/aspera/agent/connect.rb +53 -16
- data/lib/aspera/agent/desktop.rb +52 -17
- data/lib/aspera/agent/direct.rb +27 -25
- data/lib/aspera/agent/factory.rb +4 -0
- data/lib/aspera/agent/httpgw.rb +2 -2
- data/lib/aspera/agent/node.rb +34 -2
- data/lib/aspera/agent/transferd.rb +39 -3
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +56 -51
- data/lib/aspera/api/cos_node.rb +2 -2
- data/lib/aspera/api/faspex.rb +22 -20
- data/lib/aspera/api/httpgw.rb +4 -4
- data/lib/aspera/api/node.rb +33 -30
- data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
- data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
- data/lib/aspera/ascmd.rb +25 -23
- data/lib/aspera/ascp/installation.rb +43 -40
- data/lib/aspera/ascp/management.rb +2 -3
- data/lib/aspera/cli/ascp_actions.rb +155 -0
- data/lib/aspera/cli/async_transfer_store.rb +81 -0
- data/lib/aspera/cli/bootstrapper.rb +197 -0
- data/lib/aspera/cli/command_registry.rb +145 -0
- data/lib/aspera/cli/command_spec.rb +138 -0
- data/lib/aspera/cli/context.rb +71 -0
- data/lib/aspera/cli/error.rb +16 -0
- data/lib/aspera/cli/extended_value.rb +5 -3
- data/lib/aspera/cli/formatter.rb +122 -209
- data/lib/aspera/cli/gem_checker.rb +65 -0
- data/lib/aspera/cli/hints.rb +2 -2
- data/lib/aspera/cli/http.rb +218 -0
- data/lib/aspera/cli/info.rb +2 -0
- data/lib/aspera/cli/mailer.rb +97 -0
- data/lib/aspera/cli/mcp_tool.rb +198 -0
- data/lib/aspera/cli/options.schema.yaml +364 -7
- data/lib/aspera/cli/{manager.rb → parser.rb} +337 -144
- data/lib/aspera/cli/plugins/alee.rb +20 -22
- data/lib/aspera/cli/plugins/aoc.rb +1173 -849
- data/lib/aspera/cli/plugins/ats.rb +200 -161
- data/lib/aspera/cli/plugins/base.rb +470 -107
- data/lib/aspera/cli/plugins/basic_auth.rb +14 -4
- data/lib/aspera/cli/plugins/config.rb +434 -1032
- data/lib/aspera/cli/plugins/console.rb +106 -64
- data/lib/aspera/cli/plugins/cos.rb +44 -32
- data/lib/aspera/cli/plugins/factory.rb +7 -4
- data/lib/aspera/cli/plugins/faspex.rb +296 -259
- data/lib/aspera/cli/plugins/faspex5.rb +592 -344
- data/lib/aspera/cli/plugins/faspio.rb +49 -51
- data/lib/aspera/cli/plugins/httpgw.rb +18 -25
- data/lib/aspera/cli/plugins/mcp.rb +279 -0
- data/lib/aspera/cli/plugins/node.rb +1001 -797
- data/lib/aspera/cli/plugins/oauth.rb +7 -10
- data/lib/aspera/cli/plugins/orchestrator.rb +111 -139
- data/lib/aspera/cli/plugins/preview.rb +183 -142
- data/lib/aspera/cli/plugins/server.rb +125 -80
- data/lib/aspera/cli/plugins/shares.rb +301 -107
- data/lib/aspera/cli/preset_actions.rb +139 -0
- data/lib/aspera/cli/preset_manager.rb +236 -0
- data/lib/aspera/cli/result.rb +360 -0
- data/lib/aspera/cli/runner.rb +473 -0
- data/lib/aspera/cli/secret_finder.rb +40 -0
- data/lib/aspera/cli/special_values.rb +1 -0
- data/lib/aspera/cli/sync_actions.rb +84 -56
- data/lib/aspera/cli/terminal_formatter.rb +65 -0
- data/lib/aspera/cli/transfer_actions.rb +83 -0
- data/lib/aspera/cli/transfer_agent.rb +116 -51
- data/lib/aspera/cli/transfer_progress.rb +9 -9
- data/lib/aspera/cli/vault_manager.rb +57 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +26 -26
- data/lib/aspera/command_line_builder.rb +23 -23
- data/lib/aspera/data_repository.rb +6 -1
- data/lib/aspera/dot_container.rb +5 -5
- data/lib/aspera/environment.rb +39 -13
- data/lib/aspera/exec_spec.rb +13 -0
- data/lib/aspera/faspex_gw.rb +2 -2
- data/lib/aspera/faspex_postproc.rb +4 -3
- data/lib/aspera/graphql.rb +35 -0
- data/lib/aspera/hash_ext.rb +6 -0
- data/lib/aspera/json_rpc/client.rb +62 -0
- data/lib/aspera/json_rpc/version.rb +7 -0
- data/lib/aspera/keychain/base.rb +1 -1
- data/lib/aspera/keychain/encrypted_hash.rb +2 -2
- data/lib/aspera/keychain/factory.rb +4 -4
- data/lib/aspera/keychain/macos_security.rb +4 -4
- data/lib/aspera/link_header.rb +82 -0
- data/lib/aspera/log.rb +23 -5
- data/lib/aspera/markdown.rb +85 -2
- data/lib/aspera/nagios.rb +2 -2
- data/lib/aspera/node_simulator.rb +3 -1
- data/lib/aspera/oauth/base.rb +10 -10
- data/lib/aspera/oauth/boot.rb +43 -0
- data/lib/aspera/oauth/factory.rb +41 -15
- data/lib/aspera/oauth/jwt.rb +4 -4
- data/lib/aspera/oauth/web.rb +4 -4
- data/lib/aspera/oauth.rb +1 -0
- data/lib/aspera/persistency_action_once.rb +10 -10
- data/lib/aspera/persistency_folder.rb +10 -3
- data/lib/aspera/preview/file_types.rb +7 -3
- data/lib/aspera/preview/generator.rb +3 -3
- data/lib/aspera/preview/utils.rb +1 -1
- data/lib/aspera/products/connect.rb +4 -1
- data/lib/aspera/products/other.rb +1 -1
- data/lib/aspera/products/transferd.rb +3 -3
- data/lib/aspera/proxy_auto_config.rb +10 -9
- data/lib/aspera/rest.rb +18 -40
- data/lib/aspera/rest_error_analyzer.rb +7 -3
- data/lib/aspera/rest_errors_aspera.rb +0 -10
- data/lib/aspera/rest_list.rb +13 -6
- data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +14 -4
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +2730 -648
- data/lib/aspera/schema/async_tables.yaml +361 -0
- data/lib/aspera/schema/documentation.rb +57 -26
- data/lib/aspera/schema/reader.rb +60 -11
- data/lib/aspera/schema/registry.rb +50 -10
- data/lib/aspera/secret_hider.rb +11 -2
- data/lib/aspera/ssl.rb +4 -4
- data/lib/aspera/sync/conf.schema.yaml +2 -2
- data/lib/aspera/sync/database.rb +14 -10
- data/lib/aspera/sync/operations.rb +15 -18
- data/lib/aspera/temp_file_manager.rb +5 -1
- data/lib/aspera/timer_limiter.rb +1 -1
- data/lib/aspera/transfer/faux_file.rb +24 -11
- data/lib/aspera/transfer/parameters.rb +27 -25
- data/lib/aspera/transfer/result.rb +74 -0
- data/lib/aspera/transfer/resumer.rb +7 -7
- data/lib/aspera/transfer/spec.rb +18 -1
- data/lib/aspera/transfer/spec.schema.yaml +26 -8
- data/lib/aspera/transfer/uri.rb +1 -1
- data/lib/aspera/uri_reader.rb +2 -2
- data/lib/aspera/web_auth.rb +2 -2
- data/lib/aspera/web_server_simple.rb +8 -5
- data/lib/aspera/yaml.rb +2 -1
- data.tar.gz.sig +0 -0
- metadata +45 -16
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/main.rb +0 -480
- data/lib/aspera/json_rpc.rb +0 -51
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/cli/command_spec'
|
|
4
|
+
require 'aspera/cli/context'
|
|
5
|
+
require 'aspera/cli/parser'
|
|
6
|
+
require 'aspera/cli/formatter'
|
|
7
|
+
require 'aspera/cli/plugins/factory'
|
|
8
|
+
require 'aspera/cli/bootstrapper'
|
|
9
|
+
require 'aspera/cli/plugins/config'
|
|
10
|
+
require 'aspera/cli/mailer'
|
|
11
|
+
require 'aspera/cli/secret_finder'
|
|
12
|
+
require 'aspera/cli/extended_value'
|
|
13
|
+
require 'aspera/cli/transfer_agent'
|
|
14
|
+
require 'aspera/cli/version'
|
|
15
|
+
require 'aspera/cli/info'
|
|
16
|
+
require 'aspera/cli/hints'
|
|
17
|
+
require 'aspera/cli/result'
|
|
18
|
+
require 'aspera/transfer/result'
|
|
19
|
+
require 'aspera/secret_hider'
|
|
20
|
+
require 'aspera/log'
|
|
21
|
+
require 'aspera/assert'
|
|
22
|
+
require 'aspera/schema/documentation'
|
|
23
|
+
require 'aspera/schema/registry'
|
|
24
|
+
require 'net/ssh/errors'
|
|
25
|
+
require 'openssl'
|
|
26
|
+
|
|
27
|
+
module Aspera
|
|
28
|
+
module Cli
|
|
29
|
+
# The main CLI class
|
|
30
|
+
class Runner
|
|
31
|
+
# Plugins store transfer result using this key and use result_transfer_multiple()
|
|
32
|
+
STATUS_FIELD = 'status'
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
# Process the typed result of a finished (or submitted) transfer.
|
|
36
|
+
# @param transfer_result [Transfer::Result] typed result from TransferAgent#start
|
|
37
|
+
# @raise [StandardError] if the transfer failed
|
|
38
|
+
# @return [Result] CLI result object
|
|
39
|
+
def result_transfer(transfer_result)
|
|
40
|
+
case transfer_result
|
|
41
|
+
when Transfer::Result::Async
|
|
42
|
+
return Result::SingleObject.new(transfer_result.to_h)
|
|
43
|
+
when Transfer::Result::Error
|
|
44
|
+
raise transfer_result.exception
|
|
45
|
+
when Transfer::Result::Success
|
|
46
|
+
return Result::Nothing.new
|
|
47
|
+
else
|
|
48
|
+
raise "Unexpected transfer result type: #{transfer_result.class}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Used when one command executes several transfer jobs (each job being possibly multi session)
|
|
53
|
+
# @param status_table [Array] [{STATUS_FIELD=>Transfer::Result,...},...]
|
|
54
|
+
# @return [Result] a status object suitable as command result
|
|
55
|
+
def result_transfer_multiple(status_table)
|
|
56
|
+
failed_result = nil
|
|
57
|
+
status_table.each do |item|
|
|
58
|
+
tr = item[STATUS_FIELD]
|
|
59
|
+
case tr
|
|
60
|
+
when Transfer::Result::Error
|
|
61
|
+
failed_result ||= tr
|
|
62
|
+
item[STATUS_FIELD] = tr.exception.message
|
|
63
|
+
when Transfer::Result::Success
|
|
64
|
+
item[STATUS_FIELD] = 'success'
|
|
65
|
+
when Transfer::Result::Async
|
|
66
|
+
item[STATUS_FIELD] = "async:#{tr.job_id}"
|
|
67
|
+
else
|
|
68
|
+
item[STATUS_FIELD] = tr.to_s
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
raise failed_result.exception unless failed_result.nil?
|
|
72
|
+
return Result::ObjectList.new(status_table)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Minimum initialization, no exception raised
|
|
77
|
+
# @param argv [Array<String>] command line arguments
|
|
78
|
+
# @return [nil]
|
|
79
|
+
def initialize(argv)
|
|
80
|
+
@argv = argv
|
|
81
|
+
Log.dump(:argv, @argv, level: :trace2)
|
|
82
|
+
@option_help = false
|
|
83
|
+
@option_show_config = false
|
|
84
|
+
@context = Context.new
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
attr_reader :context
|
|
88
|
+
|
|
89
|
+
# Execute the command and return the raw `Result` object.
|
|
90
|
+
# Pure computation: no display, no Process.exit - raises on any error.
|
|
91
|
+
# @return [Result, nil] the result of the command, or nil if nothing to execute
|
|
92
|
+
def run_with_result
|
|
93
|
+
init_agents_and_options
|
|
94
|
+
Plugins::Factory.instance.add_plugins_from_lookup_folders
|
|
95
|
+
# Help requested without command? Show global options + plugin list
|
|
96
|
+
return result_usage if @option_help && @context.options.command_or_arg_empty?
|
|
97
|
+
@context.config.periodic_check_newer_gem_version
|
|
98
|
+
command_sym =
|
|
99
|
+
if @option_show_config && @context.options.command_or_arg_empty?
|
|
100
|
+
COMMAND_CONFIG
|
|
101
|
+
else
|
|
102
|
+
@context.options.get_next_command(Plugins::Factory.instance.plugin_list.unshift(COMMAND_HELP))
|
|
103
|
+
end
|
|
104
|
+
@context.options.fail_on_missing_mandatory = false if @option_help || @option_show_config
|
|
105
|
+
case command_sym
|
|
106
|
+
when COMMAND_HELP
|
|
107
|
+
return result_usage
|
|
108
|
+
when COMMAND_CONFIG
|
|
109
|
+
command_plugin = @context.config
|
|
110
|
+
else
|
|
111
|
+
command_plugin = get_plugin_instance_with_options(command_sym)
|
|
112
|
+
@context.options.parse_options!
|
|
113
|
+
end
|
|
114
|
+
# --help after a plugin name: if no positional args remain, show plugin-level help now.
|
|
115
|
+
# If args remain (e.g. `ascli aoc files -h`), let the dispatch consume them and
|
|
116
|
+
# intercept --help at the right depth via Cli::HelpRequest.
|
|
117
|
+
return result_usage(plugin: command_plugin) if @option_help && @context.options.command_or_arg_empty?
|
|
118
|
+
if @option_show_config
|
|
119
|
+
result = Result::SingleObject.new(@context.options.known_options(only_defined: true).stringify_keys)
|
|
120
|
+
@context.presets.save_if_needed
|
|
121
|
+
@context.transfer.shutdown
|
|
122
|
+
TempFileManager.instance.cleanup
|
|
123
|
+
return result
|
|
124
|
+
end
|
|
125
|
+
execute_command = true
|
|
126
|
+
lock_port = @context.options.get_option(:lock_port)
|
|
127
|
+
if !lock_port.nil?
|
|
128
|
+
begin
|
|
129
|
+
Log.log.debug{"Opening lock port #{lock_port}"}
|
|
130
|
+
@tcp_server = TCPServer.new('127.0.0.1', lock_port)
|
|
131
|
+
rescue StandardError => e
|
|
132
|
+
execute_command = false
|
|
133
|
+
Log.log.warn{"Another instance is already running (#{e.message})."}
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
pid_file = @context.options.get_option(:pid_file)
|
|
137
|
+
if !pid_file.nil?
|
|
138
|
+
File.write(pid_file, Process.pid)
|
|
139
|
+
Log.log.debug{"Wrote pid #{Process.pid} to #{pid_file}"}
|
|
140
|
+
at_exit{File.delete(pid_file)}
|
|
141
|
+
end
|
|
142
|
+
begin
|
|
143
|
+
result = command_plugin.execute_action if execute_command
|
|
144
|
+
rescue Cli::HelpRequest => e
|
|
145
|
+
return result_usage(plugin: e.plugin)
|
|
146
|
+
ensure
|
|
147
|
+
@context.presets.save_if_needed
|
|
148
|
+
@context.transfer.shutdown
|
|
149
|
+
TempFileManager.instance.cleanup
|
|
150
|
+
end
|
|
151
|
+
return result
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Main entry point: execute the command, display results, exit on error.
|
|
155
|
+
# @return [nil]
|
|
156
|
+
def run
|
|
157
|
+
exception_info = nil
|
|
158
|
+
begin
|
|
159
|
+
result = run_with_result
|
|
160
|
+
@context.formatter.display_results(result) if result
|
|
161
|
+
rescue Net::SSH::AuthenticationFailed => e; exception_info = {e: e, t: 'SSH', security: true}
|
|
162
|
+
rescue OpenSSL::SSL::SSLError => e; exception_info = {e: e, t: 'SSL'}
|
|
163
|
+
rescue Cli::BadArgument => e; exception_info = {e: e, t: 'Argument', usage: true}
|
|
164
|
+
rescue Cli::MissingArgument => e; exception_info = {e: e, t: 'Missing'}
|
|
165
|
+
rescue Cli::BadIdentifier => e; exception_info = {e: e, t: 'Identifier'}
|
|
166
|
+
rescue Cli::SchemaRequest => e; exception_info = {e: e, t: 'Schema'}
|
|
167
|
+
rescue Cli::Error => e; exception_info = {e: e, t: 'Tool', usage: true}
|
|
168
|
+
rescue Transfer::Error => e; exception_info = {e: e, t: 'Transfer'}
|
|
169
|
+
rescue RestCallError => e; exception_info = {e: e, t: 'Rest'}
|
|
170
|
+
rescue SocketError => e; exception_info = {e: e, t: 'Network'}
|
|
171
|
+
rescue StandardError => e; exception_info = {e: e, t: "Other(#{e.class.name})", debug: true}
|
|
172
|
+
rescue Interrupt => e; exception_info = {e: e, t: 'Interruption', debug: true}
|
|
173
|
+
end
|
|
174
|
+
# 1- processing of error condition
|
|
175
|
+
unless exception_info.nil?
|
|
176
|
+
Log.log.warn(exception_info[:e].message) if Log.instance.logger_type.eql?(:syslog) && exception_info[:security]
|
|
177
|
+
Log.log.error{"#{exception_info[:t]}: #{exception_info[:e].message}"} unless exception_info[:e].is_a?(Cli::SchemaRequest)
|
|
178
|
+
Log.log.debug{(['Backtrace:'] + exception_info[:e].backtrace).join("\n")} if exception_info[:debug]
|
|
179
|
+
@context.formatter.display_message(:error, 'Use option -h to get help.') if exception_info[:usage]
|
|
180
|
+
Hints.hint_for(exception_info[:e], @context.formatter)
|
|
181
|
+
if exception_info[:e].is_a?(Cli::SchemaRequest)
|
|
182
|
+
Log.log.info{"#{exception_info[:t]}: #{exception_info[:e].message}"}
|
|
183
|
+
schema_path = exception_info[:e].path
|
|
184
|
+
if schema_path.nil?
|
|
185
|
+
Log.log.warn{'Sorry, no schema provided yet. Please refer to the manual or API.'}
|
|
186
|
+
else
|
|
187
|
+
builder = Schema::Documentation.new(TerminalFormatter, Schema::Registry.instance.reader(schema_path)).build
|
|
188
|
+
@context.formatter.display_results(Result::ObjectList.new(builder.rows, fields: builder.columns))
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
# 2- processing of unprocessed arguments (skip when help was displayed: sub-commands are not consumed)
|
|
193
|
+
unless @option_help
|
|
194
|
+
@context.options&.final_errors&.each do |msg|
|
|
195
|
+
Log.log.error{"Argument: #{msg}"}
|
|
196
|
+
exception_info = {e: Exception.new(msg), t: 'UnusedArg'} if exception_info.nil?
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
# 3- exit on error
|
|
200
|
+
unless exception_info.nil?
|
|
201
|
+
raise exception_info[:e] if Log.log.debug?
|
|
202
|
+
@context.formatter.display_message(:error, 'Use --log-level=debug to get more details.') if exception_info[:debug]
|
|
203
|
+
Process.exit(1)
|
|
204
|
+
end
|
|
205
|
+
return
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Display usage information and exit (used by the interactive CLI).
|
|
209
|
+
# @param plugin [Plugins::Base, nil] plugin instance to show subcommands for
|
|
210
|
+
# @return [nil]
|
|
211
|
+
def show_usage(plugin: nil)
|
|
212
|
+
@context.formatter.display_message(:error, usage_text(plugin: plugin))
|
|
213
|
+
Process.exit(0)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Return usage as a Result::Text (used by run_with_result, no display, no exit).
|
|
217
|
+
# @param plugin [Plugins::Base, nil] plugin instance to show subcommands for
|
|
218
|
+
# @return [Result::Text]
|
|
219
|
+
def result_usage(plugin: nil)
|
|
220
|
+
Result::Text.new(usage_text(plugin: plugin))
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Composite option handler for the `log` option (dot-notation sub-properties).
|
|
224
|
+
# Supported sub-properties: +level+, +type+, +format+
|
|
225
|
+
# @param _option_sym [Symbol] Option name (unused, always :log)
|
|
226
|
+
# @param operation [Symbol] +:set+ or +:get+
|
|
227
|
+
# @param value [Hash,nil] Hash of sub-properties to set (only for +:set+)
|
|
228
|
+
def option_log(_option_sym, operation, value = nil)
|
|
229
|
+
Aspera.assert_values(operation, %i[set get])
|
|
230
|
+
case operation
|
|
231
|
+
when :set
|
|
232
|
+
Aspera.assert_type(value, Hash)
|
|
233
|
+
value.each do |k, v|
|
|
234
|
+
case k.to_sym
|
|
235
|
+
when :level then Log.instance.level = v.to_sym
|
|
236
|
+
when :type then Log.instance.logger_type = v.to_sym
|
|
237
|
+
when :format then Log.instance.formatter = v
|
|
238
|
+
when :secrets then SecretHider.instance.log_secrets = BoolValue.true?(v)
|
|
239
|
+
else Aspera.error_unexpected_value(k){'log sub-option (level, type, format, secrets)'}
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
when :get
|
|
243
|
+
return {level: Log.instance.level, type: Log.instance.logger_type, format: Log.instance.formatter, secrets: SecretHider.instance.log_secrets}
|
|
244
|
+
end
|
|
245
|
+
nil
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
private
|
|
249
|
+
|
|
250
|
+
# Build the usage/help text.
|
|
251
|
+
#
|
|
252
|
+
# - No plugin: global options + list of top-level plugins
|
|
253
|
+
# - With plugin: global options + plugin options + subcommands at the path
|
|
254
|
+
# that was reached before --help was encountered
|
|
255
|
+
#
|
|
256
|
+
# @param plugin [Plugins::Base, nil] plugin instance (carries the current dispatch path)
|
|
257
|
+
# @return [String] the full help text
|
|
258
|
+
def usage_text(plugin: nil)
|
|
259
|
+
lines = [@context.options.help_text(banner: app_banner)]
|
|
260
|
+
if plugin.nil?
|
|
261
|
+
# Top-level: list all available plugins
|
|
262
|
+
plugin_names = Plugins::Factory.instance.plugin_list.reject{ |s| s.eql?(COMMAND_CONFIG)}.sort
|
|
263
|
+
lines << "\nPLUGINS"
|
|
264
|
+
col_w = plugin_names.map{ |n| n.to_s.length}.max + 2
|
|
265
|
+
plugin_names.each do |name|
|
|
266
|
+
app = Plugins::Factory.instance.plugin_class(name).application_name
|
|
267
|
+
lines << " #{name.to_s.ljust(col_w)} #{app}"
|
|
268
|
+
end
|
|
269
|
+
else
|
|
270
|
+
path = plugin.help_path || []
|
|
271
|
+
registry = plugin.class.command_registry
|
|
272
|
+
cmds = registry.children_of(path)
|
|
273
|
+
label = plugin.class.name.split('::').last.downcase
|
|
274
|
+
# Build label with positional argument slots inserted after each intermediate node
|
|
275
|
+
# e.g. [:access_keys, :do, :download] -> "node access_keys do <access_key_id> download"
|
|
276
|
+
path.each_with_index do |seg, i|
|
|
277
|
+
label += " #{seg}"
|
|
278
|
+
seg_path = path[0..i]
|
|
279
|
+
seg_spec = registry[seg_path]
|
|
280
|
+
if seg_spec && registry.children_of(seg_path).any? && seg_spec.arguments
|
|
281
|
+
seg_spec.arguments.each do |arg_spec|
|
|
282
|
+
label += " <#{arg_spec.name}>"
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
if cmds.any?
|
|
287
|
+
# Intermediate node: list subcommands
|
|
288
|
+
lines << "\nCOMMANDS: #{label}"
|
|
289
|
+
col_w = cmds.keys.map{ |k| k.to_s.length}.max + 2
|
|
290
|
+
cmds.each do |id, spec|
|
|
291
|
+
lines << " #{id.to_s.ljust(col_w)} #{spec.description}"
|
|
292
|
+
end
|
|
293
|
+
else
|
|
294
|
+
# Leaf node: show description + arguments
|
|
295
|
+
spec = registry[path]
|
|
296
|
+
lines << "\nCOMMAND: #{label}"
|
|
297
|
+
lines << " #{spec.description}" if spec&.description
|
|
298
|
+
display_args = spec&.arguments || []
|
|
299
|
+
# transfer_paths commands use --sources for the file list; default is positional args (@args)
|
|
300
|
+
if spec&.transfer_paths
|
|
301
|
+
file_desc = spec.transfer_paths == :receive \
|
|
302
|
+
? "Remote path(s) to download (default --sources=#{TransferAgent::FILE_LIST_FROM_ARGS}; see also --to-folder)" \
|
|
303
|
+
: "Source file(s) to upload (default --sources=#{TransferAgent::FILE_LIST_FROM_ARGS}; see also --src-type, --to-folder)"
|
|
304
|
+
display_args += [ArgumentSpec.new(name: :source_file, description: file_desc, mandatory: false, multiple: true)]
|
|
305
|
+
end
|
|
306
|
+
if display_args.any?
|
|
307
|
+
lines << "\nARGUMENTS:"
|
|
308
|
+
col_w = display_args.map{ |a| a.name.to_s.length}.max + 2
|
|
309
|
+
display_args.each do |arg|
|
|
310
|
+
flag = arg.mandatory ? arg.name.to_s : "[#{arg.name}]"
|
|
311
|
+
flag += '...' if arg.multiple
|
|
312
|
+
types = case arg.type
|
|
313
|
+
when :identifier then 'identifier'
|
|
314
|
+
when Array then arg.type.map(&:name).join(', ')
|
|
315
|
+
when nil then ''
|
|
316
|
+
else arg.type.name
|
|
317
|
+
end
|
|
318
|
+
hint = arg.type.eql?(Hash) && arg.schema ? " (use 'help' as value to see schema)" : ''
|
|
319
|
+
lines << " #{flag.ljust(col_w)} #{arg.description || types}#{hint}"
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
lines << "\nTIP: use --query=help to list available query parameters" if spec&.query_schema || spec&.entity_execute&.[](:query_schema)
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
lines.join("\n")
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Initialize agents and options
|
|
329
|
+
# This can throw exception if there is a problem with the environment, needs to be caught by execute method
|
|
330
|
+
# @raise [StandardError] if there is a problem with the environment
|
|
331
|
+
# @return [nil]
|
|
332
|
+
def init_agents_and_options
|
|
333
|
+
@context.man_header = true
|
|
334
|
+
# Create formatter, in case there is an exception, it is used to display.
|
|
335
|
+
@context.formatter = Formatter.new
|
|
336
|
+
# Create command line manager with arguments
|
|
337
|
+
@context.options = Parser.new(Info::CMD_NAME, @argv)
|
|
338
|
+
ExtendedValue.instance.on(EXTEND_ARGS){ |v| @context.options.args_as_extended(v)}
|
|
339
|
+
# Formatter: declare metadata (class method), then bind to the instance
|
|
340
|
+
Formatter.declare_options(@context.options)
|
|
341
|
+
@context.formatter.bind_options(@context.options)
|
|
342
|
+
# Compare $0 with expected name
|
|
343
|
+
current_prog_name = File.basename($PROGRAM_NAME)
|
|
344
|
+
Aspera.assert(current_prog_name.eql?(Info::CMD_NAME), type: :warn){"Please use '#{Info::CMD_NAME}' instead of '#{current_prog_name}'"}
|
|
345
|
+
# Declare and parse global options
|
|
346
|
+
declare_global_options
|
|
347
|
+
# Bootstrap: populate context services (main_folder, persistency, presets, http_config,
|
|
348
|
+
# progress_bar) and configure global singletons before any plugin is instantiated.
|
|
349
|
+
# The vault callback is lazy: @vault extended-values are only resolved after Config.new,
|
|
350
|
+
# so @context.config is always set by the time it is called.
|
|
351
|
+
@bootstrapper = Bootstrapper.new(@context)
|
|
352
|
+
@bootstrapper.run(
|
|
353
|
+
gem_plugins_folder: Plugins::Config.gem_plugins_folder,
|
|
354
|
+
vault_value_cb: ->(v){@context.config.vault_value(v)}
|
|
355
|
+
)
|
|
356
|
+
# Do not display config commands if help is asked
|
|
357
|
+
@context.man_header = false
|
|
358
|
+
# Config declares remaining plugin options on top of what Bootstrapper already parsed
|
|
359
|
+
@context.config = Plugins::Config.new(context: @context)
|
|
360
|
+
@context.man_header = true
|
|
361
|
+
# Sync cache_tokens from Config into the OAuth persist_mgr (now that option is parsed)
|
|
362
|
+
OAuth::Factory.instance.persist_mgr = @context.persistency if @context.config.option_cache_tokens
|
|
363
|
+
# Email service: depends on options declared by Config
|
|
364
|
+
@context.mailer = Mailer.new(@context.options, @context.main_folder)
|
|
365
|
+
# Secret finder: depends on options (:secret) and presets, both set by Bootstrapper
|
|
366
|
+
@context.secret_finder = SecretFinder.new(@context.options, @context.presets)
|
|
367
|
+
# The TransferAgent plugin may use the @preset parser
|
|
368
|
+
@context.transfer = TransferAgent.new(@context)
|
|
369
|
+
# Add commands for config plugin after all options have been added
|
|
370
|
+
@context.config.add_manual_header(false)
|
|
371
|
+
@context.validate
|
|
372
|
+
# Set banner when all environment is created so that additional extended value modifiers are known, e.g. @preset
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Generate the application banner for help display
|
|
376
|
+
# @return [String] formatted banner text
|
|
377
|
+
def app_banner
|
|
378
|
+
t = ' ' * 8
|
|
379
|
+
return <<~END_OF_BANNER
|
|
380
|
+
NAME
|
|
381
|
+
#{t}#{Info::CMD_NAME} -- a command line tool for Aspera Applications (v#{Cli::VERSION})
|
|
382
|
+
|
|
383
|
+
SYNOPSIS
|
|
384
|
+
#{t}#{Info::CMD_NAME} COMMANDS [OPTIONS] [ARGS]
|
|
385
|
+
|
|
386
|
+
DESCRIPTION
|
|
387
|
+
#{t}Use Aspera application to perform operations on command line.
|
|
388
|
+
#{t}Documentation and examples: #{Info::GEM_URL}
|
|
389
|
+
#{t}execute: #{Info::CMD_NAME} conf doc
|
|
390
|
+
#{t}or visit: #{Info::DOC_URL}
|
|
391
|
+
#{t}source repo: #{Info::SRC_URL}
|
|
392
|
+
|
|
393
|
+
ENVIRONMENT VARIABLES
|
|
394
|
+
#{t}Any option can be set as an environment variable, refer to the manual
|
|
395
|
+
|
|
396
|
+
COMMANDS
|
|
397
|
+
#{t}To list first level commands, execute: #{Info::CMD_NAME}
|
|
398
|
+
#{t}Note that commands can be written shortened (provided it is unique).
|
|
399
|
+
|
|
400
|
+
OPTIONS
|
|
401
|
+
#{t}Options begin with a '-' (minus), and value is provided on command line.
|
|
402
|
+
#{t}Special values are supported beginning with special prefix @pfx:, where pfx is one of:
|
|
403
|
+
#{t}#{ExtendedValue.instance.modifiers.join(', ')}
|
|
404
|
+
#{t}Dates format is 'DD-MM-YY HH:MM:SS', or 'now' or '-<num>h'
|
|
405
|
+
|
|
406
|
+
ARGS
|
|
407
|
+
#{t}Some commands require mandatory arguments, e.g. a path.
|
|
408
|
+
END_OF_BANNER
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Define header for manual and declare all global options
|
|
412
|
+
# @return [nil]
|
|
413
|
+
def declare_global_options
|
|
414
|
+
Log.log.debug('declare_global_options')
|
|
415
|
+
@context.options.declare(:help, description: 'Show this message', allowed: Allowed::TYPES_NONE, short: 'h') do
|
|
416
|
+
@option_help = true
|
|
417
|
+
@context.options.help_requested = true
|
|
418
|
+
end
|
|
419
|
+
@context.options.declare(:show_config, description: 'Display parameters used for the provided action', allowed: Allowed::TYPES_NONE){@option_show_config = true}
|
|
420
|
+
@context.options.declare(:version, description: 'Display version', allowed: Allowed::TYPES_NONE, short: 'v'){@context.formatter.display_message(:data, Cli::VERSION); Process.exit(0)} # rubocop:disable Style/Semicolon
|
|
421
|
+
@context.options.declare(
|
|
422
|
+
:ui, description: 'Method to start browser',
|
|
423
|
+
allowed: USER_INTERFACES,
|
|
424
|
+
handler: {o: Environment.instance, m: :url_method}
|
|
425
|
+
)
|
|
426
|
+
@context.options.declare(
|
|
427
|
+
:invalid_characters, description: 'Replacement character and invalid filename characters',
|
|
428
|
+
handler: {o: Environment.instance, m: :file_illegal_characters}
|
|
429
|
+
)
|
|
430
|
+
@context.options.declare(:log_level, description: 'Log level', allowed: Log::LEVELS, handler: {o: Log.instance, m: :level})
|
|
431
|
+
@context.options.declare(:log_format, description: 'Log formatter', allowed: [Proc, Logger::Formatter, String], handler: {o: Log.instance, m: :formatter})
|
|
432
|
+
@context.options.declare(:logger, description: 'Logging method', allowed: Log::LOG_TYPES, handler: {o: Log.instance, m: :logger_type})
|
|
433
|
+
@context.options.declare(:log, description: 'Logging options (dot-notation: level, type, format, secrets)', handler: {o: self, m: :option_log}, schema: Schema::Registry::LOG_OPTIONS)
|
|
434
|
+
@context.options.declare(:lock_port, description: 'Prevent dual execution of a command, e.g. in cron', allowed: Allowed::TYPES_INTEGER)
|
|
435
|
+
@context.options.declare(:once_only, description: 'Process only new items (some commands)', allowed: Allowed::TYPES_BOOLEAN, default: false)
|
|
436
|
+
@context.options.declare(:log_secrets, description: 'Show passwords in logs', allowed: Allowed::TYPES_BOOLEAN, handler: {o: SecretHider.instance, m: :log_secrets})
|
|
437
|
+
@context.options.declare(:clean_temp, description: 'Cleanup temporary files on exit', allowed: Allowed::TYPES_BOOLEAN, handler: {o: TempFileManager.instance, m: :cleanup_on_exit})
|
|
438
|
+
@context.options.declare(:temp_folder, description: 'Temporary folder', handler: {o: TempFileManager.instance, m: :global_temp})
|
|
439
|
+
@context.options.declare(:pid_file, description: 'Write process identifier to file, delete on exit')
|
|
440
|
+
@context.options.declare(
|
|
441
|
+
:parser, description: 'Default parser for structured parameters and options',
|
|
442
|
+
handler: {o: ExtendedValue.instance, m: :default_decoder},
|
|
443
|
+
allowed: ExtendedValue::DEFAULT_DECODERS,
|
|
444
|
+
default: ExtendedValue::DEFAULT_DECODERS.first
|
|
445
|
+
)
|
|
446
|
+
# Parse declared options
|
|
447
|
+
@context.options.parse_options!
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Get the plugin instance based on name
|
|
451
|
+
# Also loads the plugin options, and default values from conf file
|
|
452
|
+
# @param plugin_name_sym [Symbol] symbol for plugin name
|
|
453
|
+
# @return [Plugins::Base] the plugin instance
|
|
454
|
+
def get_plugin_instance_with_options(plugin_name_sym)
|
|
455
|
+
Log.log.debug{"get_plugin_instance_with_options(#{plugin_name_sym})"}
|
|
456
|
+
# Load default preset options for this plugin from config file
|
|
457
|
+
default_config_name = @context.presets.plugin_default_name(plugin_name_sym)
|
|
458
|
+
Log.log.debug{"add_plugin_default_preset:#{plugin_name_sym}:#{default_config_name}"}
|
|
459
|
+
@context.options.add_option_preset(@context.presets.by_name(default_config_name), 'default_plugin', override: false) unless default_config_name.nil?
|
|
460
|
+
command_plugin = Plugins::Factory.instance.create(plugin_name_sym, context: @context)
|
|
461
|
+
return command_plugin
|
|
462
|
+
end
|
|
463
|
+
COMMAND_CONFIG = :config
|
|
464
|
+
COMMAND_HELP = :help
|
|
465
|
+
# Types that go to result of type = text
|
|
466
|
+
SCALAR_TYPES = [String, Integer, Symbol].freeze
|
|
467
|
+
USER_INTERFACES = %i[text graphical].freeze
|
|
468
|
+
EXTEND_ARGS = :''
|
|
469
|
+
|
|
470
|
+
private_constant :COMMAND_CONFIG, :COMMAND_HELP, :SCALAR_TYPES, :USER_INTERFACES, :EXTEND_ARGS
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/log'
|
|
4
|
+
|
|
5
|
+
module Aspera
|
|
6
|
+
module Cli
|
|
7
|
+
# Resolves the secret (password) for a given URL + username pair.
|
|
8
|
+
# Injected into Context as :secret_finder so that any component
|
|
9
|
+
# (plugins, Api::AoC, ...) can look up secrets without going through Plugins::Config.
|
|
10
|
+
class SecretFinder
|
|
11
|
+
# Special value for the :secret option that triggers a preset lookup
|
|
12
|
+
PRESET_MAGIC = 'PRESET'
|
|
13
|
+
|
|
14
|
+
# @param options [Parser] CLI options manager (provides :secret)
|
|
15
|
+
# @param presets [PresetManager] preset resolver (provides #lookup_preset)
|
|
16
|
+
def initialize(options, presets)
|
|
17
|
+
@options = options
|
|
18
|
+
@presets = presets
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Return the secret for the given URL + username.
|
|
22
|
+
# If the :secret option equals 'PRESET', the preset store is searched for a
|
|
23
|
+
# matching url/username entry and its 'password' field is returned.
|
|
24
|
+
# @param url [String]
|
|
25
|
+
# @param username [String]
|
|
26
|
+
# @return [String, nil]
|
|
27
|
+
def lookup(url:, username:)
|
|
28
|
+
secret = @options.get_option(:secret)
|
|
29
|
+
if secret.eql?(PRESET_MAGIC)
|
|
30
|
+
conf = @presets.lookup_preset(url: url, username: username)
|
|
31
|
+
if conf.is_a?(Hash)
|
|
32
|
+
Log.log.debug{"Found preset #{conf} with URL and username"}
|
|
33
|
+
secret = conf['password']
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
secret
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|