aspera-cli 4.18.0 → 4.19.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 +23 -0
- data/CONTRIBUTING.md +5 -12
- data/README.md +152 -84
- data/examples/build_exec +85 -0
- data/examples/build_package.sh +28 -0
- data/lib/aspera/agent/alpha.rb +4 -4
- data/lib/aspera/agent/base.rb +2 -0
- data/lib/aspera/agent/connect.rb +3 -4
- data/lib/aspera/agent/direct.rb +108 -104
- data/lib/aspera/agent/httpgw.rb +1 -1
- data/lib/aspera/api/aoc.rb +2 -2
- data/lib/aspera/api/httpgw.rb +95 -57
- data/lib/aspera/api/node.rb +110 -77
- data/lib/aspera/ascp/installation.rb +47 -32
- data/lib/aspera/ascp/management.rb +4 -1
- data/lib/aspera/ascp/products.rb +2 -8
- data/lib/aspera/cli/extended_value.rb +27 -14
- data/lib/aspera/cli/formatter.rb +35 -28
- data/lib/aspera/cli/main.rb +11 -11
- data/lib/aspera/cli/manager.rb +109 -94
- data/lib/aspera/cli/plugin.rb +4 -7
- data/lib/aspera/cli/plugin_factory.rb +10 -1
- data/lib/aspera/cli/plugins/aoc.rb +15 -14
- data/lib/aspera/cli/plugins/config.rb +35 -29
- data/lib/aspera/cli/plugins/faspex.rb +5 -4
- data/lib/aspera/cli/plugins/faspex5.rb +16 -13
- data/lib/aspera/cli/plugins/node.rb +50 -41
- data/lib/aspera/cli/plugins/orchestrator.rb +3 -2
- data/lib/aspera/cli/plugins/preview.rb +1 -1
- data/lib/aspera/cli/plugins/server.rb +2 -2
- data/lib/aspera/cli/plugins/shares.rb +11 -7
- data/lib/aspera/cli/special_values.rb +13 -0
- data/lib/aspera/cli/sync_actions.rb +73 -32
- data/lib/aspera/cli/transfer_agent.rb +3 -2
- data/lib/aspera/cli/transfer_progress.rb +1 -1
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/environment.rb +100 -7
- data/lib/aspera/faspex_gw.rb +1 -1
- data/lib/aspera/keychain/encrypted_hash.rb +2 -0
- data/lib/aspera/log.rb +1 -0
- data/lib/aspera/node_simulator.rb +1 -1
- data/lib/aspera/oauth/jwt.rb +1 -1
- data/lib/aspera/oauth/url_json.rb +2 -0
- data/lib/aspera/oauth/web.rb +7 -6
- data/lib/aspera/rest.rb +46 -15
- 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 +27 -19
- data/lib/aspera/transfer/spec.rb +8 -10
- data/lib/aspera/transfer/sync.rb +52 -47
- data/lib/aspera/web_auth.rb +0 -1
- data/lib/aspera/web_server_simple.rb +24 -13
- data.tar.gz.sig +0 -0
- metadata +5 -4
- metadata.gz.sig +0 -0
- data/examples/rubyc +0 -24
- data/lib/aspera/open_application.rb +0 -69
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
|
|
@@ -139,34 +139,12 @@ module Aspera
|
|
|
139
139
|
# identify this session uniquely
|
|
140
140
|
session_id = SecureRandom.uuid
|
|
141
141
|
@notify_cb&.call(session_id: nil, type: :pre_start, info: 'starting')
|
|
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
|
|
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
150
|
@notify_cb&.call(session_id: nil, type: :pre_start, info: 'connecting wss')
|
|
@@ -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_id: session_id, type: :session_start)
|
|
176
|
+
@notify_cb&.call(session_id: session_id, type: :session_size, info: total_bytes_to_transfer)
|
|
177
|
+
sleep(1)
|
|
200
178
|
# notify progress bar
|
|
201
179
|
@notify_cb&.call(type: :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?
|
|
@@ -310,11 +283,14 @@ module Aspera
|
|
|
310
283
|
notify_cb: nil,
|
|
311
284
|
**opts
|
|
312
285
|
)
|
|
286
|
+
Log.log.debug{Log.dump(:gw_url, url)}
|
|
313
287
|
# add scheme if missing
|
|
314
288
|
url = "https://#{url}" unless url.match?(%r{^[a-z]{1,6}://})
|
|
315
289
|
raise 'GW URL shall be with scheme https' unless url.start_with?('https://')
|
|
316
|
-
# remove trailing slash and version if
|
|
290
|
+
# remove trailing slash and version (o=only once) if present
|
|
291
|
+
# TODO: issue warning ?
|
|
317
292
|
url = url.gsub(%r{/+$}, '').gsub(%r{/#{API_V1}$}o, '')
|
|
293
|
+
# assume GW is always under specific path (TODO: remove this ?)
|
|
318
294
|
url = File.join(url, DEFAULT_BASE_PATH) unless url.end_with?(DEFAULT_BASE_PATH)
|
|
319
295
|
@gw_root_url = url
|
|
320
296
|
super(base_url: "#{@gw_root_url}/#{API_V1}", **opts)
|
|
@@ -334,6 +310,68 @@ module Aspera
|
|
|
334
310
|
end
|
|
335
311
|
end
|
|
336
312
|
end
|
|
313
|
+
|
|
314
|
+
private
|
|
315
|
+
|
|
316
|
+
# compute total size of files to upload (for progress)
|
|
317
|
+
# modify transfer spec to be suitable for HTTPGW
|
|
318
|
+
# @param transfer_spec [Hash] transfer specification
|
|
319
|
+
# @return [Array] info on files to send
|
|
320
|
+
def process_upload_list(transfer_spec)
|
|
321
|
+
total_bytes_to_transfer = 0
|
|
322
|
+
source_prefix = transfer_spec.key?('source_root') && !transfer_spec['source_root'].empty? ? transfer_spec['source_root'] + '/' : ''
|
|
323
|
+
files_to_send = []
|
|
324
|
+
transfer_spec['paths'].each do |one_path|
|
|
325
|
+
source_path = source_prefix + one_path['source']
|
|
326
|
+
faux_file = Transfer::FauxFile.create(source_path)
|
|
327
|
+
if faux_file
|
|
328
|
+
total_bytes_to_transfer += faux_file.size
|
|
329
|
+
files_to_send.push({
|
|
330
|
+
file: faux_file,
|
|
331
|
+
name: faux_file.path,
|
|
332
|
+
size: faux_file.size
|
|
333
|
+
})
|
|
334
|
+
elsif File.file?(source_path)
|
|
335
|
+
# regular file
|
|
336
|
+
file_size = File.size(source_path)
|
|
337
|
+
total_bytes_to_transfer += file_size
|
|
338
|
+
files_to_send.push({
|
|
339
|
+
file: source_path,
|
|
340
|
+
# GW expects a simple file name in 'source' but if user wants to change the name, we take it
|
|
341
|
+
name: File.basename(one_path['destination'].nil? ? source_path : one_path['destination']),
|
|
342
|
+
size: file_size
|
|
343
|
+
})
|
|
344
|
+
elsif File.directory?(source_path)
|
|
345
|
+
folders_to_process = [source_path]
|
|
346
|
+
until folders_to_process.empty?
|
|
347
|
+
folder = folders_to_process.shift
|
|
348
|
+
# read all entries
|
|
349
|
+
Dir.entries(folder).each do |entry|
|
|
350
|
+
next if entry.eql?('.') || entry.eql?('..')
|
|
351
|
+
entry_path = File.join(folder, entry)
|
|
352
|
+
if File.directory?(entry_path)
|
|
353
|
+
folders_to_process.push(entry_path)
|
|
354
|
+
elsif File.file?(entry_path)
|
|
355
|
+
file_size = File.size(entry_path)
|
|
356
|
+
total_bytes_to_transfer += file_size
|
|
357
|
+
files_to_send.push({
|
|
358
|
+
file: entry_path,
|
|
359
|
+
name: entry_path,
|
|
360
|
+
size: file_size
|
|
361
|
+
})
|
|
362
|
+
else
|
|
363
|
+
Log.log.warn{"Ignoring non file/directory: #{entry_path}"}
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
else
|
|
368
|
+
raise "File not found: #{source_path}"
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
transfer_spec['paths'] = files_to_send.map{|i|{'source' => i[:name]}}
|
|
372
|
+
files_to_send.push(total_bytes_to_transfer)
|
|
373
|
+
return files_to_send
|
|
374
|
+
end
|
|
337
375
|
end
|
|
338
376
|
end
|
|
339
377
|
end
|
data/lib/aspera/api/node.rb
CHANGED
|
@@ -14,23 +14,28 @@ module Aspera
|
|
|
14
14
|
module Api
|
|
15
15
|
# Provides additional functions using node API with gen4 extensions (access keys)
|
|
16
16
|
class Node < Aspera::Rest
|
|
17
|
-
# node api permissions
|
|
18
|
-
ACCESS_LEVELS = %w[delete list mkdir preview read rename write].freeze
|
|
19
|
-
HEADER_X_ASPERA_ACCESS_KEY = 'X-Aspera-AccessKey'
|
|
20
17
|
SCOPE_SEPARATOR = ':'
|
|
21
|
-
SCOPE_USER = 'user:all'
|
|
22
|
-
SCOPE_ADMIN = 'admin:all'
|
|
23
18
|
SCOPE_NODE_PREFIX = 'node.'
|
|
24
19
|
# prefix for ruby code for filter (deprecated)
|
|
25
20
|
MATCH_EXEC_PREFIX = 'exec:'
|
|
26
21
|
MATCH_TYPES = [String, Proc, Regexp, NilClass].freeze
|
|
27
|
-
PATH_SEPARATOR = '/'
|
|
28
22
|
SIGNATURE_DELIMITER = '==SIGNATURE=='
|
|
29
23
|
BEARER_TOKEN_VALIDITY_DEFAULT = 86400
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
# fields in @app_info
|
|
25
|
+
REQUIRED_APP_INFO_FIELDS = %i[api app node_info workspace_id workspace_name].freeze
|
|
26
|
+
# methods of @app_info[:api]
|
|
27
|
+
REQUIRED_APP_API_METHODS = %i[node_api_from add_ts_tags].freeze
|
|
28
|
+
private_constant :SCOPE_SEPARATOR, :SCOPE_NODE_PREFIX, :MATCH_EXEC_PREFIX, :MATCH_TYPES,
|
|
29
|
+
:SIGNATURE_DELIMITER, :BEARER_TOKEN_VALIDITY_DEFAULT,
|
|
30
|
+
:REQUIRED_APP_INFO_FIELDS, :REQUIRED_APP_API_METHODS
|
|
31
|
+
|
|
32
|
+
# node api permissions
|
|
33
|
+
ACCESS_LEVELS = %w[delete list mkdir preview read rename write].freeze
|
|
34
|
+
HEADER_X_ASPERA_ACCESS_KEY = 'X-Aspera-AccessKey'
|
|
35
|
+
HEADER_X_TOTAL_COUNT = 'X-Total-Count'
|
|
36
|
+
SCOPE_USER = 'user:all'
|
|
37
|
+
SCOPE_ADMIN = 'admin:all'
|
|
38
|
+
PATH_SEPARATOR = '/'
|
|
34
39
|
|
|
35
40
|
# register node special token decoder
|
|
36
41
|
OAuth::Factory.instance.register_decoder(lambda{|token|Node.decode_bearer_token(token)})
|
|
@@ -59,7 +64,7 @@ module Aspera
|
|
|
59
64
|
end
|
|
60
65
|
|
|
61
66
|
def file_matcher_from_argument(options)
|
|
62
|
-
return file_matcher(options.get_next_argument('filter',
|
|
67
|
+
return file_matcher(options.get_next_argument('filter', validation: MATCH_TYPES, mandatory: false))
|
|
63
68
|
end
|
|
64
69
|
|
|
65
70
|
# node API scopes
|
|
@@ -86,7 +91,7 @@ module Aspera
|
|
|
86
91
|
# manage convenience parameters
|
|
87
92
|
expiration_sec = payload['_validity'] || BEARER_TOKEN_VALIDITY_DEFAULT
|
|
88
93
|
payload.delete('_validity')
|
|
89
|
-
scope = payload['_scope'] ||
|
|
94
|
+
scope = payload['_scope'] || SCOPE_USER
|
|
90
95
|
payload.delete('_scope')
|
|
91
96
|
payload['scope'] ||= token_scope(access_key, scope)
|
|
92
97
|
payload['auth_type'] ||= 'access_key'
|
|
@@ -117,19 +122,13 @@ module Aspera
|
|
|
117
122
|
end
|
|
118
123
|
end
|
|
119
124
|
|
|
120
|
-
# fields in @app_info
|
|
121
|
-
REQUIRED_APP_INFO_FIELDS = %i[api app node_info workspace_id workspace_name].freeze
|
|
122
|
-
# methods of @app_info[:api]
|
|
123
|
-
REQUIRED_APP_API_METHODS = %i[node_api_from add_ts_tags].freeze
|
|
124
|
-
private_constant :REQUIRED_APP_INFO_FIELDS, :REQUIRED_APP_API_METHODS
|
|
125
|
-
|
|
126
125
|
attr_reader :app_info
|
|
127
126
|
|
|
127
|
+
# @param app_info [Hash,NilClass] Special processing for AoC
|
|
128
|
+
# @param add_tspec [Hash,NilClass] Additional transfer spec
|
|
128
129
|
# @param base_url [String] Rest parameters
|
|
129
130
|
# @param auth [String,NilClass] Rest parameters
|
|
130
131
|
# @param headers [String,NilClass] Rest parameters
|
|
131
|
-
# @param app_info [Hash,NilClass] Special processing for AoC
|
|
132
|
-
# @param add_tspec [Hash,NilClass] Additional transfer spec
|
|
133
132
|
def initialize(app_info: nil, add_tspec: nil, **rest_args)
|
|
134
133
|
# init Rest
|
|
135
134
|
super(**rest_args)
|
|
@@ -162,25 +161,42 @@ module Aspera
|
|
|
162
161
|
workspace_id: @app_info[:workspace_id],
|
|
163
162
|
workspace_name: @app_info[:workspace_name])
|
|
164
163
|
end
|
|
165
|
-
Log.log.warn{"
|
|
164
|
+
Log.log.warn{"Cannot resolve link with node id #{node_id}, no resolver"}
|
|
166
165
|
return nil
|
|
167
166
|
end
|
|
168
167
|
|
|
168
|
+
# Check if a link entry in folder has target information
|
|
169
|
+
# @param entry [Hash] entry in folder
|
|
170
|
+
# @return [Boolean] true if target information is available
|
|
171
|
+
def entry_has_link_information(entry)
|
|
172
|
+
# if target information is missing in folder, try to get it on entry
|
|
173
|
+
if entry['target_node_id'].nil? || entry['target_id'].nil?
|
|
174
|
+
link_entry = read("files/#{entry['id']}")[:data]
|
|
175
|
+
entry['target_node_id'] = link_entry['target_node_id']
|
|
176
|
+
entry['target_id'] = link_entry['target_id']
|
|
177
|
+
end
|
|
178
|
+
return true unless entry['target_node_id'].nil? || entry['target_id'].nil?
|
|
179
|
+
Log.log.warn{"Missing target information for link: #{entry['name']}"}
|
|
180
|
+
return false
|
|
181
|
+
end
|
|
182
|
+
|
|
169
183
|
# Recursively browse in a folder (with non-recursive method)
|
|
170
184
|
# sub folders are processed if the processing method returns true
|
|
185
|
+
# links are processed on the respective node
|
|
171
186
|
# @param state [Object] state object sent to processing method
|
|
172
187
|
# @param top_file_id [String] file id to start at (default = access key root file id)
|
|
173
188
|
# @param top_file_path [String] path of top folder (default = /)
|
|
174
189
|
# @param block [Proc] processing method, arguments: entry, path, state
|
|
175
|
-
def process_folder_tree(state:, top_file_id:, top_file_path: '/'
|
|
190
|
+
def process_folder_tree(method_sym:, state:, top_file_id:, top_file_path: '/')
|
|
176
191
|
Aspera.assert(!top_file_path.nil?){'top_file_path not set'}
|
|
177
|
-
|
|
192
|
+
Log.log.debug{"process_folder_tree: node=#{@app_info ? @app_info[:node_info]['id'] : 'nil'}, file id=#{top_file_id}, path=#{top_file_path}"}
|
|
178
193
|
# start at top folder
|
|
179
194
|
folders_to_explore = [{id: top_file_id, path: top_file_path}]
|
|
180
195
|
Log.log.debug{Log.dump(:folders_to_explore, folders_to_explore)}
|
|
181
196
|
until folders_to_explore.empty?
|
|
197
|
+
# consume first in job list
|
|
182
198
|
current_item = folders_to_explore.shift
|
|
183
|
-
Log.log.debug{"
|
|
199
|
+
Log.log.debug{"Exploring #{current_item[:path]}".bg_green}
|
|
184
200
|
# get folder content
|
|
185
201
|
folder_contents =
|
|
186
202
|
begin
|
|
@@ -192,19 +208,21 @@ module Aspera
|
|
|
192
208
|
Log.log.debug{Log.dump(:folder_contents, folder_contents)}
|
|
193
209
|
folder_contents.each do |entry|
|
|
194
210
|
relative_path = File.join(current_item[:path], entry['name'])
|
|
195
|
-
Log.log.debug{"process_folder_tree checking #{relative_path}"}
|
|
196
|
-
# continue only if method returns true
|
|
197
|
-
next unless
|
|
211
|
+
Log.log.debug{"process_folder_tree: checking #{relative_path}"}
|
|
212
|
+
# call block, continue only if method returns true
|
|
213
|
+
next unless send(method_sym, entry, relative_path, state)
|
|
198
214
|
# entry type is file, folder or link
|
|
199
215
|
case entry['type']
|
|
200
216
|
when 'folder'
|
|
201
217
|
folders_to_explore.push({id: entry['id'], path: relative_path})
|
|
202
218
|
when 'link'
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
219
|
+
if entry_has_link_information(entry)
|
|
220
|
+
node_id_to_node(entry['target_node_id'])&.process_folder_tree(
|
|
221
|
+
method_sym: method_sym,
|
|
222
|
+
state: state,
|
|
223
|
+
top_file_id: entry['target_id'],
|
|
224
|
+
top_file_path: relative_path)
|
|
225
|
+
end
|
|
208
226
|
end
|
|
209
227
|
end
|
|
210
228
|
end
|
|
@@ -216,60 +234,23 @@ module Aspera
|
|
|
216
234
|
# @return [Hash] {.api,.file_id}
|
|
217
235
|
def resolve_api_fid(top_file_id, path)
|
|
218
236
|
Aspera.assert_type(top_file_id, String)
|
|
237
|
+
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
|
|
219
239
|
process_last_link = path.end_with?(PATH_SEPARATOR)
|
|
240
|
+
# keep only non-empty elements
|
|
220
241
|
path_elements = path.split(PATH_SEPARATOR).reject(&:empty?)
|
|
221
242
|
return {api: self, file_id: top_file_id} if path_elements.empty?
|
|
222
|
-
resolve_state = {path: path_elements, result: nil}
|
|
223
|
-
process_folder_tree(state: resolve_state, top_file_id: top_file_id)
|
|
224
|
-
# this block is called recursively for each entry in folder
|
|
225
|
-
# stop digging here if not in right path
|
|
226
|
-
next false unless entry['name'].eql?(state[:path].first)
|
|
227
|
-
# ok it matches, so we remove the match
|
|
228
|
-
state[:path].shift
|
|
229
|
-
case entry['type']
|
|
230
|
-
when 'file'
|
|
231
|
-
# file must be terminal
|
|
232
|
-
raise "#{entry['name']} is a file, expecting folder to find: #{state[:path]}" unless state[:path].empty?
|
|
233
|
-
# it's terminal, we found it
|
|
234
|
-
state[:result] = {api: self, file_id: entry['id']}
|
|
235
|
-
next false
|
|
236
|
-
when 'folder'
|
|
237
|
-
if state[:path].empty?
|
|
238
|
-
# we found it
|
|
239
|
-
state[:result] = {api: self, file_id: entry['id']}
|
|
240
|
-
next false
|
|
241
|
-
end
|
|
242
|
-
when 'link'
|
|
243
|
-
if state[:path].empty?
|
|
244
|
-
if process_last_link
|
|
245
|
-
# we found it
|
|
246
|
-
other_node = node_id_to_node(entry['target_node_id'])
|
|
247
|
-
raise 'cannot resolve link' if other_node.nil?
|
|
248
|
-
state[:result] = {api: other_node, file_id: entry['target_id']}
|
|
249
|
-
else
|
|
250
|
-
# we found it but we do not process the link
|
|
251
|
-
state[:result] = {api: self, file_id: entry['id']}
|
|
252
|
-
end
|
|
253
|
-
next false
|
|
254
|
-
end
|
|
255
|
-
else
|
|
256
|
-
Log.log.warn{"Unknown element type: #{entry['type']}"}
|
|
257
|
-
end
|
|
258
|
-
# continue to dig folder
|
|
259
|
-
next true
|
|
260
|
-
end
|
|
243
|
+
resolve_state = {path: path_elements, result: nil, process_last_link: process_last_link}
|
|
244
|
+
process_folder_tree(method_sym: :process_api_fid, state: resolve_state, top_file_id: top_file_id)
|
|
261
245
|
raise "entry not found: #{resolve_state[:path]}" if resolve_state[:result].nil?
|
|
246
|
+
Log.log.debug{"resolve_api_fid: #{path} -> #{resolve_state[:result][:api].base_url} #{resolve_state[:result][:file_id]}"}
|
|
262
247
|
return resolve_state[:result]
|
|
263
248
|
end
|
|
264
249
|
|
|
265
250
|
def find_files(top_file_id, test_block)
|
|
266
251
|
Log.log.debug{"find_files: file id=#{top_file_id}"}
|
|
267
252
|
find_state = {found: [], test_block: test_block}
|
|
268
|
-
process_folder_tree(state: find_state, top_file_id: top_file_id)
|
|
269
|
-
state[:found].push(entry.merge({'path' => path})) if state[:test_block].call(entry)
|
|
270
|
-
# test all files deeply
|
|
271
|
-
true
|
|
272
|
-
end
|
|
253
|
+
process_folder_tree(method_sym: :process_find_files, state: find_state, top_file_id: top_file_id)
|
|
273
254
|
return find_state[:found]
|
|
274
255
|
end
|
|
275
256
|
|
|
@@ -306,7 +287,7 @@ module Aspera
|
|
|
306
287
|
ak_name = params[:headers][HEADER_X_ASPERA_ACCESS_KEY]
|
|
307
288
|
# TODO: token_generation_lambda = lambda{|do_refresh|oauth.token(refresh: do_refresh)}
|
|
308
289
|
# get bearer token, possibly use cache
|
|
309
|
-
ak_token = oauth.token
|
|
290
|
+
ak_token = oauth.token
|
|
310
291
|
else Aspera.error_unexpected_value(auth_params[:type])
|
|
311
292
|
end
|
|
312
293
|
transfer_spec = {
|
|
@@ -352,6 +333,58 @@ module Aspera
|
|
|
352
333
|
unless transfer_spec['remote_user'].eql?(Transfer::Spec::ACCESS_KEY_TRANSFER_USER)
|
|
353
334
|
return transfer_spec
|
|
354
335
|
end
|
|
336
|
+
|
|
337
|
+
private
|
|
338
|
+
|
|
339
|
+
def process_api_fid(entry, path, state)
|
|
340
|
+
# this block is called recursively for each entry in folder
|
|
341
|
+
# stop digging here if not in right path
|
|
342
|
+
return false unless entry['name'].eql?(state[:path].first)
|
|
343
|
+
# ok it matches, so we remove the match, and continue digging
|
|
344
|
+
state[:path].shift
|
|
345
|
+
path_fully_consumed = state[:path].empty?
|
|
346
|
+
case entry['type']
|
|
347
|
+
when 'file'
|
|
348
|
+
# file must be terminal
|
|
349
|
+
raise "#{entry['name']} is a file, expecting folder to find: #{state[:path]}" unless path_fully_consumed
|
|
350
|
+
# it's terminal, we found it
|
|
351
|
+
Log.log.debug{"resolve_api_fid: found #{path} -> #{entry['id']}"}
|
|
352
|
+
state[:result] = {api: self, file_id: entry['id']}
|
|
353
|
+
return false
|
|
354
|
+
when 'folder'
|
|
355
|
+
if path_fully_consumed
|
|
356
|
+
# we found it
|
|
357
|
+
state[:result] = {api: self, file_id: entry['id']}
|
|
358
|
+
return false
|
|
359
|
+
end
|
|
360
|
+
when 'link'
|
|
361
|
+
if path_fully_consumed
|
|
362
|
+
if state[:process_last_link]
|
|
363
|
+
# we found it
|
|
364
|
+
other_node = nil
|
|
365
|
+
if entry_has_link_information(entry)
|
|
366
|
+
other_node = node_id_to_node(entry['target_node_id'])
|
|
367
|
+
end
|
|
368
|
+
raise 'Cannot resolve link' if other_node.nil?
|
|
369
|
+
state[:result] = {api: other_node, file_id: entry['target_id']}
|
|
370
|
+
else
|
|
371
|
+
# we found it but we do not process the link
|
|
372
|
+
state[:result] = {api: self, file_id: entry['id']}
|
|
373
|
+
end
|
|
374
|
+
return false
|
|
375
|
+
end
|
|
376
|
+
else
|
|
377
|
+
Log.log.warn{"Unknown element type: #{entry['type']}"}
|
|
378
|
+
end
|
|
379
|
+
# continue to dig folder
|
|
380
|
+
return true
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def process_find_files(entry, _path, state)
|
|
384
|
+
state[:found].push(entry.merge({'path' => path})) if state[:test_block].call(entry)
|
|
385
|
+
# test all files deeply
|
|
386
|
+
return true
|
|
387
|
+
end
|
|
355
388
|
end
|
|
356
389
|
end
|
|
357
390
|
end
|