aspera-cli 4.0.0.pre1

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 (88) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +3592 -0
  3. data/bin/ascli +7 -0
  4. data/bin/asession +89 -0
  5. data/docs/Makefile +59 -0
  6. data/docs/README.erb.md +3012 -0
  7. data/docs/README.md +13 -0
  8. data/docs/diagrams.txt +49 -0
  9. data/docs/secrets.make +38 -0
  10. data/docs/test_env.conf +117 -0
  11. data/docs/transfer_spec.html +99 -0
  12. data/examples/aoc.rb +17 -0
  13. data/examples/proxy.pac +60 -0
  14. data/examples/transfer.rb +115 -0
  15. data/lib/aspera/api_detector.rb +60 -0
  16. data/lib/aspera/ascmd.rb +151 -0
  17. data/lib/aspera/ats_api.rb +43 -0
  18. data/lib/aspera/cli/basic_auth_plugin.rb +38 -0
  19. data/lib/aspera/cli/extended_value.rb +88 -0
  20. data/lib/aspera/cli/formater.rb +238 -0
  21. data/lib/aspera/cli/listener/line_dump.rb +17 -0
  22. data/lib/aspera/cli/listener/logger.rb +20 -0
  23. data/lib/aspera/cli/listener/progress.rb +52 -0
  24. data/lib/aspera/cli/listener/progress_multi.rb +91 -0
  25. data/lib/aspera/cli/main.rb +304 -0
  26. data/lib/aspera/cli/manager.rb +440 -0
  27. data/lib/aspera/cli/plugin.rb +90 -0
  28. data/lib/aspera/cli/plugins/alee.rb +24 -0
  29. data/lib/aspera/cli/plugins/ats.rb +231 -0
  30. data/lib/aspera/cli/plugins/bss.rb +71 -0
  31. data/lib/aspera/cli/plugins/config.rb +806 -0
  32. data/lib/aspera/cli/plugins/console.rb +62 -0
  33. data/lib/aspera/cli/plugins/cos.rb +106 -0
  34. data/lib/aspera/cli/plugins/faspex.rb +377 -0
  35. data/lib/aspera/cli/plugins/faspex5.rb +93 -0
  36. data/lib/aspera/cli/plugins/node.rb +438 -0
  37. data/lib/aspera/cli/plugins/oncloud.rb +937 -0
  38. data/lib/aspera/cli/plugins/orchestrator.rb +169 -0
  39. data/lib/aspera/cli/plugins/preview.rb +464 -0
  40. data/lib/aspera/cli/plugins/server.rb +216 -0
  41. data/lib/aspera/cli/plugins/shares.rb +63 -0
  42. data/lib/aspera/cli/plugins/shares2.rb +114 -0
  43. data/lib/aspera/cli/plugins/sync.rb +65 -0
  44. data/lib/aspera/cli/plugins/xnode.rb +115 -0
  45. data/lib/aspera/cli/transfer_agent.rb +251 -0
  46. data/lib/aspera/cli/version.rb +5 -0
  47. data/lib/aspera/colors.rb +39 -0
  48. data/lib/aspera/command_line_builder.rb +137 -0
  49. data/lib/aspera/fasp/aoc.rb +24 -0
  50. data/lib/aspera/fasp/connect.rb +99 -0
  51. data/lib/aspera/fasp/error.rb +21 -0
  52. data/lib/aspera/fasp/error_info.rb +60 -0
  53. data/lib/aspera/fasp/http_gw.rb +81 -0
  54. data/lib/aspera/fasp/installation.rb +240 -0
  55. data/lib/aspera/fasp/listener.rb +11 -0
  56. data/lib/aspera/fasp/local.rb +377 -0
  57. data/lib/aspera/fasp/manager.rb +69 -0
  58. data/lib/aspera/fasp/node.rb +88 -0
  59. data/lib/aspera/fasp/parameters.rb +235 -0
  60. data/lib/aspera/fasp/resume_policy.rb +76 -0
  61. data/lib/aspera/fasp/uri.rb +51 -0
  62. data/lib/aspera/faspex_gw.rb +196 -0
  63. data/lib/aspera/hash_ext.rb +28 -0
  64. data/lib/aspera/log.rb +80 -0
  65. data/lib/aspera/nagios.rb +71 -0
  66. data/lib/aspera/node.rb +14 -0
  67. data/lib/aspera/oauth.rb +319 -0
  68. data/lib/aspera/on_cloud.rb +421 -0
  69. data/lib/aspera/open_application.rb +72 -0
  70. data/lib/aspera/persistency_action_once.rb +42 -0
  71. data/lib/aspera/persistency_folder.rb +91 -0
  72. data/lib/aspera/preview/file_types.rb +300 -0
  73. data/lib/aspera/preview/generator.rb +258 -0
  74. data/lib/aspera/preview/image_error.png +0 -0
  75. data/lib/aspera/preview/options.rb +35 -0
  76. data/lib/aspera/preview/utils.rb +131 -0
  77. data/lib/aspera/preview/video_error.png +0 -0
  78. data/lib/aspera/proxy_auto_config.erb.js +287 -0
  79. data/lib/aspera/proxy_auto_config.rb +34 -0
  80. data/lib/aspera/rest.rb +296 -0
  81. data/lib/aspera/rest_call_error.rb +13 -0
  82. data/lib/aspera/rest_error_analyzer.rb +98 -0
  83. data/lib/aspera/rest_errors_aspera.rb +58 -0
  84. data/lib/aspera/ssh.rb +53 -0
  85. data/lib/aspera/sync.rb +82 -0
  86. data/lib/aspera/temp_file_manager.rb +37 -0
  87. data/lib/aspera/uri_reader.rb +25 -0
  88. metadata +288 -0
