aspera-cli 4.14.0 → 4.16.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/BUGS.md +29 -3
- data/CHANGELOG.md +300 -185
- data/CONTRIBUTING.md +74 -23
- data/README.md +2346 -1619
- data/bin/ascli +16 -25
- data/bin/asession +15 -15
- data/examples/dascli +2 -2
- data/examples/proxy.pac +1 -1
- data/lib/aspera/aoc.rb +216 -150
- data/lib/aspera/ascmd.rb +25 -18
- data/lib/aspera/assert.rb +45 -0
- data/lib/aspera/cli/basic_auth_plugin.rb +9 -6
- data/lib/aspera/cli/error.rb +17 -0
- data/lib/aspera/cli/extended_value.rb +51 -16
- data/lib/aspera/cli/formatter.rb +276 -174
- data/lib/aspera/cli/hints.rb +81 -0
- data/lib/aspera/cli/main.rb +114 -147
- data/lib/aspera/cli/manager.rb +181 -136
- data/lib/aspera/cli/plugin.rb +82 -64
- data/lib/aspera/cli/plugins/alee.rb +0 -1
- data/lib/aspera/cli/plugins/aoc.rb +327 -331
- data/lib/aspera/cli/plugins/ats.rb +12 -8
- data/lib/aspera/cli/plugins/bss.rb +2 -2
- data/lib/aspera/cli/plugins/config.rb +575 -439
- data/lib/aspera/cli/plugins/console.rb +40 -0
- data/lib/aspera/cli/plugins/cos.rb +4 -5
- data/lib/aspera/cli/plugins/faspex.rb +111 -92
- data/lib/aspera/cli/plugins/faspex5.rb +245 -182
- data/lib/aspera/cli/plugins/node.rb +239 -160
- data/lib/aspera/cli/plugins/orchestrator.rb +56 -19
- data/lib/aspera/cli/plugins/preview.rb +54 -38
- data/lib/aspera/cli/plugins/server.rb +63 -20
- data/lib/aspera/cli/plugins/shares.rb +64 -38
- data/lib/aspera/cli/sync_actions.rb +68 -0
- data/lib/aspera/cli/transfer_agent.rb +64 -67
- data/lib/aspera/cli/transfer_progress.rb +73 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/colors.rb +3 -1
- data/lib/aspera/command_line_builder.rb +27 -22
- data/lib/aspera/cos_node.rb +6 -4
- data/lib/aspera/coverage.rb +22 -0
- data/lib/aspera/data_repository.rb +33 -2
- data/lib/aspera/environment.rb +21 -8
- data/lib/aspera/fasp/agent_alpha.rb +116 -0
- data/lib/aspera/fasp/agent_base.rb +40 -76
- data/lib/aspera/fasp/agent_connect.rb +21 -22
- data/lib/aspera/fasp/agent_direct.rb +169 -179
- data/lib/aspera/fasp/agent_httpgw.rb +200 -195
- data/lib/aspera/fasp/agent_node.rb +43 -35
- data/lib/aspera/fasp/agent_trsdk.rb +124 -41
- data/lib/aspera/fasp/error_info.rb +2 -2
- data/lib/aspera/fasp/faux_file.rb +52 -0
- data/lib/aspera/fasp/installation.rb +89 -191
- data/lib/aspera/fasp/management.rb +249 -0
- data/lib/aspera/fasp/parameters.rb +86 -47
- data/lib/aspera/fasp/parameters.yaml +75 -8
- data/lib/aspera/fasp/products.rb +162 -0
- data/lib/aspera/fasp/resume_policy.rb +7 -5
- data/lib/aspera/fasp/sync.rb +273 -0
- data/lib/aspera/fasp/transfer_spec.rb +10 -8
- data/lib/aspera/fasp/uri.rb +6 -6
- data/lib/aspera/faspex_gw.rb +11 -8
- data/lib/aspera/faspex_postproc.rb +8 -7
- data/lib/aspera/hash_ext.rb +2 -2
- data/lib/aspera/id_generator.rb +3 -1
- data/lib/aspera/json_rpc.rb +51 -0
- data/lib/aspera/keychain/encrypted_hash.rb +46 -11
- data/lib/aspera/keychain/macos_security.rb +15 -13
- data/lib/aspera/line_logger.rb +23 -0
- data/lib/aspera/log.rb +61 -19
- data/lib/aspera/nagios.rb +7 -2
- data/lib/aspera/node.rb +105 -21
- data/lib/aspera/node_simulator.rb +214 -0
- data/lib/aspera/oauth.rb +57 -36
- data/lib/aspera/open_application.rb +4 -4
- data/lib/aspera/persistency_action_once.rb +13 -14
- data/lib/aspera/persistency_folder.rb +5 -4
- data/lib/aspera/preview/file_types.rb +56 -268
- data/lib/aspera/preview/generator.rb +28 -39
- data/lib/aspera/preview/options.rb +2 -0
- data/lib/aspera/preview/terminal.rb +36 -16
- data/lib/aspera/preview/utils.rb +23 -29
- data/lib/aspera/proxy_auto_config.rb +6 -3
- data/lib/aspera/rest.rb +127 -80
- data/lib/aspera/rest_call_error.rb +1 -1
- data/lib/aspera/rest_error_analyzer.rb +16 -14
- data/lib/aspera/rest_errors_aspera.rb +39 -34
- data/lib/aspera/secret_hider.rb +18 -17
- data/lib/aspera/ssh.rb +10 -5
- data/lib/aspera/temp_file_manager.rb +11 -4
- data/lib/aspera/web_auth.rb +10 -7
- data/lib/aspera/web_server_simple.rb +11 -5
- data.tar.gz.sig +0 -0
- metadata +108 -39
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/listener/line_dump.rb +0 -19
- data/lib/aspera/cli/listener/logger.rb +0 -22
- data/lib/aspera/cli/listener/progress.rb +0 -50
- data/lib/aspera/cli/listener/progress_multi.rb +0 -84
- data/lib/aspera/cli/plugins/sync.rb +0 -44
- data/lib/aspera/fasp/listener.rb +0 -13
- data/lib/aspera/sync.rb +0 -213
|
@@ -1,33 +1,54 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'aspera/cli/plugins/node'
|
|
4
|
-
|
|
4
|
+
require 'aspera/assert'
|
|
5
5
|
module Aspera
|
|
6
6
|
module Cli
|
|
7
7
|
module Plugins
|
|
8
8
|
# Plugin for Aspera Shares v1
|
|
9
9
|
class Shares < Aspera::Cli::BasicAuthPlugin
|
|
10
|
+
API_BASE = 'node_api'
|
|
10
11
|
class << self
|
|
11
|
-
def detect(
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
def detect(address_or_url)
|
|
13
|
+
address_or_url = "https://#{address_or_url}" unless address_or_url.match?(%r{^[a-z]{1,6}://})
|
|
14
|
+
api = Rest.new(base_url: address_or_url, redirect_max: 1)
|
|
15
|
+
found = false
|
|
14
16
|
begin
|
|
15
17
|
# shall fail: shares requires auth, but we check error message
|
|
16
18
|
# TODO: use ping instead ?
|
|
17
|
-
api.read(
|
|
19
|
+
api.read("#{API_BASE}/app")
|
|
18
20
|
rescue RestCallError => e
|
|
19
21
|
if e.response.code.to_s.eql?('401') && e.response.body.eql?('{"error":{"user_message":"API user authentication failed"}}')
|
|
20
|
-
|
|
22
|
+
found = true
|
|
21
23
|
end
|
|
22
24
|
end
|
|
23
|
-
nil
|
|
25
|
+
return nil unless found
|
|
26
|
+
version = 'unknown'
|
|
27
|
+
test_page = api.call({ operation: 'GET', subpath: 'login' })
|
|
28
|
+
if (m = test_page[:http].body.match(/\(v(1\..*)\)/))
|
|
29
|
+
version = m[1]
|
|
30
|
+
end
|
|
31
|
+
return {
|
|
32
|
+
version: version,
|
|
33
|
+
url: address_or_url
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def wizard(object:, private_key_path: nil, pub_key_pem: nil)
|
|
38
|
+
options = object.options
|
|
39
|
+
return {
|
|
40
|
+
preset_value: {
|
|
41
|
+
url: options.get_option(:url, mandatory: true),
|
|
42
|
+
username: options.get_option(:username, mandatory: true),
|
|
43
|
+
password: options.get_option(:password, mandatory: true)
|
|
44
|
+
},
|
|
45
|
+
test_args: 'files br /'
|
|
46
|
+
}
|
|
24
47
|
end
|
|
25
48
|
end
|
|
26
49
|
|
|
27
50
|
def initialize(env)
|
|
28
51
|
super(env)
|
|
29
|
-
options.declare(:type, 'Type of user/group for operations', values: %i[any local ldap saml], default: :any)
|
|
30
|
-
options.parse_options!
|
|
31
52
|
end
|
|
32
53
|
|
|
33
54
|
SAML_IMPORT_MANDATORY = %w[id name_id].freeze
|
|
@@ -44,7 +65,7 @@ module Aspera
|
|
|
44
65
|
nagios = Nagios.new
|
|
45
66
|
begin
|
|
46
67
|
Rest
|
|
47
|
-
.new(base_url: options.get_option(:url, mandatory: true)
|
|
68
|
+
.new(base_url: "#{options.get_option(:url, mandatory: true)}/#{API_BASE}")
|
|
48
69
|
.call(
|
|
49
70
|
operation: 'GET',
|
|
50
71
|
subpath: 'ping',
|
|
@@ -56,69 +77,74 @@ module Aspera
|
|
|
56
77
|
end
|
|
57
78
|
return nagios.result
|
|
58
79
|
when :repository, :files
|
|
59
|
-
api_shares_node = basic_auth_api(
|
|
80
|
+
api_shares_node = basic_auth_api(API_BASE)
|
|
60
81
|
repo_command = options.get_next_command(Node::COMMANDS_SHARES)
|
|
61
|
-
return Node.new(@agents
|
|
82
|
+
return Node.new(@agents, api: api_shares_node).execute_action(repo_command)
|
|
62
83
|
when :admin
|
|
63
84
|
api_shares_admin = basic_auth_api('api/v1')
|
|
64
|
-
admin_command = options.get_next_command(%i[user group
|
|
85
|
+
admin_command = options.get_next_command(%i[node share transfer_settings user group].freeze)
|
|
65
86
|
case admin_command
|
|
66
87
|
when :node
|
|
67
88
|
return entity_action(api_shares_admin, 'data/nodes')
|
|
89
|
+
when :share
|
|
90
|
+
share_command = options.get_next_command(%i[user_permissions group_permissions].concat(Plugin::ALL_OPS))
|
|
91
|
+
case share_command
|
|
92
|
+
when *Plugin::ALL_OPS
|
|
93
|
+
return entity_command(share_command, api_shares_admin, 'data/shares')
|
|
94
|
+
# return {type: :object_list, data: all_shares, fields: %w[id name status status_message]}
|
|
95
|
+
when :user_permissions, :group_permissions
|
|
96
|
+
share_id = instance_identifier
|
|
97
|
+
return entity_action(api_shares_admin, "data/shares/#{share_id}/#{share_command}")
|
|
98
|
+
end
|
|
99
|
+
when :transfer_settings
|
|
100
|
+
xfer_settings_command = options.get_next_command(%i[show modify])
|
|
101
|
+
return entity_command(xfer_settings_command, api_shares_admin, 'data/transfer_settings', is_singleton: true)
|
|
68
102
|
when :user, :group
|
|
69
103
|
entity_type = admin_command
|
|
70
|
-
entities_location = options.
|
|
104
|
+
entities_location = options.get_next_command(%i[all local ldap saml])
|
|
71
105
|
entities_path = "data/#{entities_location}_#{entity_type}s"
|
|
72
106
|
entity_action = nil
|
|
73
107
|
case entities_location
|
|
74
|
-
when :
|
|
108
|
+
when :all
|
|
75
109
|
entities_path = "data/#{entity_type}s"
|
|
76
110
|
entity_action = %i[list show delete]
|
|
77
111
|
entity_action.concat(USR_GRP_SETTINGS)
|
|
78
112
|
entity_action.push(:users) if entity_type.eql?(:group)
|
|
79
113
|
entity_action.freeze
|
|
80
114
|
when :local
|
|
81
|
-
entity_action = %i[list show create modify
|
|
115
|
+
entity_action = %i[list show delete create modify].freeze
|
|
82
116
|
when :ldap
|
|
83
117
|
entity_action = %i[add].freeze
|
|
84
118
|
when :saml
|
|
85
119
|
entity_action = %i[import].freeze
|
|
86
120
|
end
|
|
87
121
|
entity_verb = options.get_next_command(entity_action)
|
|
88
|
-
# entity_path = "#{entities_path}/#{instance_identifier}" if %i[app_authorizations share_permissions].include?(entity_verb)
|
|
89
122
|
case entity_verb
|
|
90
|
-
when *Plugin::ALL_OPS
|
|
123
|
+
when *Plugin::ALL_OPS # list, show, delete, create, modify
|
|
91
124
|
display_fields = entity_type.eql?(:user) ? %w[id username first_name last_name email] : nil
|
|
92
|
-
display_fields.push(:directory_user) if entity_type.eql?(:user) && entities_location.eql?(:
|
|
125
|
+
display_fields.push(:directory_user) if entity_type.eql?(:user) && entities_location.eql?(:all)
|
|
93
126
|
return entity_command(entity_verb, api_shares_admin, entities_path, display_fields: display_fields)
|
|
94
|
-
when
|
|
95
|
-
|
|
127
|
+
when *USR_GRP_SETTINGS # transfer_settings, app_authorizations, share_permissions
|
|
128
|
+
group_id = instance_identifier
|
|
129
|
+
entities_path = "#{entities_path}/#{group_id}/#{entity_verb}"
|
|
130
|
+
return entity_action(api_shares_admin, entities_path, is_singleton: !entity_verb.eql?(:share_permissions))
|
|
131
|
+
when :import # saml
|
|
132
|
+
return do_bulk_operation(command: entity_verb, descr: 'user information') do |entity_parameters|
|
|
96
133
|
entity_parameters = entity_parameters.transform_keys{|k|k.gsub(/\s+/, '_').downcase}
|
|
97
|
-
|
|
134
|
+
assert_type(entity_parameters, Hash)
|
|
98
135
|
SAML_IMPORT_MANDATORY.each{|p|raise "missing mandatory field: #{p}" if entity_parameters[p].nil?}
|
|
99
136
|
entity_parameters.each_key do |p|
|
|
100
137
|
raise "unsupported field: #{p}, use: #{SAML_IMPORT_ALLOWED.join(',')}" unless SAML_IMPORT_ALLOWED.include?(p)
|
|
101
138
|
end
|
|
102
139
|
api_shares_admin.create("#{entities_path}/import", entity_parameters)[:data]
|
|
103
140
|
end
|
|
104
|
-
when :add
|
|
105
|
-
return do_bulk_operation(
|
|
106
|
-
raise "expecting string (name), have #{entity_name.class}" unless entity_name.is_a?(String)
|
|
141
|
+
when :add # ldap
|
|
142
|
+
return do_bulk_operation(command: entity_verb, descr: "#{entity_type} name", values: String) do |entity_name|
|
|
107
143
|
api_shares_admin.create(entities_path, {entity_type=>entity_name})[:data]
|
|
108
144
|
end
|
|
109
|
-
when
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return entity_action(api_shares_admin, entities_path, is_singleton: !entity_verb.eql?(:share_permissions))
|
|
113
|
-
end
|
|
114
|
-
when :share
|
|
115
|
-
share_command = options.get_next_command(%i[user_permissions group_permissions].concat(Plugin::ALL_OPS))
|
|
116
|
-
case share_command
|
|
117
|
-
when *Plugin::ALL_OPS
|
|
118
|
-
return entity_command(share_command, api_shares_admin, 'data/shares')
|
|
119
|
-
# return {type: :object_list, data: all_shares, fields: %w[id name status status_message]}
|
|
120
|
-
when :user_permissions, :group_permissions
|
|
121
|
-
return entity_action(api_shares_admin, "data/shares/#{instance_identifier}/#{share_command}")
|
|
145
|
+
when :users # group
|
|
146
|
+
raise "TODO, not implemented"
|
|
147
|
+
else error_unexpected_value(entity_verb)
|
|
122
148
|
end
|
|
123
149
|
end
|
|
124
150
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/fasp/sync'
|
|
4
|
+
require 'aspera/assert'
|
|
5
|
+
|
|
6
|
+
module Aspera
|
|
7
|
+
module Cli
|
|
8
|
+
# Module for sync actions
|
|
9
|
+
module SyncActions
|
|
10
|
+
SIMPLE_ARGUMENTS_SYNC = {
|
|
11
|
+
direction: Aspera::Fasp::Sync::DIRECTIONS,
|
|
12
|
+
local_dir: String,
|
|
13
|
+
remote_dir: String
|
|
14
|
+
}.stringify_keys.freeze
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def declare_options(options)
|
|
18
|
+
options.declare(:sync_info, 'Information for sync instance and sessions', types: Hash)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def execute_sync_action(&block)
|
|
23
|
+
assert(block){'No block given'}
|
|
24
|
+
command = options.get_next_command(%i[start admin])
|
|
25
|
+
# try to get 3 arguments as simple arguments
|
|
26
|
+
case command
|
|
27
|
+
when :start
|
|
28
|
+
simple_session_args = {}
|
|
29
|
+
SIMPLE_ARGUMENTS_SYNC.each do |arg, check|
|
|
30
|
+
value = options.get_next_argument(
|
|
31
|
+
arg,
|
|
32
|
+
type: check.is_a?(Class) ? check : nil,
|
|
33
|
+
expected: check.is_a?(Class) ? :single : check,
|
|
34
|
+
mandatory: false)
|
|
35
|
+
break if value.nil?
|
|
36
|
+
simple_session_args[arg] = value.to_s
|
|
37
|
+
end
|
|
38
|
+
async_params = nil
|
|
39
|
+
if simple_session_args.empty?
|
|
40
|
+
async_params = options.get_option(:sync_info, mandatory: true)
|
|
41
|
+
else
|
|
42
|
+
raise Cli::BadArgument,
|
|
43
|
+
"Provide zero or 3 arguments: #{SIMPLE_ARGUMENTS_SYNC.keys.join(',')}" unless simple_session_args.keys.sort == SIMPLE_ARGUMENTS_SYNC.keys.sort
|
|
44
|
+
async_params = options.get_option(
|
|
45
|
+
:sync_info,
|
|
46
|
+
mandatory: false,
|
|
47
|
+
default: {'sessions' => [{'name' => File.basename(simple_session_args['local_dir'])}]})
|
|
48
|
+
assert_type(async_params, Hash){'sync_info'}
|
|
49
|
+
assert_type(async_params['sessions'], Array){'sync_info[sessions]'}
|
|
50
|
+
assert_type(async_params['sessions'].first, Hash){'sync_info[sessions][0]'}
|
|
51
|
+
async_params['sessions'].first.merge!(simple_session_args)
|
|
52
|
+
end
|
|
53
|
+
Log.log.debug{Log.dump('async_params', async_params)}
|
|
54
|
+
Aspera::Fasp::Sync.start(async_params, &block)
|
|
55
|
+
return Main.result_success
|
|
56
|
+
when :admin
|
|
57
|
+
command2 = options.get_next_command([:status])
|
|
58
|
+
case command2
|
|
59
|
+
when :status
|
|
60
|
+
sync_session_name = options.get_next_argument('name of sync session', mandatory: false, type: String)
|
|
61
|
+
async_params = options.get_option(:sync_info, mandatory: true)
|
|
62
|
+
return {type: :single_object, data: Aspera::Fasp::Sync.admin_status(async_params, sync_session_name)}
|
|
63
|
+
end # command2
|
|
64
|
+
end # command
|
|
65
|
+
end # execute_action
|
|
66
|
+
end # SyncActions
|
|
67
|
+
end # Cli
|
|
68
|
+
end # Aspera
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'aspera/fasp/agent_base'
|
|
3
4
|
require 'aspera/fasp/transfer_spec'
|
|
4
|
-
require 'aspera/cli/listener/logger'
|
|
5
|
-
require 'aspera/cli/listener/progress_multi'
|
|
6
5
|
require 'aspera/cli/info'
|
|
6
|
+
require 'aspera/log'
|
|
7
|
+
require 'aspera/assert'
|
|
7
8
|
|
|
8
9
|
module Aspera
|
|
9
10
|
module Cli
|
|
@@ -16,7 +17,7 @@ module Aspera
|
|
|
16
17
|
# special value for --sources : read file list from transfer spec (--ts)
|
|
17
18
|
FILE_LIST_FROM_TRANSFER_SPEC = '@ts'
|
|
18
19
|
FILE_LIST_OPTIONS = [FILE_LIST_FROM_ARGS, FILE_LIST_FROM_TRANSFER_SPEC, 'Array'].freeze
|
|
19
|
-
|
|
20
|
+
DEFAULT_TRANSFER_NOTIFY_TEMPLATE = <<~END_OF_TEMPLATE
|
|
20
21
|
From: <%=from_name%> <<%=from_email%>>
|
|
21
22
|
To: <<%=to%>>
|
|
22
23
|
Subject: <%=subject%>
|
|
@@ -29,8 +30,8 @@ module Aspera
|
|
|
29
30
|
private_constant :FILE_LIST_FROM_ARGS,
|
|
30
31
|
:FILE_LIST_FROM_TRANSFER_SPEC,
|
|
31
32
|
:FILE_LIST_OPTIONS,
|
|
32
|
-
:
|
|
33
|
-
TRANSFER_AGENTS =
|
|
33
|
+
:DEFAULT_TRANSFER_NOTIFY_TEMPLATE
|
|
34
|
+
TRANSFER_AGENTS = Fasp::AgentBase.agent_list.freeze
|
|
34
35
|
|
|
35
36
|
class << self
|
|
36
37
|
# @return :success if all sessions statuses returned by "start" are success
|
|
@@ -43,15 +44,15 @@ module Aspera
|
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
# @param env external objects: option manager, config file manager
|
|
46
|
-
def initialize(opt_mgr,
|
|
47
|
+
def initialize(opt_mgr, config_plugin)
|
|
47
48
|
@opt_mgr = opt_mgr
|
|
48
|
-
@config =
|
|
49
|
+
@config = config_plugin
|
|
49
50
|
# command line can override transfer spec
|
|
50
|
-
@
|
|
51
|
+
@transfer_spec_command_line = {'create_dir' => true}
|
|
52
|
+
# options for transfer agent
|
|
51
53
|
@transfer_info = {}
|
|
52
54
|
# the currently selected transfer agent
|
|
53
55
|
@agent = nil
|
|
54
|
-
@progress_listener = Listener::ProgressMulti.new
|
|
55
56
|
# source/destination pair, like "paths" of transfer spec
|
|
56
57
|
@transfer_paths = nil
|
|
57
58
|
@opt_mgr.declare(:ts, 'Override transfer spec values', types: Hash, handler: {o: self, m: :option_transfer_spec})
|
|
@@ -59,73 +60,68 @@ module Aspera
|
|
|
59
60
|
@opt_mgr.declare(:sources, "How list of transferred files is provided (#{FILE_LIST_OPTIONS.join(',')})")
|
|
60
61
|
@opt_mgr.declare(:src_type, 'Type of file list', values: %i[list pair], default: :list)
|
|
61
62
|
@opt_mgr.declare(:transfer, 'Type of transfer agent', values: TRANSFER_AGENTS, default: :direct)
|
|
62
|
-
@opt_mgr.declare(:transfer_info, 'Parameters for transfer agent', types: Hash, handler: {o: self, m: :
|
|
63
|
-
@opt_mgr.declare(:progress, 'Type of progress bar', values: %i[none native multi], default: :native)
|
|
63
|
+
@opt_mgr.declare(:transfer_info, 'Parameters for transfer agent', types: Hash, handler: {o: self, m: :transfer_info})
|
|
64
64
|
@opt_mgr.parse_options!
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
def option_transfer_spec; @
|
|
67
|
+
def option_transfer_spec; @transfer_spec_command_line; end
|
|
68
68
|
|
|
69
69
|
# multiple option are merged
|
|
70
70
|
def option_transfer_spec=(value)
|
|
71
|
-
|
|
72
|
-
@
|
|
71
|
+
assert_type(value, Hash){'ts'}
|
|
72
|
+
@transfer_spec_command_line.deep_merge!(value)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
# add other transfer spec parameters
|
|
76
|
-
def option_transfer_spec_deep_merge(ts); @
|
|
76
|
+
def option_transfer_spec_deep_merge(ts); @transfer_spec_command_line.deep_merge!(ts); end
|
|
77
77
|
|
|
78
78
|
# @return [Hash] transfer spec with updated values from command line, including removed values
|
|
79
79
|
def updated_ts(transfer_spec={})
|
|
80
|
-
transfer_spec.deep_merge!(@
|
|
80
|
+
transfer_spec.deep_merge!(@transfer_spec_command_line)
|
|
81
81
|
# recursively remove values that are nil (user wants to delete)
|
|
82
82
|
transfer_spec.deep_do { |hash, key, value, _unused| hash.delete(key) if value.nil?}
|
|
83
83
|
return transfer_spec
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
attr_reader :transfer_info
|
|
87
87
|
|
|
88
88
|
# multiple option are merged
|
|
89
|
-
def
|
|
90
|
-
raise 'option transfer_info shall be a Hash' unless value.is_a?(Hash)
|
|
89
|
+
def transfer_info=(value)
|
|
91
90
|
@transfer_info.deep_merge!(value)
|
|
92
91
|
end
|
|
93
92
|
|
|
94
93
|
def agent_instance=(instance)
|
|
95
94
|
@agent = instance
|
|
96
|
-
@agent.add_listener(Listener::Logger.new)
|
|
97
|
-
# use local progress bar if asked so, or if native and non local ascp (because only local ascp has native progress bar)
|
|
98
|
-
if @opt_mgr.get_option(:progress, mandatory: true).eql?(:multi) ||
|
|
99
|
-
(@opt_mgr.get_option(:progress, mandatory: true).eql?(:native) && !instance.class.to_s.eql?('Aspera::Fasp::AgentDirect'))
|
|
100
|
-
@agent.add_listener(@progress_listener)
|
|
101
|
-
end
|
|
102
95
|
end
|
|
103
96
|
|
|
104
97
|
# analyze options and create new agent if not already created or set
|
|
105
|
-
def
|
|
106
|
-
return
|
|
98
|
+
def agent_instance
|
|
99
|
+
return @agent unless @agent.nil?
|
|
107
100
|
agent_type = @opt_mgr.get_option(:transfer, mandatory: true)
|
|
108
101
|
# agent plugin is loaded on demand to avoid loading unnecessary dependencies
|
|
109
102
|
require "aspera/fasp/agent_#{agent_type}"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
103
|
+
# set keys as symbols
|
|
104
|
+
agent_options = @opt_mgr.get_option(:transfer_info).symbolize_keys
|
|
105
|
+
# special cases
|
|
106
|
+
case agent_type
|
|
107
|
+
when :node
|
|
108
|
+
if agent_options.empty?
|
|
109
|
+
param_set_name = @config.get_plugin_default_config_name(:node)
|
|
110
|
+
raise Cli::BadArgument, "No default node configured. Please specify #{Manager.option_name_to_line(:transfer_info)}" if param_set_name.nil?
|
|
111
|
+
agent_options = @config.preset_by_name(param_set_name).symbolize_keys
|
|
112
|
+
end
|
|
113
|
+
when :direct
|
|
114
|
+
# by default do not display ascp native progress bar
|
|
115
|
+
agent_options[:quiet] = true unless agent_options.key?(:quiet)
|
|
116
|
+
agent_options[:check_ignore] = ->(host, port){@config.ignore_cert?(host, port)}
|
|
117
|
+
agent_options[:trusted_certs] = @config.trusted_cert_locations(files_only: true) unless agent_options.key?(:trusted_certs)
|
|
122
118
|
end
|
|
123
|
-
|
|
124
|
-
agent_options = agent_options.symbolize_keys
|
|
119
|
+
agent_options[:progress] = @config.progress_bar
|
|
125
120
|
# get agent instance
|
|
126
121
|
new_agent = Kernel.const_get("Aspera::Fasp::Agent#{agent_type.capitalize}").new(agent_options)
|
|
127
122
|
self.agent_instance = new_agent
|
|
128
|
-
|
|
123
|
+
Log.log.debug{"transfer agent is a #{@agent.class}"}
|
|
124
|
+
return @agent
|
|
129
125
|
end
|
|
130
126
|
|
|
131
127
|
# return destination folder for transfers
|
|
@@ -135,13 +131,13 @@ module Aspera
|
|
|
135
131
|
dest_folder = @opt_mgr.get_option(:to_folder)
|
|
136
132
|
# do not expand path, if user wants to expand path: user @path:
|
|
137
133
|
return dest_folder unless dest_folder.nil?
|
|
138
|
-
dest_folder = @
|
|
134
|
+
dest_folder = @transfer_spec_command_line['destination_root']
|
|
139
135
|
return dest_folder unless dest_folder.nil?
|
|
140
136
|
# default: / on remote, . on local
|
|
141
137
|
case direction.to_s
|
|
142
138
|
when Fasp::TransferSpec::DIRECTION_SEND then dest_folder = '/'
|
|
143
139
|
when Fasp::TransferSpec::DIRECTION_RECEIVE then dest_folder = '.'
|
|
144
|
-
else
|
|
140
|
+
else error_unexpected_value(direction)
|
|
145
141
|
end
|
|
146
142
|
return dest_folder
|
|
147
143
|
end
|
|
@@ -161,7 +157,7 @@ module Aspera
|
|
|
161
157
|
# return cache if set
|
|
162
158
|
return @transfer_paths unless @transfer_paths.nil?
|
|
163
159
|
# start with lower priority : get paths from transfer spec on command line
|
|
164
|
-
@transfer_paths = @
|
|
160
|
+
@transfer_paths = @transfer_spec_command_line['paths'] if @transfer_spec_command_line.key?('paths')
|
|
165
161
|
# is there a source list option ?
|
|
166
162
|
file_list = @opt_mgr.get_option(:sources)
|
|
167
163
|
case file_list
|
|
@@ -169,34 +165,35 @@ module Aspera
|
|
|
169
165
|
Log.log.debug('getting file list as parameters')
|
|
170
166
|
# get remaining arguments
|
|
171
167
|
file_list = @opt_mgr.get_next_argument('source file list', expected: :multiple)
|
|
172
|
-
raise
|
|
168
|
+
raise Cli::BadArgument, 'specify at least one file on command line or use ' \
|
|
173
169
|
"--sources=#{FILE_LIST_FROM_TRANSFER_SPEC} to use transfer spec" if !file_list.is_a?(Array) || file_list.empty?
|
|
174
170
|
when FILE_LIST_FROM_TRANSFER_SPEC
|
|
175
171
|
Log.log.debug('assume list provided in transfer spec')
|
|
176
172
|
special_case_direct_with_list =
|
|
177
173
|
@opt_mgr.get_option(:transfer, mandatory: true).eql?(:direct) &&
|
|
178
|
-
Fasp::Parameters.ts_has_ascp_file_list(@
|
|
179
|
-
raise
|
|
174
|
+
Fasp::Parameters.ts_has_ascp_file_list(@transfer_spec_command_line, @opt_mgr.get_option(:transfer_info))
|
|
175
|
+
raise Cli::BadArgument, 'transfer spec on command line must have sources' if @transfer_paths.nil? && !special_case_direct_with_list
|
|
180
176
|
# here we assume check of sources is made in transfer agent
|
|
181
177
|
return @transfer_paths
|
|
182
178
|
when Array
|
|
183
179
|
Log.log.debug('getting file list as extended value')
|
|
184
|
-
raise
|
|
180
|
+
raise Cli::BadArgument, 'sources must be a Array of String' if !file_list.reject{|f|f.is_a?(String)}.empty?
|
|
185
181
|
else
|
|
186
|
-
raise
|
|
182
|
+
raise Cli::BadArgument, "sources must be a Array, not #{file_list.class}"
|
|
187
183
|
end
|
|
188
184
|
# here, file_list is an Array or String
|
|
189
185
|
if !@transfer_paths.nil?
|
|
190
186
|
Log.log.warn('--sources overrides paths from --ts')
|
|
191
187
|
end
|
|
192
|
-
|
|
188
|
+
source_type=@opt_mgr.get_option(:src_type, mandatory: true)
|
|
189
|
+
case source_type
|
|
193
190
|
when :list
|
|
194
191
|
# when providing a list, just specify source
|
|
195
192
|
@transfer_paths = file_list.map{|i|{'source' => i}}
|
|
196
193
|
when :pair
|
|
197
|
-
|
|
194
|
+
assert(file_list.length.even?, exception_class: Cli::BadArgument){"When using pair, provide an even number of paths: #{file_list.length}"}
|
|
198
195
|
@transfer_paths = file_list.each_slice(2).to_a.map{|s, d|{'source' => s, 'destination' => d}}
|
|
199
|
-
else
|
|
196
|
+
else error_unexpected_value(source_type)
|
|
200
197
|
end
|
|
201
198
|
Log.log.debug{"paths=#{@transfer_paths}"}
|
|
202
199
|
return @transfer_paths
|
|
@@ -207,44 +204,44 @@ module Aspera
|
|
|
207
204
|
# @param rest_token [Rest] if oauth token regeneration supported
|
|
208
205
|
def start(transfer_spec, rest_token: nil)
|
|
209
206
|
# check parameters
|
|
210
|
-
|
|
207
|
+
assert_type(transfer_spec, Hash){'transfer_spec'}
|
|
211
208
|
# process :src option
|
|
212
209
|
case transfer_spec['direction']
|
|
213
210
|
when Fasp::TransferSpec::DIRECTION_RECEIVE
|
|
214
211
|
# init default if required in any case
|
|
215
|
-
@
|
|
212
|
+
@transfer_spec_command_line['destination_root'] ||= destination_folder(transfer_spec['direction'])
|
|
216
213
|
when Fasp::TransferSpec::DIRECTION_SEND
|
|
217
214
|
if transfer_spec.dig('tags', Fasp::TransferSpec::TAG_RESERVED, 'node', 'access_key')
|
|
218
215
|
# gen4
|
|
219
|
-
@
|
|
216
|
+
@transfer_spec_command_line.delete('destination_root') if @transfer_spec_command_line.key?('destination_root_id')
|
|
220
217
|
elsif transfer_spec.key?('token')
|
|
221
218
|
# gen3
|
|
222
219
|
# in that case, destination is set in return by application (API/upload_setup)
|
|
223
220
|
# but to_folder was used in initial API call
|
|
224
|
-
@
|
|
221
|
+
@transfer_spec_command_line.delete('destination_root')
|
|
225
222
|
else
|
|
226
223
|
# init default if required
|
|
227
|
-
@
|
|
224
|
+
@transfer_spec_command_line['destination_root'] ||= destination_folder(transfer_spec['direction'])
|
|
228
225
|
end
|
|
229
226
|
end
|
|
230
227
|
# update command line paths, unless destination already has one
|
|
231
|
-
@
|
|
228
|
+
@transfer_spec_command_line['paths'] = transfer_spec['paths'] || ts_source_paths
|
|
232
229
|
# updated transfer spec with command line
|
|
233
230
|
updated_ts(transfer_spec)
|
|
231
|
+
# if TS from app has content_protection (e.g. F5), that means content is protected: ask password if not provided
|
|
232
|
+
if transfer_spec['content_protection'].eql?('decrypt') && !transfer_spec.key?('content_protection_password')
|
|
233
|
+
transfer_spec['content_protection_password'] = @opt_mgr.prompt_user_input('content protection password', true)
|
|
234
|
+
end
|
|
234
235
|
# create transfer agent
|
|
235
|
-
|
|
236
|
-
Log.log.debug{"transfer agent is a #{@agent.class}"}
|
|
237
|
-
@agent.start_transfer(transfer_spec, token_regenerator: rest_token)
|
|
236
|
+
agent_instance.start_transfer(transfer_spec, token_regenerator: rest_token)
|
|
238
237
|
# list of: :success or "error message string"
|
|
239
|
-
result =
|
|
240
|
-
@progress_listener.reset
|
|
241
|
-
Fasp::AgentBase.validate_status_list(result)
|
|
238
|
+
result = agent_instance.wait_for_completion
|
|
242
239
|
send_email_transfer_notification(transfer_spec, result)
|
|
243
240
|
return result
|
|
244
241
|
end
|
|
245
242
|
|
|
246
243
|
def send_email_transfer_notification(transfer_spec, statuses)
|
|
247
|
-
return if @opt_mgr.get_option(:
|
|
244
|
+
return if @opt_mgr.get_option(:notify_to).nil?
|
|
248
245
|
global_status = self.class.session_status(statuses)
|
|
249
246
|
email_vars = {
|
|
250
247
|
global_transfer_status: global_status,
|
|
@@ -252,7 +249,7 @@ module Aspera
|
|
|
252
249
|
body: "Transfer is: #{global_status}",
|
|
253
250
|
ts: transfer_spec
|
|
254
251
|
}
|
|
255
|
-
@config.send_email_template(email_template_default:
|
|
252
|
+
@config.send_email_template(email_template_default: DEFAULT_TRANSFER_NOTIFY_TEMPLATE, values: email_vars)
|
|
256
253
|
end
|
|
257
254
|
|
|
258
255
|
# shut down if agent requires it
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/log'
|
|
4
|
+
require 'aspera/assert'
|
|
5
|
+
require 'ruby-progressbar'
|
|
6
|
+
|
|
7
|
+
module Aspera
|
|
8
|
+
module Cli
|
|
9
|
+
# progress bar for transfers, supports multi-session
|
|
10
|
+
class TransferProgress
|
|
11
|
+
def initialize
|
|
12
|
+
reset
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def reset
|
|
16
|
+
@progress_bar = nil
|
|
17
|
+
# key is session id
|
|
18
|
+
@sessions = {}
|
|
19
|
+
@completed = false
|
|
20
|
+
@title = ''
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def total(key)
|
|
24
|
+
@sessions.values.inject(0){|m, s|m + s[key]}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def event(session_id:, type:, info: nil)
|
|
28
|
+
Log.log.debug{"progress: #{type} #{session_id} #{info}"}
|
|
29
|
+
assert(!session_id.nil? || type.eql?(:pre_start)){'session_id is nil'}
|
|
30
|
+
return if @completed
|
|
31
|
+
if @progress_bar.nil?
|
|
32
|
+
@progress_bar = ProgressBar.create(
|
|
33
|
+
format: '%t %a %B %p%% %r Mbps %E',
|
|
34
|
+
rate_scale: lambda{|rate|rate / Environment::BYTES_PER_MEBIBIT},
|
|
35
|
+
title: '',
|
|
36
|
+
total: nil)
|
|
37
|
+
end
|
|
38
|
+
need_increment = true
|
|
39
|
+
case type
|
|
40
|
+
when :pre_start
|
|
41
|
+
@title = info
|
|
42
|
+
when :session_start
|
|
43
|
+
raise "Session #{session_id} already started" if @sessions[session_id]
|
|
44
|
+
@sessions[session_id] = {
|
|
45
|
+
job_size: 0, # total size of transfer (pre-calc)
|
|
46
|
+
current: 0
|
|
47
|
+
}
|
|
48
|
+
@title = ''
|
|
49
|
+
when :session_size
|
|
50
|
+
@sessions[session_id][:job_size] = info.to_i
|
|
51
|
+
current_total = total(:job_size)
|
|
52
|
+
@progress_bar.total = current_total unless current_total.eql?(@progress_bar.total) || current_total < @progress_bar.progress
|
|
53
|
+
when :transfer
|
|
54
|
+
if !@progress_bar.total.nil?
|
|
55
|
+
need_increment = false
|
|
56
|
+
@sessions[session_id][:current] = info.to_i
|
|
57
|
+
current_total = total(:current)
|
|
58
|
+
@progress_bar.progress = current_total unless @progress_bar.progress.eql?(current_total)
|
|
59
|
+
end
|
|
60
|
+
when :end
|
|
61
|
+
@title = ''
|
|
62
|
+
@completed = true
|
|
63
|
+
@progress_bar.finish
|
|
64
|
+
else
|
|
65
|
+
raise "Unknown event type #{type}"
|
|
66
|
+
end
|
|
67
|
+
new_title = @sessions.length < 2 ? @title : "[#{@sessions.length}] #{@title}"
|
|
68
|
+
@progress_bar.title = new_title unless @progress_bar.title.eql?(new_title)
|
|
69
|
+
@progress_bar.increment if need_increment && !@completed
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/aspera/cli/version.rb
CHANGED
data/lib/aspera/colors.rb
CHANGED
|
@@ -14,10 +14,12 @@ class String
|
|
|
14
14
|
# it adds control chars to set color (and reset at the end).
|
|
15
15
|
VT_STYLES = {
|
|
16
16
|
bold: 1,
|
|
17
|
+
dim: 2,
|
|
17
18
|
italic: 3,
|
|
18
19
|
underline: 4,
|
|
19
20
|
blink: 5,
|
|
20
21
|
reverse_color: 7,
|
|
22
|
+
invisible: 8,
|
|
21
23
|
black: 30,
|
|
22
24
|
red: 31,
|
|
23
25
|
green: 32,
|
|
@@ -38,7 +40,7 @@ class String
|
|
|
38
40
|
private_constant :VT_STYLES
|
|
39
41
|
# defines methods to String, one per entry in VT_STYLES
|
|
40
42
|
VT_STYLES.each do |name, code|
|
|
41
|
-
if $
|
|
43
|
+
if $stdout.tty?
|
|
42
44
|
begin_seq = vt_cmd(code)
|
|
43
45
|
end_code = 0 # by default reset all
|
|
44
46
|
if code <= 7 then code + 20
|