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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +13 -0
  4. data/CONTRIBUTING.md +5 -12
  5. data/README.md +60 -29
  6. data/examples/build_exec +85 -0
  7. data/lib/aspera/agent/base.rb +2 -0
  8. data/lib/aspera/agent/direct.rb +108 -104
  9. data/lib/aspera/api/aoc.rb +2 -2
  10. data/lib/aspera/api/httpgw.rb +91 -56
  11. data/lib/aspera/ascp/installation.rb +47 -32
  12. data/lib/aspera/ascp/management.rb +4 -1
  13. data/lib/aspera/ascp/products.rb +1 -7
  14. data/lib/aspera/cli/formatter.rb +24 -18
  15. data/lib/aspera/cli/manager.rb +10 -10
  16. data/lib/aspera/cli/plugin.rb +2 -2
  17. data/lib/aspera/cli/plugin_factory.rb +10 -1
  18. data/lib/aspera/cli/plugins/config.rb +15 -10
  19. data/lib/aspera/cli/plugins/node.rb +4 -3
  20. data/lib/aspera/cli/plugins/server.rb +1 -1
  21. data/lib/aspera/cli/plugins/shares.rb +11 -7
  22. data/lib/aspera/cli/sync_actions.rb +72 -31
  23. data/lib/aspera/cli/transfer_agent.rb +1 -0
  24. data/lib/aspera/cli/transfer_progress.rb +1 -1
  25. data/lib/aspera/cli/version.rb +1 -1
  26. data/lib/aspera/environment.rb +43 -10
  27. data/lib/aspera/faspex_gw.rb +1 -1
  28. data/lib/aspera/keychain/encrypted_hash.rb +2 -0
  29. data/lib/aspera/log.rb +1 -0
  30. data/lib/aspera/node_simulator.rb +1 -1
  31. data/lib/aspera/oauth/jwt.rb +1 -1
  32. data/lib/aspera/oauth/url_json.rb +2 -0
  33. data/lib/aspera/oauth/web.rb +5 -4
  34. data/lib/aspera/secret_hider.rb +3 -2
  35. data/lib/aspera/ssh.rb +1 -1
  36. data/lib/aspera/transfer/faux_file.rb +7 -5
  37. data/lib/aspera/transfer/parameters.rb +27 -19
  38. data/lib/aspera/transfer/spec.rb +8 -10
  39. data/lib/aspera/transfer/sync.rb +52 -47
  40. data/lib/aspera/web_auth.rb +0 -1
  41. data/lib/aspera/web_server_simple.rb +24 -13
  42. data.tar.gz.sig +0 -0
  43. metadata +3 -3
  44. metadata.gz.sig +0 -0
  45. data/examples/rubyc +0 -24
@@ -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
- ANY_AVAILABLE_PORT = 0 # 0 means any available port
22
+ # 0 means: select an available port
23
+ SELECT_AVAILABLE_PORT = 0
23
24
  # spellchecker: enable
24
- private_constant :LISTEN_LOCAL_ADDRESS, :ANY_AVAILABLE_PORT
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
- this_session[:env_args][:args] = this_session[:env_args][:args].clone
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
- this_session[:env_args][:args].unshift("-C#{i}:#{multi_session_info[:count]}")
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
- this_session[:env_args][:args].unshift('-O', (multi_session_info[:udp_base] + i - 1).to_s) if @multi_incr_udp
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
- # @param event management port event
133
- def process_progress(event)
134
- session_id = event['SessionId']
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 "ascp" process
170
- # currently, relies on command line arguments
171
- # start ascp with management port.
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
- # if there is a thread info: set and broadcast session id
174
- # runs in separate thread
175
- # @param env_args a hash containing :args :env :ascp_version
176
- # @param session this session information
177
- # could be private method
178
- def start_transfer_with_args_env(env_args, session)
179
- Aspera.assert_type(env_args, Hash)
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
- ascp_pid = nil
156
+ command_pid = nil
185
157
  # we use Socket directly, instead of TCPServer, as it gives access to lower level options
