aspera-cli 4.13.0 → 4.15.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 (99) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +81 -7
  4. data/CONTRIBUTING.md +22 -6
  5. data/README.md +2038 -1080
  6. data/bin/ascli +18 -9
  7. data/bin/asession +12 -14
  8. data/examples/dascli +1 -1
  9. data/examples/proxy.pac +1 -1
  10. data/examples/rubyc +24 -0
  11. data/lib/aspera/aoc.rb +219 -159
  12. data/lib/aspera/ascmd.rb +25 -14
  13. data/lib/aspera/cli/basic_auth_plugin.rb +12 -9
  14. data/lib/aspera/cli/error.rb +17 -0
  15. data/lib/aspera/cli/extended_value.rb +47 -12
  16. data/lib/aspera/cli/formatter.rb +260 -179
  17. data/lib/aspera/cli/hints.rb +80 -0
  18. data/lib/aspera/cli/main.rb +104 -156
  19. data/lib/aspera/cli/manager.rb +259 -209
  20. data/lib/aspera/cli/plugin.rb +123 -63
  21. data/lib/aspera/cli/plugins/alee.rb +2 -3
  22. data/lib/aspera/cli/plugins/aoc.rb +341 -261
  23. data/lib/aspera/cli/plugins/ats.rb +22 -21
  24. data/lib/aspera/cli/plugins/bss.rb +5 -5
  25. data/lib/aspera/cli/plugins/config.rb +578 -627
  26. data/lib/aspera/cli/plugins/console.rb +44 -6
  27. data/lib/aspera/cli/plugins/cos.rb +15 -17
  28. data/lib/aspera/cli/plugins/faspex.rb +114 -100
  29. data/lib/aspera/cli/plugins/faspex5.rb +411 -264
  30. data/lib/aspera/cli/plugins/node.rb +354 -259
  31. data/lib/aspera/cli/plugins/orchestrator.rb +61 -29
  32. data/lib/aspera/cli/plugins/preview.rb +82 -90
  33. data/lib/aspera/cli/plugins/server.rb +79 -32
  34. data/lib/aspera/cli/plugins/shares.rb +55 -42
  35. data/lib/aspera/cli/sync_actions.rb +68 -0
  36. data/lib/aspera/cli/transfer_agent.rb +66 -73
  37. data/lib/aspera/cli/transfer_progress.rb +74 -0
  38. data/lib/aspera/cli/version.rb +1 -1
  39. data/lib/aspera/colors.rb +12 -8
  40. data/lib/aspera/command_line_builder.rb +14 -11
  41. data/lib/aspera/cos_node.rb +3 -2
  42. data/lib/aspera/data/6 +0 -0
  43. data/lib/aspera/environment.rb +24 -9
  44. data/lib/aspera/fasp/agent_aspera.rb +126 -0
  45. data/lib/aspera/fasp/agent_base.rb +31 -77
  46. data/lib/aspera/fasp/agent_connect.rb +25 -21
  47. data/lib/aspera/fasp/agent_direct.rb +89 -103
  48. data/lib/aspera/fasp/agent_httpgw.rb +231 -149
  49. data/lib/aspera/fasp/agent_node.rb +41 -34
  50. data/lib/aspera/fasp/agent_trsdk.rb +75 -32
  51. data/lib/aspera/fasp/error_info.rb +4 -2
  52. data/lib/aspera/fasp/faux_file.rb +52 -0
  53. data/lib/aspera/fasp/installation.rb +53 -195
  54. data/lib/aspera/fasp/management.rb +244 -0
  55. data/lib/aspera/fasp/parameters.rb +71 -37
  56. data/lib/aspera/fasp/parameters.yaml +76 -8
  57. data/lib/aspera/fasp/products.rb +162 -0
  58. data/lib/aspera/fasp/resume_policy.rb +3 -3
  59. data/lib/aspera/fasp/transfer_spec.rb +7 -6
  60. data/lib/aspera/fasp/uri.rb +26 -24
  61. data/lib/aspera/faspex_gw.rb +2 -2
  62. data/lib/aspera/faspex_postproc.rb +2 -2
  63. data/lib/aspera/hash_ext.rb +14 -4
  64. data/lib/aspera/json_rpc.rb +49 -0
  65. data/lib/aspera/keychain/macos_security.rb +13 -13
  66. data/lib/aspera/line_logger.rb +23 -0
  67. data/lib/aspera/log.rb +58 -16
  68. data/lib/aspera/node.rb +157 -92
  69. data/lib/aspera/oauth.rb +37 -19
  70. data/lib/aspera/open_application.rb +4 -4
  71. data/lib/aspera/persistency_action_once.rb +1 -1
  72. data/lib/aspera/persistency_folder.rb +2 -2
  73. data/lib/aspera/preview/file_types.rb +4 -2
  74. data/lib/aspera/preview/generator.rb +22 -35
  75. data/lib/aspera/preview/options.rb +2 -0
  76. data/lib/aspera/preview/terminal.rb +73 -16
  77. data/lib/aspera/preview/utils.rb +21 -28
  78. data/lib/aspera/proxy_auto_config.js +2 -2
  79. data/lib/aspera/rest.rb +136 -68
  80. data/lib/aspera/rest_call_error.rb +1 -1
  81. data/lib/aspera/rest_error_analyzer.rb +15 -14
  82. data/lib/aspera/rest_errors_aspera.rb +37 -34
  83. data/lib/aspera/secret_hider.rb +18 -15
  84. data/lib/aspera/ssh.rb +5 -2
  85. data/lib/aspera/sync.rb +127 -119
  86. data/lib/aspera/temp_file_manager.rb +10 -3
  87. data/lib/aspera/web_auth.rb +10 -7
  88. data/lib/aspera/web_server_simple.rb +9 -4
  89. data.tar.gz.sig +0 -0
  90. metadata +34 -17
  91. metadata.gz.sig +0 -0
  92. data/docs/test_env.conf +0 -186
  93. data/lib/aspera/cli/listener/line_dump.rb +0 -19
  94. data/lib/aspera/cli/listener/logger.rb +0 -22
  95. data/lib/aspera/cli/listener/progress.rb +0 -50
  96. data/lib/aspera/cli/listener/progress_multi.rb +0 -84
  97. data/lib/aspera/cli/plugins/sync.rb +0 -44
  98. data/lib/aspera/data/7 +0 -0
  99. data/lib/aspera/fasp/listener.rb +0 -13
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'aspera/fasp/agent_base'
4
4
  require 'aspera/fasp/transfer_spec'
