aspera-cli 4.26.2 → 4.27.1
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 +310 -21
- data/CONTRIBUTING.md +99 -14
- data/TODO.md +50 -0
- data/bin/ascli +3 -3
- data/docs/README.md +10977 -0
- data/docs/test-mcp-with-ai.md +202 -0
- data/lib/aspera/agent/base.rb +29 -7
- data/lib/aspera/agent/connect.rb +58 -21
- data/lib/aspera/agent/desktop.rb +53 -18
- data/lib/aspera/agent/direct.rb +71 -40
- data/lib/aspera/agent/factory.rb +3 -3
- data/lib/aspera/agent/httpgw.rb +55 -14
- data/lib/aspera/agent/node.rb +34 -2
- data/lib/aspera/agent/transferd.rb +48 -12
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +42 -44
- data/lib/aspera/api/cos_node.rb +10 -9
- data/lib/aspera/api/faspex.rb +16 -15
- data/lib/aspera/api/httpgw.rb +23 -23
- data/lib/aspera/api/node.rb +51 -70
- 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 +32 -30
- data/lib/aspera/ascp/installation.rb +75 -69
- data/lib/aspera/ascp/management.rb +7 -8
- data/lib/aspera/assert.rb +12 -13
- data/lib/aspera/cli/ascp_actions.rb +155 -0
- data/lib/aspera/cli/async_transfer_store.rb +125 -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 +141 -0
- data/lib/aspera/cli/context.rb +19 -6
- data/lib/aspera/cli/error.rb +16 -0
- data/lib/aspera/cli/extended_value.rb +27 -26
- data/lib/aspera/cli/formatter.rb +95 -59
- data/lib/aspera/cli/gem_checker.rb +65 -0
- data/lib/aspera/cli/hints.rb +3 -3
- data/lib/aspera/cli/http.rb +74 -30
- data/lib/aspera/cli/info.rb +2 -0
- data/lib/aspera/cli/mailer.rb +97 -0
- data/lib/aspera/cli/mcp_tool.rb +225 -0
- data/lib/aspera/cli/option_declarator.rb +77 -0
- data/lib/aspera/cli/options.schema.yaml +634 -7
- data/lib/aspera/cli/parser.rb +1217 -0
- data/lib/aspera/cli/plugins/alee.rb +20 -22
- data/lib/aspera/cli/plugins/aoc.rb +1178 -881
- data/lib/aspera/cli/plugins/ats.rb +205 -162
- data/lib/aspera/cli/plugins/base.rb +703 -175
- data/lib/aspera/cli/plugins/basic_auth.rb +7 -9
- data/lib/aspera/cli/plugins/config.rb +434 -757
- data/lib/aspera/cli/plugins/console.rb +107 -65
- data/lib/aspera/cli/plugins/cos.rb +46 -33
- data/lib/aspera/cli/plugins/factory.rb +6 -6
- data/lib/aspera/cli/plugins/faspex5.rb +627 -388
- data/lib/aspera/cli/plugins/faspio.rb +54 -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 +988 -840
- data/lib/aspera/cli/plugins/oauth.rb +7 -10
- data/lib/aspera/cli/plugins/orchestrator.rb +112 -135
- data/lib/aspera/cli/plugins/preview.rb +195 -157
- data/lib/aspera/cli/plugins/server.rb +141 -91
- data/lib/aspera/cli/plugins/shares.rb +343 -110
- data/lib/aspera/cli/preset_actions.rb +159 -0
- data/lib/aspera/cli/preset_manager.rb +82 -39
- data/lib/aspera/cli/result.rb +78 -26
- data/lib/aspera/cli/runner.rb +323 -153
- 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 +89 -64
- data/lib/aspera/cli/terminal_formatter.rb +3 -3
- data/lib/aspera/cli/transfer_actions.rb +90 -0
- data/lib/aspera/cli/transfer_agent.rb +129 -64
- data/lib/aspera/cli/transfer_progress.rb +9 -9
- data/lib/aspera/cli/vault_manager.rb +79 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +28 -30
- data/lib/aspera/colors.rb +3 -3
- data/lib/aspera/command_line_builder.rb +34 -34
- data/lib/aspera/command_line_converter.rb +1 -1
- data/lib/aspera/coverage.rb +1 -2
- data/lib/aspera/data_repository.rb +2 -1
- data/lib/aspera/dot_container.rb +12 -12
- data/lib/aspera/environment.rb +30 -23
- data/lib/aspera/exec_spec.rb +13 -0
- data/lib/aspera/faspex_gw.rb +5 -5
- data/lib/aspera/faspex_postproc.rb +16 -10
- data/lib/aspera/graphql.rb +37 -0
- data/lib/aspera/hash_ext.rb +8 -2
- data/lib/aspera/json_rpc/client.rb +62 -0
- data/lib/aspera/json_rpc/version.rb +7 -0
- data/lib/aspera/keychain/base.rb +10 -3
- data/lib/aspera/keychain/encrypted_hash.rb +92 -17
- data/lib/aspera/keychain/factory.rb +20 -8
- 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 +82 -0
- data/lib/aspera/log.rb +29 -15
- data/lib/aspera/markdown.rb +90 -7
- data/lib/aspera/nagios.rb +8 -8
- data/lib/aspera/node_simulator.rb +35 -27
- data/lib/aspera/oauth/base.rb +9 -10
- data/lib/aspera/oauth/boot.rb +5 -5
- data/lib/aspera/oauth/factory.rb +12 -10
- data/lib/aspera/oauth/jwt.rb +9 -11
- data/lib/aspera/oauth/web.rb +6 -6
- data/lib/aspera/persistency_action_once.rb +12 -12
- data/lib/aspera/persistency_folder.rb +18 -11
- data/lib/aspera/preview/file_types.rb +5 -5
- data/lib/aspera/preview/generator.rb +52 -49
- data/lib/aspera/preview/options.rb +3 -2
- data/lib/aspera/preview/terminal.rb +5 -5
- data/lib/aspera/preview/utils.rb +66 -19
- data/lib/aspera/products/connect.rb +2 -2
- data/lib/aspera/products/desktop.rb +1 -1
- data/lib/aspera/products/other.rb +3 -3
- data/lib/aspera/products/transferd.rb +4 -4
- data/lib/aspera/proxy_auto_config.rb +15 -14
- data/lib/aspera/rest.rb +159 -151
- data/lib/aspera/rest_error_analyzer.rb +6 -6
- data/lib/aspera/rest_errors_aspera.rb +0 -10
- data/lib/aspera/rest_list.rb +11 -10
- 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 +1853 -510
- data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
- data/lib/aspera/schema/documentation.rb +65 -28
- data/lib/aspera/schema/reader.rb +136 -13
- data/lib/aspera/schema/registry.rb +60 -8
- data/lib/aspera/secret_hider.rb +12 -7
- data/lib/aspera/ssh.rb +64 -31
- data/lib/aspera/ssl.rb +5 -5
- data/lib/aspera/sync/conf.schema.yaml +2 -2
- data/lib/aspera/sync/database.rb +2 -2
- data/lib/aspera/sync/operations.rb +22 -25
- data/lib/aspera/temp_file_manager.rb +25 -5
- data/lib/aspera/timer_limiter.rb +1 -1
- data/lib/aspera/transfer/faux_file.rb +24 -11
- data/lib/aspera/transfer/parameters.rb +36 -34
- data/lib/aspera/transfer/result.rb +74 -0
- data/lib/aspera/transfer/resumer.rb +10 -10
- data/lib/aspera/transfer/spec.rb +18 -1
- data/lib/aspera/transfer/spec.schema.yaml +12 -3
- data/lib/aspera/uri_reader.rb +52 -15
- data/lib/aspera/web_auth.rb +8 -7
- data/lib/aspera/web_server_simple.rb +15 -12
- data/lib/aspera/yaml.rb +5 -4
- data.tar.gz.sig +0 -0
- metadata +34 -6
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/manager.rb +0 -766
- data/lib/aspera/cli/plugins/faspex.rb +0 -539
- data/lib/aspera/json_rpc.rb +0 -52
- data/lib/aspera/transfer/uri.rb +0 -56
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/log'
|
|
4
|
+
|
|
5
|
+
module Aspera
|
|
6
|
+
module Cli
|
|
7
|
+
# Resolves the secret (password) for a given URL + username pair.
|
|
8
|
+
# Injected into Context as :secret_finder so that any component
|
|
9
|
+
# (plugins, Api::AoC, ...) can look up secrets without going through Plugins::Config.
|
|
10
|
+
class SecretFinder
|
|
11
|
+
# Special value for the :secret option that triggers a preset lookup
|
|
12
|
+
PRESET_MAGIC = 'PRESET'
|
|
13
|
+
|
|
14
|
+
# @param options [Parser] CLI options manager (provides :secret)
|
|
15
|
+
# @param presets [PresetManager] preset resolver (provides #lookup_preset)
|
|
16
|
+
def initialize(options, presets)
|
|
17
|
+
@options = options
|
|
18
|
+
@presets = presets
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Return the secret for the given URL + username.
|
|
22
|
+
# If the :secret option equals 'PRESET', the preset store is searched for a
|
|
23
|
+
# matching url/username entry and its 'password' field is returned.
|
|
24
|
+
# @param url [String]
|
|
25
|
+
# @param username [String]
|
|
26
|
+
# @return [String, nil]
|
|
27
|
+
def lookup(url:, username:)
|
|
28
|
+
secret = @options.get_option(:secret)
|
|
29
|
+
if secret.eql?(PRESET_MAGIC)
|
|
30
|
+
conf = @presets.lookup_preset(url: url, username: username)
|
|
31
|
+
if conf.is_a?(Hash)
|
|
32
|
+
Log.log.debug { "Found preset #{conf} with URL and username" }
|
|
33
|
+
secret = conf['password']
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
secret
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -7,28 +7,51 @@ 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
|
|
22
|
+
# When a plugin class includes SyncActions, register the :sql option
|
|
23
|
+
# in that class's DSL registry so Base#initialize auto-declares it.
|
|
17
24
|
class << self
|
|
18
|
-
def
|
|
19
|
-
|
|
25
|
+
def included(base)
|
|
26
|
+
base.option(:sql, description: 'SQL suffix appended to sqlite3 queries for admin subcommands (e.g. WHERE clause)')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# DSL helper: register the 7 `sync admin` leaf commands under the given parent path.
|
|
30
|
+
# Called at class-load time from any plugin that includes SyncActions.
|
|
31
|
+
# @param base [Class] the plugin class (receiver of DSL methods)
|
|
32
|
+
# @param admin_path [Symbol, Array<Symbol>] path relative to current scope, e.g. :admin
|
|
33
|
+
def register_sync_admin_commands(base, admin_path)
|
|
34
|
+
base.commands_under(admin_path) do
|
|
35
|
+
base.command(:find, description: 'Find sync database files', arguments: [{name: :path, type: String}], action: :action_sync_admin_find)
|
|
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)
|
|
42
|
+
end
|
|
20
43
|
end
|
|
21
44
|
end
|
|
22
45
|
|
|
23
|
-
#
|
|
46
|
+
# Convert path + sync_info to internal `sync_info` format.
|
|
24
47
|
# The resulting sync_info has `args` format only if it contains one of the `sessions` or `instance` keys.
|
|
25
48
|
# It has the `conf` format (default) otherwise.
|
|
26
49
|
# If the `conf` format is detected, then both `local` and `remote` keys are set.
|
|
27
|
-
# @param
|
|
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
|
|
28
53
|
# @return [Hash] sync info
|
|
29
|
-
def async_info_from_args(direction: nil)
|
|
30
|
-
path = options.get_next_argument('path')
|
|
31
|
-
sync_info = options.get_next_argument('sync info', mandatory: false, validation: Hash, default: {}, schema: Schema::Registry::SYNC_CONF)
|
|
54
|
+
def async_info_from_args(path:, sync_info: {}, direction: nil)
|
|
32
55
|
# is the positional path a remote path ?
|
|
33
56
|
path_is_remote = direction.eql?(:pull)
|
|
34
57
|
if sync_info.key?('sessions') || sync_info.key?('instance')
|
|
@@ -37,33 +60,33 @@ module Aspera
|
|
|
37
60
|
Aspera.assert(sync_info['sessions'].length == 1, 'Only one session is supported')
|
|
38
61
|
session = sync_info['sessions'].first
|
|
39
62
|
dir_key = path_is_remote ? 'remote_dir' : 'local_dir'
|
|
40
|
-
|
|
63
|
+
Aspera.assert(!session.key?(dir_key)) { "Parameter #{dir_key} shall not be in sync_info" }
|
|
41
64
|
session[dir_key] = path
|
|
42
65
|
if direction
|
|
43
66
|
dir_key = path_is_remote ? 'local_dir' : 'remote_dir'
|
|
44
|
-
|
|
67
|
+
Aspera.assert(!session.key?(dir_key)) { "Parameter #{dir_key} shall not be in sync_info" }
|
|
45
68
|
session[dir_key] = transfer.destination_folder(path_is_remote ? Transfer::Spec::DIRECTION_RECEIVE : Transfer::Spec::DIRECTION_SEND)
|
|
46
|
-
local_remote = %w[local remote].map{ |i| session["#{i}_dir"]}
|
|
69
|
+
local_remote = %w[local remote].map { |i| session["#{i}_dir"] }
|
|
47
70
|
end
|
|
48
71
|
else
|
|
49
72
|
# `conf`
|
|
50
73
|
session = sync_info
|
|
51
74
|
dir_key = path_is_remote ? 'remote' : 'local'
|
|
52
75
|
session[dir_key] ||= {}
|
|
53
|
-
|
|
76
|
+
Aspera.assert(!session[dir_key].key?('path')) { "Parameter #{dir_key}.path shall not be in sync_info" }
|
|
54
77
|
session[dir_key]['path'] = path
|
|
55
78
|
if direction
|
|
56
79
|
dir_key = path_is_remote ? 'local' : 'remote'
|
|
57
80
|
session[dir_key] ||= {}
|
|
58
|
-
|
|
81
|
+
Aspera.assert(!session[dir_key].key?('path')) { "Parameter #{dir_key}.path shall not be in sync_info" }
|
|
59
82
|
session[dir_key]['path'] = transfer.destination_folder(path_is_remote ? Transfer::Spec::DIRECTION_RECEIVE : Transfer::Spec::DIRECTION_SEND)
|
|
60
|
-
local_remote = %w[local remote].map{ |i| session[i]['path']}
|
|
83
|
+
local_remote = %w[local remote].map { |i| session[i]['path'] }
|
|
61
84
|
end
|
|
62
85
|
# `conf` is quiet by default
|
|
63
86
|
session['quiet'] = false if !session.key?('quiet') && Environment.terminal?
|
|
64
87
|
end
|
|
65
88
|
if direction
|
|
66
|
-
|
|
89
|
+
Aspera.assert(!session.key?('direction'), type: BadArgument) { 'direction shall not be in sync_info' }
|
|
67
90
|
session['direction'] = direction.to_s
|
|
68
91
|
# generate name if not provided by user
|
|
69
92
|
if !session.key?('name')
|
|
@@ -80,66 +103,68 @@ module Aspera
|
|
|
80
103
|
sync_info
|
|
81
104
|
end
|
|
82
105
|
|
|
83
|
-
# Provide database object from
|
|
84
|
-
|
|
85
|
-
|
|
106
|
+
# Provide database object from path + sync_info for admin ops
|
|
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: {})
|
|
110
|
+
sync_info = async_info_from_args(path: path, sync_info: sync_info)
|
|
86
111
|
session = sync_info.key?('sessions') ? sync_info['sessions'].first : sync_info
|
|
87
112
|
# if name not provided, check in db folder if there is only one name
|
|
88
113
|
if !session.key?('name')
|
|
89
114
|
local_db_dir = Sync::Operations.local_db_folder(sync_info)
|
|
90
115
|
dbs = Sync::Operations.list_db_files(local_db_dir)
|
|
91
|
-
|
|
116
|
+
Aspera.assert(dbs.length == 1) { "#{dbs.length} session found in #{local_db_dir}, please provide a name" }
|
|
92
117
|
session['name'] = dbs.keys.first
|
|
93
118
|
end
|
|
94
119
|
Sync::Database.new(Sync::Operations.session_db_file(sync_info))
|
|
95
120
|
end
|
|
96
121
|
|
|
97
|
-
def
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return Result::SingleObject.new(db_from_args.send(command2, sql_suffix))
|
|
110
|
-
when :file_info
|
|
111
|
-
result = db_from_args.file_info(sql_suffix)
|
|
112
|
-
result.each do |r|
|
|
113
|
-
r['sstate'] = SyncActions::STATE_STR[r['state']] if r['state']
|
|
114
|
-
end
|
|
115
|
-
return Result::ObjectList.new(
|
|
116
|
-
result,
|
|
117
|
-
fields: %w[sstate record_id f_meta_path message]
|
|
118
|
-
)
|
|
119
|
-
when :overview
|
|
120
|
-
return Result::ObjectList.new(
|
|
121
|
-
db_from_args.overview,
|
|
122
|
-
fields: %w[table name type]
|
|
123
|
-
)
|
|
124
|
-
when :query
|
|
125
|
-
return Result.auto(db_from_args.execute(options.get_option(:sql, mandatory: true)))
|
|
126
|
-
else Aspera.error_unexpected_value(command2)
|
|
127
|
-
end
|
|
122
|
+
def action_sync_admin_status(path:, sync_info: {}, **)
|
|
123
|
+
Result::SingleObject.new(Sync::Operations.admin_status(async_info_from_args(path: path, sync_info: sync_info)))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def action_sync_admin_find(path:, **)
|
|
127
|
+
dbs = Sync::Operations.list_db_files(path)
|
|
128
|
+
Result::ObjectList.new(dbs.keys.map { |n| {name: n, path: dbs[n]} })
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def action_sync_admin_meta(path:, sync_info: {}, **)
|
|
132
|
+
require 'aspera/sync/database'
|
|
133
|
+
Result::SingleObject.new(db_from_args(path: path, sync_info: sync_info).meta(options.get_option(:sql)))
|
|
128
134
|
end
|
|
129
135
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return execute_sync_admin
|
|
141
|
-
else Aspera.error_unexpected_value(command)
|
|
136
|
+
def action_sync_admin_counters(path:, sync_info: {}, **)
|
|
137
|
+
require 'aspera/sync/database'
|
|
138
|
+
Result::SingleObject.new(db_from_args(path: path, sync_info: sync_info).counters(options.get_option(:sql)))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def action_sync_admin_file_info(path:, sync_info: {}, **)
|
|
142
|
+
require 'aspera/sync/database'
|
|
143
|
+
result = db_from_args(path: path, sync_info: sync_info).file_info(options.get_option(:sql))
|
|
144
|
+
result.each do |r|
|
|
145
|
+
r['sstate'] = SyncActions::STATE_STR[r['state']] if r['state']
|
|
142
146
|
end
|
|
147
|
+
Result::ObjectList.new(result, fields: %w[sstate record_id f_meta_path message])
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def action_sync_admin_overview(path:, sync_info: {}, **)
|
|
151
|
+
require 'aspera/sync/database'
|
|
152
|
+
Result::ObjectList.new(db_from_args(path: path, sync_info: sync_info).overview, fields: %w[table name type])
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def action_sync_admin_query(path:, sync_info: {}, **)
|
|
156
|
+
require 'aspera/sync/database'
|
|
157
|
+
Result.auto(db_from_args(path: path, sync_info: sync_info).execute(options.get_option(:sql, mandatory: true)))
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Execute a sync transfer for a given direction.
|
|
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)
|
|
164
|
+
# @param block [Proc, nil] block to generate transfer spec; receives (direction, local_dir, remote_dir)
|
|
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)
|
|
167
|
+
Result::Success.new
|
|
143
168
|
end
|
|
144
169
|
end
|
|
145
170
|
end
|
|
@@ -14,7 +14,7 @@ module Aspera
|
|
|
14
14
|
HINT = 'HINT:'.bg_green.gray.blink.freeze
|
|
15
15
|
include FormatterInterface
|
|
16
16
|
|
|
17
|
-
# Format boolean with colored symbol (
|
|
17
|
+
# Format boolean with colored symbol (+/- or Y/ )
|
|
18
18
|
def tick(yes)
|
|
19
19
|
result =
|
|
20
20
|
if Environment.terminal_supports_unicode?
|
|
@@ -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
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/cli/async_transfer_store'
|
|
4
|
+
require 'aspera/agent/factory'
|
|
5
|
+
require 'aspera/log'
|
|
6
|
+
require 'aspera/assert'
|
|
7
|
+
|
|
8
|
+
module Aspera
|
|
9
|
+
module Cli
|
|
10
|
+
# Mixin for Config plugin: async transfer management actions.
|
|
11
|
+
# Exposes three sub-commands under `config transfer`:
|
|
12
|
+
# status --id=<job_id> Re-query a running/completed transfer
|
|
13
|
+
# list List all persisted async transfer entries
|
|
14
|
+
# cleanup Remove completed/failed/cancelled entries
|
|
15
|
+
#
|
|
16
|
+
# Architecture:
|
|
17
|
+
# - remote-daemon agents (desktop, node, connect, transferd): transfer_id + agent_params
|
|
18
|
+
# are persisted in AsyncTransferStore; status is re-queried via Agent::Xxx.transfer_status
|
|
19
|
+
# - direct agent: transfers live in Ruby threads - store is the agent's @sessions;
|
|
20
|
+
# status is not persistable across process restarts (returns an informational message)
|
|
21
|
+
module TransferActions
|
|
22
|
+
# Re-query the status of a single async transfer job.
|
|
23
|
+
# @param job_id [String] UUID returned at submission time
|
|
24
|
+
def action_transfer_status(job_id:, **)
|
|
25
|
+
store = async_transfer_store
|
|
26
|
+
entry = store.read(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
|
|
31
|
+
live = query_live_status(entry)
|
|
32
|
+
if live
|
|
33
|
+
entry.merge!(live)
|
|
34
|
+
store.write(job_id, entry)
|
|
35
|
+
end
|
|
36
|
+
Result::SingleObject.new(entry)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# List all persisted async transfer entries.
|
|
40
|
+
def action_transfer_list(**)
|
|
41
|
+
rows = async_transfer_store.list
|
|
42
|
+
Result::ObjectList.new(rows, fields: %w[job_id agent_type status started_at ended_at bytes_transferred transfer_id])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
TERMINAL_STATUSES = %w[completed failed cancelled].freeze
|
|
46
|
+
private_constant :TERMINAL_STATUSES
|
|
47
|
+
|
|
48
|
+
# Delete completed, failed, and cancelled entries from the store.
|
|
49
|
+
def action_transfer_cleanup(**)
|
|
50
|
+
store = async_transfer_store
|
|
51
|
+
deleted = store.list
|
|
52
|
+
.select { |e| TERMINAL_STATUSES.include?(e['status']) }
|
|
53
|
+
.map do |e|
|
|
54
|
+
store.delete(e['job_id'])
|
|
55
|
+
e['job_id']
|
|
56
|
+
end
|
|
57
|
+
Result::Status.new("Deleted #{deleted.size} completed transfer(s)#{": #{deleted.join(', ')}" unless deleted.empty?}")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
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.
|
|
65
|
+
def async_transfer_store
|
|
66
|
+
context.transfer.async_store
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Delegate to the appropriate agent class method.
|
|
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.
|
|
76
|
+
def query_live_status(entry)
|
|
77
|
+
agent_type = entry['agent_type']
|
|
78
|
+
transfer_id = entry['transfer_id']
|
|
79
|
+
agent_params = entry['agent_params'] || {}
|
|
80
|
+
|
|
81
|
+
require "aspera/agent/#{agent_type}"
|
|
82
|
+
agent_class = Aspera::Agent.const_get(agent_type.capitalize)
|
|
83
|
+
agent_class.transfer_status(transfer_id, agent_params)
|
|
84
|
+
rescue StandardError => e
|
|
85
|
+
Log.log.warn { "Could not re-query #{entry['agent_type']} agent for job #{entry['job_id']}: #{e}" }
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|