aspera-cli 4.0.0.pre3 → 4.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +695 -205
  3. data/bin/dascli +13 -0
  4. data/docs/README.erb.md +615 -157
  5. data/docs/test_env.conf +23 -5
  6. data/docs/transfer_spec.html +1 -1
  7. data/examples/aoc.rb +14 -3
  8. data/examples/faspex4.rb +78 -0
  9. data/lib/aspera/aoc.rb +87 -108
  10. data/lib/aspera/cli/formater.rb +2 -0
  11. data/lib/aspera/cli/main.rb +46 -34
  12. data/lib/aspera/cli/plugin.rb +9 -4
  13. data/lib/aspera/cli/plugins/alee.rb +1 -1
  14. data/lib/aspera/cli/plugins/aoc.rb +207 -182
  15. data/lib/aspera/cli/plugins/ats.rb +2 -2
  16. data/lib/aspera/cli/plugins/config.rb +173 -117
  17. data/lib/aspera/cli/plugins/console.rb +2 -2
  18. data/lib/aspera/cli/plugins/faspex.rb +51 -36
  19. data/lib/aspera/cli/plugins/faspex5.rb +82 -41
  20. data/lib/aspera/cli/plugins/node.rb +3 -3
  21. data/lib/aspera/cli/plugins/preview.rb +35 -25
  22. data/lib/aspera/cli/plugins/server.rb +23 -8
  23. data/lib/aspera/cli/transfer_agent.rb +7 -6
  24. data/lib/aspera/cli/version.rb +1 -1
  25. data/lib/aspera/cos_node.rb +33 -28
  26. data/lib/aspera/environment.rb +2 -2
  27. data/lib/aspera/fasp/connect.rb +28 -21
  28. data/lib/aspera/fasp/http_gw.rb +140 -28
  29. data/lib/aspera/fasp/installation.rb +101 -53
  30. data/lib/aspera/fasp/local.rb +88 -45
  31. data/lib/aspera/fasp/manager.rb +15 -0
  32. data/lib/aspera/fasp/node.rb +4 -4
  33. data/lib/aspera/fasp/parameters.rb +6 -18
  34. data/lib/aspera/fasp/resume_policy.rb +13 -12
  35. data/lib/aspera/log.rb +1 -1
  36. data/lib/aspera/node.rb +61 -1
  37. data/lib/aspera/oauth.rb +49 -46
  38. data/lib/aspera/persistency_folder.rb +9 -4
  39. data/lib/aspera/preview/file_types.rb +53 -21
  40. data/lib/aspera/preview/generator.rb +3 -3
  41. data/lib/aspera/rest.rb +29 -18
  42. data/lib/aspera/secrets.rb +20 -0
  43. data/lib/aspera/temp_file_manager.rb +19 -0
  44. data/lib/aspera/web_auth.rb +105 -0
  45. metadata +42 -22
data/docs/test_env.conf CHANGED
@@ -5,8 +5,8 @@ default:
5
5
  config: cli_default
6
6
  aoc: tst_aoc1
7
7
  faspex: tst_faspex
8
- faspex5: tst_faspex5
9
- shares: tst_shares
8
+ faspex5: tst_faspex5_web
9
+ shares: tst_shares_1
10
10
  shares2: tst_shares2
11
11
  node: tst_node
12
12
  server: tst_server
@@ -19,7 +19,6 @@ default:
19
19
  cli_default:
20
20
  interactive: your value here
21
21
  smtp: your value here
22
- ascp_path: your value here
23
22
  local_user:
24
23
  ssh_keys: your value here
25
24
  smtp_config:
@@ -48,10 +47,24 @@ tst_node_faspex:
48
47
  url: your value here
49
48
  username: your value here
50
49
  password: your value here
51
- tst_faspex5:
50
+ tst_faspex5_boot:
52
51
  url: your value here
52
+ auth: your value here
53
53
  username: your value here
54
54
  password: your value here
55
+ tst_faspex5_web:
56
+ url: your value here
57
+ auth: your value here
58
+ redirect_uri: your value here
59
+ client_id: your value here
60
+ client_secret: your value here
61
+ tst_faspex5:
62
+ url: your value here
63
+ auth: your value here
64
+ client_id: your value here
65
+ client_secret: your value here
66
+ private_key: your value here
67
+ username: your value here
55
68
  tst_shares:
56
69
  url: your value here
57
70
  username: your value here
