aspera-cli 4.26.0 → 4.26.2
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 +36 -4
- data/CONTRIBUTING.md +2 -5
- data/bin/ascli +2 -2
- data/lib/aspera/agent/direct.rb +1 -1
- data/lib/aspera/agent/factory.rb +4 -0
- data/lib/aspera/agent/httpgw.rb +1 -1
- data/lib/aspera/agent/node.rb +2 -2
- data/lib/aspera/api/aoc.rb +45 -37
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/faspex.rb +7 -6
- data/lib/aspera/api/node.rb +9 -9
- data/lib/aspera/ascmd.rb +4 -4
- data/lib/aspera/ascp/installation.rb +13 -6
- data/lib/aspera/assert.rb +17 -13
- data/lib/aspera/cli/context.rb +58 -0
- data/lib/aspera/cli/extended_value.rb +9 -4
- data/lib/aspera/cli/formatter.rb +91 -187
- data/lib/aspera/cli/http.rb +167 -0
- data/lib/aspera/cli/manager.rb +132 -77
- data/lib/aspera/cli/options.schema.yaml +82 -0
- data/lib/aspera/cli/plugins/alee.rb +2 -2
- data/lib/aspera/cli/plugins/aoc.rb +113 -77
- data/lib/aspera/cli/plugins/ats.rb +16 -16
- data/lib/aspera/cli/plugins/base.rb +64 -49
- data/lib/aspera/cli/plugins/config.rb +172 -444
- data/lib/aspera/cli/plugins/console.rb +7 -7
- data/lib/aspera/cli/plugins/cos.rb +2 -2
- data/lib/aspera/cli/plugins/factory.rb +3 -0
- data/lib/aspera/cli/plugins/faspex.rb +22 -22
- data/lib/aspera/cli/plugins/faspex5.rb +68 -49
- data/lib/aspera/cli/plugins/faspio.rb +1 -1
- data/lib/aspera/cli/plugins/httpgw.rb +2 -2
- data/lib/aspera/cli/plugins/node.rb +80 -80
- data/lib/aspera/cli/plugins/orchestrator.rb +21 -26
- data/lib/aspera/cli/plugins/preview.rb +9 -9
- data/lib/aspera/cli/plugins/server.rb +7 -7
- data/lib/aspera/cli/plugins/shares.rb +2 -2
- data/lib/aspera/cli/preset_manager.rb +235 -0
- data/lib/aspera/cli/result.rb +310 -0
- data/lib/aspera/cli/{main.rb → runner.rb} +48 -130
- data/lib/aspera/cli/sync_actions.rb +15 -11
- data/lib/aspera/cli/terminal_formatter.rb +65 -0
- data/lib/aspera/cli/transfer_agent.rb +17 -15
- data/lib/aspera/cli/transfer_progress.rb +6 -6
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +5 -6
- data/lib/aspera/command_line_builder.rb +22 -18
- data/lib/aspera/data_repository.rb +4 -0
- data/lib/aspera/dot_container.rb +1 -1
- data/lib/aspera/environment.rb +25 -6
- data/lib/aspera/faspex_gw.rb +1 -1
- data/lib/aspera/formatter_interface.rb +14 -0
- data/lib/aspera/hash_ext.rb +6 -0
- data/lib/aspera/json_rpc.rb +6 -5
- data/lib/aspera/keychain/base.rb +1 -1
- data/lib/aspera/keychain/encrypted_hash.rb +1 -1
- data/lib/aspera/keychain/factory.rb +1 -1
- data/lib/aspera/keychain/macos_security.rb +1 -1
- data/lib/aspera/log.rb +10 -5
- data/lib/aspera/markdown.rb +4 -1
- data/lib/aspera/nagios.rb +2 -2
- data/lib/aspera/oauth/base.rb +5 -5
- data/lib/aspera/oauth/boot.rb +43 -0
- data/lib/aspera/oauth/factory.rb +38 -14
- data/lib/aspera/oauth/web.rb +2 -2
- data/lib/aspera/oauth.rb +1 -0
- data/lib/aspera/persistency_action_once.rb +2 -2
- data/lib/aspera/preview/file_types.rb +4 -0
- data/lib/aspera/products/connect.rb +3 -0
- data/lib/aspera/products/transferd.rb +2 -2
- data/lib/aspera/proxy_auto_config.rb +6 -3
- data/lib/aspera/rest.rb +9 -5
- data/lib/aspera/rest_error_analyzer.rb +4 -0
- data/lib/aspera/rest_list.rb +10 -3
- data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +62811 -0
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +9637 -0
- data/lib/aspera/schema/async_tables.yaml +361 -0
- data/lib/aspera/schema/documentation.rb +107 -0
- data/lib/aspera/schema/reader.rb +75 -0
- data/lib/aspera/schema/registry.rb +70 -0
- data/lib/aspera/secret_hider.rb +4 -0
- data/lib/aspera/sync/conf.schema.yaml +0 -26
- data/lib/aspera/sync/database.rb +14 -10
- data/lib/aspera/sync/operations.rb +14 -10
- data/lib/aspera/temp_file_manager.rb +4 -0
- data/lib/aspera/transfer/faux_file.rb +1 -1
- data/lib/aspera/transfer/parameters.rb +4 -4
- data/lib/aspera/transfer/resumer.rb +2 -2
- data/lib/aspera/transfer/spec.rb +3 -3
- data/lib/aspera/transfer/spec.schema.yaml +18 -9
- data/lib/aspera/transfer/uri.rb +1 -1
- data/lib/aspera/uri_reader.rb +1 -1
- data/lib/aspera/yaml.rb +4 -2
- data.tar.gz.sig +0 -0
- metadata +28 -15
- metadata.gz.sig +1 -1
- data/lib/aspera/transfer/spec_doc.rb +0 -76
|
@@ -198,7 +198,7 @@ module Aspera
|
|
|
198
198
|
# @param destination [String, nil] local destination root for receive operations
|
|
199
199
|
# @return [Object] transfer result returned by [`Main.result_transfer`](lib/aspera/cli/main.rb)
|
|
200
200
|
def do_transfer(direction, folder_id, source_filename, destination = '/')
|
|
201
|
-
Aspera.assert(!(destination.nil? && direction.eql?(Transfer::Spec::DIRECTION_RECEIVE)))
|
|
201
|
+
Aspera.assert(!(destination.nil? && direction.eql?(Transfer::Spec::DIRECTION_RECEIVE)), 'destination must be set for receive direction')
|
|
202
202
|
t_spec = @api_node.transfer_spec_gen4(folder_id, direction, {
|
|
203
203
|
'paths' => [{'source' => source_filename}],
|
|
204
204
|
'tags' => {Transfer::Spec::TAG_RESERVED => {PREV_GEN_TAG => true}}
|
|
@@ -206,7 +206,7 @@ module Aspera
|
|
|
206
206
|
# Force the destination on the transfer agent object.
|
|
207
207
|
# Setting `t_spec['destination_root']` directly would later be overwritten.
|
|
208
208
|
transfer.user_transfer_spec['destination_root'] = destination
|
|
209
|
-
|
|
209
|
+
Runner.result_transfer(transfer.start(t_spec))
|
|
210
210
|
end
|
|
211
211
|
|
|
212
212
|
# Populate generation metadata for a source file available on the local filesystem.
|
|
@@ -323,7 +323,7 @@ module Aspera
|
|
|
323
323
|
# create folder if needed
|
|
324
324
|
FileUtils.mkdir_p(local_entry_preview_dir)
|
|
325
325
|
if @access_remote
|
|
326
|
-
Aspera.assert(!entry['parent_file_id'].nil
|
|
326
|
+
Aspera.assert(!entry['parent_file_id'].nil?, 'missing parent_file_id in entry')
|
|
327
327
|
# download original file to temp folder
|
|
328
328
|
do_transfer(Transfer::Spec::DIRECTION_RECEIVE, entry['parent_file_id'], entry['name'], @tmp_folder)
|
|
329
329
|
end
|
|
@@ -471,7 +471,7 @@ module Aspera
|
|
|
471
471
|
end
|
|
472
472
|
@filter_block = Api::Node.file_matcher_from_argument(options)
|
|
473
473
|
scan_folder_files(folder_info, scan_path)
|
|
474
|
-
return
|
|
474
|
+
return Result::Status.new('scan finished')
|
|
475
475
|
when :events, :trevents
|
|
476
476
|
@filter_block = Api::Node.file_matcher_from_argument(options)
|
|
477
477
|
iteration_persistency = nil
|
|
@@ -489,15 +489,15 @@ module Aspera
|
|
|
489
489
|
end
|
|
490
490
|
# call processing method specified by command line command
|
|
491
491
|
send(:"process_#{command}", iteration_persistency)
|
|
492
|
-
return
|
|
492
|
+
return Result::Status.new("#{command} finished")
|
|
493
493
|
when :check
|
|
494
|
-
return
|
|
494
|
+
return Result::Status.new('Tools validated')
|
|
495
495
|
when :test
|
|
496
496
|
source = options.get_next_argument('source file')
|
|
497
497
|
format = options.get_next_argument('format', accept_list: Aspera::Preview::Generator::PREVIEW_FORMATS, default: :png)
|
|
498
498
|
generated_file_path = preview_filename(format, options.get_option(:base))
|
|
499
499
|
Aspera::Preview::Generator.new(source, generated_file_path, @gen_options, @tmp_folder).generate
|
|
500
|
-
return
|
|
500
|
+
return Result::Status.new("generated: #{generated_file_path}")
|
|
501
501
|
when :show
|
|
502
502
|
source = options.get_next_argument('source file')
|
|
503
503
|
# terminal_options = options.get_next_argument('options', validation: Hash, default: {}).symbolize_keys
|
|
@@ -505,8 +505,8 @@ module Aspera
|
|
|
505
505
|
Aspera::Preview::Generator.new(source, generated_file_path, @gen_options, @tmp_folder).generate
|
|
506
506
|
formatter.display_status("generated: #{generated_file_path}")
|
|
507
507
|
# formatter.display_status(Aspera::Preview::Terminal.build(File.read(generated_file_path), **terminal_options))
|
|
508
|
-
# return
|
|
509
|
-
return
|
|
508
|
+
# return Result::Status.new("generated: #{generated_file_path}")
|
|
509
|
+
return Result::Image.new(UriReader.file_url(generated_file_path))
|
|
510
510
|
else Aspera.error_unexpected_value(command)
|
|
511
511
|
end
|
|
512
512
|
ensure
|
|
@@ -165,7 +165,7 @@ module Aspera
|
|
|
165
165
|
end
|
|
166
166
|
# if user provided transfer spec has a token, we will use bypass keys
|
|
167
167
|
cred_set = true if transfer.user_transfer_spec['token'].is_a?(String)
|
|
168
|
-
Aspera.assert(cred_set,
|
|
168
|
+
Aspera.assert(cred_set, 'Either password, key , or transfer spec token must be provided', type: BadArgument)
|
|
169
169
|
return server_transfer_spec
|
|
170
170
|
end
|
|
171
171
|
|
|
@@ -173,7 +173,7 @@ module Aspera
|
|
|
173
173
|
case command
|
|
174
174
|
when :upload, :download
|
|
175
175
|
transfer_spec['direction'] = Transfer::Spec.transfer_type_to_direction(command)
|
|
176
|
-
return
|
|
176
|
+
return Runner.result_transfer(transfer.start(transfer_spec))
|
|
177
177
|
when :sync
|
|
178
178
|
# lets ignore the arguments provided by execute_sync_action, we just give the transfer spec
|
|
179
179
|
return execute_sync_action{transfer_spec}
|
|
@@ -218,7 +218,7 @@ module Aspera
|
|
|
218
218
|
end
|
|
219
219
|
else Aspera.error_unexpected_value(command_nagios)
|
|
220
220
|
end
|
|
221
|
-
|
|
221
|
+
Result::ObjectList.new(nagios.status_list)
|
|
222
222
|
when *TRANSFER_COMMANDS
|
|
223
223
|
return execute_transfer(command, server_transfer_spec)
|
|
224
224
|
when *AsCmd::OPERATIONS
|
|
@@ -228,13 +228,13 @@ module Aspera
|
|
|
228
228
|
result = ascmd.execute_single(command, command_arguments)
|
|
229
229
|
case command
|
|
230
230
|
when :mkdir, :mv, :cp, :rm
|
|
231
|
-
return
|
|
231
|
+
return Result::Success.new
|
|
232
232
|
when :ls
|
|
233
|
-
return
|
|
233
|
+
return Result::ObjectList.new(result.map(&:stringify_keys), fields: %w[zmode zuid zgid size mtime name])
|
|
234
234
|
when :df
|
|
235
|
-
return
|
|
235
|
+
return Result::ObjectList.new(result.map(&:stringify_keys))
|
|
236
236
|
when :du, :md5sum, :info
|
|
237
|
-
return
|
|
237
|
+
return Result::SingleObject.new(result.stringify_keys)
|
|
238
238
|
end
|
|
239
239
|
rescue AsCmd::Error => e
|
|
240
240
|
raise Cli::BadArgument, e.extended_message
|
|
@@ -118,9 +118,9 @@ module Aspera
|
|
|
118
118
|
else
|
|
119
119
|
nagios.add_critical('API', health[:api].to_s)
|
|
120
120
|
end
|
|
121
|
-
|
|
121
|
+
Result::ObjectList.new(nagios.status_list)
|
|
122
122
|
when :info
|
|
123
|
-
return
|
|
123
|
+
return Result::SingleObject.new(basic_auth_api(NODE_API_PATH).read('info', headers: {'Content-Type'=>'application/json'}))
|
|
124
124
|
when :files
|
|
125
125
|
api_shares_node = basic_auth_api(NODE_API_PATH)
|
|
126
126
|
repo_command = options.get_next_command(Node::COMMANDS_SHARES)
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/cli/extended_value'
|
|
4
|
+
require 'aspera/cli/version'
|
|
5
|
+
require 'aspera/cli/info'
|
|
6
|
+
require 'aspera/log'
|
|
7
|
+
require 'aspera/assert'
|
|
8
|
+
require 'digest'
|
|
9
|
+
require 'yaml'
|
|
10
|
+
|
|
11
|
+
module Aspera
|
|
12
|
+
module Cli
|
|
13
|
+
# Manages the YAML config file and all preset-related operations.
|
|
14
|
+
# Extracted from Plugins::Config so it can be referenced independently
|
|
15
|
+
# via Context#presets without coupling to the plugin machinery.
|
|
16
|
+
class PresetManager
|
|
17
|
+
# Reserved keys in the YAML config file
|
|
18
|
+
module Key
|
|
19
|
+
CONFIG = 'config'
|
|
20
|
+
VERSION = 'version'
|
|
21
|
+
DEFAULTS = 'default'
|
|
22
|
+
GLOBAL = 'global_common_defaults'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
GLOBAL_DEFAULT_KEYWORD = 'GLOBAL'
|
|
26
|
+
CONF_GLOBAL_SYM = :config
|
|
27
|
+
# Separator for dot-notation digging into presets
|
|
28
|
+
PRESET_DIG_SEPARATOR = '.'
|
|
29
|
+
|
|
30
|
+
private_constant :PRESET_DIG_SEPARATOR
|
|
31
|
+
|
|
32
|
+
# @param config_file [String] absolute path to the YAML config file
|
|
33
|
+
# @param use_plugin_defaults [Boolean] if false, skip default preset lookup
|
|
34
|
+
def initialize(config_file:, use_plugin_defaults: true)
|
|
35
|
+
@config_file = config_file
|
|
36
|
+
@use_plugin_defaults = use_plugin_defaults
|
|
37
|
+
@config_presets = {}
|
|
38
|
+
@checksum_on_disk = nil
|
|
39
|
+
read_config_file
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
attr_reader :config_presets, :use_plugin_defaults
|
|
43
|
+
attr_writer :use_plugin_defaults
|
|
44
|
+
|
|
45
|
+
# ------------------------------------------------------------------
|
|
46
|
+
# File I/O
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
# @return [String] SHA1 of current in-memory presets
|
|
50
|
+
def checksum
|
|
51
|
+
Digest::SHA1.hexdigest(JSON.generate(@config_presets))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Read and validate the YAML config file.
|
|
55
|
+
# Sets @config_presets and @checksum_on_disk.
|
|
56
|
+
def read_config_file
|
|
57
|
+
Log.log.debug{"config file is: #{@config_file}".red}
|
|
58
|
+
if File.exist?(@config_file)
|
|
59
|
+
Log.log.debug{"loading #{@config_file}"}
|
|
60
|
+
@config_presets = YAML.load_file(@config_file)
|
|
61
|
+
@checksum_on_disk = checksum
|
|
62
|
+
else
|
|
63
|
+
Log.log.warn{"No config file found. New configuration file: #{@config_file}"}
|
|
64
|
+
@config_presets = {Key::CONFIG => {Key::VERSION => 'new file'}}
|
|
65
|
+
# @checksum_on_disk remains nil → will be saved on first write
|
|
66
|
+
end
|
|
67
|
+
validate_config_presets!
|
|
68
|
+
rescue Psych::SyntaxError => e
|
|
69
|
+
Log.log.error('YAML error in config file')
|
|
70
|
+
raise e
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
Log.log.debug{"-> #{e.class.name} : #{e}"}
|
|
73
|
+
if File.exist?(@config_file)
|
|
74
|
+
new_name = "#{@config_file}.pre#{Cli::VERSION}.manual_conversion_needed"
|
|
75
|
+
File.rename(@config_file, new_name)
|
|
76
|
+
Log.log.warn{"Renamed config file to #{new_name}."}
|
|
77
|
+
Log.log.warn('Manual Conversion is required. Next time, a new empty file will be created.')
|
|
78
|
+
end
|
|
79
|
+
raise Cli::Error, e.to_s
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Save to disk only if content changed since last load/save.
|
|
83
|
+
# @return [Boolean] true if actually written
|
|
84
|
+
def save_if_needed
|
|
85
|
+
raise Cli::Error, 'no configuration loaded' if @config_presets.nil?
|
|
86
|
+
current = checksum
|
|
87
|
+
return false if @checksum_on_disk.eql?(current)
|
|
88
|
+
FileUtils.mkdir_p(File.dirname(@config_file))
|
|
89
|
+
Environment.restrict_file_access(File.dirname(@config_file))
|
|
90
|
+
Log.log.info{"Saving config file: #{@config_file}"}
|
|
91
|
+
Environment.write_file_restricted(@config_file, force: true){ @config_presets.to_yaml }
|
|
92
|
+
@checksum_on_disk = current
|
|
93
|
+
true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# ------------------------------------------------------------------
|
|
97
|
+
# Preset read/write helpers
|
|
98
|
+
# ------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
# @return [String, nil] name of the default preset for a plugin, or nil
|
|
101
|
+
def plugin_default_name(plugin_name_sym)
|
|
102
|
+
Aspera.assert(!@config_presets.nil?, 'config_presets shall be defined')
|
|
103
|
+
return nil unless @use_plugin_defaults
|
|
104
|
+
return nil unless @config_presets.key?(Key::DEFAULTS)
|
|
105
|
+
Aspera.assert_type(@config_presets[Key::DEFAULTS], Hash){'default section'}
|
|
106
|
+
return nil unless @config_presets[Key::DEFAULTS].key?(plugin_name_sym.to_s)
|
|
107
|
+
default_name = @config_presets[Key::DEFAULTS][plugin_name_sym.to_s]
|
|
108
|
+
unless @config_presets.key?(default_name)
|
|
109
|
+
Log.log.error do
|
|
110
|
+
"Default config name [#{default_name}] specified for plugin [#{plugin_name_sym}], but it does not exist in config file.\n" \
|
|
111
|
+
"Please fix: either create preset:\n" \
|
|
112
|
+
"#{Info::CMD_NAME} config id #{default_name} init @json:'{}'\n" \
|
|
113
|
+
"or remove default:\n#{Info::CMD_NAME} config id default remove #{plugin_name_sym}"
|
|
114
|
+
end
|
|
115
|
+
raise Cli::Error, "No such preset: #{default_name}"
|
|
116
|
+
end
|
|
117
|
+
Aspera.assert_type(@config_presets[default_name], Hash, type: Cli::Error){'preset type'}
|
|
118
|
+
default_name
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Resolve and return a deep-cloned, extended copy of a named preset.
|
|
122
|
+
# Supports dot-notation (e.g. "outer.inner").
|
|
123
|
+
# @param config_name [String]
|
|
124
|
+
# @param include_path [Array] guard against include loops
|
|
125
|
+
def by_name(config_name, include_path = [])
|
|
126
|
+
raise Cli::Error, 'loop in include' if include_path.include?(config_name)
|
|
127
|
+
include_path = include_path.clone
|
|
128
|
+
current = @config_presets
|
|
129
|
+
config_name.split(PRESET_DIG_SEPARATOR).each do |name|
|
|
130
|
+
Aspera.assert_type(current, Hash, type: Cli::Error){"sub key: #{include_path}"}
|
|
131
|
+
include_path.push(name)
|
|
132
|
+
current = current[name]
|
|
133
|
+
raise Cli::Error, "Unknown config preset: #{include_path}" if current.nil?
|
|
134
|
+
end
|
|
135
|
+
current = self.class.deep_clone(current) unless current.is_a?(String)
|
|
136
|
+
ExtendedValue.instance.evaluate(current, context: 'preset')
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Set a single key in a preset hash (creates the preset if absent).
|
|
140
|
+
def set_key(preset, param_name, param_value)
|
|
141
|
+
Aspera.assert_type(param_name, String, Symbol){'parameter'}
|
|
142
|
+
param_name = param_name.to_s
|
|
143
|
+
selected = @config_presets[preset]
|
|
144
|
+
if selected.nil?
|
|
145
|
+
Log.log.debug{"Unknown preset name: #{preset}, initializing"}
|
|
146
|
+
selected = @config_presets[preset] = {}
|
|
147
|
+
end
|
|
148
|
+
Aspera.assert_type(selected, Hash){"#{preset}.#{param_name}"}
|
|
149
|
+
if selected.key?(param_name)
|
|
150
|
+
if selected[param_name].eql?(param_value)
|
|
151
|
+
Log.log.warn{"keeping same value for #{preset}: #{param_name}: #{param_value}"}
|
|
152
|
+
return
|
|
153
|
+
end
|
|
154
|
+
Log.log.warn{"overwriting value for #{param_name}: #{selected[param_name]}"}
|
|
155
|
+
end
|
|
156
|
+
selected[param_name] = param_value
|
|
157
|
+
Log.log.info("Updated: #{preset}: #{param_name} <- #{param_value}")
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# @return [String] name of the global default preset, creating it if needed
|
|
162
|
+
def global_default_preset
|
|
163
|
+
result = plugin_default_name(CONF_GLOBAL_SYM)
|
|
164
|
+
if result.nil?
|
|
165
|
+
result = Key::GLOBAL
|
|
166
|
+
set_key(Key::DEFAULTS, CONF_GLOBAL_SYM, result)
|
|
167
|
+
end
|
|
168
|
+
result
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Set param in the global defaults preset
|
|
172
|
+
def set_global_default(key, value)
|
|
173
|
+
set_key(global_default_preset, key, value)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Create / overwrite a preset and optionally set it as plugin default.
|
|
177
|
+
def defaults_set(plugin_name, preset_name, preset_values, option_default, option_override)
|
|
178
|
+
@config_presets[Key::DEFAULTS] ||= {}
|
|
179
|
+
raise Cli::Error, "A default configuration already exists for plugin '#{plugin_name}' (use --override=yes or --default=no)" \
|
|
180
|
+
if !option_override && option_default && @config_presets[Key::DEFAULTS].key?(plugin_name)
|
|
181
|
+
raise Cli::Error, "Preset already exists: #{preset_name} (use --override=yes or provide alternate name on command line)" \
|
|
182
|
+
if !option_override && @config_presets.key?(preset_name)
|
|
183
|
+
if option_default
|
|
184
|
+
Log.log.info("Setting config preset as default for #{plugin_name}")
|
|
185
|
+
@config_presets[Key::DEFAULTS][plugin_name.to_s] = preset_name
|
|
186
|
+
end
|
|
187
|
+
@config_presets[preset_name] = preset_values
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Find the first preset whose url+username match
|
|
191
|
+
# @return [Hash, nil]
|
|
192
|
+
def lookup_preset(url:, username:)
|
|
193
|
+
url = canonical_url(url)
|
|
194
|
+
Log.log.debug{"Lookup preset for #{username}@#{url}"}
|
|
195
|
+
@config_presets.each_value do |v|
|
|
196
|
+
next unless v.is_a?(Hash)
|
|
197
|
+
conf_url = v['url'].is_a?(String) ? canonical_url(v['url']) : nil
|
|
198
|
+
return self.class.deep_clone(v) if conf_url.eql?(url) && v['username'].eql?(username)
|
|
199
|
+
end
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# ------------------------------------------------------------------
|
|
204
|
+
# Class helpers
|
|
205
|
+
# ------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
class << self
|
|
208
|
+
def deep_clone(val)
|
|
209
|
+
Marshal.load(Marshal.dump(val))
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
private
|
|
214
|
+
|
|
215
|
+
# Normalise a URL for comparison
|
|
216
|
+
def canonical_url(url)
|
|
217
|
+
url.chomp('/').sub(%r{^(https://[^/]+):443$}, '\1')
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Structural validation + compatibility fixes on @config_presets
|
|
221
|
+
def validate_config_presets!
|
|
222
|
+
Log.dump(:available_presets, @config_presets, level: :trace1)
|
|
223
|
+
Aspera.assert_type(@config_presets, Hash){'config file YAML'}
|
|
224
|
+
Aspera.assert(@config_presets.key?(Key::CONFIG)){"Cannot find key: #{Key::CONFIG}"}
|
|
225
|
+
version = @config_presets[Key::CONFIG][Key::VERSION]
|
|
226
|
+
raise Cli::Error, 'No version found in config section.' if version.nil?
|
|
227
|
+
Log.log.debug{"conf version: #{version}"}
|
|
228
|
+
# Fix bug in 4.4 (creating key "true" in "default" preset)
|
|
229
|
+
@config_presets[Key::DEFAULTS].delete(true) if @config_presets[Key::DEFAULTS].is_a?(Hash)
|
|
230
|
+
# Stamp with current version
|
|
231
|
+
@config_presets[Key::CONFIG][Key::VERSION] = Cli::VERSION
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/assert'
|
|
4
|
+
require 'aspera/nagios'
|
|
5
|
+
require 'aspera/log'
|
|
6
|
+
require 'aspera/environment'
|
|
7
|
+
require 'aspera/uri_reader'
|
|
8
|
+
require 'aspera/preview/terminal'
|
|
9
|
+
require 'aspera/dot_container'
|
|
10
|
+
require 'base64'
|
|
11
|
+
require 'uri'
|
|
12
|
+
require 'pp'
|
|
13
|
+
require 'json'
|
|
14
|
+
require 'yaml'
|
|
15
|
+
|
|
16
|
+
module Aspera
|
|
17
|
+
module Cli
|
|
18
|
+
# Base class for all result types
|
|
19
|
+
# Each result type is now a separate class instead of using a type field
|
|
20
|
+
class Result
|
|
21
|
+
attr_reader :data, :fields
|
|
22
|
+
|
|
23
|
+
# @param data [Object,nil] The result data
|
|
24
|
+
# @param fields [Object] Specification of fields to include
|
|
25
|
+
def initialize(data: nil, fields: nil)
|
|
26
|
+
@data = data
|
|
27
|
+
@fields = fields
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Format this result using the provided formatter
|
|
31
|
+
# This method implements the Visitor pattern, allowing each Result subclass
|
|
32
|
+
# to define how it should be formatted without the Formatter needing to know
|
|
33
|
+
# about all Result types.
|
|
34
|
+
# Base implementation handles common formats: text, nagios, ruby, json, jsonpp, yaml
|
|
35
|
+
# Subclasses should call super first, then handle their specific formats
|
|
36
|
+
# @param formatter [Formatter] The formatter to use
|
|
37
|
+
# @return [void]
|
|
38
|
+
def format(formatter)
|
|
39
|
+
# Apply field filtering once for formats that need it
|
|
40
|
+
filtered_data = formatter.filter_list_on_fields(@data)
|
|
41
|
+
case formatter.format_type
|
|
42
|
+
when :text
|
|
43
|
+
formatter.display_message(:data, @data.to_s)
|
|
44
|
+
when :nagios
|
|
45
|
+
Nagios.process(@data)
|
|
46
|
+
when :ruby
|
|
47
|
+
formatter.display_message(:data, PP.pp(filtered_data, +''))
|
|
48
|
+
when :json
|
|
49
|
+
formatter.display_message(:data, JSON.generate(filtered_data))
|
|
50
|
+
when :jsonpp
|
|
51
|
+
formatter.display_message(:data, JSON.pretty_generate(filtered_data))
|
|
52
|
+
when :yaml
|
|
53
|
+
formatter.display_message(:data, YAML.dump(filtered_data))
|
|
54
|
+
when :image
|
|
55
|
+
if @data.nil?
|
|
56
|
+
formatter.display_message(:data, formatter.special_format('null (no image)'))
|
|
57
|
+
return
|
|
58
|
+
end
|
|
59
|
+
Aspera.assert_type(@data, String){'image: URL or blob'}
|
|
60
|
+
# Check if URL
|
|
61
|
+
data =
|
|
62
|
+
begin
|
|
63
|
+
# just validate
|
|
64
|
+
URI.parse(@data)
|
|
65
|
+
if Environment.instance.url_method.eql?(:text)
|
|
66
|
+
UriReader.read(@data)
|
|
67
|
+
else
|
|
68
|
+
Environment.instance.open_uri(@data)
|
|
69
|
+
formatter.display_message(:info, "Opened URL in browser: #{@data}")
|
|
70
|
+
:done
|
|
71
|
+
end
|
|
72
|
+
rescue URI::InvalidURIError
|
|
73
|
+
@data
|
|
74
|
+
end
|
|
75
|
+
# try base64
|
|
76
|
+
data = begin
|
|
77
|
+
Base64.strict_decode64(data)
|
|
78
|
+
rescue
|
|
79
|
+
data
|
|
80
|
+
end
|
|
81
|
+
# here, data is the image blob
|
|
82
|
+
formatter.display_message(:data, Preview::Terminal.build(data, **formatter.image_options)) unless data.eql?(:done)
|
|
83
|
+
else
|
|
84
|
+
Aspera.error_unexpected_value(formatter.format_type){'format'}
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Special result types - each has its own class
|
|
90
|
+
class Result
|
|
91
|
+
# Base class for special results
|
|
92
|
+
# The type is automatically derived from the class name
|
|
93
|
+
class Special < Result
|
|
94
|
+
def initialize
|
|
95
|
+
# Convert class name to symbol: Empty -> :empty, Nothing -> :nothing
|
|
96
|
+
type = self.class.name.split('::').last.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym
|
|
97
|
+
super(data: type)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def format(formatter)
|
|
101
|
+
case formatter.format_type
|
|
102
|
+
when :text, :nagios, :ruby, :json, :jsonpp, :yaml
|
|
103
|
+
formatter.display_message(:data, @data.to_s)
|
|
104
|
+
when :table, :csv
|
|
105
|
+
if @data.eql?(:nothing)
|
|
106
|
+
Log.log.debug('no result expected')
|
|
107
|
+
return
|
|
108
|
+
end
|
|
109
|
+
formatter.display_message(:info, formatter.special_format(@data.to_s))
|
|
110
|
+
else
|
|
111
|
+
super
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Empty list result
|
|
117
|
+
class Empty < Special
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Nothing expected result
|
|
121
|
+
class Nothing < Special
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Null result
|
|
125
|
+
class Null < Special
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Status result (success, complete, etc.)
|
|
129
|
+
class Status < Result
|
|
130
|
+
def initialize(data)
|
|
131
|
+
Aspera.assert_type(data, String){'status result data'}
|
|
132
|
+
super(data: data)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def format(formatter)
|
|
136
|
+
data = formatter.hide_secrets? ? formatter.hide_secrets_in_string(@data) : @data
|
|
137
|
+
case formatter.format_type
|
|
138
|
+
when :text, :nagios, :ruby, :json, :jsonpp, :yaml
|
|
139
|
+
formatter.display_message(:data, data)
|
|
140
|
+
when :table, :csv
|
|
141
|
+
formatter.display_message(:info, data)
|
|
142
|
+
else
|
|
143
|
+
super
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Success status result
|
|
149
|
+
class Success < Status
|
|
150
|
+
def initialize
|
|
151
|
+
super('complete')
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Text result
|
|
156
|
+
class Text < Result
|
|
157
|
+
def initialize(data)
|
|
158
|
+
Aspera.assert_type(data, String, Integer, Symbol, type: ArgumentError){'text result data'}
|
|
159
|
+
super(data: data)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def format(formatter)
|
|
163
|
+
data = @data.is_a?(String) && formatter.hide_secrets? ? formatter.hide_secrets_in_string(@data) : @data
|
|
164
|
+
case formatter.format_type
|
|
165
|
+
when :text, :nagios
|
|
166
|
+
formatter.display_message(:data, data.to_s)
|
|
167
|
+
when :ruby
|
|
168
|
+
formatter.display_message(:data, PP.pp(data, +''))
|
|
169
|
+
when :json
|
|
170
|
+
formatter.display_message(:data, JSON.generate(data))
|
|
171
|
+
when :jsonpp
|
|
172
|
+
formatter.display_message(:data, JSON.pretty_generate(data))
|
|
173
|
+
when :yaml
|
|
174
|
+
formatter.display_message(:data, YAML.dump(data))
|
|
175
|
+
when :table, :csv
|
|
176
|
+
formatter.display_message(:data, data)
|
|
177
|
+
else
|
|
178
|
+
super
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Image result (URL or blob)
|
|
184
|
+
class Image < Result
|
|
185
|
+
def initialize(data)
|
|
186
|
+
Aspera.assert_type(data, String, NilClass){'image result data'}
|
|
187
|
+
super(data: data)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def format(formatter)
|
|
191
|
+
# Force image format for Image results
|
|
192
|
+
formatter.set_format_type(:image)
|
|
193
|
+
super
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Single object result (Hash)
|
|
198
|
+
class SingleObject < Result
|
|
199
|
+
def initialize(data, fields: nil)
|
|
200
|
+
Aspera.assert_type(data, Hash){'single object result data'}
|
|
201
|
+
super(data: data, fields: fields)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def format(formatter)
|
|
205
|
+
case formatter.format_type
|
|
206
|
+
when :image
|
|
207
|
+
# Extract single field for image display
|
|
208
|
+
data_array = [@data]
|
|
209
|
+
fields = formatter.compute_fields(data_array, @fields)
|
|
210
|
+
Aspera.assert(fields.length == 1, 'select a single field to display', type: Cli::BadArgument)
|
|
211
|
+
Aspera.assert(@data.key?(fields.first), 'no such field', type: Cli::BadArgument)
|
|
212
|
+
# Create an Image result and format it
|
|
213
|
+
Image.new(@data[fields.first]).format(formatter)
|
|
214
|
+
when :table, :csv
|
|
215
|
+
Aspera.assert_type(@data, Hash){'result'}
|
|
216
|
+
if @data.empty?
|
|
217
|
+
formatter.display_message(:data, formatter.special_format('empty dict'))
|
|
218
|
+
else
|
|
219
|
+
data = formatter.flat_hash? ? DotContainer.new(@data).to_dotted : @data
|
|
220
|
+
formatter.display_table([data], formatter.compute_fields([data], @fields), single: true)
|
|
221
|
+
end
|
|
222
|
+
else
|
|
223
|
+
super
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Object list result (Array of Hash)
|
|
229
|
+
# @note The +total+ parameter is used to display pagination information (e.g., "Items: 10/100")
|
|
230
|
+
class ObjectList < Result
|
|
231
|
+
attr_reader :total
|
|
232
|
+
|
|
233
|
+
# @param data [Array<Hash>] Array of hash objects to display
|
|
234
|
+
# @param fields [Array<String>, Proc, nil] Fields to display in table/csv format
|
|
235
|
+
# @param total [Integer, nil] Total number of items available (for pagination display)
|
|
236
|
+
def initialize(data, fields: nil, total: nil)
|
|
237
|
+
Aspera.assert_type(data, Array){'object list result data'}
|
|
238
|
+
raise ArgumentError, 'Object list result requires Array of Hash' unless data.all?(Hash)
|
|
239
|
+
Aspera.assert_type(total, Integer, NilClass){'total'}
|
|
240
|
+
super(data: data, fields: fields)
|
|
241
|
+
@total = total
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def format(formatter)
|
|
245
|
+
formatter.display_item_count(@data.length, @total)
|
|
246
|
+
case formatter.format_type
|
|
247
|
+
when :image
|
|
248
|
+
# Extract single field for image display
|
|
249
|
+
Aspera.assert(@data.length == 1, 'image display requires a single result', type: Cli::BadArgument)
|
|
250
|
+
SingleObject.new(@data.first).format(formatter)
|
|
251
|
+
when :table, :csv
|
|
252
|
+
Aspera.assert_array_all(@data, Hash){'result'}
|
|
253
|
+
data = formatter.flat_hash? ? @data.map{ |obj| DotContainer.new(obj).to_dotted} : @data
|
|
254
|
+
formatter.display_table(data, formatter.compute_fields(data, @fields), single: false)
|
|
255
|
+
else
|
|
256
|
+
super
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Value list result (Array of values with a name)
|
|
262
|
+
class ValueList < Result
|
|
263
|
+
attr_reader :name
|
|
264
|
+
|
|
265
|
+
def initialize(data, name: 'id')
|
|
266
|
+
Aspera.assert_type(data, Array){'value list result data'}
|
|
267
|
+
Aspera.assert_type(name, String){'value list name'}
|
|
268
|
+
super(data: data)
|
|
269
|
+
@name = name
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def format(formatter)
|
|
273
|
+
case formatter.format_type
|
|
274
|
+
when :table, :csv
|
|
275
|
+
formatter.display_table(@data.map{ |i| {@name => i}}, [@name])
|
|
276
|
+
else
|
|
277
|
+
super
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
class << self
|
|
283
|
+
# Class method to automatically determine result type from data
|
|
284
|
+
# @param data [Object] the data to analyze and format
|
|
285
|
+
# @return [Result]
|
|
286
|
+
def auto(data)
|
|
287
|
+
case data
|
|
288
|
+
when NilClass
|
|
289
|
+
Null.new
|
|
290
|
+
when Hash
|
|
291
|
+
SingleObject.new(data)
|
|
292
|
+
when Array
|
|
293
|
+
all_types = data.map(&:class).uniq
|
|
294
|
+
return ObjectList.new(data) if all_types.eql?([Hash])
|
|
295
|
+
|
|
296
|
+
scalar_types = [String, Integer, Symbol]
|
|
297
|
+
unsupported_types = all_types - scalar_types
|
|
298
|
+
return ValueList.new(data, name: 'list') if unsupported_types.empty?
|
|
299
|
+
|
|
300
|
+
Aspera.error_unexpected_value(unsupported_types){'list item types'}
|
|
301
|
+
when String, Integer, Symbol
|
|
302
|
+
Text.new(data)
|
|
303
|
+
else
|
|
304
|
+
Aspera.error_unexpected_value(data.class.name){'result type'}
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|