aspera-cli 4.18.1 → 4.19.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 +13 -0
- data/CONTRIBUTING.md +5 -12
- data/README.md +60 -29
- data/examples/build_exec +85 -0
- data/lib/aspera/agent/base.rb +2 -0
- data/lib/aspera/agent/direct.rb +108 -104
- data/lib/aspera/api/aoc.rb +2 -2
- data/lib/aspera/api/httpgw.rb +91 -56
- data/lib/aspera/ascp/installation.rb +47 -32
- data/lib/aspera/ascp/management.rb +4 -1
- data/lib/aspera/ascp/products.rb +1 -7
- data/lib/aspera/cli/formatter.rb +24 -18
- data/lib/aspera/cli/manager.rb +10 -10
- data/lib/aspera/cli/plugin.rb +2 -2
- data/lib/aspera/cli/plugin_factory.rb +10 -1
- data/lib/aspera/cli/plugins/config.rb +15 -10
- data/lib/aspera/cli/plugins/node.rb +4 -3
- data/lib/aspera/cli/plugins/server.rb +1 -1
- data/lib/aspera/cli/plugins/shares.rb +11 -7
- data/lib/aspera/cli/sync_actions.rb +72 -31
- data/lib/aspera/cli/transfer_agent.rb +1 -0
- data/lib/aspera/cli/transfer_progress.rb +1 -1
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/environment.rb +43 -10
- data/lib/aspera/faspex_gw.rb +1 -1
- data/lib/aspera/keychain/encrypted_hash.rb +2 -0
- data/lib/aspera/log.rb +1 -0
- data/lib/aspera/node_simulator.rb +1 -1
- data/lib/aspera/oauth/jwt.rb +1 -1
- data/lib/aspera/oauth/url_json.rb +2 -0
- data/lib/aspera/oauth/web.rb +5 -4
- 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 +27 -19
- data/lib/aspera/transfer/spec.rb +8 -10
- data/lib/aspera/transfer/sync.rb +52 -47
- data/lib/aspera/web_auth.rb +0 -1
- data/lib/aspera/web_server_simple.rb +24 -13
- data.tar.gz.sig +0 -0
- metadata +3 -3
- metadata.gz.sig +0 -0
- data/examples/rubyc +0 -24
data/lib/aspera/agent/direct.rb
CHANGED
@@ -19,9 +19,10 @@ module Aspera
|
|
19
19
|
# executes a local "ascp", connects mgt port, equivalent of "Fasp Manager"
|
20
20
|
class Direct < Base
|
21
21
|
LISTEN_LOCAL_ADDRESS = '127.0.0.1'
|
22
|
-
|
22
|
+
# 0 means: select an available port
|
23
|
+
SELECT_AVAILABLE_PORT = 0
|
23
24
|
# spellchecker: enable
|
24
|
-
private_constant :LISTEN_LOCAL_ADDRESS, :
|
25
|
+
private_constant :LISTEN_LOCAL_ADDRESS, :SELECT_AVAILABLE_PORT
|
25
26
|
|
26
27
|
# method of Base
|
27
28
|
# start ascp transfer(s) (non blocking), single or multi-session
|
@@ -93,12 +94,12 @@ module Aspera
|
|
93
94
|
# do deep copy (each thread has its own copy because it is modified here below and in thread)
|
94
95
|
this_session = session.clone
|
95
96
|
this_session[:ts] = this_session[:ts].clone
|
96
|
-
this_session[:env_args] = this_session[:env_args].clone
|
97
|
-
|
97
|
+
env_args = this_session[:env_args] = this_session[:env_args].clone
|
98
|
+
args = env_args[:args] = env_args[:args].clone
|
98
99
|
# set multi session part
|
99
|
-
|
100
|
+
args.unshift("-C#{i}:#{multi_session_info[:count]}")
|
100
101
|
# option: increment (default as per ascp manual) or not (cluster on other side ?)
|
101
|
-
|
102
|
+
args.unshift('-O', (multi_session_info[:udp_base] + i - 1).to_s) if @multi_incr_udp
|
102
103
|
# finally start the thread
|
103
104
|
this_session[:thread] = Thread.new(this_session) {|session_info|transfer_thread_entry(session_info)}
|
104
105
|
@sessions.push(this_session)
|
@@ -129,81 +130,46 @@ module Aspera
|
|
129
130
|
Log.log.debug('fasp local shutdown')
|
130
131
|
end
|
131
132
|
|
132
|
-
# @
|
133
|
-
def
|
134
|
-
|
135
|
-
case event['Type']
|
136
|
-
when 'INIT'
|
137
|
-
@pre_calc_sent = false
|
138
|
-
@pre_calc_last_size = nil
|
139
|
-
notify_progress(session_id: session_id, type: :session_start)
|
140
|
-
when 'NOTIFICATION' # sent from remote
|
141
|
-
if event.key?('PreTransferBytes')
|
142
|
-
@pre_calc_sent = true
|
143
|
-
notify_progress(session_id: session_id, type: :session_size, info: event['PreTransferBytes'])
|
144
|
-
end
|
145
|
-
when 'STATS' # during transfer
|
146
|
-
@pre_calc_last_size = event['TransferBytes'].to_i + event['StartByte'].to_i
|
147
|
-
notify_progress(session_id: session_id, type: :transfer, info: @pre_calc_last_size)
|
148
|
-
when 'DONE', 'ERROR' # end of session
|
149
|
-
total_size = event['TransferBytes'].to_i + event['StartByte'].to_i
|
150
|
-
if !@pre_calc_sent && !total_size.zero?
|
151
|
-
notify_progress(session_id: session_id, type: :session_size, info: total_size)
|
152
|
-
end
|
153
|
-
if @pre_calc_last_size != total_size
|
154
|
-
notify_progress(session_id: session_id, type: :transfer, info: total_size)
|
155
|
-
end
|
156
|
-
notify_progress(session_id: session_id, type: :end)
|
157
|
-
# cspell:disable
|
158
|
-
when 'SESSION'
|
159
|
-
when 'ARGSTOP'
|
160
|
-
when 'FILEERROR'
|
161
|
-
when 'STOP'
|
162
|
-
# cspell:enable
|
163
|
-
# stop event when one file is completed
|
164
|
-
else
|
165
|
-
Log.log.debug{"unknown event type #{event['Type']}"}
|
166
|
-
end
|
133
|
+
# @return [Array] list of sessions for a job
|
134
|
+
def sessions_by_job(job_id)
|
135
|
+
@sessions.select{|session_info| session_info[:job_id].eql?(job_id)}
|
167
136
|
end
|
168
137
|
|
169
|
-
# This is the low level method to start the
|
170
|
-
#
|
171
|
-
#
|
138
|
+
# This is the low level method to start the transfer process
|
139
|
+
# Start process with management port.
|
140
|
+
# returns
|
172
141
|
# raises FaspError on error
|
173
|
-
#
|
174
|
-
#
|
175
|
-
# @param
|
176
|
-
# @param
|
177
|
-
#
|
178
|
-
def
|
179
|
-
|
142
|
+
# @param session this session information, keys :io and :token_regenerator
|
143
|
+
# @param env [Hash] environment variables
|
144
|
+
# @param name [Symbol] name of executable: :ascp, :ascp4 or :async
|
145
|
+
# @param args [Array] command line arguments
|
146
|
+
# @return [nil] when process has exited
|
147
|
+
def start_and_monitor_process(
|
148
|
+
session:,
|
149
|
+
env:,
|
150
|
+
name:,
|
151
|
+
args:
|
152
|
+
)
|
180
153
|
Aspera.assert_type(session, Hash)
|
181
|
-
Log.log.debug{"env_args=#{env_args.inspect}"}
|
182
154
|
notify_progress(session_id: nil, type: :pre_start, info: 'starting')
|
183
155
|
begin
|
184
|
-
|
156
|
+
command_pid = nil
|
185
157
|
# we use Socket directly, instead of TCPServer, as it gives access to lower level options
|
186
|
-
socket_class =
|
158
|
+
socket_class = defined?(JRUBY_VERSION) ? ServerSocket : Socket
|
187
159
|
mgt_server_socket = socket_class.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
|
188
|
-
# open any available (0) local TCP port for use as
|
189
|
-
mgt_server_socket.bind(Addrinfo.tcp(LISTEN_LOCAL_ADDRESS,
|
160
|
+
# open any available (0) local TCP port for use as management port
|
161
|
+
mgt_server_socket.bind(Addrinfo.tcp(LISTEN_LOCAL_ADDRESS, SELECT_AVAILABLE_PORT))
|
190
162
|
# build arguments and add mgt port
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
Log.log.debug do
|
196
|
-
[
|
197
|
-
'execute:'.red,
|
198
|
-
env_args[:env].map{|k, v| "#{k}=#{Shellwords.shellescape(v)}"},
|
199
|
-
Shellwords.shellescape(ascp_path),
|
200
|
-
ascp_arguments.map{|a|Shellwords.shellescape(a)}
|
201
|
-
].flatten.join(' ')
|
163
|
+
command_arguments = if name.eql?(:async)
|
164
|
+
["--exclusive-mgmt-port=#{mgt_server_socket.local_address.ip_port}"]
|
165
|
+
else
|
166
|
+
['-M', mgt_server_socket.local_address.ip_port.to_s]
|
202
167
|
end
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
168
|
+
command_arguments.concat(args)
|
169
|
+
# get location of command executable (ascp, async)
|
170
|
+
command_path = Ascp::Installation.instance.path(name)
|
171
|
+
command_pid = Environment.secure_spawn(env: env, exec: command_path, args: command_arguments)
|
172
|
+
notify_progress(session_id: nil, type: :pre_start, info: "waiting for #{name} to start")
|
207
173
|
mgt_server_socket.listen(1)
|
208
174
|
# TODO: timeout does not work when Process.spawn is used... until process exits, then it works
|
209
175
|
Log.log.debug{"before select, timeout: #{@spawn_timeout_sec}"}
|
@@ -213,15 +179,15 @@ module Aspera
|
|
213
179
|
# There is a connection to accept
|
214
180
|
client_socket, _client_addrinfo = mgt_server_socket.accept
|
215
181
|
Log.log.debug('after accept')
|
216
|
-
|
182
|
+
management_port_io = client_socket.to_io
|
217
183
|
# management messages include file names which may be utf8
|
218
184
|
# by default socket is US-ASCII
|
219
185
|
# TODO: use same value as Encoding.default_external
|
220
|
-
|
221
|
-
session[:io] =
|
186
|
+
management_port_io.set_encoding(Encoding::UTF_8)
|
187
|
+
session[:io] = management_port_io
|
222
188
|
processor = Ascp::Management.new
|
223
189
|
# read management port, until socket is closed (gets returns nil)
|
224
|
-
while (line =
|
190
|
+
while (line = management_port_io.gets)
|
225
191
|
event = processor.process_line(line.chomp)
|
226
192
|
next unless event
|
227
193
|
# event is ready
|
@@ -241,7 +207,7 @@ module Aspera
|
|
241
207
|
# regenerate token here, expired, or error on it
|
242
208
|
# Note: in multi-session, each session will have a different one.
|
243
209
|
Log.log.warn('Regenerating token for transfer')
|
244
|
-
|
210
|
+
env['ASPERA_SCP_TOKEN'] = session[:token_regenerator].refreshed_transfer_token
|
245
211
|
end
|
246
212
|
raise Transfer::Error.new(last_event['Description'], last_event['Code'].to_i)
|
247
213
|
when 'DONE'
|
@@ -257,16 +223,16 @@ module Aspera
|
|
257
223
|
raise Transfer::Error, 'transfer interrupted by user'
|
258
224
|
ensure
|
259
225
|
mgt_server_socket.close
|
260
|
-
# if
|
261
|
-
unless
|
226
|
+
# if command was successfully started, check its status
|
227
|
+
unless command_pid.nil?
|
262
228
|
# "wait" for process to avoid zombie
|
263
|
-
Process.wait(
|
229
|
+
Process.wait(command_pid)
|
264
230
|
status = $CHILD_STATUS
|
265
|
-
|
231
|
+
# command_pid = nil
|
266
232
|
session.delete(:io)
|
267
|
-
# status is nil if an exception occurred before starting
|
233
|
+
# status is nil if an exception occurred before starting command
|
268
234
|
if !status&.success?
|
269
|
-
message = status.nil? ?
|
235
|
+
message = status.nil? ? "#{name} not started" : "#{name} failed (#{status})"
|
270
236
|
# raise error only if there was not already an exception (ERROR_INFO)
|
271
237
|
raise Transfer::Error, message unless $ERROR_INFO
|
272
238
|
# else display this message also, as main exception is already here
|
@@ -274,11 +240,47 @@ module Aspera
|
|
274
240
|
end
|
275
241
|
end
|
276
242
|
end
|
243
|
+
nil
|
277
244
|
end
|
278
245
|
|
279
|
-
|
280
|
-
|
281
|
-
|
246
|
+
private
|
247
|
+
|
248
|
+
# notify progress to callback
|
249
|
+
# @param event management port event
|
250
|
+
def process_progress(event)
|
251
|
+
session_id = event['SessionId']
|
252
|
+
case event['Type']
|
253
|
+
when 'INIT'
|
254
|
+
@pre_calc_sent = false
|
255
|
+
@pre_calc_last_size = nil
|
256
|
+
notify_progress(session_id: session_id, type: :session_start)
|
257
|
+
when 'NOTIFICATION' # sent from remote
|
258
|
+
if event.key?('PreTransferBytes')
|
259
|
+
@pre_calc_sent = true
|
260
|
+
notify_progress(session_id: session_id, type: :session_size, info: event['PreTransferBytes'])
|
261
|
+
end
|
262
|
+
when 'STATS' # during transfer
|
263
|
+
@pre_calc_last_size = event['TransferBytes'].to_i + event['StartByte'].to_i
|
264
|
+
notify_progress(session_id: session_id, type: :transfer, info: @pre_calc_last_size)
|
265
|
+
when 'DONE', 'ERROR' # end of session
|
266
|
+
total_size = event['TransferBytes'].to_i + event['StartByte'].to_i
|
267
|
+
if !@pre_calc_sent && !total_size.zero?
|
268
|
+
notify_progress(session_id: session_id, type: :session_size, info: total_size)
|
269
|
+
end
|
270
|
+
if @pre_calc_last_size != total_size
|
271
|
+
notify_progress(session_id: session_id, type: :transfer, info: total_size)
|
272
|
+
end
|
273
|
+
notify_progress(session_id: session_id, type: :end)
|
274
|
+
# cspell:disable
|
275
|
+
when 'SESSION'
|
276
|
+
when 'ARGSTOP'
|
277
|
+
when 'FILEERROR'
|
278
|
+
when 'STOP'
|
279
|
+
# cspell:enable
|
280
|
+
# stop event when one file is completed
|
281
|
+
else
|
282
|
+
Log.log.debug{"unknown event type #{event['Type']}"}
|
283
|
+
end
|
282
284
|
end
|
283
285
|
|
284
286
|
# @return [Hash] session information
|
@@ -289,7 +291,7 @@ module Aspera
|
|
289
291
|
return matches.first
|
290
292
|
end
|
291
293
|
|
292
|
-
# send command
|
294
|
+
# send command to management port of command (used in `asession)
|
293
295
|
# @param job_id identified transfer process
|
294
296
|
# @param session_index index of session (for multi session)
|
295
297
|
# @param data command on mgt port, examples:
|
@@ -309,47 +311,49 @@ module Aspera
|
|
309
311
|
end
|
310
312
|
attr_reader :sessions
|
311
313
|
|
312
|
-
private
|
313
|
-
|
314
314
|
# options for initialize (same as values in option transfer_info)
|
315
|
-
# @param wss [Boolean] true: if both SSH and wss in ts: prefer wss
|
316
315
|
# @param ascp_args [Array] additional arguments to ascp
|
317
|
-
# @param
|
318
|
-
# @param spawn_delay_sec [Integer] optional delay to start between sessions
|
319
|
-
# @param multi_incr_udp [Boolean] true: increment udp port for each session
|
320
|
-
# @param trusted_certs [Array] list of files with trusted certificates (stores)
|
321
|
-
# @param resume [Hash] resume policy
|
316
|
+
# @param wss [Boolean] true: if both SSH and wss in ts: prefer wss
|
322
317
|
# @param quiet [Boolean] by default no native ascp progress bar
|
318
|
+
# @param trusted_certs [Array,NilClass] list of files with trusted certificates (stores)
|
319
|
+
# @param client_ssh_key [String] client ssh key option (from CLIENT_SSH_KEY_OPTIONS)
|
323
320
|
# @param check_ignore_cb [Proc] callback with host,port
|
321
|
+
# @param spawn_timeout_sec [Integer] timeout for ascp spawn
|
322
|
+
# @param spawn_delay_sec [Integer] optional delay to start between sessions
|
323
|
+
# @param multi_incr_udp [Boolean,NilClass] true: increment udp port for each session
|
324
|
+
# @param resume [Hash,NilClass] resume policy
|
324
325
|
# @param management_cb [Proc] callback for management events
|
325
326
|
# @param base_options [Hash] other options for base class
|
326
327
|
def initialize(
|
328
|
+
ascp_args: nil,
|
327
329
|
wss: true,
|
328
|
-
ascp_args: [],
|
329
|
-
spawn_timeout_sec: 2,
|
330
|
-
spawn_delay_sec: 2,
|
331
|
-
multi_incr_udp: true,
|
332
|
-
trusted_certs: [],
|
333
|
-
resume: {},
|
334
330
|
quiet: true,
|
331
|
+
trusted_certs: nil,
|
332
|
+
client_ssh_key: nil,
|
335
333
|
check_ignore_cb: nil,
|
334
|
+
spawn_timeout_sec: 2,
|
335
|
+
spawn_delay_sec: 2,
|
336
|
+
multi_incr_udp: nil,
|
337
|
+
resume: nil,
|
336
338
|
management_cb: nil,
|
337
339
|
**base_options
|
338
340
|
)
|
339
341
|
super(**base_options)
|
342
|
+
# special transfer parameters
|
340
343
|
@tr_opts = {
|
341
344
|
ascp_args: ascp_args,
|
342
345
|
wss: wss,
|
343
346
|
quiet: quiet,
|
344
347
|
trusted_certs: trusted_certs,
|
348
|
+
client_ssh_key: client_ssh_key,
|
345
349
|
check_ignore_cb: check_ignore_cb
|
346
350
|
}
|
347
351
|
@spawn_timeout_sec = spawn_timeout_sec
|
348
352
|
@spawn_delay_sec = spawn_delay_sec
|
349
|
-
|
350
|
-
@
|
353
|
+
# default is true on windows, false on other platforms
|
354
|
+
@multi_incr_udp = multi_incr_udp.nil? ? Environment.os.eql?(Environment::OS_WINDOWS) : multi_incr_udp
|
351
355
|
@management_cb = management_cb
|
352
|
-
@resume_policy = Resumer.new(
|
356
|
+
@resume_policy = Resumer.new(resume.nil? ? {} : resume.symbolize_keys)
|
353
357
|
# all transfer jobs, key = SecureRandom.uuid, protected by mutex, cond var on change
|
354
358
|
@sessions = []
|
355
359
|
# mutex protects global data accessed by threads
|
@@ -365,7 +369,7 @@ module Aspera
|
|
365
369
|
Log.log.debug{"ENTER (#{Thread.current[:name]})"}
|
366
370
|
# start transfer with selected resumer policy
|
367
371
|
@resume_policy.execute_with_resume do
|
368
|
-
|
372
|
+
start_and_monitor_process(session: session, **session[:env_args])
|
369
373
|
end
|
370
374
|
Log.log.debug('transfer ok'.bg_green)
|
371
375
|
rescue StandardError => e
|
data/lib/aspera/api/aoc.rb
CHANGED
@@ -439,7 +439,7 @@ module Aspera
|
|
439
439
|
# create a package
|
440
440
|
# @param package_data [Hash] package creation (with extensions...)
|
441
441
|
# @param validate_meta [TrueClass,FalseClass] true to validate parameters locally
|
442
|
-
# @param new_user_option [Hash] options if an unknown user is specified
|
442
|
+
# @param new_user_option [Hash,NilClass] options if an unknown user is specified
|
443
443
|
# @return transfer spec, node api and package information
|
444
444
|
def create_package_simple(package_data, validate_meta, new_user_option)
|
445
445
|
update_package_metadata_for_api(package_data)
|
@@ -476,7 +476,7 @@ module Aspera
|
|
476
476
|
# callback in Node (transfer_spec_gen4)
|
477
477
|
def add_ts_tags(transfer_spec:, app_info:)
|
478
478
|
# translate transfer direction to upload/download
|
479
|
-
transfer_type = Transfer::Spec.
|
479
|
+
transfer_type = Transfer::Spec.direction_to_transfer_type(transfer_spec['direction'])
|
480
480
|
# Analytics tags
|
481
481
|
################
|
482
482
|
transfer_spec.deep_merge!({
|
data/lib/aspera/api/httpgw.rb
CHANGED
@@ -45,7 +45,7 @@ module Aspera
|
|
45
45
|
else
|
46
46
|
@shared_info[:count][:sent_general] += 1
|
47
47
|
end
|
48
|
-
Log.log.
|
48
|
+
Log.log.trace1 do
|
49
49
|
log_data = payload.dup
|
50
50
|
log_data[:data] = "[data #{log_data[:data].length} bytes]" if log_data.key?(:data)
|
51
51
|
"#{LOG_WS_SEND}json: #{msg_type}: #{JSON.generate(log_data)}"
|
@@ -55,7 +55,7 @@ module Aspera
|
|
55
55
|
|
56
56
|
# send data on http gw web socket
|
57
57
|
def ws_send(ws_type:, data:)
|
58
|
-
Log.log.
|
58
|
+
Log.log.trace1{"#{LOG_WS_SEND}sending: #{ws_type} (#{data&.length || 0} bytes)"}
|
59
59
|
@shared_info[:count][:sent_general] += 1 if ws_type.eql?(:binary)
|
60
60
|
frame_generator = ::WebSocket::Frame::Outgoing::Client.new(data: data, type: ws_type, version: @ws_handshake.version)
|
61
61
|
@ws_io.write(frame_generator.to_s)
|
@@ -68,13 +68,13 @@ module Aspera
|
|
68
68
|
(((@shared_info[:count][:sent_general] - @shared_info[:count][:received_general]) > 1) ||
|
69
69
|
((@shared_info[:count][:received_v2_delimiter] - @shared_info[:count][:sent_v2_delimiter]) > 1))
|
70
70
|
if !@shared_info[:cond_var].wait(@shared_info[:mutex], 2.0)
|
71
|
-
Log.log.
|
71
|
+
Log.log.trace1{"#{LOG_WS_SEND}#{'timeout'.blue}: #{@shared_info[:count]}"}
|
72
72
|
end
|
73
73
|
end
|
74
74
|
end
|
75
75
|
end
|
76
76
|
raise @shared_info[:read_exception] unless @shared_info[:read_exception].nil?
|
77
|
-
Log.log.
|
77
|
+
Log.log.trace2{"#{LOG_WS_SEND}counts: #{@shared_info[:count]}"}
|
78
78
|
end
|
79
79
|
|
80
80
|
# message processing for read thread
|
@@ -114,12 +114,12 @@ module Aspera
|
|
114
114
|
# ready byte by byte until frame is ready
|
115
115
|
# blocking read
|
116
116
|
byte = @ws_io.read(1)
|
117
|
-
Log.log.
|
117
|
+
Log.log.trace2{"#{LOG_WS_RECV}read: #{byte} (#{byte.class}) eof=#{@ws_io.eof?}"}
|
118
118
|
frame_parser << byte
|
119
119
|
frame_ok = frame_parser.next
|
120
120
|
next if frame_ok.nil?
|
121
121
|
process_received_message(frame_ok.data.to_s)
|
122
|
-
Log.log.
|
122
|
+
Log.log.trace2{"#{LOG_WS_RECV}counts: #{@shared_info[:count]}"}
|
123
123
|
rescue => e
|
124
124
|
Log.log.debug{"#{LOG_WS_RECV}Exception: #{e}"}
|
125
125
|
@shared_info[:mutex].synchronize do
|
@@ -139,34 +139,12 @@ module Aspera
|
|
139
139
|
# identify this session uniquely
|
140
140
|
session_id = SecureRandom.uuid
|
141
141
|
@notify_cb&.call(session_id: nil, type: :pre_start, info: 'starting')
|
142
|
-
#
|
143
|
-
|
144
|
-
#
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
# source root is ignored by GW, used only here
|
149
|
-
transfer_spec.delete('source_root')
|
150
|
-
# compute total size of files to upload (for progress)
|
151
|
-
# modify transfer spec to be suitable for GW
|
152
|
-
transfer_spec['paths'].each do |item|
|
153
|
-
# save actual file location to be able read contents later
|
154
|
-
file_to_add = Transfer::FauxFile.open(item['source'])
|
155
|
-
if file_to_add
|
156
|
-
item['source'] = file_to_add.path
|
157
|
-
item['file_size'] = file_to_add.size
|
158
|
-
else
|
159
|
-
file_to_add = item['source']
|
160
|
-
# add source root if needed
|
161
|
-
file_to_add = File.join(source_root, file_to_add) unless source_root.nil?
|
162
|
-
# GW expects a simple file name in 'source' but if user wants to change the name, we take it
|
163
|
-
item['source'] = File.basename(item['destination'].nil? ? item['source'] : item['destination'])
|
164
|
-
item['file_size'] = File.size(file_to_add)
|
165
|
-
end
|
166
|
-
# save so that we can actually read the file later
|
167
|
-
files_to_read.push(file_to_add)
|
168
|
-
total_bytes_to_transfer += item['file_size']
|
169
|
-
end
|
142
|
+
# process files to send, modify `paths` in transfer_spec
|
143
|
+
files_to_send = process_upload_list(transfer_spec)
|
144
|
+
# total size of all files is last element
|
145
|
+
total_bytes_to_transfer = files_to_send.pop
|
146
|
+
Log.log.trace1{Log.dump(:modified_tspec, transfer_spec)}
|
147
|
+
Log.log.trace1{Log.dump(:files_to_send, files_to_send)}
|
170
148
|
# TODO: check that this is available in endpoints: @api_info['endpoints']
|
171
149
|
upload_url = File.join(@gw_root_url, @upload_version, 'upload')
|
172
150
|
@notify_cb&.call(session_id: nil, type: :pre_start, info: 'connecting wss')
|
@@ -180,11 +158,6 @@ module Aspera
|
|
180
158
|
@ws_handshake << @ws_io.readuntil("\r\n\r\n")
|
181
159
|
Aspera.assert(@ws_handshake.finished?){'Error in websocket handshake'}
|
182
160
|
Log.log.debug{"#{LOG_WS_SEND}handshake success"}
|
183
|
-
# start read thread after handshake
|
184
|
-
@ws_read_thread = Thread.new {process_read_thread}
|
185
|
-
@notify_cb&.call(session_id: session_id, type: :session_start)
|
186
|
-
@notify_cb&.call(session_id: session_id, type: :session_size, info: total_bytes_to_transfer)
|
187
|
-
sleep(1)
|
188
161
|
# data shared between main thread and read thread
|
189
162
|
@shared_info = {
|
190
163
|
read_exception: nil, # error message if any in callback
|
@@ -197,34 +170,34 @@ module Aspera
|
|
197
170
|
mutex: Mutex.new,
|
198
171
|
cond_var: ConditionVariable.new
|
199
172
|
}
|
173
|
+
# start read thread after handshake
|
174
|
+
@ws_read_thread = Thread.new {process_read_thread}
|
175
|
+
@notify_cb&.call(session_id: session_id, type: :session_start)
|
176
|
+
@notify_cb&.call(session_id: session_id, type: :session_size, info: total_bytes_to_transfer)
|
177
|
+
sleep(1)
|
200
178
|
# notify progress bar
|
201
179
|
@notify_cb&.call(type: :session_size, session_id: session_id, info: total_bytes_to_transfer)
|
202
180
|
# first step send transfer spec
|
203
|
-
Log.log.debug{Log.dump(:ws_spec, transfer_spec)}
|
204
181
|
ws_snd_json(MSG_SEND_TRANSFER_SPEC, transfer_spec)
|
205
182
|
# current file index
|
206
183
|
file_index = 0
|
207
184
|
# aggregate size sent
|
208
185
|
session_sent_bytes = 0
|
209
186
|
# process each file
|
210
|
-
|
187
|
+
files_to_send.each do |file_to_send|
|
188
|
+
last_slice = (file_to_send[:size] - 1) / @upload_chunk_size
|
211
189
|
slice_info = {
|
212
|
-
name:
|
190
|
+
name: file_to_send[:name],
|
213
191
|
# TODO: get mime type?
|
214
|
-
type:
|
215
|
-
size:
|
216
|
-
slice:
|
192
|
+
type: 'application/octet-stream',
|
193
|
+
size: file_to_send[:size],
|
194
|
+
slice: 0, # current slice index
|
217
195
|
# index of last slice (i.e number of slices - 1)
|
218
|
-
|
219
|
-
fileIndex:
|
196
|
+
total_slices: last_slice + 1,
|
197
|
+
fileIndex: file_index
|
220
198
|
}
|
221
|
-
file =
|
222
|
-
|
223
|
-
slice_info[:name] = file.path
|
224
|
-
else
|
225
|
-
file = File.open(file)
|
226
|
-
slice_info[:name] = File.basename(item[item['destination'].nil? ? 'source' : 'destination'])
|
227
|
-
end
|
199
|
+
file = file_to_send[:file]
|
200
|
+
file = File.open(file) unless file.is_a?(Transfer::FauxFile)
|
228
201
|
begin
|
229
202
|
until file.eof?
|
230
203
|
slice_bin_data = file.read(@upload_chunk_size)
|
@@ -238,9 +211,9 @@ module Aspera
|
|
238
211
|
# send once, before data, at beginning
|
239
212
|
ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info) if slice_info[:slice].eql?(0)
|
240
213
|
ws_send(ws_type: :binary, data: slice_bin_data)
|
241
|
-
Log.log.
|
214
|
+
Log.log.trace1{"#{LOG_WS_SEND}buffer: file: #{file_index}, slice: #{slice_info[:slice]}/#{last_slice}"}
|
242
215
|
# send once, after data, at end
|
243
|
-
ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info) if slice_info[:slice].eql?(
|
216
|
+
ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info) if slice_info[:slice].eql?(last_slice)
|
244
217
|
end
|
245
218
|
rescue Errno::EPIPE => e
|
246
219
|
raise @shared_info[:read_exception] unless @shared_info[:read_exception].nil?
|
@@ -337,6 +310,68 @@ module Aspera
|
|
337
310
|
end
|
338
311
|
end
|
339
312
|
end
|
313
|
+
|
314
|
+
private
|
315
|
+
|
316
|
+
# compute total size of files to upload (for progress)
|
317
|
+
# modify transfer spec to be suitable for HTTPGW
|
318
|
+
# @param transfer_spec [Hash] transfer specification
|
319
|
+
# @return [Array] info on files to send
|
320
|
+
def process_upload_list(transfer_spec)
|
321
|
+
total_bytes_to_transfer = 0
|
322
|
+
source_prefix = transfer_spec.key?('source_root') && !transfer_spec['source_root'].empty? ? transfer_spec['source_root'] + '/' : ''
|
323
|
+
files_to_send = []
|
324
|
+
transfer_spec['paths'].each do |one_path|
|
325
|
+
source_path = source_prefix + one_path['source']
|
326
|
+
faux_file = Transfer::FauxFile.create(source_path)
|
327
|
+
if faux_file
|
328
|
+
total_bytes_to_transfer += faux_file.size
|
329
|
+
files_to_send.push({
|
330
|
+
file: faux_file,
|
331
|
+
name: faux_file.path,
|
332
|
+
size: faux_file.size
|
333
|
+
})
|
334
|
+
elsif File.file?(source_path)
|
335
|
+
# regular file
|
336
|
+
file_size = File.size(source_path)
|
337
|
+
total_bytes_to_transfer += file_size
|
338
|
+
files_to_send.push({
|
339
|
+
file: source_path,
|
340
|
+
# GW expects a simple file name in 'source' but if user wants to change the name, we take it
|
341
|
+
name: File.basename(one_path['destination'].nil? ? source_path : one_path['destination']),
|
342
|
+
size: file_size
|
343
|
+
})
|
344
|
+
elsif File.directory?(source_path)
|
345
|
+
folders_to_process = [source_path]
|
346
|
+
until folders_to_process.empty?
|
347
|
+
folder = folders_to_process.shift
|
348
|
+
# read all entries
|
349
|
+
Dir.entries(folder).each do |entry|
|
350
|
+
next if entry.eql?('.') || entry.eql?('..')
|
351
|
+
entry_path = File.join(folder, entry)
|
352
|
+
if File.directory?(entry_path)
|
353
|
+
folders_to_process.push(entry_path)
|
354
|
+
elsif File.file?(entry_path)
|
355
|
+
file_size = File.size(entry_path)
|
356
|
+
total_bytes_to_transfer += file_size
|
357
|
+
files_to_send.push({
|
358
|
+
file: entry_path,
|
359
|
+
name: entry_path,
|
360
|
+
size: file_size
|
361
|
+
})
|
362
|
+
else
|
363
|
+
Log.log.warn{"Ignoring non file/directory: #{entry_path}"}
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
else
|
368
|
+
raise "File not found: #{source_path}"
|
369
|
+
end
|
370
|
+
end
|
371
|
+
transfer_spec['paths'] = files_to_send.map{|i|{'source' => i[:name]}}
|
372
|
+
files_to_send.push(total_bytes_to_transfer)
|
373
|
+
return files_to_send
|
374
|
+
end
|
340
375
|
end
|
341
376
|
end
|
342
377
|
end
|