aspera-cli 4.18.1 → 4.20.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 (85) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +33 -0
  4. data/CONTRIBUTING.md +17 -12
  5. data/README.md +396 -185
  6. data/bin/asession +26 -19
  7. data/examples/build_exec +74 -0
  8. data/examples/{rubyc → build_exec_rubyc} +18 -2
  9. data/examples/get_proto_file.rb +7 -0
  10. data/lib/aspera/agent/alpha.rb +8 -8
  11. data/lib/aspera/agent/base.rb +4 -18
  12. data/lib/aspera/agent/connect.rb +14 -13
  13. data/lib/aspera/agent/direct.rb +123 -120
  14. data/lib/aspera/agent/httpgw.rb +2 -3
  15. data/lib/aspera/agent/node.rb +10 -10
  16. data/lib/aspera/agent/trsdk.rb +17 -20
  17. data/lib/aspera/api/alee.rb +15 -0
  18. data/lib/aspera/api/aoc.rb +128 -99
  19. data/lib/aspera/api/ats.rb +1 -1
  20. data/lib/aspera/api/cos_node.rb +1 -1
  21. data/lib/aspera/api/httpgw.rb +104 -64
  22. data/lib/aspera/api/node.rb +33 -12
  23. data/lib/aspera/ascmd.rb +56 -48
  24. data/lib/aspera/ascp/installation.rb +142 -70
  25. data/lib/aspera/ascp/management.rb +7 -3
  26. data/lib/aspera/ascp/products.rb +13 -7
  27. data/lib/aspera/assert.rb +10 -5
  28. data/lib/aspera/cli/formatter.rb +42 -26
  29. data/lib/aspera/cli/hints.rb +2 -1
  30. data/lib/aspera/cli/info.rb +12 -10
  31. data/lib/aspera/cli/main.rb +16 -13
  32. data/lib/aspera/cli/manager.rb +15 -10
  33. data/lib/aspera/cli/plugin.rb +17 -31
  34. data/lib/aspera/cli/plugin_factory.rb +10 -1
  35. data/lib/aspera/cli/plugins/alee.rb +3 -3
  36. data/lib/aspera/cli/plugins/aoc.rb +222 -194
  37. data/lib/aspera/cli/plugins/ats.rb +16 -14
  38. data/lib/aspera/cli/plugins/config.rb +66 -53
  39. data/lib/aspera/cli/plugins/console.rb +3 -3
  40. data/lib/aspera/cli/plugins/faspex.rb +11 -21
  41. data/lib/aspera/cli/plugins/faspex5.rb +44 -42
  42. data/lib/aspera/cli/plugins/faspio.rb +2 -2
  43. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  44. data/lib/aspera/cli/plugins/node.rb +155 -96
  45. data/lib/aspera/cli/plugins/orchestrator.rb +14 -13
  46. data/lib/aspera/cli/plugins/preview.rb +8 -9
  47. data/lib/aspera/cli/plugins/server.rb +6 -10
  48. data/lib/aspera/cli/plugins/shares.rb +13 -9
  49. data/lib/aspera/cli/sync_actions.rb +72 -31
  50. data/lib/aspera/cli/transfer_agent.rb +13 -14
  51. data/lib/aspera/cli/transfer_progress.rb +36 -18
  52. data/lib/aspera/cli/version.rb +1 -1
  53. data/lib/aspera/command_line_builder.rb +3 -4
  54. data/lib/aspera/coverage.rb +13 -1
  55. data/lib/aspera/environment.rb +59 -10
  56. data/lib/aspera/faspex_gw.rb +3 -3
  57. data/lib/aspera/json_rpc.rb +1 -1
  58. data/lib/aspera/keychain/encrypted_hash.rb +2 -0
  59. data/lib/aspera/keychain/macos_security.rb +7 -12
  60. data/lib/aspera/log.rb +4 -4
  61. data/lib/aspera/node_simulator.rb +1 -1
  62. data/lib/aspera/oauth/base.rb +39 -45
  63. data/lib/aspera/oauth/factory.rb +11 -4
  64. data/lib/aspera/oauth/generic.rb +4 -8
  65. data/lib/aspera/oauth/jwt.rb +4 -4
  66. data/lib/aspera/oauth/url_json.rb +3 -2
  67. data/lib/aspera/oauth/web.rb +10 -6
  68. data/lib/aspera/persistency_action_once.rb +16 -8
  69. data/lib/aspera/preview/utils.rb +5 -16
  70. data/lib/aspera/rest.rb +100 -76
  71. data/lib/aspera/secret_hider.rb +3 -2
  72. data/lib/aspera/ssh.rb +1 -1
  73. data/lib/aspera/transfer/faux_file.rb +7 -5
  74. data/lib/aspera/transfer/parameters.rb +41 -35
  75. data/lib/aspera/transfer/spec.rb +16 -18
  76. data/lib/aspera/transfer/sync.rb +51 -50
  77. data/lib/aspera/transfer/uri.rb +1 -1
  78. data/lib/aspera/uri_reader.rb +1 -1
  79. data/lib/aspera/web_auth.rb +166 -18
  80. data/lib/aspera/web_server_simple.rb +27 -15
  81. data/lib/transfer_pb.rb +84 -0
  82. data/lib/transfer_services_pb.rb +82 -0
  83. data.tar.gz.sig +0 -0
  84. metadata +25 -6
  85. metadata.gz.sig +0 -0
