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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +23 -0
  4. data/CONTRIBUTING.md +5 -12
  5. data/README.md +152 -84
  6. data/examples/build_exec +85 -0
  7. data/examples/build_package.sh +28 -0
  8. data/lib/aspera/agent/alpha.rb +4 -4
  9. data/lib/aspera/agent/base.rb +2 -0
  10. data/lib/aspera/agent/connect.rb +3 -4
  11. data/lib/aspera/agent/direct.rb +108 -104
  12. data/lib/aspera/agent/httpgw.rb +1 -1
  13. data/lib/aspera/api/aoc.rb +2 -2
  14. data/lib/aspera/api/httpgw.rb +95 -57
  15. data/lib/aspera/api/node.rb +110 -77
  16. data/lib/aspera/ascp/installation.rb +47 -32
  17. data/lib/aspera/ascp/management.rb +4 -1
  18. data/lib/aspera/ascp/products.rb +2 -8
  19. data/lib/aspera/cli/extended_value.rb +27 -14
  20. data/lib/aspera/cli/formatter.rb +35 -28
  21. data/lib/aspera/cli/main.rb +11 -11
  22. data/lib/aspera/cli/manager.rb +109 -94
  23. data/lib/aspera/cli/plugin.rb +4 -7
  24. data/lib/aspera/cli/plugin_factory.rb +10 -1
  25. data/lib/aspera/cli/plugins/aoc.rb +15 -14
  26. data/lib/aspera/cli/plugins/config.rb +35 -29
  27. data/lib/aspera/cli/plugins/faspex.rb +5 -4
  28. data/lib/aspera/cli/plugins/faspex5.rb +16 -13
  29. data/lib/aspera/cli/plugins/node.rb +50 -41
  30. data/lib/aspera/cli/plugins/orchestrator.rb +3 -2
  31. data/lib/aspera/cli/plugins/preview.rb +1 -1
  32. data/lib/aspera/cli/plugins/server.rb +2 -2
  33. data/lib/aspera/cli/plugins/shares.rb +11 -7
  34. data/lib/aspera/cli/special_values.rb +13 -0
  35. data/lib/aspera/cli/sync_actions.rb +73 -32
  36. data/lib/aspera/cli/transfer_agent.rb +3 -2
  37. data/lib/aspera/cli/transfer_progress.rb +1 -1
  38. data/lib/aspera/cli/version.rb +1 -1
  39. data/lib/aspera/environment.rb +100 -7
  40. data/lib/aspera/faspex_gw.rb +1 -1
  41. data/lib/aspera/keychain/encrypted_hash.rb +2 -0
  42. data/lib/aspera/log.rb +1 -0
  43. data/lib/aspera/node_simulator.rb +1 -1
  44. data/lib/aspera/oauth/jwt.rb +1 -1
  45. data/lib/aspera/oauth/url_json.rb +2 -0
  46. data/lib/aspera/oauth/web.rb +7 -6
  47. data/lib/aspera/rest.rb +46 -15
  48. data/lib/aspera/secret_hider.rb +3 -2
  49. data/lib/aspera/ssh.rb +1 -1
  50. data/lib/aspera/transfer/faux_file.rb +7 -5
  51. data/lib/aspera/transfer/parameters.rb +27 -19
  52. data/lib/aspera/transfer/spec.rb +8 -10
  53. data/lib/aspera/transfer/sync.rb +52 -47
  54. data/lib/aspera/web_auth.rb +0 -1
  55. data/lib/aspera/web_server_simple.rb +24 -13
  56. data.tar.gz.sig +0 -0
  57. metadata +5 -4
  58. metadata.gz.sig +0 -0
  59. data/examples/rubyc +0 -24
  60. data/lib/aspera/open_application.rb +0 -69
@@ -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
@@ -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
- # 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
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
- 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?
@@ -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 any
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
@@ -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
- BEARER_TOKEN_SCOPE_DEFAULT = SCOPE_USER
31
- private_constant :MATCH_EXEC_PREFIX, :MATCH_TYPES,
32
- :SIGNATURE_DELIMITER, :BEARER_TOKEN_VALIDITY_DEFAULT, :BEARER_TOKEN_SCOPE_DEFAULT,
33
- :SCOPE_SEPARATOR, :SCOPE_NODE_PREFIX
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', type: MATCH_TYPES, mandatory: false))
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'] || BEARER_TOKEN_SCOPE_DEFAULT
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{"cannot resolve link with node id #{node_id}"}
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: '/', &block)
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
- Aspera.assert(block){'Missing block'}
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{"searching #{current_item[:path]}".bg_green}
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 yield(entry, relative_path, state)
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
- node_id_to_node(entry['target_node_id'])&.process_folder_tree(
204
- state: state,
205
- top_file_id: entry['target_id'],
206
- top_file_path: relative_path,
207
- &block)
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) do |entry, _path, state|
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) do |entry, path, state|
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(refresh: false)
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