aspera-cli 4.15.0 → 4.17.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 (108) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +29 -3
  4. data/CHANGELOG.md +375 -280
  5. data/CONTRIBUTING.md +71 -18
  6. data/README.md +1978 -1656
  7. data/bin/ascli +13 -31
  8. data/bin/asession +32 -22
  9. data/examples/dascli +2 -2
  10. data/lib/aspera/agent/alpha.rb +117 -0
  11. data/lib/aspera/agent/base.rb +61 -0
  12. data/lib/aspera/{fasp/agent_connect.rb → agent/connect.rb} +13 -11
  13. data/lib/aspera/{fasp/agent_direct.rb → agent/direct.rb} +116 -116
  14. data/lib/aspera/{fasp/agent_httpgw.rb → agent/httpgw.rb} +21 -19
  15. data/lib/aspera/{fasp/agent_node.rb → agent/node.rb} +21 -33
  16. data/lib/aspera/agent/trsdk.rb +188 -0
  17. data/lib/aspera/api/aoc.rb +586 -0
  18. data/lib/aspera/api/ats.rb +46 -0
  19. data/lib/aspera/api/cos_node.rb +95 -0
  20. data/lib/aspera/api/node.rb +344 -0
  21. data/lib/aspera/ascmd.rb +47 -14
  22. data/lib/aspera/{fasp → ascp}/installation.rb +54 -15
  23. data/lib/aspera/{fasp → ascp}/management.rb +14 -14
  24. data/lib/aspera/{fasp → ascp}/products.rb +1 -1
  25. data/lib/aspera/assert.rb +45 -0
  26. data/lib/aspera/cli/basic_auth_plugin.rb +11 -10
  27. data/lib/aspera/cli/extended_value.rb +5 -5
  28. data/lib/aspera/cli/formatter.rb +27 -14
  29. data/lib/aspera/cli/hints.rb +7 -6
  30. data/lib/aspera/cli/main.rb +49 -29
  31. data/lib/aspera/cli/manager.rb +46 -36
  32. data/lib/aspera/cli/plugin.rb +34 -20
  33. data/lib/aspera/cli/plugin_factory.rb +61 -0
  34. data/lib/aspera/cli/plugins/alee.rb +7 -7
  35. data/lib/aspera/cli/plugins/aoc.rb +168 -132
  36. data/lib/aspera/cli/plugins/ats.rb +33 -33
  37. data/lib/aspera/cli/plugins/bss.rb +3 -4
  38. data/lib/aspera/cli/plugins/config.rb +250 -272
  39. data/lib/aspera/cli/plugins/console.rb +8 -6
  40. data/lib/aspera/cli/plugins/cos.rb +20 -19
  41. data/lib/aspera/cli/plugins/faspex.rb +71 -60
  42. data/lib/aspera/cli/plugins/faspex5.rb +212 -133
  43. data/lib/aspera/cli/plugins/node.rb +83 -75
  44. data/lib/aspera/cli/plugins/orchestrator.rb +36 -44
  45. data/lib/aspera/cli/plugins/preview.rb +33 -31
  46. data/lib/aspera/cli/plugins/server.rb +33 -32
  47. data/lib/aspera/cli/plugins/shares.rb +39 -33
  48. data/lib/aspera/cli/sync_actions.rb +9 -9
  49. data/lib/aspera/cli/transfer_agent.rb +45 -25
  50. data/lib/aspera/cli/transfer_progress.rb +2 -3
  51. data/lib/aspera/cli/version.rb +1 -1
  52. data/lib/aspera/colors.rb +5 -0
  53. data/lib/aspera/command_line_builder.rb +16 -14
  54. data/lib/aspera/coverage.rb +21 -0
  55. data/lib/aspera/data_repository.rb +33 -2
  56. data/lib/aspera/environment.rb +5 -4
  57. data/lib/aspera/faspex_gw.rb +13 -11
  58. data/lib/aspera/faspex_postproc.rb +6 -5
  59. data/lib/aspera/id_generator.rb +4 -2
  60. data/lib/aspera/json_rpc.rb +10 -8
  61. data/lib/aspera/keychain/encrypted_hash.rb +46 -11
  62. data/lib/aspera/keychain/macos_security.rb +29 -22
  63. data/lib/aspera/log.rb +5 -4
  64. data/lib/aspera/nagios.rb +7 -2
  65. data/lib/aspera/node_simulator.rb +213 -0
  66. data/lib/aspera/oauth/base.rb +143 -0
  67. data/lib/aspera/oauth/factory.rb +124 -0
  68. data/lib/aspera/oauth/generic.rb +34 -0
  69. data/lib/aspera/oauth/jwt.rb +51 -0
  70. data/lib/aspera/oauth/url_json.rb +31 -0
  71. data/lib/aspera/oauth/web.rb +50 -0
  72. data/lib/aspera/oauth.rb +5 -328
  73. data/lib/aspera/open_application.rb +7 -7
  74. data/lib/aspera/persistency_action_once.rb +13 -14
  75. data/lib/aspera/persistency_folder.rb +3 -2
  76. data/lib/aspera/preview/file_types.rb +53 -267
  77. data/lib/aspera/preview/generator.rb +7 -5
  78. data/lib/aspera/preview/terminal.rb +17 -7
  79. data/lib/aspera/preview/utils.rb +8 -7
  80. data/lib/aspera/proxy_auto_config.rb +6 -3
  81. data/lib/aspera/rest.rb +187 -140
  82. data/lib/aspera/rest_error_analyzer.rb +1 -0
  83. data/lib/aspera/rest_errors_aspera.rb +5 -3
  84. data/lib/aspera/resumer.rb +77 -0
  85. data/lib/aspera/secret_hider.rb +5 -2
  86. data/lib/aspera/ssh.rb +15 -8
  87. data/lib/aspera/temp_file_manager.rb +1 -1
  88. data/lib/aspera/{fasp → transfer}/error.rb +3 -3
  89. data/lib/aspera/{fasp → transfer}/error_info.rb +1 -1
  90. data/lib/aspera/{fasp → transfer}/faux_file.rb +1 -1
  91. data/lib/aspera/{fasp → transfer}/parameters.rb +95 -120
  92. data/lib/aspera/{fasp/transfer_spec.rb → transfer/spec.rb} +23 -19
  93. data/lib/aspera/{fasp/parameters.yaml → transfer/spec.yaml} +4 -99
  94. data/lib/aspera/transfer/sync.rb +273 -0
  95. data/lib/aspera/{fasp → transfer}/uri.rb +10 -9
  96. data/lib/aspera/web_server_simple.rb +12 -3
  97. data.tar.gz.sig +0 -0
  98. metadata +92 -68
  99. metadata.gz.sig +0 -0
  100. data/lib/aspera/aoc.rb +0 -606
  101. data/lib/aspera/ats_api.rb +0 -47
  102. data/lib/aspera/cos_node.rb +0 -93
  103. data/lib/aspera/fasp/agent_aspera.rb +0 -126
  104. data/lib/aspera/fasp/agent_base.rb +0 -48
  105. data/lib/aspera/fasp/agent_trsdk.rb +0 -146
  106. data/lib/aspera/fasp/resume_policy.rb +0 -77
  107. data/lib/aspera/node.rb +0 -338
  108. data/lib/aspera/sync.rb +0 -219