@@ -45,7 +45,7 @@ module Aspera
45
45
  else
46
46
  @shared_info[:count][:sent_general] += 1
47
47
  end
48
- Log.log.debug do
48
+ Log.log.trace1 do
49
49
  log_data = payload.dup
50
50
  log_data[:data] = "[data #{log_data[:data].length} bytes]" if log_data.key?(:data)
51
51
  "#{LOG_WS_SEND}json: #{msg_type}: #{JSON.generate(log_data)}"
@@ -55,7 +55,7 @@ module Aspera
55
55
 
56
56
  # send data on http gw web socket
57
57
  def ws_send(ws_type:, data:)
58
- Log.log.debug{"#{LOG_WS_SEND}sending: #{ws_type} (#{data&.length || 0} bytes)"}
58
+ Log.log.trace1{"#{LOG_WS_SEND}sending: #{ws_type} (#{data&.length || 0} bytes)"}
59
59
  @shared_info[:count][:sent_general] += 1 if ws_type.eql?(:binary)
60
60
  frame_generator = ::WebSocket::Frame::Outgoing::Client.new(data: data, type: ws_type, version: @ws_handshake.version)
61
61
  @ws_io.write(frame_generator.to_s)
@@ -68,13 +68,13 @@ module Aspera
68
68
  (((@shared_info[:count][:sent_general] - @shared_info[:count][:received_general]) > 1) ||
69
69
  ((@shared_info[:count][:received_v2_delimiter] - @shared_info[:count][:sent_v2_delimiter]) > 1))
70
70
  if !@shared_info[:cond_var].wait(@shared_info[:mutex], 2.0)
71
- Log.log.debug{"#{LOG_WS_SEND}#{'timeout'.blue}: #{@shared_info[:count]}"}
71
+ Log.log.trace1{"#{LOG_WS_SEND}#{'timeout'.blue}: #{@shared_info[:count]}"}
72
72
  end
73
73
  end
74
74
  end
75
75
  end
76
76
  raise @shared_info[:read_exception] unless @shared_info[:read_exception].nil?
77
- Log.log.debug{"#{LOG_WS_SEND}counts: #{@shared_info[:count]}"}
77
+ Log.log.trace2{"#{LOG_WS_SEND}counts: #{@shared_info[:count]}"}
78
78
  end
79
79
 
80
80
  # message processing for read thread
@@ -114,12 +114,12 @@ module Aspera
114
114
  # ready byte by byte until frame is ready
115
115
  # blocking read
116
116
  byte = @ws_io.read(1)
117
- Log.log.trace1{"#{LOG_WS_RECV}read: #{byte} (#{byte.class}) eof=#{@ws_io.eof?}"}
117
+ Log.log.trace2{"#{LOG_WS_RECV}read: #{byte} (#{byte.class}) eof=#{@ws_io.eof?}"}
118
118
  frame_parser << byte
119
119
  frame_ok = frame_parser.next
120
120
  next if frame_ok.nil?
121
121
  process_received_message(frame_ok.data.to_s)
122
- Log.log.debug{"#{LOG_WS_RECV}counts: #{@shared_info[:count]}"}
122
+ Log.log.trace2{"#{LOG_WS_RECV}counts: #{@shared_info[:count]}"}
123
123
  rescue => e
124
124
  Log.log.debug{"#{LOG_WS_RECV}Exception: #{e}"}
125
125
  @shared_info[:mutex].synchronize do
@@ -138,38 +138,16 @@ module Aspera
138
138
  def upload(transfer_spec)
139
139
  # identify this session uniquely
140
140
  session_id = SecureRandom.uuid