5
+ require 'aspera/fasp/faux_file'
5
6
  require 'aspera/log'
6
7
  require 'aspera/rest'
7
8
  require 'securerandom'
@@ -9,44 +10,147 @@ require 'websocket'
9
10
  require 'base64'
10
11
  require 'json'
11
12
 
12
- # ref: https://api.ibm.com/explorer/catalog/aspera/product/ibm-aspera/api/http-gateway-api/doc/guides-toc
13
- # https://developer.ibm.com/apis/catalog?search=%22aspera%20http%22
14
13
  module Aspera
15
14
  module Fasp
16
- # start a transfer using Aspera HTTP Gateway, using web socket session for uploads
15
+ # Start a transfer using Aspera HTTP Gateway, using web socket secure for uploads
16
+ # ref: https://api.ibm.com/explorer/catalog/aspera/product/ibm-aspera/api/http-gateway-api/doc/guides-toc
17
+ # https://developer.ibm.com/apis/catalog?search=%22aspera%20http%22
18
+ # HTTP GW Upload protocol:
19
+ # # type Contents Ack Counter
20
+ # v1
21
+ # 0 JSON.transfer_spec Transfer Spec "end upload" sent_general
22
+ # 1.. JSON.slice_upload File base64 chunks "end upload" sent_general
23
+ # v2
24
+ # 0 JSON.transfer_spec Transfer Spec "end upload" sent_general
25
+ # 1 JSON.slice_upload File start "end_slice_upload" sent_v2_delimiter
26
+ # 2.. Binary File binary chunks "end upload" sent_general
27
+ # last JSON.slice_upload File end "end_slice_upload" sent_v2_delimiter
17
28
  class AgentHttpgw < Aspera::Fasp::AgentBase
