aspera-cli 4.12.0 → 4.14.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 (80) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +45 -5
  4. data/CONTRIBUTING.md +113 -22
  5. data/README.md +1289 -754
  6. data/bin/ascli +3 -3
  7. data/examples/dascli +1 -1
  8. data/examples/rubyc +24 -0
  9. data/lib/aspera/aoc.rb +63 -74
  10. data/lib/aspera/ascmd.rb +5 -3
  11. data/lib/aspera/cli/basic_auth_plugin.rb +6 -6
  12. data/lib/aspera/cli/extended_value.rb +24 -37
  13. data/lib/aspera/cli/formatter.rb +23 -25
  14. data/lib/aspera/cli/info.rb +2 -4
  15. data/lib/aspera/cli/main.rb +27 -27
  16. data/lib/aspera/cli/manager.rb +143 -120
  17. data/lib/aspera/cli/plugin.rb +88 -43
  18. data/lib/aspera/cli/plugins/alee.rb +2 -2
  19. data/lib/aspera/cli/plugins/aoc.rb +235 -104
  20. data/lib/aspera/cli/plugins/ats.rb +16 -18
  21. data/lib/aspera/cli/plugins/bss.rb +3 -3
  22. data/lib/aspera/cli/plugins/config.rb +190 -373
  23. data/lib/aspera/cli/plugins/console.rb +4 -6
  24. data/lib/aspera/cli/plugins/cos.rb +12 -13
  25. data/lib/aspera/cli/plugins/faspex.rb +21 -21
  26. data/lib/aspera/cli/plugins/faspex5.rb +399 -150
  27. data/lib/aspera/cli/plugins/node.rb +260 -174
  28. data/lib/aspera/cli/plugins/orchestrator.rb +15 -18
  29. data/lib/aspera/cli/plugins/preview.rb +40 -62
  30. data/lib/aspera/cli/plugins/server.rb +33 -16
  31. data/lib/aspera/cli/plugins/shares.rb +24 -33
  32. data/lib/aspera/cli/plugins/sync.rb +6 -6
  33. data/lib/aspera/cli/transfer_agent.rb +47 -30
  34. data/lib/aspera/cli/version.rb +2 -1
  35. data/lib/aspera/colors.rb +9 -7
  36. data/lib/aspera/command_line_builder.rb +2 -1
  37. data/lib/aspera/cos_node.rb +1 -1
  38. data/lib/aspera/data/6 +0 -0
  39. data/lib/aspera/environment.rb +7 -3
  40. data/lib/aspera/fasp/agent_connect.rb +6 -1
  41. data/lib/aspera/fasp/agent_direct.rb +17 -17
  42. data/lib/aspera/fasp/agent_httpgw.rb +138 -60
  43. data/lib/aspera/fasp/agent_node.rb +14 -4
  44. data/lib/aspera/fasp/agent_trsdk.rb +2 -0
  45. data/lib/aspera/fasp/error_info.rb +2 -0
  46. data/lib/aspera/fasp/installation.rb +19 -19
  47. data/lib/aspera/fasp/parameters.rb +29 -20
  48. data/lib/aspera/fasp/parameters.yaml +5 -2
  49. data/lib/aspera/fasp/resume_policy.rb +3 -3
  50. data/lib/aspera/fasp/transfer_spec.rb +8 -5
  51. data/lib/aspera/fasp/uri.rb +23 -21
  52. data/lib/aspera/faspex_gw.rb +1 -0
  53. data/lib/aspera/faspex_postproc.rb +3 -3
  54. data/lib/aspera/hash_ext.rb +12 -2
  55. data/lib/aspera/keychain/macos_security.rb +13 -13
  56. data/lib/aspera/log.rb +1 -0
  57. data/lib/aspera/node.rb +73 -84
  58. data/lib/aspera/oauth.rb +4 -3
  59. data/lib/aspera/persistency_action_once.rb +1 -1
  60. data/lib/aspera/preview/file_types.rb +8 -6
  61. data/lib/aspera/preview/generator.rb +23 -11
  62. data/lib/aspera/preview/options.rb +3 -2
  63. data/lib/aspera/preview/terminal.rb +80 -0
  64. data/lib/aspera/preview/utils.rb +11 -11
  65. data/lib/aspera/proxy_auto_config.js +2 -2
  66. data/lib/aspera/rest.rb +42 -4
  67. data/lib/aspera/rest_call_error.rb +3 -1
  68. data/lib/aspera/secret_hider.rb +10 -5
  69. data/lib/aspera/ssh.rb +1 -1
  70. data/lib/aspera/sync.rb +41 -33
  71. data/lib/aspera/web_server_simple.rb +22 -18
  72. data.tar.gz.sig +0 -0
  73. metadata +40 -48
  74. metadata.gz.sig +0 -0
  75. data/docs/test_env.conf +0 -179
  76. data/examples/aoc.rb +0 -30
  77. data/examples/faspex4.rb +0 -94
  78. data/examples/node.rb +0 -96
  79. data/examples/server.rb +0 -93
  80. data/lib/aspera/data/7 +0 -0
@@ -9,6 +9,17 @@ require 'websocket'
9
9
  require 'base64'