@@ -1,58 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'aspera/fasp/agent_base'
4
- require 'aspera/fasp/error'
5
- require 'aspera/fasp/parameters'
6
- require 'aspera/fasp/installation'
7
- require 'aspera/fasp/resume_policy'
8
- require 'aspera/fasp/transfer_spec'
9
- require 'aspera/fasp/management'
3
+ require 'aspera/agent/base'
4
+ require 'aspera/ascp/installation'
5
+ require 'aspera/ascp/management'
6
+ require 'aspera/transfer/parameters'
7
+ require 'aspera/transfer/error'
8
+ require 'aspera/transfer/spec'
9
+ require 'aspera/resumer'
10
10
  require 'aspera/log'
11
+ require 'aspera/assert'
11
12
  require 'socket'
12
- require 'timeout'
13
13
  require 'securerandom'
14
14
  require 'shellwords'
15
15
  require 'English'
16
16
 
17
17
  module Aspera
18
- module Fasp
18
+ module Agent
19
19
  # executes a local "ascp", connects mgt port, equivalent of "Fasp Manager"
20
- class AgentDirect < Aspera::Fasp::AgentBase
20
+ class Direct < Base
21
21
  # options for initialize (same as values in option transfer_info)
22
22
  DEFAULT_OPTIONS = {
23
- spawn_timeout_sec: 3,
24
- spawn_delay_sec: 2,
25
23
  wss: true, # true: if both SSH and wss in ts: prefer wss
24
+ ascp_args: [],
25
+ spawn_timeout_sec: 2,
26
+ spawn_delay_sec: 2, # optional delay to start between sessions
26
27
  multi_incr_udp: true,
28
+ trusted_certs: [], # list of files with trusted certificates (stores)
27
29
  resume: {},
28
- ascp_args: [],
29
- check_ignore: nil, # callback with host,port
30
30
  quiet: true, # by default no native ascp progress bar
31
- trusted_certs: [] # list of files with trusted certificates (stores)
31
+ check_ignore_cb: nil, # callback with host,port
32
+ management_cb: nil # callback for management events
32
33
  }.freeze
34
+ LISTEN_LOCAL_ADDRESS = '127.0.0.1'
35
+ ANY_AVAILABLE_PORT = 0 # 0 means any available port
33
36
  # spellchecker: enable
34
- private_constant :DEFAULT_OPTIONS
37
+ private_constant :DEFAULT_OPTIONS, :LISTEN_LOCAL_ADDRESS, :ANY_AVAILABLE_PORT
35
38
 
36
- # start ascp transfer (non blocking), single or multi-session
37
- # job information added to @jobs
39
+ # method of Base
40
+ # start ascp transfer(s) (non blocking), single or multi-session
41
+ # session information added to @sessions
38
42
  # @param transfer_spec [Hash] aspera transfer specification
43
+ # @param token_regenerator [Object] object with method refreshed_transfer_token
39
44
  def start_transfer(transfer_spec, token_regenerator: nil)
40
- the_job_id = SecureRandom.uuid
41
45
  # clone transfer spec because we modify it (first level keys)
42
46
  transfer_spec = transfer_spec.clone