@@ -94,6 +107,7 @@ tst_ak_preview:
94
107
  url: your value here
95
108
  username: your value here
96
109
  password: your value here
110
+ mimemagic: your value here
97
111
  tst_node_preview:
98
112
  url: your value here
99
113
  username: your value here
@@ -110,6 +124,8 @@ misc:
110
124
  faspex_publink_send_to_fxuser: your value here
111
125
  faspex_publink_send_to_dropbox: your value here
112
126
  shares_upload: your value here
127
+ console_smart_id: your value here
128
+ console_smart_file: your value here
113
129
  orch_workflow_id: your value here
114
130
  file_dcm: your value here
115
131
  file_pdf: your value here
@@ -122,6 +138,7 @@ misc:
122
138
  aoc_publink_recv_from_aocuser: your value here
123
139
  aoc_publink_send_shd_inbox: your value here
124
140
  aoc_publink_send_aoc_user: your value here
141
+ aoc_publink_send_use_pass: your value here
125
142
  aoc_publink_folder: your value here
126
143
  aoc_shbx_ws: your value here
127
144
  aoc_shbx_name: your value here
@@ -137,4 +154,5 @@ misc:
137
154
  email_internal: your value here
138
155
  email_external: your value here
139
156
  aoc_org: your value here
140
- aoc_user: your value here
157
+ aoc_user_email: your value here
158
+ http_gw_fqdn_port: your value here
@@ -28,7 +28,7 @@ arg: related ascp argument or env var suffix (PASS for ASPERA_SCP_PASS)
28
28
  </p>
29
29
  <p>
30
30
  UNDER CONSTRUCTION<br/>
31
- <a href="https://developer.ibm.com/api/view/aspera-prod:ibm-aspera:title-IBM_Aspera#id90944">Documentation&rarr;Node API&rarr;/opt/transfers</a><br/>
31
+ <a href="https://developer.ibm.com/apis/catalog/?search=aspera">Aspera API Documentation</a>&rarr;Node API&rarr;/opt/transfers<br/>
32
32
  </p>
33
33
 
34
34
  <table>
data/examples/aoc.rb CHANGED
@@ -4,11 +4,22 @@ require 'aspera/log'
4
4
 
5
5
  Aspera::Log.instance.level=:debug
6
6
 
7
+ if ! ARGV.length.eql?(3)
8
+ Aspera::Log.log.error("wrong number of args: #{ARGV.length}")
9
+ Aspera::Log.log.error("Usage: #{$0} <aoc URL> <aoc username> <aoc private key content>")
10
+ Aspera::Log.log.error("Example: #{$0} https://myorg.ibmaspera.com john@example.com $(cat /home/john/my_key.pem)")
11
+ Process.exit(1)
12
+ end
13
+
14
+ aoc_url=ARGV[0]
15
+ aoc_user=ARGV[1]
16
+ aoc_key_value=ARGV[2]
17
+
7
18
  aocapi=Aspera::AoC.new(
8
- url: 'https://myorg.ibmaspera.com',
19
+ url: aoc_url,
9
20
  auth: :jwt,
10
- private_key: File.read('path/to_your_private_key.pem'),
11
- username: 'my.email@example.com',
21
+ private_key: aoc_key_value,
22
+ username: aoc_user,
12
23
  scope: 'user:all',
13
24
  subpath: 'api/v1')