10
10
  require 'json'
11
11
 
12
+ # HTTP GW Upload protocol
13
+ # -----------------------
14
+ # v1
15
+ # 1 - MessageType: String (Transfer Spec) JSON : type: transfer_spec, acknowledged with "end upload"
16
+ # 2.. - MessageType: String (Slice Upload start) JSON : type: slice_upload, acknowledged with "end upload"
17
+ # v2
18
+ # 1 - MessageType: String (Transfer Spec) JSON : type: transfer_spec, acknowledged with "end upload"
19
+ # 2 - MessageType: String (Slice Upload start) JSON : type: slice_upload, acknowledged with "end_slice_upload"
20
+ # 3.. - MessageType: ByteArray (File Size) Chunks : acknowledged with "end upload"
21
+ # last - MessageType: String (Slice Upload end) JSON : type: slice_upload, acknowledged with "end_slice_upload"
22
+
12
23
  # ref: https://api.ibm.com/explorer/catalog/aspera/product/ibm-aspera/api/http-gateway-api/doc/guides-toc
13
24
  # https://developer.ibm.com/apis/catalog?search=%22aspera%20http%22
14
25
  module Aspera
@@ -16,35 +27,66 @@ module Aspera
16
27
  # start a transfer using Aspera HTTP Gateway, using web socket session for uploads
17
28
  class AgentHttpgw < Aspera::Fasp::AgentBase
18
29
  # message returned by HTTP GW in case of success
19
- MSG_END_UPLOAD = 'end upload'
20
- MSG_END_SLICE = 'end_slice_upload'
30
+ MSG_RECV_DATA_RECEIVED_SIGNAL = 'end upload'
31
+ MSG_RECV_SLICE_UPLOAD_SIGNAL = 'end_slice_upload'
32
+ MSG_SEND_SLICE_UPLOAD = 'slice_upload'
33
+ MSG_SEND_TRANSFER_SPEC = 'transfer_spec'
34
+ # upload API versions
35
+ API_V1 = 'v1'
36
+ API_V2 = 'v2'
21
37
  # options available in CLI (transfer_info)
22
38
  DEFAULT_OPTIONS = {
23
39
  url: nil,
24
40
  upload_chunk_size: 64_000,
25
- upload_bar_refresh_sec: 0.5
41
+ upload_bar_refresh_sec: 0.5,
42
+ api_version: API_V2,
43
+ synchronous: true
26
44
  }.freeze
27
45
  DEFAULT_BASE_PATH = '/aspera/http-gwy'
28
- # upload endpoints
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
46
+ LOG_WS_MAIN = 'ws: send: '.green
47
+ LOG_WS_THREAD = 'ws: ack: '.red
48
+ private_constant :DEFAULT_OPTIONS, :MSG_RECV_DATA_RECEIVED_SIGNAL, :MSG_RECV_SLICE_UPLOAD_SIGNAL, :API_V1, :API_V2
32
49
 
33
50
  # send message on http gw web socket
34
- def ws_snd_json(data)
35
- @slice_uploads += 1 if data.key?(:slice_upload)
36
- Log.log.debug{JSON.generate(data)}
37
- ws_send(JSON.generate(data))
51
+ def ws_snd_json(msg_type, payload)
52
+ if msg_type.eql?(MSG_SEND_SLICE_UPLOAD) && @options[:api_version].eql?(API_V2)
53
+ @shared_info[:count][:sent_v2_slice] += 1
54
+ else
55
+ @shared_info[:count][:sent_other] += 1
56
+ end
57
+ Log.log.debug do
58
+ log_data = payload.dup
59
+ log_data[:data] = "[data #{log_data[:data].length} bytes]" if log_data.key?(:data)
60
+ "send_txt: #{msg_type}: #{JSON.generate(log_data)}"
61
+ end
62
+ ws_send(JSON.generate({msg_type => payload}))
38
63
  end
39
64
 
40
- def ws_send(data, type: :text)
41
- frame = ::WebSocket::Frame::Outgoing::Client.new(data: data, type: type, version: @ws_handshake.version)
65
+ def ws_send(data_to_send, type: :text)
66
+ Log.log.debug{"#{LOG_WS_MAIN}send low: type: #{type}"}
67
+ @shared_info[:count][:sent_other] += 1 if type.eql?(:binary)
68
+ Log.log.debug{"#{LOG_WS_MAIN}counts: #{@shared_info[:count]}"}
69
+ frame = ::WebSocket::Frame::Outgoing::Client.new(data: data_to_send, type: type, version: @ws_handshake.version)
42
70
  @ws_io.write(frame.to_s)
43
71
  end
44
72
 
73
+ # wait for all message sent to be acknowledged by HTTPGW server
74
+ def wait_for_sent_msg_ack_or_exception
75
+ return unless @options[:synchronous]
76
+ @shared_info[:mutex].synchronize do
77
+ while (@shared_info[:count][:received_data] != @shared_info[:count][:sent_other]) ||
78
+ (@shared_info[:count][:received_v2_slice] != @shared_info[:count][:sent_v2_slice])
79
+ Log.log.debug{"#{LOG_WS_MAIN}wait: counts: #{@shared_info[:count]}"}
80
+ @shared_info[:cond_var].wait(@shared_info[:mutex], 1.0)
81
+ raise @shared_info[:read_exception] unless @shared_info[:read_exception].nil?
82
+ end
83
+ end
84
+ Log.log.debug{"#{LOG_WS_MAIN}sync ok: counts: #{@shared_info[:count]}"}
85
+ end
86
+
45
87
  def upload(transfer_spec)