43
- # if there is aspera tags
44
- if transfer_spec['tags'].is_a?(Hash) && transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED].is_a?(Hash)
47
+ # if there are aspera tags
48
+ if transfer_spec.dig('tags', Transfer::Spec::TAG_RESERVED).is_a?(Hash)
45
49
  # TODO: what is this for ? only on local ascp ?
46
50
  # NOTE: important: transfer id must be unique: generate random id
47
51
  # using a non unique id results in discard of tags in AoC, and a package is never finalized
48
52
  # all sessions in a multi-session transfer must have the same xfer_id (see admin manual)
49
- transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['xfer_id'] ||= SecureRandom.uuid
53
+ transfer_spec['tags'][Transfer::Spec::TAG_RESERVED]['xfer_id'] ||= SecureRandom.uuid
50
54
  Log.log.debug{"xfer id=#{transfer_spec['xfer_id']}"}
51
- # TODO: useful ? node only ?
52
- transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['xfer_retry'] ||= 3600
55
+ # TODO: useful ? node only ? seems to be a timeout for retry in node
56
+ transfer_spec['tags'][Transfer::Spec::TAG_RESERVED]['xfer_retry'] ||= 3600
53
57
  end
54
58
  Log.log.debug{Log.dump('ts', transfer_spec)}
55
-
56
59
  # Compute this before using transfer spec because it potentially modifies the transfer spec
57
60
  # (even if the var is not used in single session)
58
61
  multi_session_info = nil
@@ -70,37 +73,31 @@ module Aspera
70
73
  multi_session_info = nil
71
74
  elsif @options[:multi_incr_udp] # multi_session_info[:count] > 0
72
75
  # if option not true: keep default udp port for all sessions
73
- multi_session_info[:udp_base] = transfer_spec.key?('fasp_port') ? transfer_spec['fasp_port'] : TransferSpec::UDP_PORT
76
+ multi_session_info[:udp_base] = transfer_spec.key?('fasp_port') ? transfer_spec['fasp_port'] : Transfer::Spec::UDP_PORT
74
77
  # delete from original transfer spec, as we will increment values
75
78
  transfer_spec.delete('fasp_port')
76
79
  # override if specified, else use default value
77
80
  end
78
81
  end
79
82
 
80
- # compute known arguments and environment variables
81
- env_args = Parameters.new(transfer_spec, @options).ascp_args
82
-
83
- # transfer job can be multi session
84
- xfer_job = {
85
- id: the_job_id,
86
- sessions: [] # all sessions as below
87
- }
88
-
89
83
  # generic session information
90
84
  session = {
85
+ id: nil, # SessionId from INIT message in mgt port
86
+ job_id: SecureRandom.uuid, # job id (regroup sessions)
87
+ ts: transfer_spec, # transfer spec
91
88
  thread: nil, # Thread object monitoring management port, not nil when pushed to :sessions
92
89
  error: nil, # exception if failed
93
90
  io: nil, # management port server socket
94
- id: nil, # SessionId from INIT message in mgt port
95
91
  token_regenerator: token_regenerator, # regenerate bearer token with oauth
96
- env_args: env_args # env vars and args to ascp (from transfer spec)
92
+ # env vars and args to ascp (from transfer spec)
93
+ env_args: Transfer::Parameters.new(transfer_spec, @options).ascp_args
97
94
  }
98
95
 
99
96
  if multi_session_info.nil?
100
97
  Log.log.debug('Starting single session thread')
101
98
  # single session for transfer : simple
102
99
  session[:thread] = Thread.new(session) {|s|transfer_thread_entry(s)}
103
- xfer_job[:sessions].push(session)
100
+ @sessions.push(session)
104
101
  else
105
102
  Log.log.debug('Starting multi session threads')
106
103
  1.upto(multi_session_info[:count]) do |i|
@@ -108,22 +105,19 @@ module Aspera
108
105
  sleep(@options[:spawn_delay_sec]) unless i.eql?(1)
109
106
  # do deep copy (each thread has its own copy because it is modified here below and in thread)
110
107
  this_session = session.clone
108
+ this_session[:ts] = this_session[:ts].clone
111
109
  this_session[:env_args] = this_session[:env_args].clone
112
110
  this_session[:env_args][:args] = this_session[:env_args][:args].clone
111
+ # set multi session part
113
112
  this_session[:env_args][:args].unshift("-C#{i}:#{multi_session_info[:count]}")
114
113
  # option: increment (default as per ascp manual) or not (cluster on other side ?)
115
114
  this_session[:env_args][:args].unshift('-O', (multi_session_info[:udp_base] + i - 1).to_s) if @options[:multi_incr_udp]
115
+ # finally start the thread
116
116
  this_session[:thread] = Thread.new(this_session) {|s|transfer_thread_entry(s)}
117
- xfer_job[:sessions].push(this_session)
117
+ @sessions.push(this_session)
118
118
  end
119
119
  end
120
- Log.log.debug('started session thread(s)')
121
-
122
- # add job to list of jobs
123
- @jobs[the_job_id] = xfer_job
124
- Log.log.debug{"jobs: #{@jobs.keys.count}"}
125
-
126
- return the_job_id
120
+ return session[:job_id]
127
121
  end # start_transfer
128
122
 
129
123
  # wait for completion of all jobs started
@@ -132,16 +126,14 @@ module Aspera
132
126
  Log.log.debug('wait_for_transfers_completion')
133
127
  # set to non-nil to exit loop