141
- @notify_cb&.call(session_id: nil, type: :pre_start, info: 'starting')
142
- # total size of all files
143
- total_bytes_to_transfer = 0
144
- # we need to keep track of actual file path because transfer spec is modified to be sent in web socket
145
- files_to_read = []
146
- # get source root or nil
147
- source_root = transfer_spec.key?('source_root') && !transfer_spec['source_root'].empty? ? transfer_spec['source_root'] : nil
148
- # source root is ignored by GW, used only here
149
- transfer_spec.delete('source_root')
150
- # compute total size of files to upload (for progress)
151
- # modify transfer spec to be suitable for GW
152
- transfer_spec['paths'].each do |item|
153
- # save actual file location to be able read contents later
154
- file_to_add = Transfer::FauxFile.open(item['source'])
155
- if file_to_add
156
- item['source'] = file_to_add.path
157
- item['file_size'] = file_to_add.size
158
- else
159
- file_to_add = item['source']
160
- # add source root if needed
161
- file_to_add = File.join(source_root, file_to_add) unless source_root.nil?
162
- # GW expects a simple file name in 'source' but if user wants to change the name, we take it
163
- item['source'] = File.basename(item['destination'].nil? ? item['source'] : item['destination'])
164
- item['file_size'] = File.size(file_to_add)
165
- end
166
- # save so that we can actually read the file later
167
- files_to_read.push(file_to_add)
168
- total_bytes_to_transfer += item['file_size']
169
- end
141
+ @notify_cb&.call(:pre_start, session_id: nil, info: 'starting')
142
+ # process files to send, modify `paths` in transfer_spec
143
+ files_to_send = process_upload_list(transfer_spec)
144
+ # total size of all files is last element
145
+ total_bytes_to_transfer = files_to_send.pop
146
+ Log.log.trace1{Log.dump(:modified_tspec, transfer_spec)}
147
+ Log.log.trace1{Log.dump(:files_to_send, files_to_send)}
170
148
  # TODO: check that this is available in endpoints: @api_info['endpoints']
171
149
  upload_url = File.join(@gw_root_url, @upload_version, 'upload')
172
- @notify_cb&.call(session_id: nil, type: :pre_start, info: 'connecting wss')
150
+ @notify_cb&.call(:pre_start, session_id: nil, info: 'connecting wss')
173
151
  # open web socket to end point (equivalent to Net::HTTP.start)
174
152
  http_session = Rest.start_http_session(upload_url)
175
153
  # get the underlying socket i/o
@@ -180,11 +158,6 @@ module Aspera
180
158
  @ws_handshake << @ws_io.readuntil("\r\n\r\n")
181
159
  Aspera.assert(@ws_handshake.finished?){'Error in websocket handshake'}
182
160
  Log.log.debug{"#{LOG_WS_SEND}handshake success"}
183
- # start read thread after handshake
184
- @ws_read_thread = Thread.new {process_read_thread}
185
- @notify_cb&.call(session_id: session_id, type: :session_start)
186
- @notify_cb&.call(session_id: session_id, type: :session_size, info: total_bytes_to_transfer)
187
- sleep(1)
188
161
  # data shared between main thread and read thread
189
162
  @shared_info = {
190
163
  read_exception: nil, # error message if any in callback
@@ -197,34 +170,34 @@ module Aspera
197
170
  mutex: Mutex.new,
198
171
  cond_var: ConditionVariable.new
199
172
  }
173
+ # start read thread after handshake
174
+ @ws_read_thread = Thread.new {process_read_thread}
175
+ @notify_cb&.call(:session_start, session_id: session_id)
176
+ @notify_cb&.call(:session_size, session_id: session_id, info: total_bytes_to_transfer)
177
+ sleep(1)
200
178
  # notify progress bar
201
- @notify_cb&.call(type: :session_size, session_id: session_id, info: total_bytes_to_transfer)
179
+ @notify_cb&.call(:session_size, session_id: session_id, info: total_bytes_to_transfer)
202
180
  # first step send transfer spec
203
- Log.log.debug{Log.dump(:ws_spec, transfer_spec)}
204
181
  ws_snd_json(MSG_SEND_TRANSFER_SPEC, transfer_spec)
205
182
  # current file index
206
183
  file_index = 0
207
184
  # aggregate size sent
208
185
  session_sent_bytes = 0
209
186
  # process each file
210
- transfer_spec['paths'].each do |item|
187
+ files_to_send.each do |file_to_send|
188
+ last_slice = (file_to_send[:size] - 1) / @upload_chunk_size
211
189
  slice_info = {
212
- name: nil,
190
+ name: file_to_send[:name],
213
191
  # TODO: get mime type?
214
- type: 'application/octet-stream',
215
- size: item['file_size'],
216
- slice: 0, # current slice index
192
+ type: 'application/octet-stream',
193
+ size: file_to_send[:size],
194
+ slice: 0, # current slice index
217
195
  # index of last slice (i.e number of slices - 1)
218
- last_slice: (item['file_size'] - 1) / @upload_chunk_size,
219
- fileIndex: file_index
196
+ total_slices: last_slice + 1,
197
+ fileIndex: file_index
220
198
  }
221
- file = files_to_read[file_index]
222
- if file.is_a?(Transfer::FauxFile)
223
- slice_info[:name] = file.path
224
- else
225
- file = File.open(file)
226
- slice_info[:name] = File.basename(item[item['destination'].nil? ? 'source' : 'destination'])
227
- end
199
+ file = file_to_send[:file]
200
+ file = File.open(file) unless file.is_a?(Transfer::FauxFile)
228
201
  begin
