aspera-cli 4.26.2 → 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 +281 -21
- data/CONTRIBUTING.md +98 -7
- data/bin/ascli +3 -3
- 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 +26 -24
- data/lib/aspera/agent/httpgw.rb +1 -1
- data/lib/aspera/agent/node.rb +32 -0
- data/lib/aspera/agent/transferd.rb +39 -3
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +14 -16
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/faspex.rb +15 -14
- data/lib/aspera/api/httpgw.rb +4 -4
- data/lib/aspera/api/node.rb +26 -23
- 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 +21 -19
- data/lib/aspera/ascp/installation.rb +37 -38
- 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 +16 -3
- data/lib/aspera/cli/error.rb +16 -0
- data/lib/aspera/cli/extended_value.rb +2 -1
- data/lib/aspera/cli/formatter.rb +41 -27
- data/lib/aspera/cli/gem_checker.rb +65 -0
- data/lib/aspera/cli/hints.rb +2 -2
- data/lib/aspera/cli/http.rb +70 -19
- 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} +334 -142
- data/lib/aspera/cli/plugins/alee.rb +20 -22
- data/lib/aspera/cli/plugins/aoc.rb +1166 -853
- data/lib/aspera/cli/plugins/ats.rb +200 -161
- data/lib/aspera/cli/plugins/base.rb +457 -100
- data/lib/aspera/cli/plugins/basic_auth.rb +14 -4
- data/lib/aspera/cli/plugins/config.rb +428 -754
- 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 +4 -4
- data/lib/aspera/cli/plugins/faspex.rb +294 -257
- data/lib/aspera/cli/plugins/faspex5.rb +585 -357
- 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 +998 -794
- data/lib/aspera/cli/plugins/oauth.rb +7 -10
- data/lib/aspera/cli/plugins/orchestrator.rb +111 -134
- data/lib/aspera/cli/plugins/preview.rb +180 -139
- data/lib/aspera/cli/plugins/server.rb +124 -79
- 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 +13 -12
- data/lib/aspera/cli/result.rb +61 -11
- data/lib/aspera/cli/runner.rb +275 -141
- 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 +83 -59
- data/lib/aspera/cli/terminal_formatter.rb +1 -1
- 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 +3 -3
- data/lib/aspera/cli/vault_manager.rb +57 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +21 -20
- data/lib/aspera/command_line_builder.rb +22 -22
- data/lib/aspera/data_repository.rb +2 -1
- data/lib/aspera/dot_container.rb +4 -4
- data/lib/aspera/environment.rb +18 -11
- data/lib/aspera/exec_spec.rb +13 -0
- data/lib/aspera/faspex_gw.rb +1 -1
- 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/encrypted_hash.rb +1 -1
- data/lib/aspera/keychain/factory.rb +3 -3
- data/lib/aspera/keychain/macos_security.rb +3 -3
- data/lib/aspera/link_header.rb +82 -0
- data/lib/aspera/log.rb +17 -3
- data/lib/aspera/markdown.rb +85 -2
- data/lib/aspera/node_simulator.rb +3 -1
- data/lib/aspera/oauth/base.rb +5 -5
- data/lib/aspera/oauth/boot.rb +2 -2
- data/lib/aspera/oauth/factory.rb +7 -5
- data/lib/aspera/oauth/jwt.rb +4 -4
- data/lib/aspera/oauth/web.rb +2 -2
- data/lib/aspera/persistency_action_once.rb +8 -8
- data/lib/aspera/persistency_folder.rb +10 -3
- data/lib/aspera/preview/file_types.rb +3 -3
- data/lib/aspera/preview/generator.rb +3 -3
- data/lib/aspera/preview/utils.rb +1 -1
- data/lib/aspera/products/connect.rb +1 -1
- data/lib/aspera/products/other.rb +1 -1
- data/lib/aspera/products/transferd.rb +1 -1
- data/lib/aspera/proxy_auto_config.rb +7 -6
- data/lib/aspera/rest.rb +10 -36
- data/lib/aspera/rest_error_analyzer.rb +3 -3
- data/lib/aspera/rest_errors_aspera.rb +0 -10
- data/lib/aspera/rest_list.rb +3 -3
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1853 -510
- data/lib/aspera/schema/documentation.rb +57 -26
- data/lib/aspera/schema/reader.rb +59 -10
- data/lib/aspera/schema/registry.rb +39 -6
- data/lib/aspera/secret_hider.rb +7 -2
- data/lib/aspera/ssl.rb +4 -4
- data/lib/aspera/sync/conf.schema.yaml +2 -2
- data/lib/aspera/sync/operations.rb +10 -13
- data/lib/aspera/temp_file_manager.rb +1 -1
- data/lib/aspera/timer_limiter.rb +1 -1
- data/lib/aspera/transfer/faux_file.rb +24 -11
- data/lib/aspera/transfer/parameters.rb +23 -21
- data/lib/aspera/transfer/result.rb +74 -0
- data/lib/aspera/transfer/resumer.rb +6 -6
- data/lib/aspera/transfer/spec.rb +18 -1
- data/lib/aspera/transfer/spec.schema.yaml +12 -3
- 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 +25 -3
- metadata.gz.sig +0 -0
- data/lib/aspera/json_rpc.rb +0 -52
data/lib/aspera/cli/runner.rb
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'aspera/cli/command_spec'
|
|
3
4
|
require 'aspera/cli/context'
|
|
4
|
-
require 'aspera/cli/
|
|
5
|
+
require 'aspera/cli/parser'
|
|
5
6
|
require 'aspera/cli/formatter'
|
|
6
7
|
require 'aspera/cli/plugins/factory'
|
|
8
|
+
require 'aspera/cli/bootstrapper'
|
|
7
9
|
require 'aspera/cli/plugins/config'
|
|
10
|
+
require 'aspera/cli/mailer'
|
|
11
|
+
require 'aspera/cli/secret_finder'
|
|
8
12
|
require 'aspera/cli/extended_value'
|
|
9
13
|
require 'aspera/cli/transfer_agent'
|
|
10
14
|
require 'aspera/cli/version'
|
|
11
15
|
require 'aspera/cli/info'
|
|
12
16
|
require 'aspera/cli/hints'
|
|
13
17
|
require 'aspera/cli/result'
|
|
18
|
+
require 'aspera/transfer/result'
|
|
14
19
|
require 'aspera/secret_hider'
|
|
15
20
|
require 'aspera/log'
|
|
16
21
|
require 'aspera/assert'
|
|
@@ -25,38 +30,45 @@ module Aspera
|
|
|
25
30
|
class Runner
|
|
26
31
|
# Plugins store transfer result using this key and use result_transfer_multiple()
|
|
27
32
|
STATUS_FIELD = 'status'
|
|
28
|
-
COMMAND_CONFIG = :config
|
|
29
|
-
COMMAND_HELP = :help
|
|
30
|
-
# Types that go to result of type = text
|
|
31
|
-
SCALAR_TYPES = [String, Integer, Symbol].freeze
|
|
32
|
-
USER_INTERFACES = %i[text graphical].freeze
|
|
33
|
-
|
|
34
|
-
private_constant :COMMAND_CONFIG, :COMMAND_HELP, :SCALAR_TYPES, :USER_INTERFACES
|
|
35
33
|
|
|
36
34
|
class << self
|
|
37
|
-
# Process
|
|
38
|
-
# @param
|
|
39
|
-
# @raise [
|
|
40
|
-
# @return [Result]
|
|
41
|
-
def result_transfer(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
45
50
|
end
|
|
46
51
|
|
|
47
52
|
# Used when one command executes several transfer jobs (each job being possibly multi session)
|
|
48
|
-
# @param status_table [Array] [{STATUS_FIELD=>
|
|
53
|
+
# @param status_table [Array] [{STATUS_FIELD=>Transfer::Result,...},...]
|
|
49
54
|
# @return [Result] a status object suitable as command result
|
|
50
|
-
# Each element has a key STATUS_FIELD which contains the result of possibly multiple sessions
|
|
51
55
|
def result_transfer_multiple(status_table)
|
|
52
|
-
|
|
53
|
-
# Transform status array into string and find if there was problem
|
|
56
|
+
failed_result = nil
|
|
54
57
|
status_table.each do |item|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
58
70
|
end
|
|
59
|
-
raise
|
|
71
|
+
raise failed_result.exception unless failed_result.nil?
|
|
60
72
|
return Result::ObjectList.new(status_table)
|
|
61
73
|
end
|
|
62
74
|
end
|
|
@@ -72,73 +84,80 @@ module Aspera
|
|
|
72
84
|
@context = Context.new
|
|
73
85
|
end
|
|
74
86
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
show_usage if @option_help && @context.options.command_or_arg_empty?
|
|
90
|
-
@context.config.periodic_check_newer_gem_version
|
|
91
|
-
command_sym =
|
|
92
|
-
if @option_show_config && @context.options.command_or_arg_empty?
|
|
93
|
-
COMMAND_CONFIG
|
|
94
|
-
else
|
|
95
|
-
@context.options.get_next_command(Plugins::Factory.instance.plugin_list.unshift(COMMAND_HELP))
|
|
96
|
-
end
|
|
97
|
-
# Command will not be executed, but we need manual
|
|
98
|
-
@context.options.fail_on_missing_mandatory = false if @option_help || @option_show_config
|
|
99
|
-
# Main plugin is not dynamically instantiated
|
|
100
|
-
case command_sym
|
|
101
|
-
when COMMAND_HELP
|
|
102
|
-
show_usage
|
|
103
|
-
when COMMAND_CONFIG
|
|
104
|
-
command_plugin = @context.config
|
|
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
|
|
105
101
|
else
|
|
106
|
-
|
|
107
|
-
command_plugin = get_plugin_instance_with_options(command_sym)
|
|
108
|
-
# Parse plugin specific options
|
|
109
|
-
@context.options.parse_options!
|
|
102
|
+
@context.options.get_next_command(Plugins::Factory.instance.plugin_list.unshift(COMMAND_HELP))
|
|
110
103
|
end
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
115
132
|
execute_command = false
|
|
133
|
+
Log.log.warn{"Another instance is already running (#{e.message})."}
|
|
116
134
|
end
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
pid_file = @context.options.get_option(:pid_file)
|
|
131
|
-
if !pid_file.nil?
|
|
132
|
-
File.write(pid_file, Process.pid)
|
|
133
|
-
Log.log.debug{"Wrote pid #{Process.pid} to #{pid_file}"}
|
|
134
|
-
at_exit{File.delete(pid_file)}
|
|
135
|
-
end
|
|
136
|
-
# Execute and display (if not exclusive execution)
|
|
137
|
-
@context.formatter.display_results(command_plugin.execute_action) if execute_command
|
|
138
|
-
# Save config file if command modified it
|
|
139
|
-
@context.config.save_config_file_if_needed
|
|
140
|
-
# Finish
|
|
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
|
|
141
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
|
|
142
161
|
rescue Net::SSH::AuthenticationFailed => e; exception_info = {e: e, t: 'SSH', security: true}
|
|
143
162
|
rescue OpenSSL::SSL::SSLError => e; exception_info = {e: e, t: 'SSL'}
|
|
144
163
|
rescue Cli::BadArgument => e; exception_info = {e: e, t: 'Argument', usage: true}
|
|
@@ -152,17 +171,13 @@ module Aspera
|
|
|
152
171
|
rescue StandardError => e; exception_info = {e: e, t: "Other(#{e.class.name})", debug: true}
|
|
153
172
|
rescue Interrupt => e; exception_info = {e: e, t: 'Interruption', debug: true}
|
|
154
173
|
end
|
|
155
|
-
# Cleanup file list files
|
|
156
|
-
TempFileManager.instance.cleanup
|
|
157
174
|
# 1- processing of error condition
|
|
158
175
|
unless exception_info.nil?
|
|
159
176
|
Log.log.warn(exception_info[:e].message) if Log.instance.logger_type.eql?(:syslog) && exception_info[:security]
|
|
160
177
|
Log.log.error{"#{exception_info[:t]}: #{exception_info[:e].message}"} unless exception_info[:e].is_a?(Cli::SchemaRequest)
|
|
161
178
|
Log.log.debug{(['Backtrace:'] + exception_info[:e].backtrace).join("\n")} if exception_info[:debug]
|
|
162
179
|
@context.formatter.display_message(:error, 'Use option -h to get help.') if exception_info[:usage]
|
|
163
|
-
# Is that a known error condition with proposal for remediation ?
|
|
164
180
|
Hints.hint_for(exception_info[:e], @context.formatter)
|
|
165
|
-
# Requested help for a Hash parameter/option ?
|
|
166
181
|
if exception_info[:e].is_a?(Cli::SchemaRequest)
|
|
167
182
|
Log.log.info{"#{exception_info[:t]}: #{exception_info[:e].message}"}
|
|
168
183
|
schema_path = exception_info[:e].path
|
|
@@ -174,51 +189,142 @@ module Aspera
|
|
|
174
189
|
end
|
|
175
190
|
end
|
|
176
191
|
end
|
|
177
|
-
# 2- processing of
|
|
178
|
-
|
|
179
|
-
@context.options
|
|
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|
|
|
180
195
|
Log.log.error{"Argument: #{msg}"}
|
|
181
|
-
# Add code as exception if there is not already an error
|
|
182
196
|
exception_info = {e: Exception.new(msg), t: 'UnusedArg'} if exception_info.nil?
|
|
183
197
|
end
|
|
184
198
|
end
|
|
185
|
-
# 3-
|
|
199
|
+
# 3- exit on error
|
|
186
200
|
unless exception_info.nil?
|
|
187
|
-
# Show stack trace in debug mode
|
|
188
201
|
raise exception_info[:e] if Log.log.debug?
|
|
189
|
-
# Else give hint and exit
|
|
190
202
|
@context.formatter.display_message(:error, 'Use --log-level=debug to get more details.') if exception_info[:debug]
|
|
191
203
|
Process.exit(1)
|
|
192
204
|
end
|
|
193
205
|
return
|
|
194
206
|
end
|
|
195
207
|
|
|
196
|
-
# Display usage information and
|
|
197
|
-
# @param
|
|
198
|
-
# @param exit [Boolean] if true, exit the process after displaying help
|
|
208
|
+
# Display usage information and exit (used by the interactive CLI).
|
|
209
|
+
# @param plugin [Plugins::Base, nil] plugin instance to show subcommands for
|
|
199
210
|
# @return [nil]
|
|
200
|
-
def show_usage(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
215
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}
|
|
216
244
|
end
|
|
217
|
-
|
|
245
|
+
nil
|
|
218
246
|
end
|
|
219
247
|
|
|
220
248
|
private
|
|
221
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
|
+
|
|
222
328
|
# Initialize agents and options
|
|
223
329
|
# This can throw exception if there is a problem with the environment, needs to be caught by execute method
|
|
224
330
|
# @raise [StandardError] if there is a problem with the environment
|
|
@@ -228,28 +334,42 @@ module Aspera
|
|
|
228
334
|
# Create formatter, in case there is an exception, it is used to display.
|
|
229
335
|
@context.formatter = Formatter.new
|
|
230
336
|
# Create command line manager with arguments
|
|
231
|
-
@context.options =
|
|
232
|
-
|
|
233
|
-
|
|
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)
|
|
234
342
|
# Compare $0 with expected name
|
|
235
343
|
current_prog_name = File.basename($PROGRAM_NAME)
|
|
236
344
|
Aspera.assert(current_prog_name.eql?(Info::CMD_NAME), type: :warn){"Please use '#{Info::CMD_NAME}' instead of '#{current_prog_name}'"}
|
|
237
345
|
# Declare and parse global options
|
|
238
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
|
+
)
|
|
239
356
|
# Do not display config commands if help is asked
|
|
240
357
|
@context.man_header = false
|
|
241
|
-
#
|
|
358
|
+
# Config declares remaining plugin options on top of what Bootstrapper already parsed
|
|
242
359
|
@context.config = Plugins::Config.new(context: @context)
|
|
243
360
|
@context.man_header = true
|
|
244
|
-
#
|
|
245
|
-
|
|
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)
|
|
246
367
|
# The TransferAgent plugin may use the @preset parser
|
|
247
|
-
@context.transfer = TransferAgent.new(@context
|
|
368
|
+
@context.transfer = TransferAgent.new(@context)
|
|
248
369
|
# Add commands for config plugin after all options have been added
|
|
249
370
|
@context.config.add_manual_header(false)
|
|
250
371
|
@context.validate
|
|
251
372
|
# Set banner when all environment is created so that additional extended value modifiers are known, e.g. @preset
|
|
252
|
-
@context.options.parser.banner = app_banner
|
|
253
373
|
end
|
|
254
374
|
|
|
255
375
|
# Generate the application banner for help display
|
|
@@ -292,29 +412,33 @@ module Aspera
|
|
|
292
412
|
# @return [nil]
|
|
293
413
|
def declare_global_options
|
|
294
414
|
Log.log.debug('declare_global_options')
|
|
295
|
-
@context.options.declare(:help, 'Show this message', allowed: Allowed::TYPES_NONE, short: 'h')
|
|
296
|
-
|
|
297
|
-
|
|
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
|
|
298
421
|
@context.options.declare(
|
|
299
|
-
:ui, 'Method to start browser',
|
|
422
|
+
:ui, description: 'Method to start browser',
|
|
300
423
|
allowed: USER_INTERFACES,
|
|
301
424
|
handler: {o: Environment.instance, m: :url_method}
|
|
302
425
|
)
|
|
303
426
|
@context.options.declare(
|
|
304
|
-
:invalid_characters, 'Replacement character and invalid filename characters',
|
|
427
|
+
:invalid_characters, description: 'Replacement character and invalid filename characters',
|
|
305
428
|
handler: {o: Environment.instance, m: :file_illegal_characters}
|
|
306
429
|
)
|
|
307
|
-
@context.options.declare(:log_level, 'Log level', allowed: Log::LEVELS, handler: {o: Log.instance, m: :level})
|
|
308
|
-
@context.options.declare(:log_format, 'Log formatter', allowed: [Proc, Logger::Formatter, String], handler: {o: Log.instance, m: :formatter})
|
|
309
|
-
@context.options.declare(:logger, 'Logging method', allowed: Log::LOG_TYPES, handler: {o: Log.instance, m: :logger_type})
|
|
310
|
-
@context.options.declare(:
|
|
311
|
-
@context.options.declare(:
|
|
312
|
-
@context.options.declare(:
|
|
313
|
-
@context.options.declare(:
|
|
314
|
-
@context.options.declare(:
|
|
315
|
-
@context.options.declare(:
|
|
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')
|
|
316
440
|
@context.options.declare(
|
|
317
|
-
:parser, 'Default parser for structured parameters and options',
|
|
441
|
+
:parser, description: 'Default parser for structured parameters and options',
|
|
318
442
|
handler: {o: ExtendedValue.instance, m: :default_decoder},
|
|
319
443
|
allowed: ExtendedValue::DEFAULT_DECODERS,
|
|
320
444
|
default: ExtendedValue::DEFAULT_DECODERS.first
|
|
@@ -329,11 +453,21 @@ module Aspera
|
|
|
329
453
|
# @return [Plugins::Base] the plugin instance
|
|
330
454
|
def get_plugin_instance_with_options(plugin_name_sym)
|
|
331
455
|
Log.log.debug{"get_plugin_instance_with_options(#{plugin_name_sym})"}
|
|
332
|
-
# Load default
|
|
333
|
-
@context.
|
|
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?
|
|
334
460
|
command_plugin = Plugins::Factory.instance.create(plugin_name_sym, context: @context)
|
|
335
461
|
return command_plugin
|
|
336
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
|
|
337
471
|
end
|
|
338
472
|
end
|
|
339
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
|