46
88
  # total size of all files
47
- total_size = 0
89
+ total_bytes_to_transfer = 0
48
90
  # we need to keep track of actual file path because transfer spec is modified to be sent in web socket
49
91
  source_paths = []
50
92
  # get source root or nil
@@ -61,50 +103,65 @@ module Aspera
61
103
  # GW expects a simple file name in 'source' but if user wants to change the name, we take it
62
104
  item['source'] = File.basename(item['destination'].nil? ? item['source'] : item['destination'])
63
105
  item['file_size'] = File.size(full_src_filepath)
64
- total_size += item['file_size']
106
+ total_bytes_to_transfer += item['file_size']
65
107
  # save so that we can actually read the file later
66
108
  source_paths.push(full_src_filepath)
67
109
  end
68
110
  # identify this session uniquely
69
111
  session_id = SecureRandom.uuid
70
- @slice_uploads = 0
71
- # web socket endpoint: by default use v2 (newer gateways), without base64 encoding
72
- upload_api_version = V2_UPLOAD
73
- # is the latest supported? else revert to old api
74
- upload_api_version = V1_UPLOAD unless @api_info['endpoints'].any?{|i|i.include?(upload_api_version)}
75
- Log.log.debug{"api version: #{upload_api_version}"}
76
- url = File.join(@gw_api.params[:base_url], upload_api_version)
77
- # uri = URI.parse(url)
112
+ upload_url = File.join(@gw_api.params[:base_url], @options[:api_version], 'upload')
113
+ # uri = URI.parse(upload_url)
78
114
  # open web socket to end point (equivalent to Net::HTTP.start)
79
- http_socket = Rest.start_http_session(url)
115
+ http_socket = Rest.start_http_session(upload_url)
116
+ # little hack to get the socket opened for HTTP, handy because HTTP debug will be available
80
117
  @ws_io = http_socket.instance_variable_get(:@socket)
81
118
  # @ws_io.debug_output = Log.log
82
- @ws_handshake = ::WebSocket::Handshake::Client.new(url: url, headers: {})
119
+ @ws_handshake = ::WebSocket::Handshake::Client.new(url: upload_url, headers: {})
83
120
  @ws_io.write(@ws_handshake.to_s)
84
121
  sleep(0.1)
85
122
  @ws_handshake << @ws_io.readuntil("\r\n\r\n")
86
123
  raise 'Error in websocket handshake' unless @ws_handshake.finished?
87
- Log.log.debug('ws: handshake success')
124
+ Log.log.debug{"#{LOG_WS_MAIN}handshake success"}
88
125
  # data shared between main thread and read thread