229
202
  until file.eof?
230
203
  slice_bin_data = file.read(@upload_chunk_size)
@@ -238,9 +211,9 @@ module Aspera
238
211
  # send once, before data, at beginning
239
212
  ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info) if slice_info[:slice].eql?(0)
240
213
  ws_send(ws_type: :binary, data: slice_bin_data)
241
- Log.log.debug{"#{LOG_WS_SEND}buffer: file: #{file_index}, slice: #{slice_info[:slice]}/#{slice_info[:last_slice]}"}
214
+ Log.log.trace1{"#{LOG_WS_SEND}buffer: file: #{file_index}, slice: #{slice_info[:slice]}/#{last_slice}"}
242
215
  # send once, after data, at end
243
- ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info) if slice_info[:slice].eql?(slice_info[:last_slice])
216
+ ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info) if slice_info[:slice].eql?(last_slice)
244
217
  end
245
218
  rescue Errno::EPIPE => e
246
219
  raise @shared_info[:read_exception] unless @shared_info[:read_exception].nil?
@@ -250,7 +223,7 @@ module Aspera
250
223
  raise e
251
224
  end
252
225
  session_sent_bytes += slice_bin_data.length
253
- @notify_cb&.call(type: :transfer, session_id: session_id, info: session_sent_bytes)
226
+ @notify_cb&.call(:transfer, session_id: session_id, info: session_sent_bytes)
254
227
  slice_info[:slice] += 1
255
228
  end
256
229
  ensure
@@ -259,8 +232,8 @@ module Aspera
259
232
  file_index += 1
260
233
  end
261
234
  # throttling may have skipped last one
262
- @notify_cb&.call(type: :transfer, session_id: session_id, info: session_sent_bytes)
263
- @notify_cb&.call(type: :end, session_id: session_id)
235
+ @notify_cb&.call(:transfer, session_id: session_id, info: session_sent_bytes)
236
+ @notify_cb&.call(:end, session_id: session_id)
264
237
  ws_send(ws_type: :close, data: nil)
265
238
  Log.log.debug("Finished upload, waiting for end of #{THR_RECV} thread.")
266
239
  @ws_read_thread.join
@@ -283,7 +256,7 @@ module Aspera
283
256
  end
284
257
  transfer_spec['download_name'] = download_name
285
258
  end
286
- creation = create('download', {'transfer_spec' => transfer_spec})[:data]
259
+ creation = create('download', {'transfer_spec' => transfer_spec})
287
260
  transfer_uuid = creation['url'].split('/').last
288
261
  file_name =
289
262
  if transfer_spec['zip_required'] || transfer_spec['paths'].length > 1
@@ -301,6 +274,11 @@ module Aspera
301
274
  return @api_info
302
275
  end
303
276
 
277
+ # @return the base url of the gateway
278
+ def base_url
279
+ return @gw_root_url
280
+ end
281
+
304
282
  # @param url [String] URL of the HTTP Gateway, without version