134
128
  result = []
135
- @jobs.each do |_id, job|
136
- job[:sessions].each do |session|
137
- Log.log.debug{"join #{session[:thread]}"}
138
- session[:thread].join
139
- result.push(session[:error] || :success)
140
- end
129
+ @sessions.each do |session|
130
+ Log.log.debug{"join #{session[:thread]}"}
131
+ session[:thread].join
132
+ result.push(session[:error] || :success)
141
133
  end
142
134
  Log.log.debug('all transfers joined')
143
135
  # since all are finished and we return the result, clear statuses
144
- @jobs.clear
136
+ @sessions.clear
145
137
  return result
146
138
  end
147
139
 
@@ -150,12 +142,6 @@ module Aspera
150
142
  Log.log.debug('fasp local shutdown')
151
143
  end
152
144
 
153
- # cspell:disable
154
- # begin 'Type' => 'NOTIFICATION', 'PreTransferBytes' => size
155
- # progress 'Type' => 'STATS', 'Bytescont' => size
156
- # end 'Type' => 'DONE'
157
- # cspell:enable
158
-
159
145
  # @param event management port event
160
146
  def process_progress(event)
161
147
  session_id = event['SessionId']
@@ -198,63 +184,66 @@ module Aspera
198
184
  # start ascp with management port.
199
185
  # raises FaspError on error
200
186
  # if there is a thread info: set and broadcast session id
187
+ # runs in separate thread
201
188
  # @param env_args a hash containing :args :env :ascp_version
202
189
  # @param session this session information
203
190
  # could be private method
204
191
  def start_transfer_with_args_env(env_args, session)
205
- raise 'env_args must be Hash' unless env_args.is_a?(Hash)
206
- raise 'session must be Hash' unless session.is_a?(Hash)
192
+ Aspera.assert_type(env_args, Hash)
193
+ Aspera.assert_type(session, Hash)
194
+ Log.log.debug{"env_args=#{env_args.inspect}"}
195
+ notify_progress(session_id: nil, type: :pre_start, info: 'starting')
207
196
  begin
208
- Log.log.debug{"env_args=#{env_args.inspect}"}
197
+ ascp_pid = nil
198
+ # we use Socket directly, instead of TCPServer, as it gives access to lower level options
199
+ socket_class = RUBY_ENGINE.eql?('jruby') ? ServerSocket : Socket
200
+ mgt_server_socket = socket_class.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
201
+ # open any available (0) local TCP port for use as ascp management port
202
+ mgt_server_socket.bind(Addrinfo.tcp(LISTEN_LOCAL_ADDRESS, ANY_AVAILABLE_PORT))
203
+ # build arguments and add mgt port
204
+ ascp_arguments = ['-M', mgt_server_socket.local_address.ip_port.to_s].concat(env_args[:args])
209
205
  # get location of ascp executable
210
- ascp_path = @mutex.synchronize do
211
- Fasp::Installation.instance.path(env_args[:ascp_version])
212
- end
213
- # (optional) check it exists
214
- raise Fasp::Error, "no such file: #{ascp_path}" unless File.exist?(ascp_path)
215
- notify_progress(session_id: nil, type: :pre_start, info: 'starting ascp')
216
- # open an available (0) local TCP port as ascp management
217
- mgt_sock = TCPServer.new('127.0.0.1', 0)
218
- # clone arguments as we eed to modify with mgt port
219
- ascp_arguments = env_args[:args].clone
220
- # add management port on the selected local port
221
- ascp_arguments.unshift('-M', mgt_sock.addr[1].to_s)
206
+ ascp_path = Ascp::Installation.instance.path(env_args[:ascp_version])
222
207
  # display ascp command line
223
208
  Log.log.debug do
224
209
  [
225
- 'execute:',
210
+ 'execute:'.red,
226
211
  env_args[:env].map{|k, v| "#{k}=#{Shellwords.shellescape(v)}"},
227
212
  Shellwords.shellescape(ascp_path),
228
213
  ascp_arguments.map{|a|Shellwords.shellescape(a)}
229
214
  ].flatten.join(' ')
230
215
  end
231
216
  # start ascp in separate process
232
- ascp_pid = Process.spawn(env_args[:env], [ascp_path, ascp_path], *ascp_arguments)
233
- # in parent, wait for connection to socket max 3 seconds
234
- Log.log.debug{"before accept for pid (#{ascp_pid})"}
235
- # init management socket
236
- ascp_mgt_io = nil
217
+ ascp_pid = Process.spawn(env_args[:env], [ascp_path, ascp_path], *ascp_arguments, close_others: true)
218
+ Log.log.debug{"spawned ascp pid #{ascp_pid}"}
237
219
  notify_progress(session_id: nil, type: :pre_start, info: 'waiting for ascp')
