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.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +19 -0
  4. data/CHANGELOG.md +528 -0
  5. data/CONTRIBUTING.md +143 -0
  6. data/README.md +977 -589
  7. data/bin/ascli +4 -4
  8. data/bin/asession +12 -12
  9. data/docs/test_env.conf +29 -19
  10. data/examples/aoc.rb +6 -6
  11. data/examples/dascli +18 -16
  12. data/examples/faspex4.rb +15 -15
  13. data/examples/node.rb +12 -12
  14. data/examples/proxy.pac +2 -2
  15. data/examples/server.rb +12 -12
  16. data/lib/aspera/aoc.rb +344 -272
  17. data/lib/aspera/ascmd.rb +56 -54
  18. data/lib/aspera/ats_api.rb +4 -4
  19. data/lib/aspera/cli/basic_auth_plugin.rb +15 -12
  20. data/lib/aspera/cli/extended_value.rb +9 -9
  21. data/lib/aspera/cli/{formater.rb → formatter.rb} +69 -69
  22. data/lib/aspera/cli/listener/line_dump.rb +1 -1
  23. data/lib/aspera/cli/listener/logger.rb +1 -1
  24. data/lib/aspera/cli/listener/progress.rb +5 -6
  25. data/lib/aspera/cli/listener/progress_multi.rb +16 -21
  26. data/lib/aspera/cli/main.rb +72 -73
  27. data/lib/aspera/cli/manager.rb +112 -112
  28. data/lib/aspera/cli/plugin.rb +68 -48
  29. data/lib/aspera/cli/plugins/alee.rb +4 -4
  30. data/lib/aspera/cli/plugins/aoc.rb +322 -720
  31. data/lib/aspera/cli/plugins/ats.rb +50 -52
  32. data/lib/aspera/cli/plugins/bss.rb +10 -10
  33. data/lib/aspera/cli/plugins/config.rb +514 -410
  34. data/lib/aspera/cli/plugins/console.rb +12 -12
  35. data/lib/aspera/cli/plugins/cos.rb +18 -20
  36. data/lib/aspera/cli/plugins/faspex.rb +134 -136
  37. data/lib/aspera/cli/plugins/faspex5.rb +235 -70
  38. data/lib/aspera/cli/plugins/node.rb +378 -309
  39. data/lib/aspera/cli/plugins/orchestrator.rb +52 -49
  40. data/lib/aspera/cli/plugins/preview.rb +129 -120
  41. data/lib/aspera/cli/plugins/server.rb +137 -83
  42. data/lib/aspera/cli/plugins/shares.rb +77 -52
  43. data/lib/aspera/cli/plugins/sync.rb +13 -33
  44. data/lib/aspera/cli/transfer_agent.rb +61 -61
  45. data/lib/aspera/cli/version.rb +2 -1
  46. data/lib/aspera/colors.rb +3 -3
  47. data/lib/aspera/command_line_builder.rb +78 -74
  48. data/lib/aspera/cos_node.rb +31 -29
  49. data/lib/aspera/data_repository.rb +1 -1
  50. data/lib/aspera/environment.rb +30 -28
  51. data/lib/aspera/fasp/agent_base.rb +17 -15
  52. data/lib/aspera/fasp/agent_connect.rb +34 -32
  53. data/lib/aspera/fasp/agent_direct.rb +70 -73
  54. data/lib/aspera/fasp/agent_httpgw.rb +79 -74
  55. data/lib/aspera/fasp/agent_node.rb +26 -26
  56. data/lib/aspera/fasp/agent_trsdk.rb +20 -20
  57. data/lib/aspera/fasp/error.rb +3 -2
  58. data/lib/aspera/fasp/error_info.rb +11 -8
  59. data/lib/aspera/fasp/installation.rb +80 -80
  60. data/lib/aspera/fasp/listener.rb +2 -2
  61. data/lib/aspera/fasp/parameters.rb +103 -92
  62. data/lib/aspera/fasp/parameters.yaml +313 -214
  63. data/lib/aspera/fasp/resume_policy.rb +10 -10
  64. data/lib/aspera/fasp/transfer_spec.rb +22 -2
  65. data/lib/aspera/fasp/uri.rb +7 -7
  66. data/lib/aspera/faspex_gw.rb +80 -159
  67. data/lib/aspera/faspex_postproc.rb +77 -0
  68. data/lib/aspera/hash_ext.rb +3 -3
  69. data/lib/aspera/id_generator.rb +5 -5
  70. data/lib/aspera/keychain/encrypted_hash.rb +23 -28
  71. data/lib/aspera/keychain/macos_security.rb +21 -20
  72. data/lib/aspera/log.rb +13 -13
  73. data/lib/aspera/nagios.rb +24 -23
  74. data/lib/aspera/node.rb +217 -38
  75. data/lib/aspera/oauth.rb +78 -74
  76. data/lib/aspera/open_application.rb +19 -11
  77. data/lib/aspera/persistency_action_once.rb +4 -4
  78. data/lib/aspera/persistency_folder.rb +13 -13
  79. data/lib/aspera/preview/file_types.rb +8 -8
  80. data/lib/aspera/preview/generator.rb +67 -67
  81. data/lib/aspera/preview/utils.rb +27 -27
  82. data/lib/aspera/proxy_auto_config.js +63 -63
  83. data/lib/aspera/proxy_auto_config.rb +19 -19
  84. data/lib/aspera/rest.rb +65 -67
  85. data/lib/aspera/rest_call_error.rb +2 -1
  86. data/lib/aspera/rest_error_analyzer.rb +22 -21
  87. data/lib/aspera/rest_errors_aspera.rb +16 -16
  88. data/lib/aspera/secret_hider.rb +17 -14
  89. data/lib/aspera/ssh.rb +15 -14
  90. data/lib/aspera/sync.rb +177 -62
  91. data/lib/aspera/temp_file_manager.rb +2 -2
  92. data/lib/aspera/uri_reader.rb +4 -4
  93. data/lib/aspera/web_auth.rb +13 -64
  94. data/lib/aspera/web_server_simple.rb +76 -0
  95. data.tar.gz.sig +0 -0
  96. metadata +11 -6
  97. 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: false,
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
- # @param options [Hash] :resumer, :regenerate_token
35
- def start_transfer(transfer_spec,options={})
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("xfer id=#{transfer_spec['xfer_id']}")
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.has_key?('token') &&
57
- !transfer_spec.has_key?('remote_password') &&
58
- !transfer_spec.has_key?('EX_ssh_key_paths')
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.has_key?('multi_session')
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("multi_session(#{transfer_spec['multi_session']}) shall be integer >= 0")
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 multisession')
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.has_key?('fasp_port') ? transfer_spec['fasp_port'] : TransferSpec::UDP_PORT
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: job_options[:job_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: nil, # Thread object monitoring management port, not nil when pushed to :sessions
107
- error: nil, # exception if failed
108
- io: nil, # management port server socket
109
- id: nil, # SessionId from INIT message in mgt port
110
- env_args: env_args, # env vars and args to ascp (from transfer spec)
111
- options: job_options # [Hash]
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[job_options[:job_id]] = xfer_job
139
- Log.log.debug("jobs: #{@jobs.keys.count}")
135
+ @jobs[the_job_id] = xfer_job
136
+ Log.log.debug{"jobs: #{@jobs.keys.count}"}
140
137
 
141
- return job_options[:job_id]
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("join #{session[:thread]}")
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("env_args=#{env_args.inspect}")
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
- env_args[:env].map{|k,v| "#{k}=#{Shellwords.shellescape(v)}"}.join(' ')+
199
- ' '+
200
- Shellwords.shellescape(ascp_path)+
201
- ' '+
202
- ascp_arguments.map{|a|Shellwords.shellescape(a)}.join(' ')
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],*ascp_arguments)
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("before accept for pid (#{ascp_pid})")
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("after accept (#{ascp_mgt_io})")
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("line=[#{line}]")
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("session id: #{session[:id]}")
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("code: #{last_status_event['Code']}")
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[:options].is_a?(Hash) && session[:options].has_key?(:regenerate_token)
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[:options][:regenerate_token].call(true)
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("host: #{}")
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("command: #{data}")
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.has_key?(k)
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("local options= #{options}")
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("ENTER (#{Thread.current[:name]})")
360
+ Log.log.debug{"ENTER (#{Thread.current[:name]})"}
364
361
  # start transfer with selected resumer policy