18
- # message returned by HTTP GW in case of success
19
- MSG_END_UPLOAD = 'end upload'
20
- MSG_END_SLICE = 'end_slice_upload'
29
+ MSG_SEND_TRANSFER_SPEC = 'transfer_spec'
30
+ MSG_SEND_SLICE_UPLOAD = 'slice_upload'
31
+ MSG_RECV_DATA_RECEIVED_SIGNAL = 'end upload'
32
+ MSG_RECV_SLICE_UPLOAD_SIGNAL = 'end_slice_upload'
33
+ # upload API versions
34
+ API_V1 = 'v1'
35
+ API_V2 = 'v2'
21
36
  # options available in CLI (transfer_info)
22
37
  DEFAULT_OPTIONS = {
23
- url: nil,
24
- upload_chunk_size: 64_000,
25
- upload_bar_refresh_sec: 0.5
38
+ url: :required,
39
+ upload_chunk_size: 64_000,
40
+ api_version: API_V2,
41
+ synchronous: false
26
42
  }.freeze
27
43
  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
44
+ THR_RECV = 'recv'
45
+ LOG_WS_SEND = 'ws: send: '.red
46
+ LOG_WS_RECV = "ws: #{THR_RECV}: ".green
47
+ private_constant :DEFAULT_OPTIONS, :MSG_RECV_DATA_RECEIVED_SIGNAL, :MSG_RECV_SLICE_UPLOAD_SIGNAL, :API_V1, :API_V2
32
48
 
33
49
  # 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))
50
+ def ws_snd_json(msg_type, payload)
51
+ if msg_type.eql?(MSG_SEND_SLICE_UPLOAD) && @options[:api_version].eql?(API_V2)
52
+ @shared_info[:count][:sent_v2_delimiter] += 1
53
+ else
54
+ @shared_info[:count][:sent_general] += 1
55
+ end
56
+ Log.log.debug do
57
+ log_data = payload.dup
58
+ log_data[:data] = "[data #{log_data[:data].length} bytes]" if log_data.key?(:data)
59
+ "#{LOG_WS_SEND}json: #{msg_type}: #{JSON.generate(log_data)}"
60
+ end
61
+ ws_send(ws_type: :text, data: JSON.generate({msg_type => payload}))
62
+ end
63
+
64
+ # send data on http gw web socket
65
+ def ws_send(ws_type:, data:)
66
+ Log.log.debug{"#{LOG_WS_SEND}sending: #{ws_type} (#{data&.length || 0} bytes)"}
67
+ @shared_info[:count][:sent_general] += 1 if ws_type.eql?(:binary)
68
+ frame_generator = ::WebSocket::Frame::Outgoing::Client.new(data: data, type: ws_type, version: @ws_handshake.version)
69
+ @ws_io.write(frame_generator.to_s)
70
+ if @options[:synchronous]
71
+ @shared_info[:mutex].synchronize do
72
+ # if read thread exited, there will be no more updates
73
+ # we allow for 1 of difference else it stays blocked
74
+ while @ws_read_thread.alive? &&
75
+ @shared_info[:read_exception].nil? &&
76
+ (((@shared_info[:count][:sent_general] - @shared_info[:count][:received_general]) > 1) ||
77
+ ((@shared_info[:count][:received_v2_delimiter] - @shared_info[:count][:sent_v2_delimiter]) > 1))
78
+ if !@shared_info[:cond_var].wait(@shared_info[:mutex], 2.0)
79
+ Log.log.debug{"#{LOG_WS_SEND}#{'timeout'.blue}: #{@shared_info[:count]}"}
80
+ end
81
+ end
82
+ end
83
+ end
84
+ raise @shared_info[:read_exception] unless @shared_info[:read_exception].nil?
85
+ Log.log.debug{"#{LOG_WS_SEND}counts: #{@shared_info[:count]}"}
86
+ end
87
+
88
+ # message processing for read thread
89
+ def process_received_message(message)
90
+ Log.log.debug{"#{LOG_WS_RECV}message: [#{message}] (#{message.class})"}
91
+ if message.eql?(MSG_RECV_DATA_RECEIVED_SIGNAL)
92
+ @shared_info[:mutex].synchronize do
93
+ @shared_info[:count][:received_general] += 1
94
+ @shared_info[:cond_var].signal
95
+ end
96
+ elsif message.eql?(MSG_RECV_SLICE_UPLOAD_SIGNAL)
97
+ @shared_info[:mutex].synchronize do
98
+ @shared_info[:count][:received_v2_delimiter] += 1
99
+ @shared_info[:cond_var].signal
100
+ end
101
+ else
102
+ message.chomp!
103
+ error_message =
104
+ if message.start_with?('"') && message.end_with?('"')
105
+ # remove double quotes : 1..-2
106
+ JSON.parse(Base64.strict_decode64(message.chomp[1..-2]))['message']
107
+ elsif message.start_with?('{') && message.end_with?('}')
108
+ JSON.parse(message)['message']
109
+ else
110
+ "unknown message from gateway: [#{message}]"
111
+ end
112
+ raise error_message
113
+ end
38
114
  end