14
25
 
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env ruby
2
+ require 'aspera/rest'
3
+ require 'aspera/log'
4
+ require 'aspera/fasp/local'
5
+ require 'aspera/cli/listener/line_dump'
6
+ require 'aspera/cli/extended_value'
7
+
8
+ # set high log level for the example
9
+ Aspera::Log.instance.level=:debug
10
+
11
+ # set folder where SDK is installed
12
+ # (if ascp is not there, the lib will try to find in usual locations)
13
+ Aspera::Fasp::Installation.instance.folder='.'
14
+
15
+ if ! ARGV.length.eql?(3)
16
+ Aspera::Log.log.error("wrong number of args: #{ARGV.length}")
17
+ Aspera::Log.log.error("Usage: #{$0} <faspex URL> <faspex username> <faspex password>")
18
+ Aspera::Log.log.error("Example: #{$0} https://faspex.com/aspera/faspex john p@sSw0rd")
19
+ Process.exit(1)
20
+ end
21
+
22
+ faspex_url=ARGV[0]
23
+ faspex_user=ARGV[1]
24
+ faspex_pass=ARGV[2]
25
+
26
+ # 1: demo API v3
27
+ #---------------
28
+
29
+ # create REST API object
30
+ api_v3=Aspera::Rest.new({
31
+ :base_url => faspex_url,
32
+ :auth => {
33
+ :type => :basic,
34
+ :username => faspex_user,
35
+ :password => faspex_pass
36
+ }})
37
+
38
+ api_v3.read('me')
39
+
40
+ # 2: send a package
41
+ #---------------
42
+
43
+ # create a sample file to send
44
+ file_to_send='./myfile.bin'
45
+ File.open(file_to_send, "w") {|f| f.write("sample data") }
46
+ # package creation parameters
47
+ package_create_params={'delivery'=>{'title'=>'test package','recipients'=>['aspera.user1@gmail.com'],'sources'=>[{'paths'=>[file_to_send]}]}}
48
+ pkg_created=api_v3.create('send',package_create_params)[:data]
49
+ # get transfer specification
50
+ transfer_spec=pkg_created['xfer_sessions'].first
51
+ # set paths of files to send
52
+ transfer_spec['paths']=['source'=>file_to_send]
53
+ # get the local agent (i.e. ascp)
54
+ client=Aspera::Fasp::Local.new
55
+ # disable ascp output on stdout to not mix with JSON events
56
+ client.quiet=true
57
+ # start transfer (asynchronous)
58
+ job_id=client.start_transfer(transfer_spec)
59
+ result=client.wait_for_transfers_completion
60
+ # notify of any transfer error
61
+ result.select{|i|!i.eql?(:success)}.each do |e|
62
+ Aspera::Log.log.error("A transfer error occured: #{e.message}")
63
+ end
64
+
65
+ # 1: demo API v4
66
+ #---------------
67
+ api_v4=Aspera::Rest.new({
68
+ :base_url => faspex_url+'/api',
69
+ :auth => {
70
+ :type => :oauth2,
71
+ :base_url => faspex_url+'/auth/oauth2',
72
+ :grant => :header_userpass,
73
+ :user_name => faspex_user,
74
+ :user_pass => faspex_pass,
75
+ :scope => 'admin'
76
+ }})
77
+
78
+ Aspera::Log.dump('users',api_v4.read('users')[:data])
data/lib/aspera/aoc.rb CHANGED
@@ -9,6 +9,8 @@ module Aspera
9
9
  private
10
10
  @@use_standard_ports = true
11
11
 
12
+ API_V1='api/v1'
13
+
12
14
  PRODUCT_NAME='Aspera on Cloud'
13
15
  # Production domain of AoC
14
16
  PROD_DOMAIN='ibmaspera.com'
@@ -42,11 +44,6 @@ module Aspera
42
44
  FILES_APP='files'
43
45
  PACKAGES_APP='packages'
44
46
 
45
- CLIENT_RANDOM={
46
- 'aspera.global-cli-client' => 'frpmsRsG4mjZ0PlxCgdJlvONqBg4Vlpz_IX7gXmBMAfsgMLy2FO6CXLodKfKAuhqnCqSptLbe_wdmnm9JRuEPO-PpFqpq_Kb',
47
- 'aspera.drive' => 'UegzQ3LcbLht5dLYAXaR-7ZMnJ6-kwPEXWEXaqLSOMGmtzNA9r6kPFLElqBfq66BfgMabdO96k5sPXV-H8M3vsx9LbGlewF1'
48
- }
49
-
50
47
  def self.get_client_info(client_name=CLIENT_APPS.first)
51
48
  client_index=CLIENT_APPS.index(client_name)
52
49
  raise "no such pre-defined client: #{client_name}" if client_index.nil?
@@ -68,9 +65,14 @@ module Aspera
68
65
  return organization,instance_domain
69
66
  end
70
67
 
68
+ # base API url depends on domain, which could be "qa.xxx"
69
+ def self.api_base_url(api_domain=PROD_DOMAIN)
70
+ return "https://api.#{api_domain}"
71
+ end
72
+
71
73
  def self.metering_api(entitlement_id,customer_id,api_domain=PROD_DOMAIN)
72
74
  return Rest.new({
73
- :base_url => "https://api.#{api_domain}/metering/v1",
75
+ :base_url => "#{api_base_url(api_domain)}/metering/v1",
74
76
  :headers => {'X-Aspera-Entitlement-Authorization' => Rest.basic_creds(entitlement_id,customer_id)}
75
77
  })