89
- shared_info = {
126
+ @shared_info = {
90
127
  read_exception: nil, # error message if any in callback
91
- end_uploads: 0 # number of files totally sent
92
- # mutex: Mutex.new
93
- # cond_var: ConditionVariable.new
128
+ count: {
129
+ received_data: 0, # number of files received on other side
130
+ received_v2_slice: 0, # number of slices received on other side
131
+ sent_other: 0,
132
+ sent_v2_slice: 0
133
+ },
134
+ mutex: Mutex.new,
135
+ cond_var: ConditionVariable.new
94
136
  }
95
137
  # start read thread
96
138
  ws_read_thread = Thread.new do
97
- Log.log.debug('ws: thread: started')
98
- frame = ::WebSocket::Frame::Incoming::Client.new
139
+ Log.log.debug{"#{LOG_WS_THREAD}read started"}
140
+ frame = ::WebSocket::Frame::Incoming::Client.new(version: @ws_handshake.version)
99
141
  loop do
100
142
  begin # rubocop:disable Style/RedundantBegin
101
- frame << @ws_io.readuntil("\n")
143
+ # unless (recv_data = @ws_io.getc)
144
+ # sleep(0.1)
145
+ # next
146
+ # end
147
+ # frame << recv_data
148
+ # frame << @ws_io.readuntil("\n")
149
+ # frame << @ws_io.read_all
150
+ frame << @ws_io.read(1)
102
151
  while (msg = frame.next)
103
- Log.log.debug{"ws: thread: message: #{msg.data} #{shared_info[:end_uploads]}"}
152
+ Log.log.debug{"#{LOG_WS_THREAD}type: #{msg.class}"}
104
153
  message = msg.data
105
- if message.eql?(MSG_END_UPLOAD)
106
- shared_info[:end_uploads] += 1
107
- elsif message.eql?(MSG_END_SLICE)
154
+ Log.log.debug{"#{LOG_WS_THREAD}message: [#{message}]"}
155
+ if message.eql?(MSG_RECV_DATA_RECEIVED_SIGNAL)
156
+ @shared_info[:mutex].synchronize do
157
+ @shared_info[:count][:received_data] += 1
158
+ @shared_info[:cond_var].signal
159
+ end
160
+ elsif message.eql?(MSG_RECV_SLICE_UPLOAD_SIGNAL)
161
+ @shared_info[:mutex].synchronize do
162
+ @shared_info[:count][:received_v2_slice] += 1
163
+ @shared_info[:cond_var].signal
164
+ end
108
165
  else
109
166
  message.chomp!
110
167
  error_message =
@@ -117,19 +174,25 @@ module Aspera
117
174
  end
118
175
  raise error_message
119
176
  end
120
- end
177
+ Log.log.debug{"#{LOG_WS_THREAD}counts: #{@shared_info[:count]}"}
178
+ end # while
121
179
  rescue => e
122
- shared_info[:read_exception] = e unless e.is_a?(EOFError)
180
+ Log.log.debug{"#{LOG_WS_THREAD}Exception: #{e}"}
181
+ @shared_info[:mutex].synchronize do
182
+ @shared_info[:read_exception] = e unless e.is_a?(EOFError)
183
+ @shared_info[:cond_var].signal
184
+ end
123
185
  break
124
- end
125
- end
126
- Log.log.debug{"ws: thread: stopping (exc=#{shared_info[:read_exception]},cls=#{shared_info[:read_exception].class})"}
186
+ end # begin
187
+ end # loop
188
+ Log.log.debug{"#{LOG_WS_THREAD}stopping (exc=#{@shared_info[:read_exception]},cls=#{@shared_info[:read_exception].class})"}
127
189
  end
128
190
  # notify progress bar
129
- notify_begin(session_id, total_size)
191
+ notify_begin(session_id, total_bytes_to_transfer)
130
192
  # first step send transfer spec
131
193
  Log.dump(:ws_spec, transfer_spec)
132
- ws_snd_json(transfer_spec: transfer_spec)
194
+ ws_snd_json(MSG_SEND_TRANSFER_SPEC, transfer_spec)
195
+ wait_for_sent_msg_ack_or_exception
133
196
  # current file index
134
197
  file_index = 0
135
198
  # aggregate size sent
@@ -148,7 +211,7 @@ module Aspera
148
211
  # current slice index
149
212
  slice_index = 0
150
213
  until file.eof?
151
- data = file.read(@options[:upload_chunk_size])
214
+ file_bin_data = file.read(@options[:upload_chunk_size])
152
215
  slice_data = {
153
216
  name: file_name,
154
217
  type: file_mime_type,
@@ -159,22 +222,26 @@ module Aspera
159
222
  }
160
223
  # Log.dump(:slice_data,slice_data) #if slice_index.eql?(0)
161
224
  # interrupt main thread if read thread failed
162
- raise shared_info[:read_exception] unless shared_info[:read_exception].nil?
225
+ raise @shared_info[:read_exception] unless @shared_info[:read_exception].nil?
163
226
  begin
164
- if upload_api_version.eql?(V1_UPLOAD)
165
- slice_data[:data] = Base64.strict_encode64(data)
166
- ws_snd_json(slice_upload: slice_data)
227
+ if @options[:api_version].eql?(API_V1)
228
+ slice_data[:data] = Base64.strict_encode64(file_bin_data)
229
+ ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_data)
167
230
  else
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)
231
+ ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_data) if slice_index.eql?(0)
232
+ ws_send(file_bin_data, type: :binary)
233
+ Log.log.debug{"#{LOG_WS_MAIN}sent bin buffer: #{file_index} / #{slice_index}"}
234
+ ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_data) if slice_index.eql?(slice_total - 1)
172
235
  end
236
+ wait_for_sent_msg_ack_or_exception
173
237
  rescue Errno::EPIPE => e
174
- raise shared_info[:read_exception] unless shared_info[:read_exception].nil?
238
+ raise @shared_info[:read_exception] unless @shared_info[:read_exception].nil?
239
+ raise e
240
+ rescue Net::ReadTimeout => e
241
+ Log.log.warn{'A timeout condition using HTTPGW may signal a permission problem on destination. Check ascp logs on httpgw.'}
175
242
  raise e
176
243
  end
177
- sent_bytes += data.length
244
+ sent_bytes += file_bin_data.length
178
245
  current_time = Time.now
179
246
  if last_progress_time.nil? || ((current_time - last_progress_time) > @options[:upload_bar_refresh_sec])
180
247
  notify_progress(session_id, sent_bytes)
@@ -186,9 +253,9 @@ module Aspera
186
253
  file_index += 1
187
254
  end
188
255
 
189
- Log.log.debug('Finished upload')
256
+ Log.log.debug('Finished upload, waiting for end of read thread.')
190
257
  ws_read_thread.join
191
- Log.log.debug{"result: #{shared_info[:end_uploads]} / #{@slice_uploads}"}
258
+ Log.log.debug{"Read thread joined, result: #{@shared_info[:count][:received_data]} / #{@shared_info[:count][:sent_other]}"}
192
259
  ws_send(nil, type: :close) unless @ws_io.nil?
193
260
  @ws_io = nil
194
261
  http_socket&.finish
@@ -258,7 +325,7 @@ module Aspera
258
325
  private
259
326
 
260
327
  def initialize(opts)