186
- socket_class = RUBY_ENGINE.eql?('jruby') ? ServerSocket : Socket
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 ascp management port
189
- mgt_server_socket.bind(Addrinfo.tcp(LISTEN_LOCAL_ADDRESS, ANY_AVAILABLE_PORT))
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
- ascp_arguments = ['-M', mgt_server_socket.local_address.ip_port.to_s].concat(env_args[:args])
192
- # get location of ascp executable
193
- ascp_path = Ascp::Installation.instance.path(env_args[:ascp_version])
194
- # display ascp command line
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
- # start ascp in separate process
204
- ascp_pid = Process.spawn(env_args[:env], [ascp_path, ascp_path], *ascp_arguments, close_others: true)
205
- Log.log.debug{"spawned ascp pid #{ascp_pid}"}
206
- notify_progress(session_id: nil, type: :pre_start, info: 'waiting for ascp')
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
- ascp_mgt_io = client_socket.to_io
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
- ascp_mgt_io.set_encoding(Encoding::UTF_8)
221
- session[:io] = ascp_mgt_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 = ascp_mgt_io.gets)
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
- env_args[:env]['ASPERA_SCP_TOKEN'] = session[:token_regenerator].refreshed_transfer_token
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 ascp was successfully started, check its status
261
- unless ascp_pid.nil?
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(ascp_pid)
229
+ Process.wait(command_pid)
264
230
  status = $CHILD_STATUS
265
- ascp_pid = nil
231
+ # command_pid = nil
266
232
  session.delete(:io)
267
- # status is nil if an exception occurred before starting ascp
233
+ # status is nil if an exception occurred before starting command
268
234
  if !status&.success?
269
- message = status.nil? ? 'ascp not started' : "ascp failed (#{status})"
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
- # @return [Array] list of sessions for a job
280
- def sessions_by_job(job_id)
281
- @sessions.select{|session_info| session_info[:job_id].eql?(job_id)}
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 of management port to ascp session (used in `asession)
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 spawn_timeout_sec [Integer] timeout for ascp spawn
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
- @multi_incr_udp = multi_incr_udp
350
- @resume = resume
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(@resume.symbolize_keys)
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
- start_transfer_with_args_env(session[:env_args], session)
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
@@ -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.action(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!({
@@ -45,7 +45,7 @@ module Aspera
45
45
  else
46
46
  @shared_info[:count][:sent_general] += 1
47
47
  end
48
- Log.log.debug do
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.debug{"#{LOG_WS_SEND}sending: #{ws_type} (#{data&.length || 0} bytes)"}
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.debug{"#{LOG_WS_SEND}#{'timeout'.blue}: #{@shared_info[:count]}"}
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.debug{"#{LOG_WS_SEND}counts: #{@shared_info[:count]}"}
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.trace1{"#{LOG_WS_RECV}read: #{byte} (#{byte.class}) eof=#{@ws_io.eof?}"}
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.debug{"#{LOG_WS_RECV}counts: #{@shared_info[:count]}"}
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
- # total size of all files
143
- total_bytes_to_transfer = 0
144
- # we need to keep track of actual file path because transfer spec is modified to be sent in web socket
145
- files_to_read = []
146
- # get source root or nil
147
- source_root = transfer_spec.key?('source_root') && !transfer_spec['source_root'].empty? ? transfer_spec['source_root'] : nil
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
- transfer_spec['paths'].each do |item|
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: nil,
190
+ name: file_to_send[:name],
213
191
  # TODO: get mime type?
214
- type: 'application/octet-stream',
215
- size: item['file_size'],
216
- slice: 0, # current slice index
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
- last_slice: (item['file_size'] - 1) / @upload_chunk_size,
219
- fileIndex: file_index
196
+ total_slices: last_slice + 1,
197
+ fileIndex: file_index
220
198
  }
221
- file = files_to_read[file_index]
222
- if file.is_a?(Transfer::FauxFile)
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.debug{"#{LOG_WS_SEND}buffer: file: #{file_index}, slice: #{slice_info[:slice]}/#{slice_info[:last_slice]}"}
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?(slice_info[:last_slice])
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