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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +33 -0
- data/CONTRIBUTING.md +17 -12
- data/README.md +396 -185
- data/bin/asession +26 -19
- data/examples/build_exec +74 -0
- data/examples/{rubyc → build_exec_rubyc} +18 -2
- data/examples/get_proto_file.rb +7 -0
- data/lib/aspera/agent/alpha.rb +8 -8
- data/lib/aspera/agent/base.rb +4 -18
- data/lib/aspera/agent/connect.rb +14 -13
- data/lib/aspera/agent/direct.rb +123 -120
- data/lib/aspera/agent/httpgw.rb +2 -3
- data/lib/aspera/agent/node.rb +10 -10
- data/lib/aspera/agent/trsdk.rb +17 -20
- data/lib/aspera/api/alee.rb +15 -0
- data/lib/aspera/api/aoc.rb +128 -99
- data/lib/aspera/api/ats.rb +1 -1
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/httpgw.rb +104 -64
- data/lib/aspera/api/node.rb +33 -12
- data/lib/aspera/ascmd.rb +56 -48
- data/lib/aspera/ascp/installation.rb +142 -70
- data/lib/aspera/ascp/management.rb +7 -3
- data/lib/aspera/ascp/products.rb +13 -7
- data/lib/aspera/assert.rb +10 -5
- data/lib/aspera/cli/formatter.rb +42 -26
- data/lib/aspera/cli/hints.rb +2 -1
- data/lib/aspera/cli/info.rb +12 -10
- data/lib/aspera/cli/main.rb +16 -13
- data/lib/aspera/cli/manager.rb +15 -10
- data/lib/aspera/cli/plugin.rb +17 -31
- data/lib/aspera/cli/plugin_factory.rb +10 -1
- data/lib/aspera/cli/plugins/alee.rb +3 -3
- data/lib/aspera/cli/plugins/aoc.rb +222 -194
- data/lib/aspera/cli/plugins/ats.rb +16 -14
- data/lib/aspera/cli/plugins/config.rb +66 -53
- data/lib/aspera/cli/plugins/console.rb +3 -3
- data/lib/aspera/cli/plugins/faspex.rb +11 -21
- data/lib/aspera/cli/plugins/faspex5.rb +44 -42
- data/lib/aspera/cli/plugins/faspio.rb +2 -2
- data/lib/aspera/cli/plugins/httpgw.rb +1 -1
- data/lib/aspera/cli/plugins/node.rb +155 -96
- data/lib/aspera/cli/plugins/orchestrator.rb +14 -13
- data/lib/aspera/cli/plugins/preview.rb +8 -9
- data/lib/aspera/cli/plugins/server.rb +6 -10
- data/lib/aspera/cli/plugins/shares.rb +13 -9
- data/lib/aspera/cli/sync_actions.rb +72 -31
- data/lib/aspera/cli/transfer_agent.rb +13 -14
- data/lib/aspera/cli/transfer_progress.rb +36 -18
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/command_line_builder.rb +3 -4
- data/lib/aspera/coverage.rb +13 -1
- data/lib/aspera/environment.rb +59 -10
- data/lib/aspera/faspex_gw.rb +3 -3
- data/lib/aspera/json_rpc.rb +1 -1
- data/lib/aspera/keychain/encrypted_hash.rb +2 -0
- data/lib/aspera/keychain/macos_security.rb +7 -12
- data/lib/aspera/log.rb +4 -4
- data/lib/aspera/node_simulator.rb +1 -1
- data/lib/aspera/oauth/base.rb +39 -45
- data/lib/aspera/oauth/factory.rb +11 -4
- data/lib/aspera/oauth/generic.rb +4 -8
- data/lib/aspera/oauth/jwt.rb +4 -4
- data/lib/aspera/oauth/url_json.rb +3 -2
- data/lib/aspera/oauth/web.rb +10 -6
- data/lib/aspera/persistency_action_once.rb +16 -8
- data/lib/aspera/preview/utils.rb +5 -16
- data/lib/aspera/rest.rb +100 -76
- data/lib/aspera/secret_hider.rb +3 -2
- data/lib/aspera/ssh.rb +1 -1
- data/lib/aspera/transfer/faux_file.rb +7 -5
- data/lib/aspera/transfer/parameters.rb +41 -35
- data/lib/aspera/transfer/spec.rb +16 -18
- data/lib/aspera/transfer/sync.rb +51 -50
- data/lib/aspera/transfer/uri.rb +1 -1
- data/lib/aspera/uri_reader.rb +1 -1
- data/lib/aspera/web_auth.rb +166 -18
- data/lib/aspera/web_server_simple.rb +27 -15
- data/lib/transfer_pb.rb +84 -0
- data/lib/transfer_services_pb.rb +82 -0
- data.tar.gz.sig +0 -0
- metadata +25 -6
- metadata.gz.sig +0 -0
data/lib/aspera/api/httpgw.rb
CHANGED
@@ -45,7 +45,7 @@ module Aspera
|
|
45
45
|
else
|
46
46
|
@shared_info[:count][:sent_general] += 1
|
47
47
|
end
|
48
|
-
Log.log.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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(
|
142
|
-
#
|
143
|
-
|
144
|
-
#
|
145
|
-
|
146
|
-
|
147
|
-
|
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(
|
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(
|
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
|
-
|
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:
|
190
|
+
name: file_to_send[:name],
|
213
191
|
# TODO: get mime type?
|
214
|
-
type:
|
215
|
-
size:
|
216
|
-
slice:
|
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
|
-
|
219
|
-
fileIndex:
|
196
|
+
total_slices: last_slice + 1,
|
197
|
+
fileIndex: file_index
|
220
198
|
}
|
221
|
-
file =
|
222
|
-
|
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.
|
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?(
|
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(
|
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(
|
263
|
-
@notify_cb&.call(
|
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})
|
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')
|
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
|
data/lib/aspera/api/node.rb
CHANGED
@@ -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']}")
|
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")
|
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]
|
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
|
-
|
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
|
-
)[
|
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')
|
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
|
-
|
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,
|
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
|
-
|
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 :
|
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 :
|
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)
|