305
283
  def initialize(
306
284
  url:,
@@ -326,7 +304,7 @@ module Aspera
326
304
  @synchronous = synchronous
327
305
  @notify_cb = notify_cb
328
306
  # get API info
329
- @api_info = read('info')[:data].freeze
307
+ @api_info = read('info').freeze
330
308
  Log.log.debug{Log.dump(:api_info, @api_info)}
331
309
  # web socket endpoint: by default use v2 (newer gateways), without base64 encoding
332
310
  # is the latest supported? else revert to old api
@@ -337,6 +315,68 @@ module Aspera
337
315
  end
338
316
  end
339
317
  end
318
+
319
+ private
320
+
321
+ # compute total size of files to upload (for progress)
322
+ # modify transfer spec to be suitable for HTTPGW
323
+ # @param transfer_spec [Hash] transfer specification
324
+ # @return [Array] info on files to send
325
+ def process_upload_list(transfer_spec)
326
+ total_bytes_to_transfer = 0
327
+ source_prefix = transfer_spec.key?('source_root') && !transfer_spec['source_root'].empty? ? transfer_spec['source_root'] + '/' : ''
328
+ files_to_send = []
329
+ transfer_spec['paths'].each do |one_path|
330
+ source_path = source_prefix + one_path['source']
331
+ faux_file = Transfer::FauxFile.create(source_path)
332
+ if faux_file
333
+ total_bytes_to_transfer += faux_file.size
334
+ files_to_send.push({
335
+ file: faux_file,
336
+ name: faux_file.path,
337
+ size: faux_file.size
338
+ })
339
+ elsif File.file?(source_path)
340
+ # regular file
341
+ file_size = File.size(source_path)
342
+ total_bytes_to_transfer += file_size
343
+ files_to_send.push({
344
+ file: source_path,
345
+ # GW expects a simple file name in 'source' but if user wants to change the name, we take it
346
+ name: File.basename(one_path['destination'].nil? ? source_path : one_path['destination']),
347
+ size: file_size
348
+ })
349
+ elsif File.directory?(source_path)
350
+ folders_to_process = [source_path]
351
+ until folders_to_process.empty?
352
+ folder = folders_to_process.shift
353
+ # read all entries
354
+ Dir.entries(folder).each do |entry|
355
+ next if entry.eql?('.') || entry.eql?('..')
356
+ entry_path = File.join(folder, entry)
357
+ if File.directory?(entry_path)
358
+ folders_to_process.push(entry_path)
359
+ elsif File.file?(entry_path)
360
+ file_size = File.size(entry_path)
361
+ total_bytes_to_transfer += file_size
362
+ files_to_send.push({
363
+ file: entry_path,
364
+ name: entry_path,
365
+ size: file_size
366
+ })
367
+ else
368
+ Log.log.warn{"Ignoring non file/directory: #{entry_path}"}
369
+ end
370
+ end
371
+ end
372
+ else
373
+ raise "File not found: #{source_path}"
374
+ end
375
+ end
376
+ transfer_spec['paths'] = files_to_send.map{|i|{'source' => i[:name]}}
377
+ files_to_send.push(total_bytes_to_transfer)
378
+ return files_to_send
379
+ end
340
380
  end
341
381
  end
342
382
  end
@@ -33,6 +33,8 @@ module Aspera
33
33
  ACCESS_LEVELS = %w[delete list mkdir preview read rename write].freeze
34
34
  HEADER_X_ASPERA_ACCESS_KEY = 'X-Aspera-AccessKey'
35
35
  HEADER_X_TOTAL_COUNT = 'X-Total-Count'
36
+ HEADER_X_CACHE_CONTROL = 'X-Aspera-Cache-Control'
37
+ HEADER_X_NEXT_ITER_TOKEN = 'X-Aspera-Next-Iteration-Token'
36
38
  SCOPE_USER = 'user:all'
37
39
  SCOPE_ADMIN = 'admin:all'
38
40
  PATH_SEPARATOR = '/'
@@ -42,9 +44,17 @@ module Aspera
42
44
 
43
45
  # class instance variable, access with accessors on class
44
46
  @use_standard_ports = true
47
+ @use_node_cache = true
45
48
 
46
49
  class << self
47
50
  attr_accessor :use_standard_ports
51
+ attr_accessor :use_node_cache
52
+
53
+ def cache_control_headers
54
+ h = {'Accept' => 'application/json'}
55
+ h[HEADER_X_CACHE_CONTROL] = 'no-cache' unless use_node_cache
56
+ h
57
+ end
48
58
 
49
59
  # For access keys: provide expression to match entry in folder
50
60
  def file_matcher(match_expression)
@@ -146,6 +156,11 @@ module Aspera
146
156
  end
147
157
  end
148
158
 
159
+ # Call node API, possibly adding cache control header, as globally specified
160
+ def read_with_cache(subpath, query=nil)
161
+ return call(operation: 'GET', subpath: subpath, headers: self.class.cache_control_headers, query: query)[:data]
162
+ end
163
+
149
164
  # update transfer spec with special additional tags
150
165
  def add_tspec_info(tspec)
151
166
  tspec.deep_merge!(@add_tspec) unless @add_tspec.nil?
@@ -171,7 +186,7 @@ module Aspera
171
186
  def entry_has_link_information(entry)
172
187
  # if target information is missing in folder, try to get it on entry
173
188
  if entry['target_node_id'].nil? || entry['target_id'].nil?
174
- link_entry = read("files/#{entry['id']}")[:data]
189
+ link_entry = read("files/#{entry['id']}")
175
190
  entry['target_node_id'] = link_entry['target_node_id']
176
191
  entry['target_id'] = link_entry['target_id']
177
192
  end
@@ -200,13 +215,19 @@ module Aspera
200
215
  # get folder content
201
216
  folder_contents =
202
217
  begin
203
- read("files/#{current_item[:id]}/files")[:data]
218
+ read("files/#{current_item[:id]}/files")
204
219
  rescue StandardError => e
205
220
  Log.log.warn{"#{current_item[:path]}: #{e.class} #{e.message}"}
206
221
  []
207
222
  end
208
223
  Log.log.debug{Log.dump(:folder_contents, folder_contents)}
209
224
  folder_contents.each do |entry|
225
+ if entry.key?('error')
226
+ if entry['error'].is_a?(Hash) && entry['error'].key?('user_message')
227
+ Log.log.error(entry['error']['user_message'])
228
+ end
229
+ next
230
+ end
210
231
  relative_path = File.join(current_item[:path], entry['name'])
211
232
  Log.log.debug{"process_folder_tree: checking #{relative_path}"}
212
233
  # call block, continue only if method returns true
@@ -228,16 +249,16 @@ module Aspera
228
249
  end
229
250
  end
230
251
 
231
- # Navigate the path from given file id
252
+ # Navigate the path from given file id on current node, and return the node and file id of target.
253
+ # If the path ends with a "/" or process_last_link is true then if the last item in path is a link, it is followed.
232
254
  # @param top_file_id [String] id initial file id
233
- # @param path [String] file path
255
+ # @param path [String] file or folder path (end with "/" is like setting process_last_link)
256
+ # @param process_last_link [Boolean] if true, follow the last link
234
257
  # @return [Hash] {.api,.file_id}
235
- def resolve_api_fid(top_file_id, path)
258
+ def resolve_api_fid(top_file_id, path, process_last_link=false)
236
259
  Aspera.assert_type(top_file_id, String)
237
260
  Aspera.assert_type(path, String)
238
- # if last element is a link and followed by "/", we list the content of that folder, else we return the link
239
- process_last_link = path.end_with?(PATH_SEPARATOR)
240
- # keep only non-empty elements
261
+ process_last_link ||= path.end_with?(PATH_SEPARATOR)
241
262
  path_elements = path.split(PATH_SEPARATOR).reject(&:empty?)
242
263
  return {api: self, file_id: top_file_id} if path_elements.empty?
243
264
  resolve_state = {path: path_elements, result: nil, process_last_link: process_last_link}
@@ -265,7 +286,7 @@ module Aspera
265
286
  full_spec = create(
266
287
  'files/download_setup',
267
288
  {transfer_requests: [{transfer_request: {paths: [{source: '/'}]}}]}
268
- )[:data]['transfer_specs'].first['transfer_spec']
289
+ )['transfer_specs'].first['transfer_spec']
269
290
  # set available fields