238
- Timeout.timeout(@options[:spawn_timeout_sec]) do
239
- ascp_mgt_io = mgt_sock.accept
240
- # management messages include file names which may be utf8
241
- # by default socket is US-ASCII
242
- # TODO: use same value as Encoding.default_external
243
- ascp_mgt_io.set_encoding(Encoding::UTF_8)
244
- end
245
- Log.log.debug{"after accept (#{ascp_mgt_io})"}
220
+ mgt_server_socket.listen(1)
221
+ # TODO: timeout does not work when Process.spawn is used... until process exits, then it works
222
+ Log.log.debug{"before select, timeout: #{@options[:spawn_timeout_sec]}"}
223
+ readable, _, _ = IO.select([mgt_server_socket], nil, nil, @options[:spawn_timeout_sec])
224
+ Log.log.debug('after select, before accept')
225
+ Aspera.assert(readable, exception_class: Transfer::Error){'timeout waiting mgt port connect (select not readable)'}
226
+ # There is a connection to accept
227
+ client_socket, _client_addrinfo = mgt_server_socket.accept
228
+ Log.log.debug('after accept')
229
+ ascp_mgt_io = client_socket.to_io
230
+ # management messages include file names which may be utf8
231
+ # by default socket is US-ASCII
232
+ # TODO: use same value as Encoding.default_external
233
+ ascp_mgt_io.set_encoding(Encoding::UTF_8)
246
234
  session[:io] = ascp_mgt_io
247
- processor = Management.new
235
+ processor = Ascp::Management.new
248
236
  # read management port, until socket is closed (gets returns nil)
249
237
  while (line = ascp_mgt_io.gets)
250
238
  event = processor.process_line(line.chomp)
251
239
  next unless event
252
240
  # event is ready
253
- Log.log.debug{Log.dump(:management_port, event)}
254
- # Log.log.trace1{"event: #{JSON.generate(Management.enhanced_event_format(event))}"}
241
+ Log.log.trace1{Log.dump(:management_port, event)}
242
+ @options[:management_cb]&.call(event)
255
243
  process_progress(event)
256
244
  Log.log.error((event['Description']).to_s) if event['Type'].eql?('FILEERROR') # cspell:disable-line
257
245
  end
246
+ Log.log.debug('management io closed')
258
247
  last_event = processor.last_event
259
248
  # check that last status was received before process exit
260
249
  if last_event.is_a?(Hash)
@@ -264,10 +253,10 @@ module Aspera
264
253
  session[:token_regenerator].respond_to?(:refreshed_transfer_token)
265
254
  # regenerate token here, expired, or error on it
266
255
  # Note: in multi-session, each session will have a different one.
267
- Log.log.warn('Regenerating bearer token')
256
+ Log.log.warn('Regenerating token for transfer')
268
257
  env_args[:env]['ASPERA_SCP_TOKEN'] = session[:token_regenerator].refreshed_transfer_token
269
258
  end
270
- raise Fasp::Error.new(last_event['Description'], last_event['Code'].to_i)
259
+ raise Transfer::Error.new(last_event['Description'], last_event['Code'].to_i)
271
260
  when 'DONE'
272
261
  nil
273
262
  else
@@ -275,42 +264,52 @@ module Aspera
275
264
  end # case
276
265
  end
277
266
  rescue SystemCallError => e
278
- # Process.spawn
279
- raise Fasp::Error, e.message
280
- rescue Timeout::Error
281
- raise Fasp::Error, 'timeout waiting mgt port connect'
267
+ # Process.spawn failed, or socket error
268
+ raise Transfer::Error, e.message
282
269
  rescue Interrupt
283
- raise Fasp::Error, 'transfer interrupted by user'
270
+ raise Transfer::Error, 'transfer interrupted by user'
284
271
  ensure
285
- # if ascp was successfully started
272
+ mgt_server_socket.close
273
+ # if ascp was successfully started, check its status
286
274
  unless ascp_pid.nil?
287
275
  # "wait" for process to avoid zombie
288
276
  Process.wait(ascp_pid)
289
277
  status = $CHILD_STATUS
290
278
  ascp_pid = nil
291
279
  session.delete(:io)
292
- if !status.success?
293
- message = "ascp failed with code #{status.exitstatus}"
294
- # raise error only if there was not already an exception
295
- raise Fasp::Error, message unless $ERROR_INFO
296
- # else just debug, as main exception is already here
297
- Log.log.debug(message)
280
+ # status is nil if an exception occurred before starting ascp
281
+ if !status&.success?
282
+ message = status.nil? ? 'ascp not started' : "ascp failed (#{status})"
283
+ # raise error only if there was not already an exception (ERROR_INFO)
284
+ raise Transfer::Error, message unless $ERROR_INFO
285
+ # else display this message also, as main exception is already here
286
+ Log.log.error(message)
298
287
  end
299
288
  end
300
289
  end # begin-ensure
301
290
  end # start_transfer_with_args_env
302
291
 
303
- # send command of management port to ascp session
292
+ # @return [Array] list of sessions for a job
293
+ def sessions_by_job(job_id)
294
+ @sessions.select{|s| s[:job_id].eql?(job_id)}
295
+ end
296
+
297
+ # @return [Hash] session information
298
+ def session_by_id(id)
299
+ matches = @sessions.select{|s| s[:id].eql?(id)}
300
+ raise 'no such session' if matches.empty?
301
+ raise 'more than one session' if matches.length > 1
302
+ return matches.first
303
+ end
304
+
305
+ # send command of management port to ascp session (used in `asession)
304
306
  # @param job_id identified transfer process
305
307
  # @param session_index index of session (for multi session)
306
308
  # @param data command on mgt port, examples:
307
309
  # {'type'=>'START','source'=>_path_,'destination'=>_path_}
308
310
  # {'type'=>'DONE'}
