aspera-cli 4.27.0 → 4.27.2
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 +46 -1
- data/CONTRIBUTING.md +1 -7
- data/TODO.md +50 -0
- data/docs/README.md +709 -489
- data/lib/aspera/agent/base.rb +10 -2
- data/lib/aspera/agent/connect.rb +7 -7
- data/lib/aspera/agent/desktop.rb +3 -3
- data/lib/aspera/agent/direct.rb +48 -19
- data/lib/aspera/agent/factory.rb +3 -3
- data/lib/aspera/agent/httpgw.rb +55 -14
- data/lib/aspera/agent/node.rb +2 -2
- data/lib/aspera/agent/transferd.rb +12 -12
- data/lib/aspera/api/aoc.rb +28 -28
- data/lib/aspera/api/cos_node.rb +10 -9
- data/lib/aspera/api/faspex.rb +2 -2
- data/lib/aspera/api/httpgw.rb +20 -20
- data/lib/aspera/api/node.rb +31 -53
- data/lib/aspera/ascmd.rb +13 -13
- data/lib/aspera/ascp/installation.rb +43 -36
- data/lib/aspera/ascp/management.rb +6 -6
- data/lib/aspera/assert.rb +12 -13
- data/lib/aspera/cli/ascp_actions.rb +7 -7
- data/lib/aspera/cli/async_transfer_store.rb +49 -5
- data/lib/aspera/cli/bootstrapper.rb +9 -9
- data/lib/aspera/cli/command_registry.rb +1 -1
- data/lib/aspera/cli/command_spec.rb +6 -3
- data/lib/aspera/cli/context.rb +3 -3
- data/lib/aspera/cli/extended_value.rb +26 -26
- data/lib/aspera/cli/formatter.rb +76 -54
- data/lib/aspera/cli/gem_checker.rb +1 -1
- data/lib/aspera/cli/hints.rb +2 -2
- data/lib/aspera/cli/http.rb +22 -29
- data/lib/aspera/cli/mailer.rb +7 -7
- data/lib/aspera/cli/mcp_tool.rb +31 -4
- data/lib/aspera/cli/option_declarator.rb +77 -0
- data/lib/aspera/cli/options.schema.yaml +273 -3
- data/lib/aspera/cli/parser.rb +539 -280
- data/lib/aspera/cli/plugins/alee.rb +1 -1
- data/lib/aspera/cli/plugins/aoc.rb +303 -314
- data/lib/aspera/cli/plugins/ats.rb +25 -21
- data/lib/aspera/cli/plugins/base.rb +357 -186
- data/lib/aspera/cli/plugins/basic_auth.rb +0 -12
- data/lib/aspera/cli/plugins/config.rb +76 -73
- data/lib/aspera/cli/plugins/console.rb +8 -8
- data/lib/aspera/cli/plugins/cos.rb +4 -3
- data/lib/aspera/cli/plugins/factory.rb +2 -2
- data/lib/aspera/cli/plugins/faspex5.rb +259 -248
- data/lib/aspera/cli/plugins/faspio.rb +7 -2
- data/lib/aspera/cli/plugins/httpgw.rb +1 -1
- data/lib/aspera/cli/plugins/mcp.rb +11 -11
- data/lib/aspera/cli/plugins/node.rb +227 -283
- data/lib/aspera/cli/plugins/orchestrator.rb +23 -23
- data/lib/aspera/cli/plugins/preview.rb +61 -64
- data/lib/aspera/cli/plugins/server.rb +36 -31
- data/lib/aspera/cli/plugins/shares.rb +174 -135
- data/lib/aspera/cli/preset_actions.rb +39 -19
- data/lib/aspera/cli/preset_manager.rb +74 -32
- data/lib/aspera/cli/result.rb +18 -16
- data/lib/aspera/cli/runner.rb +75 -39
- data/lib/aspera/cli/secret_finder.rb +1 -1
- data/lib/aspera/cli/sync_actions.rb +36 -35
- data/lib/aspera/cli/terminal_formatter.rb +2 -2
- data/lib/aspera/cli/transfer_actions.rb +17 -10
- data/lib/aspera/cli/transfer_agent.rb +44 -44
- data/lib/aspera/cli/transfer_progress.rb +7 -7
- data/lib/aspera/cli/vault_manager.rb +34 -12
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +17 -20
- data/lib/aspera/colors.rb +3 -3
- data/lib/aspera/command_line_builder.rb +17 -16
- data/lib/aspera/command_line_converter.rb +1 -1
- data/lib/aspera/coverage.rb +1 -2
- data/lib/aspera/data_repository.rb +1 -1
- data/lib/aspera/dot_container.rb +8 -8
- data/lib/aspera/environment.rb +13 -13
- data/lib/aspera/faspex_gw.rb +4 -4
- data/lib/aspera/faspex_postproc.rb +14 -9
- data/lib/aspera/graphql.rb +4 -2
- data/lib/aspera/hash_ext.rb +2 -2
- data/lib/aspera/json_rpc/client.rb +1 -1
- data/lib/aspera/keychain/base.rb +10 -3
- data/lib/aspera/keychain/encrypted_hash.rb +92 -17
- data/lib/aspera/keychain/factory.rb +17 -5
- data/lib/aspera/keychain/hashicorp_vault.rb +1 -1
- data/lib/aspera/keychain/macos_security.rb +23 -25
- data/lib/aspera/keychain/one_password_api.rb +86 -0
- data/lib/aspera/keychain/one_password_base.rb +34 -0
- data/lib/aspera/keychain/one_password_cli.rb +98 -0
- data/lib/aspera/link_header.rb +2 -2
- data/lib/aspera/log.rb +13 -13
- data/lib/aspera/markdown.rb +5 -5
- data/lib/aspera/nagios.rb +8 -8
- data/lib/aspera/node_simulator.rb +33 -27
- data/lib/aspera/oauth/base.rb +4 -5
- data/lib/aspera/oauth/boot.rb +3 -3
- data/lib/aspera/oauth/factory.rb +5 -5
- data/lib/aspera/oauth/jwt.rb +5 -7
- data/lib/aspera/oauth/web.rb +4 -4
- data/lib/aspera/persistency_action_once.rb +4 -4
- data/lib/aspera/persistency_folder.rb +8 -8
- data/lib/aspera/preview/file_types.rb +4 -4
- data/lib/aspera/preview/generator.rb +49 -46
- data/lib/aspera/preview/options.rb +3 -2
- data/lib/aspera/preview/terminal.rb +5 -5
- data/lib/aspera/preview/utils.rb +65 -18
- data/lib/aspera/products/connect.rb +2 -2
- data/lib/aspera/products/desktop.rb +1 -1
- data/lib/aspera/products/other.rb +2 -2
- data/lib/aspera/products/transferd.rb +3 -3
- data/lib/aspera/proxy_auto_config.rb +10 -10
- data/lib/aspera/rest.rb +149 -115
- data/lib/aspera/rest_error_analyzer.rb +3 -3
- data/lib/aspera/rest_list.rb +10 -9
- data/lib/aspera/schema/IBM Aspera Node API-4.4.6.yaml +6232 -0
- data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +249 -0
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +39 -0
- data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
- data/lib/aspera/schema/documentation.rb +17 -11
- data/lib/aspera/schema/reader.rb +78 -4
- data/lib/aspera/schema/registry.rb +26 -7
- data/lib/aspera/secret_hider.rb +5 -5
- data/lib/aspera/ssh.rb +64 -31
- data/lib/aspera/ssl.rb +3 -3
- data/lib/aspera/sync/database.rb +2 -2
- data/lib/aspera/sync/operations.rb +13 -13
- data/lib/aspera/temp_file_manager.rb +24 -4
- data/lib/aspera/transfer/faux_file.rb +2 -2
- data/lib/aspera/transfer/parameters.rb +15 -15
- data/lib/aspera/transfer/resumer.rb +9 -9
- data/lib/aspera/transfer/spec.rb +1 -1
- data/lib/aspera/transfer/spec.schema.yaml +1 -0
- data/lib/aspera/uri_reader.rb +52 -15
- data/lib/aspera/web_auth.rb +6 -5
- data/lib/aspera/web_server_simple.rb +9 -9
- data/lib/aspera/yaml.rb +4 -4
- data.tar.gz.sig +0 -0
- metadata +10 -4
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/plugins/faspex.rb +0 -576
- data/lib/aspera/transfer/uri.rb +0 -56
|
@@ -7,13 +7,18 @@ require 'pathname'
|
|
|
7
7
|
|
|
8
8
|
module Aspera
|
|
9
9
|
module Cli
|
|
10
|
-
#
|
|
10
|
+
# Helpers shared by plugins that expose Aspera Sync (async) operations.
|
|
11
|
+
# Provides DSL registration, argument-to-sync-info conversion, and action methods.
|
|
11
12
|
module SyncActions
|
|
12
13
|
# Translate state id (int) to string
|
|
13
14
|
STATE_STR = (['Nil'] +
|
|
14
|
-
(1..18).map{ |i| "P(#{i})"} +
|
|
15
|
+
(1..18).map { |i| "P(#{i})" } +
|
|
15
16
|
%w[Syncd Error Confl Pconf] +
|
|
16
|
-
(23..24).map{ |i| "P(#{i})"}).freeze
|
|
17
|
+
(23..24).map { |i| "P(#{i})" }).freeze
|
|
18
|
+
# Positional arguments shared by sync transfer commands (push/pull/bidi) and sync admin commands.
|
|
19
|
+
PATH_AND_INFO_ARGS = [{name: :path, type: String}, {name: :sync_info, type: Hash, mandatory: false, default: {}}].freeze
|
|
20
|
+
# Names of the leaf commands registered under any `sync admin` node.
|
|
21
|
+
ADMIN_COMMANDS = %i[find status meta counters file_info overview query].freeze
|
|
17
22
|
# When a plugin class includes SyncActions, register the :sql option
|
|
18
23
|
# in that class's DSL registry so Base#initialize auto-declares it.
|
|
19
24
|
class << self
|
|
@@ -24,17 +29,16 @@ module Aspera
|
|
|
24
29
|
# DSL helper: register the 7 `sync admin` leaf commands under the given parent path.
|
|
25
30
|
# Called at class-load time from any plugin that includes SyncActions.
|
|
26
31
|
# @param base [Class] the plugin class (receiver of DSL methods)
|
|
27
|
-
# @param admin_path [Symbol, Array<Symbol>]
|
|
32
|
+
# @param admin_path [Symbol, Array<Symbol>] path relative to current scope, e.g. :admin
|
|
28
33
|
def register_sync_admin_commands(base, admin_path)
|
|
29
|
-
path_and_info_args = [{name: :path, type: String}, {name: :sync_info, type: Hash, mandatory: false, default: {}}]
|
|
30
34
|
base.commands_under(admin_path) do
|
|
31
|
-
base.command(:status, description: 'Show sync session status', arguments: path_and_info_args, action: :action_sync_admin_status)
|
|
32
35
|
base.command(:find, description: 'Find sync database files', arguments: [{name: :path, type: String}], action: :action_sync_admin_find)
|
|
33
|
-
base.command(:
|
|
34
|
-
base.command(:
|
|
35
|
-
base.command(:
|
|
36
|
-
base.command(:
|
|
37
|
-
base.command(:
|
|
36
|
+
base.command(:status, description: 'Show sync session status', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_status)
|
|
37
|
+
base.command(:meta, description: 'Show sync session metadata', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_meta)
|
|
38
|
+
base.command(:counters, description: 'Show sync counters', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_counters)
|
|
39
|
+
base.command(:file_info, description: 'Show per-file sync state', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_file_info)
|
|
40
|
+
base.command(:overview, description: 'Show sync database overview', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_overview)
|
|
41
|
+
base.command(:query, description: 'Execute a raw SQL query', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_query)
|
|
38
42
|
end
|
|
39
43
|
end
|
|
40
44
|
end
|
|
@@ -43,16 +47,11 @@ module Aspera
|
|
|
43
47
|
# The resulting sync_info has `args` format only if it contains one of the `sessions` or `instance` keys.
|
|
44
48
|
# It has the `conf` format (default) otherwise.
|
|
45
49
|
# If the `conf` format is detected, then both `local` and `remote` keys are set.
|
|
46
|
-
# @param path [String
|
|
47
|
-
# @param sync_info [Hash
|
|
48
|
-
# @param direction [Symbol,NilClass]
|
|
50
|
+
# @param path [String] local/remote path
|
|
51
|
+
# @param sync_info [Hash] extra sync info (may be empty)
|
|
52
|
+
# @param direction [Symbol,NilClass] one of DIRECTIONS, or nil for admin commands
|
|
49
53
|
# @return [Hash] sync info
|
|
50
|
-
def async_info_from_args(path
|
|
51
|
-
if path.nil?
|
|
52
|
-
path = options.get_next_argument('path')
|
|
53
|
-
sync_info = options.get_next_argument('sync info', mandatory: false, validation: Hash, default: {}, schema: Schema::Registry::SYNC_CONF)
|
|
54
|
-
end
|
|
55
|
-
sync_info ||= {}
|
|
54
|
+
def async_info_from_args(path:, sync_info: {}, direction: nil)
|
|
56
55
|
# is the positional path a remote path ?
|
|
57
56
|
path_is_remote = direction.eql?(:pull)
|
|
58
57
|
if sync_info.key?('sessions') || sync_info.key?('instance')
|
|
@@ -61,33 +60,33 @@ module Aspera
|
|
|
61
60
|
Aspera.assert(sync_info['sessions'].length == 1, 'Only one session is supported')
|
|
62
61
|
session = sync_info['sessions'].first
|
|
63
62
|
dir_key = path_is_remote ? 'remote_dir' : 'local_dir'
|
|
64
|
-
Aspera.assert(!session.key?(dir_key)){"Parameter #{dir_key} shall not be in sync_info"}
|
|
63
|
+
Aspera.assert(!session.key?(dir_key)) { "Parameter #{dir_key} shall not be in sync_info" }
|
|
65
64
|
session[dir_key] = path
|
|
66
65
|
if direction
|
|
67
66
|
dir_key = path_is_remote ? 'local_dir' : 'remote_dir'
|
|
68
|
-
Aspera.assert(!session.key?(dir_key)){"Parameter #{dir_key} shall not be in sync_info"}
|
|
67
|
+
Aspera.assert(!session.key?(dir_key)) { "Parameter #{dir_key} shall not be in sync_info" }
|
|
69
68
|
session[dir_key] = transfer.destination_folder(path_is_remote ? Transfer::Spec::DIRECTION_RECEIVE : Transfer::Spec::DIRECTION_SEND)
|
|
70
|
-
local_remote = %w[local remote].map{ |i| session["#{i}_dir"]}
|
|
69
|
+
local_remote = %w[local remote].map { |i| session["#{i}_dir"] }
|
|
71
70
|
end
|
|
72
71
|
else
|
|
73
72
|
# `conf`
|
|
74
73
|
session = sync_info
|
|
75
74
|
dir_key = path_is_remote ? 'remote' : 'local'
|
|
76
75
|
session[dir_key] ||= {}
|
|
77
|
-
Aspera.assert(!session[dir_key].key?('path')){"Parameter #{dir_key}.path shall not be in sync_info"}
|
|
76
|
+
Aspera.assert(!session[dir_key].key?('path')) { "Parameter #{dir_key}.path shall not be in sync_info" }
|
|
78
77
|
session[dir_key]['path'] = path
|
|
79
78
|
if direction
|
|
80
79
|
dir_key = path_is_remote ? 'local' : 'remote'
|
|
81
80
|
session[dir_key] ||= {}
|
|
82
|
-
Aspera.assert(!session[dir_key].key?('path')){"Parameter #{dir_key}.path shall not be in sync_info"}
|
|
81
|
+
Aspera.assert(!session[dir_key].key?('path')) { "Parameter #{dir_key}.path shall not be in sync_info" }
|
|
83
82
|
session[dir_key]['path'] = transfer.destination_folder(path_is_remote ? Transfer::Spec::DIRECTION_RECEIVE : Transfer::Spec::DIRECTION_SEND)
|
|
84
|
-
local_remote = %w[local remote].map{ |i| session[i]['path']}
|
|
83
|
+
local_remote = %w[local remote].map { |i| session[i]['path'] }
|
|
85
84
|
end
|
|
86
85
|
# `conf` is quiet by default
|
|
87
86
|
session['quiet'] = false if !session.key?('quiet') && Environment.terminal?
|
|
88
87
|
end
|
|
89
88
|
if direction
|
|
90
|
-
Aspera.assert(!session.key?('direction'), type: BadArgument){'direction shall not be in sync_info'}
|
|
89
|
+
Aspera.assert(!session.key?('direction'), type: BadArgument) { 'direction shall not be in sync_info' }
|
|
91
90
|
session['direction'] = direction.to_s
|
|
92
91
|
# generate name if not provided by user
|
|
93
92
|
if !session.key?('name')
|
|
@@ -105,16 +104,16 @@ module Aspera
|
|
|
105
104
|
end
|
|
106
105
|
|
|
107
106
|
# Provide database object from path + sync_info for admin ops
|
|
108
|
-
# @param path
|
|
109
|
-
# @param sync_info [Hash
|
|
110
|
-
def db_from_args(path
|
|
107
|
+
# @param path [String] local path
|
|
108
|
+
# @param sync_info [Hash] extra sync info (may be empty)
|
|
109
|
+
def db_from_args(path:, sync_info: {})
|
|
111
110
|
sync_info = async_info_from_args(path: path, sync_info: sync_info)
|
|
112
111
|
session = sync_info.key?('sessions') ? sync_info['sessions'].first : sync_info
|
|
113
112
|
# if name not provided, check in db folder if there is only one name
|
|
114
113
|
if !session.key?('name')
|
|
115
114
|
local_db_dir = Sync::Operations.local_db_folder(sync_info)
|
|
116
115
|
dbs = Sync::Operations.list_db_files(local_db_dir)
|
|
117
|
-
Aspera.assert(dbs.length == 1){"#{dbs.length} session found in #{local_db_dir}, please provide a name"}
|
|
116
|
+
Aspera.assert(dbs.length == 1) { "#{dbs.length} session found in #{local_db_dir}, please provide a name" }
|
|
118
117
|
session['name'] = dbs.keys.first
|
|
119
118
|
end
|
|
120
119
|
Sync::Database.new(Sync::Operations.session_db_file(sync_info))
|
|
@@ -126,7 +125,7 @@ module Aspera
|
|
|
126
125
|
|
|
127
126
|
def action_sync_admin_find(path:, **)
|
|
128
127
|
dbs = Sync::Operations.list_db_files(path)
|
|
129
|
-
Result::ObjectList.new(dbs.keys.map{ |n| {name: n, path: dbs[n]}})
|
|
128
|
+
Result::ObjectList.new(dbs.keys.map { |n| {name: n, path: dbs[n]} })
|
|
130
129
|
end
|
|
131
130
|
|
|
132
131
|
def action_sync_admin_meta(path:, sync_info: {}, **)
|
|
@@ -159,10 +158,12 @@ module Aspera
|
|
|
159
158
|
end
|
|
160
159
|
|
|
161
160
|
# Execute a sync transfer for a given direction.
|
|
162
|
-
# @param direction [Symbol]
|
|
161
|
+
# @param direction [Symbol] one of Sync::Operations::DIRECTIONS (:push, :pull, :bidi)
|
|
162
|
+
# @param path [String] local or remote path (depends on direction)
|
|
163
|
+
# @param sync_info [Hash] extra sync configuration (may be empty)
|
|
163
164
|
# @param block [Proc, nil] block to generate transfer spec; receives (direction, local_dir, remote_dir)
|
|
164
|
-
def run_sync_transfer(direction, &block)
|
|
165
|
-
Sync::Operations.start(async_info_from_args(direction: direction), transfer.user_transfer_spec, &block)
|
|
165
|
+
def run_sync_transfer(direction, path:, sync_info: {}, &block)
|
|
166
|
+
Sync::Operations.start(async_info_from_args(path: path, sync_info: sync_info, direction: direction), transfer.user_transfer_spec, &block)
|
|
166
167
|
Result::Success.new
|
|
167
168
|
end
|
|
168
169
|
end
|
|
@@ -29,13 +29,13 @@ module Aspera
|
|
|
29
29
|
# Format special values with colors (dim for empty, reverse for others)
|
|
30
30
|
def special_format(what)
|
|
31
31
|
result = "<#{what}>"
|
|
32
|
-
return %w[null empty].any?{ |s| what.include?(s)} ? result.dim : result.reverse_color
|
|
32
|
+
return %w[null empty].any? { |s| what.include?(s) } ? result.dim : result.reverse_color
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# Prepare table row for terminal display (word wrap arrays)
|
|
36
36
|
def check_row(row)
|
|
37
37
|
row.each_key do |k|
|
|
38
|
-
row[k] = row[k].map{ |i| WordWrap.ww(i.to_s, 120).chomp}.join("\n") if row[k].is_a?(Array)
|
|
38
|
+
row[k] = row[k].map { |i| WordWrap.ww(i.to_s, 120).chomp }.join("\n") if row[k].is_a?(Array)
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
|
|
@@ -24,7 +24,10 @@ module Aspera
|
|
|
24
24
|
def action_transfer_status(job_id:, **)
|
|
25
25
|
store = async_transfer_store
|
|
26
26
|
entry = store.read(job_id)
|
|
27
|
-
Aspera.assert(!entry.nil?, type: Cli::BadArgument){"Unknown job_id: #{job_id}"}
|
|
27
|
+
Aspera.assert(!entry.nil?, type: Cli::BadArgument) { "Unknown job_id: #{job_id}" }
|
|
28
|
+
# Inject the in-process agent reference (direct/httpgw) if still alive in this process.
|
|
29
|
+
ref = store.agent_ref(job_id)
|
|
30
|
+
entry['agent_params']['_agent_ref'] = ref if ref
|
|
28
31
|
live = query_live_status(entry)
|
|
29
32
|
if live
|
|
30
33
|
entry.merge!(live)
|
|
@@ -46,7 +49,7 @@ module Aspera
|
|
|
46
49
|
def action_transfer_cleanup(**)
|
|
47
50
|
store = async_transfer_store
|
|
48
51
|
deleted = store.list
|
|
49
|
-
.select{ |e| TERMINAL_STATUSES.include?(e['status'])}
|
|
52
|
+
.select { |e| TERMINAL_STATUSES.include?(e['status']) }
|
|
50
53
|
.map do |e|
|
|
51
54
|
store.delete(e['job_id'])
|
|
52
55
|
e['job_id']
|
|
@@ -57,25 +60,29 @@ module Aspera
|
|
|
57
60
|
private
|
|
58
61
|
|
|
59
62
|
# Lazy accessor - requires context.persistency (available in all Base sub-classes).
|
|
63
|
+
# Delegate to the TransferAgent's store so that the in-memory agent-ref
|
|
64
|
+
# registry is shared between the two components in the same process.
|
|
60
65
|
def async_transfer_store
|
|
61
|
-
|
|
66
|
+
context.transfer.async_store
|
|
62
67
|
end
|
|
63
68
|
|
|
64
69
|
# Delegate to the appropriate agent class method.
|
|
65
|
-
#
|
|
70
|
+
# All agent types are supported:
|
|
71
|
+
# - in-process agents (direct, httpgw): re-queryable while the same process lives
|
|
72
|
+
# (e.g. MCP mode); '_agent_ref' in agent_params points to the live instance.
|
|
73
|
+
# When the process has restarted, '_agent_ref' is nil and transfer_status returns
|
|
74
|
+
# {'status' => 'unknown'} — which is the correct and safe answer.
|
|
75
|
+
# - daemon agents (node, desktop, connect, transferd): re-query via their REST/IPC API.
|
|
66
76
|
def query_live_status(entry)
|
|
67
|
-
agent_type
|
|
68
|
-
transfer_id
|
|
77
|
+
agent_type = entry['agent_type']
|
|
78
|
+
transfer_id = entry['transfer_id']
|
|
69
79
|
agent_params = entry['agent_params'] || {}
|
|
70
80
|
|
|
71
|
-
# direct transfers live in Ruby threads - not persistable across process restarts
|
|
72
|
-
return if agent_type.eql?('direct')
|
|
73
|
-
|
|
74
81
|
require "aspera/agent/#{agent_type}"
|
|
75
82
|
agent_class = Aspera::Agent.const_get(agent_type.capitalize)
|
|
76
83
|
agent_class.transfer_status(transfer_id, agent_params)
|
|
77
84
|
rescue StandardError => e
|
|
78
|
-
Log.log.warn{"Could not re-query #{entry['agent_type']} agent for job #{entry['job_id']}: #{e}"}
|
|
85
|
+
Log.log.warn { "Could not re-query #{entry['agent_type']} agent for job #{entry['job_id']}: #{e}" }
|
|
79
86
|
nil
|
|
80
87
|
end
|
|
81
88
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'aspera/agent/factory'
|
|
4
4
|
require 'aspera/schema/registry'
|
|
5
|
+
require 'aspera/cli/option_declarator'
|
|
5
6
|
require 'aspera/transfer/result'
|
|
6
7
|
require 'aspera/transfer/spec'
|
|
7
8
|
require 'aspera/cli/info'
|
|
@@ -35,23 +36,19 @@ module Aspera
|
|
|
35
36
|
:FILE_LIST_OPTIONS,
|
|
36
37
|
:DEFAULT_TRANSFER_NOTIFY_TEMPLATE
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
options.declare(:transfer, description: 'Transfer agent type, or agent parameters with optional agent key', allowed: [Hash, String], schema: Schema::Registry::TRANSFER_AGENT_OPTIONS)
|
|
47
|
-
options.declare(:transfer_info, description: 'Parameters for transfer agent', allowed: Hash, deprecation: 'use --transfer instead', schema: Schema::Registry::TRANSFER_AGENT_OPTIONS)
|
|
48
|
-
end
|
|
49
|
-
end
|
|
39
|
+
extend OptionDeclarator
|
|
40
|
+
|
|
41
|
+
option :ts, description: 'Override transfer spec values', schema: Schema::Registry::TRANSFER_SPEC
|
|
42
|
+
option :to_folder, description: 'Destination folder for transferred files'
|
|
43
|
+
option :sources, description: "How list of transferred files is provided (#{FILE_LIST_OPTIONS.join(',')})", default: FILE_LIST_FROM_ARGS
|
|
44
|
+
option :src_type, description: 'Type of file list', allowed: %i[list pair], default: :list
|
|
45
|
+
option :transfer, description: 'Transfer agent type, or agent parameters with optional agent key', allowed: [Hash, String], schema: Schema::Registry::TRANSFER_AGENT_OPTIONS
|
|
46
|
+
option :transfer_info, description: 'Parameters for transfer agent', deprecation: 'use --transfer instead', schema: Schema::Registry::TRANSFER_AGENT_OPTIONS
|
|
50
47
|
|
|
51
48
|
# @param context [Context] Application context
|
|
52
49
|
def initialize(context)
|
|
53
|
-
Aspera.assert_type(context, Context){'context'}
|
|
54
|
-
Aspera.assert_type(context.options, Parser){'context.options'}
|
|
50
|
+
Aspera.assert_type(context, Context) { 'context' }
|
|
51
|
+
Aspera.assert_type(context.options, Parser) { 'context.options' }
|
|
55
52
|
@context = context
|
|
56
53
|
# Command line can override transfer spec
|
|
57
54
|
@user_transfer_spec = {
|
|
@@ -120,13 +117,13 @@ module Aspera
|
|
|
120
117
|
when :node
|
|
121
118
|
if !agent_options.key?(:url)
|
|
122
119
|
param_set_name = @context.presets.plugin_default_name(:node)
|
|
123
|
-
Aspera.assert(!param_set_name.nil?, type: Cli::BadArgument){"No default node configured. Please specify #{Options.option_name_to_line(:transfer)}.url or #{Options.option_name_to_line(:transfer)}"}
|
|
120
|
+
Aspera.assert(!param_set_name.nil?, type: Cli::BadArgument) { "No default node configured. Please specify #{Options.option_name_to_line(:transfer)}.url or #{Options.option_name_to_line(:transfer)}" }
|
|
124
121
|
agent_options.merge!(@context.presets.by_name(param_set_name).symbolize_keys)
|
|
125
122
|
end
|
|
126
123
|
when :direct
|
|
127
124
|
# by default do not display ascp native progress bar
|
|
128
125
|
agent_options[:quiet] = true unless agent_options.key?(:quiet)
|
|
129
|
-
agent_options[:check_ignore_cb] = ->(host, port){@context.http_config.ignore_cert?(host, port)}
|
|
126
|
+
agent_options[:check_ignore_cb] = ->(host, port) { @context.http_config.ignore_cert?(host, port) }
|
|
130
127
|
# JRuby
|
|
131
128
|
agent_options[:trusted_certs] = @context.http_config.trusted_cert_locations unless agent_options.key?(:trusted_certs)
|
|
132
129
|
when :httpgw
|
|
@@ -137,7 +134,7 @@ module Aspera
|
|
|
137
134
|
end
|
|
138
135
|
# get agent instance
|
|
139
136
|
self.agent_instance = Agent::Factory.instance.create(agent_type, agent_options)
|
|
140
|
-
Log.log.debug{"transfer agent is a #{@agent.class}"}
|
|
137
|
+
Log.log.debug { "transfer agent is a #{@agent.class}" }
|
|
141
138
|
return @agent
|
|
142
139
|
end
|
|
143
140
|
|
|
@@ -168,7 +165,7 @@ module Aspera
|
|
|
168
165
|
|
|
169
166
|
# @param httpgw_url_proc [Proc]
|
|
170
167
|
def httpgw_url_cb=(httpgw_url_proc)
|
|
171
|
-
Aspera.assert_type(httpgw_url_proc, Proc){'httpgw_url_cb'}
|
|
168
|
+
Aspera.assert_type(httpgw_url_proc, Proc) { 'httpgw_url_cb' }
|
|
172
169
|
@httpgw_url_lambda = httpgw_url_proc
|
|
173
170
|
end
|
|
174
171
|
|
|
@@ -180,10 +177,10 @@ module Aspera
|
|
|
180
177
|
case source_type
|
|
181
178
|
when :list
|
|
182
179
|
# when providing a list, just specify source
|
|
183
|
-
file_list.map{ |i| {'source' => i}}
|
|
180
|
+
file_list.map { |i| {'source' => i} }
|
|
184
181
|
when :pair
|
|
185
|
-
Aspera.assert(file_list.length.even?, type: Cli::BadArgument){"When using pair, provide an even number of paths: #{file_list.length}"}
|
|
186
|
-
file_list.each_slice(2).map{ |s, d| {'source' => s, 'destination' => d}}
|
|
182
|
+
Aspera.assert(file_list.length.even?, type: Cli::BadArgument) { "When using pair, provide an even number of paths: #{file_list.length}" }
|
|
183
|
+
file_list.each_slice(2).map { |s, d| {'source' => s, 'destination' => d} }
|
|
187
184
|
else Aspera.error_unexpected_value(source_type)
|
|
188
185
|
end
|
|
189
186
|
end
|
|
@@ -215,16 +212,16 @@ module Aspera
|
|
|
215
212
|
special_case_direct_with_list =
|
|
216
213
|
(@transfer_options['agent'] || :direct).to_sym.eql?(:direct) &&
|
|
217
214
|
Transfer::Parameters.ascp_args_file_list?(@transfer_options['ascp_args'])
|
|
218
|
-
Aspera.assert(!@transfer_paths.nil? || special_case_direct_with_list, type: Cli::BadArgument){'transfer spec on command line must have sources'}
|
|
215
|
+
Aspera.assert(!@transfer_paths.nil? || special_case_direct_with_list, type: Cli::BadArgument) { 'transfer spec on command line must have sources' }
|
|
219
216
|
# can be nil
|
|
220
217
|
@transfer_paths
|
|
221
218
|
when Array
|
|
222
219
|
Log.log.debug('getting file list as extended value')
|
|
223
|
-
Aspera.assert_array_all(sources, String, type: Cli::BadArgument){'sources must be a Array of String'}
|
|
220
|
+
Aspera.assert_array_all(sources, String, type: Cli::BadArgument) { 'sources must be a Array of String' }
|
|
224
221
|
list_to_paths(sources)
|
|
225
|
-
else Aspera.error_unexpected_value(sources){'sources'}
|
|
222
|
+
else Aspera.error_unexpected_value(sources) { 'sources' }
|
|
226
223
|
end
|
|
227
|
-
Log.
|
|
224
|
+
Log.dump(:paths, @transfer_paths)
|
|
228
225
|
return @transfer_paths
|
|
229
226
|
end
|
|
230
227
|
|
|
@@ -233,7 +230,7 @@ module Aspera
|
|
|
233
230
|
# @param rest_token [Rest, nil] if oauth token regeneration supported
|
|
234
231
|
def start(transfer_spec, rest_token: nil)
|
|
235
232
|
# check parameters
|
|
236
|
-
Aspera.assert_type(transfer_spec, Hash){'transfer_spec'}
|
|
233
|
+
Aspera.assert_type(transfer_spec, Hash) { 'transfer_spec' }
|
|
237
234
|
raise "Wrong remote host: #{CP4I_REMOTE_HOST_LB}" if transfer_spec['remote_host'].eql?(CP4I_REMOTE_HOST_LB)
|
|
238
235
|
# process :src option
|
|
239
236
|
case transfer_spec['direction']
|
|
@@ -261,7 +258,7 @@ module Aspera
|
|
|
261
258
|
# resolve pseudo-parameter: target_rate -> target_rate_kbps (overrides target_rate_kbps if both are present)
|
|
262
259
|
transfer_spec['target_rate_kbps'] = Transfer::Spec.rate_string_to_kbps(transfer_spec.delete('target_rate')) if transfer_spec.key?('target_rate')
|
|
263
260
|
# recursively remove values that are nil (user wants to delete)
|
|
264
|
-
transfer_spec.deep_do{ |hash, key, value, _unused| hash.delete(key) if value.nil?}
|
|
261
|
+
transfer_spec.deep_do { |hash, key, value, _unused| hash.delete(key) if value.nil? }
|
|
265
262
|
# if TS from app has content_protection (e.g. F5), that means content is protected: ask password if not provided
|
|
266
263
|
transfer_spec['content_protection_password'] = @context.options.prompt_user_input('content protection password', sensitive: true) if transfer_spec['content_protection'].eql?('decrypt') && !transfer_spec.key?('content_protection_password')
|
|
267
264
|
# create transfer agent
|
|
@@ -269,31 +266,36 @@ module Aspera
|
|
|
269
266
|
# --- async mode ---
|
|
270
267
|
if @transfer_options['asynchronous']
|
|
271
268
|
agent_type = (@transfer_options['agent'] || 'direct').to_s
|
|
272
|
-
|
|
273
|
-
#
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
job_id = agent_instance.instance_variable_get(:@sessions).last[:job_id]
|
|
277
|
-
Log.log.info{"Async direct transfer started: job_id=#{job_id}"}
|
|
278
|
-
return Transfer::Result.async(job_id: job_id)
|
|
279
|
-
end
|
|
280
|
-
# For remote-daemon agents (desktop, node, connect, transferd): persist transfer_id
|
|
281
|
-
# so the status can be re-queried in a later process invocation.
|
|
269
|
+
# In-process agents (direct, httpgw) expose last_job_id directly.
|
|
270
|
+
# Remote-daemon agents do not override last_job_id (returns nil): generate a UUID.
|
|
271
|
+
job_id = agent_instance.last_job_id || SecureRandom.uuid
|
|
272
|
+
# Base agent_params from transfer options (strip internal keys)
|
|
282
273
|
agent_params = @transfer_options.except('agent', 'asynchronous')
|
|
274
|
+
# For daemon agents: capture any runtime-resolved connection detail
|
|
283
275
|
case agent_type
|
|
284
276
|
when 'desktop'
|
|
285
277
|
agent_params['application_id'] = agent_instance.application_id
|
|
286
278
|
when 'connect'
|
|
287
279
|
agent_params['app_id'] = agent_instance.app_id
|
|
288
280
|
when 'transferd'
|
|
289
|
-
# store the resolved daemon endpoint (auto-port may have been assigned)
|
|
290
281
|
agent_params['url'] = agent_instance.daemon_endpoint
|
|
291
282
|
end
|
|
292
|
-
|
|
283
|
+
# Register an in-process agent reference for direct/httpgw agents so that
|
|
284
|
+
# config transfer status can re-query them while the same process is alive
|
|
285
|
+
# (e.g. MCP server mode). The ref is stored in AsyncTransferStore's memory map,
|
|
286
|
+
# never on disk.
|
|
287
|
+
async_store.register_agent_ref(job_id, agent_instance) if %w[direct httpgw].include?(agent_type)
|
|
288
|
+
# transfer_id: in-process agents use job_id; daemon agents expose @transfer_id
|
|
289
|
+
transfer_id =
|
|
290
|
+
if %w[direct httpgw].include?(agent_type)
|
|
291
|
+
job_id
|
|
292
|
+
else
|
|
293
|
+
agent_instance.instance_variable_get(:@transfer_id).to_s
|
|
294
|
+
end
|
|
293
295
|
async_store.write(job_id, {
|
|
294
296
|
'job_id' => job_id,
|
|
295
297
|
'agent_type' => agent_type,
|
|
296
|
-
'transfer_id' =>
|
|
298
|
+
'transfer_id' => transfer_id,
|
|
297
299
|
'agent_params' => agent_params,
|
|
298
300
|
'status' => 'running',
|
|
299
301
|
'bytes_transferred' => 0,
|
|
@@ -301,7 +303,7 @@ module Aspera
|
|
|
301
303
|
'ended_at' => nil,
|
|
302
304
|
'error' => nil
|
|
303
305
|
})
|
|
304
|
-
Log.log.info{"Async transfer started: job_id=#{job_id}"}
|
|
306
|
+
Log.log.info { "Async transfer started: job_id=#{job_id}, agent=#{agent_type}" }
|
|
305
307
|
return Transfer::Result.async(job_id: job_id)
|
|
306
308
|
end
|
|
307
309
|
# --- synchronous mode (default) ---
|
|
@@ -315,9 +317,7 @@ module Aspera
|
|
|
315
317
|
@agent.shutdown if @agent.respond_to?(:shutdown)
|
|
316
318
|
end
|
|
317
319
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
# Lazy accessor for the async transfer store (only created when needed)
|
|
320
|
+
# Lazy accessor for the async transfer store (shared with TransferActions via context.transfer).
|
|
321
321
|
def async_store
|
|
322
322
|
@async_store ||= AsyncTransferStore.new(@context.persistency)
|
|
323
323
|
end
|
|
@@ -29,12 +29,12 @@ module Aspera
|
|
|
29
29
|
# @param type [Symbol] one of: sessions_init, session_start, session_size, transfer, session_end and end
|
|
30
30
|
# @param info [Object, nil] optional specific additional info for the given event type
|
|
31
31
|
def event(type, session_id: nil, info: nil)
|
|
32
|
-
Log.log.trace1{"progress: #{type} #{session_id} #{info}"}
|
|
32
|
+
Log.log.trace1 { "progress: #{type} #{session_id} #{info}" }
|
|
33
33
|
return if @completed
|
|
34
34
|
if @progress_bar.nil?
|
|
35
35
|
@progress_bar = ProgressBar.create(
|
|
36
36
|
format: '%t %a %B %p%% %r Mbps %E',
|
|
37
|
-
rate_scale: lambda{ |rate| rate / Environment::BYTES_PER_MEBIBIT},
|
|
37
|
+
rate_scale: lambda { |rate| rate / Environment::BYTES_PER_MEBIBIT },
|
|
38
38
|
title: '',
|
|
39
39
|
total: nil
|
|
40
40
|
)
|
|
@@ -50,7 +50,7 @@ module Aspera
|
|
|
50
50
|
when :session_start
|
|
51
51
|
Aspera.assert_type(session_id, String)
|
|
52
52
|
Aspera.assert(info.nil?, 'info must be nil for :session_start event')
|
|
53
|
-
Aspera.assert(!@sessions[session_id]){"Session #{session_id} already started"}
|
|
53
|
+
Aspera.assert(!@sessions[session_id]) { "Session #{session_id} already started" }
|
|
54
54
|
@sessions[session_id] = {
|
|
55
55
|
job_size: 0, # Total size of transfer (pre-calc)
|
|
56
56
|
current: 0,
|
|
@@ -83,19 +83,19 @@ module Aspera
|
|
|
83
83
|
Aspera.assert(session_id.nil?, 'session_id must be nil for :end event')
|
|
84
84
|
Aspera.assert(info.nil?, 'info must be nil for :end event')
|
|
85
85
|
@progress_bar.finish
|
|
86
|
-
else Aspera.error_unexpected_value(type){'event type'}
|
|
86
|
+
else Aspera.error_unexpected_value(type) { 'event type' }
|
|
87
87
|
end
|
|
88
|
-
new_title = @sessions.length < 2 ? @title.to_s : "[#{@sessions.count{ |_i, d| d[:running]}}] #{@title}"
|
|
88
|
+
new_title = @sessions.length < 2 ? @title.to_s : "[#{@sessions.count { |_i, d| d[:running] }}] #{@title}"
|
|
89
89
|
@progress_bar&.title = new_title unless @progress_bar&.title.eql?(new_title)
|
|
90
90
|
@progress_bar&.increment if !progress_provided && @progress_bar.progress.nil?
|
|
91
91
|
rescue ProgressBar::InvalidProgressError => e
|
|
92
|
-
Log.log.error{"Progress error: #{e}"}
|
|
92
|
+
Log.log.error { "Progress error: #{e}" }
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
private
|
|
96
96
|
|
|
97
97
|
def total(key)
|
|
98
|
-
@sessions.values.inject(0){ |m, s| m + s[key]}
|
|
98
|
+
@sessions.values.inject(0) { |m, s| m + s[key] }
|
|
99
99
|
end
|
|
100
100
|
end
|
|
101
101
|
end
|
|
@@ -3,46 +3,68 @@
|
|
|
3
3
|
require 'aspera/cli/error'
|
|
4
4
|
require 'aspera/log'
|
|
5
5
|
require 'aspera/assert'
|
|
6
|
+
require 'json'
|
|
6
7
|
|
|
7
8
|
module Aspera
|
|
8
9
|
module Cli
|
|
9
10
|
# Mixin providing vault/keychain functionality to Plugin::Config.
|
|
10
11
|
# Depends on `options` and `context.main_folder` being available in the including class.
|
|
11
12
|
module VaultManager
|
|
12
|
-
def action_vault_show(label:, **)
|
|
13
|
-
|
|
13
|
+
def action_vault_show(label:, id: nil, **)
|
|
14
|
+
v = vault_required
|
|
15
|
+
kwargs = id && v.method(:get).parameters.any? { |_t, n| n == :id } ? {id: id} : {}
|
|
16
|
+
Result::SingleObject.new(v.get(label: label, **kwargs))
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def action_vault_create(info:, **)
|
|
17
|
-
|
|
20
|
+
vault_required.set(info.symbolize_keys)
|
|
18
21
|
Result::Status.new('Secret added')
|
|
19
22
|
end
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
# Import secrets from a JSON array; skips entries missing :label
|
|
25
|
+
def action_vault_import(secrets:, **)
|
|
26
|
+
bulk_result(secrets, command: :import, id_result: 'label') do |entry|
|
|
27
|
+
vault_required.set(entry.symbolize_keys)
|
|
28
|
+
{'label' => entry['label'] || entry[:label]}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def action_vault_delete(label:, id: nil, **)
|
|
33
|
+
v = vault_required
|
|
34
|
+
kwargs = id && v.method(:delete).parameters.any? { |_t, n| n == :id } ? {id: id} : {}
|
|
35
|
+
v.delete(label: label, **kwargs)
|
|
23
36
|
Result::Status.new("Secret deleted: #{label}")
|
|
24
37
|
end
|
|
25
38
|
|
|
26
39
|
def action_vault_password(new_password:, **)
|
|
27
|
-
Aspera.assert(
|
|
28
|
-
|
|
40
|
+
Aspera.assert(vault_required.respond_to?(:change_password), 'Vault does not support password change')
|
|
41
|
+
vault_required.change_password(new_password)
|
|
29
42
|
Result::Status.new('Vault password updated')
|
|
30
43
|
end
|
|
31
44
|
|
|
45
|
+
# @return [Keychain::Base] vault instance, raises if not configured
|
|
46
|
+
def vault_required
|
|
47
|
+
Aspera.assert(!vault.nil?, type: Cli::BadArgument) { 'Missing mandatory option: vault' }
|
|
48
|
+
vault
|
|
49
|
+
end
|
|
50
|
+
|
|
32
51
|
# @return [String] value from vault matching <name>.<param>
|
|
33
52
|
def vault_value(name)
|
|
34
53
|
m = name.split('.')
|
|
35
|
-
Aspera.assert(m.length.eql?(2), type: BadArgument){'vault name shall match <name>.<param>'}
|
|
36
|
-
info =
|
|
54
|
+
Aspera.assert(m.length.eql?(2), type: BadArgument) { 'vault name shall match <name>.<param>' }
|
|
55
|
+
info = vault_required.get(label: m[0])
|
|
37
56
|
value = info[m[1].to_sym]
|
|
38
57
|
raise "no such entry value: #{m[1]}" if value.nil?
|
|
39
58
|
return value
|
|
40
59
|
end
|
|
41
60
|
|
|
42
|
-
# @return [Keychain::Base] vault instance, lazily created from options
|
|
61
|
+
# @return [Keychain::Base, nil] vault instance, lazily created from options, or nil if not configured
|
|
43
62
|
def vault
|
|
44
|
-
return @vault_instance
|
|
45
|
-
|
|
63
|
+
return @vault_instance if @vault_instance_initialized
|
|
64
|
+
@vault_instance_initialized = true
|
|
65
|
+
info = options.get_option(:vault, mandatory: false)
|
|
66
|
+
return @vault_instance = nil if info.nil? || (info.is_a?(Hash) && info.empty?)
|
|
67
|
+
info = info.symbolize_keys
|
|
46
68
|
info[:type] ||= 'file'
|
|
47
69
|
require 'aspera/keychain/factory'
|
|
48
70
|
@vault_instance = Keychain::Factory.create(
|