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
|
@@ -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,12 +75,12 @@ 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.empty?){"No known application found at #{app_url}"}
|
|
83
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
|
|
@@ -122,7 +123,7 @@ 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
|
|
@@ -133,7 +134,7 @@ module Aspera
|
|
|
133
134
|
answer = options.prompt_user_input_in_list('product', apps.map{ |a| a[:product]})
|
|
134
135
|
apps.find{ |a| a[:product].eql?(answer)}
|
|
135
136
|
end
|
|
136
|
-
wiz_preset_name =
|
|
137
|
+
wiz_preset_name = preset_name
|
|
137
138
|
Log.dump(:identification, identification)
|
|
138
139
|
wiz_url = identification[:url]
|
|
139
140
|
formatter.display_status("Using: #{identification[:name]} at #{wiz_url}".bold)
|
|
@@ -160,7 +161,7 @@ module Aspera
|
|
|
160
161
|
# Init defaults if necessary
|
|
161
162
|
option_override = options.get_option(:override, mandatory: true)
|
|
162
163
|
option_default = options.get_option(:default, mandatory: true)
|
|
163
|
-
|
|
164
|
+
@parent.context.presets.defaults_set(identification[:product], wiz_preset_name, wizard_result[:preset_value].stringify_keys, option_default, option_override)
|
|
164
165
|
test_args = wizard_result[:test_args]
|
|
165
166
|
test_args = "-P#{wiz_preset_name} #{test_args}" unless option_default
|
|
166
167
|
# TODO: actually test the command
|
|
@@ -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
|
|
@@ -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,6 +3,7 @@
|
|
|
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
|
|
@@ -23,7 +24,7 @@ module Aspera
|
|
|
23
24
|
# @return [String] decoded data
|
|
24
25
|
def item(name)
|
|
25
26
|
index = ELEMENTS.index(name)
|
|
26
|
-
|
|
27
|
+
Aspera.assert(index, type: ParameterError){"Unknown data item #{name} (#{name.class})"}
|
|
27
28
|
raw_data = data(START_INDEX + index)
|
|
28
29
|
case name
|
|
29
30
|
when :dsa, :rsa
|
data/lib/aspera/dot_container.rb
CHANGED
|
@@ -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
|
@@ -78,7 +78,9 @@ module Aspera
|
|
|
78
78
|
argv
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
#
|
|
81
|
+
# Like `Shellwords.shellescape`, but does not escape `=`
|
|
82
|
+
# @param str [String] String to escape for shell usage
|
|
83
|
+
# @return [String] Shell-safe string
|
|
82
84
|
def shell_escape_pretty(str)
|
|
83
85
|
# Safe unquoted characters + '=' explicitly allowed
|
|
84
86
|
return str if str.match?(%r{\A[A-Za-z0-9_.,:/@+=-]+\z})
|
|
@@ -168,7 +170,10 @@ module Aspera
|
|
|
168
170
|
return path
|
|
169
171
|
end
|
|
170
172
|
|
|
171
|
-
#
|
|
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]
|
|
172
177
|
def restrict_file_access(path, mode: nil)
|
|
173
178
|
if mode.nil?
|
|
174
179
|
# or FileUtils ?
|
|
@@ -181,11 +186,12 @@ module Aspera
|
|
|
181
186
|
end
|
|
182
187
|
end
|
|
183
188
|
File.chmod(mode, path) unless mode.nil?
|
|
189
|
+
nil
|
|
184
190
|
rescue => e
|
|
185
191
|
Log.log.warn(e.message)
|
|
186
192
|
end
|
|
187
193
|
|
|
188
|
-
# @return true if we are in a terminal
|
|
194
|
+
# @return [Boolean] true if we are in a terminal
|
|
189
195
|
def terminal?
|
|
190
196
|
$stdout.tty?
|
|
191
197
|
end
|
|
@@ -195,7 +201,7 @@ module Aspera
|
|
|
195
201
|
I18N_VARS.each{ |var| ENV[var] = 'C'}
|
|
196
202
|
end
|
|
197
203
|
|
|
198
|
-
# @return true if we can display Unicode characters
|
|
204
|
+
# @return [Boolean] true if we can display Unicode characters
|
|
199
205
|
# Uses Encoding.locale_charmap for OS-independent detection.
|
|
200
206
|
# Falls back to locale env vars for systems where charmap is not available.
|
|
201
207
|
# https://www.gnu.org/software/libc/manual/html_node/Locale-Categories.html
|
|
@@ -207,7 +213,7 @@ module Aspera
|
|
|
207
213
|
|
|
208
214
|
private
|
|
209
215
|
|
|
210
|
-
# @return true if the locale charmap resolves to UTF-8
|
|
216
|
+
# @return [Boolean] true if the locale charmap resolves to UTF-8
|
|
211
217
|
# Unix: nl_langinfo(CODESET) returns "UTF-8"
|
|
212
218
|
# Windows: GetACP returns "CP65001" which Encoding.find resolves as UTF-8
|
|
213
219
|
def locale_charmap_utf8?
|
|
@@ -291,16 +297,17 @@ module Aspera
|
|
|
291
297
|
@default_gui_mode == :graphical
|
|
292
298
|
end
|
|
293
299
|
|
|
294
|
-
# Open a URI in
|
|
295
|
-
#
|
|
296
|
-
# @
|
|
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]
|
|
297
303
|
def open_uri_graphical(uri)
|
|
298
304
|
case @os
|
|
299
|
-
when Environment::OS_MACOS then
|
|
300
|
-
when Environment::OS_WINDOWS then
|
|
301
|
-
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)
|
|
302
308
|
else Aspera.error_unexpected_value(os){'no graphical open method'}
|
|
303
309
|
end
|
|
310
|
+
nil
|
|
304
311
|
end
|
|
305
312
|
|
|
306
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
|
@@ -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}")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/rest_call_error'
|
|
4
|
+
|
|
5
|
+
module Aspera
|
|
6
|
+
# Helper to execute GraphQL queries via a Rest object.
|
|
7
|
+
# Follows the GraphQL-over-HTTP convention: POST {query, variables} to endpoint,
|
|
8
|
+
# parse top-level `data` key, raise on top-level `errors` array.
|
|
9
|
+
module GraphQL
|
|
10
|
+
QUERIES_FOLDER = File.join(__dir__, 'api', 'queries')
|
|
11
|
+
private_constant :QUERIES_FOLDER
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
# Execute a GraphQL query loaded from a .graphql file in lib/aspera/api/queries/
|
|
15
|
+
# @param rest [Rest] API object pointing at the GraphQL endpoint
|
|
16
|
+
# @param name [String] Base filename without extension (e.g. 'my_query')
|
|
17
|
+
# @param variables [Hash] GraphQL variables
|
|
18
|
+
# @return [Hash] The `data` object from the response
|
|
19
|
+
# @raise [RestCallError] if the response contains GraphQL errors
|
|
20
|
+
def execute(rest, name, variables = {})
|
|
21
|
+
query = load_query(name)
|
|
22
|
+
response = rest.create(nil, {query: query, variables: variables})
|
|
23
|
+
errors = response['errors']
|
|
24
|
+
raise RestCallError, errors.map{ |e| e['message']}.join("\n") if errors.is_a?(Array) && !errors.empty?
|
|
25
|
+
response['data']
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def load_query(name)
|
|
31
|
+
File.read(File.join(QUERIES_FOLDER, "#{name}.graphql"))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/aspera/hash_ext.rb
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class ::Hash
|
|
4
|
+
# Recursively merge another hash, with nested hashes merged deeply
|
|
5
|
+
# @param second [Hash] Hash to merge into self
|
|
6
|
+
# @return [Hash] New merged hash
|
|
4
7
|
def deep_merge(second)
|
|
5
8
|
merge(second){ |_key, v1, v2| v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.deep_merge(v2) : v2}
|
|
6
9
|
end
|
|
7
10
|
|
|
11
|
+
# Recursively merge another hash in-place, with nested hashes merged deeply
|
|
12
|
+
# @param second [Hash] Hash to merge into self
|
|
13
|
+
# @return [Hash] self
|
|
8
14
|
def deep_merge!(second)
|
|
9
15
|
merge!(second){ |_key, v1, v2| v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.deep_merge!(v2) : v2}
|
|
10
16
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# cspell:ignore blankslate jsonrpc
|
|
4
|
+
|
|
5
|
+
require 'aspera/rest_error_analyzer'
|
|
6
|
+
require 'aspera/assert'
|
|
7
|
+
require 'aspera/json_rpc/version'
|
|
8
|
+
require 'blankslate'
|
|
9
|
+
|
|
10
|
+
Aspera::RestErrorAnalyzer.instance.add_simple_handler(name: 'JSON RPC', path: %w[error message], always: true)
|
|
11
|
+
|
|
12
|
+
module Aspera
|
|
13
|
+
module JsonRpc
|
|
14
|
+
# JSON-RPC 2.0 client over an Aspera::Rest HTTP endpoint.
|
|
15
|
+
# Methods are dispatched dynamically via method_missing.
|
|
16
|
+
# Example:
|
|
17
|
+
# client = JsonRpc::Client.new(Rest.new(base_url: 'http://127.0.0.1:33024'))
|
|
18
|
+
# client.get_info
|
|
19
|
+
# client.start_transfer(app_id: '...', transfer_spec: {...})
|
|
20
|
+
class Client < BlankSlate
|
|
21
|
+
reveal :instance_variable_get
|
|
22
|
+
reveal :inspect
|
|
23
|
+
reveal :to_s
|
|
24
|
+
|
|
25
|
+
# @param api [Rest] Aspera REST object pointing at the JSON-RPC endpoint
|
|
26
|
+
# @param namespace [String, nil] optional method prefix, e.g. "myns."
|
|
27
|
+
def initialize(api, namespace = nil)
|
|
28
|
+
super()
|
|
29
|
+
@api = api
|
|
30
|
+
@namespace = namespace
|
|
31
|
+
@request_id = 0
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def respond_to_missing?(_sym, _include_private = false)
|
|
35
|
+
true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Dispatch any Ruby method call as a JSON-RPC request
|
|
39
|
+
def method_missing(method, *args, &block)
|
|
40
|
+
args = args.first if args.size == 1 && args.first.is_a?(Hash)
|
|
41
|
+
data = @api.create('', {
|
|
42
|
+
jsonrpc: VERSION,
|
|
43
|
+
method: "#{@namespace}#{method}",
|
|
44
|
+
params: args,
|
|
45
|
+
id: @request_id += 1
|
|
46
|
+
})
|
|
47
|
+
Aspera.assert_type(data, Hash){'response'}
|
|
48
|
+
Aspera.assert(data['jsonrpc'] == VERSION, 'bad version in response')
|
|
49
|
+
Aspera.assert(data.key?('id'), 'missing id in response')
|
|
50
|
+
Aspera.assert(!(data.key?('error') && data.key?('result')), 'both error and response')
|
|
51
|
+
Aspera.assert(
|
|
52
|
+
!data.key?('error') ||
|
|
53
|
+
data['error'].is_a?(Hash) &&
|
|
54
|
+
data['error']['code'].is_a?(Integer) &&
|
|
55
|
+
data['error']['message'].is_a?(String),
|
|
56
|
+
'bad error response'
|
|
57
|
+
)
|
|
58
|
+
return data['result']
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -64,7 +64,7 @@ module Aspera
|
|
|
64
64
|
def set(options)
|
|
65
65
|
validate_set(options)
|
|
66
66
|
label = options.delete(:label)
|
|
67
|
-
|
|
67
|
+
Aspera.assert(!@all_secrets.key?(label)){"secret #{label} already exist, delete first"}
|
|
68
68
|
@all_secrets[label] = options.symbolize_keys
|
|
69
69
|
save
|
|
70
70
|
end
|
|
@@ -24,19 +24,19 @@ module Aspera
|
|
|
24
24
|
info[:password] = password
|
|
25
25
|
# this module requires compilation, so it is optional
|
|
26
26
|
require 'aspera/keychain/encrypted_hash'
|
|
27
|
-
|
|
27
|
+
Keychain::EncryptedHash.new(**info)
|
|
28
28
|
when 'system'
|
|
29
29
|
case Environment.instance.os
|
|
30
30
|
when Environment::OS_MACOS
|
|
31
31
|
info[:name] ||= name
|
|
32
|
-
|
|
32
|
+
Keychain::MacosSystem.new(**info)
|
|
33
33
|
else
|
|
34
34
|
raise Error, 'not implemented for this OS'
|
|
35
35
|
end
|
|
36
36
|
when 'vault'
|
|
37
37
|
require 'aspera/keychain/hashicorp_vault'
|
|
38
38
|
info[:token] ||= password
|
|
39
|
-
|
|
39
|
+
Keychain::HashicorpVault.new(**info)
|
|
40
40
|
else Aspera.error_unexpected_value(vault_type)
|
|
41
41
|
end
|
|
42
42
|
end
|
|
@@ -44,7 +44,7 @@ module Aspera
|
|
|
44
44
|
uri = URI.parse(url)
|
|
45
45
|
Aspera.assert(uri.scheme.eql?('https'), 'only https')
|
|
46
46
|
options[:protocol] = 'htps' # cspell: disable-line
|
|
47
|
-
|
|
47
|
+
Aspera.assert(!uri.host.nil?, type: Error){'host required in URL'}
|
|
48
48
|
options[:server] = uri.host
|
|
49
49
|
options[:path] = uri.path unless ['', '/'].include?(uri.path)
|
|
50
50
|
options[:port] = uri.port unless uri.port.eql?(443) && !url.include?(':443/')
|
|
@@ -128,7 +128,7 @@ module Aspera
|
|
|
128
128
|
super()
|
|
129
129
|
@keychain_name = name.nil? ? 'default keychain' : name
|
|
130
130
|
@keychain = name.nil? ? MacosSecurity::Keychain.default : MacosSecurity::Keychain.by_name(name)
|
|
131
|
-
|
|
131
|
+
Aspera.assert(!@keychain.nil?){"no such keychain #{name}"}
|
|
132
132
|
end
|
|
133
133
|
|
|
134
134
|
def info
|
|
@@ -155,7 +155,7 @@ module Aspera
|
|
|
155
155
|
unsupported = options.keys - %i[label]
|
|
156
156
|
Aspera.assert(unsupported.empty?){"unsupported options: #{unsupported}"}
|
|
157
157
|
info = @keychain.password(:find, :generic, label: options[:label])
|
|
158
|
-
|
|
158
|
+
Aspera.assert(!info.nil?, type: Error){'not found'}
|
|
159
159
|
result = options.clone
|
|
160
160
|
result[:secret] = info['password']
|
|
161
161
|
result[:description] = info['icmt'] # cspell: disable-line
|