aspera-cli 4.10.0 → 4.12.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 +19 -0
- data/CHANGELOG.md +528 -0
- data/CONTRIBUTING.md +143 -0
- data/README.md +977 -589
- data/bin/ascli +4 -4
- data/bin/asession +12 -12
- data/docs/test_env.conf +29 -19
- data/examples/aoc.rb +6 -6
- data/examples/dascli +18 -16
- data/examples/faspex4.rb +15 -15
- data/examples/node.rb +12 -12
- data/examples/proxy.pac +2 -2
- data/examples/server.rb +12 -12
- data/lib/aspera/aoc.rb +344 -272
- data/lib/aspera/ascmd.rb +56 -54
- data/lib/aspera/ats_api.rb +4 -4
- data/lib/aspera/cli/basic_auth_plugin.rb +15 -12
- data/lib/aspera/cli/extended_value.rb +9 -9
- data/lib/aspera/cli/{formater.rb → formatter.rb} +69 -69
- data/lib/aspera/cli/listener/line_dump.rb +1 -1
- data/lib/aspera/cli/listener/logger.rb +1 -1
- data/lib/aspera/cli/listener/progress.rb +5 -6
- data/lib/aspera/cli/listener/progress_multi.rb +16 -21
- data/lib/aspera/cli/main.rb +72 -73
- data/lib/aspera/cli/manager.rb +112 -112
- data/lib/aspera/cli/plugin.rb +68 -48
- data/lib/aspera/cli/plugins/alee.rb +4 -4
- data/lib/aspera/cli/plugins/aoc.rb +322 -720
- data/lib/aspera/cli/plugins/ats.rb +50 -52
- data/lib/aspera/cli/plugins/bss.rb +10 -10
- data/lib/aspera/cli/plugins/config.rb +514 -410
- data/lib/aspera/cli/plugins/console.rb +12 -12
- data/lib/aspera/cli/plugins/cos.rb +18 -20
- data/lib/aspera/cli/plugins/faspex.rb +134 -136
- data/lib/aspera/cli/plugins/faspex5.rb +235 -70
- data/lib/aspera/cli/plugins/node.rb +378 -309
- data/lib/aspera/cli/plugins/orchestrator.rb +52 -49
- data/lib/aspera/cli/plugins/preview.rb +129 -120
- data/lib/aspera/cli/plugins/server.rb +137 -83
- data/lib/aspera/cli/plugins/shares.rb +77 -52
- data/lib/aspera/cli/plugins/sync.rb +13 -33
- data/lib/aspera/cli/transfer_agent.rb +61 -61
- data/lib/aspera/cli/version.rb +2 -1
- data/lib/aspera/colors.rb +3 -3
- data/lib/aspera/command_line_builder.rb +78 -74
- data/lib/aspera/cos_node.rb +31 -29
- data/lib/aspera/data_repository.rb +1 -1
- data/lib/aspera/environment.rb +30 -28
- data/lib/aspera/fasp/agent_base.rb +17 -15
- data/lib/aspera/fasp/agent_connect.rb +34 -32
- data/lib/aspera/fasp/agent_direct.rb +70 -73
- data/lib/aspera/fasp/agent_httpgw.rb +79 -74
- data/lib/aspera/fasp/agent_node.rb +26 -26
- data/lib/aspera/fasp/agent_trsdk.rb +20 -20
- data/lib/aspera/fasp/error.rb +3 -2
- data/lib/aspera/fasp/error_info.rb +11 -8
- data/lib/aspera/fasp/installation.rb +80 -80
- data/lib/aspera/fasp/listener.rb +2 -2
- data/lib/aspera/fasp/parameters.rb +103 -92
- data/lib/aspera/fasp/parameters.yaml +313 -214
- data/lib/aspera/fasp/resume_policy.rb +10 -10
- data/lib/aspera/fasp/transfer_spec.rb +22 -2
- data/lib/aspera/fasp/uri.rb +7 -7
- data/lib/aspera/faspex_gw.rb +80 -159
- data/lib/aspera/faspex_postproc.rb +77 -0
- data/lib/aspera/hash_ext.rb +3 -3
- data/lib/aspera/id_generator.rb +5 -5
- data/lib/aspera/keychain/encrypted_hash.rb +23 -28
- data/lib/aspera/keychain/macos_security.rb +21 -20
- data/lib/aspera/log.rb +13 -13
- data/lib/aspera/nagios.rb +24 -23
- data/lib/aspera/node.rb +217 -38
- data/lib/aspera/oauth.rb +78 -74
- data/lib/aspera/open_application.rb +19 -11
- data/lib/aspera/persistency_action_once.rb +4 -4
- data/lib/aspera/persistency_folder.rb +13 -13
- data/lib/aspera/preview/file_types.rb +8 -8
- data/lib/aspera/preview/generator.rb +67 -67
- data/lib/aspera/preview/utils.rb +27 -27
- data/lib/aspera/proxy_auto_config.js +63 -63
- data/lib/aspera/proxy_auto_config.rb +19 -19
- data/lib/aspera/rest.rb +65 -67
- data/lib/aspera/rest_call_error.rb +2 -1
- data/lib/aspera/rest_error_analyzer.rb +22 -21
- data/lib/aspera/rest_errors_aspera.rb +16 -16
- data/lib/aspera/secret_hider.rb +17 -14
- data/lib/aspera/ssh.rb +15 -14
- data/lib/aspera/sync.rb +177 -62
- data/lib/aspera/temp_file_manager.rb +2 -2
- data/lib/aspera/uri_reader.rb +4 -4
- data/lib/aspera/web_auth.rb +13 -64
- data/lib/aspera/web_server_simple.rb +76 -0
- data.tar.gz.sig +0 -0
- metadata +11 -6
- metadata.gz.sig +0 -0
@@ -16,14 +16,15 @@ require 'shellwords'
|
|
16
16
|
module Aspera
|
17
17
|
module Fasp
|
18
18
|
# executes a local "ascp", connects mgt port, equivalent of "Fasp Manager"
|
19
|
-
class AgentDirect < AgentBase
|
19
|
+
class AgentDirect < Aspera::Fasp::AgentBase
|
20
20
|
# options for initialize (same as values in option transfer_info)
|
21
21
|
DEFAULT_OPTIONS = {
|
22
22
|
spawn_timeout_sec: 3,
|
23
23
|
spawn_delay_sec: 2,
|
24
|
-
wss:
|
24
|
+
wss: true, # true: if both SSH and wss in ts: prefer wss
|
25
25
|
multi_incr_udp: true,
|
26
26
|
resume: {},
|
27
|
+
ascp_args: [],
|
27
28
|
quiet: true # by default no interactive progress bar
|
28
29
|
}.freeze
|
29
30
|
private_constant :DEFAULT_OPTIONS
|
@@ -31,12 +32,8 @@ module Aspera
|
|
31
32
|
# start ascp transfer (non blocking), single or multi-session
|
32
33
|
# job information added to @jobs
|
33
34
|
# @param transfer_spec [Hash] aspera transfer specification
|
34
|
-
|
35
|
-
|
36
|
-
raise 'option: must be hash (or nil)' unless options.is_a?(Hash)
|
37
|
-
job_options = options.clone
|
38
|
-
job_options[:resumer] ||= @resume_policy
|
39
|
-
job_options[:job_id] ||= SecureRandom.uuid
|
35
|
+
def start_transfer(transfer_spec, token_regenerator: nil)
|
36
|
+
the_job_id = SecureRandom.uuid
|
40
37
|
# clone transfer spec because we modify it (first level keys)
|
41
38
|
transfer_spec = transfer_spec.clone
|
42
39
|
# if there is aspera tags
|
@@ -46,38 +43,38 @@ module Aspera
|
|
46
43
|
# using a non unique id results in discard of tags in AoC, and a package is never finalized
|
47
44
|
# all sessions in a multi-session transfer must have the same xfer_id (see admin manual)
|
48
45
|
transfer_spec['tags']['aspera']['xfer_id'] ||= SecureRandom.uuid
|
49
|
-
Log.log.debug
|
46
|
+
Log.log.debug{"xfer id=#{transfer_spec['xfer_id']}"}
|
50
47
|
# TODO: useful ? node only ?
|
51
48
|
transfer_spec['tags']['aspera']['xfer_retry'] ||= 3600
|
52
49
|
end
|
53
|
-
Log.dump('ts',transfer_spec)
|
50
|
+
Log.dump('ts', transfer_spec)
|
54
51
|
|
55
52
|
# add bypass keys when authentication is token and no auth is provided
|
56
|
-
if transfer_spec.
|
57
|
-
|
58
|
-
|
59
|
-
# transfer_spec['remote_password'] = Installation.instance.bypass_pass # not used
|
53
|
+
if transfer_spec.key?('token') &&
|
54
|
+
!transfer_spec.key?('remote_password') &&
|
55
|
+
!transfer_spec.key?('EX_ssh_key_paths')
|
56
|
+
# transfer_spec['remote_password'] = Installation.instance.bypass_pass # not used: no passphrase
|
60
57
|
transfer_spec['EX_ssh_key_paths'] = Installation.instance.bypass_keys
|
61
58
|
end
|
62
59
|
|
63
60
|
# Compute this before using transfer spec because it potentially modifies the transfer spec
|
64
61
|
# (even if the var is not used in single session)
|
65
62
|
multi_session_info = nil
|
66
|
-
if transfer_spec.
|
63
|
+
if transfer_spec.key?('multi_session')
|
67
64
|
multi_session_info = {
|
68
65
|
count: transfer_spec['multi_session'].to_i
|
69
66
|
}
|
70
67
|
# Managed by multi-session, so delete from transfer spec
|
71
68
|
transfer_spec.delete('multi_session')
|
72
69
|
if multi_session_info[:count].negative?
|
73
|
-
Log.log.error
|
70
|
+
Log.log.error{"multi_session(#{transfer_spec['multi_session']}) shall be integer >= 0"}
|
74
71
|
multi_session_info = nil
|
75
72
|
elsif multi_session_info[:count].eql?(0)
|
76
|
-
Log.log.debug('multi_session count is zero: no
|
73
|
+
Log.log.debug('multi_session count is zero: no multi session')
|
77
74
|
multi_session_info = nil
|
78
75
|
elsif @options[:multi_incr_udp] # multi_session_info[:count] > 0
|
79
76
|
# if option not true: keep default udp port for all sessions
|
80
|
-
multi_session_info[:udp_base] = transfer_spec.
|
77
|
+
multi_session_info[:udp_base] = transfer_spec.key?('fasp_port') ? transfer_spec['fasp_port'] : TransferSpec::UDP_PORT
|
81
78
|
# delete from original transfer spec, as we will increment values
|
82
79
|
transfer_spec.delete('fasp_port')
|
83
80
|
# override if specified, else use default value
|
@@ -85,30 +82,30 @@ module Aspera
|
|
85
82
|
end
|
86
83
|
|
87
84
|
# compute known args
|
88
|
-
env_args = Parameters.ts_to_env_args(transfer_spec,wss: @options[:wss])
|
85
|
+
env_args = Parameters.ts_to_env_args(transfer_spec, wss: @options[:wss], ascp_args: @options[:ascp_args])
|
89
86
|
|
90
87
|
# add fallback cert and key as arguments if needed
|
91
88
|
if %w[1 force].include?(transfer_spec['http_fallback'])
|
92
|
-
env_args[:args].unshift('-Y',Installation.instance.path(:fallback_key))
|
93
|
-
env_args[:args].unshift('-I',Installation.instance.path(:fallback_cert))
|
89
|
+
env_args[:args].unshift('-Y', Installation.instance.path(:fallback_key))
|
90
|
+
env_args[:args].unshift('-I', Installation.instance.path(:fallback_cert))
|
94
91
|
end
|
95
92
|
|
96
93
|
env_args[:args].unshift('-q') if @options[:quiet]
|
97
94
|
|
98
95
|
# transfer job can be multi session
|
99
96
|
xfer_job = {
|
100
|
-
id:
|
97
|
+
id: the_job_id,
|
101
98
|
sessions: [] # all sessions as below
|
102
99
|
}
|
103
100
|
|
104
101
|
# generic session information
|
105
102
|
session = {
|
106
|
-
thread:
|
107
|
-
error:
|
108
|
-
io:
|
109
|
-
id:
|
110
|
-
|
111
|
-
|
103
|
+
thread: nil, # Thread object monitoring management port, not nil when pushed to :sessions
|
104
|
+
error: nil, # exception if failed
|
105
|
+
io: nil, # management port server socket
|
106
|
+
id: nil, # SessionId from INIT message in mgt port
|
107
|
+
token_regenerator: token_regenerator, # regenerate bearer token with oauth
|
108
|
+
env_args: env_args # env vars and args to ascp (from transfer spec)
|
112
109
|
}
|
113
110
|
|
114
111
|
if multi_session_info.nil?
|
@@ -127,7 +124,7 @@ module Aspera
|
|
127
124
|
this_session[:env_args][:args] = this_session[:env_args][:args].clone
|
128
125
|
this_session[:env_args][:args].unshift("-C#{i}:#{multi_session_info[:count]}")
|
129
126
|
# option: increment (default as per ascp manual) or not (cluster on other side ?)
|
130
|
-
this_session[:env_args][:args].unshift('-O',(multi_session_info[:udp_base] + i - 1).to_s) if @options[:multi_incr_udp]
|
127
|
+
this_session[:env_args][:args].unshift('-O', (multi_session_info[:udp_base] + i - 1).to_s) if @options[:multi_incr_udp]
|
131
128
|
this_session[:thread] = Thread.new(this_session) {|s|transfer_thread_entry(s)}
|
132
129
|
xfer_job[:sessions].push(this_session)
|
133
130
|
end
|
@@ -135,10 +132,10 @@ module Aspera
|
|
135
132
|
Log.log.debug('started session thread(s)')
|
136
133
|
|
137
134
|
# add job to list of jobs
|
138
|
-
@jobs[
|
139
|
-
Log.log.debug
|
135
|
+
@jobs[the_job_id] = xfer_job
|
136
|
+
Log.log.debug{"jobs: #{@jobs.keys.count}"}
|
140
137
|
|
141
|
-
return
|
138
|
+
return the_job_id
|
142
139
|
end # start_transfer
|
143
140
|
|
144
141
|
# wait for completion of all jobs started
|
@@ -147,9 +144,9 @@ module Aspera
|
|
147
144
|
Log.log.debug('wait_for_transfers_completion')
|
148
145
|
# set to non-nil to exit loop
|
149
146
|
result = []
|
150
|
-
@jobs.each do |_id,job|
|
147
|
+
@jobs.each do |_id, job|
|
151
148
|
job[:sessions].each do |session|
|
152
|
-
Log.log.debug
|
149
|
+
Log.log.debug{"join #{session[:thread]}"}
|
153
150
|
session[:thread].join
|
154
151
|
result.push(session[:error] || :success)
|
155
152
|
end
|
@@ -173,13 +170,13 @@ module Aspera
|
|
173
170
|
# @param env_args a hash containing :args :env :ascp_version
|
174
171
|
# @param session this session information
|
175
172
|
# could be private method
|
176
|
-
def start_transfer_with_args_env(env_args,session)
|
173
|
+
def start_transfer_with_args_env(env_args, session)
|
177
174
|
raise 'env_args must be Hash' unless env_args.is_a?(Hash)
|
178
175
|
raise 'session must be Hash' unless session.is_a?(Hash)
|
179
176
|
# by default we assume an exception will be raised (for ensure block)
|
180
177
|
exception_raised = true
|
181
178
|
begin
|
182
|
-
Log.log.debug
|
179
|
+
Log.log.debug{"env_args=#{env_args.inspect}"}
|
183
180
|
# get location of ascp executable
|
184
181
|
ascp_path = @mutex.synchronize do
|
185
182
|
Fasp::Installation.instance.path(env_args[:ascp_version])
|
@@ -187,24 +184,24 @@ module Aspera
|
|
187
184
|
# (optional) check it exists
|
188
185
|
raise Fasp::Error, "no such file: #{ascp_path}" unless File.exist?(ascp_path)
|
189
186
|
# open random local TCP port for listening for ascp management
|
190
|
-
mgt_sock = TCPServer.new('127.0.0.1',0)
|
187
|
+
mgt_sock = TCPServer.new('127.0.0.1', 0)
|
191
188
|
# clone arguments as we eed to modify with mgt port
|
192
189
|
ascp_arguments = env_args[:args].clone
|
193
190
|
# add management port
|
194
191
|
ascp_arguments.unshift('-M', mgt_sock.addr[1].to_s)
|
195
192
|
# start ascp in sub process
|
196
193
|
Log.log.debug do
|
197
|
-
'execute: '+
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
194
|
+
'execute: ' +
|
195
|
+
env_args[:env].map{|k, v| "#{k}=#{Shellwords.shellescape(v)}"}.join(' ') +
|
196
|
+
' ' +
|
197
|
+
Shellwords.shellescape(ascp_path) +
|
198
|
+
' ' +
|
199
|
+
ascp_arguments.map{|a|Shellwords.shellescape(a)}.join(' ')
|
203
200
|
end
|
204
201
|
# start process
|
205
|
-
ascp_pid = Process.spawn(env_args[:env],[ascp_path,ascp_path]
|
202
|
+
ascp_pid = Process.spawn(env_args[:env], [ascp_path, ascp_path], *ascp_arguments)
|
206
203
|
# in parent, wait for connection to socket max 3 seconds
|
207
|
-
Log.log.debug
|
204
|
+
Log.log.debug{"before accept for pid (#{ascp_pid})"}
|
208
205
|
# init management socket
|
209
206
|
ascp_mgt_io = nil
|
210
207
|
Timeout.timeout(@options[:spawn_timeout_sec]) do
|
@@ -214,7 +211,7 @@ module Aspera
|
|
214
211
|
# TODO: use same value as Encoding.default_external
|
215
212
|
ascp_mgt_io.set_encoding(Encoding::UTF_8)
|
216
213
|
end
|
217
|
-
Log.log.debug
|
214
|
+
Log.log.debug{"after accept (#{ascp_mgt_io})"}
|
218
215
|
session[:io] = ascp_mgt_io
|
219
216
|
# exact text for event, with \n
|
220
217
|
current_event_text = ''
|
@@ -230,7 +227,7 @@ module Aspera
|
|
230
227
|
break if line.nil?
|
231
228
|
current_event_text += line
|
232
229
|
line.chomp!
|
233
|
-
Log.log.debug
|
230
|
+
Log.log.debug{"line=[#{line}]"}
|
234
231
|
case line
|
235
232
|
when 'FASPMGR 2'
|
236
233
|
# begin event
|
@@ -243,12 +240,12 @@ module Aspera
|
|
243
240
|
# empty line is separator to end event information
|
244
241
|
raise 'unexpected empty line' if current_event_data.nil?
|
245
242
|
current_event_data[AgentBase::LISTENER_SESSION_ID_B] = ascp_pid
|
246
|
-
notify_listeners(current_event_text,current_event_data)
|
243
|
+
notify_listeners(current_event_text, current_event_data)
|
247
244
|
case current_event_data['Type']
|
248
245
|
when 'INIT'
|
249
246
|
session[:id] = current_event_data['SessionId']
|
250
|
-
Log.log.debug
|
251
|
-
when 'DONE','ERROR'
|
247
|
+
Log.log.debug{"session id: #{session[:id]}"}
|
248
|
+
when 'DONE', 'ERROR'
|
252
249
|
# TODO: check if this is always the last event
|
253
250
|
last_status_event = current_event_data
|
254
251
|
end # event type
|
@@ -263,20 +260,20 @@ module Aspera
|
|
263
260
|
# all went well
|
264
261
|
exception_raised = false
|
265
262
|
when 'ERROR'
|
266
|
-
Log.log.error
|
263
|
+
Log.log.error{"code: #{last_status_event['Code']}"}
|
267
264
|
if /bearer token/i.match?(last_status_event['Description'])
|
268
265
|
Log.log.error('need to regenerate token'.red)
|
269
|
-
if session[:
|
266
|
+
if session[:token_regenerator].respond_to?(:refreshed_transfer_token)
|
270
267
|
# regenerate token here, expired, or error on it
|
271
268
|
# Note: in multi-session, each session will have a different one.
|
272
|
-
env_args[:env]['ASPERA_SCP_TOKEN'] = session[:
|
269
|
+
env_args[:env]['ASPERA_SCP_TOKEN'] = session[:token_regenerator].refreshed_transfer_token
|
273
270
|
end
|
274
271
|
end
|
275
272
|
# cannot resolve address
|
276
|
-
#if last_status_event['Code'].to_i.eql?(14)
|
277
|
-
# Log.log.warn
|
278
|
-
#end
|
279
|
-
raise Fasp::Error.new(last_status_event['Description'],last_status_event['Code'].to_i)
|
273
|
+
# if last_status_event['Code'].to_i.eql?(14)
|
274
|
+
# Log.log.warn{"host: #{}"}
|
275
|
+
# end
|
276
|
+
raise Fasp::Error.new(last_status_event['Description'], last_status_event['Code'].to_i)
|
280
277
|
else # case
|
281
278
|
raise "unexpected last event type: #{last_status_event['Type']}"
|
282
279
|
end
|
@@ -316,19 +313,19 @@ module Aspera
|
|
316
313
|
# @param data command on mgt port, examples:
|
317
314
|
# {'type'=>'START','source'=>_path_,'destination'=>_path_}
|
318
315
|
# {'type'=>'DONE'}
|
319
|
-
def send_command(job_id,session_index,data)
|
316
|
+
def send_command(job_id, session_index, data)
|
320
317
|
job = @jobs[job_id]
|
321
318
|
raise 'no such job' if job.nil?
|
322
319
|
session = job[:sessions][session_index]
|
323
320
|
raise 'no such session' if session.nil?
|
324
|
-
Log.log.debug
|
321
|
+
Log.log.debug{"command: #{data}"}
|
325
322
|
# build command
|
326
|
-
command = data
|
327
|
-
keys
|
328
|
-
map{|k|"#{k.capitalize}: #{data[k]}"}
|
329
|
-
unshift('FASPMGR 2')
|
330
|
-
push('','')
|
331
|
-
join("\n")
|
323
|
+
command = data
|
324
|
+
.keys
|
325
|
+
.map{|k|"#{k.capitalize}: #{data[k]}"}
|
326
|
+
.unshift('FASPMGR 2')
|
327
|
+
.push('', '')
|
328
|
+
.join("\n")
|
332
329
|
session[:io].puts(command)
|
333
330
|
end
|
334
331
|
|
@@ -345,12 +342,12 @@ module Aspera
|
|
345
342
|
@options = DEFAULT_OPTIONS.dup
|
346
343
|
if !options.nil?
|
347
344
|
raise "expecting Hash (or nil), but have #{options.class}" unless options.is_a?(Hash)
|
348
|
-
options.each do |k,v|
|
349
|
-
raise "Unknown local agent parameter: #{k}, expect one of #{DEFAULT_OPTIONS.keys.map(&:to_s).join(',')}" unless DEFAULT_OPTIONS.
|
345
|
+
options.each do |k, v|
|
346
|
+
raise "Unknown local agent parameter: #{k}, expect one of #{DEFAULT_OPTIONS.keys.map(&:to_s).join(',')}" unless DEFAULT_OPTIONS.key?(k)
|
350
347
|
@options[k] = v
|
351
348
|
end
|
352
349
|
end
|
353
|
-
Log.log.debug
|
350
|
+
Log.log.debug{"local options= #{options}"}
|
354
351
|
@resume_policy = ResumePolicy.new(@options[:resume].symbolize_keys)
|
355
352
|
end
|
356
353
|
|
@@ -360,17 +357,17 @@ module Aspera
|
|
360
357
|
begin
|
361
358
|
# set name for logging
|
362
359
|
Thread.current[:name] = 'transfer'
|
363
|
-
Log.log.debug
|
360
|
+
Log.log.debug{"ENTER (#{Thread.current[:name]})"}
|
364
361
|
# start transfer with selected resumer policy
|
365
|
-
|
366
|
-
start_transfer_with_args_env(session[:env_args],session)
|
362
|
+
@resume_policy.execute_with_resume do
|
363
|
+
start_transfer_with_args_env(session[:env_args], session)
|
367
364
|
end
|
368
365
|
Log.log.debug('transfer ok'.bg_green)
|
369
366
|
rescue StandardError => e
|
370
367
|
session[:error] = e
|
371
|
-
Log.log.error
|
368
|
+
Log.log.error{"Transfer thread error: #{e.class}:\n#{e.message}:\n#{e.backtrace.join("\n")}".red} if Log.instance.level.eql?(:debug)
|
372
369
|
end
|
373
|
-
Log.log.debug
|
370
|
+
Log.log.debug{"EXIT (#{Thread.current[:name]})"}
|
374
371
|
end
|
375
372
|
end # AgentDirect
|
376
373
|
end
|
@@ -14,24 +14,25 @@ require 'json'
|
|
14
14
|
module Aspera
|
15
15
|
module Fasp
|
16
16
|
# start a transfer using Aspera HTTP Gateway, using web socket session for uploads
|
17
|
-
class AgentHttpgw < AgentBase
|
17
|
+
class AgentHttpgw < Aspera::Fasp::AgentBase
|
18
18
|
# message returned by HTTP GW in case of success
|
19
19
|
MSG_END_UPLOAD = 'end upload'
|
20
20
|
MSG_END_SLICE = 'end_slice_upload'
|
21
|
+
# options available in CLI (transfer_info)
|
21
22
|
DEFAULT_OPTIONS = {
|
22
23
|
url: nil,
|
23
|
-
|
24
|
+
upload_chunk_size: 64_000,
|
24
25
|
upload_bar_refresh_sec: 0.5
|
25
26
|
}.freeze
|
26
|
-
DEFAULT_BASE_PATH='/aspera/http-gwy'
|
27
|
+
DEFAULT_BASE_PATH = '/aspera/http-gwy'
|
27
28
|
# upload endpoints
|
28
|
-
V1_UPLOAD='/v1/upload'
|
29
|
-
V2_UPLOAD='/v2/upload'
|
30
|
-
private_constant :DEFAULT_OPTIONS
|
29
|
+
V1_UPLOAD = '/v1/upload'
|
30
|
+
V2_UPLOAD = '/v2/upload'
|
31
|
+
private_constant :DEFAULT_OPTIONS, :MSG_END_UPLOAD, :MSG_END_SLICE, :V1_UPLOAD, :V2_UPLOAD
|
31
32
|
|
32
33
|
# send message on http gw web socket
|
33
34
|
def ws_snd_json(data)
|
34
|
-
@slice_uploads += 1 if data.
|
35
|
+
@slice_uploads += 1 if data.key?(:slice_upload)
|
35
36
|
Log.log.debug{JSON.generate(data)}
|
36
37
|
ws_send(JSON.generate(data))
|
37
38
|
end
|
@@ -47,7 +48,7 @@ module Aspera
|
|
47
48
|
# we need to keep track of actual file path because transfer spec is modified to be sent in web socket
|
48
49
|
source_paths = []
|
49
50
|
# get source root or nil
|
50
|
-
source_root = transfer_spec.
|
51
|
+
source_root = transfer_spec.key?('source_root') && !transfer_spec['source_root'].empty? ? transfer_spec['source_root'] : nil
|
51
52
|
# source root is ignored by GW, used only here
|
52
53
|
transfer_spec.delete('source_root')
|
53
54
|
# compute total size of files to upload (for progress)
|
@@ -56,7 +57,7 @@ module Aspera
|
|
56
57
|
# save actual file location to be able read contents later
|
57
58
|
full_src_filepath = item['source']
|
58
59
|
# add source root if needed
|
59
|
-
full_src_filepath = File.join(source_root,full_src_filepath) unless source_root.nil?
|
60
|
+
full_src_filepath = File.join(source_root, full_src_filepath) unless source_root.nil?
|
60
61
|
# GW expects a simple file name in 'source' but if user wants to change the name, we take it
|
61
62
|
item['source'] = File.basename(item['destination'].nil? ? item['source'] : item['destination'])
|
62
63
|
item['file_size'] = File.size(full_src_filepath)
|
@@ -66,18 +67,18 @@ module Aspera
|
|
66
67
|
end
|
67
68
|
# identify this session uniquely
|
68
69
|
session_id = SecureRandom.uuid
|
69
|
-
@slice_uploads=0
|
70
|
+
@slice_uploads = 0
|
70
71
|
# web socket endpoint: by default use v2 (newer gateways), without base64 encoding
|
71
72
|
upload_api_version = V2_UPLOAD
|
72
73
|
# is the latest supported? else revert to old api
|
73
|
-
upload_api_version=V1_UPLOAD unless @api_info['endpoints'].any?{|i|i.include?(upload_api_version)}
|
74
|
+
upload_api_version = V1_UPLOAD unless @api_info['endpoints'].any?{|i|i.include?(upload_api_version)}
|
74
75
|
Log.log.debug{"api version: #{upload_api_version}"}
|
75
|
-
url=File.join(@gw_api.params[:base_url],upload_api_version)
|
76
|
-
#uri = URI.parse(url)
|
76
|
+
url = File.join(@gw_api.params[:base_url], upload_api_version)
|
77
|
+
# uri = URI.parse(url)
|
77
78
|
# open web socket to end point (equivalent to Net::HTTP.start)
|
78
79
|
http_socket = Rest.start_http_session(url)
|
79
80
|
@ws_io = http_socket.instance_variable_get(:@socket)
|
80
|
-
|
81
|
+
# @ws_io.debug_output = Log.log
|
81
82
|
@ws_handshake = ::WebSocket::Handshake::Client.new(url: url, headers: {})
|
82
83
|
@ws_io.write(@ws_handshake.to_s)
|
83
84
|
sleep(0.1)
|
@@ -85,21 +86,21 @@ module Aspera
|
|
85
86
|
raise 'Error in websocket handshake' unless @ws_handshake.finished?
|
86
87
|
Log.log.debug('ws: handshake success')
|
87
88
|
# data shared between main thread and read thread
|
88
|
-
shared_info={
|
89
|
+
shared_info = {
|
89
90
|
read_exception: nil, # error message if any in callback
|
90
91
|
end_uploads: 0 # number of files totally sent
|
91
|
-
#mutex: Mutex.new
|
92
|
-
#cond_var: ConditionVariable.new
|
92
|
+
# mutex: Mutex.new
|
93
|
+
# cond_var: ConditionVariable.new
|
93
94
|
}
|
94
95
|
# start read thread
|
95
96
|
ws_read_thread = Thread.new do
|
96
97
|
Log.log.debug('ws: thread: started')
|
97
98
|
frame = ::WebSocket::Frame::Incoming::Client.new
|
98
99
|
loop do
|
99
|
-
begin
|
100
|
+
begin # rubocop:disable Style/RedundantBegin
|
100
101
|
frame << @ws_io.readuntil("\n")
|
101
102
|
while (msg = frame.next)
|
102
|
-
Log.log.debug
|
103
|
+
Log.log.debug{"ws: thread: message: #{msg.data} #{shared_info[:end_uploads]}"}
|
103
104
|
message = msg.data
|
104
105
|
if message.eql?(MSG_END_UPLOAD)
|
105
106
|
shared_info[:end_uploads] += 1
|
@@ -122,12 +123,12 @@ module Aspera
|
|
122
123
|
break
|
123
124
|
end
|
124
125
|
end
|
125
|
-
Log.log.debug
|
126
|
+
Log.log.debug{"ws: thread: stopping (exc=#{shared_info[:read_exception]},cls=#{shared_info[:read_exception].class})"}
|
126
127
|
end
|
127
128
|
# notify progress bar
|
128
|
-
notify_begin(session_id,total_size)
|
129
|
+
notify_begin(session_id, total_size)
|
129
130
|
# first step send transfer spec
|
130
|
-
Log.dump(:ws_spec,transfer_spec)
|
131
|
+
Log.dump(:ws_spec, transfer_spec)
|
131
132
|
ws_snd_json(transfer_spec: transfer_spec)
|
132
133
|
# current file index
|
133
134
|
file_index = 0
|
@@ -135,58 +136,63 @@ module Aspera
|
|
135
136
|
sent_bytes = 0
|
136
137
|
# last progress event
|
137
138
|
last_progress_time = nil
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
139
|
+
|
140
|
+
transfer_spec['paths'].each do |item|
|
141
|
+
# TODO: get mime type?
|
142
|
+
file_mime_type = ''
|
143
|
+
file_size = item['file_size']
|
144
|
+
file_name = File.basename(item[item['destination'].nil? ? 'source' : 'destination'])
|
145
|
+
# compute total number of slices
|
146
|
+
slice_total = ((file_size - 1) / @options[:upload_chunk_size]) + 1
|
147
|
+
File.open(source_paths[file_index]) do |file|
|
148
|
+
# current slice index
|
149
|
+
slice_index = 0
|
150
|
+
until file.eof?
|
151
|
+
data = file.read(@options[:upload_chunk_size])
|
152
|
+
slice_data = {
|
153
|
+
name: file_name,
|
154
|
+
type: file_mime_type,
|
155
|
+
size: file_size,
|
156
|
+
slice: slice_index,
|
157
|
+
total_slices: slice_total,
|
158
|
+
fileIndex: file_index
|
159
|
+
}
|
160
|
+
# Log.dump(:slice_data,slice_data) #if slice_index.eql?(0)
|
161
|
+
# interrupt main thread if read thread failed
|
162
|
+
raise shared_info[:read_exception] unless shared_info[:read_exception].nil?
|
163
|
+
begin
|
162
164
|
if upload_api_version.eql?(V1_UPLOAD)
|
163
165
|
slice_data[:data] = Base64.strict_encode64(data)
|
164
166
|
ws_snd_json(slice_upload: slice_data)
|
165
167
|
else
|
166
|
-
ws_snd_json(slice_upload: slice_data) if
|
167
|
-
ws_send(data,type: :binary)
|
168
|
-
Log.log.debug{"ws: sent buffer: #{file_index} / #{
|
169
|
-
ws_snd_json(slice_upload: slice_data) if
|
170
|
-
end
|
171
|
-
sent_bytes += data.length
|
172
|
-
currenttime = Time.now
|
173
|
-
if last_progress_time.nil? || ((currenttime - last_progress_time) > @options[:upload_bar_refresh_sec])
|
174
|
-
notify_progress(session_id,sent_bytes)
|
175
|
-
last_progress_time = currenttime
|
168
|
+
ws_snd_json(slice_upload: slice_data) if slice_index.eql?(0)
|
169
|
+
ws_send(data, type: :binary)
|
170
|
+
Log.log.debug{"ws: sent buffer: #{file_index} / #{slice_index}"}
|
171
|
+
ws_snd_json(slice_upload: slice_data) if slice_index.eql?(slice_total - 1)
|
176
172
|
end
|
177
|
-
|
173
|
+
rescue Errno::EPIPE => e
|
174
|
+
raise shared_info[:read_exception] unless shared_info[:read_exception].nil?
|
175
|
+
raise e
|
176
|
+
end
|
177
|
+
sent_bytes += data.length
|
178
|
+
current_time = Time.now
|
179
|
+
if last_progress_time.nil? || ((current_time - last_progress_time) > @options[:upload_bar_refresh_sec])
|
180
|
+
notify_progress(session_id, sent_bytes)
|
181
|
+
last_progress_time = current_time
|
178
182
|
end
|
183
|
+
slice_index += 1
|
179
184
|
end
|
180
|
-
file_index += 1
|
181
185
|
end
|
186
|
+
file_index += 1
|
182
187
|
end
|
188
|
+
|
183
189
|
Log.log.debug('Finished upload')
|
184
190
|
ws_read_thread.join
|
185
191
|
Log.log.debug{"result: #{shared_info[:end_uploads]} / #{@slice_uploads}"}
|
186
192
|
ws_send(nil, type: :close) unless @ws_io.nil?
|
187
193
|
@ws_io = nil
|
188
194
|
http_socket&.finish
|
189
|
-
notify_progress(session_id,sent_bytes)
|
195
|
+
notify_progress(session_id, sent_bytes)
|
190
196
|
notify_end(session_id)
|
191
197
|
end
|
192
198
|
|
@@ -194,18 +200,18 @@ module Aspera
|
|
194
200
|
transfer_spec['zip_required'] ||= false
|
195
201
|
transfer_spec['source_root'] ||= '/'
|
196
202
|
# is normally provided by application, like package name
|
197
|
-
if !transfer_spec.
|
203
|
+
if !transfer_spec.key?('download_name')
|
198
204
|
# by default it is the name of first file
|
199
|
-
|
205
|
+
download_name = File.basename(transfer_spec['paths'].first['source'])
|
200
206
|
# we remove extension
|
201
|
-
|
207
|
+
download_name = download_name.gsub(/\.@gw_api.*$/, '')
|
202
208
|
# ands add indication of number of files if there is more than one
|
203
209
|
if transfer_spec['paths'].length > 1
|
204
|
-
|
210
|
+
download_name += " #{transfer_spec['paths'].length} Files"
|
205
211
|
end
|
206
|
-
transfer_spec['download_name'] =
|
212
|
+
transfer_spec['download_name'] = download_name
|
207
213
|
end
|
208
|
-
creation = @gw_api.create('v1/download',{'transfer_spec' => transfer_spec})[:data]
|
214
|
+
creation = @gw_api.create('v1/download', {'transfer_spec' => transfer_spec})[:data]
|
209
215
|
transfer_uuid = creation['url'].split('/').last
|
210
216
|
file_dest =
|
211
217
|
if transfer_spec['zip_required'] || transfer_spec['paths'].length > 1
|
@@ -215,19 +221,18 @@ module Aspera
|
|
215
221
|
# it is a plain file if we don't require zip and there is only one file
|
216
222
|
File.basename(transfer_spec['paths'].first['source'])
|
217
223
|
end
|
218
|
-
file_dest = File.join(transfer_spec['destination_root'],file_dest)
|
219
|
-
@gw_api.call({operation: 'GET',subpath: "v1/download/#{transfer_uuid}",save_to_file: file_dest})
|
224
|
+
file_dest = File.join(transfer_spec['destination_root'], file_dest)
|
225
|
+
@gw_api.call({operation: 'GET', subpath: "v1/download/#{transfer_uuid}", save_to_file: file_dest})
|
220
226
|
end
|
221
227
|
|
222
228
|
# start FASP transfer based on transfer spec (hash table)
|
223
229
|
# note that it is asynchronous
|
224
230
|
# HTTP download only supports file list
|
225
|
-
def start_transfer(transfer_spec,
|
231
|
+
def start_transfer(transfer_spec, token_regenerator: nil)
|
226
232
|
raise 'GW URL must be set' if @gw_api.nil?
|
227
|
-
raise 'option: must be hash (or nil)' unless options.is_a?(Hash)
|
228
233
|
raise 'paths: must be Array' unless transfer_spec['paths'].is_a?(Array)
|
229
234
|
raise 'only token based transfer is supported in GW' unless transfer_spec['token'].is_a?(String)
|
230
|
-
Log.dump(:user_spec,transfer_spec)
|
235
|
+
Log.dump(:user_spec, transfer_spec)
|
231
236
|
transfer_spec['authentication'] ||= 'token'
|
232
237
|
case transfer_spec['direction']
|
233
238
|
when Fasp::TransferSpec::DIRECTION_SEND
|
@@ -253,17 +258,17 @@ module Aspera
|
|
253
258
|
private
|
254
259
|
|
255
260
|
def initialize(opts)
|
256
|
-
Log.log.debug
|
261
|
+
Log.log.debug{"local options= #{opts}"}
|
257
262
|
# set default options and override if specified
|
258
263
|
@options = DEFAULT_OPTIONS.dup
|
259
264
|
raise "httpgw agent parameters (transfer_info): expecting Hash, but have #{opts.class}" unless opts.is_a?(Hash)
|
260
|
-
opts.symbolize_keys.each do |k,v|
|
261
|
-
raise "httpgw agent parameter: Unknown: #{k}, expect one of #{DEFAULT_OPTIONS.keys.map(&:to_s).join(',')}" unless DEFAULT_OPTIONS.
|
265
|
+
opts.symbolize_keys.each do |k, v|
|
266
|
+
raise "httpgw agent parameter: Unknown: #{k}, expect one of #{DEFAULT_OPTIONS.keys.map(&:to_s).join(',')}" unless DEFAULT_OPTIONS.key?(k)
|
262
267
|
@options[k] = v
|
263
268
|
end
|
264
269
|
raise 'missing param: url' if @options[:url].nil?
|
265
270
|
# remove /v1 from end
|
266
|
-
@options[:url].gsub(%r{/v1/*$},'')
|
271
|
+
@options[:url].gsub(%r{/v1/*$}, '')
|
267
272
|
super()
|
268
273
|
@gw_api = Rest.new({base_url: @options[:url]})
|
269
274
|
@api_info = @gw_api.read('v1/info')[:data]
|