270
291
  @std_t_spec_cache = Transfer::Spec::TRANSPORT_FIELDS.each_with_object({}) do |i, h|
271
292
  h[i] = full_spec[i] if full_spec.key?(i)
@@ -317,13 +338,13 @@ module Aspera
317
338
  if !@app_info.nil? && !@app_info[:node_info]['transfer_url'].nil? && !@app_info[:node_info]['transfer_url'].empty?
318
339
  transfer_spec['remote_host'] = @app_info[:node_info]['transfer_url']
319
340
  end
320
- info = read('info')[:data]
341
+ info = read('info')
321
342
  # get the transfer user from info on access key
322
343
  transfer_spec['remote_user'] = info['transfer_user'] if info['transfer_user']
323
344
  # get settings from name.value array to hash key.value
324
345
  settings = info['settings']&.each_with_object({}){|i, h|h[i['name']] = i['value']}
325
346
  # check WSS ports
326
- %w[wss_enabled wss_port].each do |i|
347
+ Transfer::Spec::WSS_FIELDS.each do |i|
327
348
  transfer_spec[i] = settings[i] if settings.key?(i)
328
349
  end if settings.is_a?(Hash)
329
350
  else
@@ -380,7 +401,7 @@ module Aspera
380
401
  return true
381
402
  end
382
403
 
383
- def process_find_files(entry, _path, state)
404
+ def process_find_files(entry, path, state)
384
405
  state[:found].push(entry.merge({'path' => path})) if state[:test_block].call(entry)
385
406
  # test all files deeply
386
407
  return true
data/lib/aspera/ascmd.rb CHANGED
@@ -22,7 +22,54 @@ module Aspera
22
22
  mv: 2,
23
23
  rm: 1
24
24
  }.freeze