261
- Log.log.debug{"local options= #{opts}"}
328
+ Log.dump(:in_options, opts)
262
329
  # set default options and override if specified
263
330
  @options = DEFAULT_OPTIONS.dup
264
331
  raise "httpgw agent parameters (transfer_info): expecting Hash, but have #{opts.class}" unless opts.is_a?(Hash)
@@ -266,13 +333,24 @@ module Aspera
266
333
  raise "httpgw agent parameter: Unknown: #{k}, expect one of #{DEFAULT_OPTIONS.keys.map(&:to_s).join(',')}" unless DEFAULT_OPTIONS.key?(k)
267
334
  @options[k] = v
268
335
  end
269
- raise 'missing param: url' if @options[:url].nil?
336
+ if @options[:url].nil?
337
+ available = DEFAULT_OPTIONS.map { |k, v| "#{k}(#{v})"}.join(', ')
338
+ raise "Missing mandatory parameter for HTTP GW in transfer_info: url. Allowed parameters: #{available}."
339
+ end
270
340
  # remove /v1 from end
271
341
  @options[:url].gsub(%r{/v1/*$}, '')
272
342
  super()
273
343
  @gw_api = Rest.new({base_url: @options[:url]})
274
344
  @api_info = @gw_api.read('v1/info')[:data]
275
- Log.log.info(@api_info.to_s)
345
+ Log.dump(:api_info, @api_info)
346
+ if @options[:api_version].nil?
347
+ # web socket endpoint: by default use v2 (newer gateways), without base64 encoding
348
+ @options[:api_version] = API_V2
349
+ # is the latest supported? else revert to old api
350
+ @options[:api_version] = API_V1 unless @api_info['endpoints'].any?{|i|i.include?(@options[:api_version])}
351
+ end
352
+ @options.freeze
353
+ Log.dump(:final_options, @options)
276
354
  end
277
355
  end # AgentHttpgw
278
356
  end
@@ -76,10 +76,13 @@ module Aspera
76
76
  transfer_spec.delete('EX_ssh_key_paths')
77
77
  end
78
78
  end
79
- if transfer_spec['tags'].is_a?(Hash) && transfer_spec['tags']['aspera'].is_a?(Hash)
80
- transfer_spec['tags']['aspera']['xfer_retry'] ||= 150
79
+ # add mandatory retry parameter for node api
80
+ ts_tags = transfer_spec['tags']
81
+ if ts_tags.is_a?(Hash) && ts_tags[Fasp::TransferSpec::TAG_RESERVED].is_a?(Hash)
82
+ ts_tags[Fasp::TransferSpec::TAG_RESERVED]['xfer_retry'] ||= 150
81
83
  end
82
- # Optimization in case of sending to the same node (TODO: probably remove this, as /etc/hosts shall be used for that)
84
+ # Optimization in case of sending to the same node
85
+ # TODO: probably remove this, as /etc/hosts shall be used for that
83
86
  if !transfer_spec['wss_enabled'] && transfer_spec['remote_host'].eql?(URI.parse(node_api_.params[:base_url]).host)
84
87
  transfer_spec['remote_host'] = '127.0.0.1'
85
88
  end
@@ -116,9 +119,16 @@ module Aspera
116
119
  else
117
120
  notify_progress(@transfer_id, transfer_data['bytes_transferred'])
118
121
  end
122
+ when 'failed'
123
+ # Bug in HSTS ? transfer is marked failed, but there is no reason
124
+ if transfer_data['error_code'].eql?(0) && transfer_data['error_desc'].empty?
125
+ notify_end(@transfer_id)
126
+ break
127
+ end
128
+ raise Fasp::Error, "status: #{transfer_data['status']}. code: #{transfer_data['error_code']}. description: #{transfer_data['error_desc']}"
119
129
  else
120
130
  Log.log.warn{"transfer_data -> #{transfer_data}"}
121
- raise Fasp::Error, "#{transfer_data['status']}: #{transfer_data['error_desc']}"
131
+ raise Fasp::Error, "status: #{transfer_data['status']}. code: #{transfer_data['error_code']}. description: #{transfer_data['error_desc']}"
122
132
  end
123
133
  sleep(1)
124
134
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # cspell:words Transfersdk
4
+
3
5
  require 'aspera/fasp/agent_base'
4
6
  require 'aspera/fasp/installation'
5
7
  require 'json'
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # cspell:words mnemo PROTO RCVR NOLIC PMTU BRTT VLINK BWMEAS SSEAR FIPS
4
+
3
5
  module Aspera
4
6
  module Fasp
5
7
  # from https://www.google.com/search?q=FASP+error+codes
@@ -62,6 +62,7 @@ module Aspera
62
62
 
63
63
  # location of SDK files
64
64
  def sdk_folder=(v)
65
+ Log.log.debug{"sdk_folder=#{v}"}
65
66
  @sdk_dir = v
66
67
  sdk_folder
67
68
  end
@@ -125,7 +126,11 @@ module Aspera
125
126
  end
126
127
 
127
128
  # all ascp files (in SDK)
128
- FILES = %i[ascp ascp4 ssh_bypass_key_dsa ssh_bypass_key_rsa aspera_license aspera_conf fallback_cert fallback_key].freeze
129
+ FILES = %i[ascp ascp4 ssh_bypass_dsa_privkey ssh_bypass_rsa_privkey aspera_license aspera_conf fallback_certificate fallback_cert_privkey].freeze
130
+
131
+ def check_or_create_sdk_file(filename, force: false, &block)
132
+ return Environment.write_file_restricted(File.join(sdk_folder, filename), force: force, mode: 0o644, &block)
133
+ end
129
134
 
130
135
  # get path of one resource file of currently activated product
131
136
  # keys and certs are generated locally... (they are well known values, arch. independent)
@@ -138,23 +143,18 @@ module Aspera
138
143
  file = file.gsub('ascp', 'ascp4') if k.eql?(:ascp4)
139
144
  when :transferd
140
145
  file = transferd_filepath
141
- when :ssh_bypass_key_dsa
142
- file = Environment.write_file_restricted(File.join(sdk_folder, 'aspera_bypass_dsa.pem')) {get_key('dsa', 1)}
143
- when :ssh_bypass_key_rsa
144
- file = Environment.write_file_restricted(File.join(sdk_folder, 'aspera_bypass_rsa.pem')) {get_key('rsa', 2)}
146
+ when :ssh_bypass_dsa_privkey
147
+ file = check_or_create_sdk_file('aspera_bypass_dsa.pem') {get_key('dsa', 1)}
148
+ when :ssh_bypass_rsa_privkey
149
+ file = check_or_create_sdk_file('aspera_bypass_rsa.pem') {get_key('rsa', 2)}
145
150
  when :aspera_license
146
- file = Environment.write_file_restricted(File.join(sdk_folder, 'aspera-license')) do
147
- clear = [
148
- Zlib::Inflate.inflate(DataRepository.instance.data(6)),
149
- "==SIGNATURE==\n",
150
- Base64.strict_encode64(DataRepository.instance.data(7))
151
- ]
152
- Base64.strict_encode64(clear.join)
151
+ file = check_or_create_sdk_file('aspera-license') do
152
+ Zlib::Inflate.inflate(DataRepository.instance.data(6))
153
153
  end
154
154
  when :aspera_conf
155
- file = Environment.write_file_restricted(File.join(sdk_folder, 'aspera.conf')) {DEFAULT_ASPERA_CONF}
156
- when :fallback_cert, :fallback_key
157
- file_key = File.join(sdk_folder, 'aspera_fallback_key.pem')
155
+ file = check_or_create_sdk_file('aspera.conf') {DEFAULT_ASPERA_CONF}
156
+ when :fallback_certificate, :fallback_cert_privkey
157
+ file_key = File.join(sdk_folder, 'aspera_fallback_cert_private_key.pem')
158
158
  file_cert = File.join(sdk_folder, 'aspera_fallback_cert.pem')
159
159
  if !File.exist?(file_key) || !File.exist?(file_cert)
160
160
  require 'openssl'
@@ -168,10 +168,10 @@ module Aspera
168
168
  cert.serial = 0x0
169
169
  cert.version = 2
170
170
  cert.sign(private_key, OpenSSL::Digest.new('SHA1'))
171
- Environment.write_file_restricted(file_key, force: true) {private_key.to_pem}
172
- Environment.write_file_restricted(file_cert, force: true) {cert.to_pem}
171
+ check_or_create_sdk_file('aspera_fallback_cert_private_key.pem', force: true) {private_key.to_pem}
172
+ check_or_create_sdk_file('aspera_fallback_cert.pem', force: true) {cert.to_pem}
173
173
  end
174
- file = k.eql?(:fallback_cert) ? file_cert : file_key
174
+ file = k.eql?(:fallback_certificate) ? file_cert : file_key
175
175
  else
176
176
  raise "INTERNAL ERROR: #{k}"
177
177
  end
@@ -205,7 +205,7 @@ module Aspera
205
205
  end
206
206
 
207
207
  def bypass_keys
208
- return %i[ssh_bypass_key_dsa ssh_bypass_key_rsa].map{|i|Installation.instance.path(i)}
208
+ return %i[ssh_bypass_dsa_privkey ssh_bypass_rsa_privkey].map{|i|Installation.instance.path(i)}
209
209
  end
210
210
 
211
211
  # use in plugin `config`
@@ -3,6 +3,7 @@
3
3
  require 'aspera/log'
4
4
  require 'aspera/command_line_builder'
5
5
  require 'aspera/temp_file_manager'
6
+ require 'aspera/fasp/error'
6
7
  require 'securerandom'
7
8
  require 'base64'
8
9
  require 'json'
@@ -19,6 +20,7 @@ module Aspera
19
20
  # Short names of columns in manual
20
21
  SUPPORTED_AGENTS_SHORT = SUPPORTED_AGENTS.map{|a|a.to_s[0].to_sym}
21
22
  FILE_LIST_OPTIONS = ['--file-list', '--file-pair-list'].freeze
23
+ SUPPORTED_OPTIONS = %i[ascp_args wss].freeze
22
24
 
23
25
  private_constant :SUPPORTED_AGENTS, :FILE_LIST_OPTIONS
24
26
 
@@ -38,8 +40,9 @@ module Aspera
38
40
  return @param_description_cache
39
41
  end
40
42
 
43
+ # @param to_text [bool] replace HTML entities with text equivalent
41
44
  # @return a table suitable to display in manual
42
- def man_table(to_text: true)
45
+ def man_table
43
46
  result = []
44
47
  description.each do |name, options|
45
48
  param = {name: name, type: [options[:accepted_types]].flatten.join(','), description: options[:desc]}
@@ -70,10 +73,17 @@ module Aspera
70
73
  if options.key?(:enum)
71
74
  param[:description] += "\nAllowed values: #{options[:enum].join(', ')}"
72
75
  end
73
- param[:description] = param[:description].gsub('&sol;', '/') if to_text
76
+ # replace "solidus" HTML entity with its text value
77
+ param[:description] = param[:description].gsub('&sol;', '\\')
74
78
  result.push(param)
75
79
  end
76
- return result
80
+ return result.sort do |a, b|
81
+ if a[:name].start_with?('EX_').eql?(b[:name].start_with?('EX_'))
82
+ a[:name] <=> b[:name]
83
+ else
84
+ b[:name] <=> a[:name]
85
+ end
86
+ end
77
87
  end
78
88
 
79
89
  # special encoding methods used in YAML (key: :convert)
@@ -95,10 +105,6 @@ module Aspera
95
105
  ts.key?('EX_file_pair_list')
96
106
  end
97
107
 
98
- def ts_to_env_args(transfer_spec, wss:, ascp_args:)
99
- return Parameters.new(transfer_spec, wss: wss, ascp_args: ascp_args).ascp_args
100
- end
101
-
102
108
  # temp file list files are created here
103
109
  def file_list_folder=(v)
104
110
  @file_list_folder = v
@@ -112,19 +118,20 @@ module Aspera
112
118
  end # self
113
119
 
114
120
  # @param options [Hash] key: :wss: bool, :ascp_args: array of strings
115
- def initialize(job_spec, wss:, ascp_args:)
121
+ def initialize(job_spec, options)
116
122
  @job_spec = job_spec
117
- @opt_wss = wss
118
- @opt_args = ascp_args
119
- Log.log.debug{"agent options: #{@opt_wss} #{@opt_args}"}
120
- raise 'ascp args must be an Array' unless @opt_args.is_a?(Array)
121
- raise 'ascp args must be an Array of String' if @opt_args.any?{|i|!i.is_a?(String)}
123
+ # check necessary options
124
+ raise 'Internal: missing options' unless (SUPPORTED_OPTIONS - options.keys).empty?
125
+ @options = SUPPORTED_OPTIONS.each_with_object({}){|o, h| h[o] = options[o]}
126
+ Log.dump(:options, @options)
127
+ raise 'ascp args must be an Array' unless @options[:ascp_args].is_a?(Array)
128
+ raise 'ascp args must be an Array of String' if @options[:ascp_args].any?{|i|!i.is_a?(String)}
122
129
  @builder = Aspera::CommandLineBuilder.new(@job_spec, self.class.description)
123
130
  end
124
131
 
125
132
  def process_file_list
126
133
  # is the file list provided through EX_ parameters?
127
- ascp_file_list_provided = self.class.ts_has_ascp_file_list(@job_spec, @opt_args)
134
+ ascp_file_list_provided = self.class.ts_has_ascp_file_list(@job_spec, @options[:ascp_args])
128
135
  # set if paths is mandatory in ts
129
136
  @builder.params_definition['paths'][:mandatory] = !@job_spec.key?('keepalive') && !ascp_file_list_provided
130
137
  # get paths in transfer spec (after setting if it is mandatory)
@@ -146,12 +153,14 @@ module Aspera
146
153
  Log.log.debug('placing source file list on command line (no file list file)')
147
154
  @builder.add_command_line_options(ts_paths_array.map{|i|i['source']})
148
155
  else
156
+ raise "All elements of paths must have a 'source' key" unless ts_paths_array.all?{|i|i.key?('source')}
157
+ is_pair_list = ts_paths_array.any?{|i|i.key?('destination')}
158
+ raise "All elements of paths must be consistent with 'destination' key" if is_pair_list && !ts_paths_array.all?{|i|i.key?('destination')}
149
159
  # safer option: generate a file list file if there is storage defined for it
150
- # if there is destination in paths, then use file-pair-list
151
- # TODO: well, we test only the first one, but anyway it shall be consistent
152
- if ts_paths_array.first.key?('destination')
160
+ # if there is one destination in paths, then use file-pair-list
161
+ if is_pair_list
153
162
  option = '--file-pair-list'
154
- lines = ts_paths_array.each_with_object([]){|e, m|m.push(e['source'], e['destination']); }
163
+ lines = ts_paths_array.each_with_object([]){|e, m|m.push(e['source'], e['destination'] || e['source']); }
155
164
  else
156
165
  option = '--file-list'
157
166
  lines = ts_paths_array.map{|i|i['source']}
@@ -186,7 +195,7 @@ module Aspera
186
195
  @job_spec.delete('source_root') if @job_spec.key?('source_root') && @job_spec['source_root'].empty?
187
196
 
188
197
  # use web socket session initiation ?
189
- if @builder.read_param('wss_enabled') && (@opt_wss || !@job_spec.key?('fasp_port'))
198
+ if @builder.read_param('wss_enabled') && (@options[:wss] || !@job_spec.key?('fasp_port'))
190
199
  # by default use web socket session if available, unless removed by user
191
200
  @builder.add_command_line_options(['--ws-connect'])
192
201
  # TODO: option to give order ssh,ws (legacy http is implied bu ssh)
@@ -218,7 +227,7 @@ module Aspera
218
227
  process_file_list
219
228
  # optional args, at the end to override previous ones (to allow override)
220
229
  @builder.add_command_line_options(@builder.read_param('EX_ascp_args'))
221
- @builder.add_command_line_options(@opt_args)
230
+ @builder.add_command_line_options(@options[:ascp_args])
222
231
  # process destination folder
223
232
  destination_folder = @builder.read_param('destination_root') || '/'
224
233
  # ascp4 does not support base64 encoding of destination
@@ -8,6 +8,7 @@
8
8
  # cli.switch : ascp: switch for ascp command line
9
9
  # cli.convert : ascp: transform value: either a Hash with conversion values, or name of class
10
10
  # cli.variable : ascp: name of env var
11
+ # cspell:words dgram
11
12
  ---
12
13
  cipher:
13
14
  :desc: "In transit encryption type."
@@ -559,7 +560,8 @@ remove_empty_source_directory:
559
560
  :cli:
560
561
  :type: :opt_without_arg
561
562
  EX_at_rest_password:
562
- :desc: "DEPRECATED: Prefer to use standard parameter: content_protection_password"
563
+ :desc: Content protection password
564
+ :deprecation: "Use standard spec parameter: content_protection_password"
563
565
  :agents:
564
566
  - :direct
565
567
  :cli:
@@ -568,7 +570,7 @@ EX_at_rest_password:
568
570
  EX_proxy_password:
569
571
  :desc: |-
570
572
  Password used for Aspera proxy server authentication.
571
- May be overridden by password in URL EX_fasp_proxy_url.
573
+ May be overridden by password in URL provided in parameter: proxy.
572
574
 
573
575
 
574
576
  :agents:
@@ -703,6 +705,7 @@ EX_file_pair_list:
703
705
  :switch: "--file-pair-list"
704
706
  EX_ascp_args:
705
707
  :desc: Add native command line arguments to ascp
708
+ :deprecation: Use parameter ascp_args in option transfer_info
706
709
  :accepted_types: :array
707
710
  :agents:
708
711
  - :direct
@@ -49,11 +49,11 @@ module Aspera
49
49
  # failure in ascp
50
50
  if e.retryable?
51
51
  # exit if we exceed the max number of retry
52
- raise Fasp::Error, 'Maximum number of retry reached' if remaining_resumes <= 0
52
+ raise Fasp::Error, "Maximum number of retry reached (#{@parameters[:iter_max]})" if remaining_resumes <= 0
53
53
  else
54
54
  # give one chance only to non retryable errors
55
55
  unless remaining_resumes.eql?(@parameters[:iter_max])
56
- Log.log.error('non-retryable error')
56
+ Log.log.error('non-retryable error'.red.blink)
57
57
  raise e
58
58
  end
59
59
  end
@@ -61,7 +61,7 @@ module Aspera
61
61
 
62
62
  # take this retry in account
63
63
  remaining_resumes -= 1
64
- Log.log.warn{"resuming in #{sleep_seconds} seconds (retry left:#{remaining_resumes})"}
64
+ Log.log.warn{"Resuming in #{sleep_seconds} seconds (retry left:#{remaining_resumes})"}
65
65
 
66
66
  # wait a bit before retrying, maybe network condition will be better
67
67
  sleep(sleep_seconds)
@@ -15,11 +15,14 @@ module Aspera
15
15
  'ssh_port' => SSH_PORT,
16
16
  'fasp_port' => UDP_PORT
17
17
  }.freeze
18
- # define constants for enums of parameters: <paramater>_<enum>, e.g. CIPHER_AES_128
19
- Aspera::Fasp::Parameters.description.each do |k, v|
20
- next unless v[:enum].is_a?(Array)
21
- v[:enum].each do |enum|
22
- TransferSpec.const_set("#{k.to_s.upcase}_#{enum.upcase.gsub(/[^A-Z0-9]/, '_')}", enum.freeze)
18
+ # reserved tag for Aspera
19
+ TAG_RESERVED = 'aspera'
20
+ # define constants for enums of parameters: <parameter>_<enum>, e.g. CIPHER_AES_128, DIRECTION_SEND, ...
21
+ Aspera::Fasp::Parameters.description.each do |name, description|
22
+ next unless description[:enum].is_a?(Array)
23
+ TransferSpec.const_set("#{name.to_s.upcase}_ENUM_VALUES", description[:enum])
24
+ description[:enum].each do |enum|
25
+ TransferSpec.const_set("#{name.to_s.upcase}_#{enum.upcase.gsub(/[^A-Z0-9]/, '_')}", enum.freeze)
23
26
  end
24
27
  end
25
28
  class << self