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.
- checksums.yaml +7 -0
- data/README.md +3592 -0
- data/bin/ascli +7 -0
- data/bin/asession +89 -0
- data/docs/Makefile +59 -0
- data/docs/README.erb.md +3012 -0
- data/docs/README.md +13 -0
- data/docs/diagrams.txt +49 -0
- data/docs/secrets.make +38 -0
- data/docs/test_env.conf +117 -0
- data/docs/transfer_spec.html +99 -0
- data/examples/aoc.rb +17 -0
- data/examples/proxy.pac +60 -0
- data/examples/transfer.rb +115 -0
- data/lib/aspera/api_detector.rb +60 -0
- data/lib/aspera/ascmd.rb +151 -0
- data/lib/aspera/ats_api.rb +43 -0
- data/lib/aspera/cli/basic_auth_plugin.rb +38 -0
- data/lib/aspera/cli/extended_value.rb +88 -0
- data/lib/aspera/cli/formater.rb +238 -0
- data/lib/aspera/cli/listener/line_dump.rb +17 -0
- data/lib/aspera/cli/listener/logger.rb +20 -0
- data/lib/aspera/cli/listener/progress.rb +52 -0
- data/lib/aspera/cli/listener/progress_multi.rb +91 -0
- data/lib/aspera/cli/main.rb +304 -0
- data/lib/aspera/cli/manager.rb +440 -0
- data/lib/aspera/cli/plugin.rb +90 -0
- data/lib/aspera/cli/plugins/alee.rb +24 -0
- data/lib/aspera/cli/plugins/ats.rb +231 -0
- data/lib/aspera/cli/plugins/bss.rb +71 -0
- data/lib/aspera/cli/plugins/config.rb +806 -0
- data/lib/aspera/cli/plugins/console.rb +62 -0
- data/lib/aspera/cli/plugins/cos.rb +106 -0
- data/lib/aspera/cli/plugins/faspex.rb +377 -0
- data/lib/aspera/cli/plugins/faspex5.rb +93 -0
- data/lib/aspera/cli/plugins/node.rb +438 -0
- data/lib/aspera/cli/plugins/oncloud.rb +937 -0
- data/lib/aspera/cli/plugins/orchestrator.rb +169 -0
- data/lib/aspera/cli/plugins/preview.rb +464 -0
- data/lib/aspera/cli/plugins/server.rb +216 -0
- data/lib/aspera/cli/plugins/shares.rb +63 -0
- data/lib/aspera/cli/plugins/shares2.rb +114 -0
- data/lib/aspera/cli/plugins/sync.rb +65 -0
- data/lib/aspera/cli/plugins/xnode.rb +115 -0
- data/lib/aspera/cli/transfer_agent.rb +251 -0
- data/lib/aspera/cli/version.rb +5 -0
- data/lib/aspera/colors.rb +39 -0
- data/lib/aspera/command_line_builder.rb +137 -0
- data/lib/aspera/fasp/aoc.rb +24 -0
- data/lib/aspera/fasp/connect.rb +99 -0
- data/lib/aspera/fasp/error.rb +21 -0
- data/lib/aspera/fasp/error_info.rb +60 -0
- data/lib/aspera/fasp/http_gw.rb +81 -0
- data/lib/aspera/fasp/installation.rb +240 -0
- data/lib/aspera/fasp/listener.rb +11 -0
- data/lib/aspera/fasp/local.rb +377 -0
- data/lib/aspera/fasp/manager.rb +69 -0
- data/lib/aspera/fasp/node.rb +88 -0
- data/lib/aspera/fasp/parameters.rb +235 -0
- data/lib/aspera/fasp/resume_policy.rb +76 -0
- data/lib/aspera/fasp/uri.rb +51 -0
- data/lib/aspera/faspex_gw.rb +196 -0
- data/lib/aspera/hash_ext.rb +28 -0
- data/lib/aspera/log.rb +80 -0
- data/lib/aspera/nagios.rb +71 -0
- data/lib/aspera/node.rb +14 -0
- data/lib/aspera/oauth.rb +319 -0
- data/lib/aspera/on_cloud.rb +421 -0
- data/lib/aspera/open_application.rb +72 -0
- data/lib/aspera/persistency_action_once.rb +42 -0
- data/lib/aspera/persistency_folder.rb +91 -0
- data/lib/aspera/preview/file_types.rb +300 -0
- data/lib/aspera/preview/generator.rb +258 -0
- data/lib/aspera/preview/image_error.png +0 -0
- data/lib/aspera/preview/options.rb +35 -0
- data/lib/aspera/preview/utils.rb +131 -0
- data/lib/aspera/preview/video_error.png +0 -0
- data/lib/aspera/proxy_auto_config.erb.js +287 -0
- data/lib/aspera/proxy_auto_config.rb +34 -0
- data/lib/aspera/rest.rb +296 -0
- data/lib/aspera/rest_call_error.rb +13 -0
- data/lib/aspera/rest_error_analyzer.rb +98 -0
- data/lib/aspera/rest_errors_aspera.rb +58 -0
- data/lib/aspera/ssh.rb +53 -0
- data/lib/aspera/sync.rb +82 -0
- data/lib/aspera/temp_file_manager.rb +37 -0
- data/lib/aspera/uri_reader.rb +25 -0
- 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
|