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
|
@@ -25,9 +25,9 @@ module Aspera
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# Called by user of progress bar with a status on a transfer session
|
|
28
|
-
# @param session_id the unique identifier of a transfer session
|
|
28
|
+
# @param session_id [String] the unique identifier of a transfer session
|
|
29
29
|
# @param type [Symbol] one of: sessions_init, session_start, session_size, transfer, session_end and end
|
|
30
|
-
# @param info optional specific additional info for the given event type
|
|
30
|
+
# @param info [Object, nil] optional specific additional info for the given event type
|
|
31
31
|
def event(type, session_id: nil, info: nil)
|
|
32
32
|
Log.log.trace1{"progress: #{type} #{session_id} #{info}"}
|
|
33
33
|
return if @completed
|
|
@@ -43,14 +43,14 @@ module Aspera
|
|
|
43
43
|
case type
|
|
44
44
|
when :sessions_init
|
|
45
45
|
# Give opportunity to show progress of initialization with multiple status
|
|
46
|
-
Aspera.assert(session_id.nil
|
|
46
|
+
Aspera.assert(session_id.nil?, 'session_id must be nil for :sessions_init event')
|
|
47
47
|
Aspera.assert_type(info, String)
|
|
48
48
|
# Initialization of progress bar
|
|
49
49
|
@title = info
|
|
50
50
|
when :session_start
|
|
51
51
|
Aspera.assert_type(session_id, String)
|
|
52
|
-
Aspera.assert(info.nil
|
|
53
|
-
|
|
52
|
+
Aspera.assert(info.nil?, 'info must be nil for :session_start event')
|
|
53
|
+
Aspera.assert(!@sessions[session_id]){"Session #{session_id} already started"}
|
|
54
54
|
@sessions[session_id] = {
|
|
55
55
|
job_size: 0, # Total size of transfer (pre-calc)
|
|
56
56
|
current: 0,
|
|
@@ -60,7 +60,7 @@ module Aspera
|
|
|
60
60
|
@title = nil
|
|
61
61
|
when :session_size
|
|
62
62
|
Aspera.assert_type(session_id, String)
|
|
63
|
-
Aspera.assert(!info.nil
|
|
63
|
+
Aspera.assert(!info.nil?, 'info must not be nil for :session_size event')
|
|
64
64
|
Aspera.assert_type(@sessions[session_id], Hash)
|
|
65
65
|
@sessions[session_id][:job_size] = info.to_i
|
|
66
66
|
sessions_total = total(:job_size)
|
|
@@ -76,12 +76,12 @@ module Aspera
|
|
|
76
76
|
end
|
|
77
77
|
when :session_end
|
|
78
78
|
Aspera.assert_type(session_id, String)
|
|
79
|
-
Aspera.assert(info.nil
|
|
79
|
+
Aspera.assert(info.nil?, 'info must be nil for :session_end event')
|
|
80
80
|
# A session may be too short and finish before it has been started
|
|
81
81
|
@sessions[session_id][:running] = false if @sessions[session_id].is_a?(Hash)
|
|
82
82
|
when :end
|
|
83
|
-
Aspera.assert(session_id.nil
|
|
84
|
-
Aspera.assert(info.nil
|
|
83
|
+
Aspera.assert(session_id.nil?, 'session_id must be nil for :end event')
|
|
84
|
+
Aspera.assert(info.nil?, 'info must be nil for :end event')
|
|
85
85
|
@progress_bar.finish
|
|
86
86
|
else Aspera.error_unexpected_value(type){'event type'}
|
|
87
87
|
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/cli/error'
|
|
4
|
+
require 'aspera/log'
|
|
5
|
+
require 'aspera/assert'
|
|
6
|
+
|
|
7
|
+
module Aspera
|
|
8
|
+
module Cli
|
|
9
|
+
# Mixin providing vault/keychain functionality to Plugin::Config.
|
|
10
|
+
# Depends on `options` and `context.main_folder` being available in the including class.
|
|
11
|
+
module VaultManager
|
|
12
|
+
def action_vault_show(label:, **)
|
|
13
|
+
Result::SingleObject.new(vault.get(label: label))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def action_vault_create(info:, **)
|
|
17
|
+
vault.set(info.symbolize_keys)
|
|
18
|
+
Result::Status.new('Secret added')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def action_vault_delete(label:, **)
|
|
22
|
+
vault.delete(label: label)
|
|
23
|
+
Result::Status.new("Secret deleted: #{label}")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def action_vault_password(new_password:, **)
|
|
27
|
+
Aspera.assert(vault.respond_to?(:change_password), 'Vault does not support password change')
|
|
28
|
+
vault.change_password(new_password)
|
|
29
|
+
Result::Status.new('Vault password updated')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [String] value from vault matching <name>.<param>
|
|
33
|
+
def vault_value(name)
|
|
34
|
+
m = name.split('.')
|
|
35
|
+
Aspera.assert(m.length.eql?(2), type: BadArgument){'vault name shall match <name>.<param>'}
|
|
36
|
+
info = vault.get(label: m[0])
|
|
37
|
+
value = info[m[1].to_sym]
|
|
38
|
+
raise "no such entry value: #{m[1]}" if value.nil?
|
|
39
|
+
return value
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [Keychain::Base] vault instance, lazily created from options
|
|
43
|
+
def vault
|
|
44
|
+
return @vault_instance unless @vault_instance.nil?
|
|
45
|
+
info = options.get_option(:vault, mandatory: true).symbolize_keys
|
|
46
|
+
info[:type] ||= 'file'
|
|
47
|
+
require 'aspera/keychain/factory'
|
|
48
|
+
@vault_instance = Keychain::Factory.create(
|
|
49
|
+
info,
|
|
50
|
+
Info::CMD_NAME,
|
|
51
|
+
context.main_folder,
|
|
52
|
+
options.get_option(:vault_password)
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/aspera/cli/version.rb
CHANGED
data/lib/aspera/cli/wizard.rb
CHANGED
|
@@ -10,19 +10,25 @@ module Aspera
|
|
|
10
10
|
class Wizard
|
|
11
11
|
WIZARD_RESULT_KEYS = %i[preset_value test_args].freeze
|
|
12
12
|
DEFAULT_PRIV_KEY_FILENAME = 'my_private_key.pem' # pragma: allowlist secret
|
|
13
|
-
private_constant :WIZARD_RESULT_KEYS,
|
|
14
|
-
|
|
13
|
+
private_constant :WIZARD_RESULT_KEYS, :DEFAULT_PRIV_KEY_FILENAME
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# Declare all wizard CLI options (metadata only - no handler binding yet).
|
|
17
|
+
# @param options [Aspera::Cli::Parser]
|
|
18
|
+
def declare_options(options)
|
|
19
|
+
options.declare(:override, description: 'Wizard: override existing value', allowed: Allowed::TYPES_BOOLEAN, default: false)
|
|
20
|
+
options.declare(:default, description: 'Wizard: set as default configuration for specified plugin (also: update)', allowed: Allowed::TYPES_BOOLEAN, default: true)
|
|
21
|
+
options.declare(:key_path, description: 'Wizard: path to private key for JWT')
|
|
22
|
+
end
|
|
23
|
+
end
|
|
15
24
|
|
|
16
25
|
def initialize(parent, main_folder)
|
|
17
26
|
@parent = parent
|
|
18
27
|
@main_folder = main_folder
|
|
19
|
-
|
|
20
|
-
options.declare(:override, 'Wizard: override existing value', allowed: Allowed::TYPES_BOOLEAN, default: false)
|
|
21
|
-
options.declare(:default, 'Wizard: set as default configuration for specified plugin (also: update)', allowed: Allowed::TYPES_BOOLEAN, default: true)
|
|
22
|
-
options.declare(:key_path, 'Wizard: path to private key for JWT')
|
|
28
|
+
self.class.declare_options(options)
|
|
23
29
|
end
|
|
24
30
|
|
|
25
|
-
# @return false if in test mode to avoid interactive input
|
|
31
|
+
# @return [Boolean] false if in test mode to avoid interactive input
|
|
26
32
|
def required
|
|
27
33
|
!ENV['ASCLI_WIZ_TEST']
|
|
28
34
|
end
|
|
@@ -35,20 +41,15 @@ module Aspera
|
|
|
35
41
|
@parent.formatter
|
|
36
42
|
end
|
|
37
43
|
|
|
38
|
-
def config
|
|
39
|
-
@parent.config
|
|
40
|
-
end
|
|
41
|
-
|
|
42
44
|
def check_email(email)
|
|
43
45
|
Aspera.assert(email =~ /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i, type: ParameterError){"Username shall be an email: #{email}"}
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
# Find a plugin, and issue the "require"
|
|
47
49
|
# @return [Hash] plugin info: { product:, name:, url:, version: }
|
|
48
|
-
def identify_plugins_for_url
|
|
49
|
-
app_url =
|
|
50
|
-
check_only =
|
|
51
|
-
check_only = check_only.to_sym unless check_only.nil?
|
|
50
|
+
def identify_plugins_for_url(url:, plugin_name: nil)
|
|
51
|
+
app_url = url
|
|
52
|
+
check_only = plugin_name&.to_sym
|
|
52
53
|
found_apps = []
|
|
53
54
|
my_self_plugin_sym = self.class.name.split('::').last.downcase.to_sym
|
|
54
55
|
Plugins::Factory.instance.plugin_list.each do |plugin_name_sym|
|
|
@@ -74,13 +75,13 @@ module Aspera
|
|
|
74
75
|
next if detection_info.nil?
|
|
75
76
|
Aspera.assert_type(detection_info, Hash)
|
|
76
77
|
Aspera.assert_type(detection_info[:url], String) if detection_info.key?(:url)
|
|
77
|
-
app_name = plugin_klass.
|
|
78
|
+
app_name = plugin_klass.application_name
|
|
78
79
|
# If there is a redirect, then the detector can override the url.
|
|
79
80
|
found_apps.push({product: plugin_name_sym, name: app_name, url: app_url, version: 'unknown'}.merge(detection_info))
|
|
80
81
|
end
|
|
81
82
|
RestParameters.instance.spinner_cb.call(action: :success)
|
|
82
|
-
|
|
83
|
-
Aspera.assert(found_apps.all?{ |a| a.keys.all?(Symbol)})
|
|
83
|
+
Aspera.assert(!found_apps.empty?){"No known application found at #{app_url}"}
|
|
84
|
+
Aspera.assert(found_apps.all?{ |a| a.keys.all?(Symbol)}, 'all app info keys must be symbols')
|
|
84
85
|
return found_apps
|
|
85
86
|
end
|
|
86
87
|
|
|
@@ -122,17 +123,18 @@ module Aspera
|
|
|
122
123
|
|
|
123
124
|
# Wizard function, creates configuration
|
|
124
125
|
# @param apps [Array] list of detected apps
|
|
125
|
-
def find(apps)
|
|
126
|
+
def find(apps, preset_name: '')
|
|
126
127
|
identification = if apps.length.eql?(1)
|
|
127
128
|
Log.log.debug{"Detected: #{identification}"}
|
|
128
129
|
apps.first
|
|
129
130
|
else
|
|
130
131
|
formatter.display_status('Multiple applications detected, please select from:')
|
|
131
|
-
|
|
132
|
+
require 'aspera/cli/result'
|
|
133
|
+
formatter.display_results(Result::ObjectList.new(apps, fields: %w[product url version]))
|
|
132
134
|
answer = options.prompt_user_input_in_list('product', apps.map{ |a| a[:product]})
|
|
133
135
|
apps.find{ |a| a[:product].eql?(answer)}
|
|
134
136
|
end
|
|
135
|
-
wiz_preset_name =
|
|
137
|
+
wiz_preset_name = preset_name
|
|
136
138
|
Log.dump(:identification, identification)
|
|
137
139
|
wiz_url = identification[:url]
|
|
138
140
|
formatter.display_status("Using: #{identification[:name]} at #{wiz_url}".bold)
|
|
@@ -140,9 +142,7 @@ module Aspera
|
|
|
140
142
|
options.add_option_preset({url: wiz_url}, 'wizard')
|
|
141
143
|
# Instantiate plugin: command line options will be known, e.g. private_key, and wizard can be called
|
|
142
144
|
plugin_instance = Plugins::Factory.instance.plugin_class(identification[:product]).new(context: @parent.context)
|
|
143
|
-
Aspera.assert(plugin_instance.respond_to?(:wizard), type: Cli::BadArgument)
|
|
144
|
-
"Detected: #{identification[:product]}, but this application has no wizard"
|
|
145
|
-
end
|
|
145
|
+
Aspera.assert(plugin_instance.respond_to?(:wizard), type: Cli::BadArgument){"Detected: #{identification[:product]}, but this application has no wizard"}
|
|
146
146
|
# Call the wizard
|
|
147
147
|
wizard_result = plugin_instance.wizard(self, wiz_url)
|
|
148
148
|
Log.log.debug{"wizard result: #{wizard_result}"}
|
|
@@ -161,12 +161,12 @@ module Aspera
|
|
|
161
161
|
# Init defaults if necessary
|
|
162
162
|
option_override = options.get_option(:override, mandatory: true)
|
|
163
163
|
option_default = options.get_option(:default, mandatory: true)
|
|
164
|
-
|
|
164
|
+
@parent.context.presets.defaults_set(identification[:product], wiz_preset_name, wizard_result[:preset_value].stringify_keys, option_default, option_override)
|
|
165
165
|
test_args = wizard_result[:test_args]
|
|
166
166
|
test_args = "-P#{wiz_preset_name} #{test_args}" unless option_default
|
|
167
167
|
# TODO: actually test the command
|
|
168
168
|
test_cmd = "#{Info::CMD_NAME} #{identification[:product]} #{test_args}"
|
|
169
|
-
return
|
|
169
|
+
return Result::Status.new("You can test with:\n#{test_cmd.red}")
|
|
170
170
|
end
|
|
171
171
|
end
|
|
172
172
|
end
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
require 'aspera/log'
|
|
4
4
|
require 'aspera/assert'
|
|
5
5
|
require 'aspera/schema/registry'
|
|
6
|
+
require 'aspera/exec_spec'
|
|
6
7
|
require 'yaml'
|
|
7
8
|
module Aspera
|
|
8
9
|
# Helper class to build command line from a parameter list (key-value hash)
|
|
9
10
|
# Constructor takes hash: `{ 'param1':'value1', ...}`
|
|
10
11
|
# `process_param` is called repeatedly with all known parameters
|
|
11
|
-
# `
|
|
12
|
+
# `add_to_exec_spec` is called to get resulting param list and env var (also checks that all params were used)
|
|
12
13
|
class CommandLineBuilder
|
|
13
14
|
# Supported keys in JSON schema
|
|
14
15
|
PROPERTY_KEYS = [
|
|
@@ -22,6 +23,7 @@ module Aspera
|
|
|
22
23
|
'$comment', # [String]
|
|
23
24
|
'x-cli-envvar', # [String] Name of env var
|
|
24
25
|
'x-cli-option', # [String] Command line option (starts with "-")
|
|
26
|
+
'x-cli-false', # [String]. Option to use when switch value is false
|
|
25
27
|
'x-cli-short', # [String] Command line option (starts with "-")
|
|
26
28
|
'x-cli-switch', # [Boolean] `true` if option has no arg, else by default option has a value
|
|
27
29
|
'x-cli-special', # [Boolean] `true` if special handling (deferred)
|
|
@@ -36,7 +38,9 @@ module Aspera
|
|
|
36
38
|
|
|
37
39
|
class << self
|
|
38
40
|
# Called by provider of definition before constructor of this class so that schema has all mandatory fields
|
|
39
|
-
# @
|
|
41
|
+
# @param name_sym [Symbol] name of the schema to read
|
|
42
|
+
# @param ascp [Boolean] `true` if `ascp` mode
|
|
43
|
+
# @return [Aspera::Schema::Reader] validated schema reader
|
|
40
44
|
def read_schema(name_sym, ascp: false)
|
|
41
45
|
validate_schema(Schema::Registry.instance.reader(name_sym), ascp: ascp)
|
|
42
46
|
end
|
|
@@ -50,6 +54,7 @@ module Aspera
|
|
|
50
54
|
|
|
51
55
|
private
|
|
52
56
|
|
|
57
|
+
# `ascp`: mandatory type
|
|
53
58
|
DIRECT_PROPERTIES = %w[x-cli-option x-cli-envvar x-cli-special].freeze
|
|
54
59
|
|
|
55
60
|
# Fill default values for some fields in the schema
|
|
@@ -66,7 +71,7 @@ module Aspera
|
|
|
66
71
|
Aspera.assert(node.key?('type') || node.key?('enum')){"Missing type for #{name} in #{schema.current.dig('description').current}"}
|
|
67
72
|
Aspera.assert(node['type'].eql?('boolean')){"switch must be bool: #{name}"} if node['x-cli-switch'] && !node['x-cli-special']
|
|
68
73
|
node['x-cli-option'] = "--#{name.to_s.tr('_', '-')}" if node['x-cli-option'].eql?(true) || (node['x-cli-switch'].eql?(true) && !node.key?('x-cli-option'))
|
|
69
|
-
Aspera.assert(DIRECT_PROPERTIES.any?{ |i| node.key?(i)}, type: :warn)
|
|
74
|
+
Aspera.assert(DIRECT_PROPERTIES.any?{ |i| node.key?(i)}, name, type: :warn) if ascp && supported_by_agent(:direct, node)
|
|
70
75
|
node.freeze
|
|
71
76
|
end
|
|
72
77
|
schema
|
|
@@ -80,10 +85,7 @@ module Aspera
|
|
|
80
85
|
@object = object # keep reference so that it can be modified by caller before calling `process_params`
|
|
81
86
|
@schema = schema
|
|
82
87
|
@convert = convert
|
|
83
|
-
@result =
|
|
84
|
-
env: {},
|
|
85
|
-
args: []
|
|
86
|
-
}
|
|
88
|
+
@result = ExecSpec.new
|
|
87
89
|
@processed_parameters = []
|
|
88
90
|
end
|
|
89
91
|
|
|
@@ -97,16 +99,16 @@ module Aspera
|
|
|
97
99
|
end
|
|
98
100
|
|
|
99
101
|
# Add processed parameters to env and args, warns about unused parameters
|
|
100
|
-
# @param [
|
|
101
|
-
def
|
|
102
|
-
Log.dump(:
|
|
102
|
+
# @param exec_spec [ExecSpec]
|
|
103
|
+
def add_to_exec_spec(exec_spec)
|
|
104
|
+
Log.dump(:exec_spec, @result)
|
|
103
105
|
# warn about non translated arguments
|
|
104
106
|
@object.each_pair do |name, value|
|
|
105
107
|
Log.log.warn{"Unknown transfer spec parameter: #{name} = \"#{value}\""} unless @processed_parameters.include?(name)
|
|
106
108
|
end
|
|
107
109
|
# set result
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
exec_spec.env.merge!(@result.env)
|
|
111
|
+
exec_spec.args.concat(@result.args)
|
|
110
112
|
return
|
|
111
113
|
end
|
|
112
114
|
|
|
@@ -114,7 +116,7 @@ module Aspera
|
|
|
114
116
|
def add_command_line_options(*options)
|
|
115
117
|
options = options.first if options.first.is_a?(Array) && options.length.eql?(1)
|
|
116
118
|
Aspera.assert_type(options, Array)
|
|
117
|
-
options.each{ |o| @result
|
|
119
|
+
options.each{ |o| @result.args.push(o.to_s)}
|
|
118
120
|
end
|
|
119
121
|
|
|
120
122
|
def process_params
|
|
@@ -140,7 +142,7 @@ module Aspera
|
|
|
140
142
|
return
|
|
141
143
|
end
|
|
142
144
|
# check mandatory parameter (nil is valid value), TODO: change exception ?
|
|
143
|
-
|
|
145
|
+
Aspera.assert(!(@schema['required']&.include?(name) && !properties['x-cli-special'] && !@object.key?(name)), type: Transfer::Error){"Missing mandatory parameter: #{name}"}
|
|
144
146
|
parameter_value = @object[name]
|
|
145
147
|
# no default setting
|
|
146
148
|
# parameter_value=properties['default'] if parameter_value.nil? and properties.has_key?('default')
|
|
@@ -166,7 +168,7 @@ module Aspera
|
|
|
166
168
|
return if parameter_value.nil?
|
|
167
169
|
|
|
168
170
|
# check that value is of an accepted type (string, integer, boolean)
|
|
169
|
-
|
|
171
|
+
Aspera.assert(!(properties.key?('enum') && !properties['enum'].include?(parameter_value))){"Enum value #{parameter_value} is not allowed for #{name}"}
|
|
170
172
|
|
|
171
173
|
# convert some values if value on command line needs processing from value in structure
|
|
172
174
|
if (convert = properties['x-cli-convert'])
|
|
@@ -176,7 +178,7 @@ module Aspera
|
|
|
176
178
|
when String then @convert.send(convert, parameter_value)
|
|
177
179
|
else Aspera.error_unexpected_value(convert){"Conversion type for #{name} is Hash or String only."}
|
|
178
180
|
end
|
|
179
|
-
|
|
181
|
+
Aspera.assert(!converted_value.nil?){"No conversion for: #{name}=#{parameter_value}"}
|
|
180
182
|
parameter_value = converted_value
|
|
181
183
|
end
|
|
182
184
|
|
|
@@ -190,17 +192,15 @@ module Aspera
|
|
|
190
192
|
return
|
|
191
193
|
elsif properties.key?('x-cli-envvar')
|
|
192
194
|
# set in env var
|
|
193
|
-
@result
|
|
195
|
+
@result.env[properties['x-cli-envvar']] = parameter_value
|
|
194
196
|
elsif properties['x-cli-switch']
|
|
195
|
-
# if present and true : just add option without value
|
|
196
|
-
add_param = false
|
|
197
197
|
case parameter_value
|
|
198
|
-
when
|
|
199
|
-
|
|
198
|
+
when true
|
|
199
|
+
add_command_line_options(properties['x-cli-option'])
|
|
200
|
+
when false
|
|
201
|
+
add_command_line_options(properties['x-cli-false']) if properties.key?('x-cli-false')
|
|
200
202
|
else Aspera.error_unexpected_value(parameter_value){name}
|
|
201
203
|
end
|
|
202
|
-
# add_param = !add_param if properties[:add_on_false]
|
|
203
|
-
add_command_line_options(properties['x-cli-option']) if add_param
|
|
204
204
|
else
|
|
205
205
|
# transform into command line option with value
|
|
206
206
|
# parameter_value=parameter_value.to_s if parameter_value.is_a?(Integer)
|
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
require 'aspera/assert'
|
|
4
4
|
require 'singleton'
|
|
5
5
|
require 'openssl'
|
|
6
|
+
require 'zlib'
|
|
6
7
|
|
|
7
8
|
module Aspera
|
|
8
9
|
# a simple binary data repository
|
|
10
|
+
#
|
|
11
|
+
# @!method self.instance
|
|
12
|
+
# Returns the singleton instance of DataRepository
|
|
13
|
+
# @return [DataRepository] the singleton instance
|
|
9
14
|
class DataRepository
|
|
10
15
|
include Singleton
|
|
11
16
|
|
|
@@ -19,7 +24,7 @@ module Aspera
|
|
|
19
24
|
# @return [String] decoded data
|
|
20
25
|
def item(name)
|
|
21
26
|
index = ELEMENTS.index(name)
|
|
22
|
-
|
|
27
|
+
Aspera.assert(index, type: ParameterError){"Unknown data item #{name} (#{name.class})"}
|
|
23
28
|
raw_data = data(START_INDEX + index)
|
|
24
29
|
case name
|
|
25
30
|
when :dsa, :rsa
|
data/lib/aspera/dot_container.rb
CHANGED
|
@@ -39,7 +39,7 @@ module Aspera
|
|
|
39
39
|
# @param container [Hash, Array]
|
|
40
40
|
# @param index [String, Integer]
|
|
41
41
|
def array_requires_integer_index!(container, index)
|
|
42
|
-
Aspera.assert(container.is_a?(Hash) || index.is_a?(Integer)
|
|
42
|
+
Aspera.assert(container.is_a?(Hash) || index.is_a?(Integer), 'Using String index when Integer index used previously')
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
# Create a new `Hash` or `Array` depending on type of `key`
|
|
@@ -48,7 +48,7 @@ module Aspera
|
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
# @param [Hash,Array] Container object
|
|
51
|
+
# @param container [Hash,Array] Container object
|
|
52
52
|
def initialize(container)
|
|
53
53
|
Aspera.assert_type(container, Hash)
|
|
54
54
|
# tail (pop,push) contains the next element to display
|
|
@@ -72,13 +72,13 @@ module Aspera
|
|
|
72
72
|
when Hash
|
|
73
73
|
add_elements(path, current)
|
|
74
74
|
when Array
|
|
75
|
-
# Array has no nested structures
|
|
75
|
+
# Array has no nested structures: list of Strings
|
|
76
76
|
if current.none?{ |i| i.is_a?(Array) || i.is_a?(Hash)}
|
|
77
77
|
to_insert = current.map(&:to_s)
|
|
78
|
-
# Array of Hashes with only 'name' keys
|
|
78
|
+
# Array of Hashes with only 'name' keys: list of Strings
|
|
79
79
|
elsif current.all?{ |i| i.is_a?(Hash) && i.keys == ['name']}
|
|
80
80
|
to_insert = current.map{ |i| i['name']}
|
|
81
|
-
# Array of Hashes with only 'name' and 'value' keys
|
|
81
|
+
# Array of Hashes with only 'name' and 'value' keys: Hash of key/values
|
|
82
82
|
elsif current.all?{ |i| i.is_a?(Hash) && i.key?('name') && i.key?('value') && i.length <= 3}
|
|
83
83
|
# if there is an extra key, other than 'name' and 'value', insert that key as is
|
|
84
84
|
add_elements(path, current.flat_map{ |h| h.except('name', 'value').to_a})
|
data/lib/aspera/environment.rb
CHANGED
|
@@ -13,6 +13,10 @@ require 'shellwords'
|
|
|
13
13
|
|
|
14
14
|
module Aspera
|
|
15
15
|
# detect OS, architecture, and specific stuff
|
|
16
|
+
#
|
|
17
|
+
# @!method self.instance
|
|
18
|
+
# Returns the singleton instance of Environment
|
|
19
|
+
# @return [Environment] the singleton instance
|
|
16
20
|
class Environment
|
|
17
21
|
include Singleton
|
|
18
22
|
|
|
@@ -74,7 +78,9 @@ module Aspera
|
|
|
74
78
|
argv
|
|
75
79
|
end
|
|
76
80
|
|
|
77
|
-
#
|
|
81
|
+
# Like `Shellwords.shellescape`, but does not escape `=`
|
|
82
|
+
# @param str [String] String to escape for shell usage
|
|
83
|
+
# @return [String] Shell-safe string
|
|
78
84
|
def shell_escape_pretty(str)
|
|
79
85
|
# Safe unquoted characters + '=' explicitly allowed
|
|
80
86
|
return str if str.match?(%r{\A[A-Za-z0-9_.,:/@+=-]+\z})
|
|
@@ -111,7 +117,7 @@ module Aspera
|
|
|
111
117
|
# @raise [RuntimeError] If `:exception` is `true` and the process fails in `:capture` mode.
|
|
112
118
|
def secure_execute(*cmd, mode: :execute, **kwargs)
|
|
113
119
|
cmd = cmd.map(&:to_s)
|
|
114
|
-
Aspera.assert(cmd.size.positive?,
|
|
120
|
+
Aspera.assert(cmd.size.positive?, 'executable must be present', type: ArgumentError)
|
|
115
121
|
Aspera.assert_values(mode, PROCESS_MODES, type: ArgumentError){'mode'}
|
|
116
122
|
Log.log.debug do
|
|
117
123
|
parts = [mode.to_s, 'command:']
|
|
@@ -153,7 +159,7 @@ module Aspera
|
|
|
153
159
|
# @param mode [Integer] the file mode (permissions)
|
|
154
160
|
# @yieldreturn [String] The content to write to the file
|
|
155
161
|
def write_file_restricted(path, force: false, mode: nil)
|
|
156
|
-
Aspera.assert(block_given?, type: Aspera::InternalError)
|
|
162
|
+
Aspera.assert(block_given?, 'block required for write_file_restricted', type: Aspera::InternalError)
|
|
157
163
|
if force || !File.exist?(path)
|
|
158
164
|
# Windows may give error
|
|
159
165
|
File.unlink(path) rescue nil
|
|
@@ -164,7 +170,10 @@ module Aspera
|
|
|
164
170
|
return path
|
|
165
171
|
end
|
|
166
172
|
|
|
167
|
-
#
|
|
173
|
+
# Restrict access to a file or folder to the current user only (chmod 600/700)
|
|
174
|
+
# @param path [String] Path to the file or directory
|
|
175
|
+
# @param mode [Integer, nil] Octal permission mode; if nil, inferred from path type
|
|
176
|
+
# @return [nil]
|
|
168
177
|
def restrict_file_access(path, mode: nil)
|
|
169
178
|
if mode.nil?
|
|
170
179
|
# or FileUtils ?
|
|
@@ -177,11 +186,12 @@ module Aspera
|
|
|
177
186
|
end
|
|
178
187
|
end
|
|
179
188
|
File.chmod(mode, path) unless mode.nil?
|
|
189
|
+
nil
|
|
180
190
|
rescue => e
|
|
181
191
|
Log.log.warn(e.message)
|
|
182
192
|
end
|
|
183
193
|
|
|
184
|
-
# @return true if we are in a terminal
|
|
194
|
+
# @return [Boolean] true if we are in a terminal
|
|
185
195
|
def terminal?
|
|
186
196
|
$stdout.tty?
|
|
187
197
|
end
|
|
@@ -191,11 +201,25 @@ module Aspera
|
|
|
191
201
|
I18N_VARS.each{ |var| ENV[var] = 'C'}
|
|
192
202
|
end
|
|
193
203
|
|
|
194
|
-
# @return true if we can display Unicode characters
|
|
204
|
+
# @return [Boolean] true if we can display Unicode characters
|
|
205
|
+
# Uses Encoding.locale_charmap for OS-independent detection.
|
|
206
|
+
# Falls back to locale env vars for systems where charmap is not available.
|
|
195
207
|
# https://www.gnu.org/software/libc/manual/html_node/Locale-Categories.html
|
|
196
208
|
# https://pubs.opengroup.org/onlinepubs/7908799/xbd/envvar.html
|
|
197
209
|
def terminal_supports_unicode?
|
|
198
|
-
|
|
210
|
+
return false unless terminal?
|
|
211
|
+
locale_charmap_utf8? || I18N_VARS.any?{ |var| ENV[var]&.include?('UTF-8')}
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
private
|
|
215
|
+
|
|
216
|
+
# @return [Boolean] true if the locale charmap resolves to UTF-8
|
|
217
|
+
# Unix: nl_langinfo(CODESET) returns "UTF-8"
|
|
218
|
+
# Windows: GetACP returns "CP65001" which Encoding.find resolves as UTF-8
|
|
219
|
+
def locale_charmap_utf8?
|
|
220
|
+
Encoding.find(Encoding.locale_charmap) == Encoding::UTF_8
|
|
221
|
+
rescue ArgumentError
|
|
222
|
+
false
|
|
199
223
|
end
|
|
200
224
|
end
|
|
201
225
|
attr_accessor :url_method, :file_illegal_characters
|
|
@@ -266,22 +290,24 @@ module Aspera
|
|
|
266
290
|
return unless @os.eql?(OS_WINDOWS) && ENV.key?('USERPROFILE') && Dir.exist?(ENV.fetch('USERPROFILE', nil))
|
|
267
291
|
ENV['HOME'] = ENV.fetch('USERPROFILE', nil)
|
|
268
292
|
Log.log.debug{"Windows: set HOME to USERPROFILE: #{Dir.home}"}
|
|
293
|
+
nil
|
|
269
294
|
end
|
|
270
295
|
|
|
271
296
|
def graphical?
|
|
272
297
|
@default_gui_mode == :graphical
|
|
273
298
|
end
|
|
274
299
|
|
|
275
|
-
# Open a URI in
|
|
276
|
-
#
|
|
277
|
-
# @
|
|
300
|
+
# Open a URI in the system's default graphical browser (non-blocking)
|
|
301
|
+
# @param uri [String, URI] the URI to open
|
|
302
|
+
# @return [nil]
|
|
278
303
|
def open_uri_graphical(uri)
|
|
279
304
|
case @os
|
|
280
|
-
when Environment::OS_MACOS then
|
|
281
|
-
when Environment::OS_WINDOWS then
|
|
282
|
-
when Environment::OS_LINUX then
|
|
305
|
+
when Environment::OS_MACOS then self.class.secure_execute('open', uri.to_s)
|
|
306
|
+
when Environment::OS_WINDOWS then self.class.secure_execute('start', 'explorer', %Q{"#{uri}"})
|
|
307
|
+
when Environment::OS_LINUX then self.class.secure_execute('xdg-open', uri.to_s)
|
|
283
308
|
else Aspera.error_unexpected_value(os){'no graphical open method'}
|
|
284
309
|
end
|
|
310
|
+
nil
|
|
285
311
|
end
|
|
286
312
|
|
|
287
313
|
# open a file in an editor
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aspera
|
|
4
|
+
# Holds the executable name, environment variables and command line arguments for a spawned process.
|
|
5
|
+
ExecSpec = Struct.new(:exec, :env, :args, keyword_init: true) do
|
|
6
|
+
def initialize(exec: nil, env: {}, args: [], **) = super
|
|
7
|
+
|
|
8
|
+
# @return [ExecSpec] deep copy with independent :args and :env
|
|
9
|
+
def deep_clone
|
|
10
|
+
self.class.new(exec: exec, env: env.dup, args: args.dup)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/aspera/faspex_gw.rb
CHANGED
|
@@ -68,7 +68,7 @@ module Aspera
|
|
|
68
68
|
case request.path
|
|
69
69
|
when '/aspera/faspex/send'
|
|
70
70
|
begin
|
|
71
|
-
Aspera.assert(!request.body.nil
|
|
71
|
+
Aspera.assert(!request.body.nil?, 'payload missing')
|
|
72
72
|
faspex_pkg_parameters = JSON.parse(request.body)
|
|
73
73
|
Log.log.debug{"faspex pkg create parameters=#{faspex_pkg_parameters}"}
|
|
74
74
|
# compare string, as class is not yet known here
|
|
@@ -84,7 +84,7 @@ module Aspera
|
|
|
84
84
|
response.status = 200
|
|
85
85
|
response.content_type = Mime::JSON
|
|
86
86
|
response.body = JSON.generate(faspex_package_create_result)
|
|
87
|
-
rescue => e
|
|
87
|
+
rescue StandardError => e
|
|
88
88
|
response.status = 500
|
|
89
89
|
response['Content-Type'] = Mime::JSON
|
|
90
90
|
response.body = {error: e.message, stacktrace: e.backtrace}.to_json
|
|
@@ -16,7 +16,7 @@ module Aspera
|
|
|
16
16
|
@parameters = parameters.symbolize_keys
|
|
17
17
|
Log.dump(:post_proc_parameters, @parameters)
|
|
18
18
|
not_allowed = @parameters.keys - ALLOWED_PARAMETERS
|
|
19
|
-
|
|
19
|
+
Aspera.assert(not_allowed.empty?){"unsupported parameters: #{not_allowed.join(', ')}"}
|
|
20
20
|
@parameters[:script_folder] ||= '.'
|
|
21
21
|
@parameters[:fail_on_error] ||= false
|
|
22
22
|
@parameters[:timeout_seconds] ||= 60
|
|
@@ -49,6 +49,7 @@ module Aspera
|
|
|
49
49
|
Log.log.debug{"script=#{script_path}"}
|
|
50
50
|
webhook_parameters = JSON.parse(request.body)
|
|
51
51
|
Log.dump(:webhook_parameters, webhook_parameters)
|
|
52
|
+
process_status = nil
|
|
52
53
|
if request.query.key?('lambda')
|
|
53
54
|
# Code can throw exception, source code must return a lambda
|
|
54
55
|
Environment.secure_eval(File.read(script_path), __FILE__, __LINE__).call(webhook_parameters)
|
|
@@ -62,11 +63,11 @@ module Aspera
|
|
|
62
63
|
post_proc_pid = nil
|
|
63
64
|
end
|
|
64
65
|
process_status = $CHILD_STATUS
|
|
65
|
-
|
|
66
|
+
Aspera.assert(process_status.success? || !@parameters[:fail_on_error]){"script #{script_path} failed with code #{process_status.exitstatus}"}
|
|
66
67
|
end
|
|
67
68
|
response.status = 200
|
|
68
69
|
response.content_type = Mime::JSON
|
|
69
|
-
response.body = JSON.generate({status: 'success', script: script_path, exit_code: process_status
|
|
70
|
+
response.body = JSON.generate({status: 'success', script: script_path, exit_code: process_status&.exitstatus})
|
|
70
71
|
Log.log.debug{'Script executed successfully'}
|
|
71
72
|
rescue => e
|
|
72
73
|
Log.log.error("Script failed: #{e.class}:#{e.message}")
|