39
115
 
40
- def ws_send(data, type: :text)
41
- frame = ::WebSocket::Frame::Outgoing::Client.new(data: data, type: type, version: @ws_handshake.version)
42
- @ws_io.write(frame.to_s)
116
+ # main function of read thread
117
+ def process_read_thread
118
+ Log.log.debug{"#{LOG_WS_RECV}read thread started"}
119
+ frame_parser = ::WebSocket::Frame::Incoming::Client.new(version: @ws_handshake.version)
120
+ until @ws_io.eof?
121
+ begin # rubocop:disable Style/RedundantBegin
122
+ # ready byte by byte until frame is ready
123
+ # blocking read
124
+ byte = @ws_io.read(1)
125
+ Log.log.trace1{"#{LOG_WS_RECV}read: #{byte} (#{byte.class}) eof=#{@ws_io.eof?}"}
126
+ frame_parser << byte
127
+ frame_ok = frame_parser.next
128
+ next if frame_ok.nil?
129
+ process_received_message(frame_ok.data.to_s)
130
+ Log.log.debug{"#{LOG_WS_RECV}counts: #{@shared_info[:count]}"}
131
+ rescue => e
132
+ Log.log.debug{"#{LOG_WS_RECV}Exception: #{e}"}
133
+ @shared_info[:mutex].synchronize do
134
+ @shared_info[:read_exception] = e
135
+ @shared_info[:cond_var].signal
136
+ end
137
+ break
138
+ end # begin/rescue
139
+ end # loop
140
+ Log.log.debug do
141
+ "#{LOG_WS_RECV}exception: #{@shared_info[:read_exception]},cls=#{@shared_info[:read_exception].class})"
142
+ end unless @shared_info[:read_exception].nil?
143
+ Log.log.debug{"#{LOG_WS_RECV}read thread stopped (ws eof=#{@ws_io.eof?})"}
43
144
  end
44
145
 
45
146
  def upload(transfer_spec)
147
+ # identify this session uniquely
148
+ session_id = SecureRandom.uuid
149
+ notify_progress(session_id: nil, type: :pre_start, info: 'starting')
46
150
  # total size of all files
47
- total_size = 0
151
+ total_bytes_to_transfer = 0
48
152
  # we need to keep track of actual file path because transfer spec is modified to be sent in web socket
49
- source_paths = []
153
+ files_to_read = []
50
154
  # get source root or nil
51
155
  source_root = transfer_spec.key?('source_root') && !transfer_spec['source_root'].empty? ? transfer_spec['source_root'] : nil
52
156
  # source root is ignored by GW, used only here
@@ -55,145 +159,122 @@ module Aspera
55
159
  # modify transfer spec to be suitable for GW
56
160
  transfer_spec['paths'].each do |item|
57
161
  # save actual file location to be able read contents later
58
- full_src_filepath = item['source']
59
- # add source root if needed
60
- full_src_filepath = File.join(source_root, full_src_filepath) unless source_root.nil?
61
- # GW expects a simple file name in 'source' but if user wants to change the name, we take it
62
- item['source'] = File.basename(item['destination'].nil? ? item['source'] : item['destination'])
63
- item['file_size'] = File.size(full_src_filepath)
64
- total_size += item['file_size']
162
+ file_to_add = FauxFile.open(item['source'])
163
+ if file_to_add
164
+ item['source'] = file_to_add.path
165
+ item['file_size'] = file_to_add.size
166
+ else
167
+ file_to_add = item['source']
168
+ # add source root if needed
169
+ file_to_add = File.join(source_root, file_to_add) unless source_root.nil?
170
+ # GW expects a simple file name in 'source' but if user wants to change the name, we take it
171
+ item['source'] = File.basename(item['destination'].nil? ? item['source'] : item['destination'])
172
+ item['file_size'] = File.size(file_to_add)
173
+ end
65
174
  # save so that we can actually read the file later
