aspera-cli 4.18.1 → 4.20.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 +33 -0
- data/CONTRIBUTING.md +17 -12
- data/README.md +396 -185
- data/bin/asession +26 -19
- data/examples/build_exec +74 -0
- data/examples/{rubyc → build_exec_rubyc} +18 -2
- data/examples/get_proto_file.rb +7 -0
- data/lib/aspera/agent/alpha.rb +8 -8
- data/lib/aspera/agent/base.rb +4 -18
- data/lib/aspera/agent/connect.rb +14 -13
- data/lib/aspera/agent/direct.rb +123 -120
- data/lib/aspera/agent/httpgw.rb +2 -3
- data/lib/aspera/agent/node.rb +10 -10
- data/lib/aspera/agent/trsdk.rb +17 -20
- data/lib/aspera/api/alee.rb +15 -0
- data/lib/aspera/api/aoc.rb +128 -99
- data/lib/aspera/api/ats.rb +1 -1
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/httpgw.rb +104 -64
- data/lib/aspera/api/node.rb +33 -12
- data/lib/aspera/ascmd.rb +56 -48
- data/lib/aspera/ascp/installation.rb +142 -70
- data/lib/aspera/ascp/management.rb +7 -3
- data/lib/aspera/ascp/products.rb +13 -7
- data/lib/aspera/assert.rb +10 -5
- data/lib/aspera/cli/formatter.rb +42 -26
- data/lib/aspera/cli/hints.rb +2 -1
- data/lib/aspera/cli/info.rb +12 -10
- data/lib/aspera/cli/main.rb +16 -13
- data/lib/aspera/cli/manager.rb +15 -10
- data/lib/aspera/cli/plugin.rb +17 -31
- data/lib/aspera/cli/plugin_factory.rb +10 -1
- data/lib/aspera/cli/plugins/alee.rb +3 -3
- data/lib/aspera/cli/plugins/aoc.rb +222 -194
- data/lib/aspera/cli/plugins/ats.rb +16 -14
- data/lib/aspera/cli/plugins/config.rb +66 -53
- data/lib/aspera/cli/plugins/console.rb +3 -3
- data/lib/aspera/cli/plugins/faspex.rb +11 -21
- data/lib/aspera/cli/plugins/faspex5.rb +44 -42
- data/lib/aspera/cli/plugins/faspio.rb +2 -2
- data/lib/aspera/cli/plugins/httpgw.rb +1 -1
- data/lib/aspera/cli/plugins/node.rb +155 -96
- data/lib/aspera/cli/plugins/orchestrator.rb +14 -13
- data/lib/aspera/cli/plugins/preview.rb +8 -9
- data/lib/aspera/cli/plugins/server.rb +6 -10
- data/lib/aspera/cli/plugins/shares.rb +13 -9
- data/lib/aspera/cli/sync_actions.rb +72 -31
- data/lib/aspera/cli/transfer_agent.rb +13 -14
- data/lib/aspera/cli/transfer_progress.rb +36 -18
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/command_line_builder.rb +3 -4
- data/lib/aspera/coverage.rb +13 -1
- data/lib/aspera/environment.rb +59 -10
- data/lib/aspera/faspex_gw.rb +3 -3
- data/lib/aspera/json_rpc.rb +1 -1
- data/lib/aspera/keychain/encrypted_hash.rb +2 -0
- data/lib/aspera/keychain/macos_security.rb +7 -12
- data/lib/aspera/log.rb +4 -4
- data/lib/aspera/node_simulator.rb +1 -1
- data/lib/aspera/oauth/base.rb +39 -45
- data/lib/aspera/oauth/factory.rb +11 -4
- data/lib/aspera/oauth/generic.rb +4 -8
- data/lib/aspera/oauth/jwt.rb +4 -4
- data/lib/aspera/oauth/url_json.rb +3 -2
- data/lib/aspera/oauth/web.rb +10 -6
- data/lib/aspera/persistency_action_once.rb +16 -8
- data/lib/aspera/preview/utils.rb +5 -16
- data/lib/aspera/rest.rb +100 -76
- data/lib/aspera/secret_hider.rb +3 -2
- data/lib/aspera/ssh.rb +1 -1
- data/lib/aspera/transfer/faux_file.rb +7 -5
- data/lib/aspera/transfer/parameters.rb +41 -35
- data/lib/aspera/transfer/spec.rb +16 -18
- data/lib/aspera/transfer/sync.rb +51 -50
- data/lib/aspera/transfer/uri.rb +1 -1
- data/lib/aspera/uri_reader.rb +1 -1
- data/lib/aspera/web_auth.rb +166 -18
- data/lib/aspera/web_server_simple.rb +27 -15
- data/lib/transfer_pb.rb +84 -0
- data/lib/transfer_services_pb.rb +82 -0
- data.tar.gz.sig +0 -0
- metadata +25 -6
- metadata.gz.sig +0 -0
@@ -22,7 +22,7 @@ module Aspera
|
|
22
22
|
next unless base_url.match?('https?://')
|
23
23
|
api = Rest.new(base_url: base_url)
|
24
24
|
test_endpoint = 'api/remote_node_ping'
|
25
|
-
result = api.
|
25
|
+
result = api.call(operation: 'GET', subpath: test_endpoint, headers: {'Accept' => 'application/json'}, query: {format: :json})
|
26
26
|
next unless result[:data]['remote_orchestrator_info']
|
27
27
|
url = result[:http].uri.to_s
|
28
28
|
return {
|
@@ -69,7 +69,7 @@ module Aspera
|
|
69
69
|
# @param format [String] the format to request, 'json', 'xml', nil
|
70
70
|
# @param args [Hash] the arguments to pass
|
71
71
|
# @param xml_arrays [Boolean] if true, force arrays in xml parsing
|
72
|
-
def call_ao(endpoint, prefix: 'api', id: nil, ret_style: nil, format: 'json', args: nil, xml_arrays: true)
|
72
|
+
def call_ao(endpoint, prefix: 'api', id: nil, ret_style: nil, format: 'json', args: nil, xml_arrays: true, http: false)
|
73
73
|
# calls are GET
|
74
74
|
call_args = {operation: 'GET', subpath: endpoint}
|
75
75
|
# specify prefix if necessary
|
@@ -91,9 +91,10 @@ module Aspera
|
|
91
91
|
end
|
92
92
|
end
|
93
93
|
result = @api_orch.call(**call_args)
|
94
|
+
return result[:http] if http
|
94
95
|
result[:data] = XmlSimple.xml_in(result[:http].body, {'ForceArray' => xml_arrays}) if format.eql?('xml')
|
95
96
|
Log.log.debug{Log.dump(:data, result[:data])}
|
96
|
-
return result
|
97
|
+
return result[:data]
|
97
98
|
end
|
98
99
|
|
99
100
|
def execute_action
|
@@ -126,7 +127,7 @@ module Aspera
|
|
126
127
|
when :health
|
127
128
|
nagios = Nagios.new
|
128
129
|
begin
|
129
|
-
info = call_ao('remote_node_ping', format: 'xml', xml_arrays: false)
|
130
|
+
info = call_ao('remote_node_ping', format: 'xml', xml_arrays: false)
|
130
131
|
nagios.add_ok('api', 'accessible')
|
131
132
|
nagios.check_product_version('api', 'orchestrator', info['orchestrator-version'])
|
132
133
|
rescue StandardError => e
|
@@ -134,15 +135,15 @@ module Aspera
|
|
134
135
|
end
|
135
136
|
return nagios.result
|
136
137
|
when :info
|
137
|
-
result = call_ao('remote_node_ping', format: 'xml', xml_arrays: false)
|
138
|
+
result = call_ao('remote_node_ping', format: 'xml', xml_arrays: false)
|
138
139
|
return {type: :single_object, data: result}
|
139
140
|
when :processes
|
140
141
|
# TODO: Bug ? API has only XML format
|
141
|
-
result = call_ao('processes_status', format: 'xml')
|
142
|
+
result = call_ao('processes_status', format: 'xml')
|
142
143
|
return {type: :object_list, data: result['process']}
|
143
144
|
when :plugins
|
144
145
|
# TODO: Bug ? only json format on url
|
145
|
-
result = call_ao('plugin_version')
|
146
|
+
result = call_ao('plugin_version')
|
146
147
|
return {type: :object_list, data: result['Plugin']}
|
147
148
|
when :workflow
|
148
149
|
command = options.get_next_command(%i[list status inputs details start export])
|
@@ -152,23 +153,23 @@ module Aspera
|
|
152
153
|
case command
|
153
154
|
when :status
|
154
155
|
wf_id = nil if wf_id.eql?(SpecialValues::ALL)
|
155
|
-
result = call_ao('workflows_status', id: wf_id)
|
156
|
+
result = call_ao('workflows_status', id: wf_id)
|
156
157
|
return {type: :object_list, data: result['workflows']['workflow']}
|
157
158
|
when :list
|
158
|
-
result = call_ao('workflows_list', id: 0)
|
159
|
+
result = call_ao('workflows_list', id: 0)
|
159
160
|
return {
|
160
161
|
type: :object_list,
|
161
162
|
data: result['workflows']['workflow'],
|
162
163
|
fields: %w[id portable_id name published_status published_revision_id latest_revision_id last_modification]
|
163
164
|
}
|
164
165
|
when :details
|
165
|
-
result = call_ao('workflow_details', id: wf_id)
|
166
|
+
result = call_ao('workflow_details', id: wf_id)
|
166
167
|
return {type: :object_list, data: result['workflows']['workflow']['statuses']}
|
167
168
|
when :inputs
|
168
|
-
result = call_ao('workflow_inputs_spec', id: wf_id)
|
169
|
+
result = call_ao('workflow_inputs_spec', id: wf_id)
|
169
170
|
return {type: :single_object, data: result['workflow_inputs_spec']}
|
170
171
|
when :export
|
171
|
-
result = call_ao('export_workflow', id: wf_id, format: nil)
|
172
|
+
result = call_ao('export_workflow', id: wf_id, format: nil, http: true)
|
172
173
|
return {type: :text, data: result.body}
|
173
174
|
when :start
|
174
175
|
result = {
|
@@ -196,7 +197,7 @@ module Aspera
|
|
196
197
|
if call_params['synchronous']
|
197
198
|
result[:type] = :text
|
198
199
|
end
|
199
|
-
result[:data] = call_ao('initiate', id: wf_id, args: call_params)
|
200
|
+
result[:data] = call_ao('initiate', id: wf_id, args: call_params)
|
200
201
|
return result
|
201
202
|
end
|
202
203
|
else Aspera.error_unexpected_value(command)
|
@@ -133,7 +133,6 @@ module Aspera
|
|
133
133
|
subpath: "files/#{file_id}/files",
|
134
134
|
headers: headers,
|
135
135
|
query: request_args)[:data]
|
136
|
-
# return @api_node.read("files/#{file_id}/files",request_args)[:data]
|
137
136
|
end
|
138
137
|
|
139
138
|
# old version based on folders
|
@@ -146,7 +145,7 @@ module Aspera
|
|
146
145
|
# optionally add iteration token from persistency
|
147
146
|
events_filter['iteration_token'] = iteration_persistency.data.first unless iteration_persistency.nil?
|
148
147
|
begin
|
149
|
-
events = @api_node.read('events', events_filter)
|
148
|
+
events = @api_node.read('events', events_filter)
|
150
149
|
rescue RestCallError => e
|
151
150
|
if e.message.include?('Invalid iteration_token')
|
152
151
|
Log.log.warn{"Retrying without iteration token: #{e}"}
|
@@ -164,7 +163,7 @@ module Aspera
|
|
164
163
|
folder_id = event.dig('data', 'tags', Transfer::Spec::TAG_RESERVED, 'node', 'file_id')
|
165
164
|
folder_id ||= event.dig('data', 'file_id')
|
166
165
|
if !folder_id.nil?
|
167
|
-
folder_entry = @api_node.read("files/#{folder_id}")
|
166
|
+
folder_entry = @api_node.read("files/#{folder_id}") rescue nil
|
168
167
|
scan_folder_files(folder_entry) unless folder_entry.nil?
|
169
168
|
end
|
170
169
|
end
|
@@ -188,12 +187,12 @@ module Aspera
|
|
188
187
|
}
|
189
188
|
# optionally add iteration token from persistency
|
190
189
|
events_filter['iteration_token'] = iteration_persistency.data.first unless iteration_persistency.nil?
|
191
|
-
events = @api_node.read('events', events_filter)
|
190
|
+
events = @api_node.read('events', events_filter)
|
192
191
|
return if events.empty?
|
193
192
|
events.each do |event|
|
194
193
|
# process only files
|
195
194
|
if event.dig('data', 'type').eql?('file')
|
196
|
-
file_entry = @api_node.read("files/#{event['data']['id']}")
|
195
|
+
file_entry = @api_node.read("files/#{event['data']['id']}") rescue nil
|
197
196
|
if !file_entry.nil? &&
|
198
197
|
@option_skip_folders.none?{|d|file_entry['path'].start_with?(d)}
|
199
198
|
file_entry['parent_file_id'] = event['data']['parent_file_id']
|
@@ -248,7 +247,7 @@ module Aspera
|
|
248
247
|
# original_mtime=DateTime.parse(entry['modified_time'])
|
249
248
|
# out: where previews are generated
|
250
249
|
local_entry_preview_dir = File.join(@tmp_folder, entry_preview_folder_name(entry))
|
251
|
-
file_info = @api_node.read("files/#{entry['id']}")
|
250
|
+
file_info = @api_node.read("files/#{entry['id']}")
|
252
251
|
# TODO: this does not work because previews is hidden in api (gen4)
|
253
252
|
# this_preview_folder_entries=get_folder_entries(@previews_folder_entry['id'],{name: @entry_preview_folder_name})
|
254
253
|
# TODO: use gen3 api to list files and get date
|
@@ -410,10 +409,10 @@ module Aspera
|
|
410
409
|
@api_node = Api::Node.new(**basic_auth_params)
|
411
410
|
@transfer_server_address = URI.parse(@api_node.base_url).host
|
412
411
|
# get current access key
|
413
|
-
@access_key_self = @api_node.read('access_keys/self')
|
412
|
+
@access_key_self = @api_node.read('access_keys/self')
|
414
413
|
# TODO: check events is activated here:
|
415
414
|
# note that docroot is good to look at as well
|
416
|
-
node_info = @api_node.read('info')
|
415
|
+
node_info = @api_node.read('info')
|
417
416
|
Log.log.debug{"root: #{node_info['docroot']}"}
|
418
417
|
@access_remote = @option_file_access.eql?(:remote)
|
419
418
|
Log.log.debug{"remote: #{@access_remote}"}
|
@@ -463,7 +462,7 @@ module Aspera
|
|
463
462
|
'type' => 'folder',
|
464
463
|
'path' => '/' }
|
465
464
|
else
|
466
|
-
@api_node.read("files/#{scan_id}")
|
465
|
+
@api_node.read("files/#{scan_id}")
|
467
466
|
end
|
468
467
|
@filter_block = Api::Node.file_matcher_from_argument(options)
|
469
468
|
scan_folder_files(folder_info, scan_path)
|
@@ -9,6 +9,7 @@ require 'aspera/ssh'
|
|
9
9
|
require 'aspera/nagios'
|
10
10
|
require 'aspera/log'
|
11
11
|
require 'aspera/assert'
|
12
|
+
require 'aspera/environment'
|
12
13
|
require 'tempfile'
|
13
14
|
require 'open3'
|
14
15
|
|
@@ -32,14 +33,8 @@ module Aspera
|
|
32
33
|
private_constant :SSH_SCHEME, :URI_SCHEMES, :ASCMD_ALIASES, :TRANSFER_COMMANDS
|
33
34
|
|
34
35
|
class LocalExecutor
|
35
|
-
def execute(
|
36
|
-
|
37
|
-
cmd = cmd.map{|v|%Q("#{v}")}.join(' ') if cmd.is_a?(Array)
|
38
|
-
Log.log.debug{"Executing: #{cmd} with '#{line}'"}
|
39
|
-
stdout_str, stderr_str, status = Open3.capture3(cmd, stdin_data: line, binmode: true)
|
40
|
-
Log.log.debug{"exec status: #{status} -> #{stderr_str}"}
|
41
|
-
raise "command #{cmd} failed with code #{status.exitstatus} #{stderr_str}" unless status.success?
|
42
|
-
return stdout_str
|
36
|
+
def execute(ascmd_path, line)
|
37
|
+
return Environment.secure_capture(exec: ascmd_path, stdin_data: line, binmode: true)
|
43
38
|
end
|
44
39
|
end
|
45
40
|
|
@@ -157,7 +152,8 @@ module Aspera
|
|
157
152
|
Log.log.debug{"SSH keys=#{ssh_key_list}"}
|
158
153
|
if !ssh_key_list.empty?
|
159
154
|
@ssh_opts[:keys] = ssh_key_list
|
160
|
-
|
155
|
+
# PEM as per RFC 7468
|
156
|
+
server_transfer_spec['ssh_private_key'] = File.read(ssh_key_list.first).strip
|
161
157
|
Log.log.warn{'Using only first SSH key for transfers'} unless ssh_key_list.length.eql?(1)
|
162
158
|
cred_set = true
|
163
159
|
end
|
@@ -176,7 +172,7 @@ module Aspera
|
|
176
172
|
def execute_transfer(command, transfer_spec)
|
177
173
|
case command
|
178
174
|
when :upload, :download
|
179
|
-
Transfer::Spec.
|
175
|
+
transfer_spec['direction'] = Transfer::Spec.transfer_type_to_direction(command)
|
180
176
|
return Main.result_transfer(transfer.start(transfer_spec))
|
181
177
|
when :sync
|
182
178
|
# lets ignore the arguments provided by execute_sync_action, we just give the transfer spec
|
@@ -7,7 +7,10 @@ module Aspera
|
|
7
7
|
module Plugins
|
8
8
|
# Plugin for Aspera Shares v1
|
9
9
|
class Shares < Cli::BasicAuthPlugin
|
10
|
-
|
10
|
+
# path for node API after base url
|
11
|
+
NODE_API_PATH = 'node_api'
|
12
|
+
# path for node admin after base url
|
13
|
+
ADMIN_API_PATH = 'api/v1'
|
11
14
|
class << self
|
12
15
|
def detect(address_or_url)
|
13
16
|
address_or_url = "https://#{address_or_url}" unless address_or_url.match?(%r{^[a-z]{1,6}://})
|
@@ -16,7 +19,7 @@ module Aspera
|
|
16
19
|
begin
|
17
20
|
# shall fail: shares requires auth, but we check error message
|
18
21
|
# TODO: use ping instead ?
|
19
|
-
api.read("#{
|
22
|
+
api.read("#{NODE_API_PATH}/app")
|
20
23
|
rescue RestCallError => e
|
21
24
|
if e.response.code.to_s.eql?('401') && e.response.body.eql?('{"error":{"user_message":"API user authentication failed"}}')
|
22
25
|
found = true
|
@@ -65,7 +68,7 @@ module Aspera
|
|
65
68
|
nagios = Nagios.new
|
66
69
|
begin
|
67
70
|
res = Rest
|
68
|
-
.new(base_url: "#{options.get_option(:url, mandatory: true)}/#{
|
71
|
+
.new(base_url: "#{options.get_option(:url, mandatory: true)}/#{NODE_API_PATH}")
|
69
72
|
.call(
|
70
73
|
operation: 'GET',
|
71
74
|
subpath: 'ping',
|
@@ -77,13 +80,13 @@ module Aspera
|
|
77
80
|
end
|
78
81
|
return nagios.result
|
79
82
|
when :repository, :files
|
80
|
-
api_shares_node = basic_auth_api(
|
83
|
+
api_shares_node = basic_auth_api(NODE_API_PATH)
|
81
84
|
repo_command = options.get_next_command(Node::COMMANDS_SHARES)
|
82
85
|
return Node
|
83
86
|
.new(**init_params, api: api_shares_node)
|
84
87
|
.execute_action(repo_command)
|
85
88
|
when :admin
|
86
|
-
api_shares_admin = basic_auth_api(
|
89
|
+
api_shares_admin = basic_auth_api(ADMIN_API_PATH)
|
87
90
|
admin_command = options.get_next_command(%i[node share transfer_settings user group].freeze)
|
88
91
|
case admin_command
|
89
92
|
when :node
|
@@ -92,8 +95,9 @@ module Aspera
|
|
92
95
|
share_command = options.get_next_command(%i[user_permissions group_permissions].concat(Plugin::ALL_OPS))
|
93
96
|
case share_command
|
94
97
|
when *Plugin::ALL_OPS
|
95
|
-
return entity_command(
|
96
|
-
|
98
|
+
return entity_command(
|
99
|
+
share_command, api_shares_admin, 'data/shares',
|
100
|
+
display_fields: %w[id name node_id directory percent_free])
|
97
101
|
when :user_permissions, :group_permissions
|
98
102
|
share_id = instance_identifier
|
99
103
|
return entity_action(api_shares_admin, "data/shares/#{share_id}/#{share_command}")
|
@@ -140,11 +144,11 @@ module Aspera
|
|
140
144
|
entity_parameters.each_key do |p|
|
141
145
|
raise "unsupported field: #{p}, use: #{SAML_IMPORT_ALLOWED.join(',')}" unless SAML_IMPORT_ALLOWED.include?(p)
|
142
146
|
end
|
143
|
-
api_shares_admin.create("#{entities_path}/import", entity_parameters)
|
147
|
+
api_shares_admin.create("#{entities_path}/import", entity_parameters)
|
144
148
|
end
|
145
149
|
when :add # ldap
|
146
150
|
return do_bulk_operation(command: entity_verb, descr: "#{entity_type} name", values: String) do |entity_name|
|
147
|
-
api_shares_admin.create(entities_path, {entity_type=>entity_name})
|
151
|
+
api_shares_admin.create(entities_path, {entity_type=>entity_name})
|
148
152
|
end
|
149
153
|
when :users # group
|
150
154
|
return entity_action(api_shares_admin, "#{entities_path}/#{instance_identifier}/#{entities_prefix}users")
|
@@ -7,11 +7,30 @@ module Aspera
|
|
7
7
|
module Cli
|
8
8
|
# Module for sync actions
|
9
9
|
module SyncActions
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
10
|
+
# optional simple command line arguments for sync
|
11
|
+
# in Array to keep option order
|
12
|
+
# conf: key in option --conf
|
13
|
+
# args: key for command line args
|
14
|
+
# values: possible values for argument
|
15
|
+
# type: type for validation
|
16
|
+
SYNC_ARGUMENTS_INFO = [
|
17
|
+
{
|
18
|
+
conf: 'direction',
|
19
|
+
args: 'direction',
|
20
|
+
values: Transfer::Sync::DIRECTIONS
|
21
|
+
}, {
|
22
|
+
conf: 'remote.path',
|
23
|
+
args: 'remote_dir',
|
24
|
+
type: String
|
25
|
+
}, {
|
26
|
+
conf: 'local.path',
|
27
|
+
args: 'local_dir',
|
28
|
+
type: String
|
29
|
+
}
|
30
|
+
].freeze
|
31
|
+
# name of minimal arguments required, also used to generate a session name
|
32
|
+
SYNC_SIMPLE_ARGS = SYNC_ARGUMENTS_INFO.map{|i|i[:conf]}.freeze
|
33
|
+
private_constant :SYNC_ARGUMENTS_INFO, :SYNC_SIMPLE_ARGS
|
15
34
|
|
16
35
|
class << self
|
17
36
|
def declare_options(options)
|
@@ -19,38 +38,60 @@ module Aspera
|
|
19
38
|
end
|
20
39
|
end
|
21
40
|
|
41
|
+
# Read command line arguments (3) and converts to sync_info format
|
42
|
+
def sync_args_to_params(async_params)
|
43
|
+
# sync session parameters can be provided on command line instead of sync_info
|
44
|
+
arguments = {}
|
45
|
+
SYNC_ARGUMENTS_INFO.each do |info|
|
46
|
+
value = options.get_next_argument(
|
47
|
+
info[:conf],
|
48
|
+
mandatory: false,
|
49
|
+
validation: info[:type],
|
50
|
+
accept_list: info[:values])
|
51
|
+
break if value.nil?
|
52
|
+
arguments[info[:conf]] = value.to_s
|
53
|
+
end
|
54
|
+
Log.log.debug{Log.dump('arguments', arguments)}
|
55
|
+
raise Cli::BadArgument, "Provide 0 or 3 arguments, not #{arguments.keys.length} for: #{SYNC_SIMPLE_ARGS.join(', ')}" unless
|
56
|
+
[0, 3].include?(arguments.keys.length)
|
57
|
+
if !arguments.empty?
|
58
|
+
session_info = async_params
|
59
|
+
param_path = :conf
|
60
|
+
if async_params.key?('sessions') || async_params.key?('instance')
|
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
|
65
|
+
end
|
66
|
+
SYNC_ARGUMENTS_INFO.each do |info|
|
67
|
+
key_path = info[param_path].split('.')
|
68
|
+
hash_for_key = session_info
|
69
|
+
if key_path.length > 1
|
70
|
+
first = key_path.shift
|
71
|
+
async_params[first] ||= {}
|
72
|
+
hash_for_key = async_params[first]
|
73
|
+
end
|
74
|
+
raise "Parameter #{info[:conf]} is also set in sync_info, remove from sync_info" if hash_for_key.key?(key_path.last)
|
75
|
+
hash_for_key[key_path.last] = arguments[info[:conf]]
|
76
|
+
end
|
77
|
+
if !session_info.key?('name')
|
78
|
+
# if no name is specified, generate one from simple arguments
|
79
|
+
session_info['name'] = SYNC_SIMPLE_ARGS.filter_map do |arg_name|
|
80
|
+
arguments[arg_name]&.gsub(/[^a-zA-Z0-9]/, '')
|
81
|
+
end.reject(&:empty?).join('_')
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
22
86
|
def execute_sync_action(&block)
|
23
87
|
Aspera.assert(block){'No block given'}
|
24
88
|
command = options.get_next_command(%i[start admin])
|
25
89
|
# try to get 3 arguments as simple arguments
|
26
90
|
case command
|
27
91
|
when :start
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
arg,
|
32
|
-
mandatory: false,
|
33
|
-
validation: check.is_a?(Class) ? check : nil,
|
34
|
-
accept_list: check.is_a?(Class) ? nil : check)
|
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
|
-
Aspera.assert_type(async_params, Hash){'sync_info'}
|
49
|
-
Aspera.assert_type(async_params['sessions'], Array){'sync_info[sessions]'}
|
50
|
-
Aspera.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)}
|
92
|
+
# possibilities are:
|
93
|
+
async_params = options.get_option(:sync_info, default: {})
|
94
|
+
sync_args_to_params(async_params)
|
54
95
|
Transfer::Sync.start(async_params, &block)
|
55
96
|
return Main.result_success
|
56
97
|
when :admin
|
@@ -22,7 +22,7 @@ module Aspera
|
|
22
22
|
To: <<%=to%>>
|
23
23
|
Subject: <%=subject%>
|
24
24
|
|
25
|
-
Transfer is: <%=
|
25
|
+
Transfer is: <%=status%>
|
26
26
|
|
27
27
|
<%=ts.to_yaml%>
|
28
28
|
END_OF_TEMPLATE
|
@@ -65,6 +65,16 @@ module Aspera
|
|
65
65
|
@opt_mgr.declare(:transfer, 'Type of transfer agent', values: TRANSFER_AGENTS, default: :direct)
|
66
66
|
@opt_mgr.declare(:transfer_info, 'Parameters for transfer agent', types: Hash, handler: {o: self, m: :transfer_info})
|
67
67
|
@opt_mgr.parse_options!
|
68
|
+
@notification_cb = nil
|
69
|
+
if !@opt_mgr.get_option(:notify_to).nil?
|
70
|
+
@notification_cb = ->(transfer_spec, global_status) do
|
71
|
+
@config.send_email_template(email_template_default: DEFAULT_TRANSFER_NOTIFY_TEMPLATE, values: {
|
72
|
+
subject: "#{Info::CMD_NAME} transfer: #{global_status}",
|
73
|
+
status: global_status,
|
74
|
+
ts: transfer_spec
|
75
|
+
})
|
76
|
+
end
|
77
|
+
end
|
68
78
|
end
|
69
79
|
|
70
80
|
def option_transfer_spec; @transfer_spec_command_line; end
|
@@ -116,6 +126,7 @@ module Aspera
|
|
116
126
|
# by default do not display ascp native progress bar
|
117
127
|
agent_options[:quiet] = true unless agent_options.key?(:quiet)
|
118
128
|
agent_options[:check_ignore_cb] = ->(host, port){@config.ignore_cert?(host, port)}
|
129
|
+
# JRuby
|
119
130
|
agent_options[:trusted_certs] = @config.trusted_cert_locations unless agent_options.key?(:trusted_certs)
|
120
131
|
when :httpgw
|
121
132
|
unless agent_options.key?(:url) || @httpgw_url_lambda.nil?
|
@@ -250,22 +261,10 @@ module Aspera
|
|
250
261
|
agent_instance.start_transfer(transfer_spec, token_regenerator: rest_token)
|
251
262
|
# list of: :success or "error message string"
|
252
263
|
result = agent_instance.wait_for_completion
|
253
|
-
|
264
|
+
@notification_cb&.call(transfer_spec, self.class.session_status(result))
|
254
265
|
return result
|
255
266
|
end
|
256
267
|
|
257
|
-
def send_email_transfer_notification(transfer_spec, statuses)
|
258
|
-
return if @opt_mgr.get_option(:notify_to).nil?
|
259
|
-
global_status = self.class.session_status(statuses)
|
260
|
-
email_vars = {
|
261
|
-
global_transfer_status: global_status,
|
262
|
-
subject: "#{PROGRAM_NAME} transfer: #{global_status}",
|
263
|
-
body: "Transfer is: #{global_status}",
|
264
|
-
ts: transfer_spec
|
265
|
-
}
|
266
|
-
@config.send_email_template(email_template_default: DEFAULT_TRANSFER_NOTIFY_TEMPLATE, values: email_vars)
|
267
|
-
end
|
268
|
-
|
269
268
|
# shut down if agent requires it
|
270
269
|
def shutdown
|
271
270
|
@agent.shutdown if @agent.respond_to?(:shutdown)
|
@@ -6,27 +6,28 @@ require 'ruby-progressbar'
|
|
6
6
|
|
7
7
|
module Aspera
|
8
8
|
module Cli
|
9
|
-
#
|
9
|
+
# Progress bar for transfers.
|
10
|
+
# Supports multi-session.
|
10
11
|
class TransferProgress
|
11
12
|
def initialize
|
12
13
|
reset
|
13
14
|
end
|
14
15
|
|
16
|
+
# Reset progress bar, to re-use it.
|
15
17
|
def reset
|
16
18
|
@progress_bar = nil
|
17
19
|
# key is session id
|
18
20
|
@sessions = {}
|
19
21
|
@completed = false
|
20
|
-
@title =
|
22
|
+
@title = nil
|
21
23
|
end
|
22
24
|
|
23
|
-
|
24
|
-
|
25
|
-
end
|
26
|
-
|
27
|
-
def event(session_id:,
|
28
|
-
Log.log.
|
29
|
-
Aspera.assert(!session_id.nil? || type.eql?(:pre_start)){'session_id is nil'}
|
25
|
+
# Called by user of progress bar with a status on a transfer session
|
26
|
+
# @param session_id the unique identifier of a transfer session
|
27
|
+
# @param type one of: pre_start, session_start, session_size, transfer, end
|
28
|
+
# @param info optional specific additional info for the given event type
|
29
|
+
def event(type, session_id:, info: nil)
|
30
|
+
Log.log.trace1{"progress: #{type} #{session_id} #{info}"}
|
30
31
|
return if @completed
|
31
32
|
if @progress_bar.nil?
|
32
33
|
@progress_bar = ProgressBar.create(
|
@@ -35,38 +36,55 @@ module Aspera
|
|
35
36
|
title: '',
|
36
37
|
total: nil)
|
37
38
|
end
|
38
|
-
|
39
|
+
progress_provided = false
|
39
40
|
case type
|
40
41
|
when :pre_start
|
42
|
+
# give opportunity to show progress of initialization with multiple status
|
43
|
+
Aspera.assert(session_id.nil?)
|
44
|
+
Aspera.assert_type(info, String)
|
45
|
+
# initialization of progress bar
|
41
46
|
@title = info
|
42
47
|
when :session_start
|
48
|
+
Aspera.assert_type(session_id, String)
|
49
|
+
Aspera.assert(info.nil?)
|
43
50
|
raise "Session #{session_id} already started" if @sessions[session_id]
|
44
51
|
@sessions[session_id] = {
|
45
52
|
job_size: 0, # total size of transfer (pre-calc)
|
46
53
|
current: 0
|
47
54
|
}
|
48
|
-
|
55
|
+
# remove last pre-start message if any
|
56
|
+
@title = nil
|
49
57
|
when :session_size
|
58
|
+
Aspera.assert_type(session_id, String)
|
59
|
+
Aspera.assert(!info.nil?)
|
50
60
|
@sessions[session_id][:job_size] = info.to_i
|
51
61
|
current_total = total(:job_size)
|
52
62
|
@progress_bar.total = current_total unless current_total.eql?(@progress_bar.total) || current_total < @progress_bar.progress
|
53
63
|
when :transfer
|
54
|
-
|
55
|
-
|
64
|
+
Aspera.assert_type(session_id, String)
|
65
|
+
if !@progress_bar.total.nil? && !info.nil?
|
66
|
+
progress_provided = true
|
56
67
|
@sessions[session_id][:current] = info.to_i
|
57
68
|
current_total = total(:current)
|
58
69
|
@progress_bar.progress = current_total unless @progress_bar.progress.eql?(current_total)
|
59
70
|
end
|
60
71
|
when :end
|
61
|
-
|
72
|
+
Aspera.assert(session_id, String)
|
73
|
+
Aspera.assert(info.nil?)
|
74
|
+
@title = nil
|
62
75
|
@completed = true
|
63
76
|
@progress_bar.finish
|
64
|
-
else
|
65
|
-
raise "Unknown event type #{type}"
|
77
|
+
else Aspera.error_unexpected_value(type){'event type'}
|
66
78
|
end
|
67
|
-
new_title = @sessions.length < 2 ? @title : "[#{@sessions.length}] #{@title}"
|
79
|
+
new_title = @sessions.length < 2 ? @title.to_s : "[#{@sessions.length}] #{@title}"
|
68
80
|
@progress_bar.title = new_title unless @progress_bar.title.eql?(new_title)
|
69
|
-
@progress_bar.increment if
|
81
|
+
@progress_bar.increment if !progress_provided && !@completed
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def total(key)
|
87
|
+
@sessions.values.inject(0){|m, s|m + s[key]}
|
70
88
|
end
|
71
89
|
end
|
72
90
|
end
|
data/lib/aspera/cli/version.rb
CHANGED
@@ -14,7 +14,7 @@ module Aspera
|
|
14
14
|
OPTIONS_KEYS = %i[desc accepted_types default enum agents required cli ts deprecation].freeze
|
15
15
|
CLI_KEYS = %i[type switch convert variable].freeze
|
16
16
|
|
17
|
-
private_constant :CLI_OPTION_TYPE_SWITCH, :OPTIONS_KEYS, :CLI_KEYS
|
17
|
+
private_constant :CLI_OPTION_TYPE_SWITCH, :CLI_OPTION_TYPES, :OPTIONS_KEYS, :CLI_KEYS
|
18
18
|
|
19
19
|
class << self
|
20
20
|
# transform yes/no to true/false
|
@@ -22,8 +22,8 @@ module Aspera
|
|
22
22
|
case value
|
23
23
|
when 'yes' then return true
|
24
24
|
when 'no' then return false
|
25
|
+
else Aspera.error_unexpected_value(value){'only: yes or no: '}
|
25
26
|
end
|
26
|
-
raise "unsupported value: #{value}"
|
27
27
|
end
|
28
28
|
|
29
29
|
# Called by provider of definition before constructor of this class so that params_definition has all mandatory fields
|
@@ -177,8 +177,7 @@ module Aspera
|
|
177
177
|
parameter_value = [parameter_value] unless parameter_value.is_a?(Array)
|
178
178
|
# if transfer_spec value is an array, applies option many times
|
179
179
|
parameter_value.each{|v|add_command_line_options([options[:cli][:switch], v])}
|
180
|
-
else
|
181
|
-
raise "ERROR: unknown option processing type: #{processing_type}/#{processing_type.class}"
|
180
|
+
else Aspera.error_unexpected_value(processing_type){processing_type.class.name}
|
182
181
|
end
|
183
182
|
end
|
184
183
|
end
|
data/lib/aspera/coverage.rb
CHANGED
@@ -17,5 +17,17 @@ if ENV.key?('ENABLE_COVERAGE')
|
|
17
17
|
SimpleCov.result.format!
|
18
18
|
$stdout.reopen(original_file_descriptor)
|
19
19
|
end
|
20
|
-
|
20
|
+
# lines with those words are ignored from coverage
|
21
|
+
no_cov_functions = %w[error_unreachable_line error_unexpected_value Log.log.trace].freeze
|
22
|
+
SimpleCov.start do
|
23
|
+
add_filter 'lib/aspera/cli/plugins/faspex.rb'
|
24
|
+
add_filter 'lib/aspera/node_simulator.rb'
|
25
|
+
add_filter 'lib/aspera/keychain/macos_security.rb'
|
26
|
+
add_filter do |source_file|
|
27
|
+
source_file.lines.each do |line|
|
28
|
+
line.skipped! if no_cov_functions.any?{|i|line.src.include?(i)}
|
29
|
+
end
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
21
33
|
end
|