76
78
  end
@@ -121,12 +123,12 @@ module Aspera
121
123
  raise RuntimeError,'too many redirections'
122
124
  end
123
125
 
124
- # @param :link,:url,:auth,:client_id,:client_secret,:scope,:redirect_uri,:private_key,:username,:subpath
126
+ # @param :link,:url,:auth,:client_id,:client_secret,:scope,:redirect_uri,:private_key,:username,:subpath,:password (for pub link)
125
127
  def initialize(opt)
126
128
  # access key secrets are provided out of band to get node api access
127
129
  # key: access key
128
130
  # value: associated secret
129
- @secrets={}
131
+ @key_chain=nil
130
132
 
131
133
  # init rest params
132
134
  aoc_rest_p={:auth=>{:type =>:oauth2}}
@@ -147,12 +149,12 @@ module Aspera
147
149
 
148
150
  # get org name and domain from url
149
151
  organization,instance_domain=self.class.parse_url(opt[:url])
150
- # this is the base API url (depends on domain, which could be "qa.xxx")
151
- api_base_url="https://api.#{instance_domain}"
152
- # base API URL
153
- aoc_rest_p[:base_url]="#{api_base_url}/#{opt[:subpath]}"
152
+ # this is the base API url
153
+ api_url_base=self.class.api_base_url(instance_domain)
154
+ # API URL, including subpath (version ...)
155
+ aoc_rest_p[:base_url]="#{api_url_base}/#{opt[:subpath]}"
154
156
  # base auth URL
155
- aoc_auth_p[:base_url] = "#{api_base_url}/#{OAUTH_API_SUBPATH}/#{organization}"
157
+ aoc_auth_p[:base_url] = "#{api_url_base}/#{OAUTH_API_SUBPATH}/#{organization}"
156
158
  aoc_auth_p[:client_id]=opt[:client_id]
157
159
  aoc_auth_p[:client_secret] = opt[:client_secret]
158
160
 
@@ -185,23 +187,19 @@ module Aspera
185
187
  aoc_auth_p[:jwt_subject] = opt[:username]
186
188
  aoc_auth_p[:jwt_private_key_obj] = OpenSSL::PKey::RSA.new(private_key_PEM_string)
187
189
  when :url_token
190
+ aoc_auth_p[:password]=opt[:password] unless opt[:password].nil?
188
191
  # nothing more
189
192
  else raise "ERROR: unsupported auth method: #{aoc_auth_p[:grant]}"
190
193
  end
191
194
  super(aoc_rest_p)
192
195
  end
193
196
 
194
- def add_secrets(secrets)
195
- @secrets.merge!(secrets)
196
- Log.log.debug("now secrets:#{secrets}")
197
+ def key_chain=(keychain)
198
+ raise "keychain already set" unless @key_chain.nil?
199
+ @key_chain=keychain
197
200
  nil
198
201
  end
199
202
 
200
- def has_secret(ak)
201
- Log.log.debug("has key:#{ak} -> #{@secrets.has_key?(ak)}")
202
- return @secrets.has_key?(ak)
203
- end
204
-
205
203
  # additional transfer spec (tags) for package information
206
204
  def self.package_tags(package_info,operation)