365
- session[:options][:resumer].execute_with_resume do
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("Transfer thread error: #{e.class}:\n#{e.message}:\n#{e.backtrace.join("\n")}".red) if Log.instance.level.eql?(:debug)
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("EXIT (#{Thread.current[:name]})")
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
- upload_chunksize: 64_000,
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,:MSG_END_UPLOAD,:MSG_END_SLICE,:V1_UPLOAD,:V2_UPLOAD
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.has_key?(:slice_upload)
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.has_key?('source_root') && !transfer_spec['source_root'].empty? ? transfer_spec['source_root'] : nil
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
- #@ws_io.debug_output = Log.log
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("ws: thread: message: #{msg.data} #{shared_info[:end_uploads]}")
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("ws: thread: stopping #{shared_info[:read_exception]} #{shared_info[:read_exception].class}")
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
- begin
139
- transfer_spec['paths'].each do |item|
140
- # TODO: get mime type?
141
- file_mime_type = ''
142
- file_size = item['file_size']
143
- file_name = File.basename(item[item['destination'].nil? ? 'source' : 'destination'])
144
- # compute total number of slices
145
- numslices = 1 + ((file_size - 1) / @options[:upload_chunksize])
146
- File.open(source_paths[file_index]) do |file|
147
- # current slice index
148
- slicenum = 0
149
- while !file.eof?
150
- # interrupt main thread if read thread failed
151
- raise shared_info[:read_exception] unless shared_info[:read_exception].nil?
152
- data = file.read(@options[:upload_chunksize])
153
- slice_data = {
154
- name: file_name,
155
- type: file_mime_type,
156
- size: file_size,
157
- slice: slicenum,
158
- total_slices: numslices,
159
- fileIndex: file_index
160
- }
161
- #Log.dump(:slice_data,slice_data) #if slicenum.eql?(0)
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 slicenum.eql?(0)
167
- ws_send(data,type: :binary)
168
- Log.log.debug{"ws: sent buffer: #{file_index} / #{slicenum}"}
169
- ws_snd_json(slice_upload: slice_data) if slicenum.eql?(numslices-1)
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
- slicenum += 1
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.has_key?('download_name')
203
+ if !transfer_spec.key?('download_name')
198
204
  # by default it is the name of first file
199
- dname = File.basename(transfer_spec['paths'].first['source'])
205
+ download_name = File.basename(transfer_spec['paths'].first['source'])
200
206
  # we remove extension
201
- dname = dname.gsub(/\.@gw_api.*$/,'')
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
- dname += " #{transfer_spec['paths'].length} Files"
210
+ download_name += " #{transfer_spec['paths'].length} Files"
205
211
  end
206
- transfer_spec['download_name'] = dname
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,options={})
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("local options= #{opts}")
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.has_key?(k)
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]