aspera-cli 4.23.0 → 4.24.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 +32 -1
- data/CONTRIBUTING.md +86 -29
- data/README.md +1651 -856
- data/bin/ascli +2 -1
- data/bin/asession +4 -4
- data/lib/aspera/agent/base.rb +4 -0
- data/lib/aspera/agent/connect.rb +20 -18
- data/lib/aspera/agent/desktop.rb +14 -11
- data/lib/aspera/agent/direct.rb +39 -31
- data/lib/aspera/agent/httpgw.rb +2 -2
- data/lib/aspera/agent/node.rb +9 -11
- data/lib/aspera/agent/transferd.rb +18 -11
- data/lib/aspera/api/aoc.rb +44 -31
- data/lib/aspera/api/cos_node.rb +7 -5
- data/lib/aspera/api/httpgw.rb +15 -18
- data/lib/aspera/api/node.rb +104 -22
- data/lib/aspera/ascmd.rb +22 -16
- data/lib/aspera/ascp/installation.rb +37 -40
- data/lib/aspera/ascp/management.rb +5 -4
- data/lib/aspera/assert.rb +54 -23
- data/lib/aspera/cli/basic_auth_plugin.rb +8 -7
- data/lib/aspera/cli/error.rb +1 -1
- data/lib/aspera/cli/extended_value.rb +28 -29
- data/lib/aspera/cli/formatter.rb +191 -168
- data/lib/aspera/cli/hints.rb +29 -3
- data/lib/aspera/cli/main.rb +138 -107
- data/lib/aspera/cli/manager.rb +50 -30
- data/lib/aspera/cli/plugin.rb +148 -77
- data/lib/aspera/cli/plugin_factory.rb +2 -2
- data/lib/aspera/cli/plugins/aoc.rb +189 -70
- data/lib/aspera/cli/plugins/ats.rb +15 -13
- data/lib/aspera/cli/plugins/config.rb +86 -213
- data/lib/aspera/cli/plugins/console.rb +49 -18
- data/lib/aspera/cli/plugins/cos.rb +4 -4
- data/lib/aspera/cli/plugins/faspex.rb +45 -51
- data/lib/aspera/cli/plugins/faspex5.rb +162 -163
- data/lib/aspera/cli/plugins/faspio.rb +6 -5
- data/lib/aspera/cli/plugins/httpgw.rb +2 -2
- data/lib/aspera/cli/plugins/node.rb +144 -162
- data/lib/aspera/cli/plugins/orchestrator.rb +10 -14
- data/lib/aspera/cli/plugins/preview.rb +26 -29
- data/lib/aspera/cli/plugins/server.rb +28 -28
- data/lib/aspera/cli/plugins/shares.rb +40 -28
- data/lib/aspera/cli/sync_actions.rb +101 -80
- data/lib/aspera/cli/transfer_agent.rb +51 -50
- data/lib/aspera/cli/transfer_progress.rb +29 -20
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +160 -0
- data/lib/aspera/colors.rb +13 -8
- data/lib/aspera/command_line_builder.rb +28 -22
- data/lib/aspera/command_line_converter.rb +31 -0
- data/lib/aspera/environment.rb +144 -101
- data/lib/aspera/faspex_gw.rb +1 -1
- data/lib/aspera/faspex_postproc.rb +3 -2
- data/lib/aspera/hash_ext.rb +1 -1
- data/lib/aspera/id_generator.rb +10 -10
- data/lib/aspera/keychain/base.rb +18 -0
- data/lib/aspera/keychain/encrypted_hash.rb +6 -12
- data/lib/aspera/keychain/factory.rb +9 -3
- data/lib/aspera/keychain/hashicorp_vault.rb +9 -6
- data/lib/aspera/keychain/macos_security.rb +13 -13
- data/lib/aspera/log.rb +69 -20
- data/lib/aspera/nagios.rb +5 -6
- data/lib/aspera/node_simulator.rb +12 -7
- data/lib/aspera/oauth/base.rb +5 -3
- data/lib/aspera/oauth/factory.rb +24 -18
- data/lib/aspera/oauth/jwt.rb +13 -1
- data/lib/aspera/oauth/url_json.rb +3 -3
- data/lib/aspera/oauth/web.rb +5 -3
- data/lib/aspera/persistency_folder.rb +2 -2
- data/lib/aspera/preview/file_types.rb +4 -3
- data/lib/aspera/preview/generator.rb +25 -12
- data/lib/aspera/preview/terminal.rb +10 -7
- data/lib/aspera/preview/utils.rb +11 -9
- data/lib/aspera/products/connect.rb +1 -1
- data/lib/aspera/products/desktop.rb +1 -1
- data/lib/aspera/products/other.rb +2 -2
- data/lib/aspera/products/transferd.rb +8 -6
- data/lib/aspera/proxy_auto_config.rb +1 -1
- data/lib/aspera/rest.rb +29 -22
- data/lib/aspera/rest_call_error.rb +1 -1
- data/lib/aspera/resumer.rb +1 -1
- data/lib/aspera/secret_hider.rb +46 -40
- data/lib/aspera/ssh.rb +13 -3
- data/lib/aspera/sync/args.schema.yaml +102 -0
- data/lib/aspera/sync/conf.schema.yaml +701 -0
- data/lib/aspera/sync/database.rb +83 -0
- data/lib/aspera/{transfer/sync.rb → sync/operations.rb} +132 -65
- data/lib/aspera/temp_file_manager.rb +3 -2
- data/lib/aspera/transfer/error.rb +1 -1
- data/lib/aspera/transfer/error_info.rb +1 -2
- data/lib/aspera/transfer/faux_file.rb +11 -10
- data/lib/aspera/transfer/parameters.rb +6 -5
- data/lib/aspera/transfer/spec.rb +15 -1
- data/lib/aspera/transfer/spec.schema.yaml +316 -293
- data/lib/aspera/transfer/spec_doc.rb +34 -16
- data/lib/aspera/transfer/uri.rb +5 -5
- data/lib/aspera/uri_reader.rb +14 -10
- data/lib/aspera/web_auth.rb +2 -2
- data/lib/aspera/web_server_simple.rb +2 -2
- data.tar.gz.sig +0 -0
- metadata +15 -13
- metadata.gz.sig +2 -2
- data/lib/aspera/transfer/async_conf.schema.yaml +0 -716
- data/lib/aspera/transfer/convert.rb +0 -29
- data/lib/aspera/transfer/sync_instance.schema.yaml +0 -20
- data/lib/aspera/transfer/sync_session.schema.yaml +0 -86
@@ -1,108 +1,129 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'aspera/
|
3
|
+
require 'aspera/sync/operations'
|
4
4
|
require 'aspera/assert'
|
5
|
+
require 'aspera/environment'
|
6
|
+
require 'pathname'
|
5
7
|
|
6
8
|
module Aspera
|
7
9
|
module Cli
|
8
|
-
#
|
10
|
+
# Manage command line arguments to provide to Sync::Run, Sync::Database and Sync::Operations
|
9
11
|
module SyncActions
|
10
|
-
#
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
# type: type for validation
|
16
|
-
ARGUMENTS_INFO = [
|
17
|
-
{
|
18
|
-
conf: 'direction',
|
19
|
-
args: 'direction',
|
20
|
-
values: Transfer::Sync::DIRECTIONS
|
21
|
-
}, {
|
22
|
-
conf: 'local.path',
|
23
|
-
args: 'local_dir',
|
24
|
-
type: String
|
25
|
-
}, {
|
26
|
-
conf: 'remote.path',
|
27
|
-
args: 'remote_dir',
|
28
|
-
type: String
|
29
|
-
}
|
30
|
-
].freeze
|
31
|
-
# name of minimal arguments required, also used to generate a session name
|
32
|
-
ARGUMENTS_LIST = ARGUMENTS_INFO.map{ |i| i[:conf]}.freeze
|
33
|
-
private_constant :ARGUMENTS_INFO
|
34
|
-
|
12
|
+
# translate state id (int) to string
|
13
|
+
STATE_STR = (['Nil'] +
|
14
|
+
(1..18).map{ |i| "P(#{i})"} +
|
15
|
+
%w[Syncd Error Confl Pconf] +
|
16
|
+
(23..24).map{ |i| "P(#{i})"}).freeze
|
35
17
|
class << self
|
36
|
-
def declare_options(
|
37
|
-
options.declare(:sync_info, 'Information for sync instance and sessions', types: Hash)
|
18
|
+
def declare_options(_options)
|
38
19
|
end
|
39
20
|
end
|
40
21
|
|
41
|
-
# Read command line arguments (3) and converts to sync_info format
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
async_params['sessions'] ||= [{}]
|
62
|
-
Aspera.assert(async_params['sessions'].length == 1){'Only one session is supported with arguments'}
|
63
|
-
session_info = async_params['sessions'][0]
|
64
|
-
param_path = :args
|
22
|
+
# Read command line arguments (1 to 3) and converts to sync_info format
|
23
|
+
# @param sync [Bool] Set to `true` for non-admin
|
24
|
+
# @return [Hash] sync info
|
25
|
+
def async_info_from_args(direction: nil)
|
26
|
+
path = options.get_next_argument('path')
|
27
|
+
sync_info = options.get_next_argument('sync info', mandatory: false, validation: Hash, default: {})
|
28
|
+
path_is_remote = direction.eql?(:pull)
|
29
|
+
if sync_info.key?('sessions') || sync_info.key?('instance')
|
30
|
+
# "args"
|
31
|
+
sync_info['sessions'] ||= [{}]
|
32
|
+
Aspera.assert(sync_info['sessions'].length == 1){'Only one session is supported'}
|
33
|
+
session = sync_info['sessions'].first
|
34
|
+
dir_key = path_is_remote ? 'remote_dir' : 'local_dir'
|
35
|
+
raise "Parameter #{dir_key} shall not be in sync_info" if session.key?(dir_key)
|
36
|
+
session[dir_key] = path
|
37
|
+
if direction
|
38
|
+
dir_key = path_is_remote ? 'local_dir' : 'remote_dir'
|
39
|
+
raise "Parameter #{dir_key} shall not be in sync_info" if session.key?(dir_key)
|
40
|
+
session[dir_key] = transfer.destination_folder(path_is_remote ? Transfer::Spec::DIRECTION_RECEIVE : Transfer::Spec::DIRECTION_SEND)
|
41
|
+
local_remote = %w[local remote].map{ |i| session["#{i}_dir"]}
|
65
42
|
end
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
43
|
+
else
|
44
|
+
# "conf"
|
45
|
+
session = sync_info
|
46
|
+
dir_key = path_is_remote ? 'remote' : 'local'
|
47
|
+
session[dir_key] ||= {}
|
48
|
+
raise "Parameter #{dir_key}.path shall not be in sync_info" if session[dir_key].key?('path')
|
49
|
+
session[dir_key]['path'] = path
|
50
|
+
if direction
|
51
|
+
dir_key = path_is_remote ? 'local' : 'remote'
|
52
|
+
session[dir_key] ||= {}
|
53
|
+
raise "Parameter #{dir_key}.path shall not be in sync_info" if session[dir_key].key?('path')
|
54
|
+
session[dir_key]['path'] = transfer.destination_folder(path_is_remote ? Transfer::Spec::DIRECTION_RECEIVE : Transfer::Spec::DIRECTION_SEND)
|
55
|
+
local_remote = %w[local remote].map{ |i| session[i]['path']}
|
76
56
|
end
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
57
|
+
# "conf" is quiet by default
|
58
|
+
session['quiet'] = false if !session.key?('quiet') && Environment.terminal?
|
59
|
+
end
|
60
|
+
if direction
|
61
|
+
raise BadArgument, 'direction shall not be in sync_info' if session.key?('direction')
|
62
|
+
session['direction'] = direction.to_s
|
63
|
+
# generate name if not provided by user
|
64
|
+
if !session.key?('name')
|
65
|
+
session['name'] = Environment.instance.sanitized_filename(
|
66
|
+
([direction.to_s] + local_remote).map do |value|
|
67
|
+
Pathname(value).each_filename.to_a.last(2).join(Environment.instance.safe_filename_character)
|
68
|
+
end.join(Environment.instance.safe_filename_character)
|
69
|
+
)
|
82
70
|
end
|
83
|
-
else raise Cli::BadArgument, "Provide 0 or 3 arguments, not #{arguments.keys.length} for: #{ARGUMENTS_LIST.join(', ')}"
|
84
71
|
end
|
72
|
+
sync_info
|
85
73
|
end
|
86
74
|
|
75
|
+
# provide database object from command line arguments for admin ops
|
76
|
+
def db_from_args
|
77
|
+
sync_info = async_info_from_args
|
78
|
+
session = sync_info.key?('sessions') ? sync_info['sessions'].first : sync_info
|
79
|
+
# if name not provided, check in db folder if there is only one name
|
80
|
+
if !session.key?('name')
|
81
|
+
local_db_dir = Sync::Operations.local_db_folder(sync_info)
|
82
|
+
dbs = Sync::Operations.list_db_files(local_db_dir)
|
83
|
+
raise "#{dbs.length} session found in #{local_db_dir}, please provide a name" unless dbs.length == 1
|
84
|
+
session['name'] = dbs.keys.first
|
85
|
+
end
|
86
|
+
Sync::Database.new(Sync::Operations.session_db_file(sync_info))
|
87
|
+
end
|
88
|
+
|
89
|
+
# Execute sync action
|
90
|
+
# @param &block [nil, Proc] block to generate transfer spec, takes: direction (one of DIRECTIONS), local_dir, remote_dir
|
87
91
|
def execute_sync_action(&block)
|
88
|
-
|
89
|
-
command = options.get_next_command(%i[start admin])
|
92
|
+
command = options.get_next_command(%i[admin] + Sync::Operations::DIRECTIONS)
|
90
93
|
# try to get 3 arguments as simple arguments
|
91
94
|
case command
|
92
|
-
when
|
93
|
-
|
94
|
-
async_params = options.get_option(:sync_info, default: {})
|
95
|
-
sync_args_to_params(async_params)
|
96
|
-
Transfer::Sync.start(async_params, &block)
|
95
|
+
when *Sync::Operations::DIRECTIONS
|
96
|
+
Sync::Operations.start(async_info_from_args(direction: command), transfer.option_transfer_spec, &block)
|
97
97
|
return Main.result_success
|
98
98
|
when :admin
|
99
|
-
command2 = options.get_next_command([
|
99
|
+
command2 = options.get_next_command(%i[status find meta counters file_info overview])
|
100
|
+
require 'aspera/sync/database' unless command2.eql?(:status)
|
100
101
|
case command2
|
101
102
|
when :status
|
102
|
-
|
103
|
-
|
104
|
-
|
103
|
+
return Main.result_single_object(Sync::Operations.admin_status(async_info_from_args))
|
104
|
+
when :find
|
105
|
+
folder = options.get_next_argument('path')
|
106
|
+
dbs = Sync::Operations.list_db_files(folder)
|
107
|
+
return Main.result_object_list(dbs.keys.map{ |n| {name: n, path: dbs[n]}})
|
108
|
+
when :meta, :counters
|
109
|
+
return Main.result_single_object(db_from_args.send(command2))
|
110
|
+
when :file_info
|
111
|
+
result = db_from_args.send(command2)
|
112
|
+
result.each do |r|
|
113
|
+
r['sstate'] = SyncActions::STATE_STR[r['state']] if r['state']
|
114
|
+
end
|
115
|
+
return Main.result_object_list(
|
116
|
+
result,
|
117
|
+
fields: %w[sstate record_id f_meta_path message]
|
118
|
+
)
|
119
|
+
when :overview
|
120
|
+
return Main.result_object_list(
|
121
|
+
db_from_args.overview,
|
122
|
+
fields: %w[table name type]
|
123
|
+
)
|
124
|
+
else Aspera.error_unexpected_value(command2)
|
105
125
|
end
|
126
|
+
else Aspera.error_unexpected_value(command)
|
106
127
|
end
|
107
128
|
end
|
108
129
|
end
|
@@ -12,9 +12,9 @@ module Aspera
|
|
12
12
|
# one of the supported transfer agents
|
13
13
|
# provides CLI options to select one of the transfer agents (FASP/ascp client)
|
14
14
|
class TransferAgent
|
15
|
-
# special value for --sources : read file list from arguments
|
15
|
+
# @args special value for --sources : read file list from arguments
|
16
16
|
FILE_LIST_FROM_ARGS = '@args'
|
17
|
-
# special value for --sources : read file list from transfer spec (--ts)
|
17
|
+
# @ts special value for --sources : read file list from transfer spec (--ts)
|
18
18
|
FILE_LIST_FROM_TRANSFER_SPEC = '@ts'
|
19
19
|
FILE_LIST_OPTIONS = [FILE_LIST_FROM_ARGS, FILE_LIST_FROM_TRANSFER_SPEC, 'Array'].freeze
|
20
20
|
DEFAULT_TRANSFER_NOTIFY_TEMPLATE = <<~END_OF_TEMPLATE
|
@@ -49,7 +49,10 @@ module Aspera
|
|
49
49
|
@opt_mgr = opt_mgr
|
50
50
|
@config = config_plugin
|
51
51
|
# command line can override transfer spec
|
52
|
-
@transfer_spec_command_line = {
|
52
|
+
@transfer_spec_command_line = {
|
53
|
+
'create_dir' => true,
|
54
|
+
'resume_policy' => 'sparse_csum'
|
55
|
+
}
|
53
56
|
# options for transfer agent
|
54
57
|
@transfer_info = {}
|
55
58
|
# the currently selected transfer agent
|
@@ -60,7 +63,7 @@ module Aspera
|
|
60
63
|
@httpgw_url_lambda = nil
|
61
64
|
@opt_mgr.declare(:ts, 'Override transfer spec values', types: Hash, handler: {o: self, m: :option_transfer_spec})
|
62
65
|
@opt_mgr.declare(:to_folder, 'Destination folder for transferred files')
|
63
|
-
@opt_mgr.declare(:sources, "How list of transferred files is provided (#{FILE_LIST_OPTIONS.join(',')})")
|
66
|
+
@opt_mgr.declare(:sources, "How list of transferred files is provided (#{FILE_LIST_OPTIONS.join(',')})", default: FILE_LIST_FROM_ARGS)
|
64
67
|
@opt_mgr.declare(:src_type, 'Type of file list', values: %i[list pair], default: :list)
|
65
68
|
@opt_mgr.declare(:transfer, 'Type of transfer agent', values: TRANSFER_AGENTS, default: :direct)
|
66
69
|
@opt_mgr.declare(:transfer_info, 'Parameters for transfer agent', types: Hash, handler: {o: self, m: :transfer_info})
|
@@ -86,7 +89,7 @@ module Aspera
|
|
86
89
|
end
|
87
90
|
|
88
91
|
# add other transfer spec parameters
|
89
|
-
def option_transfer_spec_deep_merge(
|
92
|
+
def option_transfer_spec_deep_merge(value); @transfer_spec_command_line.deep_merge!(value); end
|
90
93
|
|
91
94
|
attr_reader :transfer_info
|
92
95
|
|
@@ -163,54 +166,56 @@ module Aspera
|
|
163
166
|
@httpgw_url_lambda = httpgw_url_proc
|
164
167
|
end
|
165
168
|
|
169
|
+
# transform the list of paths to a list of hash with source/dest
|
170
|
+
def list_to_paths(file_list)
|
171
|
+
source_type = @opt_mgr.get_option(:src_type, mandatory: true)
|
172
|
+
case source_type
|
173
|
+
when :list
|
174
|
+
# when providing a list, just specify source
|
175
|
+
@transfer_paths = file_list.map{ |i| {'source' => i}}
|
176
|
+
when :pair
|
177
|
+
Aspera.assert(file_list.length.even?, type: Cli::BadArgument){"When using pair, provide an even number of paths: #{file_list.length}"}
|
178
|
+
@transfer_paths = file_list.each_slice(2).map{ |s, d| {'source' => s, 'destination' => d}}
|
179
|
+
else Aspera.error_unexpected_value(source_type)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
166
183
|
# This is how the list of files to be transferred is specified
|
167
184
|
# get paths suitable for transfer spec from command line
|
168
|
-
# @param default [String] if set, used as default file for --sources=@args
|
169
|
-
# @return [Hash] {source: (mandatory), destination: (optional)}
|
170
185
|
# computation is done only once, cache is kept in @transfer_paths
|
186
|
+
# @param default [Array] of [String] if set, used as default file for --sources=@args
|
187
|
+
# @return [Array, nil] of Hash {source: (mandatory), destination: (optional)}
|
171
188
|
def ts_source_paths(default: nil)
|
172
189
|
# return cache if set
|
173
190
|
return @transfer_paths unless @transfer_paths.nil?
|
174
191
|
# start with lower priority : get paths from transfer spec on command line
|
175
192
|
@transfer_paths = @transfer_spec_command_line['paths'] if @transfer_spec_command_line.key?('paths')
|
176
193
|
# is there a source list option ?
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
Log.log.warn('--sources overrides paths from --ts')
|
203
|
-
end
|
204
|
-
source_type = @opt_mgr.get_option(:src_type, mandatory: true)
|
205
|
-
case source_type
|
206
|
-
when :list
|
207
|
-
# when providing a list, just specify source
|
208
|
-
@transfer_paths = file_list.map{ |i| {'source' => i}}
|
209
|
-
when :pair
|
210
|
-
Aspera.assert(file_list.length.even?, exception_class: Cli::BadArgument){"When using pair, provide an even number of paths: #{file_list.length}"}
|
211
|
-
@transfer_paths = file_list.each_slice(2).to_a.map{ |s, d| {'source' => s, 'destination' => d}}
|
212
|
-
else Aspera.error_unexpected_value(source_type)
|
213
|
-
end
|
194
|
+
sources = @opt_mgr.get_option(:sources)
|
195
|
+
@transfer_paths =
|
196
|
+
case sources
|
197
|
+
when FILE_LIST_FROM_ARGS
|
198
|
+
Log.log.debug('getting file list as parameters')
|
199
|
+
Aspera.assert_type(default, Array, NilClass)
|
200
|
+
# get remaining arguments
|
201
|
+
list = @opt_mgr.get_next_argument('source file list', multiple: true, default: default)
|
202
|
+
raise Cli::BadArgument, 'specify at least one file on command line or use ' \
|
203
|
+
"--sources=#{FILE_LIST_FROM_TRANSFER_SPEC} to use transfer spec" if !list.is_a?(Array) || list.empty?
|
204
|
+
list_to_paths(list)
|
205
|
+
when FILE_LIST_FROM_TRANSFER_SPEC
|
206
|
+
Log.log.debug('assume list provided in transfer spec')
|
207
|
+
special_case_direct_with_list =
|
208
|
+
@opt_mgr.get_option(:transfer, mandatory: true).eql?(:direct) &&
|
209
|
+
Transfer::Parameters.ascp_args_file_list?(@opt_mgr.get_option(:transfer_info)['ascp_args'])
|
210
|
+
raise Cli::BadArgument, 'transfer spec on command line must have sources' if @transfer_paths.nil? && !special_case_direct_with_list
|
211
|
+
# can be nil
|
212
|
+
@transfer_paths
|
213
|
+
when Array
|
214
|
+
Log.log.debug('getting file list as extended value')
|
215
|
+
Aspera.assert(sources.all?(String), type: Cli::BadArgument){'sources must be a Array of String'}
|
216
|
+
list_to_paths(sources)
|
217
|
+
else Aspera.error_unexpected_value(sources){'sources'}
|
218
|
+
end
|
214
219
|
Log.log.debug{"paths=#{@transfer_paths}"}
|
215
220
|
return @transfer_paths
|
216
221
|
end
|
@@ -221,9 +226,7 @@ module Aspera
|
|
221
226
|
def start(transfer_spec, rest_token: nil)
|
222
227
|
# check parameters
|
223
228
|
Aspera.assert_type(transfer_spec, Hash){'transfer_spec'}
|
224
|
-
if transfer_spec['remote_host'].eql?(CP4I_REMOTE_HOST_LB)
|
225
|
-
raise "Wrong remote host: #{CP4I_REMOTE_HOST_LB}"
|
226
|
-
end
|
229
|
+
raise "Wrong remote host: #{CP4I_REMOTE_HOST_LB}" if transfer_spec['remote_host'].eql?(CP4I_REMOTE_HOST_LB)
|
227
230
|
# process :src option
|
228
231
|
case transfer_spec['direction']
|
229
232
|
when Transfer::Spec::DIRECTION_RECEIVE
|
@@ -250,9 +253,7 @@ module Aspera
|
|
250
253
|
# recursively remove values that are nil (user wants to delete)
|
251
254
|
transfer_spec.deep_do{ |hash, key, value, _unused| hash.delete(key) if value.nil?}
|
252
255
|
# if TS from app has content_protection (e.g. F5), that means content is protected: ask password if not provided
|
253
|
-
if transfer_spec['content_protection'].eql?('decrypt') && !transfer_spec.key?('content_protection_password')
|
254
|
-
transfer_spec['content_protection_password'] = @opt_mgr.prompt_user_input('content protection password', sensitive: true)
|
255
|
-
end
|
256
|
+
transfer_spec['content_protection_password'] = @opt_mgr.prompt_user_input('content protection password', sensitive: true) if transfer_spec['content_protection'].eql?('decrypt') && !transfer_spec.key?('content_protection_password')
|
256
257
|
# create transfer agent
|
257
258
|
agent_instance.start_transfer(transfer_spec, token_regenerator: rest_token)
|
258
259
|
# list of: :success or "error message string"
|
@@ -8,13 +8,10 @@ module Aspera
|
|
8
8
|
module Cli
|
9
9
|
# Progress bar for transfers.
|
10
10
|
# Supports multi-session.
|
11
|
+
# Note that we can have this case:
|
12
|
+
# 2 sessions (-C x:2), but one session fails and restarts...
|
11
13
|
class TransferProgress
|
12
14
|
def initialize
|
13
|
-
reset
|
14
|
-
end
|
15
|
-
|
16
|
-
# Reset progress bar, to re-use it.
|
17
|
-
def reset
|
18
15
|
@progress_bar = nil
|
19
16
|
# key is session id
|
20
17
|
@sessions = {}
|
@@ -22,11 +19,16 @@ module Aspera
|
|
22
19
|
@title = nil
|
23
20
|
end
|
24
21
|
|
22
|
+
# Reset progress bar, to re-use it.
|
23
|
+
def reset
|
24
|
+
send(:initialize)
|
25
|
+
end
|
26
|
+
|
25
27
|
# Called by user of progress bar with a status on a transfer session
|
26
28
|
# @param session_id the unique identifier of a transfer session
|
27
|
-
# @param type one of:
|
29
|
+
# @param type [Symbol] one of: sessions_init, session_start, session_size, transfer, session_end and end
|
28
30
|
# @param info optional specific additional info for the given event type
|
29
|
-
def event(type, session_id
|
31
|
+
def event(type, session_id: nil, info: nil)
|
30
32
|
Log.log.trace1{"progress: #{type} #{session_id} #{info}"}
|
31
33
|
return if @completed
|
32
34
|
if @progress_bar.nil?
|
@@ -34,11 +36,12 @@ module Aspera
|
|
34
36
|
format: '%t %a %B %p%% %r Mbps %E',
|
35
37
|
rate_scale: lambda{ |rate| rate / Environment::BYTES_PER_MEBIBIT},
|
36
38
|
title: '',
|
37
|
-
total: nil
|
39
|
+
total: nil
|
40
|
+
)
|
38
41
|
end
|
39
42
|
progress_provided = false
|
40
43
|
case type
|
41
|
-
when :
|
44
|
+
when :sessions_init
|
42
45
|
# give opportunity to show progress of initialization with multiple status
|
43
46
|
Aspera.assert(session_id.nil?)
|
44
47
|
Aspera.assert_type(info, String)
|
@@ -50,35 +53,41 @@ module Aspera
|
|
50
53
|
raise "Session #{session_id} already started" if @sessions[session_id]
|
51
54
|
@sessions[session_id] = {
|
52
55
|
job_size: 0, # total size of transfer (pre-calc)
|
53
|
-
current: 0
|
56
|
+
current: 0,
|
57
|
+
running: true
|
54
58
|
}
|
55
59
|
# remove last pre-start message if any
|
56
60
|
@title = nil
|
57
61
|
when :session_size
|
58
62
|
Aspera.assert_type(session_id, String)
|
59
63
|
Aspera.assert(!info.nil?)
|
64
|
+
Aspera.assert_type(@sessions[session_id], Hash)
|
60
65
|
@sessions[session_id][:job_size] = info.to_i
|
61
|
-
|
62
|
-
@progress_bar.total =
|
66
|
+
sessions_total = total(:job_size)
|
67
|
+
@progress_bar.total = sessions_total unless sessions_total.eql?(@progress_bar.total) || sessions_total < @progress_bar.progress
|
63
68
|
when :transfer
|
64
69
|
Aspera.assert_type(session_id, String)
|
70
|
+
Aspera.assert_type(@sessions[session_id], Hash)
|
65
71
|
if !@progress_bar.total.nil? && !info.nil?
|
66
72
|
progress_provided = true
|
67
73
|
@sessions[session_id][:current] = info.to_i
|
68
|
-
|
69
|
-
@progress_bar.progress =
|
74
|
+
sessions_current = total(:current)
|
75
|
+
@progress_bar.progress = sessions_current unless @progress_bar.progress.eql?(sessions_current) || sessions_current > total(:job_size)
|
70
76
|
end
|
77
|
+
when :session_end
|
78
|
+
Aspera.assert_type(session_id, String)
|
79
|
+
Aspera.assert(info.nil?)
|
80
|
+
# a session may be too short and finish before it has been started
|
81
|
+
@sessions[session_id][:running] = false if @sessions[session_id].is_a?(Hash)
|
71
82
|
when :end
|
72
|
-
Aspera.assert(session_id
|
83
|
+
Aspera.assert(session_id.nil?)
|
73
84
|
Aspera.assert(info.nil?)
|
74
|
-
@title = nil
|
75
|
-
@completed = true
|
76
85
|
@progress_bar.finish
|
77
86
|
else Aspera.error_unexpected_value(type){'event type'}
|
78
87
|
end
|
79
|
-
new_title = @sessions.length < 2 ? @title.to_s : "[#{@sessions.
|
80
|
-
@progress_bar
|
81
|
-
@progress_bar
|
88
|
+
new_title = @sessions.length < 2 ? @title.to_s : "[#{@sessions.count{ |_i, d| d[:running]}}] #{@title}"
|
89
|
+
@progress_bar&.title = new_title unless @progress_bar&.title.eql?(new_title)
|
90
|
+
@progress_bar&.increment if !progress_provided && @progress_bar.progress.nil?
|
82
91
|
rescue ProgressBar::InvalidProgressError => e
|
83
92
|
Log.log.error{"Progress error: #{e}"}
|
84
93
|
end
|
data/lib/aspera/cli/version.rb
CHANGED
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'aspera/oauth/jwt'
|
4
|
+
|
5
|
+
module Aspera
|
6
|
+
module Cli
|
7
|
+
class Wizard
|
8
|
+
WIZARD_RESULT_KEYS = %i[preset_value test_args].freeze
|
9
|
+
DEFAULT_PRIV_KEY_FILENAME = 'my_private_key.pem' # pragma: allowlist secret
|
10
|
+
private_constant :WIZARD_RESULT_KEYS,
|
11
|
+
:DEFAULT_PRIV_KEY_FILENAME
|
12
|
+
|
13
|
+
def initialize(parent, main_folder)
|
14
|
+
@parent = parent
|
15
|
+
@main_folder = main_folder
|
16
|
+
# wizard options
|
17
|
+
options.declare(:override, 'Wizard: override existing value', values: :bool, default: :no)
|
18
|
+
options.declare(:default, 'Wizard: set as default configuration for specified plugin (also: update)', values: :bool, default: true)
|
19
|
+
options.declare(:test_mode, 'Wizard: skip private key check step', values: :bool, default: false)
|
20
|
+
options.declare(:key_path, 'Wizard: path to private key for JWT')
|
21
|
+
end
|
22
|
+
|
23
|
+
def options
|
24
|
+
@parent.options
|
25
|
+
end
|
26
|
+
|
27
|
+
def formatter
|
28
|
+
@parent.formatter
|
29
|
+
end
|
30
|
+
|
31
|
+
# Find a plugin, and issue the "require"
|
32
|
+
# @return [Hash] plugin info: { product:, name:, url:, version: }
|
33
|
+
def identify_plugins_for_url
|
34
|
+
app_url = options.get_next_argument('url', mandatory: true)
|
35
|
+
check_only = options.get_next_argument('plugin name', mandatory: false)
|
36
|
+
check_only = check_only.to_sym unless check_only.nil?
|
37
|
+
found_apps = []
|
38
|
+
my_self_plugin_sym = self.class.name.split('::').last.downcase.to_sym
|
39
|
+
PluginFactory.instance.plugin_list.each do |plugin_name_sym|
|
40
|
+
# no detection for internal plugin
|
41
|
+
next if plugin_name_sym.eql?(my_self_plugin_sym)
|
42
|
+
next if check_only && !check_only.eql?(plugin_name_sym)
|
43
|
+
# load plugin class
|
44
|
+
detect_plugin_class = PluginFactory.instance.plugin_class(plugin_name_sym)
|
45
|
+
# requires detection method
|
46
|
+
next unless detect_plugin_class.respond_to?(:detect)
|
47
|
+
detection_info = nil
|
48
|
+
begin
|
49
|
+
Log.log.debug{"detecting #{plugin_name_sym} at #{app_url}"}
|
50
|
+
formatter.long_operation_running("#{plugin_name_sym}\r")
|
51
|
+
detection_info = detect_plugin_class.detect(app_url)
|
52
|
+
rescue OpenSSL::SSL::SSLError => e
|
53
|
+
Log.log.warn(e.message)
|
54
|
+
Log.log.warn('Use option --insecure=yes to allow unchecked certificate') if e.message.include?('cert')
|
55
|
+
rescue StandardError => e
|
56
|
+
Log.log.debug{"detect error: [#{e.class}] #{e}"}
|
57
|
+
next
|
58
|
+
end
|
59
|
+
next if detection_info.nil?
|
60
|
+
Aspera.assert_type(detection_info, Hash)
|
61
|
+
Aspera.assert_type(detection_info[:url], String) if detection_info.key?(:url)
|
62
|
+
app_name = detect_plugin_class.respond_to?(:application_name) ? detect_plugin_class.application_name : detect_plugin_class.name.split('::').last
|
63
|
+
# if there is a redirect, then the detector can override the url.
|
64
|
+
found_apps.push({product: plugin_name_sym, name: app_name, url: app_url, version: 'unknown'}.merge(detection_info))
|
65
|
+
end
|
66
|
+
raise "No known application found at #{app_url}" if found_apps.empty?
|
67
|
+
Aspera.assert(found_apps.all?{ |a| a.keys.all?(Symbol)})
|
68
|
+
return found_apps
|
69
|
+
end
|
70
|
+
|
71
|
+
def find(apps)
|
72
|
+
identification = if apps.length.eql?(1)
|
73
|
+
Log.log.debug{"Detected: #{identification}"}
|
74
|
+
apps.first
|
75
|
+
else
|
76
|
+
formatter.display_status('Multiple applications detected, please select from:')
|
77
|
+
formatter.display_results(type: :object_list, data: apps, fields: %w[product url version])
|
78
|
+
answer = options.prompt_user_input_in_list('product', apps.map{ |a| a[:product]})
|
79
|
+
apps.find{ |a| a[:product].eql?(answer)}
|
80
|
+
end
|
81
|
+
wiz_preset_name = options.get_next_argument('preset name', default: '')
|
82
|
+
Log.dump(:identification, identification)
|
83
|
+
wiz_url = identification[:url]
|
84
|
+
formatter.display_status("Using: #{identification[:name]} at #{wiz_url}".bold)
|
85
|
+
# set url for instantiation of plugin
|
86
|
+
options.add_option_preset({url: wiz_url}, 'wizard')
|
87
|
+
# instantiate plugin: command line options will be known and wizard can be called
|
88
|
+
wiz_plugin_class = PluginFactory.instance.plugin_class(identification[:product])
|
89
|
+
Aspera.assert(wiz_plugin_class.respond_to?(:wizard), type: Cli::BadArgument) do
|
90
|
+
"Detected: #{identification[:product]}, but this application has no wizard"
|
91
|
+
end
|
92
|
+
# instantiate plugin: command line options will be known, e.g. private_key
|
93
|
+
plugin_instance = wiz_plugin_class.new(context: @parent.context)
|
94
|
+
wiz_params = {
|
95
|
+
object: plugin_instance
|
96
|
+
}
|
97
|
+
# is private key needed ?
|
98
|
+
if options.known_options.key?(:private_key) &&
|
99
|
+
(!wiz_plugin_class.respond_to?(:private_key_required?) || wiz_plugin_class.private_key_required?(wiz_url))
|
100
|
+
# lets see if path to priv key is provided
|
101
|
+
private_key_path = options.get_option(:key_path)
|
102
|
+
# give a chance to provide
|
103
|
+
if private_key_path.nil?
|
104
|
+
formatter.display_status('Please provide the path to your private RSA key, or nothing to generate one:')
|
105
|
+
private_key_path = options.get_option(:key_path, mandatory: true).to_s
|
106
|
+
end
|
107
|
+
# else generate path
|
108
|
+
private_key_path = File.join(@main_folder, DEFAULT_PRIV_KEY_FILENAME) if private_key_path.empty?
|
109
|
+
if File.exist?(private_key_path)
|
110
|
+
formatter.display_status('Using existing key:')
|
111
|
+
else
|
112
|
+
formatter.display_status("Generating #{OAuth::Jwt::DEFAULT_PRIV_KEY_LENGTH} bit RSA key...")
|
113
|
+
OAuth::Jwt.generate_rsa_private_key(path: private_key_path)
|
114
|
+
formatter.display_status('Created key:')
|
115
|
+
end
|
116
|
+
formatter.display_status(private_key_path)
|
117
|
+
private_key_pem = File.read(private_key_path)
|
118
|
+
options.set_option(:private_key, private_key_pem)
|
119
|
+
wiz_params[:private_key_path] = private_key_path
|
120
|
+
wiz_params[:pub_key_pem] = OpenSSL::PKey::RSA.new(private_key_pem).public_key.to_s
|
121
|
+
end
|
122
|
+
Log.dump(:wiz_params, wiz_params)
|
123
|
+
# finally, call the wizard
|
124
|
+
wizard_result = wiz_plugin_class.wizard(**wiz_params)
|
125
|
+
Log.log.debug{"wizard result: #{wizard_result}"}
|
126
|
+
Aspera.assert(WIZARD_RESULT_KEYS.eql?(wizard_result.keys.sort)){"missing or extra keys in wizard result: #{wizard_result.keys}"}
|
127
|
+
# get preset name from user or default
|
128
|
+
if wiz_preset_name.empty?
|
129
|
+
elements = [
|
130
|
+
identification[:product],
|
131
|
+
URI.parse(wiz_url).host
|
132
|
+
]
|
133
|
+
elements.push(options.get_option(:username, mandatory: true)) unless wizard_result[:preset_value].key?(:link) rescue nil
|
134
|
+
wiz_preset_name = elements.join('_').strip.downcase.gsub(/[^a-z0-9]/, '_').squeeze('_')
|
135
|
+
end
|
136
|
+
# test mode does not change conf file
|
137
|
+
return Main.result_single_object(wizard_result) if options.get_option(:test_mode)
|
138
|
+
# Write configuration file
|
139
|
+
formatter.display_status("Preparing preset: #{wiz_preset_name}")
|
140
|
+
# init defaults if necessary
|
141
|
+
@config_presets[CONF_PRESET_DEFAULTS] ||= {}
|
142
|
+
option_override = options.get_option(:override, mandatory: true)
|
143
|
+
raise Cli::Error, "A default configuration already exists for plugin '#{identification[:product]}' (use --override=yes or --default=no)" \
|
144
|
+
if !option_override && options.get_option(:default, mandatory: true) && @config_presets[CONF_PRESET_DEFAULTS].key?(identification[:product])
|
145
|
+
raise Cli::Error, "Preset already exists: #{wiz_preset_name} (use --override=yes or --id=<name>)" \
|
146
|
+
if !option_override && @config_presets.key?(wiz_preset_name)
|
147
|
+
@config_presets[wiz_preset_name] = wizard_result[:preset_value].stringify_keys
|
148
|
+
test_args = wizard_result[:test_args]
|
149
|
+
if options.get_option(:default, mandatory: true)
|
150
|
+
formatter.display_status("Setting config preset as default for #{identification[:product]}")
|
151
|
+
@config_presets[CONF_PRESET_DEFAULTS][identification[:product].to_s] = wiz_preset_name
|
152
|
+
else
|
153
|
+
test_args = "-P#{wiz_preset_name} #{test_args}"
|
154
|
+
end
|
155
|
+
# TODO: actually test the command
|
156
|
+
return Main.result_status("You can test with:\n#{Info::CMD_NAME} #{identification[:product]} #{test_args}")
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|