309
- def send_command(job_id, session_index, data)
310
- job = @jobs[job_id]
311
- raise 'no such job' if job.nil?
312
- session = job[:sessions][session_index]
313
- raise 'no such session' if session.nil?
311
+ def send_command(job_id, data)
312
+ session = session_by_id(job_id)
314
313
  Log.log.debug{"command: #{data}"}
315
314
  # build command
316
315
  command = data
@@ -321,20 +320,21 @@ module Aspera
321
320
  .join("\n")
322
321
  session[:io].puts(command)
323
322
  end
323
+ attr_reader :sessions
324
324
 
325
325
  private
326
326
 
327
327
  # @param options : keys(symbol): see DEFAULT_OPTIONS
328
328
  def initialize(options={})
329
329
  super(options)
330
+ # set default options and override if specified
331
+ @options = Base.options(default: DEFAULT_OPTIONS, options: options)
332
+ Log.log.debug{Log.dump(:agent_options, @options)}
333
+ @resume_policy = Resumer.new(@options[:resume].symbolize_keys)
330
334
  # all transfer jobs, key = SecureRandom.uuid, protected by mutex, cond var on change
331
- @jobs = {}
335
+ @sessions = []
332
336
  # mutex protects global data accessed by threads
333
337
  @mutex = Mutex.new
334
- # set default options and override if specified
335
- @options = AgentBase.options(default: DEFAULT_OPTIONS, options: options)
336
- @resume_policy = ResumePolicy.new(@options[:resume].symbolize_keys)
337
- Log.log.debug{Log.dump(:agent_options, @options)}
338
338
  end
339
339
 
340
340
  # transfer thread entry
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'aspera/fasp/agent_base'
4
- require 'aspera/fasp/transfer_spec'
5
- require 'aspera/fasp/faux_file'
3
+ require 'aspera/agent/base'
4
+ require 'aspera/transfer/spec'
5
+ require 'aspera/transfer/faux_file'
6
6
  require 'aspera/log'
7
+ require 'aspera/assert'
7
8
  require 'aspera/rest'
8
9
  require 'securerandom'
9
10
  require 'websocket'
@@ -11,7 +12,7 @@ require 'base64'
11
12
  require 'json'
12
13
 
13
14
  module Aspera
14
- module Fasp
15
+ module Agent
15
16
  # Start a transfer using Aspera HTTP Gateway, using web socket secure for uploads
16
17
  # ref: https://api.ibm.com/explorer/catalog/aspera/product/ibm-aspera/api/http-gateway-api/doc/guides-toc
17
18
  # https://developer.ibm.com/apis/catalog?search=%22aspera%20http%22
@@ -25,7 +26,7 @@ module Aspera
25
26
  # 1 JSON.slice_upload File start "end_slice_upload" sent_v2_delimiter
26
27
  # 2.. Binary File binary chunks "end upload" sent_general
27
28
  # last JSON.slice_upload File end "end_slice_upload" sent_v2_delimiter
28
- class AgentHttpgw < Aspera::Fasp::AgentBase
29
+ class Httpgw < Base
29
30
  MSG_SEND_TRANSFER_SPEC = 'transfer_spec'
30
31
  MSG_SEND_SLICE_UPLOAD = 'slice_upload'
31
32
  MSG_RECV_DATA_RECEIVED_SIGNAL = 'end upload'
@@ -159,7 +160,7 @@ module Aspera
159
160
  # modify transfer spec to be suitable for GW
160
161
  transfer_spec['paths'].each do |item|
161
162
  # save actual file location to be able read contents later
162
- file_to_add = FauxFile.open(item['source'])
163
+ file_to_add = Transfer::FauxFile.open(item['source'])
163
164
  if file_to_add
164
165
  item['source'] = file_to_add.path
165
166
  item['file_size'] = file_to_add.size
@@ -175,7 +176,8 @@ module Aspera
175
176
  files_to_read.push(file_to_add)
176
177
  total_bytes_to_transfer += item['file_size']
177
178
  end
178
- upload_url = File.join(@gw_api.params[:base_url], @options[:api_version], 'upload')
179
+ # TODO: check that this is available in endpoints: @api_info['endpoints']
180
+ upload_url = File.join(@gw_base_url, @options[:api_version], 'upload')
179
181
  notify_progress(session_id: nil, type: :pre_start, info: 'connecting wss')
180
182
  # open web socket to end point (equivalent to Net::HTTP.start)
181
183
  http_session = Rest.start_http_session(upload_url)
@@ -185,7 +187,7 @@ module Aspera
185
187
  @ws_io.write(@ws_handshake.to_s)
186
188
  sleep(0.1)
187
189
  @ws_handshake << @ws_io.readuntil("\r\n\r\n")
188
- raise 'Error in websocket handshake' unless @ws_handshake.finished?
190
+ Aspera.assert(@ws_handshake.finished?){'Error in websocket handshake'}
189
191
  Log.log.debug{"#{LOG_WS_SEND}handshake success"}
190
192
  # start read thread after handshake
191
193
  @ws_read_thread = Thread.new {process_read_thread}
@@ -226,7 +228,7 @@ module Aspera
226
228
  fileIndex: file_index
227
229
  }
228
230
  file = files_to_read[file_index]
229
- if file.is_a?(FauxFile)
231
+ if file.is_a?(Transfer::FauxFile)
230
232
  slice_info[:name] = file.path
