aspera-cli 4.26.2 → 4.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +281 -21
- data/CONTRIBUTING.md +98 -7
- data/bin/ascli +3 -3
- data/docs/README.md +10757 -0
- data/docs/test-mcp-with-ai.md +202 -0
- data/lib/aspera/agent/base.rb +20 -6
- data/lib/aspera/agent/connect.rb +53 -16
- data/lib/aspera/agent/desktop.rb +52 -17
- data/lib/aspera/agent/direct.rb +26 -24
- data/lib/aspera/agent/httpgw.rb +1 -1
- data/lib/aspera/agent/node.rb +32 -0
- data/lib/aspera/agent/transferd.rb +39 -3
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +14 -16
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/faspex.rb +15 -14
- data/lib/aspera/api/httpgw.rb +4 -4
- data/lib/aspera/api/node.rb +26 -23
- data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
- data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
- data/lib/aspera/ascmd.rb +21 -19
- data/lib/aspera/ascp/installation.rb +37 -38
- data/lib/aspera/ascp/management.rb +2 -3
- data/lib/aspera/cli/ascp_actions.rb +155 -0
- data/lib/aspera/cli/async_transfer_store.rb +81 -0
- data/lib/aspera/cli/bootstrapper.rb +197 -0
- data/lib/aspera/cli/command_registry.rb +145 -0
- data/lib/aspera/cli/command_spec.rb +138 -0
- data/lib/aspera/cli/context.rb +16 -3
- data/lib/aspera/cli/error.rb +16 -0
- data/lib/aspera/cli/extended_value.rb +2 -1
- data/lib/aspera/cli/formatter.rb +41 -27
- data/lib/aspera/cli/gem_checker.rb +65 -0
- data/lib/aspera/cli/hints.rb +2 -2
- data/lib/aspera/cli/http.rb +70 -19
- data/lib/aspera/cli/info.rb +2 -0
- data/lib/aspera/cli/mailer.rb +97 -0
- data/lib/aspera/cli/mcp_tool.rb +198 -0
- data/lib/aspera/cli/options.schema.yaml +364 -7
- data/lib/aspera/cli/{manager.rb → parser.rb} +334 -142
- data/lib/aspera/cli/plugins/alee.rb +20 -22
- data/lib/aspera/cli/plugins/aoc.rb +1166 -853
- data/lib/aspera/cli/plugins/ats.rb +200 -161
- data/lib/aspera/cli/plugins/base.rb +457 -100
- data/lib/aspera/cli/plugins/basic_auth.rb +14 -4
- data/lib/aspera/cli/plugins/config.rb +428 -754
- data/lib/aspera/cli/plugins/console.rb +106 -64
- data/lib/aspera/cli/plugins/cos.rb +44 -32
- data/lib/aspera/cli/plugins/factory.rb +4 -4
- data/lib/aspera/cli/plugins/faspex.rb +294 -257
- data/lib/aspera/cli/plugins/faspex5.rb +585 -357
- data/lib/aspera/cli/plugins/faspio.rb +49 -51
- data/lib/aspera/cli/plugins/httpgw.rb +18 -25
- data/lib/aspera/cli/plugins/mcp.rb +279 -0
- data/lib/aspera/cli/plugins/node.rb +998 -794
- data/lib/aspera/cli/plugins/oauth.rb +7 -10
- data/lib/aspera/cli/plugins/orchestrator.rb +111 -134
- data/lib/aspera/cli/plugins/preview.rb +180 -139
- data/lib/aspera/cli/plugins/server.rb +124 -79
- data/lib/aspera/cli/plugins/shares.rb +301 -107
- data/lib/aspera/cli/preset_actions.rb +139 -0
- data/lib/aspera/cli/preset_manager.rb +13 -12
- data/lib/aspera/cli/result.rb +61 -11
- data/lib/aspera/cli/runner.rb +275 -141
- data/lib/aspera/cli/secret_finder.rb +40 -0
- data/lib/aspera/cli/special_values.rb +1 -0
- data/lib/aspera/cli/sync_actions.rb +83 -59
- data/lib/aspera/cli/terminal_formatter.rb +1 -1
- data/lib/aspera/cli/transfer_actions.rb +83 -0
- data/lib/aspera/cli/transfer_agent.rb +116 -51
- data/lib/aspera/cli/transfer_progress.rb +3 -3
- data/lib/aspera/cli/vault_manager.rb +57 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +21 -20
- data/lib/aspera/command_line_builder.rb +22 -22
- data/lib/aspera/data_repository.rb +2 -1
- data/lib/aspera/dot_container.rb +4 -4
- data/lib/aspera/environment.rb +18 -11
- data/lib/aspera/exec_spec.rb +13 -0
- data/lib/aspera/faspex_gw.rb +1 -1
- data/lib/aspera/faspex_postproc.rb +4 -3
- data/lib/aspera/graphql.rb +35 -0
- data/lib/aspera/hash_ext.rb +6 -0
- data/lib/aspera/json_rpc/client.rb +62 -0
- data/lib/aspera/json_rpc/version.rb +7 -0
- data/lib/aspera/keychain/encrypted_hash.rb +1 -1
- data/lib/aspera/keychain/factory.rb +3 -3
- data/lib/aspera/keychain/macos_security.rb +3 -3
- data/lib/aspera/link_header.rb +82 -0
- data/lib/aspera/log.rb +17 -3
- data/lib/aspera/markdown.rb +85 -2
- data/lib/aspera/node_simulator.rb +3 -1
- data/lib/aspera/oauth/base.rb +5 -5
- data/lib/aspera/oauth/boot.rb +2 -2
- data/lib/aspera/oauth/factory.rb +7 -5
- data/lib/aspera/oauth/jwt.rb +4 -4
- data/lib/aspera/oauth/web.rb +2 -2
- data/lib/aspera/persistency_action_once.rb +8 -8
- data/lib/aspera/persistency_folder.rb +10 -3
- data/lib/aspera/preview/file_types.rb +3 -3
- data/lib/aspera/preview/generator.rb +3 -3
- data/lib/aspera/preview/utils.rb +1 -1
- data/lib/aspera/products/connect.rb +1 -1
- data/lib/aspera/products/other.rb +1 -1
- data/lib/aspera/products/transferd.rb +1 -1
- data/lib/aspera/proxy_auto_config.rb +7 -6
- data/lib/aspera/rest.rb +10 -36
- data/lib/aspera/rest_error_analyzer.rb +3 -3
- data/lib/aspera/rest_errors_aspera.rb +0 -10
- data/lib/aspera/rest_list.rb +3 -3
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1853 -510
- data/lib/aspera/schema/documentation.rb +57 -26
- data/lib/aspera/schema/reader.rb +59 -10
- data/lib/aspera/schema/registry.rb +39 -6
- data/lib/aspera/secret_hider.rb +7 -2
- data/lib/aspera/ssl.rb +4 -4
- data/lib/aspera/sync/conf.schema.yaml +2 -2
- data/lib/aspera/sync/operations.rb +10 -13
- data/lib/aspera/temp_file_manager.rb +1 -1
- data/lib/aspera/timer_limiter.rb +1 -1
- data/lib/aspera/transfer/faux_file.rb +24 -11
- data/lib/aspera/transfer/parameters.rb +23 -21
- data/lib/aspera/transfer/result.rb +74 -0
- data/lib/aspera/transfer/resumer.rb +6 -6
- data/lib/aspera/transfer/spec.rb +18 -1
- data/lib/aspera/transfer/spec.schema.yaml +12 -3
- data/lib/aspera/uri_reader.rb +2 -2
- data/lib/aspera/web_auth.rb +2 -2
- data/lib/aspera/web_server_simple.rb +8 -5
- data/lib/aspera/yaml.rb +2 -1
- data.tar.gz.sig +0 -0
- metadata +25 -3
- metadata.gz.sig +0 -0
- data/lib/aspera/json_rpc.rb +0 -52
|
@@ -0,0 +1,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
|
|
@@ -142,9 +145,13 @@ module Aspera
|
|
|
142
145
|
attr_reader :logger_type, :logger
|
|
143
146
|
attr_accessor :dump_format
|
|
144
147
|
|
|
148
|
+
# Set the program name used in log output
|
|
149
|
+
# @param value [String] Program name
|
|
150
|
+
# @return [nil]
|
|
145
151
|
def program_name=(value)
|
|
146
152
|
@program_name = value
|
|
147
153
|
self.logger_type = @logger_type
|
|
154
|
+
nil
|
|
148
155
|
end
|
|
149
156
|
|
|
150
157
|
# Set log level of underlying logger given symbol level
|
|
@@ -154,15 +161,19 @@ module Aspera
|
|
|
154
161
|
@logger.level = Logger::Severity.const_get(new_level.to_sym.upcase)
|
|
155
162
|
end
|
|
156
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]
|
|
157
167
|
def formatter=(formatter)
|
|
158
168
|
if formatter.is_a?(String)
|
|
159
|
-
|
|
169
|
+
Aspera.assert(FORMATTER_LAMBDAS.key?(formatter.to_sym), type: Error){"Unknown formatter #{formatter}, use one of: #{FORMATTERS.join(', ')}"}
|
|
160
170
|
formatter = FORMATTER_LAMBDAS[formatter.to_sym]
|
|
161
171
|
elsif !formatter.respond_to?(:call) && !formatter.is_a?(Logger::Formatter)
|
|
162
172
|
raise Error, 'Formatter must be a String, a Logger::Formatter or a Proc'
|
|
163
173
|
end
|
|
164
174
|
# Update formatter with password hiding
|
|
165
175
|
@logger.formatter = SecretHider.instance.log_formatter(formatter)
|
|
176
|
+
nil
|
|
166
177
|
end
|
|
167
178
|
|
|
168
179
|
def formatter
|
|
@@ -176,7 +187,9 @@ module Aspera
|
|
|
176
187
|
Logger::SEVERITY_LABEL[@logger.level].downcase
|
|
177
188
|
end
|
|
178
189
|
|
|
179
|
-
# 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]
|
|
180
193
|
def logger_type=(new_log_type)
|
|
181
194
|
# [Integer]
|
|
182
195
|
current_severity_integer = @logger&.level || ENV['AS_LOG_LEVEL']&.to_i || Logger::Severity::INFO
|
|
@@ -203,6 +216,7 @@ module Aspera
|
|
|
203
216
|
@logger_type = new_log_type
|
|
204
217
|
# add secret hider to default logger
|
|
205
218
|
self.formatter = @logger.formatter
|
|
219
|
+
nil
|
|
206
220
|
end
|
|
207
221
|
|
|
208
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
|
|
@@ -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
|
@@ -48,8 +48,8 @@ module Aspera
|
|
|
48
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
|
data/lib/aspera/oauth/boot.rb
CHANGED
|
@@ -8,11 +8,11 @@ module Aspera
|
|
|
8
8
|
module OAuth
|
|
9
9
|
# Token provider bootstrapped from an existing cookie (e.g. AoC browser cookie).
|
|
10
10
|
# Injects the bearer token and optional refresh token directly into the cache.
|
|
11
|
-
# Never generates a new token from scratch
|
|
11
|
+
# Never generates a new token from scratch - raises if cache+refresh are both exhausted.
|
|
12
12
|
class Boot < Base
|
|
13
13
|
# @param cookie [String, nil] Raw cookie string (--password), nil to rely on existing cache
|
|
14
14
|
# @param username [String, nil] Expected subject; if provided, must match token's `sub` claim
|
|
15
|
-
# @param **base_params
|
|
15
|
+
# @param **base_params [Hash] Forwarded to Base (base_url:, params: {client_id:, scope:}, etc.)
|
|
16
16
|
def initialize(cookie: nil, username: nil, **base_params)
|
|
17
17
|
if cookie.nil?
|
|
18
18
|
# No cookie: rely on existing cache, identified by username if provided
|
data/lib/aspera/oauth/factory.rb
CHANGED
|
@@ -108,7 +108,7 @@ module Aspera
|
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
# Delete all existing tokens in cache
|
|
111
|
-
# @return [
|
|
111
|
+
# @return [Array<String>] List of deleted file paths
|
|
112
112
|
def flush_tokens
|
|
113
113
|
persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN)
|
|
114
114
|
end
|
|
@@ -119,7 +119,7 @@ module Aspera
|
|
|
119
119
|
data = persist_mgr.current_items(PERSIST_CATEGORY_TOKEN)
|
|
120
120
|
data.each.map do |k, v|
|
|
121
121
|
info = {id: k}
|
|
122
|
-
info.merge!(JSON.parse(v)) rescue nil
|
|
122
|
+
begin; info.merge!(JSON.parse(v)); rescue StandardError; nil; end
|
|
123
123
|
d = decode_token(info.delete(TOKEN_FIELD))
|
|
124
124
|
info.merge(d) if d
|
|
125
125
|
info
|
|
@@ -155,9 +155,10 @@ module Aspera
|
|
|
155
155
|
|
|
156
156
|
# Register a bearer token decoder for inspecting token properties
|
|
157
157
|
# @param method [Proc] The decoder lambda/proc to register
|
|
158
|
-
# @return [
|
|
158
|
+
# @return [nil]
|
|
159
159
|
def register_decoder(method)
|
|
160
160
|
@decoders.push(method)
|
|
161
|
+
nil
|
|
161
162
|
end
|
|
162
163
|
|
|
163
164
|
# Decode a token using all registered decoders
|
|
@@ -165,7 +166,7 @@ module Aspera
|
|
|
165
166
|
# @return [Hash, nil] Decoded token data or nil if no decoder succeeded
|
|
166
167
|
def decode_token(token)
|
|
167
168
|
@decoders.each do |decoder|
|
|
168
|
-
result = decoder.call(token) rescue nil
|
|
169
|
+
result = begin; decoder.call(token); rescue StandardError; nil; end
|
|
169
170
|
return result unless result.nil?
|
|
170
171
|
end
|
|
171
172
|
return
|
|
@@ -173,12 +174,13 @@ module Aspera
|
|
|
173
174
|
|
|
174
175
|
# Register a token creation method
|
|
175
176
|
# @param creator_class [Class] The token creator class to register
|
|
176
|
-
# @return [
|
|
177
|
+
# @return [nil]
|
|
177
178
|
def register_token_creator(creator_class)
|
|
178
179
|
Aspera.assert_type(creator_class, Class)
|
|
179
180
|
id = Factory.class_to_id(creator_class)
|
|
180
181
|
Log.log.debug{"registering creator for #{id}"}
|
|
181
182
|
@token_type_classes[id] = creator_class
|
|
183
|
+
nil
|
|
182
184
|
end
|
|
183
185
|
|
|
184
186
|
# Create a token creator instance for the specified grant method
|
data/lib/aspera/oauth/jwt.rb
CHANGED
|
@@ -26,9 +26,9 @@ module Aspera
|
|
|
26
26
|
end
|
|
27
27
|
DEFAULT_PRIV_KEY_LENGTH = 4096
|
|
28
28
|
GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
|
|
29
|
-
# @param private_key_obj private key object
|
|
30
|
-
# @param payload payload to be included in the JWT
|
|
31
|
-
# @param headers headers to be included in the JWT
|
|
29
|
+
# @param private_key_obj [OpenSSL::PKey::RSA] private key object
|
|
30
|
+
# @param payload [Hash] payload to be included in the JWT
|
|
31
|
+
# @param headers [Hash] headers to be included in the JWT
|
|
32
32
|
def initialize(
|
|
33
33
|
private_key_obj:,
|
|
34
34
|
payload:,
|
|
@@ -49,7 +49,7 @@ module Aspera
|
|
|
49
49
|
|
|
50
50
|
def create_token
|
|
51
51
|
require 'jwt'
|
|
52
|
-
seconds_since_epoch = Time.
|
|
52
|
+
seconds_since_epoch = Time.now.to_i
|
|
53
53
|
Log.log.debug{"seconds_since_epoch=#{seconds_since_epoch}"}
|
|
54
54
|
jwt_payload = {
|
|
55
55
|
exp: seconds_since_epoch + OAuth::Factory.instance.parameters[:jwt_expiry_offset_sec], # expiration time
|
data/lib/aspera/oauth/web.rb
CHANGED
|
@@ -11,8 +11,8 @@ module Aspera
|
|
|
11
11
|
class << self
|
|
12
12
|
attr_accessor :additional_info
|
|
13
13
|
end
|
|
14
|
-
# @param redirect_uri
|
|
15
|
-
# @param path_authorize
|
|
14
|
+
# @param redirect_uri [String] url to receive the code after auth (to be exchanged for token)
|
|
15
|
+
# @param path_authorize [String] path to login page on web app
|
|
16
16
|
def initialize(
|
|
17
17
|
redirect_uri:,
|
|
18
18
|
path_authorize: 'authorize',
|
|
@@ -14,13 +14,13 @@ module Aspera
|
|
|
14
14
|
MANAGER_METHODS = %i[get put delete]
|
|
15
15
|
private_constant :DELETE_DEFAULT, :PARSE_DEFAULT, :FORMAT_DEFAULT, :MERGE_DEFAULT, :MANAGER_METHODS
|
|
16
16
|
|
|
17
|
-
# @param
|
|
18
|
-
# @param
|
|
19
|
-
# @param
|
|
20
|
-
# @param
|
|
21
|
-
# @param
|
|
22
|
-
# @param
|
|
23
|
-
# @param
|
|
17
|
+
# @param manager [Object] Mandatory Database (must respond to get/put/delete)
|
|
18
|
+
# @param data [Object] Mandatory object to persist, must be same object from begin to end (assume array by default)
|
|
19
|
+
# @param id [String] Mandatory identifier
|
|
20
|
+
# @param delete [Proc] Optional delete persistency condition
|
|
21
|
+
# @param parse [Proc] Optional parse method (default to JSON)
|
|
22
|
+
# @param format [Proc] Optional dump method (default to JSON)
|
|
23
|
+
# @param merge [Proc] Optional merge data from file to current data
|
|
24
24
|
def initialize(manager:, data:, id:, delete: DELETE_DEFAULT, parse: PARSE_DEFAULT, format: FORMAT_DEFAULT, merge: MERGE_DEFAULT)
|
|
25
25
|
Aspera.assert(MANAGER_METHODS.all?{ |i| manager.respond_to?(i)}){"Manager must answer to #{MANAGER_METHODS}"}
|
|
26
26
|
Aspera.assert(!data.nil?, 'data must not be nil')
|
|
@@ -48,7 +48,7 @@ module Aspera
|
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
# @return internal persisted object, in order to modify its content
|
|
51
|
+
# @return [Array, Hash] internal persisted object, in order to modify its content
|
|
52
52
|
def data
|
|
53
53
|
return @persisted_object
|
|
54
54
|
end
|
|
@@ -19,7 +19,8 @@ module Aspera
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
# Get value of persisted item
|
|
22
|
-
# @
|
|
22
|
+
# @param object_id [String] Identifier of persisted item
|
|
23
|
+
# @return [String, nil] Persisted value, or nil if not found
|
|
23
24
|
def get(object_id)
|
|
24
25
|
Log.log.debug{"persistency get: #{object_id}"}
|
|
25
26
|
if @cache.key?(object_id)
|
|
@@ -59,7 +60,10 @@ module Aspera
|
|
|
59
60
|
@cache.delete(object_id)
|
|
60
61
|
end
|
|
61
62
|
|
|
62
|
-
# Delete persisted items
|
|
63
|
+
# Delete persisted items matching the given category, optionally older than a given age
|
|
64
|
+
# @param persist_category [String] Prefix used to identify files belonging to the category
|
|
65
|
+
# @param max_age_seconds [Integer, nil] If set, only delete files older than this many seconds
|
|
66
|
+
# @return [Array<String>] List of deleted file paths
|
|
63
67
|
def garbage_collect(persist_category, max_age_seconds = nil)
|
|
64
68
|
garbage_files = current_files(persist_category)
|
|
65
69
|
if !max_age_seconds.nil?
|
|
@@ -74,6 +78,9 @@ module Aspera
|
|
|
74
78
|
return garbage_files
|
|
75
79
|
end
|
|
76
80
|
|
|
81
|
+
# List all persisted files matching the given category prefix
|
|
82
|
+
# @param persist_category [String] Prefix identifying the category
|
|
83
|
+
# @return [Array<String>] List of matching file paths
|
|
77
84
|
def current_files(persist_category)
|
|
78
85
|
Dir[File.join(@folder, "#{persist_category}*#{FILE_SUFFIX}")]
|
|
79
86
|
end
|
|
@@ -84,7 +91,7 @@ module Aspera
|
|
|
84
91
|
|
|
85
92
|
private
|
|
86
93
|
|
|
87
|
-
# @param object_id String or
|
|
94
|
+
# @param object_id [String, Array] identifier or list of identifiers
|
|
88
95
|
def id_to_filepath(object_id)
|
|
89
96
|
Aspera.assert_type(object_id, String)
|
|
90
97
|
FileUtils.mkdir_p(@folder)
|
|
@@ -78,7 +78,7 @@ module Aspera
|
|
|
78
78
|
|
|
79
79
|
# @param filepath [String] Full path to file
|
|
80
80
|
# @param mimetype [String] MIME typre provided by node API
|
|
81
|
-
# @return file type, one of enum CONVERSION_TYPES
|
|
81
|
+
# @return [Symbol] file type, one of enum CONVERSION_TYPES
|
|
82
82
|
# @raise [RuntimeError] if no conversion type found
|
|
83
83
|
def conversion_type(filepath, mimetype)
|
|
84
84
|
Log.log.debug{"conversion_type(#{filepath},mime=#{mimetype},magic=#{@use_mimemagic})"}
|
|
@@ -86,9 +86,9 @@ module Aspera
|
|
|
86
86
|
mimetype = TYPE_NOT_FOUND if mimetype.nil? || (mimetype.is_a?(String) && mimetype.empty?)
|
|
87
87
|
mimetype = Marcel::MimeType.for(Pathname.new(filepath), name: File.basename(filepath), declared_type: mimetype)
|
|
88
88
|
mimetype = 'text/plain' if mimetype.eql?(TYPE_NOT_FOUND) && ascii_text_file?(filepath)
|
|
89
|
-
|
|
89
|
+
Aspera.assert(!mimetype.eql?(TYPE_NOT_FOUND)){"no MIME type found for #{File.basename(filepath)}"}
|
|
90
90
|
conversion_type = mime_to_type(mimetype)
|
|
91
|
-
|
|
91
|
+
Aspera.assert(!conversion_type.nil?){"no conversion type found for #{File.basename(filepath)}"}
|
|
92
92
|
Log.log.trace1{"conversion_type(#{File.basename(filepath)}): #{conversion_type.class.name} [#{conversion_type}]"}
|
|
93
93
|
return conversion_type
|
|
94
94
|
end
|
|
@@ -27,10 +27,10 @@ module Aspera
|
|
|
27
27
|
# Node API MIME types are from: http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types.
|
|
28
28
|
# The resulting preview file type is taken from destination file extension.
|
|
29
29
|
# Conversion methods are provided by private methods: convert_<conversion_type>_to_<preview_format>.
|
|
30
|
-
#
|
|
31
|
-
#
|
|
30
|
+
# conversion_type: one of FileTypes::CONVERSION_TYPES.
|
|
31
|
+
# preview_format: one of Generator::PREVIEW_FORMATS.
|
|
32
32
|
# The conversion video->mp4 is implemented in methods: convert_video_to_mp4_using_<video_conversion>.
|
|
33
|
-
#
|
|
33
|
+
# conversion method: one of Generator::VIDEO_CONVERSION_METHODS.
|
|
34
34
|
# @param src [String] Source file path.
|
|
35
35
|
# @param dst [String] Destination file path.
|
|
36
36
|
# @param options [Options] All conversion options.
|
data/lib/aspera/preview/utils.rb
CHANGED
|
@@ -65,7 +65,7 @@ module Aspera
|
|
|
65
65
|
Log.dump(:javascript, connect_versions_javascript)
|
|
66
66
|
# get javascript object only
|
|
67
67
|
found = connect_versions_javascript.match(/^.*? = (.*);/)
|
|
68
|
-
|
|
68
|
+
Aspera.assert(!found.nil?, type: Cli::Error){'Problem when getting connect versions from internet'}
|
|
69
69
|
all_data = JSON.parse(found[1])
|
|
70
70
|
@connect_versions = all_data['entries']
|
|
71
71
|
end
|
|
@@ -57,7 +57,7 @@ module Aspera
|
|
|
57
57
|
# Find installed products and provide paths for it.
|
|
58
58
|
# @param scan_locations [Array] Array of Hash with keys: expected, app_root, sub_bin, ascp_path, name, version
|
|
59
59
|
# @return [Array] of products found, with filled missing fields
|
|
60
|
-
# @raise
|
|
60
|
+
# @raise [RuntimeError] if no installed product found
|
|
61
61
|
def find(scan_locations)
|
|
62
62
|
product_names = []
|
|
63
63
|
found = scan_locations.select do |item|
|
|
@@ -27,7 +27,7 @@ module Aspera
|
|
|
27
27
|
sdk_directory
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
# @return the path to folder where SDK is or should be installed
|
|
30
|
+
# @return [String] the path to folder where SDK is or should be installed
|
|
31
31
|
def sdk_directory
|
|
32
32
|
Aspera.assert(!@sdk_dir.nil?, 'SDK path was not initialized')
|
|
33
33
|
@sdk_dir
|
|
@@ -44,7 +44,7 @@ module Aspera
|
|
|
44
44
|
context_ip = r_addr.to_s if r_addr.is_a?(Resolv::IPv4)
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
|
-
|
|
47
|
+
Aspera.assert(!context_ip.nil?){"DNS name not found: #{context_host}"}
|
|
48
48
|
# NOTE: Javascript code here with string inclusions
|
|
49
49
|
javascript = <<END_OF_JAVASCRIPT
|
|
50
50
|
function dnsResolve(host) {
|
|
@@ -63,7 +63,7 @@ END_OF_JAVASCRIPT
|
|
|
63
63
|
|
|
64
64
|
attr_writer :proxy_user, :proxy_pass
|
|
65
65
|
|
|
66
|
-
# @param proxy_auto_config the proxy auto config script to be evaluated
|
|
66
|
+
# @param proxy_auto_config [String] the proxy auto config script to be evaluated
|
|
67
67
|
def initialize(proxy_auto_config)
|
|
68
68
|
# user provided javascript with FindProxyForURL function
|
|
69
69
|
@proxy_auto_config = proxy_auto_config
|
|
@@ -79,7 +79,7 @@ END_OF_JAVASCRIPT
|
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
# execute proxy auto config script for the given URL : https://en.wikipedia.org/wiki/Proxy_auto-config
|
|
82
|
-
# @return either nil, or a String formatted following PAC standard
|
|
82
|
+
# @return [String, nil] either nil, or a String formatted following PAC standard
|
|
83
83
|
def find_proxy_for_url(service_url)
|
|
84
84
|
uri = URI.parse(service_url)
|
|
85
85
|
simple_url = "#{uri.scheme}://#{uri.host}"
|
|
@@ -99,7 +99,7 @@ END_OF_JAVASCRIPT
|
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
# used to replace URI::Generic.find_proxy
|
|
102
|
-
# @return Array of
|
|
102
|
+
# @return [Array<URI>] list of URIs, possibly empty
|
|
103
103
|
def get_proxies(service_url)
|
|
104
104
|
# prepare result
|
|
105
105
|
uri_list = []
|
|
@@ -114,7 +114,8 @@ END_OF_JAVASCRIPT
|
|
|
114
114
|
proxy_list_str.split(';').each do |item|
|
|
115
115
|
# strip and split by space
|
|
116
116
|
parts = item.strip.split
|
|
117
|
-
|
|
117
|
+
proxy_type = parts.shift
|
|
118
|
+
case proxy_type
|
|
118
119
|
when 'DIRECT'
|
|
119
120
|
Aspera.assert(parts.empty?, 'DIRECT has no param')
|
|
120
121
|
Log.log.debug('ignoring proxy DIRECT')
|
|
@@ -136,7 +137,7 @@ END_OF_JAVASCRIPT
|
|
|
136
137
|
rescue StandardError => e
|
|
137
138
|
Log.log.warn{"PAC: cannot parse #{addr_port} #{e}"}
|
|
138
139
|
end
|
|
139
|
-
else Log.log.warn{"PAC: ignoring proxy type #{
|
|
140
|
+
else Log.log.warn{"PAC: ignoring proxy type #{proxy_type}: not supported"}
|
|
140
141
|
end
|
|
141
142
|
end
|
|
142
143
|
Log.log.debug{"Proxies: #{uri_list}"}
|