66
- source_paths.push(full_src_filepath)
175
+ files_to_read.push(file_to_add)
176
+ total_bytes_to_transfer += item['file_size']
67
177
  end
68
- # identify this session uniquely
69
- 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)
178
+ upload_url = File.join(@gw_api.params[:base_url], @options[:api_version], 'upload')
179
+ notify_progress(session_id: nil, type: :pre_start, info: 'connecting wss')
78
180
  # open web socket to end point (equivalent to Net::HTTP.start)
79
- http_socket = Rest.start_http_session(url)
80
- @ws_io = http_socket.instance_variable_get(:@socket)
81
- # @ws_io.debug_output = Log.log
82
- @ws_handshake = ::WebSocket::Handshake::Client.new(url: url, headers: {})
181
+ http_session = Rest.start_http_session(upload_url)
182
+ # get the underlying socket i/o
183
+ @ws_io = Rest.io_http_session(http_session)
184
+ @ws_handshake = ::WebSocket::Handshake::Client.new(url: upload_url, headers: {})
83
185
  @ws_io.write(@ws_handshake.to_s)
84
186
  sleep(0.1)
85
187
  @ws_handshake << @ws_io.readuntil("\r\n\r\n")
86
188
  raise 'Error in websocket handshake' unless @ws_handshake.finished?
87
- Log.log.debug('ws: handshake success')
189
+ Log.log.debug{"#{LOG_WS_SEND}handshake success"}
190
+ # start read thread after handshake
191
+ @ws_read_thread = Thread.new {process_read_thread}
192
+ notify_progress(session_id: session_id, type: :session_start)
193
+ notify_progress(session_id: session_id, type: :session_size, info: total_bytes_to_transfer)
194
+ sleep(1)
88
195
  # data shared between main thread and read thread
89
- shared_info = {
196
+ @shared_info = {
90
197
  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
198
+ count: {
199
+ sent_general: 0,
200
+ received_general: 0,
201
+ sent_v2_delimiter: 0,
202
+ received_v2_delimiter: 0
203
+ },
204
+ mutex: Mutex.new,
205
+ cond_var: ConditionVariable.new
94
206
  }
95
- # start read thread
96
- ws_read_thread = Thread.new do
97
- Log.log.debug('ws: thread: started')
98
- frame = ::WebSocket::Frame::Incoming::Client.new
99
- loop do
100
- begin # rubocop:disable Style/RedundantBegin
101
- frame << @ws_io.readuntil("\n")
102
- while (msg = frame.next)
103
- Log.log.debug{"ws: thread: message: #{msg.data} #{shared_info[:end_uploads]}"}
104
- message = msg.data
105
- if message.eql?(MSG_END_UPLOAD)
106
- shared_info[:end_uploads] += 1
107
- elsif message.eql?(MSG_END_SLICE)
108
- else
109
- message.chomp!
110
- error_message =
111
- if message.start_with?('"') && message.end_with?('"')
112
- JSON.parse(Base64.strict_decode64(message.chomp[1..-2]))['message']
113
- elsif message.start_with?('{') && message.end_with?('}')
114
- JSON.parse(message)['message']
115
- else
116
- "unknown message from gateway: [#{message}]"
117
- end
118
- raise error_message
119
- end
120
- end
121
- rescue => e
122
- shared_info[:read_exception] = e unless e.is_a?(EOFError)
123
- break
124
- end
125
- end
126
- Log.log.debug{"ws: thread: stopping (exc=#{shared_info[:read_exception]},cls=#{shared_info[:read_exception].class})"}
127
- end
128
207
  # notify progress bar
129
- notify_begin(session_id, total_size)
208
+ notify_progress(type: :session_size, session_id: session_id, info: total_bytes_to_transfer)
130
209
  # first step send transfer spec
131
- Log.dump(:ws_spec, transfer_spec)
132
- ws_snd_json(transfer_spec: transfer_spec)
210
+ Log.log.debug{Log.dump(:ws_spec, transfer_spec)}
211
+ ws_snd_json(MSG_SEND_TRANSFER_SPEC, transfer_spec)
133
212
  # current file index
134
213
  file_index = 0
135
214
  # aggregate size sent
136
- sent_bytes = 0
137
- # last progress event
138
- last_progress_time = nil
139
-
215
+ session_sent_bytes = 0
216
+ # process each file
140
217
  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
218
+ slice_info = {
219
+ name: nil,
220
+ # TODO: get mime type?
221
+ type: 'application/octet-stream',
222
+ size: item['file_size'],
223
+ slice: 0, # current slice index
224
+ # index of last slice (i.e number of slices - 1)
225
+ last_slice: (item['file_size'] - 1) / @options[:upload_chunk_size],
226
+ fileIndex: file_index
227
+ }
228
+ file = files_to_read[file_index]
229
+ if file.is_a?(FauxFile)
230
+ slice_info[:name] = file.path
231
+ else
232
+ file = File.open(file)
233
+ slice_info[:name] = File.basename(item[item['destination'].nil? ? 'source' : 'destination'])
234
+ end
235
+ begin
150
236
  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)