231
233
  else
232
234
  file = File.open(file)
@@ -294,7 +296,7 @@ module Aspera
294
296
  end
295
297
  creation = @gw_api.create('v1/download', {'transfer_spec' => transfer_spec})[:data]
296
298
  transfer_uuid = creation['url'].split('/').last
297
- file_dest =
299
+ file_name =
298
300
  if transfer_spec['zip_required'] || transfer_spec['paths'].length > 1
299
301
  # it is a zip file if zip is required or there is more than 1 file
300
302
  transfer_spec['download_name'] + '.zip'
@@ -302,8 +304,8 @@ module Aspera
302
304
  # it is a plain file if we don't require zip and there is only one file
303
305
  File.basename(transfer_spec['paths'].first['source'])
304
306
  end
305
- file_dest = File.join(transfer_spec['destination_root'], file_dest)
306
- @gw_api.call({operation: 'GET', subpath: "v1/download/#{transfer_uuid}", save_to_file: file_dest})
307
+ file_path = File.join(transfer_spec['destination_root'], file_name)
308
+ @gw_api.call(operation: 'GET', subpath: "v1/download/#{transfer_uuid}", save_to_file: file_path)
307
309
  end
308
310
 
309
311
  # start FASP transfer based on transfer spec (hash table)
@@ -311,14 +313,14 @@ module Aspera
311
313
  # HTTP download only supports file list
312
314
  def start_transfer(transfer_spec, token_regenerator: nil)
313
315
  raise 'GW URL must be set' if @gw_api.nil?
314
- raise 'paths: must be Array' unless transfer_spec['paths'].is_a?(Array)
315
- raise 'only token based transfer is supported in GW' unless transfer_spec['token'].is_a?(String)
316
+ Aspera.assert_type(transfer_spec['paths'], Array){'paths'}
317
+ Aspera.assert_type(transfer_spec['token'], String){'only token based transfer is supported in GW'}
316
318
  Log.log.debug{Log.dump(:user_spec, transfer_spec)}
317
319
  transfer_spec['authentication'] ||= 'token'
318
320
  case transfer_spec['direction']
319
- when Fasp::TransferSpec::DIRECTION_SEND
321
+ when Transfer::Spec::DIRECTION_SEND
320
322
  upload(transfer_spec)
321
- when Fasp::TransferSpec::DIRECTION_RECEIVE
323
+ when Transfer::Spec::DIRECTION_RECEIVE
322
324
  download(transfer_spec)
323
325
  else
324
326
  raise "unexpected direction: [#{transfer_spec['direction']}]"
@@ -339,10 +341,10 @@ module Aspera
339
341
 
340
342
  def initialize(opts)
341
343
  super(opts)
342
- @options = AgentBase.options(default: DEFAULT_OPTIONS, options: opts)
344
+ @options = Base.options(default: DEFAULT_OPTIONS, options: opts)
343
345
  # remove /v1 from end of user-provided GW url: we need the base url only