25
- private_constant :OPS_ARGS
25
+
26
+ # protocol is based on Type-Length-Value
27
+ # type start at one, but array index start at zero
28
+ ENUM_START = 1
29
+
30
+ # description of result structures (see ascmdtypes.h).
31
+ # Base types are big endian
32
+ # key = name of type
33
+ # index in array `fields` is the type (minus ENUM_START)
34
+ # decoding always start at `result`
35
+ # some fields have special handling indicated by `special`
36
+ # field_list, list_tlv_list, list_tlv_restart are composed with a list of TLV
37
+ TYPES_DESCR = {
38
+ result: {decode: :field_list,
39
+ fields: [{name: :file, is_a: :stat}, {name: :dir, is_a: :stat, special: :list_tlv_list}, {name: :size, is_a: :size}, {name: :error, is_a: :error},
40
+ {name: :info, is_a: :info}, {name: :success, is_a: nil, special: :return_true}, {name: :exit, is_a: nil},
41
+ {name: :df, is_a: :mnt, special: :list_tlv_restart}, {name: :md5sum, is_a: :md5sum}]},
42
+ stat: {decode: :field_list,
43
+ fields: [{name: :name, is_a: :zstr}, {name: :size, is_a: :int64}, {name: :mode, is_a: :int32, check: nil}, {name: :zmode, is_a: :zstr},
44
+ {name: :uid, is_a: :int32, check: nil}, {name: :zuid, is_a: :zstr}, {name: :gid, is_a: :int32, check: nil}, {name: :zgid, is_a: :zstr},
45
+ {name: :ctime, is_a: :epoch}, {name: :zctime, is_a: :zstr}, {name: :mtime, is_a: :epoch}, {name: :zmtime, is_a: :zstr},
46
+ {name: :atime, is_a: :epoch}, {name: :zatime, is_a: :zstr}, {name: :symlink, is_a: :zstr}, {name: :errno, is_a: :int32},
47
+ {name: :errstr, is_a: :zstr}]},
48
+ info: {decode: :field_list,
49
+ fields: [{name: :platform, is_a: :zstr}, {name: :version, is_a: :zstr}, {name: :lang, is_a: :zstr}, {name: :territory, is_a: :zstr},
50
+ {name: :codeset, is_a: :zstr}, {name: :lc_ctype, is_a: :zstr}, {name: :lc_numeric, is_a: :zstr}, {name: :lc_time, is_a: :zstr},
51
+ {name: :lc_all, is_a: :zstr}, {name: :dev, is_a: :zstr, special: :list_multiple}, {name: :browse_caps, is_a: :zstr},
52
+ {name: :protocol, is_a: :zstr}]},
53
+ size: {decode: :field_list,
54
+ fields: [{name: :size, is_a: :int64}, {name: :fcount, is_a: :int32}, {name: :dcount, is_a: :int32}, {name: :failed_fcount, is_a: :int32},
55
+ {name: :failed_dcount, is_a: :int32}]},
56
+ error: {decode: :field_list,
57
+ fields: [{name: :errno, is_a: :int32}, {name: :errstr, is_a: :zstr}]},
58
+ mnt: {decode: :field_list,
59
+ fields: [{name: :fs, is_a: :zstr}, {name: :dir, is_a: :zstr}, {name: :is_a, is_a: :zstr}, {name: :total, is_a: :int64},
60
+ {name: :used, is_a: :int64}, {name: :free, is_a: :int64}, {name: :fcount, is_a: :int64}, {name: :errno, is_a: :int32},
61
+ {name: :errstr, is_a: :zstr}]},
62
+ md5sum: {decode: :field_list, fields: [{name: :md5sum, is_a: :zstr}]},
63
+ int8: {decode: :base, unpack: 'C', size: 1},
64
+ int32: {decode: :base, unpack: 'L>', size: 4},
65
+ int64: {decode: :base, unpack: 'Q>', size: 8},
66
+ epoch: {decode: :base, unpack: 'Q>', size: 8},
67
+ zstr: {decode: :base, unpack: 'Z*'},
68
+ blist: {decode: :buffer_list}
69
+ }.freeze
70
+
71
+ private_constant :TYPES_DESCR, :ENUM_START, :OPS_ARGS
72
+
26
73
  # list of supported actions
27
74
  OPERATIONS = OPS_ARGS.keys.freeze
28
75
 
@@ -55,6 +102,7 @@ module Aspera
55
102
  arg_batches.each do |args|
56
103
  command = [main_command]
57
104
  # enclose arguments in double quotes, protect backslash and double quotes
105
+ # ascmd uses space as token separator, and optional quotes ('") or \ to escape
58
106
  args.each do |v|
59
107
  command.push(%Q{"#{v.gsub(/["\\]/){|s|"\\#{s}"}}"})
60
108
  end
@@ -106,47 +154,6 @@ module Aspera
106
154
  def extended_message; "ascmd: errno=#{@errno} errstr=\"#{@errstr}\" command=#{@command} arguments=#{@arguments&.join(',')}"; end
107
155
  end
108
156
 
