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
|
@@ -67,10 +67,10 @@ module Aspera
|
|
|
67
67
|
product_name = ascp_location.delete_prefix(USE_PRODUCT_PREFIX)
|
|
68
68
|
if product_name.eql?(FIRST_FOUND)
|
|
69
69
|
pl = installed_products.first
|
|
70
|
-
|
|
70
|
+
Aspera.assert(!pl.nil?){"No Aspera transfer module or SDK found.\nRefer to the manual or install SDK with command:\nascli conf transferd install"}
|
|
71
71
|
else
|
|
72
72
|
pl = installed_products.find{ |i| i[:name].eql?(product_name)}
|
|
73
|
-
|
|
73
|
+
Aspera.assert(!pl.nil?){"No such product installed: #{product_name}"}
|
|
74
74
|
end
|
|
75
75
|
File.dirname(pl[:ascp_path])
|
|
76
76
|
else
|
|
@@ -88,14 +88,14 @@ module Aspera
|
|
|
88
88
|
# @return [Hash] with key = file name (String), and value = path to file
|
|
89
89
|
def file_paths
|
|
90
90
|
return SDK_FILES.to_h do |v|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
91
|
+
[v.to_s, begin
|
|
92
|
+
path(v)
|
|
93
|
+
rescue Errno::ENOENT => e
|
|
94
|
+
e.message.gsub(/.*assertion failed: /, '').gsub(/\): .*/, ')')
|
|
95
|
+
rescue => e
|
|
96
|
+
e.message
|
|
97
|
+
end]
|
|
98
|
+
end
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
# TODO: if using another product than SDK, should use files from there
|
|
@@ -106,18 +106,18 @@ module Aspera
|
|
|
106
106
|
|
|
107
107
|
# Get path of one resource file of currently activated product
|
|
108
108
|
# keys and certs are generated locally... (they are well known values, arch. independent)
|
|
109
|
-
# @param
|
|
109
|
+
# @param file_type [Symbol] key of the resource file
|
|
110
110
|
# @return [String, nil] Full path to the resource file or nil if not found
|
|
111
|
-
def path(
|
|
111
|
+
def path(file_type)
|
|
112
112
|
file_is_required = true
|
|
113
|
-
case
|
|
113
|
+
case file_type
|
|
114
114
|
when *EXE_FILES
|
|
115
|
-
file_is_required =
|
|
115
|
+
file_is_required = file_type.eql?(:ascp)
|
|
116
116
|
file = Products::Transferd.transferd_path
|
|
117
|
-
file = File.join(File.dirname(file), Environment.instance.exe_file(
|
|
117
|
+
file = File.join(File.dirname(file), Environment.instance.exe_file(file_type.to_s)) unless file_type.eql?(:transferd)
|
|
118
118
|
when :ssh_private_dsa, :ssh_private_rsa
|
|
119
119
|
# assume last 3 letters are type
|
|
120
|
-
type =
|
|
120
|
+
type = file_type.to_s[-3..].to_sym
|
|
121
121
|
file = check_or_create_sdk_file("aspera_bypass_#{type}.pem"){DataRepository.instance.item(type)}
|
|
122
122
|
when :aspera_license
|
|
123
123
|
file = check_or_create_sdk_file('aspera-license'){DataRepository.instance.item(:license)}
|
|
@@ -134,22 +134,23 @@ module Aspera
|
|
|
134
134
|
check_or_create_sdk_file('aspera_fallback_cert_private_key.pem', force: true){private_key.to_pem}
|
|
135
135
|
check_or_create_sdk_file('aspera_fallback_cert.pem', force: true){cert.to_pem}
|
|
136
136
|
end
|
|
137
|
-
file =
|
|
138
|
-
else Aspera.error_unexpected_value(
|
|
137
|
+
file = file_type.eql?(:fallback_certificate) ? file_cert : file_key
|
|
138
|
+
else Aspera.error_unexpected_value(file_type)
|
|
139
139
|
end
|
|
140
140
|
return unless file_is_required || File.exist?(file)
|
|
141
|
-
Aspera.assert(File.exist?(file), type: Errno::ENOENT){"#{
|
|
141
|
+
Aspera.assert(File.exist?(file), type: Errno::ENOENT){"#{file_type} not found (#{file})"}
|
|
142
142
|
return file
|
|
143
143
|
end
|
|
144
144
|
|
|
145
145
|
# default bypass key phrase
|
|
146
|
-
# @return [String]
|
|
146
|
+
# @return [String] UUID used as bypass key phrase for SSH certificate
|
|
147
147
|
def ssh_cert_uuid
|
|
148
148
|
return DataRepository.instance.item(:uuid)
|
|
149
149
|
end
|
|
150
150
|
|
|
151
151
|
# get paths of SSH keys to use for ascp client
|
|
152
|
-
# @param types [Symbol] types to use
|
|
152
|
+
# @param types [Symbol] types to use (one of CLIENT_SSH_KEY_OPTIONS)
|
|
153
|
+
# @return [Array<String>] list of private key file paths
|
|
153
154
|
def aspera_token_ssh_key_paths(types)
|
|
154
155
|
Aspera.assert_values(types, CLIENT_SSH_KEY_OPTIONS)
|
|
155
156
|
return case types
|
|
@@ -170,13 +171,11 @@ module Aspera
|
|
|
170
171
|
Aspera.assert_type(exe_path, String)
|
|
171
172
|
Aspera.assert_type(vers_arg, String)
|
|
172
173
|
return unless File.exist?(exe_path)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
raise "An error occurred when testing #{exe_path}: #{cmd_out}" unless $CHILD_STATUS == 0
|
|
174
|
+
cmd_out, _err, status = Environment.secure_execute(exe_path, vers_arg, mode: :capture, exception: false)
|
|
175
|
+
raise "An error occurred when testing #{exe_path}: #{cmd_out}" unless status.success?
|
|
176
176
|
# get version from ascp, only after full extract, as windows requires DLLs (SSL/TLS/etc...)
|
|
177
177
|
m = cmd_out.match(/ version ([0-9.]+)/)
|
|
178
|
-
|
|
179
|
-
return exe_version
|
|
178
|
+
m.nil? ? nil : m[1].gsub(/\.$/, '')
|
|
180
179
|
end
|
|
181
180
|
|
|
182
181
|
# Extract some stings from ascp logs
|
|
@@ -234,15 +233,15 @@ module Aspera
|
|
|
234
233
|
return ascp_data
|
|
235
234
|
end
|
|
236
235
|
|
|
237
|
-
# @return the url for download of SDK archive for the given platform and version
|
|
236
|
+
# @return [String] the url for download of SDK archive for the given platform and version
|
|
238
237
|
def sdk_url_for_platform(platform: nil, version: nil)
|
|
239
238
|
all_locations = sdk_locations
|
|
240
239
|
platform = Environment.instance.architecture if platform.nil?
|
|
241
240
|
locations = all_locations.select{ |l| l['platform'].eql?(platform)}
|
|
242
|
-
|
|
241
|
+
Aspera.assert(!locations.empty?){"No SDK for platform: #{platform}, available: #{all_locations.map{ |i| i['platform']}.uniq}"}
|
|
243
242
|
version = locations.max_by{ |entry| Gem::Version.new(entry['version'])}['version'] if version.nil?
|
|
244
243
|
info = locations.select{ |entry| entry['version'].eql?(version)}
|
|
245
|
-
|
|
244
|
+
Aspera.assert(!info.empty?){"No such version: #{version} for #{platform}"}
|
|
246
245
|
return info.first['url']
|
|
247
246
|
end
|
|
248
247
|
|
|
@@ -282,15 +281,14 @@ module Aspera
|
|
|
282
281
|
end
|
|
283
282
|
end
|
|
284
283
|
|
|
285
|
-
#
|
|
284
|
+
# Downloads and extracts the SDK archive for the current platform into +folder+
|
|
286
285
|
# @param folder [String] Destination folder path
|
|
287
286
|
# @param url [nil, String] URL to SDK archive, if nil: default url for version
|
|
288
287
|
# @param version [nil, String] Specific version, if nil: latest version
|
|
289
288
|
# @param backup [Boolean] If destination folder exists, then rename
|
|
290
289
|
# @yieldparam entry_name [String] File path from archive
|
|
291
290
|
# @yieldreturn [String, nil] Destination sub folder (end with /) or file, or nil to not extract
|
|
292
|
-
|
|
293
|
-
def install_sdk(folder:, url: nil, version: nil, backup: true)
|
|
291
|
+
def download_sdk(folder:, url: nil, version: nil, backup: true)
|
|
294
292
|
url ||= sdk_url_for_platform(version: version)
|
|
295
293
|
# Rename old install
|
|
296
294
|
if backup && Dir.exist?(folder) && !Dir.empty?(folder)
|
|
@@ -372,12 +370,13 @@ module Aspera
|
|
|
372
370
|
end
|
|
373
371
|
end
|
|
374
372
|
|
|
375
|
-
#
|
|
376
|
-
# @param url
|
|
377
|
-
# @param version [nil, String]
|
|
378
|
-
|
|
373
|
+
# Downloads, installs and configures the SDK, returns name, version and install folder
|
|
374
|
+
# @param url [nil, String] URL to SDK archive, if nil: default url for version
|
|
375
|
+
# @param version [nil, String] Specific version, if nil: latest version
|
|
376
|
+
# @return [Array<String>] sdk name, sdk version, install folder
|
|
377
|
+
def install_sdk(url: nil, version: nil)
|
|
379
378
|
folder = Products::Transferd.sdk_directory
|
|
380
|
-
|
|
379
|
+
download_sdk(folder: folder, url: url, version: version)
|
|
381
380
|
# Ensure necessary files are there, or generate them, restrict file access on SDK executables
|
|
382
381
|
SDK_FILES.each do |file_id_sym|
|
|
383
382
|
file_path = path(file_id_sym)
|
|
@@ -433,7 +432,7 @@ module Aspera
|
|
|
433
432
|
|
|
434
433
|
public
|
|
435
434
|
|
|
436
|
-
# @return the list of installed products in format of product_locations_on_current_os
|
|
435
|
+
# @return [Array<Hash>] the list of installed products in format of product_locations_on_current_os
|
|
437
436
|
def installed_products
|
|
438
437
|
return @found_products unless @found_products.nil?
|
|
439
438
|
# :expected M app name is taken from the manifest if present, else defaults to this value
|
|
@@ -12,7 +12,7 @@ module Aspera
|
|
|
12
12
|
# https://www.ibm.com/support/pages/error-code-reference-tables
|
|
13
13
|
# mgmtmess.c : as_mgmt_err_is_retryable
|
|
14
14
|
# Note that the fact that an error is retry-able is not internally defined by protocol, it's client-side responsibility
|
|
15
|
-
# rubocop:disable Layout/FirstHashElementLineBreak
|
|
15
|
+
# rubocop:disable-next Layout/FirstHashElementLineBreak
|
|
16
16
|
ERRORS = {
|
|
17
17
|
# id retry-able mnemo/code message additional info
|
|
18
18
|
0 => {r: false, c: 'UNKNOWN', m: 'unknown', a: 'unknown'},
|
|
@@ -93,7 +93,6 @@ module Aspera
|
|
|
93
93
|
66 => {r: false, c: 'PEER_REQUIRES_FIPS', m: 'Peer rejects cipher due to FIPS mode enabled on peer',
|
|
94
94
|
a: 'Peer rejects cipher due to FIPS mode enabled on peer'}
|
|
95
95
|
}.freeze
|
|
96
|
-
# rubocop:enable Layout/FirstHashElementLineBreak
|
|
97
96
|
# cspell: disable
|
|
98
97
|
OPERATIONS = %w[
|
|
99
98
|
NOP
|
|
@@ -307,7 +306,7 @@ module Aspera
|
|
|
307
306
|
def field_snake_to_native(name)
|
|
308
307
|
field = name.delete('_')
|
|
309
308
|
result = PARAMETERS.find{ |w| w.casecmp?(field)}
|
|
310
|
-
|
|
309
|
+
Aspera.assert(!result.nil?){"No such field: #{name}"}
|
|
311
310
|
result
|
|
312
311
|
end
|
|
313
312
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/agent/factory'
|
|
4
|
+
require 'aspera/schema/registry'
|
|
5
|
+
require 'aspera/schema/reader'
|
|
6
|
+
|
|
7
|
+
module Aspera
|
|
8
|
+
module Cli
|
|
9
|
+
# Mixin for Config plugin: ASCP / Transferd related actions
|
|
10
|
+
module AscpActions
|
|
11
|
+
TRANSFERD_APP_NAME = 'sdk'
|
|
12
|
+
|
|
13
|
+
# Mapping from agent symbol to schema key in options.schema.yaml
|
|
14
|
+
# Agents without dedicated schema (connect, desktop) are omitted.
|
|
15
|
+
AGENT_SCHEMA_KEY = {
|
|
16
|
+
direct: Schema::Registry::DIRECT_AGENT_OPTIONS,
|
|
17
|
+
node: Schema::Registry::NODE_AGENT_OPTIONS,
|
|
18
|
+
httpgw: Schema::Registry::HTTPGW_AGENT_OPTIONS,
|
|
19
|
+
transferd: Schema::Registry::TRANSFERD_AGENT_OPTIONS
|
|
20
|
+
}.freeze
|
|
21
|
+
private_constant :AGENT_SCHEMA_KEY
|
|
22
|
+
|
|
23
|
+
# Set the SDK directory, checking default and former locations
|
|
24
|
+
def set_sdk_dir
|
|
25
|
+
sdk_dir = Products::Transferd.sdk_directory rescue nil
|
|
26
|
+
if sdk_dir.nil?
|
|
27
|
+
@sdk_default_location = true
|
|
28
|
+
Log.log.debug('SDK folder is not set, checking default')
|
|
29
|
+
sdk_dir = self.class.default_app_main_folder(app_name: TRANSFERD_APP_NAME)
|
|
30
|
+
Log.log.debug{"Checking: #{sdk_dir}"}
|
|
31
|
+
if !Dir.exist?(sdk_dir)
|
|
32
|
+
Log.log.debug{"No such folder: #{sdk_dir}"}
|
|
33
|
+
former_sdk_folder = File.join(self.class.default_app_main_folder(app_name: Info::CMD_NAME), TRANSFERD_APP_NAME)
|
|
34
|
+
Log.log.debug{"Checking: #{former_sdk_folder}"}
|
|
35
|
+
sdk_dir = former_sdk_folder if Dir.exist?(former_sdk_folder)
|
|
36
|
+
end
|
|
37
|
+
Log.log.debug{"Using: #{sdk_dir}"}
|
|
38
|
+
Products::Transferd.sdk_directory = sdk_dir
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Install the transfer SDK (ascp + transferd) from a URL or using the default source.
|
|
43
|
+
# Version defaults to +Info::SDK_VERSION+; pass +LATEST+ as argument to install the latest available version.
|
|
44
|
+
# @param version [String, nil] version to install; nil means use the default SDK version
|
|
45
|
+
# @return [Result::Status] installation result message
|
|
46
|
+
def install_transfer_sdk(version: nil)
|
|
47
|
+
asked_version = version.nil? ? Info::SDK_VERSION : version
|
|
48
|
+
asked_version = nil if asked_version.eql?(SpecialValues::LATEST)
|
|
49
|
+
sdk_url = options.get_option(:sdk_url, mandatory: true)
|
|
50
|
+
sdk_url = nil if sdk_url.eql?(SpecialValues::DEF)
|
|
51
|
+
name, ver, folder = Ascp::Installation.instance.install_sdk(url: sdk_url, version: asked_version)
|
|
52
|
+
return Result::Status.new("Installed #{name} version #{ver} in #{folder}")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def action_ascp_show(**)
|
|
56
|
+
Result::Text.new(Ascp::Installation.instance.path(:ascp))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def action_ascp_info(**)
|
|
60
|
+
data = Ascp::Installation.instance.ascp_info
|
|
61
|
+
data['ts'] = transfer.user_transfer_spec
|
|
62
|
+
DataRepository::ELEMENTS.each_with_object(data){ |i, h| h[i.to_s] = DataRepository.instance.item(i)}
|
|
63
|
+
SecretHider::ADDITIONAL_KEYS_TO_HIDE.concat(DataRepository::ELEMENTS.map(&:to_s))
|
|
64
|
+
Result::SingleObject.new(data)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def action_ascp_install(version: nil, **)
|
|
68
|
+
install_transfer_sdk(version: version)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def action_ascp_spec(**)
|
|
72
|
+
builder = Schema::Documentation.new(TerminalFormatter, Transfer::Spec::SCHEMA, include_option: true, agent_columns: true).build
|
|
73
|
+
Result::ObjectList.new(builder.rows, fields: builder.columns)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def action_ascp_schema(agent_name: nil, **)
|
|
77
|
+
schema = Transfer::Spec::SCHEMA.current.merge({'$comment'=>'DO NOT EDIT, this file was generated from the YAML.'})
|
|
78
|
+
schema['properties'] = schema['properties'].select{ |_k, v| CommandLineBuilder.supported_by_agent(agent_name, v)} unless agent_name.nil?
|
|
79
|
+
schema['properties'] = schema['properties'].sort.to_h
|
|
80
|
+
Result::SingleObject.new(schema)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def action_ascp_errors(**)
|
|
84
|
+
error_data = []
|
|
85
|
+
Ascp::Management::ERRORS.each_pair do |code, prop|
|
|
86
|
+
error_data.push(code: code, mnemonic: prop[:c], retry: prop[:r], info: prop[:a])
|
|
87
|
+
end
|
|
88
|
+
Result::ObjectList.new(error_data)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def action_ascp_products_list(**)
|
|
92
|
+
Result::ObjectList.new(Ascp::Installation.instance.installed_products, fields: %w[name app_root])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def action_agents_list(**)
|
|
96
|
+
rows = Agent::Factory::ALL.map do |sym, names|
|
|
97
|
+
schema_key = AGENT_SCHEMA_KEY[sym]
|
|
98
|
+
param_names =
|
|
99
|
+
if schema_key
|
|
100
|
+
Schema::Registry.instance.reader(schema_key).current['properties']&.keys&.sort&.join(', ') || ''
|
|
101
|
+
else
|
|
102
|
+
''
|
|
103
|
+
end
|
|
104
|
+
{
|
|
105
|
+
'name' => sym.to_s,
|
|
106
|
+
'short' => names[:short].to_s,
|
|
107
|
+
'parameters' => param_names
|
|
108
|
+
}
|
|
109
|
+
end.sort_by{ |r| r['name']}
|
|
110
|
+
Result::ObjectList.new(rows, fields: %w[name short parameters])
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def action_agents_show(agent_name:, **)
|
|
114
|
+
names = Agent::Factory::ALL[agent_name]
|
|
115
|
+
schema_key = AGENT_SCHEMA_KEY[agent_name]
|
|
116
|
+
agent_info = {
|
|
117
|
+
'name' => agent_name.to_s,
|
|
118
|
+
'short' => names[:short].to_s,
|
|
119
|
+
'description' => schema_key ? Schema::Registry.instance.reader(schema_key).current['description'].to_s : '(no configurable parameters)'
|
|
120
|
+
}
|
|
121
|
+
if schema_key
|
|
122
|
+
properties = Schema::Registry.instance.reader(schema_key).current['properties'] || {}
|
|
123
|
+
rows = properties.map do |pname, pdef|
|
|
124
|
+
row = {'parameter' => pname, 'type' => pdef['type'].to_s, 'description' => pdef['description'].to_s}
|
|
125
|
+
row['required'] = (Schema::Registry.instance.reader(schema_key).current['required'] || []).include?(pname) ? 'yes' : 'no'
|
|
126
|
+
row['default'] = pdef.key?('default') ? pdef['default'].inspect : ''
|
|
127
|
+
row['enum'] = pdef.key?('enum') ? pdef['enum'].join(', ') : ''
|
|
128
|
+
row
|
|
129
|
+
end
|
|
130
|
+
return Result::ObjectList.new(rows, fields: %w[parameter required type default enum description])
|
|
131
|
+
end
|
|
132
|
+
Result::SingleObject.new(agent_info)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def action_agents_parameters(agent_name:, **)
|
|
136
|
+
schema_key = AGENT_SCHEMA_KEY[agent_name]
|
|
137
|
+
return Result::Nothing.new if schema_key.nil?
|
|
138
|
+
properties = Schema::Registry.instance.reader(schema_key).current['properties'] || {}
|
|
139
|
+
rows = properties.map do |pname, pdef|
|
|
140
|
+
{'name' => pname, 'type' => pdef['type'].to_s, 'description' => pdef['description'].to_s}
|
|
141
|
+
end
|
|
142
|
+
Result::ObjectList.new(rows, fields: %w[name type description])
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def action_transferd_install(**)
|
|
146
|
+
install_transfer_sdk
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def action_transferd_list(**)
|
|
150
|
+
sdk_list = Ascp::Installation.instance.sdk_locations
|
|
151
|
+
Result::ObjectList.new(sdk_list, fields: sdk_list.first.keys - ['url'])
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/log'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
|
|
7
|
+
module Aspera
|
|
8
|
+
module Cli
|
|
9
|
+
# Persist the state of asynchronous transfers on the local file system.
|
|
10
|
+
#
|
|
11
|
+
# Each entry is a JSON document identified by a `job_id` (UUID generated by ascli).
|
|
12
|
+
# The underlying storage uses PersistencyFolder with the category prefix
|
|
13
|
+
# +CATEGORY+ so all async-transfer files are grouped together and can be
|
|
14
|
+
# garbage-collected independently.
|
|
15
|
+
#
|
|
16
|
+
# Schema of a stored entry:
|
|
17
|
+
# job_id [String] UUID generated by ascli (= the store key)
|
|
18
|
+
# agent_type [String] 'desktop' | 'node' | 'connect' | 'transferd' | 'direct'
|
|
19
|
+
# transfer_id [String] Opaque ID returned by the agent's start_transfer
|
|
20
|
+
# agent_params [Hash] Agent-specific connection parameters for re-querying
|
|
21
|
+
# status [String] 'running' | 'completed' | 'failed' | 'cancelled'
|
|
22
|
+
# bytes_transferred [Integer] Last known bytes transferred (0 if unknown)
|
|
23
|
+
# started_at [String] ISO-8601 timestamp
|
|
24
|
+
# ended_at [String, nil] ISO-8601 timestamp once finished
|
|
25
|
+
# error [String, nil] Error message when status == 'failed'
|
|
26
|
+
class AsyncTransferStore
|
|
27
|
+
CATEGORY = 'async_transfer_'
|
|
28
|
+
private_constant :CATEGORY
|
|
29
|
+
|
|
30
|
+
# @param persistency [PersistencyFolder]
|
|
31
|
+
def initialize(persistency)
|
|
32
|
+
@persistency = persistency
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Persist (create or update) an async transfer entry.
|
|
36
|
+
# @param job_id [String] the ascli-generated UUID
|
|
37
|
+
# @param data [Hash] fields to store (will be JSON-serialised)
|
|
38
|
+
def write(job_id, data)
|
|
39
|
+
Aspera.assert_type(job_id, String){'job_id'}
|
|
40
|
+
Aspera.assert_type(data, Hash){'data'}
|
|
41
|
+
@persistency.put(store_key(job_id), JSON.generate(data))
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Read one entry.
|
|
46
|
+
# @param job_id [String]
|
|
47
|
+
# @return [Hash, nil] the stored data, or nil if not found
|
|
48
|
+
def read(job_id)
|
|
49
|
+
Aspera.assert_type(job_id, String){'job_id'}
|
|
50
|
+
raw = @persistency.get(store_key(job_id))
|
|
51
|
+
return if raw.nil?
|
|
52
|
+
JSON.parse(raw)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# List all async transfer entries.
|
|
56
|
+
# @return [Array<Hash>] each entry includes the +job_id+ field
|
|
57
|
+
def list
|
|
58
|
+
@persistency.current_items(CATEGORY).map do |key, raw|
|
|
59
|
+
data = JSON.parse(raw)
|
|
60
|
+
data['job_id'] ||= key.sub(CATEGORY, '')
|
|
61
|
+
data
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Delete one entry.
|
|
66
|
+
# @param job_id [String]
|
|
67
|
+
def delete(job_id)
|
|
68
|
+
Aspera.assert_type(job_id, String){'job_id'}
|
|
69
|
+
@persistency.delete(store_key(job_id))
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# Map a job_id to the PersistencyFolder key (includes category prefix).
|
|
76
|
+
def store_key(job_id)
|
|
77
|
+
"#{CATEGORY}#{job_id}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/cli/plugins/factory'
|
|
4
|
+
require 'aspera/cli/extended_value'
|
|
5
|
+
require 'aspera/cli/special_values'
|
|
6
|
+
require 'aspera/cli/info'
|
|
7
|
+
require 'aspera/cli/transfer_progress'
|
|
8
|
+
require 'aspera/cli/preset_manager'
|
|
9
|
+
require 'aspera/cli/http'
|
|
10
|
+
require 'aspera/ascp/installation'
|
|
11
|
+
require 'aspera/products/transferd'
|
|
12
|
+
require 'aspera/transfer/parameters'
|
|
13
|
+
require 'aspera/proxy_auto_config'
|
|
14
|
+
require 'aspera/environment'
|
|
15
|
+
require 'aspera/persistency_folder'
|
|
16
|
+
require 'aspera/rest'
|
|
17
|
+
require 'aspera/log'
|
|
18
|
+
require 'aspera/assert'
|
|
19
|
+
require 'aspera/oauth'
|
|
20
|
+
require 'aspera/ssl'
|
|
21
|
+
require 'aspera/schema/registry'
|
|
22
|
+
|
|
23
|
+
module Aspera
|
|
24
|
+
module Cli
|
|
25
|
+
# Performs one-time bootstrap of all shared services into Context.
|
|
26
|
+
# Called by Runner before instantiating Plugins::Config, so that Config
|
|
27
|
+
# acts only as a CLI plugin (option declaration + command handlers).
|
|
28
|
+
#
|
|
29
|
+
# Responsibilities:
|
|
30
|
+
# - resolve context.main_folder (option :home)
|
|
31
|
+
# - populate context.persistency, context.presets, context.http_config, context.progress_bar
|
|
32
|
+
# - register plugin lookup folders
|
|
33
|
+
# - register @preset / @vault extended-value handlers
|
|
34
|
+
# - configure global singletons: RestParameters, OAuth::Factory, SSL, Transfer::Parameters,
|
|
35
|
+
# RestErrorAnalyzer
|
|
36
|
+
# - set up the PAC proxy executor (option :fpac)
|
|
37
|
+
class Bootstrapper
|
|
38
|
+
# Folder name inside $HOME for all Aspera tool data (~/.aspera)
|
|
39
|
+
ASPERA_HOME_FOLDER_NAME = '.aspera'
|
|
40
|
+
# Default name for the YAML config file
|
|
41
|
+
DEFAULT_CONFIG_FILENAME = 'config.yaml'
|
|
42
|
+
# Sub-folder for user-installed plugins
|
|
43
|
+
ASPERA_PLUGINS_FOLDERNAME = 'plugins'
|
|
44
|
+
# Sub-folder for persistency data
|
|
45
|
+
PERSISTENCY_FOLDER = 'persist_store'
|
|
46
|
+
# Sub-folder for file lists used by transfers
|
|
47
|
+
FILE_LIST_FOLDER_NAME = 'filelists'
|
|
48
|
+
# Log file for REST call exceptions
|
|
49
|
+
REST_EXCEPTIONS_LOG_FILENAME = 'rest_exceptions.log'
|
|
50
|
+
# Extended-value prefix that resolves a named preset
|
|
51
|
+
EXTEND_PRESET = :preset
|
|
52
|
+
# Extended-value prefix that resolves a vault secret
|
|
53
|
+
EXTEND_VAULT = :vault
|
|
54
|
+
# Default preset name for the global config section
|
|
55
|
+
CONF_GLOBAL_SYM = :config
|
|
56
|
+
|
|
57
|
+
private_constant :ASPERA_HOME_FOLDER_NAME,
|
|
58
|
+
:DEFAULT_CONFIG_FILENAME,
|
|
59
|
+
:ASPERA_PLUGINS_FOLDERNAME,
|
|
60
|
+
:PERSISTENCY_FOLDER,
|
|
61
|
+
:FILE_LIST_FOLDER_NAME,
|
|
62
|
+
:REST_EXCEPTIONS_LOG_FILENAME,
|
|
63
|
+
:EXTEND_PRESET,
|
|
64
|
+
:EXTEND_VAULT,
|
|
65
|
+
:CONF_GLOBAL_SYM
|
|
66
|
+
|
|
67
|
+
# @param context [Context] the shared context to populate
|
|
68
|
+
def initialize(context)
|
|
69
|
+
Aspera.assert_type(context, Context){'context'}
|
|
70
|
+
@context = context
|
|
71
|
+
@pac_exec = nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Run the full bootstrap sequence.
|
|
75
|
+
# Must be called after context.options and context.formatter are set.
|
|
76
|
+
# On return, context.persistency, context.presets, context.http_config and
|
|
77
|
+
# context.progress_bar are populated and all global singletons are configured.
|
|
78
|
+
#
|
|
79
|
+
# @param gem_plugins_folder [String] folder of built-in plugins (from Plugins::Config)
|
|
80
|
+
# @param vault_value_cb [Proc] block(name): secret value (from VaultManager)
|
|
81
|
+
def run(gem_plugins_folder:, vault_value_cb:)
|
|
82
|
+
setup_main_folder
|
|
83
|
+
setup_persistency_and_plugin_folders(gem_plugins_folder)
|
|
84
|
+
setup_config_file
|
|
85
|
+
setup_extended_value_handlers(vault_value_cb)
|
|
86
|
+
setup_progress_bar
|
|
87
|
+
setup_pac_executor
|
|
88
|
+
setup_rest_and_transfer_runtime
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Public accessor used as option handler for :config_file
|
|
92
|
+
attr_accessor :config_file_option
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# Declare + parse :home option: sets context.main_folder
|
|
97
|
+
def setup_main_folder
|
|
98
|
+
@context.options.declare(
|
|
99
|
+
:home, description: 'Home folder for tool',
|
|
100
|
+
handler: {o: @context, m: :main_folder},
|
|
101
|
+
default: default_app_main_folder(app_name: Info::CMD_NAME)
|
|
102
|
+
)
|
|
103
|
+
@context.options.parse_options!
|
|
104
|
+
Log.log.debug{"#{Info::CMD_NAME} folder: #{@context.main_folder}"}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# context.persistency + plugin lookup folders
|
|
108
|
+
def setup_persistency_and_plugin_folders(gem_plugins_folder)
|
|
109
|
+
@context.persistency = PersistencyFolder.new(File.join(@context.main_folder, PERSISTENCY_FOLDER))
|
|
110
|
+
Plugins::Factory.instance.add_lookup_folder(gem_plugins_folder)
|
|
111
|
+
Plugins::Factory.instance.add_lookup_folder(File.join(@context.main_folder, ASPERA_PLUGINS_FOLDERNAME))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Declare + parse :config_file option: sets context.presets + context.http_config
|
|
115
|
+
def setup_config_file
|
|
116
|
+
@context.options.declare(
|
|
117
|
+
:config_file, description: 'Path to YAML file with preset configuration',
|
|
118
|
+
handler: {o: self, m: :config_file_option},
|
|
119
|
+
default: File.join(@context.main_folder, DEFAULT_CONFIG_FILENAME)
|
|
120
|
+
)
|
|
121
|
+
@context.options.parse_options!
|
|
122
|
+
@context.presets = PresetManager.new(config_file: @config_file_option)
|
|
123
|
+
@context.http_config = Http.new
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Register @preset and @vault extended-value handlers + global config default preset
|
|
127
|
+
def setup_extended_value_handlers(vault_value_cb)
|
|
128
|
+
@context.options.declare(:secret, description: 'Secret for access keys')
|
|
129
|
+
@context.options.declare(:vault, schema: Schema::Registry::VAULT_OPTIONS)
|
|
130
|
+
@context.options.declare(:vault_password, description: 'Vault password')
|
|
131
|
+
# Register @preset and @vault handlers BEFORE parse_options! so that
|
|
132
|
+
# values like --secret=@preset:name are correctly evaluated at parse time.
|
|
133
|
+
ExtendedValue.instance.on(EXTEND_PRESET){ |v| @context.presets.by_name(v)}
|
|
134
|
+
ExtendedValue.instance.on(EXTEND_VAULT, &vault_value_cb)
|
|
135
|
+
@context.options.parse_options!
|
|
136
|
+
# Load global config default preset (equivalent of add_plugin_default_preset(:config))
|
|
137
|
+
default_config_name = @context.presets.plugin_default_name(CONF_GLOBAL_SYM)
|
|
138
|
+
unless default_config_name.nil?
|
|
139
|
+
Log.log.debug{"add_plugin_default_preset:#{CONF_GLOBAL_SYM}:#{default_config_name}"}
|
|
140
|
+
@context.options.add_option_preset(@context.presets.by_name(default_config_name), 'default_plugin', override: false)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Declare + parse :progress_bar: sets context.progress_bar
|
|
145
|
+
def setup_progress_bar
|
|
146
|
+
@context.options.declare(:progress_bar, description: 'Display progress bar', allowed: Allowed::TYPES_BOOLEAN, default: Environment.terminal?)
|
|
147
|
+
@context.options.parse_options!
|
|
148
|
+
@context.progress_bar = TransferProgress.new if @context.options.get_option(:progress_bar)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Declare + parse :fpac / :proxy_credentials: sets up PAC executor
|
|
152
|
+
def setup_pac_executor
|
|
153
|
+
@context.options.declare(:fpac, description: 'Proxy auto configuration script')
|
|
154
|
+
@context.options.declare(:proxy_credentials, description: 'HTTP proxy credentials for fpac: user, password', allowed: [Array, NilClass])
|
|
155
|
+
@context.options.parse_options!
|
|
156
|
+
pac_script = @context.options.get_option(:fpac)
|
|
157
|
+
return unless pac_script
|
|
158
|
+
|
|
159
|
+
@context.pac_executor = ProxyAutoConfig.new(pac_script).register_uri_generic
|
|
160
|
+
proxy_user_pass = @context.options.get_option(:proxy_credentials)
|
|
161
|
+
if proxy_user_pass
|
|
162
|
+
Aspera.assert(proxy_user_pass.length.eql?(2), type: Cli::BadArgument){"proxy_credentials shall have two elements (#{proxy_user_pass.length})"}
|
|
163
|
+
@context.pac_executor.proxy_user = proxy_user_pass[0]
|
|
164
|
+
@context.pac_executor.proxy_pass = proxy_user_pass[1]
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Configure global singletons: RestParameters, SSL, Transfer, RestErrorAnalyzer.
|
|
169
|
+
# OAuth persist_mgr is NOT set here: it depends on :cache_tokens which is parsed later
|
|
170
|
+
# by Config#initialize. Runner sets it after Config.new.
|
|
171
|
+
def setup_rest_and_transfer_runtime
|
|
172
|
+
RestParameters.instance.user_agent = Info::CMD_NAME
|
|
173
|
+
RestParameters.instance.progress_bar = @context.progress_bar
|
|
174
|
+
RestParameters.instance.session_cb = ->(http_session){@context.http_config.update_session(http_session)}
|
|
175
|
+
RestParameters.instance.spinner_cb = ->(title = nil, action: :spin){@context.formatter.long_operation(title, action: action)}
|
|
176
|
+
OAuth::Web.additional_info = "#{Info::CMD_NAME} v#{Cli::VERSION}"
|
|
177
|
+
Transfer::Parameters.file_list_folder = File.join(@context.main_folder, FILE_LIST_FOLDER_NAME)
|
|
178
|
+
RestErrorAnalyzer.instance.log_file = File.join(@context.main_folder, REST_EXCEPTIONS_LOG_FILENAME)
|
|
179
|
+
RestErrorsAspera.register_handlers
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# @return [String] ~/.aspera
|
|
183
|
+
def module_family_folder
|
|
184
|
+
user_home_folder = Dir.home
|
|
185
|
+
Aspera.assert(Dir.exist?(user_home_folder), type: Cli::Error){"Home folder does not exist: #{user_home_folder}. Check your user environment."}
|
|
186
|
+
File.join(user_home_folder, ASPERA_HOME_FOLDER_NAME)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# @return [String] ~/.aspera/<app_name>
|
|
190
|
+
def default_app_main_folder(app_name:)
|
|
191
|
+
Aspera.assert_type(app_name, String)
|
|
192
|
+
Aspera.assert(!app_name.empty?, 'app_name must not be empty')
|
|
193
|
+
File.join(module_family_folder, app_name)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|