344
- @options[:url].gsub(%r{/v1/*$}, '')
345
- @gw_api = Rest.new({base_url: @options[:url]})
346
+ @gw_base_url = @options[:url].gsub(%r{/v1/*$}, '')
347
+ @gw_api = Rest.new(base_url: @gw_base_url)
346
348
  @api_info = @gw_api.read('v1/info')[:data]
347
349
  Log.log.debug{Log.dump(:api_info, @api_info)}
348
350
  # web socket endpoint: by default use v2 (newer gateways), without base64 encoding
@@ -1,17 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # cspell:ignore precalc
4
- require 'aspera/fasp/agent_base'
5
- require 'aspera/fasp/transfer_spec'
6
- require 'aspera/node'
4
+ require 'aspera/agent/base'
5
+ require 'aspera/transfer/spec'
6
+ require 'aspera/api/node'
7
7
  require 'aspera/log'
8
+ require 'aspera/assert'
8
9
  require 'aspera/oauth'
9
10
 
10
11
  module Aspera
11
- module Fasp
12
+ module Agent
12
13
  # this singleton class is used by the CLI to provide a common interface to start a transfer
13
14
  # before using it, the use must set the `node_api` member.
14
- class AgentNode < Aspera::Fasp::AgentBase
15
+ class Node < Base
15
16
  DEFAULT_OPTIONS = {
16
17
  url: :required,
17
18
  username: :required,
@@ -22,15 +23,15 @@ module Aspera
22
23
  # attr_writer :options
23
24
 
24
25
  def initialize(opts)
25
- raise 'node specification must be Hash' unless opts.is_a?(Hash)
26
+ Aspera.assert_type(opts, Hash){'node agent options'}
26
27
  super(opts)
27
- options = AgentBase.options(default: DEFAULT_OPTIONS, options: opts)
28
+ options = Base.options(default: DEFAULT_OPTIONS, options: opts)
28
29
  # root id is required for access key
29
30
  @root_id = options[:root_id]
30
31
  rest_params = { base_url: options[:url]}
31
- if Oauth.bearer?(options[:password])
32
- raise 'root_id is required for access key' if @root_id.nil?
33
- rest_params[:headers] = Aspera::Node.bearer_headers(options[:password], access_key: options[:username])
32
+ if OAuth::Factory.bearer?(options[:password])
33
+ Aspera.assert(!@root_id.nil?){'root_id not allowed for access key'}
34
+ rest_params[:headers] = Api::Node.bearer_headers(options[:password], access_key: options[:username])
34
35
  else
35
36
  rest_params[:auth] = {
36
37
  type: :basic,
@@ -38,7 +39,7 @@ module Aspera
38
39
  password: options[:password]
39
40
  }
40
41
  end
41
- @node_api = Rest.new(rest_params)
42
+ @node_api = Rest.new(**rest_params)
42
43
  # TODO: currently only supports one transfer. This is bad shortcut. but ok for CLI.
43
44
  @transfer_id = nil
44
45
  # Log.log.debug{Log.dump(:agent_options, @options)}
@@ -46,11 +47,9 @@ module Aspera
46
47
 
47
48
  # used internally to ensure node api is set before using.
48
49
  def node_api_
49
- raise StandardError, 'Before using this object, set the node_api attribute to a Aspera::Rest object' if @node_api.nil?
50
+ Aspera.assert(!@node_api.nil?){'Before using this object, set the node_api attribute to a Aspera::Rest object'}
50
51
  return @node_api
51
52
  end
52
- # use this to read the node_api end point.
53
- # attr_reader :node_api
54
53
 
55
54
  # use this to set the node_api end point before using the class.
56
55
  def node_api=(new_value)
@@ -65,30 +64,19 @@ module Aspera
65
64
  # add root id if access key
66
65
  if !@root_id.nil?
67
66
  case transfer_spec['direction']
68
- when Fasp::TransferSpec::DIRECTION_SEND then transfer_spec['source_root_id'] = @root_id
69
- when Fasp::TransferSpec::DIRECTION_RECEIVE then transfer_spec['destination_root_id'] = @root_id
70
- else raise "unexpected direction in ts: #{transfer_spec['direction']}"
71
- end
72
- end
73
- # manage special additional parameter
74
- if transfer_spec.key?('EX_ssh_key_paths') && transfer_spec['EX_ssh_key_paths'].is_a?(Array) && !transfer_spec['EX_ssh_key_paths'].empty?
75
- # not standard, so place standard field
76
- if transfer_spec.key?('ssh_private_key')
77
- Log.log.warn('Both ssh_private_key and EX_ssh_key_paths are present, using ssh_private_key')
78
- else
79
- Log.log.warn('EX_ssh_key_paths has multiple keys, using first one only') unless transfer_spec['EX_ssh_key_paths'].length.eql?(1)
80
- transfer_spec['ssh_private_key'] = File.read(transfer_spec['EX_ssh_key_paths'].first)
81
- transfer_spec.delete('EX_ssh_key_paths')
67
+ when Transfer::Spec::DIRECTION_SEND then transfer_spec['source_root_id'] = @root_id
68
+ when Transfer::Spec::DIRECTION_RECEIVE then transfer_spec['destination_root_id'] = @root_id
69
+ else Aspera.error_unexpected_value(transfer_spec['direction'])
82
70
  end
83
71
  end
84
72
  # add mandatory retry parameter for node api
85
73
  ts_tags = transfer_spec['tags']
86
- if ts_tags.is_a?(Hash) && ts_tags[Fasp::TransferSpec::TAG_RESERVED].is_a?(Hash)
87
- ts_tags[Fasp::TransferSpec::TAG_RESERVED]['xfer_retry'] ||= 150
74
+ if ts_tags.is_a?(Hash) && ts_tags[Transfer::Spec::TAG_RESERVED].is_a?(Hash)
75
+ ts_tags[Transfer::Spec::TAG_RESERVED]['xfer_retry'] ||= 150
88
76
  end
89
77
  # Optimization in case of sending to the same node
90
78
  # TODO: probably remove this, as /etc/hosts shall be used for that
91
- if !transfer_spec['wss_enabled'] && transfer_spec['remote_host'].eql?(URI.parse(node_api_.params[:base_url]).host)
79
+ if !transfer_spec['wss_enabled'] && transfer_spec['remote_host'].eql?(URI.parse(node_api_.base_url).host)
92
80
  transfer_spec['remote_host'] = '127.0.0.1'
93
81
  end
94
82
  resp = node_api_.create('ops/transfers', transfer_spec)[:data]
@@ -132,10 +120,10 @@ module Aspera
132
120
  notify_progress(type: :end, session_id: @transfer_id)
133
121
  # Bug in HSTS ? transfer is marked failed, but there is no reason
134
122
  break if transfer_data['error_code'].eql?(0) && transfer_data['error_desc'].empty?
135
- raise Fasp::Error, "status: #{transfer_data['status']}. code: #{transfer_data['error_code']}. description: #{transfer_data['error_desc']}"
123
+ raise Transfer::Error, "status: #{transfer_data['status']}. code: #{transfer_data['error_code']}. description: #{transfer_data['error_desc']}"
136
124
  else
137
125
  Log.log.warn{"transfer_data -> #{transfer_data}"}
138
- raise Fasp::Error, "status: #{transfer_data['status']}. code: #{transfer_data['error_code']}. description: #{transfer_data['error_desc']}"
126
+ raise Transfer::Error, "status: #{transfer_data['status']}. code: #{transfer_data['error_code']}. description: #{transfer_data['error_desc']}"
139
127
  end
140
128
  sleep(1.0)
141
129
  end