237
+ slice_bin_data = file.read(@options[:upload_chunk_size])
161
238
  # interrupt main thread if read thread failed
162
- raise shared_info[:read_exception] unless shared_info[:read_exception].nil?
239
+ raise @shared_info[:read_exception] unless @shared_info[:read_exception].nil?
163
240
  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)
241
+ if @options[:api_version].eql?(API_V1)
242
+ slice_info[:data] = Base64.strict_encode64(slice_bin_data)
243
+ ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info)
167
244
  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)
245
+ # send once, before data, at beginning
246
+ ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info) if slice_info[:slice].eql?(0)
247
+ ws_send(ws_type: :binary, data: slice_bin_data)
248
+ Log.log.debug{"#{LOG_WS_SEND}buffer: file: #{file_index}, slice: #{slice_info[:slice]}/#{slice_info[:last_slice]}"}
249
+ # send once, after data, at end
250
+ ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info) if slice_info[:slice].eql?(slice_info[:last_slice])
172
251
  end
173
252
  rescue Errno::EPIPE => e
174
- raise shared_info[:read_exception] unless shared_info[:read_exception].nil?
253
+ raise @shared_info[:read_exception] unless @shared_info[:read_exception].nil?
254
+ raise e
255
+ rescue Net::ReadTimeout => e
256
+ Log.log.warn{'A timeout condition using HTTPGW may signal a permission problem on destination. Check ascp logs on httpgw.'}
175
257
  raise e
176
258
  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
182
- end
183
- slice_index += 1
259
+ session_sent_bytes += slice_bin_data.length
260
+ notify_progress(type: :transfer, session_id: session_id, info: session_sent_bytes)
261
+ slice_info[:slice] += 1
184
262
  end
263
+ ensure
264
+ file.close
185
265
  end
186
266
  file_index += 1
187
- end
188
-
189
- Log.log.debug('Finished upload')
190
- ws_read_thread.join
191
- Log.log.debug{"result: #{shared_info[:end_uploads]} / #{@slice_uploads}"}
192
- ws_send(nil, type: :close) unless @ws_io.nil?
267
+ end # loop on files
268
+ # throttling may have skipped last one
269
+ notify_progress(type: :transfer, session_id: session_id, info: session_sent_bytes)
270
+ notify_progress(type: :end, session_id: session_id)
271
+ ws_send(ws_type: :close, data: nil)
272
+ Log.log.debug("Finished upload, waiting for end of #{THR_RECV} thread.")
273
+ @ws_read_thread.join
274
+ Log.log.debug{'Read thread joined'}
275
+ # session no more used
193
276
  @ws_io = nil
194
- http_socket&.finish
195
- notify_progress(session_id, sent_bytes)
196
- notify_end(session_id)
277
+ http_session&.finish
197
278
  end
198
279
 
199
280
  def download(transfer_spec)
@@ -232,7 +313,7 @@ module Aspera
232
313
  raise 'GW URL must be set' if @gw_api.nil?
233
314
  raise 'paths: must be Array' unless transfer_spec['paths'].is_a?(Array)
234
315
  raise 'only token based transfer is supported in GW' unless transfer_spec['token'].is_a?(String)
