aspera-cli 4.27.5 → 4.28.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 +160 -4
- data/CONTRIBUTING.md +4 -2
- data/README.md +1 -1
- data/bin/asession +1 -1
- data/docs/README.md +1086 -331
- data/docs/test-mcp-with-ai.md +5 -3
- data/lib/aspera/agent/direct.rb +15 -4
- data/lib/aspera/agent/factory.rb +2 -1
- data/lib/aspera/agent/node.rb +1 -1
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/faspex.rb +1 -1
- data/lib/aspera/api/node.rb +6 -2
- data/lib/aspera/ascp/installation.rb +47 -11
- data/lib/aspera/ascp/management.rb +89 -43
- data/lib/aspera/cli/ascp_actions.rb +5 -19
- data/lib/aspera/cli/bootstrapper.rb +1 -2
- data/lib/aspera/cli/command_registry.rb +43 -3
- data/lib/aspera/cli/command_spec.rb +3 -0
- data/lib/aspera/cli/completion/ascli.bash +39 -5
- data/lib/aspera/cli/completion/ascli.fish +16 -8
- data/lib/aspera/cli/completion/ascli.ps1 +41 -0
- data/lib/aspera/cli/completion/ascli.zsh +20 -4
- data/lib/aspera/cli/completion_actions.rb +360 -0
- data/lib/aspera/cli/error.rb +42 -0
- data/lib/aspera/cli/extended_value.rb +30 -5
- data/lib/aspera/cli/formatter.rb +118 -56
- data/lib/aspera/cli/hints.rb +0 -1
- data/lib/aspera/cli/http.rb +6 -5
- data/lib/aspera/cli/info.rb +10 -4
- data/lib/aspera/cli/mailer.rb +1 -1
- data/lib/aspera/cli/mcp/http_servlet.rb +156 -0
- data/lib/aspera/cli/mcp/secrets.rb +143 -0
- data/lib/aspera/cli/mcp/tool.rb +231 -0
- data/lib/aspera/cli/option_value.rb +9 -1
- data/lib/aspera/cli/options.schema.yaml +390 -39
- data/lib/aspera/cli/parser.rb +14 -5
- data/lib/aspera/cli/plugins/aoc.rb +8 -11
- data/lib/aspera/cli/plugins/base.rb +36 -15
- data/lib/aspera/cli/plugins/config.rb +15 -44
- data/lib/aspera/cli/plugins/console.rb +3 -1
- data/lib/aspera/cli/plugins/factory.rb +1 -1
- data/lib/aspera/cli/plugins/faspex5.rb +9 -19
- data/lib/aspera/cli/plugins/faspio.rb +36 -5
- data/lib/aspera/cli/plugins/httpgw.rb +1 -0
- data/lib/aspera/cli/plugins/mcp.rb +177 -118
- data/lib/aspera/cli/plugins/node.rb +14 -8
- data/lib/aspera/cli/plugins/orchestrator.rb +351 -38
- data/lib/aspera/cli/plugins/server.rb +3 -1
- data/lib/aspera/cli/plugins/shares.rb +205 -35
- data/lib/aspera/cli/preset_actions.rb +15 -7
- data/lib/aspera/cli/preset_manager.rb +24 -2
- data/lib/aspera/cli/prompt.rb +3 -1
- data/lib/aspera/cli/result.rb +17 -4
- data/lib/aspera/cli/runner.rb +133 -43
- data/lib/aspera/cli/security.rb +134 -0
- data/lib/aspera/cli/table_fitter.rb +116 -0
- data/lib/aspera/cli/terminal_formatter.rb +23 -34
- data/lib/aspera/cli/transfer_agent.rb +4 -1
- data/lib/aspera/cli/vault_manager.rb +6 -5
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/web_secret.html.erb +61 -0
- data/lib/aspera/cli/web_secret.rb +220 -0
- data/lib/aspera/cli/wizard.rb +19 -9
- data/lib/aspera/command_line_builder.rb +26 -0
- data/lib/aspera/data_repository.rb +38 -24
- data/lib/aspera/duration.rb +42 -0
- data/lib/aspera/environment.rb +88 -24
- data/lib/aspera/exec_spec.rb +6 -5
- data/lib/aspera/faspex_gw.rb +98 -32
- data/lib/aspera/faspex_postproc.rb +187 -44
- data/lib/aspera/formatter_interface.rb +0 -1
- data/lib/aspera/id_generator.rb +2 -1
- data/lib/aspera/json_rpc/client.rb +51 -32
- data/lib/aspera/json_rpc/version.rb +1 -0
- data/lib/aspera/keychain/base.rb +5 -0
- data/lib/aspera/keychain/encrypted_hash.rb +6 -2
- data/lib/aspera/keychain/factory.rb +2 -3
- data/lib/aspera/keychain/hashicorp_vault.rb +7 -4
- data/lib/aspera/keychain/macos_security.rb +119 -13
- data/lib/aspera/keychain/one_password_api.rb +8 -3
- data/lib/aspera/keychain/one_password_base.rb +14 -9
- data/lib/aspera/keychain/one_password_cli.rb +4 -2
- data/lib/aspera/log.rb +3 -6
- data/lib/aspera/markdown.rb +152 -115
- data/lib/aspera/mime.rb +6 -0
- data/lib/aspera/nagios.rb +3 -1
- data/lib/aspera/node_emulator.rb +98 -101
- data/lib/aspera/oauth/{base.rb → client/base.rb} +5 -4
- data/lib/aspera/oauth/{boot.rb → client/boot.rb} +3 -2
- data/lib/aspera/oauth/{factory.rb → client/factory.rb} +7 -0
- data/lib/aspera/oauth/{generic.rb → client/generic.rb} +1 -1
- data/lib/aspera/oauth/{json_credentials.rb → client/json_credentials.rb} +1 -1
- data/lib/aspera/oauth/{jwt.rb → client/jwt.rb} +1 -1
- data/lib/aspera/oauth/{url_json.rb → client/url_json.rb} +1 -1
- data/lib/aspera/oauth/{web.rb → client/web.rb} +2 -2
- data/lib/aspera/oauth/server/authorize.html.erb +34 -0
- data/lib/aspera/oauth/server/pkce.rb +426 -0
- data/lib/aspera/orchestrator_workflow_checker.rb +561 -0
- data/lib/aspera/preview/terminal.rb +3 -2
- data/lib/aspera/rainbow.rb +27 -1
- data/lib/aspera/rest/aspera_errors.rb +51 -59
- data/lib/aspera/rest/client.rb +21 -14
- data/lib/aspera/rest/error_analyzer.rb +9 -0
- data/lib/aspera/rest/list.rb +6 -6
- data/lib/aspera/rest/util.rb +7 -1
- data/lib/aspera/rest.rb +0 -1
- data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +0 -2
- data/lib/aspera/schema/IBM Aspera Orchestrator API-v1-enhanced.yaml +4001 -0
- data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0-enhanced.yaml +1097 -0
- data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +35 -35
- data/lib/aspera/schema/IBM_Aspera_Shares.yaml +819 -7
- data/lib/aspera/schema/documentation.rb +50 -78
- data/lib/aspera/schema/reader.rb +39 -11
- data/lib/aspera/schema/registry.rb +8 -2
- data/lib/aspera/secret_hider.rb +94 -15
- data/lib/aspera/secret_strength.rb +114 -0
- data/lib/aspera/servlet_helpers.rb +111 -0
- data/lib/aspera/sync/args.schema.yaml +3 -1
- data/lib/aspera/sync/conf.schema.yaml +16 -6
- data/lib/aspera/sync/operations.rb +21 -1
- data/lib/aspera/temp_file_manager.rb +79 -20
- data/lib/aspera/transfer/parameters.rb +5 -0
- data/lib/aspera/transfer/spec.rb +52 -12
- data/lib/aspera/transfer/spec.schema.yaml +8 -5
- data/lib/aspera/web_auth/index.html.erb +16 -0
- data/lib/aspera/web_auth/server.rb +18 -10
- data/lib/aspera/web_page.html.erb +262 -0
- data/lib/aspera/web_page.rb +61 -0
- data/lib/aspera/web_server_simple.rb +76 -10
- data.tar.gz.sig +0 -0
- metadata +47 -32
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/mcp_tool.rb +0 -189
- data/lib/aspera/oauth.rb +0 -9
- data/lib/aspera/schema/IBM Aspera Orchestrator API-v1.yaml +0 -1784
- data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +0 -249
- data/lib/aspera/web_auth/index.html +0 -147
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'webrick'
|
|
7
|
+
require 'aspera/mime'
|
|
8
|
+
require 'aspera/web_server_simple'
|
|
9
|
+
|
|
10
|
+
module Aspera
|
|
11
|
+
# Mixin for WEBrick servlets of local web servers: JSON responses, and check of requests that a web page could send.
|
|
12
|
+
#
|
|
13
|
+
# A web page open in the browser of the user can send requests to a server that the browser reaches, also on localhost:
|
|
14
|
+
# - Cross-site request forgery: a form, or a `fetch` with a CORS-safelisted `Content-Type` (`text/plain`, form), is sent cross-origin without preflight.
|
|
15
|
+
# - DNS rebinding: a host name of the attacker that resolves to `127.0.0.1` makes the page same-origin with the server, so it also reads responses.
|
|
16
|
+
module ServletHelpers
|
|
17
|
+
class << self
|
|
18
|
+
# @param url [String] URL
|
|
19
|
+
# @return [String] Origin of the URL, as in header `Origin`, e.g. `https://example.com:8443`
|
|
20
|
+
def origin(url)
|
|
21
|
+
uri = URI.parse(url)
|
|
22
|
+
port = ":#{uri.port}" unless uri.port.eql?(uri.default_port)
|
|
23
|
+
"#{uri.scheme}://#{uri.host}#{port}".downcase
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param host [String] Value of header `Host`, e.g. `[::1]:8080`
|
|
27
|
+
# @return [String] Host name, without port and IPv6 brackets
|
|
28
|
+
def host_name(host)
|
|
29
|
+
return host[/\A\[([^\]]*)\]/, 1].to_s if host.start_with?('[')
|
|
30
|
+
host.split(':').first.to_s
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Scheme is not compared: header `Host` has none
|
|
34
|
+
# @param origin [String] Value of header `Origin`, e.g. `http://localhost:8080`
|
|
35
|
+
# @param host [String] Value of header `Host`, e.g. `localhost:8080`
|
|
36
|
+
# @return [Boolean] `true` if the origin is the one of the requested server
|
|
37
|
+
def same_origin?(origin, host)
|
|
38
|
+
default_port = origin.start_with?('https://') ? ':443' : ':80'
|
|
39
|
+
origin.sub(%r{\Ahttps?://}, '').delete_suffix(default_port).eql?(host.delete_suffix(default_port))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Set a JSON response
|
|
44
|
+
# @param response [WEBrick::HTTPResponse]
|
|
45
|
+
# @param status [Integer] HTTP status code
|
|
46
|
+
# @param body [Hash, Array] Body of the response
|
|
47
|
+
# @param headers [Hash{String => String}] Other headers of the response, e.g. `Cache-Control`
|
|
48
|
+
# @return [nil]
|
|
49
|
+
def json_response(response, status, body, headers: {})
|
|
50
|
+
response.status = status
|
|
51
|
+
response['Content-Type'] = Mime::JSON
|
|
52
|
+
headers.each { |name, value| response[name] = value }
|
|
53
|
+
response.body = JSON.generate(body)
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Set a JSON error response, in the format of OAuth 2.0 (RFC 6749 §5.2)
|
|
58
|
+
# @param response [WEBrick::HTTPResponse]
|
|
59
|
+
# @param status [Integer] HTTP status code
|
|
60
|
+
# @param error [String] Error code or message
|
|
61
|
+
# @param description [String, nil] Details
|
|
62
|
+
# @param headers [Hash{String => String}] Other headers of the response, e.g. `WWW-Authenticate`
|
|
63
|
+
# @return [nil]
|
|
64
|
+
def json_error(response, status, error, description = nil, headers: {})
|
|
65
|
+
body = {error: error}
|
|
66
|
+
body[:error_description] = description unless description.nil?
|
|
67
|
+
json_response(response, status, body, headers: headers)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @param request [WEBrick::HTTPRequest]
|
|
71
|
+
# @param scheme [String] Authentication scheme, e.g. `Basic` or `Bearer`
|
|
72
|
+
# @return [String, nil] Credentials of header `Authorization` if it has this scheme, else `nil`
|
|
73
|
+
def authorization(request, scheme)
|
|
74
|
+
request_scheme, credentials = request['Authorization'].to_s.split(' ', 2)
|
|
75
|
+
credentials.to_s if request_scheme.to_s.casecmp?(scheme)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Check the credentials of HTTP Basic authentication, in constant time
|
|
79
|
+
# @param request [WEBrick::HTTPRequest]
|
|
80
|
+
# @param expected [String] Expected `username:password`
|
|
81
|
+
# @return [Boolean] `true` if the request has the expected credentials
|
|
82
|
+
def basic_authorized?(request, expected)
|
|
83
|
+
OpenSSL.secure_compare(authorization(request, 'Basic').to_s.unpack1('m').to_s, expected)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Refuse a request that a web page in a browser could send. Clients that are not browsers send no `Origin`, and the `Host` of the URL they call.
|
|
87
|
+
# - `Host`: when the server listens on loopback, a loopback name or address, or one of `allowed_hosts` (DNS rebinding)
|
|
88
|
+
# - `Origin`: when present, the origin of the server, or one of `allowed_origins` (cross-site request)
|
|
89
|
+
# - `Content-Type`: with `json`, a JSON type, which a browser sends cross-origin only after a preflight request `OPTIONS`, not answered (cross-site request)
|
|
90
|
+
# @param request [WEBrick::HTTPRequest]
|
|
91
|
+
# @param json [Boolean] `true` if the body, if any, is parsed as JSON
|
|
92
|
+
# @param allowed_hosts [Array<String>] Other values accepted in `Host`: host name, or `host:port`
|
|
93
|
+
# @param allowed_origins [Array<String>] Other values accepted in `Origin`, e.g. `https://example.com`
|
|
94
|
+
# @raise [WEBrick::HTTPStatus::Forbidden] if `Host` or `Origin` is refused
|
|
95
|
+
# @raise [WEBrick::HTTPStatus::UnsupportedMediaType] if the body is not JSON
|
|
96
|
+
def check_request(request, json: false, allowed_hosts: [], allowed_origins: [])
|
|
97
|
+
host = request['Host']&.downcase
|
|
98
|
+
if host && WebServerSimple.loopback?(@server[:BindAddress].to_s)
|
|
99
|
+
name = ServletHelpers.host_name(host)
|
|
100
|
+
host_allowed = WebServerSimple.loopback?(name) || allowed_hosts.any? { |allowed_host| [name, host].include?(allowed_host.downcase) }
|
|
101
|
+
raise WEBrick::HTTPStatus::Forbidden, "Host is not a loopback address: #{host}" unless host_allowed
|
|
102
|
+
end
|
|
103
|
+
origin = request['Origin']&.downcase
|
|
104
|
+
origin_allowed = origin.nil? || (host && ServletHelpers.same_origin?(origin, host)) || allowed_origins.any? { |allowed_origin| allowed_origin.downcase.eql?(origin) }
|
|
105
|
+
raise WEBrick::HTTPStatus::Forbidden, "Origin is not the one of the server: #{origin}" unless origin_allowed
|
|
106
|
+
return unless json && !request.body.to_s.empty?
|
|
107
|
+
media_type = request['Content-Type'].to_s.split(';').first.to_s.strip.downcase
|
|
108
|
+
raise WEBrick::HTTPStatus::UnsupportedMediaType, "Content-Type shall be #{Mime::JSON}: #{media_type}" unless Mime.json?(media_type)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -6,6 +6,7 @@ $comment: >-
|
|
|
6
6
|
It is modified with special `x-` fields.
|
|
7
7
|
x-ts-name [Boolean,String] `true` if same name in transfer spec, else real name in transfer spec
|
|
8
8
|
x-ts-convert [String] Name of methods to convert value from transfer spec to `conf`
|
|
9
|
+
x-unit [String] Time unit of the integer value (e.g. `ms`), the command line option is a DURATION
|
|
9
10
|
`x-` fields are also documented in `command_line_builder.rb`
|
|
10
11
|
type: object
|
|
11
12
|
required:
|
|
@@ -163,14 +164,14 @@ properties:
|
|
|
163
164
|
x-cli-switch: true
|
|
164
165
|
ignore_mode:
|
|
165
166
|
description:
|
|
166
|
-
Source files that have had their mode changed after the initial
|
|
167
|
+
Source files that have had their mode changed after the initial
|
|
167
168
|
transfer will not update the destination file mode.
|
|
168
169
|
type: boolean
|
|
169
170
|
default: false
|
|
170
171
|
x-cli-option: true
|
|
171
172
|
x-cli-switch: true
|
|
172
173
|
ignore_remote_host_sync_name:
|
|
173
|
-
description: Do not check that the remote host being used for the current
|
|
174
|
+
description: Do not check that the remote host being used for the current
|
|
174
175
|
transfer matches the host used when the local database was created
|
|
175
176
|
type: boolean
|
|
176
177
|
default: false
|
|
@@ -260,10 +261,14 @@ properties:
|
|
|
260
261
|
local_scan_interval_milliseconds:
|
|
261
262
|
description:
|
|
262
263
|
Enable periodic scans on the local host during a continuous sync.
|
|
263
|
-
`-1` for disabled
|
|
264
|
+
`-1` for disabled.
|
|
265
|
+
On the command line, the value is a duration, e.g. `1h 30m`, `2s500ms`, `5:30`, `1d 02:00:00.250`.
|
|
266
|
+
A plain number is in seconds.
|
|
267
|
+
Stored in milliseconds.
|
|
264
268
|
type: integer
|
|
265
269
|
default: -1
|
|
266
270
|
x-cli-option: --scan-interval
|
|
271
|
+
x-unit: ms
|
|
267
272
|
local_scan_threads:
|
|
268
273
|
description: Number of directory scanning threads on the local host.
|
|
269
274
|
Value must be between 1 and 99
|
|
@@ -506,8 +511,8 @@ properties:
|
|
|
506
511
|
description: Authenticate to the proxy with the specified password.
|
|
507
512
|
type: string
|
|
508
513
|
port:
|
|
509
|
-
description: Use the specified port
|
|
510
|
-
for dnats
|
|
514
|
+
description: Use the specified port.
|
|
515
|
+
Default is 9091 for dnat, 9092 for dnats.
|
|
511
516
|
type: integer
|
|
512
517
|
protocol:
|
|
513
518
|
description: The protocol to be used.
|
|
@@ -602,10 +607,14 @@ properties:
|
|
|
602
607
|
remote_scan_interval_milliseconds:
|
|
603
608
|
description: Enable periodic scans on the remote host.
|
|
604
609
|
`-1` for disabled.
|
|
610
|
+
On the command line, the value is a duration, e.g. `1h 30m`, `2s500ms`, `5:30`, `1d 02:00:00.250`.
|
|
611
|
+
A plain number is in seconds.
|
|
612
|
+
Stored in milliseconds.
|
|
605
613
|
type: integer
|
|
606
614
|
default: -1
|
|
607
615
|
x-cli-option: --remote-scan-interval
|
|
608
616
|
x-cli-special: true
|
|
617
|
+
x-unit: ms
|
|
609
618
|
remote_scan_threads:
|
|
610
619
|
description: Number of directory scanning threads on the remote host.
|
|
611
620
|
Value must be between 1 and 99.
|
|
@@ -665,7 +674,7 @@ properties:
|
|
|
665
674
|
x-cli-option: true
|
|
666
675
|
x-cli-switch: true
|
|
667
676
|
scan_dir_rename:
|
|
668
|
-
description: Enable the detection of renamed directories and files compared
|
|
677
|
+
description: Enable the detection of renamed directories and files compared
|
|
669
678
|
to the previous scan, based on matching inodes
|
|
670
679
|
type: boolean
|
|
671
680
|
default: false
|
|
@@ -806,6 +815,7 @@ properties:
|
|
|
806
815
|
x-cli-short: -X
|
|
807
816
|
target_rate:
|
|
808
817
|
description: Transfer no faster than the specified rate (in bps).
|
|
818
|
+
A string with suffix `k`, `m` or `g` is converted to bps by `ascli` (e.g. `100m`).
|
|
809
819
|
type: integer
|
|
810
820
|
default: 10000000
|
|
811
821
|
x-ts-name: target_rate_kbps
|
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
require 'aspera/ascp/installation'
|
|
6
6
|
require 'aspera/agent/direct'
|
|
7
|
+
require 'aspera/transfer/spec'
|
|
7
8
|
require 'aspera/exec_spec'
|
|
8
9
|
require 'aspera/command_line_converter'
|
|
9
10
|
require 'aspera/command_line_builder'
|
|
10
11
|
require 'aspera/log'
|
|
11
12
|
require 'aspera/assert'
|
|
13
|
+
require 'aspera/duration'
|
|
12
14
|
require 'aspera/environment'
|
|
13
15
|
require 'aspera/schema/registry'
|
|
14
16
|
require 'json'
|
|
@@ -98,11 +100,13 @@ module Aspera
|
|
|
98
100
|
remote = sync_info['remote']
|
|
99
101
|
Aspera.assert_type(remote, Hash) { 'remote' }
|
|
100
102
|
Aspera.assert_type(remote['path'], String) { 'remote path' }
|
|
103
|
+
normalize_target_rate(sync_info['transport'])
|
|
101
104
|
# get transfer spec if possible, and feed back to new structure
|
|
102
105
|
if block_given?
|
|
103
106
|
transfer_spec = yield(direction_sym(sync_info), sync_info['local']['path'], remote['path'])
|
|
104
107
|
Log.dump(:auth_ts, transfer_spec)
|
|
105
108
|
transfer_spec.deep_merge!(opt_ts) unless opt_ts.nil?
|
|
109
|
+
Transfer::Spec.resolve_target_rate(transfer_spec)
|
|
106
110
|
tspec_to_sync_info(transfer_spec, sync_info, CONF_SCHEMA)
|
|
107
111
|
update_remote_dir(remote, 'path', transfer_spec)
|
|
108
112
|
end
|
|
@@ -123,6 +127,7 @@ module Aspera
|
|
|
123
127
|
sync_info.keys.push('instance').uniq.sort.eql?(CMDLINE_PARAMS_KEYS)
|
|
124
128
|
Aspera.assert_type(sync_info['sessions'], Array)
|
|
125
129
|
Aspera.assert_type(sync_info['sessions'].first, Hash)
|
|
130
|
+
sync_info['sessions'].each { |session| normalize_target_rate(session) }
|
|
126
131
|
if block_given?
|
|
127
132
|
sync_info['sessions'].each do |session|
|
|
128
133
|
Aspera.assert_type(session['local_dir'], String) { 'local_dir' }
|
|
@@ -130,6 +135,7 @@ module Aspera
|
|
|
130
135
|
transfer_spec = yield(direction_sym(session), session['local_dir'], session['remote_dir'])
|
|
131
136
|
Log.dump(:auth_ts, transfer_spec)
|
|
132
137
|
transfer_spec.deep_merge!(opt_ts) unless opt_ts.nil?
|
|
138
|
+
Transfer::Spec.resolve_target_rate(transfer_spec)
|
|
133
139
|
tspec_to_sync_info(transfer_spec, session, ARGS_SESSION_SCHEMA)
|
|
134
140
|
session['private_key_paths'] = Ascp::Installation.instance.aspera_token_ssh_key_paths(:rsa) if transfer_spec.key?('token')
|
|
135
141
|
update_remote_dir(session, 'remote_dir', transfer_spec)
|
|
@@ -257,6 +263,12 @@ module Aspera
|
|
|
257
263
|
|
|
258
264
|
# private
|
|
259
265
|
|
|
266
|
+
# Convert `target_rate` given as a string with optional suffix (e.g. `100m`) into integer bps
|
|
267
|
+
# @param params [Hash, nil] `transport` (`conf` format) or session (`args` format), modified in place
|
|
268
|
+
def normalize_target_rate(params)
|
|
269
|
+
params['target_rate'] = Transfer::Spec.rate_string_to_bps(params['target_rate']) if params.is_a?(Hash) && params['target_rate'].is_a?(String)
|
|
270
|
+
end
|
|
271
|
+
|
|
260
272
|
# Transfer specification to synchronization information
|
|
261
273
|
# tag `x-ts-name` in schema is used to map transfer spec parameters to async `sync_info`
|
|
262
274
|
# @param transfer_spec [Hash] transfer specification
|
|
@@ -330,7 +342,15 @@ module Aspera
|
|
|
330
342
|
current[key] ||= {}
|
|
331
343
|
current = current[key]
|
|
332
344
|
end
|
|
333
|
-
current[last_key] =
|
|
345
|
+
current[last_key] =
|
|
346
|
+
if props['x-cli-switch']
|
|
347
|
+
# a special switch sets the non-default enum value, e.g. `--continuous` sets `mode` to `continuous`
|
|
348
|
+
props.key?('enum') ? (props['enum'] - [props['default']]).first : true
|
|
349
|
+
elsif props.key?('x-unit')
|
|
350
|
+
Duration.to_unit(args.shift, props['x-unit'])
|
|
351
|
+
else
|
|
352
|
+
args.shift
|
|
353
|
+
end
|
|
334
354
|
end
|
|
335
355
|
return result
|
|
336
356
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'aspera/assert'
|
|
4
|
+
require 'aspera/log'
|
|
3
5
|
require 'singleton'
|
|
4
6
|
require 'fileutils'
|
|
5
7
|
require 'etc'
|
|
@@ -15,21 +17,34 @@ module Aspera
|
|
|
15
17
|
class TempFileManager
|
|
16
18
|
include Singleton
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
private_constant :SEC_IN_DAY, :FILE_LIST_AGE_MAX_SEC
|
|
20
|
+
# Assume no transfer lasts longer than this
|
|
21
|
+
# (garbage collect file lists which were not deleted after transfer)
|
|
22
|
+
EXPIRY_DEFAULT_SEC = 5 * 86_400
|
|
23
|
+
private_constant :EXPIRY_DEFAULT_SEC
|
|
23
24
|
|
|
25
|
+
# @return [Boolean] `true`: files created by this execution are deleted by `cleanup`
|
|
24
26
|
attr_accessor :cleanup_on_exit
|
|
27
|
+
# @return [Numeric, nil] Age in seconds of files deleted by `cleanup_expired`, `nil`: never deleted
|
|
28
|
+
attr_reader :expiry_sec
|
|
25
29
|
attr_reader :global_temp
|
|
26
30
|
|
|
27
31
|
def initialize
|
|
28
32
|
@created_files = []
|
|
33
|
+
# Number of holders of a created file: not deleted by cleanup while held
|
|
34
|
+
@hold_counts = {}
|
|
35
|
+
# Files are created and released by transfer threads
|
|
36
|
+
@mutex = Mutex.new
|
|
29
37
|
@cleanup_on_exit = true
|
|
38
|
+
@expiry_sec = EXPIRY_DEFAULT_SEC
|
|
30
39
|
@global_temp = Etc.systmpdir
|
|
31
40
|
end
|
|
32
41
|
|
|
42
|
+
# @param value [Numeric, nil] Age in seconds, `0`: all files, `nil`: never delete
|
|
43
|
+
def expiry_sec=(value)
|
|
44
|
+
Aspera.assert(value.nil? || (value.is_a?(Numeric) && !value.negative?), type: ParameterError) { "Expiry must be a positive duration: #{value}" }
|
|
45
|
+
@expiry_sec = value
|
|
46
|
+
end
|
|
47
|
+
|
|
33
48
|
def global_temp=(value)
|
|
34
49
|
@global_temp = case value
|
|
35
50
|
when '@env' then Dir.tmpdir
|
|
@@ -39,17 +54,45 @@ module Aspera
|
|
|
39
54
|
end
|
|
40
55
|
|
|
41
56
|
def delete_file(filepath)
|
|
42
|
-
|
|
43
|
-
rescue => e
|
|
44
|
-
Log.log.warn { "Problem deleting file: #{filepath}: #{e.message}" }
|
|
57
|
+
remove_file(filepath) if @cleanup_on_exit
|
|
45
58
|
end
|
|
46
59
|
|
|
47
|
-
# Call this on process exit
|
|
60
|
+
# Call this on process exit, or at the end of a command.
|
|
61
|
+
# Held files are kept: they are deleted when released.
|
|
48
62
|
def cleanup
|
|
49
|
-
@
|
|
50
|
-
|
|
63
|
+
@mutex.synchronize do
|
|
64
|
+
@created_files.each do |filepath|
|
|
65
|
+
delete_file(filepath) if !@hold_counts.key?(filepath) && File.file?(filepath)
|
|
66
|
+
end
|
|
67
|
+
@created_files.select! { |filepath| @hold_counts.key?(filepath) }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Keep created files after `cleanup`, until released, for example a file list used by a transfer
|
|
72
|
+
# that ends after the command (asynchronous transfer).
|
|
73
|
+
# A file can be held several times (multi-session): it is deleted when the last holder releases it.
|
|
74
|
+
# @param paths [Array<String>] Files created by this manager (others are ignored)
|
|
75
|
+
def hold(paths)
|
|
76
|
+
@mutex.synchronize do
|
|
77
|
+
paths.each do |filepath|
|
|
78
|
+
@hold_counts[filepath] = @hold_counts.fetch(filepath, 0) + 1 if @created_files.include?(filepath)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Release files held with `hold`, and delete those that are no longer held.
|
|
84
|
+
# @param paths [Array<String>] Files given to `hold`
|
|
85
|
+
def release(paths)
|
|
86
|
+
@mutex.synchronize do
|
|
87
|
+
paths.each do |filepath|
|
|
88
|
+
next unless @hold_counts.key?(filepath)
|
|
89
|
+
@hold_counts[filepath] -= 1
|
|
90
|
+
next if @hold_counts[filepath].positive?
|
|
91
|
+
@hold_counts.delete(filepath)
|
|
92
|
+
@created_files.delete(filepath)
|
|
93
|
+
delete_file(filepath) if File.file?(filepath)
|
|
94
|
+
end
|
|
51
95
|
end
|
|
52
|
-
@created_files = []
|
|
53
96
|
end
|
|
54
97
|
|
|
55
98
|
# Ensure that provided folder exists, or create it, atomically create and reserve a unique file.
|
|
@@ -63,7 +106,7 @@ module Aspera
|
|
|
63
106
|
new_file = Dir::Tmpname.create([basename, suffix ? "-#{suffix}" : ''], temp_folder) do |path|
|
|
64
107
|
File.open(path, File::WRONLY | File::CREAT | File::EXCL, 0o600) { nil }
|
|
65
108
|
end
|
|
66
|
-
@created_files.push(new_file)
|
|
109
|
+
@mutex.synchronize { @created_files.push(new_file) }
|
|
67
110
|
new_file
|
|
68
111
|
end
|
|
69
112
|
|
|
@@ -93,17 +136,33 @@ module Aspera
|
|
|
93
136
|
File.join(@global_temp, dir_name)
|
|
94
137
|
end
|
|
95
138
|
|
|
96
|
-
# Garbage collect
|
|
139
|
+
# Garbage collect files not deleted by previous executions (e.g. killed), older than `expiry_sec`.
|
|
140
|
+
# Not disabled by `cleanup_on_exit`, which applies to files of this execution.
|
|
141
|
+
# @param temp_folder [String] Folder that contains only temporary files
|
|
97
142
|
def cleanup_expired(temp_folder)
|
|
98
|
-
|
|
143
|
+
return if @expiry_sec.nil?
|
|
144
|
+
now = Time.now
|
|
145
|
+
Dir.children(temp_folder).each do |name|
|
|
99
146
|
file_path = File.join(temp_folder, name)
|
|
100
|
-
age_sec = (Time.now - File.stat(file_path).mtime).to_i
|
|
101
147
|
# check age of file, delete too old
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
148
|
+
next unless File.file?(file_path) && (now - File.mtime(file_path)) >= @expiry_sec
|
|
149
|
+
Log.log.debug { "garbage collecting #{name}" }
|
|
150
|
+
remove_file(file_path)
|
|
151
|
+
rescue Errno::ENOENT
|
|
152
|
+
# deleted meanwhile by another execution
|
|
153
|
+
nil
|
|
106
154
|
end
|
|
107
155
|
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
# Delete a file, a failure is logged
|
|
160
|
+
def remove_file(filepath)
|
|
161
|
+
File.delete(filepath)
|
|
162
|
+
rescue Errno::ENOENT
|
|
163
|
+
nil
|
|
164
|
+
rescue => e
|
|
165
|
+
Log.log.warn { "Problem deleting file: #{filepath}: #{e.message}" }
|
|
166
|
+
end
|
|
108
167
|
end
|
|
109
168
|
end
|
|
@@ -80,6 +80,8 @@ module Aspera
|
|
|
80
80
|
Aspera.assert_values(@client_ssh_key, Ascp::Installation::CLIENT_SSH_KEY_OPTIONS)
|
|
81
81
|
@check_ignore_cb = check_ignore_cb
|
|
82
82
|
@builder = CommandLineBuilder.new(@job_spec, Spec::SCHEMA, CommandLineConverter)
|
|
83
|
+
# temporary files used by `ascp`: given to the agent in the ExecSpec
|
|
84
|
+
@temp_files = []
|
|
83
85
|
end
|
|
84
86
|
|
|
85
87
|
# Place source files on command line or in a temp file list, controlled by @file_list and file_list_folder
|
|
@@ -112,6 +114,7 @@ module Aspera
|
|
|
112
114
|
lines = ts_paths_array.map { |i| i['source'] }
|
|
113
115
|
end
|
|
114
116
|
file_list_file = TempFileManager.instance.new_file_path_in_folder(self.class.file_list_folder)
|
|
117
|
+
@temp_files.push(file_list_file)
|
|
115
118
|
Log.dump(:file_list, lines)
|
|
116
119
|
File.write(file_list_file, lines.join("\n"), encoding: 'UTF-8')
|
|
117
120
|
Log.log.debug { "#{file_list_option}=\n#{File.read(file_list_file)}".red }
|
|
@@ -137,6 +140,7 @@ module Aspera
|
|
|
137
140
|
# ignore cert for wss ?
|
|
138
141
|
if @check_ignore_cb&.call(@job_spec['remote_host'], @job_spec['wss_port'])
|
|
139
142
|
wss_cert_file = TempFileManager.instance.new_file_path_global('wss_cert')
|
|
143
|
+
@temp_files.push(wss_cert_file)
|
|
140
144
|
wss_url = "https://#{@job_spec['remote_host']}:#{@job_spec['wss_port']}"
|
|
141
145
|
File.write(wss_cert_file, Rest.remote_certificate_chain(wss_url))
|
|
142
146
|
# place in front, as more priority
|
|
@@ -213,6 +217,7 @@ module Aspera
|
|
|
213
217
|
end
|
|
214
218
|
# disable redis in client, only for ascp, this makes ascp4 fail
|
|
215
219
|
exec_spec.env['ASPERA_TEST_REDIS_DISABLE'] = 'true' if exec_spec.exec.eql?(:ascp)
|
|
220
|
+
exec_spec.temp_files.concat(@temp_files)
|
|
216
221
|
Log.log.debug { "ascp args: #{exec_spec}" }
|
|
217
222
|
return exec_spec
|
|
218
223
|
end
|
data/lib/aspera/transfer/spec.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'aspera/agent/factory'
|
|
3
4
|
require 'aspera/command_line_builder'
|
|
4
5
|
require 'aspera/assert'
|
|
5
6
|
|
|
@@ -27,6 +28,8 @@ module Aspera
|
|
|
27
28
|
TAG_RESERVED = 'aspera'
|
|
28
29
|
# Specific fields to not display
|
|
29
30
|
SPECIFIC = %w{token paths direction source_root destination_root}.freeze
|
|
31
|
+
# Value of agent for documentation with one column per agent
|
|
32
|
+
DOC_OVERVIEW = :overview
|
|
30
33
|
class << self
|
|
31
34
|
# wrong def in transferd
|
|
32
35
|
POLICY_FIX = {
|
|
@@ -35,13 +38,13 @@ module Aspera
|
|
|
35
38
|
'sparse_csum' => 'sparse_checksum',
|
|
36
39
|
'full_csum' => 'full_checksum'
|
|
37
40
|
}
|
|
38
|
-
# Multipliers for
|
|
39
|
-
|
|
40
|
-
'k' =>
|
|
41
|
-
'm' =>
|
|
42
|
-
'g' =>
|
|
41
|
+
# Multipliers for rate suffix (result is in bps)
|
|
42
|
+
RATE_SUFFIX_BPS = {
|
|
43
|
+
'k' => 1_000,
|
|
44
|
+
'm' => 1_000_000,
|
|
45
|
+
'g' => 1_000_000_000
|
|
43
46
|
}.freeze
|
|
44
|
-
private_constant :POLICY_FIX, :
|
|
47
|
+
private_constant :POLICY_FIX, :RATE_SUFFIX_BPS
|
|
45
48
|
# translate upload/download to send/receive
|
|
46
49
|
def transfer_type_to_direction(transfer_type)
|
|
47
50
|
XFER_TYPE_TO_DIR.fetch(transfer_type)
|
|
@@ -52,21 +55,58 @@ module Aspera
|
|
|
52
55
|
XFER_DIR_TO_TYPE.fetch(direction)
|
|
53
56
|
end
|
|
54
57
|
|
|
55
|
-
# Parse a human-readable rate
|
|
56
|
-
# Accepted: plain integer (
|
|
57
|
-
# @param value [String] e.g. "100m", "500000", "1g"
|
|
58
|
-
# @return [Integer] value in
|
|
59
|
-
def
|
|
58
|
+
# Parse a human-readable rate (as accepted by async -l) into an integer bps value.
|
|
59
|
+
# Accepted: plain integer (bps), or integer + suffix k/K (x1000), m/M (x1000000), g/G (x1000000000).
|
|
60
|
+
# @param value [String, Integer] e.g. "100m", "500000", "1g", 500000
|
|
61
|
+
# @return [Integer] value in bps
|
|
62
|
+
def rate_string_to_bps(value)
|
|
60
63
|
m = value.to_s.strip.match(/\A(\d+)([kKmMgG])?\z/)
|
|
61
64
|
Aspera.assert(m) { "Invalid rate value: #{value.inspect}. Expected integer with optional suffix k/K, m/M or g/G." }
|
|
62
|
-
multiplier = m[2] ?
|
|
65
|
+
multiplier = m[2] ? RATE_SUFFIX_BPS.fetch(m[2].downcase) : 1
|
|
63
66
|
return m[1].to_i * multiplier
|
|
64
67
|
end
|
|
65
68
|
|
|
69
|
+
# Resolve pseudo-parameter `target_rate` (bps) into `target_rate_kbps` (rounded down), overriding it if both are present.
|
|
70
|
+
# @param transfer_spec [Hash] Transfer spec, modified in place
|
|
71
|
+
def resolve_target_rate(transfer_spec)
|
|
72
|
+
return unless transfer_spec.key?('target_rate')
|
|
73
|
+
rate = transfer_spec.delete('target_rate')
|
|
74
|
+
transfer_spec['target_rate_kbps'] = rate_string_to_bps(rate) / 1000 unless rate.nil?
|
|
75
|
+
end
|
|
76
|
+
|
|
66
77
|
def fix_transferd_resume_policy(transfer_spec)
|
|
67
78
|
# Fix discrepancy in transfer spec
|
|
68
79
|
transfer_spec['resume_policy'] = POLICY_FIX[transfer_spec['resume_policy']] if transfer_spec.key?('resume_policy')
|
|
69
80
|
end
|
|
81
|
+
|
|
82
|
+
# Options of `Schema::Documentation` for the transfer spec: supported agents and `ascp` options
|
|
83
|
+
# @param agent [Symbol, nil] `nil`: all parameters, name of agent: parameters it supports, `DOC_OVERVIEW`: one column per agent
|
|
84
|
+
# @return [Hash] Keyword arguments of `Schema::Documentation.new`
|
|
85
|
+
def doc_options(agent = nil)
|
|
86
|
+
return {annotate: ->(node, formatter) { doc_lines(node, formatter, agent: agent) }} unless agent.eql?(DOC_OVERVIEW)
|
|
87
|
+
{
|
|
88
|
+
annotate: ->(node, formatter) { doc_lines(node, formatter, agents_line: false) },
|
|
89
|
+
extra_columns: Agent::Factory::ALL.sort_by { |_, names| names[:short] }.to_h do |sym, names|
|
|
90
|
+
[names[:short].to_s, ->(node, formatter) { formatter.tick(CommandLineBuilder.supported_by_agent(sym, node)) }]
|
|
91
|
+
end
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
# Documentation of supported agents and command line option of a parameter: `annotate` of `Schema::Documentation`
|
|
98
|
+
# @param node [Hash] Schema of the parameter
|
|
99
|
+
# @param formatter [FormatterInterface] Formatter of the documentation
|
|
100
|
+
# @param agent [Symbol, nil] Show only parameters supported by this agent, or all if `nil`
|
|
101
|
+
# @param agents_line [Boolean] `true`: add a line with supported agents, if not all
|
|
102
|
+
# @return [Array<String>, nil] Lines added to the description, `nil` to hide the parameter
|
|
103
|
+
def doc_lines(node, formatter, agent: nil, agents_line: true)
|
|
104
|
+
agents = Agent::Factory::ALL.keys.select { |sym| CommandLineBuilder.supported_by_agent(sym, node) }
|
|
105
|
+
return if agents.empty? || (agent && !agents.include?(agent))
|
|
106
|
+
lines = []
|
|
107
|
+
lines.push("(#{agents.map { |i| Agent::Factory::ALL[i][:short].to_s.upcase }.sort.join(', ')})") if agents_line && !agents.length.eql?(Agent::Factory::ALL.length)
|
|
108
|
+
lines.concat(CommandLineBuilder.doc_lines(node, formatter))
|
|
109
|
+
end
|
|
70
110
|
end
|
|
71
111
|
SCHEMA = CommandLineBuilder.read_schema(Schema::Registry::TRANSFER_SPEC, ascp: true)
|
|
72
112
|
# Define constants for enums of parameters: <parameter>_<enum>, e.g. CIPHER_AES_128, DIRECTION_SEND, ...
|
|
@@ -757,11 +757,14 @@ properties:
|
|
|
757
757
|
x-cli-option: -l
|
|
758
758
|
target_rate:
|
|
759
759
|
description: >-
|
|
760
|
-
Pseudo-parameter: desired transfer rate
|
|
761
|
-
Converted to `target_rate_kbps` before the transfer is started, overriding it if both are present.
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
760
|
+
Pseudo-parameter: desired transfer rate in bps, with an optional unit suffix.
|
|
761
|
+
Converted to `target_rate_kbps` (rounded down) before the transfer is started, overriding it if both are present.
|
|
762
|
+
Also applies to `async` (sync).
|
|
763
|
+
Accepted formats: integer (bps), or integer followed by `k`/`K` (x1000), `m`/`M` (x1000000), `g`/`G` (x1000000000).
|
|
764
|
+
Examples: `100000000`, `100000k`, `100m`, `1g`.
|
|
765
|
+
type:
|
|
766
|
+
- string
|
|
767
|
+
- integer
|
|
765
768
|
x-cli-special: true
|
|
766
769
|
title:
|
|
767
770
|
description: Title of the transfer.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<%# Content of the page displayed after authentication, in layout of Aspera::WebPage %>
|
|
2
|
+
<div class="icon-circle ok">
|
|
3
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
|
4
|
+
<polyline points="6 16 13 23 26 9"/>
|
|
5
|
+
</svg>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<h1>Authentication Successful</h1>
|
|
9
|
+
<p>You can close this window and return to your terminal.</p>
|
|
10
|
+
|
|
11
|
+
<% unless additional_info.to_s.empty? %>
|
|
12
|
+
<div class="info-box">
|
|
13
|
+
<%= h additional_info %>
|
|
14
|
+
</div>
|
|
15
|
+
<% end %>
|
|
16
|
+
|