aspera-cli 4.26.1 → 4.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +300 -22
- data/CONTRIBUTING.md +100 -12
- data/bin/ascli +5 -5
- data/docs/README.md +10757 -0
- data/docs/test-mcp-with-ai.md +202 -0
- data/lib/aspera/agent/base.rb +20 -6
- data/lib/aspera/agent/connect.rb +53 -16
- data/lib/aspera/agent/desktop.rb +52 -17
- data/lib/aspera/agent/direct.rb +27 -25
- data/lib/aspera/agent/factory.rb +4 -0
- data/lib/aspera/agent/httpgw.rb +2 -2
- data/lib/aspera/agent/node.rb +34 -2
- data/lib/aspera/agent/transferd.rb +39 -3
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +56 -51
- data/lib/aspera/api/cos_node.rb +2 -2
- data/lib/aspera/api/faspex.rb +22 -20
- data/lib/aspera/api/httpgw.rb +4 -4
- data/lib/aspera/api/node.rb +33 -30
- data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
- data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
- data/lib/aspera/ascmd.rb +25 -23
- data/lib/aspera/ascp/installation.rb +43 -40
- data/lib/aspera/ascp/management.rb +2 -3
- data/lib/aspera/cli/ascp_actions.rb +155 -0
- data/lib/aspera/cli/async_transfer_store.rb +81 -0
- data/lib/aspera/cli/bootstrapper.rb +197 -0
- data/lib/aspera/cli/command_registry.rb +145 -0
- data/lib/aspera/cli/command_spec.rb +138 -0
- data/lib/aspera/cli/context.rb +71 -0
- data/lib/aspera/cli/error.rb +16 -0
- data/lib/aspera/cli/extended_value.rb +5 -3
- data/lib/aspera/cli/formatter.rb +122 -209
- data/lib/aspera/cli/gem_checker.rb +65 -0
- data/lib/aspera/cli/hints.rb +2 -2
- data/lib/aspera/cli/http.rb +218 -0
- data/lib/aspera/cli/info.rb +2 -0
- data/lib/aspera/cli/mailer.rb +97 -0
- data/lib/aspera/cli/mcp_tool.rb +198 -0
- data/lib/aspera/cli/options.schema.yaml +364 -7
- data/lib/aspera/cli/{manager.rb → parser.rb} +337 -144
- data/lib/aspera/cli/plugins/alee.rb +20 -22
- data/lib/aspera/cli/plugins/aoc.rb +1173 -849
- data/lib/aspera/cli/plugins/ats.rb +200 -161
- data/lib/aspera/cli/plugins/base.rb +470 -107
- data/lib/aspera/cli/plugins/basic_auth.rb +14 -4
- data/lib/aspera/cli/plugins/config.rb +434 -1032
- data/lib/aspera/cli/plugins/console.rb +106 -64
- data/lib/aspera/cli/plugins/cos.rb +44 -32
- data/lib/aspera/cli/plugins/factory.rb +7 -4
- data/lib/aspera/cli/plugins/faspex.rb +296 -259
- data/lib/aspera/cli/plugins/faspex5.rb +592 -344
- data/lib/aspera/cli/plugins/faspio.rb +49 -51
- data/lib/aspera/cli/plugins/httpgw.rb +18 -25
- data/lib/aspera/cli/plugins/mcp.rb +279 -0
- data/lib/aspera/cli/plugins/node.rb +1001 -797
- data/lib/aspera/cli/plugins/oauth.rb +7 -10
- data/lib/aspera/cli/plugins/orchestrator.rb +111 -139
- data/lib/aspera/cli/plugins/preview.rb +183 -142
- data/lib/aspera/cli/plugins/server.rb +125 -80
- data/lib/aspera/cli/plugins/shares.rb +301 -107
- data/lib/aspera/cli/preset_actions.rb +139 -0
- data/lib/aspera/cli/preset_manager.rb +236 -0
- data/lib/aspera/cli/result.rb +360 -0
- data/lib/aspera/cli/runner.rb +473 -0
- data/lib/aspera/cli/secret_finder.rb +40 -0
- data/lib/aspera/cli/special_values.rb +1 -0
- data/lib/aspera/cli/sync_actions.rb +84 -56
- data/lib/aspera/cli/terminal_formatter.rb +65 -0
- data/lib/aspera/cli/transfer_actions.rb +83 -0
- data/lib/aspera/cli/transfer_agent.rb +116 -51
- data/lib/aspera/cli/transfer_progress.rb +9 -9
- data/lib/aspera/cli/vault_manager.rb +57 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +26 -26
- data/lib/aspera/command_line_builder.rb +23 -23
- data/lib/aspera/data_repository.rb +6 -1
- data/lib/aspera/dot_container.rb +5 -5
- data/lib/aspera/environment.rb +39 -13
- data/lib/aspera/exec_spec.rb +13 -0
- data/lib/aspera/faspex_gw.rb +2 -2
- data/lib/aspera/faspex_postproc.rb +4 -3
- data/lib/aspera/graphql.rb +35 -0
- data/lib/aspera/hash_ext.rb +6 -0
- data/lib/aspera/json_rpc/client.rb +62 -0
- data/lib/aspera/json_rpc/version.rb +7 -0
- data/lib/aspera/keychain/base.rb +1 -1
- data/lib/aspera/keychain/encrypted_hash.rb +2 -2
- data/lib/aspera/keychain/factory.rb +4 -4
- data/lib/aspera/keychain/macos_security.rb +4 -4
- data/lib/aspera/link_header.rb +82 -0
- data/lib/aspera/log.rb +23 -5
- data/lib/aspera/markdown.rb +85 -2
- data/lib/aspera/nagios.rb +2 -2
- data/lib/aspera/node_simulator.rb +3 -1
- data/lib/aspera/oauth/base.rb +10 -10
- data/lib/aspera/oauth/boot.rb +43 -0
- data/lib/aspera/oauth/factory.rb +41 -15
- data/lib/aspera/oauth/jwt.rb +4 -4
- data/lib/aspera/oauth/web.rb +4 -4
- data/lib/aspera/oauth.rb +1 -0
- data/lib/aspera/persistency_action_once.rb +10 -10
- data/lib/aspera/persistency_folder.rb +10 -3
- data/lib/aspera/preview/file_types.rb +7 -3
- data/lib/aspera/preview/generator.rb +3 -3
- data/lib/aspera/preview/utils.rb +1 -1
- data/lib/aspera/products/connect.rb +4 -1
- data/lib/aspera/products/other.rb +1 -1
- data/lib/aspera/products/transferd.rb +3 -3
- data/lib/aspera/proxy_auto_config.rb +10 -9
- data/lib/aspera/rest.rb +18 -40
- data/lib/aspera/rest_error_analyzer.rb +7 -3
- data/lib/aspera/rest_errors_aspera.rb +0 -10
- data/lib/aspera/rest_list.rb +13 -6
- data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +14 -4
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +2730 -648
- data/lib/aspera/schema/async_tables.yaml +361 -0
- data/lib/aspera/schema/documentation.rb +57 -26
- data/lib/aspera/schema/reader.rb +60 -11
- data/lib/aspera/schema/registry.rb +50 -10
- data/lib/aspera/secret_hider.rb +11 -2
- data/lib/aspera/ssl.rb +4 -4
- data/lib/aspera/sync/conf.schema.yaml +2 -2
- data/lib/aspera/sync/database.rb +14 -10
- data/lib/aspera/sync/operations.rb +15 -18
- data/lib/aspera/temp_file_manager.rb +5 -1
- data/lib/aspera/timer_limiter.rb +1 -1
- data/lib/aspera/transfer/faux_file.rb +24 -11
- data/lib/aspera/transfer/parameters.rb +27 -25
- data/lib/aspera/transfer/result.rb +74 -0
- data/lib/aspera/transfer/resumer.rb +7 -7
- data/lib/aspera/transfer/spec.rb +18 -1
- data/lib/aspera/transfer/spec.schema.yaml +26 -8
- data/lib/aspera/transfer/uri.rb +1 -1
- data/lib/aspera/uri_reader.rb +2 -2
- data/lib/aspera/web_auth.rb +2 -2
- data/lib/aspera/web_server_simple.rb +8 -5
- data/lib/aspera/yaml.rb +2 -1
- data.tar.gz.sig +0 -0
- metadata +45 -16
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/main.rb +0 -480
- data/lib/aspera/json_rpc.rb +0 -51
|
@@ -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
|
data/lib/aspera/keychain/base.rb
CHANGED
|
@@ -28,7 +28,7 @@ module Aspera
|
|
|
28
28
|
vault_file = File.read(@path)
|
|
29
29
|
if vault_file.start_with?('---')
|
|
30
30
|
vault_info = YAML.parse(vault_file).to_ruby
|
|
31
|
-
Aspera.assert(vault_info.keys.sort == FILE_KEYS
|
|
31
|
+
Aspera.assert(vault_info.keys.sort == FILE_KEYS, 'Invalid vault file')
|
|
32
32
|
@cipher_name = vault_info['cipher']
|
|
33
33
|
vault_encrypted_data = vault_info['data']
|
|
34
34
|
else
|
|
@@ -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
|
|
@@ -20,23 +20,23 @@ module Aspera
|
|
|
20
20
|
when 'file'
|
|
21
21
|
info[:file] = name || 'vault.bin'
|
|
22
22
|
info[:file] = File.join(folder, info[:file]) unless File.absolute_path?(info[:file])
|
|
23
|
-
Aspera.assert(!password.nil
|
|
23
|
+
Aspera.assert(!password.nil?, 'please provide password')
|
|
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
|
|
@@ -42,9 +42,9 @@ module Aspera
|
|
|
42
42
|
url = options&.delete(:url)
|
|
43
43
|
if !url.nil?
|
|
44
44
|
uri = URI.parse(url)
|
|
45
|
-
Aspera.assert(uri.scheme.eql?('https')
|
|
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
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'strscan'
|
|
4
|
+
|
|
5
|
+
module Aspera
|
|
6
|
+
# Parse and represent an HTTP Link header as per RFC 8288.
|
|
7
|
+
# Inspired by the link_header gem, with the following fixes:
|
|
8
|
+
# - rel lookup is case-insensitive (RFC 7230: parameter names are case-insensitive)
|
|
9
|
+
# - uses StringScanner so commas inside <URI> are never mistaken for entry separators
|
|
10
|
+
# - no external dependency
|
|
11
|
+
class LinkHeader
|
|
12
|
+
# A single link entry: one URI and its attribute pairs.
|
|
13
|
+
class Link
|
|
14
|
+
attr_reader :href, :attr_pairs
|
|
15
|
+
|
|
16
|
+
def initialize(href, attr_pairs)
|
|
17
|
+
@href = href
|
|
18
|
+
@attr_pairs = attr_pairs
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Retrieve an attribute value by name, case-insensitively.
|
|
22
|
+
def [](key)
|
|
23
|
+
pair = @attr_pairs.detect{ |k, _v| k.casecmp?(key)}
|
|
24
|
+
pair&.last
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
attr_reader :links
|
|
29
|
+
|
|
30
|
+
def initialize(links = [])
|
|
31
|
+
@links = links
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Return the href of the first link whose +rel+ attribute matches +rel+.
|
|
35
|
+
# Comparison is case-insensitive per RFC 7230 s.3.2 and RFC 8288 s.3.
|
|
36
|
+
# Returns nil if no link with that relation exists.
|
|
37
|
+
# @param rel [String]
|
|
38
|
+
# @return [String, nil]
|
|
39
|
+
def find_href(rel: 'next')
|
|
40
|
+
@links.detect{ |link| link['rel']&.casecmp?(rel)}&.href
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
# Parse a raw Link header value into a +LinkHeader+ instance.
|
|
45
|
+
# Uses StringScanner so that commas inside <URI> are not treated as separators.
|
|
46
|
+
# @param raw [String, nil]
|
|
47
|
+
# @return [LinkHeader]
|
|
48
|
+
def parse(raw)
|
|
49
|
+
return new unless raw && !raw.empty?
|
|
50
|
+
|
|
51
|
+
links = []
|
|
52
|
+
scanner = StringScanner.new(raw)
|
|
53
|
+
|
|
54
|
+
while scanner.scan(HREF_RE)
|
|
55
|
+
href = scanner[1].strip
|
|
56
|
+
attrs = []
|
|
57
|
+
while scanner.scan(ATTR_RE)
|
|
58
|
+
key = scanner[1]
|
|
59
|
+
# scanner[2] = full match (token or "quoted"), scanner[3] = content inside double-quotes
|
|
60
|
+
value = scanner[3] || scanner[2]
|
|
61
|
+
attrs << [key, value]
|
|
62
|
+
break unless scanner.scan(SEMI_RE)
|
|
63
|
+
end
|
|
64
|
+
links << Link.new(href, attrs)
|
|
65
|
+
break unless scanner.scan(COMMA_RE)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
new(links)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private :new
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# RFC 2616 token: any char except separators
|
|
75
|
+
TOKEN_RE = /[^()<>@,;:\"\[\]?={}\s]+/ # RFC 2616 token
|
|
76
|
+
QUOTED_RE = /"((?:[^"\\]|\\.)*)"/ # double-quoted string with backslash escapes
|
|
77
|
+
HREF_RE = /\s*<([^>]*)>\s*;?\s*/ # <URI> possibly followed by ;
|
|
78
|
+
ATTR_RE = /(#{TOKEN_RE})\s*=\s*(#{TOKEN_RE}|#{QUOTED_RE})\s*/ # key=value or key="value"
|
|
79
|
+
SEMI_RE = /;\s*/ # parameter separator
|
|
80
|
+
COMMA_RE = /,\s*/ # link entry separator
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/aspera/log.rb
CHANGED
|
@@ -36,13 +36,16 @@ class Logger
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
class << self
|
|
39
|
-
# Define methods for a given log level
|
|
39
|
+
# Define logger methods for a given log level (e.g. `debug`, `debug?`, `debug!`)
|
|
40
|
+
# @param str_level [String] Log level name (e.g. "DEBUG", "TRACE1")
|
|
41
|
+
# @return [nil]
|
|
40
42
|
def make_methods(str_level)
|
|
41
43
|
int_level = ::Logger.const_get(str_level.upcase)
|
|
42
44
|
method_base = str_level.downcase
|
|
43
45
|
define_method(method_base, ->(message = nil, &block){add(int_level, message, &block)})
|
|
44
46
|
define_method("#{method_base}?", ->{level <= int_level})
|
|
45
47
|
define_method("#{method_base}!", ->{self.level = int_level})
|
|
48
|
+
nil
|
|
46
49
|
end
|
|
47
50
|
end
|
|
48
51
|
# Declare methods for all levels
|
|
@@ -54,6 +57,10 @@ $VERBOSE = old_verbose
|
|
|
54
57
|
|
|
55
58
|
module Aspera
|
|
56
59
|
# Singleton object for logging
|
|
60
|
+
#
|
|
61
|
+
# @!method self.instance
|
|
62
|
+
# Returns the singleton instance of Log
|
|
63
|
+
# @return [Log] the singleton instance
|
|
57
64
|
class Log
|
|
58
65
|
include Singleton
|
|
59
66
|
|
|
@@ -80,7 +87,7 @@ module Aspera
|
|
|
80
87
|
# @yieldreturn [Object] Computed object to dump (alternative to object parameter)
|
|
81
88
|
def dump(name, object = nil, level: :debug, &block)
|
|
82
89
|
return unless instance.logger.send(:"#{level}?")
|
|
83
|
-
Aspera.assert(object.nil? || block.nil
|
|
90
|
+
Aspera.assert(object.nil? || block.nil?, 'Use either object, or block, not both')
|
|
84
91
|
object = yield if block_given?
|
|
85
92
|
instance.logger.send(level, obj_dump(name, object))
|
|
86
93
|
end
|
|
@@ -138,9 +145,13 @@ module Aspera
|
|
|
138
145
|
attr_reader :logger_type, :logger
|
|
139
146
|
attr_accessor :dump_format
|
|
140
147
|
|
|
148
|
+
# Set the program name used in log output
|
|
149
|
+
# @param value [String] Program name
|
|
150
|
+
# @return [nil]
|
|
141
151
|
def program_name=(value)
|
|
142
152
|
@program_name = value
|
|
143
153
|
self.logger_type = @logger_type
|
|
154
|
+
nil
|
|
144
155
|
end
|
|
145
156
|
|
|
146
157
|
# Set log level of underlying logger given symbol level
|
|
@@ -150,15 +161,19 @@ module Aspera
|
|
|
150
161
|
@logger.level = Logger::Severity.const_get(new_level.to_sym.upcase)
|
|
151
162
|
end
|
|
152
163
|
|
|
164
|
+
# Set log formatter; accepts a symbol name, a Proc, or a Logger::Formatter instance
|
|
165
|
+
# @param formatter [String, Proc, Logger::Formatter] Formatter to use; one of: standard, default, caller
|
|
166
|
+
# @return [nil]
|
|
153
167
|
def formatter=(formatter)
|
|
154
168
|
if formatter.is_a?(String)
|
|
155
|
-
|
|
169
|
+
Aspera.assert(FORMATTER_LAMBDAS.key?(formatter.to_sym), type: Error){"Unknown formatter #{formatter}, use one of: #{FORMATTERS.join(', ')}"}
|
|
156
170
|
formatter = FORMATTER_LAMBDAS[formatter.to_sym]
|
|
157
171
|
elsif !formatter.respond_to?(:call) && !formatter.is_a?(Logger::Formatter)
|
|
158
172
|
raise Error, 'Formatter must be a String, a Logger::Formatter or a Proc'
|
|
159
173
|
end
|
|
160
174
|
# Update formatter with password hiding
|
|
161
175
|
@logger.formatter = SecretHider.instance.log_formatter(formatter)
|
|
176
|
+
nil
|
|
162
177
|
end
|
|
163
178
|
|
|
164
179
|
def formatter
|
|
@@ -168,11 +183,13 @@ module Aspera
|
|
|
168
183
|
# Get symbol of debug level of underlying logger
|
|
169
184
|
# @return [Symbol] One of LEVELS
|
|
170
185
|
def level
|
|
171
|
-
Aspera.assert(Logger::SEVERITY_LABEL.key?(@logger.level))
|
|
186
|
+
Aspera.assert(Logger::SEVERITY_LABEL.key?(@logger.level), 'unexpected logger level value')
|
|
172
187
|
Logger::SEVERITY_LABEL[@logger.level].downcase
|
|
173
188
|
end
|
|
174
189
|
|
|
175
|
-
# Change underlying logger,
|
|
190
|
+
# Change underlying logger output destination, keeping current log level
|
|
191
|
+
# @param new_log_type [Symbol] Log destination; one of LOG_TYPES (:stderr, :stdout, :syslog)
|
|
192
|
+
# @return [nil]
|
|
176
193
|
def logger_type=(new_log_type)
|
|
177
194
|
# [Integer]
|
|
178
195
|
current_severity_integer = @logger&.level || ENV['AS_LOG_LEVEL']&.to_i || Logger::Severity::INFO
|
|
@@ -199,6 +216,7 @@ module Aspera
|
|
|
199
216
|
@logger_type = new_log_type
|
|
200
217
|
# add secret hider to default logger
|
|
201
218
|
self.formatter = @logger.formatter
|
|
219
|
+
nil
|
|
202
220
|
end
|
|
203
221
|
|
|
204
222
|
private
|
data/lib/aspera/markdown.rb
CHANGED
|
@@ -9,6 +9,71 @@ module Aspera
|
|
|
9
9
|
|
|
10
10
|
class << self
|
|
11
11
|
COL_WIDTH = 80
|
|
12
|
+
|
|
13
|
+
# Convert a Markdown heading text to a GitHub-flavoured anchor.
|
|
14
|
+
# Rules: downcase, keep letters/digits/spaces/hyphens, replace spaces with hyphens.
|
|
15
|
+
# Duplicate anchors are disambiguated by appending -1, -2, … (pass a seen Hash to track).
|
|
16
|
+
# @param text [String] raw heading text (without leading # and spaces)
|
|
17
|
+
# @param seen [Hash{String=>Integer}, nil] mutable counter; pass the same Hash across a document
|
|
18
|
+
# @return [String] anchor slug (without leading #)
|
|
19
|
+
def heading_to_anchor(text, seen: nil)
|
|
20
|
+
slug = text
|
|
21
|
+
.downcase
|
|
22
|
+
.gsub(/[`*_]/, '') # strip inline code/bold/italic markers
|
|
23
|
+
.gsub(/&[a-z]+;/, '') # strip HTML entities
|
|
24
|
+
.gsub(/[^\w\s-]/, '') # keep word chars, spaces, hyphens
|
|
25
|
+
.gsub(/\s+/, '-') # spaces → hyphens
|
|
26
|
+
.squeeze('-') # collapse consecutive hyphens
|
|
27
|
+
.strip
|
|
28
|
+
if seen
|
|
29
|
+
count = seen[slug].to_i
|
|
30
|
+
seen[slug] = count + 1
|
|
31
|
+
slug = "#{slug}-#{count}" if count > 0
|
|
32
|
+
end
|
|
33
|
+
slug
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Extract the table of contents from a Markdown document.
|
|
37
|
+
# @param content [String] full Markdown source
|
|
38
|
+
# @return [Array<Hash>] array of { level, title, anchor }
|
|
39
|
+
HEADING_RE = /^(\#{1,6})\s+(.+)$/
|
|
40
|
+
|
|
41
|
+
def toc(content)
|
|
42
|
+
seen = {}
|
|
43
|
+
content.each_line.filter_map do |line|
|
|
44
|
+
m = line.match(HEADING_RE)
|
|
45
|
+
next unless m
|
|
46
|
+
title = m[2].strip
|
|
47
|
+
{level: m[1].length, title: title, anchor: heading_to_anchor(title, seen: seen)}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Extract the content of a single section (heading + body until next heading of same/higher level).
|
|
52
|
+
# @param content [String] full Markdown source
|
|
53
|
+
# @param anchor [String] GitHub anchor slug (without #)
|
|
54
|
+
# @return [String, nil] the section content, or nil if not found
|
|
55
|
+
def extract_section(content, anchor)
|
|
56
|
+
seen = {}
|
|
57
|
+
section_level = nil
|
|
58
|
+
result = []
|
|
59
|
+
content.each_line do |line|
|
|
60
|
+
m = line.match(HEADING_RE)
|
|
61
|
+
if m
|
|
62
|
+
slug = heading_to_anchor(m[2].strip, seen: seen)
|
|
63
|
+
if section_level.nil?
|
|
64
|
+
# not yet found: check if this heading matches
|
|
65
|
+
next unless slug == anchor
|
|
66
|
+
section_level = m[1].length
|
|
67
|
+
elsif m[1].length <= section_level
|
|
68
|
+
# already in section: stop at same/higher level heading
|
|
69
|
+
break
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
result << line if section_level
|
|
73
|
+
end
|
|
74
|
+
result.empty? ? nil : result.join
|
|
75
|
+
end
|
|
76
|
+
|
|
12
77
|
# Generate markdown from the provided 2D table
|
|
13
78
|
# @param table [Array<Array<String>>] 2D array of strings
|
|
14
79
|
# @return [String] markdown table
|
|
@@ -26,28 +91,46 @@ module Aspera
|
|
|
26
91
|
end
|
|
27
92
|
|
|
28
93
|
# Generate markdown list from the provided list
|
|
94
|
+
# @param items [Array<String>] list of items
|
|
95
|
+
# @return [String] markdown unordered list
|
|
29
96
|
def list(items)
|
|
30
97
|
items.map{ |i| "- #{i}"}.join("\n")
|
|
31
98
|
end
|
|
32
99
|
|
|
100
|
+
# Generate a markdown heading
|
|
101
|
+
# @param title [String] heading text
|
|
102
|
+
# @param level [Integer] heading level (1–6)
|
|
103
|
+
# @return [String] markdown heading
|
|
33
104
|
def heading(title, level: 1)
|
|
34
105
|
"#{'#' * level} #{title}\n\n"
|
|
35
106
|
end
|
|
36
107
|
|
|
37
|
-
#
|
|
108
|
+
# Generate a GitHub-flavoured admonition block
|
|
109
|
+
# @param lines [Array<String>] lines of the admonition body
|
|
110
|
+
# @param type [String] admonition type: NOTE, CAUTION, WARNING, IMPORTANT, TIP, INFO
|
|
111
|
+
# @return [String] markdown admonition block
|
|
38
112
|
def admonition(lines, type: 'INFO')
|
|
39
113
|
"> [!#{type}]\n#{lines.map{ |l| "> #{l}"}.join("\n")}\n\n"
|
|
40
114
|
end
|
|
41
115
|
|
|
116
|
+
# Generate a fenced code block
|
|
117
|
+
# @param lines [Array<String>] lines of code
|
|
118
|
+
# @param type [String] language identifier for syntax highlighting
|
|
119
|
+
# @return [String] markdown fenced code block
|
|
42
120
|
def code(lines, type: 'shell')
|
|
43
121
|
"```#{type}\n#{lines.join("\n")}\n```\n\n"
|
|
44
122
|
end
|
|
45
123
|
|
|
46
|
-
# inline code
|
|
124
|
+
# Wrap text in inline code backticks
|
|
125
|
+
# @param text [String] text to wrap
|
|
126
|
+
# @return [String] inline code span
|
|
47
127
|
def icode(text)
|
|
48
128
|
"`#{text}`"
|
|
49
129
|
end
|
|
50
130
|
|
|
131
|
+
# Wrap text in a markdown paragraph (trailing blank line)
|
|
132
|
+
# @param text [String] paragraph content
|
|
133
|
+
# @return [String] paragraph with trailing newlines
|
|
51
134
|
def paragraph(text)
|
|
52
135
|
"#{text}\n\n"
|
|
53
136
|
end
|
data/lib/aspera/nagios.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Aspera
|
|
|
24
24
|
# Process results of a analysis and display status and exit with code
|
|
25
25
|
def process(data)
|
|
26
26
|
Aspera.assert_type(data, Array)
|
|
27
|
-
Aspera.assert(!data.empty
|
|
27
|
+
Aspera.assert(!data.empty?, 'data is empty')
|
|
28
28
|
%w[status component message].each do |c|
|
|
29
29
|
Aspera.assert(data.first.key?(c)){"result must have #{c}"}
|
|
30
30
|
end
|
|
@@ -79,7 +79,7 @@ module Aspera
|
|
|
79
79
|
# Readable status list
|
|
80
80
|
# @return [Array] of Hash
|
|
81
81
|
def status_list
|
|
82
|
-
Aspera.assert(!@data.empty
|
|
82
|
+
Aspera.assert(!@data.empty?, 'missing result')
|
|
83
83
|
@data.map{ |i| {'status' => LEVELS[i[:code]].to_s, 'component' => i[:comp], 'message' => i[:msg]}}
|
|
84
84
|
end
|
|
85
85
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'aspera/assert'
|
|
3
4
|
require 'aspera/ascp/installation'
|
|
4
5
|
require 'aspera/agent/direct'
|
|
5
6
|
require 'aspera/log'
|
|
@@ -128,6 +129,7 @@ module Aspera
|
|
|
128
129
|
|
|
129
130
|
# Process event from management port
|
|
130
131
|
def process_event(event)
|
|
132
|
+
# Code duplicated on purpose, as implementation goes, blocks will be replaced
|
|
131
133
|
case event['Type']
|
|
132
134
|
when 'NOP' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
|
|
133
135
|
# rubocop:disable Lint/DuplicateBranch
|
|
@@ -174,7 +176,7 @@ module Aspera
|
|
|
174
176
|
require 'time'
|
|
175
177
|
|
|
176
178
|
def folder_to_structure(folder_path)
|
|
177
|
-
|
|
179
|
+
Aspera.assert(Dir.exist?(folder_path)){"Path does not exist or is not a directory: #{folder_path}"}
|
|
178
180
|
|
|
179
181
|
# Build self structure
|
|
180
182
|
folder_stat = File.stat(folder_path)
|
data/lib/aspera/oauth/base.rb
CHANGED
|
@@ -15,15 +15,15 @@ module Aspera
|
|
|
15
15
|
class Base
|
|
16
16
|
Aspera.require_method!(:create_token)
|
|
17
17
|
# @param params [Hash] Parameters for token creation (client_id, client_secret, scope, etc...)
|
|
18
|
-
# @param use_query [Boolean] Provide parameters in query instead of body
|
|
19
18
|
# @param path_token [String] API end point to create a token from base URL
|
|
19
|
+
# @param use_query [Boolean] Provide parameters in query instead of body
|
|
20
20
|
# @param token_field [String] Field in result that contains the token
|
|
21
21
|
# @param cache_ids [Array] List of unique identifiers for cache id generation
|
|
22
22
|
# @param **rest_params [Hash] Parameters for REST
|
|
23
23
|
def initialize(
|
|
24
24
|
params: {},
|
|
25
|
+
path_token: 'token',
|
|
25
26
|
use_query: false,
|
|
26
|
-
path_token: 'token',
|
|
27
27
|
token_field: Factory::TOKEN_FIELD,
|
|
28
28
|
cache_ids: [],
|
|
29
29
|
**rest_params
|
|
@@ -42,14 +42,14 @@ module Aspera
|
|
|
42
42
|
|
|
43
43
|
# The OAuth API Object
|
|
44
44
|
attr_reader :api
|
|
45
|
-
# Sub path to generate token
|
|
46
|
-
attr_reader :path_token
|
|
47
45
|
# Parameters to generate token
|
|
48
46
|
attr_reader :params
|
|
47
|
+
# Sub path to generate token
|
|
48
|
+
attr_reader :path_token
|
|
49
49
|
|
|
50
50
|
# Helper method to create token as per RFC
|
|
51
|
-
# @return [HTTPResponse]
|
|
52
|
-
# @raise
|
|
51
|
+
# @return [Net::HTTPResponse] raw HTTP response with token
|
|
52
|
+
# @raise [RestCallError] if not 2XX code
|
|
53
53
|
def create_token_call(creation_params)
|
|
54
54
|
Log.log.debug{'Generating a new token'.bg_green}
|
|
55
55
|
return @api.create(@path_token, nil, query: creation_params, ret: :resp) if @use_query
|
|
@@ -73,8 +73,8 @@ module Aspera
|
|
|
73
73
|
# get an OAuth v2 token (generated, cached, refreshed)
|
|
74
74
|
# call token() to get a token.
|
|
75
75
|
# if a token is expired (api returns 4xx), call again token(refresh: true)
|
|
76
|
-
# @param cache set to false to disable cache
|
|
77
|
-
# @param refresh set to true to force refresh or re-generation (if previous failed)
|
|
76
|
+
# @param cache [Boolean] set to false to disable cache
|
|
77
|
+
# @param refresh [Boolean] set to true to force refresh or re-generation (if previous failed)
|
|
78
78
|
def token(cache: true, refresh: false)
|
|
79
79
|
# get token info from cache (or nil), decoded with date and expiration status
|
|
80
80
|
token_info = Factory.instance.get_token_info(@token_cache_id) if cache
|
|
@@ -106,7 +106,7 @@ module Aspera
|
|
|
106
106
|
json_data = http.body
|
|
107
107
|
token_data = JSON.parse(json_data)
|
|
108
108
|
Factory.instance.persist_mgr.put(@token_cache_id, json_data)
|
|
109
|
-
rescue => e
|
|
109
|
+
rescue StandardError => e
|
|
110
110
|
# Refresh token can fail.
|
|
111
111
|
Log.log.warn{"Refresh failed: #{e}"}
|
|
112
112
|
end
|
|
@@ -122,7 +122,7 @@ module Aspera
|
|
|
122
122
|
token_data = JSON.parse(json_data)
|
|
123
123
|
Factory.instance.persist_mgr.put(@token_cache_id, json_data)
|
|
124
124
|
end
|
|
125
|
-
Aspera.assert(token_data.key?(@token_field)){"API error: No such field in answer: #{@token_field}"}
|
|
125
|
+
Aspera.assert(token_data.key?(@token_field)){"API error: No such field in answer: #{@token_field}"} unless token_data.nil?
|
|
126
126
|
# ok we shall have a token here
|
|
127
127
|
return token_data[@token_field]
|
|
128
128
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/oauth/base'
|
|
4
|
+
require 'aspera/assert'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
module Aspera
|
|
8
|
+
module OAuth
|
|
9
|
+
# Token provider bootstrapped from an existing cookie (e.g. AoC browser cookie).
|
|
10
|
+
# Injects the bearer token and optional refresh token directly into the cache.
|
|
11
|
+
# Never generates a new token from scratch - raises if cache+refresh are both exhausted.
|
|
12
|
+
class Boot < Base
|
|
13
|
+
# @param cookie [String, nil] Raw cookie string (--password), nil to rely on existing cache
|
|
14
|
+
# @param username [String, nil] Expected subject; if provided, must match token's `sub` claim
|
|
15
|
+
# @param **base_params [Hash] Forwarded to Base (base_url:, params: {client_id:, scope:}, etc.)
|
|
16
|
+
def initialize(cookie: nil, username: nil, **base_params)
|
|
17
|
+
if cookie.nil?
|
|
18
|
+
# No cookie: rely on existing cache, identified by username if provided
|
|
19
|
+
Aspera.assert(username, 'Provide --password (cookie) on first use, or --username for cache lookup', type: ParameterError)
|
|
20
|
+
super(**base_params, cache_ids: [username])
|
|
21
|
+
else
|
|
22
|
+
cookies = cookie.split('; ').map{ |p| p.split('=', 2)}.to_h
|
|
23
|
+
Aspera.assert(cookies.key?('aoc.token'), '--password cookie does not contain aoc.token', type: ParameterError)
|
|
24
|
+
token = cookies['aoc.token']
|
|
25
|
+
decoded = Factory.instance.decode_token(token)
|
|
26
|
+
Aspera.assert_type(decoded, Hash){'Boot: token is not a decodable JWT'}
|
|
27
|
+
sub = decoded['sub']
|
|
28
|
+
Aspera.assert(username.nil? || username.eql?(sub)){"Boot: --username #{username} does not match token subject #{sub}"}
|
|
29
|
+
super(**base_params, cache_ids: [sub])
|
|
30
|
+
token_data = {Factory::TOKEN_FIELD => token}
|
|
31
|
+
token_data['refresh_token'] = cookies['aoc.refresh'] if cookies.key?('aoc.refresh')
|
|
32
|
+
Factory.instance.persist_mgr.put(@token_cache_id, JSON.generate(token_data))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Should never be reached: if cache and refresh are both exhausted, re-authenticate via browser
|
|
37
|
+
def create_token
|
|
38
|
+
Aspera.report_error(AssertError, 'Boot: token expired and no refresh available — re-authenticate in browser')
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
Factory.instance.register_token_creator(Boot)
|
|
42
|
+
end
|
|
43
|
+
end
|