207
205
  return {'tags'=>{'aspera'=>{'files'=>{
@@ -293,13 +291,14 @@ module Aspera
293
291
  # no scope: requires secret
294
292
  # if secret provided beforehand: use it
295
293
  def get_node_api(node_info,node_scope=nil)
294
+ # X-Aspera-AccessKey required for bearer token only
296
295
  node_rest_params={
297
296
  :base_url => node_info['url'],
298
297
  :headers => {'X-Aspera-AccessKey'=>node_info['access_key']},
299
298
  }
300
- ak_secret=@secrets[node_info['access_key']]
299
+ ak_secret=@key_chain.get_secret(node_info['access_key'],false)
301
300
  if ak_secret.nil? and node_scope.nil?
302
- raise 'There must be at least one of: secret, node scope'
301
+ raise "There must be at least one of: 'secret' or 'scope' for access key #{node_info['access_key']}"
303
302
  end
304
303
  # if secret provided on command line or if there is no scope
305
304
  if !ak_secret.nil? or node_scope.nil?
@@ -312,7 +311,7 @@ module Aspera
312
311
  node_rest_params[:auth]=self.params[:auth].clone
313
312
  node_rest_params[:auth][:scope]=self.class.node_scope(node_info['access_key'],node_scope)
314
313
  end
315
- return Rest.new(node_rest_params)
314
+ return Node.new(node_rest_params)
316
315
  end
317
316
 
318
317
  # check that parameter has necessary types
@@ -328,102 +327,82 @@ module Aspera
328
327
  return node_info,file_id
329
328
  end
330
329
 
331
- # returns node api and folder_id from soft link
332
- def read_asplnk(current_file_info)
333
- new_node_api=get_node_api(self.read("nodes/#{current_file_info['target_node_id']}")[:data],SCOPE_NODE_USER)
334
- return {:node_api=>new_node_api,:folder_id=>current_file_info['target_id']}
330
+ # add entry to list if test block is success
331
+ def process_find_files(entry,path)
332
+ begin
333
+ # add to result if match filter
334
+ @find_state[:found].push(entry.merge({'path'=>path})) if @find_state[:test_block].call(entry)
335
+ # process link
336
+ if entry[:type].eql?('link')
337
+ sub_node_info=self.read("nodes/#{entry['target_node_id']}")[:data]
338
+ sub_opt={method: process_find_files, top_file_id: entry['target_id'], top_file_path: path}
339
+ get_node_api(sub_node_info,SCOPE_NODE_USER).crawl(self,sub_opt)
340
+ end
341
+ rescue => e
342
+ Log.log.error("#{path}: #{e.message}")
343
+ end
344
+ # process all folders
345
+ return true
335
346
  end
336
347
 
337
- # @returns list of file paths that match given regex
338
348
  def find_files( top_node_file, test_block )
339
349
  top_node_info,top_file_id=check_get_node_file(top_node_file)
340
350
  Log.log.debug("find_files: node_info=#{top_node_info}, fileid=#{top_file_id}")
341
- result=[]
342
- top_node_api=get_node_api(top_node_info,SCOPE_NODE_USER)
343
- # initialize loop elements : list of folders to scan
344
- # Note: top file id is necessarily a folder
345
- items_to_explore=[{:node_api=>top_node_api,:folder_id=>top_file_id,:path=>''}]
346
-
347
- while !items_to_explore.empty? do
348
- current_item = items_to_explore.shift
349
- Log.log.debug("searching #{current_item[:path]}".bg_green)
350
- # get folder content
351
- begin
352
- folder_contents = current_item[:node_api].read("files/#{current_item[:folder_id]}/files")[:data]
353
- rescue => e
354
- Log.log.warn("#{current_item[:path]}: #{e.message}")
355
- folder_contents=[]
351
+ @find_state={found: [], test_block: test_block}
352
+ get_node_api(top_node_info,SCOPE_NODE_USER).crawl(self,{method: :process_find_files, top_file_id: top_file_id})
353
+ result=@find_state[:found]
354
+ @find_state=nil
355
+ return result
356
+ end
357
+
358
+ def process_resolve_node_file(entry,path)
359
+ # stop digging here if not in right path
360
+ return false unless entry['name'].eql?(@resolve_state[:path].first)
361
+ # ok it matches, so we remove the match
362
+ @resolve_state[:path].shift
363
+ case entry['type']
364
+ when 'file'
365
+ # file must be terminal
366
+ raise "#{entry['name']} is a file, expecting folder to find: #{@resolve_state[:path]}" unless @resolve_state[:path].empty?
367
+ @resolve_state[:result][:file_id]=entry['id']
368
+ when 'link'
369
+ @resolve_state[:result][:node_info]=self.read("nodes/#{entry['target_node_id']}")[:data]
370
+ if @resolve_state[:path].empty?
371
+ @resolve_state[:result][:file_id]=entry['target_id']
372
+ else
373
+ get_node_api(@resolve_state[:result][:node_info],SCOPE_NODE_USER).crawl(self,{method: :process_resolve_node_file, top_file_id: entry['target_id']})
356
374
  end
357
- # TODO: check if this is a folder or file ?
358
- Log.dump(:folder_contents,folder_contents)
359
- folder_contents.each do |current_file_info|
360
- item_path=File.join(current_item[:path],current_file_info['name'])
361
- Log.log.debug("looking #{item_path}".bg_green)
362
- begin
363
- # does item match ?
364
- result.push(current_file_info.merge({'path'=>item_path})) if test_block.call(current_file_info)
365
- # does it need further processing ?
366
- case current_file_info['type']
367
- when 'file'
368
- Log.log.debug("testing : #{current_file_info['name']}")
369
- when 'folder'
370
- items_to_explore.push({:node_api=>current_item[:node_api],:folder_id=>current_file_info['id'],:path=>item_path})
371
- when 'link' # .*.asp-lnk
372
- items_to_explore.push(read_asplnk(current_file_info).merge({:path=>item_path}))
373
- else
374
- Log.log.error("unknown folder item type: #{current_file_info['type']}")
375
- end
376
- rescue => e
377
- Log.log.error("#{item_path}: #{e.message}")
378
- end
375
+ when 'folder'
376
+ if @resolve_state[:path].empty?
377
+ # found: store
378
+ @resolve_state[:result][:file_id]=entry['id']
379
+ return false
379
380
  end
381
+ else
382
+ Log.log.warn("unknown element type: #{entry['type']}")
380
383
  end
381
- return result
384
+ # continue to dig folder
385
+ return true
382
386
  end
383
387
 
384
- # @return node information (returned by API) and file id, from a "/" based path
388
+ # @return Array(node_info,file_id) for the given path
389
+ # @param top_node_file Array [root node,file id]
390
+ # @param element_path_string String path of element
385
391
  # supports links to secondary nodes
386
- # input: Array(root node,file id), String path
387
- # output: Array(node_info,file_id) for the given path
388
- def resolve_node_file( top_node_file, element_path_string='' )
389
- Log.log.debug("resolve_node_file: top_node_file=#{top_node_file}, path=#{element_path_string}")
390
- # initialize loop invariants
391
- current_node_info,current_file_id=check_get_node_file(top_node_file)
392
- items_to_explore=element_path_string.split(PATH_SEPARATOR).select{|i| !i.empty?}
393
-
394
- while !items_to_explore.empty? do
395
- current_item = items_to_explore.shift
396
- Log.log.debug "searching #{current_item}".bg_green
397
- # get API if changed
398
- current_node_api=get_node_api(current_node_info,SCOPE_NODE_USER) if current_node_api.nil?
399
- # get folder content
400
- folder_contents = current_node_api.read("files/#{current_file_id}/files")
401
- Log.dump(:folder_contents,folder_contents)
402
- matching_folders = folder_contents[:data].select { |i| i['name'].eql?(current_item)}
403
- #Log.log.debug "matching_folders: #{matching_folders}"
404
- raise "no such folder: #{current_item} in #{folder_contents[:data].map { |i| i['name']}}" if matching_folders.empty?
405
- current_file_info = matching_folders.first
406
- # process type of file
407
- case current_file_info['type']
408
- when 'file'
409
- current_file_id=current_file_info['id']
410
- # a file shall be terminal
411
- if !items_to_explore.empty? then
412
- raise "#{current_item} is a file, expecting folder to find: #{items_to_explore}"
413
- end
414
- when 'link'
415
- current_node_info=self.read("nodes/#{current_file_info['target_node_id']}")[:data]
416
- current_file_id=current_file_info['target_id']
417
- # need to switch node
418
- current_node_api=nil
419
- when 'folder'
420
- current_file_id=current_file_info['id']
421
- else
422
- Log.log.warn("unknown element type: #{current_file_info['type']}")
423
- end
392
+ def resolve_node_file( top_node_file, element_path_string )
393
+ top_node_info,top_file_id=check_get_node_file(top_node_file)
394
+ path_elements=element_path_string.split(PATH_SEPARATOR).select{|i| !i.empty?}
395
+ result={node_info: top_node_info, file_id: nil}
396
+ if path_elements.empty?
397
+ result[:file_id]=top_file_id
398
+ else
399
+ @resolve_state={path: path_elements, result: result}
400
+ get_node_api(top_node_info,SCOPE_NODE_USER).crawl(self,{method: :process_resolve_node_file, top_file_id: top_file_id})
401
+ not_found=@resolve_state[:path]
402
+ @resolve_state=nil
403
+ raise "entry not found: #{not_found}" if result[:file_id].nil?
424
404
  end
425
- Log.log.info("resolve_node_file(#{element_path_string}): file_id=#{current_file_id},node_info=#{current_node_info}")
426
- return {node_info: current_node_info, file_id: current_file_id}
405
+ return result
427
406
  end
428
407
 
429
408
  end # AoC