@@ -0,0 +1,421 @@
1
+ require 'aspera/log'
2
+ require 'aspera/rest'
3
+ require 'aspera/hash_ext'
4
+ require 'base64'
5
+
6
+ module Aspera
7
+ class OnCloud < Rest
8
+ private
9
+ @@use_standard_ports = true
10
+
11
+ PRODUCT_NAME='Aspera on Cloud'
12
+ # Production domain of AoC
13
+ PROD_DOMAIN='ibmaspera.com'
14
+ # to avoid infinite loop in pub link redirection
15
+ MAX_REDIRECT=10
16
+ DEFAULT_CLIENT='aspera.global-cli-client'
17
+ # Random generator seed
18
+ # strings /Applications/Aspera\ Drive.app/Contents/MacOS/AsperaDrive|grep -E '.{100}==$'|base64 --decode
19
+ CLIENT_RANDOM={
20
+ 'aspera.drive' => '1FwelGbL9xsv3M8H-VXPs5k69OdbaMgfB66qfBqlELFPk6r9ANztmGMOSLqaXEWXEPwk-6JnMZ7-RaXAYLd5thLbcL3QzgeU',
21
+ 'aspera.global-cli-client' => 'bK_qpqFpP-OPEuRJ9mnmdw_ebLtpSqCnqhuAKfKdoLXC6OF2yLMgsfAMBmXg7XI_zplV4gBqNOvlJdgCxlP0Zjm4GsRsmprf'
22
+ }
23
+ # path in URL of public links
24
+ PATHS_PUBLIC_LINK=['/packages/public/receive','/packages/public/send','/files/public']
25
+ JWT_AUDIENCE='https://api.asperafiles.com/api/v1/oauth2/token'
26
+ OAUTH_API_SUBPATH='api/v1/oauth2'
27
+ DEFAULT_TSPEC_INFO={
28
+ 'remote_user' => 'xfer',
29
+ 'ssh_port' => 33001,
30
+ 'fasp_port' => 33001
31
+ }
32
+
33
+ private_constant :PRODUCT_NAME,:PROD_DOMAIN,:MAX_REDIRECT,:DEFAULT_CLIENT,:CLIENT_RANDOM,:PATHS_PUBLIC_LINK,:JWT_AUDIENCE,:OAUTH_API_SUBPATH,:DEFAULT_TSPEC_INFO
34
+
35
+ public
36
+ # various API scopes supported
37
+ SCOPE_FILES_SELF='self'
38
+ SCOPE_FILES_USER='user:all'
39
+ SCOPE_FILES_ADMIN='admin:all'
40
+ SCOPE_FILES_ADMIN_USER='admin-user:all'
41
+ SCOPE_FILES_ADMIN_USER_USER=SCOPE_FILES_ADMIN_USER+'+'+SCOPE_FILES_USER
42
+ SCOPE_NODE_USER='user:all'
43
+ SCOPE_NODE_ADMIN='admin:all'
44
+ PATH_SEPARATOR='/'
45
+ FILES_APP='files'
46
+ PACKAGES_APP='packages'
47
+
48
+ def self.get_client_ids(id,secret,client_name=DEFAULT_CLIENT)
49
+ return id,secret unless id.nil? and secret.nil?
50
+ return client_name,CLIENT_RANDOM[client_name].reverse
51
+ end
52
+
53
+ # @param url of AoC instance
54
+ # @return organization id in url and AoC domain: ibmaspera.com, asperafiles.com or qa.asperafiles.com, etc...
55
+ def self.parse_url(aoc_org_url)
56
+ uri=URI.parse(aoc_org_url.gsub(/\/+$/,''))
57
+ instance_fqdn=uri.host
58
+ Log.log.debug("instance_fqdn=#{instance_fqdn}")
59
+ raise "No host found in URL.Please check URL format: https://myorg.#{PROD_DOMAIN}" if instance_fqdn.nil?
60
+ organization,instance_domain=instance_fqdn.split('.',2)
61
+ Log.log.debug("instance_domain=#{instance_domain}")
62
+ Log.log.debug("organization=#{organization}")
63
+ raise "expecting a public FQDN for #{PRODUCT_NAME}" if instance_domain.nil?
64
+ return organization,instance_domain
65
+ end
66
+
67
+ def self.metering_api(entitlement_id,customer_id,api_domain=PROD_DOMAIN)
68
+ return Rest.new({
69
+ :base_url => "https://api.#{api_domain}/metering/v1",
70
+ :headers => {'X-Aspera-Entitlement-Authorization' => Rest.basic_creds(entitlement_id,customer_id)}
71
+ })
72
+ end
73
+
74
+ # node API scopes
75
+ def self.node_scope(access_key,scope)
76
+ return 'node.'+access_key+':'+scope
77
+ end
78
+
79
+ def self.set_use_default_ports(val)
80
+ @@use_standard_ports=val
81
+ end
82
+
83
+ # check option "link"
84
+ # if present try to get token value (resolve redirection if short links used)
85
+ # then set options url/token/auth
86
+ def self.resolve_pub_link(rest_opts,public_link_url)
87
+ return if public_link_url.nil?
88
+ # set to token if available after redirection
89
+ url_param_token_pair=nil
90
+ redirect_count=0
91
+ loop do
92
+ uri=URI.parse(public_link_url)
93
+ if PATHS_PUBLIC_LINK.include?(uri.path)
94
+ url_param_token_pair=URI::decode_www_form(uri.query).select{|e|e.first.eql?('token')}.first
95
+ if url_param_token_pair.nil?
96
+ raise ArgumentError,"link option must be URL with 'token' parameter"
97
+ end
98
+ # ok we get it !
99
+ rest_opts[:org_url]='https://'+uri.host
100
+ rest_opts[:auth][:grant]=:url_token
101
+ rest_opts[:auth][:url_token]=url_param_token_pair.last
102
+ return
103
+ end
104
+ Log.log.debug("no expected format: #{public_link_url}")
105
+ raise "exceeded max redirection: #{MAX_REDIRECT}" if redirect_count > MAX_REDIRECT
106
+ r = Net::HTTP.get_response(uri)
107
+ if r.code.start_with?("3")
108
+ public_link_url = r['location']
109
+ raise "no location in redirection" if public_link_url.nil?
110
+ Log.log.debug("redirect to: #{public_link_url}")
111
+ else
112
+ # not a redirection
113
+ raise ArgumentError,'link option must be redirect or have token parameter'
114
+ end
115
+ end # loop
116
+
117
+ raise RuntimeError,'too many redirections'
118
+ end
119
+
120
+ # @param :link,:url,:auth,:client_id,:client_secret,:scope,:redirect_uri,:private_key,:username,:subpath
121
+ def initialize(opt)
122
+ # access key secrets are provided out of band to get node api access
123
+ # key: access key
124
+ # value: associated secret
125
+ @secrets={}
126
+
127
+ # init rest params
128
+ aoc_rest_p={:auth=>{:type =>:oauth2}}
129
+ # shortcut to auth section
130
+ aoc_auth_p=aoc_rest_p[:auth]
131
+
132
+ # sets [:org_url], [:auth][:grant], [:auth][:url_token]
133
+ self.class.resolve_pub_link(aoc_rest_p,opt[:link])
134
+
135
+ if aoc_rest_p.has_key?(:org_url)
136
+ # Pub Link only: get org url from pub link
137
+ opt[:url] = aoc_rest_p[:org_url]
138
+ aoc_rest_p.delete(:org_url)
139
+ else
140
+ # else url is mandatory
141
+ raise ArgumentError,"Missing mandatory option: url" if opt[:url].nil?
142
+ end
143
+
144
+ # get org name and domain from url
145
+ organization,instance_domain=self.class.parse_url(opt[:url])
146
+ # this is the base API url (depends on domain, which could be "qa.xxx")
147
+ api_base_url="https://api.#{instance_domain}"
148
+ # base API URL
149
+ aoc_rest_p[:base_url]="#{api_base_url}/#{opt[:subpath]}"
150
+ # base auth URL
151
+ aoc_auth_p[:base_url] = "#{api_base_url}/#{OAUTH_API_SUBPATH}/#{organization}"
152
+
153
+ if !aoc_auth_p.has_key?(:grant)
154
+ raise ArgumentError,"Missing mandatory option: auth" if opt[:auth].nil?
155
+ aoc_auth_p[:grant] = opt[:auth]
156
+ end
157
+
158
+ aoc_auth_p[:client_id],aoc_auth_p[:client_secret] = self.class.get_client_ids(opt[:client_id],opt[:client_secret])
159
+ raise ArgumentError,"Missing mandatory option: scope" if opt[:scope].nil?
160
+ aoc_auth_p[:scope] = opt[:scope]
161
+
162
+ # fill other auth parameters based on Oauth method
163
+ case aoc_auth_p[:grant]
164
+ when :web
165
+ raise ArgumentError,"Missing mandatory option: redirect_uri" if opt[:redirect_uri].nil?
166
+ aoc_auth_p[:redirect_uri] = opt[:redirect_uri]
167
+ when :jwt
168
+ # add jwt payload for global ids
169
+ if CLIENT_RANDOM.keys.include?(aoc_auth_p[:client_id])
170
+ aoc_auth_p.merge!({:jwt_add=>{org: organization}})
171
+ end
172
+ raise ArgumentError,"Missing mandatory option: private_key" if opt[:private_key].nil?
173
+ raise ArgumentError,"Missing mandatory option: username" if opt[:username].nil?
174
+ private_key_PEM_string=opt[:private_key]
175
+ aoc_auth_p[:jwt_audience] = JWT_AUDIENCE
176
+ aoc_auth_p[:jwt_subject] = opt[:username]
177
+ aoc_auth_p[:jwt_private_key_obj] = OpenSSL::PKey::RSA.new(private_key_PEM_string)
178
+ when :url_token
179
+ # nothing more
180
+ else raise "ERROR: unsupported auth method: #{aoc_auth_p[:grant]}"
181
+ end
182
+ super(aoc_rest_p)
183
+ end
184
+
185
+ def add_secrets(secrets)
186
+ @secrets.merge!(secrets)
187
+ Log.log.debug("now secrets:#{secrets}")
188
+ nil
189
+ end
190
+
191
+ def has_secret(ak)
192
+ Log.log.debug("has key:#{ak} -> #{@secrets.has_key?(ak)}")
193
+ return @secrets.has_key?(ak)
194
+ end
195
+
196
+ # additional transfer spec (tags) for package information
197
+ def self.package_tags(package_info,operation)
198
+ return {'tags'=>{'aspera'=>{'files'=>{
199
+ 'package_id' => package_info['id'],
200
+ 'package_name' => package_info['name'],
201
+ 'package_operation' => operation
202
+ }}}}
203
+ end
204
+
205
+ # add details to show in analytics
206
+ def self.analytics_ts(app,direction,ws_id,ws_name)
207
+ # translate transfer to operation
208
+ operation=case direction
209
+ when 'send'; 'upload'
210
+ when 'receive'; 'download'
211
+ else raise "ERROR: unexpected value: #{direction}"
212
+ end
213
+
214
+ return {
215
+ 'tags' => {
216
+ 'aspera' => {
217
+ 'usage_id' => "aspera.files.workspace.#{ws_id}", # activity tracking
218
+ 'files' => {
219
+ 'files_transfer_action' => "#{operation}_#{app.gsub(/s$/,'')}",
220
+ 'workspace_name' => ws_name, # activity tracking
221
+ 'workspace_id' => ws_id,
222
+ }
223
+ }
224
+ }
225
+ }
226
+ end
227
+
228
+ # build ts addon for IBM Aspera Console (cookie)
229
+ def self.console_ts(app,user_name,user_email)
230
+ elements=[app,user_name,user_email].map{|e|Base64.strict_encode64(e)}
231
+ elements.unshift('aspera.aoc')
232
+ #Log.dump('elem1'.bg_red,elements[1])
233
+ return {
234
+ 'cookie'=>elements.join(':')
235
+ }
236
+ end
237
+
238
+ # build "transfer info", 2 elements array with:
239
+ # - transfer spec for aspera on cloud, based on node information and file id
240
+ # - source and token regeneration method
241
+ def tr_spec(app,direction,node_file,ts_add)
242
+ # prepare the rest end point is used to generate the bearer token
243
+ token_generation_method=lambda {|do_refresh|self.oauth_token(scope: self.class.node_scope(node_file[:node_info]['access_key'],SCOPE_NODE_USER), refresh: do_refresh)}
244
+ # prepare transfer specification
245
+ # note xfer_id and xfer_retry are set by the transfer agent itself
246
+ transfer_spec={
247
+ 'direction' => direction,
248
+ 'token' => token_generation_method.call(false), # first time, use cache
249
+ 'tags' => {
250
+ 'aspera' => {
251
+ 'app' => app,
252
+ 'files' => {
253
+ 'node_id' => node_file[:node_info]['id'],
254
+ }, # files
255
+ 'node' => {
256
+ 'access_key' => node_file[:node_info]['access_key'],
257
+ #'file_id' => ts_add['source_root_id']
258
+ 'file_id' => node_file[:file_id]
259
+ } # node
260
+ } # aspera
261
+ } # tags
262
+ }
263
+ # add remote host info
264
+ if @@use_standard_ports
265
+ transfer_spec.merge!(DEFAULT_TSPEC_INFO)
266
+ transfer_spec['remote_host']=node_file[:node_info]['host']
267
+ else
268
+ # retrieve values from API
269
+ std_t_spec=get_node_api(node_file[:node_info],SCOPE_NODE_USER).create('files/download_setup',{:transfer_requests => [ { :transfer_request => {:paths => [ {"source"=>'/'} ] } } ] } )[:data]['transfer_specs'].first['transfer_spec']
270
+ ['remote_host','remote_user','ssh_port','fasp_port'].each {|i| transfer_spec[i]=std_t_spec[i]}
271
+ end
272
+ # add caller provided transfer spec
273
+ transfer_spec.deep_merge!(ts_add)
274
+ # additional information for transfer agent
275
+ source_and_token_generator={
276
+ :src => :node_gen4,
277
+ :regenerate_token => token_generation_method
278
+ }
279
+ return transfer_spec,source_and_token_generator
280
+ end
281
+
282
+ # returns a node API for access key
283
+ # @param scope e.g. SCOPE_NODE_USER
284
+ # no scope: requires secret
285
+ # if secret provided beforehand: use it
286
+ def get_node_api(node_info,node_scope=nil)
287
+ node_rest_params={
288
+ :base_url => node_info['url'],
289
+ :headers => {'X-Aspera-AccessKey'=>node_info['access_key']},
290
+ }
291
+ ak_secret=@secrets[node_info['access_key']]
292
+ if ak_secret.nil? and node_scope.nil?
293
+ raise 'There must be at least one of: secret, node scope'
294
+ end
295
+ # if secret provided on command line or if there is no scope
296
+ if !ak_secret.nil? or node_scope.nil?
297
+ node_rest_params[:auth]={
298
+ :type => :basic,
299
+ :username => node_info['access_key'],
300
+ :password => ak_secret
301
+ }
302
+ else
303
+ node_rest_params[:auth]=self.params[:auth].clone
304
+ node_rest_params[:auth][:scope]=self.class.node_scope(node_info['access_key'],node_scope)
305
+ end
306
+ return Rest.new(node_rest_params)
307
+ end
308
+
309
+ # check that parameter has necessary types
310
+ # @return split values
311
+ def check_get_node_file(node_file)
312
+ raise "node_file must be Hash (got #{node_file.class})" unless node_file.is_a?(Hash)
313
+ raise "node_file must have 2 keys: :file_id and :node_info" unless node_file.keys.sort.eql?([:file_id,:node_info])
314
+ node_info=node_file[:node_info]
315
+ file_id=node_file[:file_id]
316
+ raise "node_info must be Hash (got #{node_info.class}: #{node_info})" unless node_info.is_a?(Hash)
317
+ raise 'node_info must have id' unless node_info.has_key?('id')
318
+ raise 'file_id is empty' if file_id.to_s.empty?
319
+ return node_info,file_id
320
+ end
321
+
322
+ # returns node api and folder_id from soft link
323
+ def read_asplnk(current_file_info)
324
+ new_node_api=get_node_api(self.read("nodes/#{current_file_info['target_node_id']}")[:data],SCOPE_NODE_USER)
325
+ return {:node_api=>new_node_api,:folder_id=>current_file_info['target_id']}
326
+ end
327
+
328
+ # @returns list of file paths that match given regex
329
+ def find_files( top_node_file, test_block )
330
+ top_node_info,top_file_id=check_get_node_file(top_node_file)
331
+ Log.log.debug("find_files: node_info=#{top_node_info}, fileid=#{top_file_id}")
332
+ result=[]
333
+ top_node_api=get_node_api(top_node_info,SCOPE_NODE_USER)
334
+ # initialize loop elements : list of folders to scan
335
+ # Note: top file id is necessarily a folder
336
+ items_to_explore=[{:node_api=>top_node_api,:folder_id=>top_file_id,:path=>''}]
337
+
338
+ while !items_to_explore.empty? do
339
+ current_item = items_to_explore.shift
340
+ Log.log.debug("searching #{current_item[:path]}".bg_green)
341
+ # get folder content
342
+ begin
343
+ folder_contents = current_item[:node_api].read("files/#{current_item[:folder_id]}/files")[:data]
344
+ rescue => e
345
+ Log.log.warn("#{current_item[:path]}: #{e.message}")
346
+ folder_contents=[]
347
+ end
348
+ # TODO: check if this is a folder or file ?
349
+ Log.dump(:folder_contents,folder_contents)
350
+ folder_contents.each do |current_file_info|
351
+ item_path=File.join(current_item[:path],current_file_info['name'])
352
+ Log.log.debug("looking #{item_path}".bg_green)
353
+ begin
354
+ # does item match ?
355
+ result.push(current_file_info.merge({'path'=>item_path})) if test_block.call(current_file_info)
356
+ # does it need further processing ?
357
+ case current_file_info['type']
358
+ when 'file'
359
+ Log.log.debug("testing : #{current_file_info['name']}")
360
+ when 'folder'
361
+ items_to_explore.push({:node_api=>current_item[:node_api],:folder_id=>current_file_info['id'],:path=>item_path})
362
+ when 'link' # .*.asp-lnk
363
+ items_to_explore.push(read_asplnk(current_file_info).merge({:path=>item_path}))
364
+ else
365
+ Log.log.error("unknown folder item type: #{current_file_info['type']}")
366
+ end
367
+ rescue => e
368
+ Log.log.error("#{item_path}: #{e.message}")
369
+ end
370
+ end
371
+ end
372
+ return result
373
+ end
374
+
375
+ # @return node information (returned by API) and file id, from a "/" based path
376
+ # supports links to secondary nodes
377
+ # input: Array(root node,file id), String path
378
+ # output: Array(node_info,file_id) for the given path
379
+ def resolve_node_file( top_node_file, element_path_string='' )
380
+ Log.log.debug("resolve_node_file: top_node_file=#{top_node_file}, path=#{element_path_string}")
381
+ # initialize loop invariants
382
+ current_node_info,current_file_id=check_get_node_file(top_node_file)
383
+ items_to_explore=element_path_string.split(PATH_SEPARATOR).select{|i| !i.empty?}
384
+
385
+ while !items_to_explore.empty? do
386
+ current_item = items_to_explore.shift
387
+ Log.log.debug "searching #{current_item}".bg_green
388
+ # get API if changed
389
+ current_node_api=get_node_api(current_node_info,SCOPE_NODE_USER) if current_node_api.nil?
390
+ # get folder content
391
+ folder_contents = current_node_api.read("files/#{current_file_id}/files")
392
+ Log.dump(:folder_contents,folder_contents)
393
+ matching_folders = folder_contents[:data].select { |i| i['name'].eql?(current_item)}
394
+ #Log.log.debug "matching_folders: #{matching_folders}"
395
+ raise "no such folder: #{current_item} in #{folder_contents[:data].map { |i| i['name']}}" if matching_folders.empty?
396
+ current_file_info = matching_folders.first
397
+ # process type of file
398
+ case current_file_info['type']
399
+ when 'file'
400
+ current_file_id=current_file_info['id']
401
+ # a file shall be terminal
402
+ if !items_to_explore.empty? then
403
+ raise "#{current_item} is a file, expecting folder to find: #{items_to_explore}"
404
+ end
405
+ when 'link'
406
+ current_node_info=self.read("nodes/#{current_file_info['target_node_id']}")[:data]
407
+ current_file_id=current_file_info['target_id']
408
+ # need to switch node
409
+ current_node_api=nil
410
+ when 'folder'
411
+ current_file_id=current_file_info['id']
412
+ else
413
+ Log.log.warn("unknown element type: #{current_file_info['type']}")
414
+ end
415
+ end
416
+ Log.log.info("resolve_node_file(#{element_path_string}): file_id=#{current_file_id},node_info=#{current_node_info}")
417
+ return {node_info: current_node_info, file_id: current_file_id}
418
+ end
419
+
420
+ end # OnCloud
421
+ end # Aspera
@@ -0,0 +1,72 @@
1
+ require 'aspera/log'
2
+ require 'rbconfig'
3
+ require 'singleton'
4
+
5
+ module Aspera
6
+ # Allows a user to open a Url
7
+ # if method is "text", then URL is displayed on terminal
8
+ # if method is "graphical", then the URL will be opened with the default browser.
9
+ class OpenApplication
10
+ include Singleton
11
+ # User Interfaces
12
+ def self.user_interfaces; [ :text, :graphical ]; end
13
+
14
+ def self.default_gui_mode
15
+ case current_os_type
16
+ when :windows,:mac
17
+ return :graphical
18
+ else # unix family
19
+ if ENV.has_key?("DISPLAY") and !ENV["DISPLAY"].empty?
20
+ return :graphical
21
+ end
22
+ return :text
23
+ end
24
+ end
25
+
26
+ def self.current_os_type
27
+ case RbConfig::CONFIG['host_os']
28
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
29
+ return :windows
30
+ when /darwin|mac os/
31
+ return :mac
32
+ else # unix family
33
+ return :unix
34
+ end
35
+ end
36
+
37
+ # command must be non blocking
38
+ def self.uri_graphical(uri)
39
+ case current_os_type
40
+ when :mac
41
+ return system('open',uri.to_s)
42
+ when :windows
43
+ return system('start explorer "'+uri.to_s+'"')
44
+ else # unix family
45
+ return system("xdg-open '#{uri.to_s}'")
46
+ end
47
+ end
48
+
49
+ attr_accessor :url_method
50
+
51
+ def initialize
52
+ @url_method=self.class.default_gui_mode
53
+ end
54
+
55
+ # this is non blocking
56
+ def uri(the_url)
57
+ case @url_method
58
+ when :graphical
59
+ self.class.uri_graphical(the_url)
60
+ when :text
61
+ case the_url.to_s
62
+ when /^http/
63
+ puts "USER ACTION: please enter this url in a browser:\n"+the_url.to_s.red()+"\n"
64
+ else
65
+ puts "USER ACTION: open this:\n"+the_url.to_s.red()+"\n"
66
+ end
67
+ else
68
+ raise StandardError,"unsupported url open method: #{@url_method}"
69
+ end
70
+ end
71
+ end # OpenApplication
72
+ end # Aspera