109
- # description of result structures (see ascmdtypes.h). Base types are big endian
110
- # key = name of type
111
- TYPES_DESCR = {
112
- result: {decode: :field_list,
113
- fields: [{name: :file, is_a: :stat}, {name: :dir, is_a: :stat, special: :sub_struct}, {name: :size, is_a: :size}, {name: :error, is_a: :error},
114
- {name: :info, is_a: :info}, {name: :success, is_a: nil, special: :return_true}, {name: :exit, is_a: nil},
115
- {name: :df, is_a: :mnt, special: :restart_on_first}, {name: :md5sum, is_a: :md5sum}]},
116
- stat: {decode: :field_list,
117
- fields: [{name: :name, is_a: :zstr}, {name: :size, is_a: :int64}, {name: :mode, is_a: :int32, check: nil}, {name: :zmode, is_a: :zstr},
118
- {name: :uid, is_a: :int32, check: nil}, {name: :zuid, is_a: :zstr}, {name: :gid, is_a: :int32, check: nil}, {name: :zgid, is_a: :zstr},
119
- {name: :ctime, is_a: :epoch}, {name: :zctime, is_a: :zstr}, {name: :mtime, is_a: :epoch}, {name: :zmtime, is_a: :zstr},
120
- {name: :atime, is_a: :epoch}, {name: :zatime, is_a: :zstr}, {name: :symlink, is_a: :zstr}, {name: :errno, is_a: :int32},
121
- {name: :errstr, is_a: :zstr}]},
122
- info: {decode: :field_list,
123
- fields: [{name: :platform, is_a: :zstr}, {name: :version, is_a: :zstr}, {name: :lang, is_a: :zstr}, {name: :territory, is_a: :zstr},
124
- {name: :codeset, is_a: :zstr}, {name: :lc_ctype, is_a: :zstr}, {name: :lc_numeric, is_a: :zstr}, {name: :lc_time, is_a: :zstr},
125
- {name: :lc_all, is_a: :zstr}, {name: :dev, is_a: :zstr, special: :multiple}, {name: :browse_caps, is_a: :zstr},
126
- {name: :protocol, is_a: :zstr}]},
127
- size: {decode: :field_list,
128
- fields: [{name: :size, is_a: :int64}, {name: :fcount, is_a: :int32}, {name: :dcount, is_a: :int32}, {name: :failed_fcount, is_a: :int32},
129
- {name: :failed_dcount, is_a: :int32}]},
130
- error: {decode: :field_list,
131
- fields: [{name: :errno, is_a: :int32}, {name: :errstr, is_a: :zstr}]},
132
- mnt: {decode: :field_list,
133
- fields: [{name: :fs, is_a: :zstr}, {name: :dir, is_a: :zstr}, {name: :is_a, is_a: :zstr}, {name: :total, is_a: :int64},
134
- {name: :used, is_a: :int64}, {name: :free, is_a: :int64}, {name: :fcount, is_a: :int64}, {name: :errno, is_a: :int32},
135
- {name: :errstr, is_a: :zstr}]},
136
- md5sum: {decode: :field_list, fields: [{name: :md5sum, is_a: :zstr}]},
137
- int8: {decode: :base, unpack: 'C', size: 1},
138
- int32: {decode: :base, unpack: 'L>', size: 4},
139
- int64: {decode: :base, unpack: 'Q>', size: 8},
140
- epoch: {decode: :base, unpack: 'Q>', size: 8},
141
- zstr: {decode: :base, unpack: 'Z*'},
142
- blist: {decode: :buffer_list}
143
- }.freeze
144
-
145
- # protocol enum start at one, but array index start at zero
146
- ENUM_START = 1
147
-
148
- private_constant :TYPES_DESCR, :ENUM_START
149
-
150
157
  class << self
151
158
  # get description of structure's field, @param struct_name, @param typed_buffer provides field name
152
159
  def field_description(struct_name, typed_buffer)
@@ -175,6 +182,7 @@ module Aspera
175
182
  Log.log.trace1{"#{' .' * indent_level}-> base:#{byte_array} -> #{result}"}
176
183
  result = Time.at(result) if type_name.eql?(:epoch)
177
184
  when :buffer_list
185
+ # return a list of type_buffer
178
186
  result = []
179
187
  until buffer.empty?
180
188
  btype = parse(buffer, :int8, indent_level)
@@ -193,16 +201,16 @@ module Aspera
193
201
  field_info = field_description(type_name, typed_buffer)
194
202
  Log.log.trace1{"#{' .' * indent_level}+ field(special=#{field_info[:special]})=#{field_info[:name]}".green}
195
203
  case field_info[:special]
196
- when nil
204
+ when nil # normal case
197
205
  result[field_info[:name]] = parse(typed_buffer[:buffer], field_info[:is_a], indent_level)
198
- when :return_true
206
+ when :return_true # nothing to parse, just return true
199
207
  result[field_info[:name]] = true
200
- when :sub_struct
201
- result[field_info[:name]] = parse(typed_buffer[:buffer], :blist, indent_level).map{|r|parse(r[:buffer], field_info[:is_a], indent_level)}
202
- when :multiple
208
+ when :list_multiple # field appears multiple times, and is an array of values (base type)
203
209
  result[field_info[:name]] ||= []
204
210
  result[field_info[:name]].push(parse(typed_buffer[:buffer], field_info[:is_a], indent_level))
205
- when :restart_on_first
211
+ when :list_tlv_list # field is an array of values in a list of buffers
212
+ result[field_info[:name]] = parse(typed_buffer[:buffer], :blist, indent_level).map{|r|parse(r[:buffer], field_info[:is_a], indent_level)}
213
+ when :list_tlv_restart # field is an array of values, but a new value is started on index 1
206
214
  fl = result[field_info[:name]] = []
207
215
  parse(typed_buffer[:buffer], :blist, indent_level).map do |tb|
208
216
  fl.push({}) if tb[:btype].eql?(ENUM_START)