235
- Log.dump(:user_spec, transfer_spec)
316
+ Log.log.debug{Log.dump(:user_spec, transfer_spec)}
236
317
  transfer_spec['authentication'] ||= 'token'
237
318
  case transfer_spec['direction']
238
319
  when Fasp::TransferSpec::DIRECTION_SEND
@@ -247,32 +328,33 @@ module Aspera
247
328
  # wait for completion of all jobs started
248
329
  # @return list of :success or error message
249
330
  def wait_for_transfers_completion
331
+ # well ... transfer was done in "start"
250
332
  return [:success]
251
333
  end
252
334
 
253
- # terminates monitor thread
254
- def shutdown; end
255
-
335
+ # TODO: is that useful?
256
336
  def url=(api_url); end
257
337
 
258
338
  private
259
339
 
260
340
  def initialize(opts)
261
- Log.log.debug{"local options= #{opts}"}
262
- # set default options and override if specified
263
- @options = DEFAULT_OPTIONS.dup
264
- raise "httpgw agent parameters (transfer_info): expecting Hash, but have #{opts.class}" unless opts.is_a?(Hash)
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)
267
- @options[k] = v
268
- end
269
- raise 'missing param: url' if @options[:url].nil?
270
- # remove /v1 from end
341
+ super(opts)
342
+ @options = AgentBase.options(default: DEFAULT_OPTIONS, options: opts)
343
+ # remove /v1 from end of user-provided GW url: we need the base url only
271
344
  @options[:url].gsub(%r{/v1/*$}, '')
272
- super()
273
345
  @gw_api = Rest.new({base_url: @options[:url]})
274
346
  @api_info = @gw_api.read('v1/info')[:data]
275
- Log.log.info(@api_info.to_s)
347
+ Log.log.debug{Log.dump(:api_info, @api_info)}
348
+ # web socket endpoint: by default use v2 (newer gateways), without base64 encoding
349
+ # is the latest supported? else revert to old api
350
+ if !@options[:api_version].eql?(API_V1)
351
+ if !@api_info['endpoints'].any?{|i|i.include?(@options[:api_version])}
352
+ Log.log.warn{"API version #{@options[:api_version]} not supported, reverting to #{API_V1}"}
353
+ @options[:api_version] = API_V1
354
+ end
355
+ end
356
+ @options.freeze
357
+ Log.log.debug{Log.dump(:agent_options, @options)}
276
358
  end
277
359
  end # AgentHttpgw
278
360
  end
@@ -1,32 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # cspell:ignore precalc
3
4
  require 'aspera/fasp/agent_base'
4
5
  require 'aspera/fasp/transfer_spec'
5
6
  require 'aspera/node'
6
7
  require 'aspera/log'
7
- require 'tty-spinner'
8
+ require 'aspera/oauth'
8
9
 
9
10
  module Aspera
10
11
  module Fasp
11
12
  # this singleton class is used by the CLI to provide a common interface to start a transfer
12
13
  # before using it, the use must set the `node_api` member.
13
14
  class AgentNode < Aspera::Fasp::AgentBase
15
+ DEFAULT_OPTIONS = {
16
+ url: :required,
17
+ username: :required,
18
+ password: :required,
19
+ root_id: nil
20
+ }.freeze
14
21
  # option include: root_id if the node is an access key
15
- attr_writer :options
22
+ # attr_writer :options
16
23
 
17
- def initialize(options)
18
- raise 'node specification must be Hash' unless options.is_a?(Hash)
19
- %i[url username password].each { |k| raise "missing parameter [#{k}] in node specification: #{options}" unless options.key?(k) }
20
- super()
24
+ def initialize(opts)
25
+ raise 'node specification must be Hash' unless opts.is_a?(Hash)
26
+ super(opts)
27
+ options = AgentBase.options(default: DEFAULT_OPTIONS, options: opts)
21
28
  # root id is required for access key
22
29
  @root_id = options[:root_id]
23
30
  rest_params = { base_url: options[:url]}
24
- if /^Bearer /.match?(options[:password])
25
- rest_params[:headers] = {
26
- Aspera::Node::HEADER_X_ASPERA_ACCESS_KEY => options[:username],
27
- 'Authorization' => options[:password]
28
- }
31
+ if Oauth.bearer?(options[:password])
29
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])
30
34
  else
31
35
  rest_params[:auth] = {
32
36
  type: :basic,
@@ -37,6 +41,7 @@ module Aspera
37
41
  @node_api = Rest.new(rest_params)
38
42
  # TODO: currently only supports one transfer. This is bad shortcut. but ok for CLI.
39
43
  @transfer_id = nil
44
+ # Log.log.debug{Log.dump(:agent_options, @options)}
40
45
  end
41
46
 
42
47
  # used internally to ensure node api is set before using.
@@ -45,7 +50,7 @@ module Aspera
45
50
  return @node_api
46
51
  end
47
52
  # use this to read the node_api end point.
48
- attr_reader :node_api
53
+ # attr_reader :node_api
49
54
 
50
55
  # use this to set the node_api end point before using the class.
51
56
  def node_api=(new_value)
@@ -94,43 +99,45 @@ module Aspera
94
99
 
95
100
  # generic method
96
101
  def wait_for_transfers_completion
97
- started = false
98
- spinner = nil
102
+ # set to true when we know the total size of the transfer
103
+ session_started = false
104
+ bytes_expected = nil
99
105
  # lets emulate management events to display progress bar
100
106
  loop do
101
107
  # status is empty sometimes with status 200...
102
- transfer_data = node_api_.read("ops/transfers/#{@transfer_id}")[:data] || {'status' => 'unknown'} rescue {'status' => 'waiting(read error)'}
108
+ transfer_data = node_api_.read("ops/transfers/#{@transfer_id}")[:data] || {'status' => 'unknown'} rescue {'status' => 'waiting(api error)'}
103
109
  case transfer_data['status']
104
- when 'completed'
105
- notify_end(@transfer_id)
106
- break
107
110
  when 'waiting', 'partially_completed', 'unknown', 'waiting(read error)'
108
- if spinner.nil?
109
- spinner = TTY::Spinner.new('[:spinner] :title', format: :classic)
110
- spinner.start
111
- end
112
- spinner.update(title: transfer_data['status'])
113
- spinner.spin
111
+ notify_progress(session_id: nil, type: :pre_start, info: transfer_data['status'])
114
112
  when 'running'
115
- if !started && transfer_data['precalc'].is_a?(Hash) &&
113
+ if !session_started
114
+ notify_progress(session_id: @transfer_id, type: :session_start)
115
+ session_started = true
116
+ end
117
+ message = transfer_data['status']
118
+ message = "#{message} (#{transfer_data['error_desc']})" if !transfer_data['error_desc']&.empty?
119
+ notify_progress(session_id: nil, type: :pre_start, info: message)
120
+ if bytes_expected.nil? &&
121
+ transfer_data['precalc'].is_a?(Hash) &&
116
122
  transfer_data['precalc']['status'].eql?('ready')
117
- notify_begin(@transfer_id, transfer_data['precalc']['bytes_expected'])
118
- started = true
119
- else
120
- notify_progress(@transfer_id, transfer_data['bytes_transferred'])
123
+ bytes_expected = transfer_data['precalc']['bytes_expected']
124
+ notify_progress(type: :session_size, session_id: @transfer_id, info: bytes_expected)
121
125
  end
126
+ notify_progress(type: :transfer, session_id: @transfer_id, info: transfer_data['bytes_transferred'])
127
+ when 'completed'
128
+ notify_progress(type: :transfer, session_id: @transfer_id, info: bytes_expected) if bytes_expected
129
+ notify_progress(type: :end, session_id: @transfer_id)
130
+ break
122
131
  when 'failed'
132
+ notify_progress(type: :end, session_id: @transfer_id)
123
133
  # 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
134
+ break if transfer_data['error_code'].eql?(0) && transfer_data['error_desc'].empty?
128
135
  raise Fasp::Error, "status: #{transfer_data['status']}. code: #{transfer_data['error_code']}. description: #{transfer_data['error_desc']}"
129
136
  else
130
137
  Log.log.warn{"transfer_data -> #{transfer_data}"}
131
138
  raise Fasp::Error, "status: #{transfer_data['status']}. code: #{transfer_data['error_code']}. description: #{transfer_data['error_desc']}"
132
139
  end
133
- sleep(1)
140
+ sleep(1.0)
134
141
  end
135
142
  # TODO: get status of sessions
136
143
  return []