aspera-cli 4.4.0 → 4.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2095 -1503
- data/bin/ascli +2 -1
- data/bin/asession +4 -5
- data/docs/test_env.conf +3 -0
- data/examples/aoc.rb +4 -3
- data/examples/faspex4.rb +25 -25
- data/examples/proxy.pac +1 -1
- data/examples/transfer.rb +17 -17
- data/lib/aspera/aoc.rb +238 -185
- data/lib/aspera/ascmd.rb +93 -83
- data/lib/aspera/ats_api.rb +11 -10
- data/lib/aspera/cli/basic_auth_plugin.rb +13 -14
- data/lib/aspera/cli/extended_value.rb +42 -33
- data/lib/aspera/cli/formater.rb +142 -108
- data/lib/aspera/cli/info.rb +17 -0
- data/lib/aspera/cli/listener/line_dump.rb +3 -2
- data/lib/aspera/cli/listener/logger.rb +2 -1
- data/lib/aspera/cli/listener/progress.rb +16 -18
- data/lib/aspera/cli/listener/progress_multi.rb +18 -21
- data/lib/aspera/cli/main.rb +173 -149
- data/lib/aspera/cli/manager.rb +163 -168
- data/lib/aspera/cli/plugin.rb +43 -31
- data/lib/aspera/cli/plugins/alee.rb +6 -6
- data/lib/aspera/cli/plugins/aoc.rb +405 -370
- data/lib/aspera/cli/plugins/ats.rb +86 -79
- data/lib/aspera/cli/plugins/bss.rb +14 -16
- data/lib/aspera/cli/plugins/config.rb +580 -362
- data/lib/aspera/cli/plugins/console.rb +23 -19
- data/lib/aspera/cli/plugins/cos.rb +18 -18
- data/lib/aspera/cli/plugins/faspex.rb +201 -158
- data/lib/aspera/cli/plugins/faspex5.rb +80 -57
- data/lib/aspera/cli/plugins/node.rb +183 -166
- data/lib/aspera/cli/plugins/orchestrator.rb +71 -67
- data/lib/aspera/cli/plugins/preview.rb +92 -96
- data/lib/aspera/cli/plugins/server.rb +79 -75
- data/lib/aspera/cli/plugins/shares.rb +35 -19
- data/lib/aspera/cli/plugins/sync.rb +20 -22
- data/lib/aspera/cli/transfer_agent.rb +76 -113
- data/lib/aspera/cli/version.rb +2 -1
- data/lib/aspera/colors.rb +35 -27
- data/lib/aspera/command_line_builder.rb +48 -34
- data/lib/aspera/cos_node.rb +29 -21
- data/lib/aspera/data_repository.rb +3 -2
- data/lib/aspera/environment.rb +50 -45
- data/lib/aspera/fasp/{manager.rb → agent_base.rb} +28 -25
- data/lib/aspera/fasp/{connect.rb → agent_connect.rb} +52 -43
- data/lib/aspera/fasp/{local.rb → agent_direct.rb} +58 -72
- data/lib/aspera/fasp/{http_gw.rb → agent_httpgw.rb} +37 -43
- data/lib/aspera/fasp/{node.rb → agent_node.rb} +35 -16
- data/lib/aspera/fasp/agent_trsdk.rb +104 -0
- data/lib/aspera/fasp/error.rb +2 -1
- data/lib/aspera/fasp/error_info.rb +68 -52
- data/lib/aspera/fasp/installation.rb +152 -124
- data/lib/aspera/fasp/listener.rb +1 -0
- data/lib/aspera/fasp/parameters.rb +87 -92
- data/lib/aspera/fasp/parameters.yaml +305 -249
- data/lib/aspera/fasp/resume_policy.rb +11 -14
- data/lib/aspera/fasp/transfer_spec.rb +26 -0
- data/lib/aspera/fasp/uri.rb +22 -21
- data/lib/aspera/faspex_gw.rb +55 -89
- data/lib/aspera/hash_ext.rb +4 -3
- data/lib/aspera/id_generator.rb +8 -7
- data/lib/aspera/keychain/encrypted_hash.rb +121 -0
- data/lib/aspera/keychain/macos_security.rb +90 -0
- data/lib/aspera/log.rb +55 -37
- data/lib/aspera/nagios.rb +13 -12
- data/lib/aspera/node.rb +30 -25
- data/lib/aspera/oauth.rb +175 -226
- data/lib/aspera/open_application.rb +4 -3
- data/lib/aspera/persistency_action_once.rb +6 -6
- data/lib/aspera/persistency_folder.rb +5 -9
- data/lib/aspera/preview/file_types.rb +6 -5
- data/lib/aspera/preview/generator.rb +25 -24
- data/lib/aspera/preview/options.rb +16 -14
- data/lib/aspera/preview/utils.rb +98 -98
- data/lib/aspera/{proxy_auto_config.erb.js → proxy_auto_config.js} +23 -31
- data/lib/aspera/proxy_auto_config.rb +111 -20
- data/lib/aspera/rest.rb +154 -135
- data/lib/aspera/rest_call_error.rb +2 -2
- data/lib/aspera/rest_error_analyzer.rb +23 -25
- data/lib/aspera/rest_errors_aspera.rb +15 -14
- data/lib/aspera/ssh.rb +12 -10
- data/lib/aspera/sync.rb +42 -41
- data/lib/aspera/temp_file_manager.rb +18 -14
- data/lib/aspera/timer_limiter.rb +2 -1
- data/lib/aspera/uri_reader.rb +7 -5
- data/lib/aspera/web_auth.rb +79 -76
- metadata +116 -29
- data/docs/Makefile +0 -66
- data/docs/README.erb.md +0 -3973
- data/docs/README.md +0 -13
- data/docs/diagrams.txt +0 -49
- data/docs/doc_tools.rb +0 -58
- data/lib/aspera/api_detector.rb +0 -60
- data/lib/aspera/cli/plugins/shares2.rb +0 -114
- data/lib/aspera/secrets.rb +0 -20
data/lib/aspera/aoc.rb
CHANGED
@@ -1,38 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'aspera/log'
|
2
3
|
require 'aspera/rest'
|
3
4
|
require 'aspera/hash_ext'
|
4
5
|
require 'aspera/data_repository'
|
5
|
-
require 'aspera/
|
6
|
+
require 'aspera/fasp/transfer_spec'
|
6
7
|
require 'base64'
|
7
8
|
|
9
|
+
Aspera::Oauth.register_token_creator(:aoc_pub_link,lambda{|o|
|
10
|
+
o.token_auth_api.call({
|
11
|
+
operation: 'POST',
|
12
|
+
subpath: o.params[:path_token],
|
13
|
+
headers: {'Accept'=>'application/json'},
|
14
|
+
json_params: o.params[:aoc_pub_link][:json],
|
15
|
+
url_params: o.params[:aoc_pub_link][:url].merge(scope: o.params[:scope]) # scope is here because it changes over time (node)
|
16
|
+
})
|
17
|
+
})
|
18
|
+
|
8
19
|
module Aspera
|
9
20
|
class AoC < Rest
|
10
|
-
private
|
11
|
-
@@use_standard_ports = true
|
12
|
-
|
13
|
-
API_V1='api/v1'
|
14
|
-
|
15
21
|
PRODUCT_NAME='Aspera on Cloud'
|
16
22
|
# Production domain of AoC
|
17
23
|
PROD_DOMAIN='ibmaspera.com'
|
18
24
|
# to avoid infinite loop in pub link redirection
|
19
25
|
MAX_REDIRECT=10
|
20
|
-
|
26
|
+
# Well-known AoC globals client apps
|
27
|
+
GLOBAL_CLIENT_APPS=['aspera.global-cli-client','aspera.drive']
|
28
|
+
# index offset in data repository of client app
|
21
29
|
DATA_REPO_INDEX_START = 4
|
22
|
-
|
30
|
+
# cookie prefix so that console can decode identity
|
31
|
+
COOKIE_PREFIX='aspera.aoc'
|
23
32
|
# path in URL of public links
|
24
|
-
|
33
|
+
PUBLIC_LINK_PATHS=['/packages/public/receive','/packages/public/send','/files/public']
|
25
34
|
JWT_AUDIENCE='https://api.asperafiles.com/api/v1/oauth2/token'
|
26
35
|
OAUTH_API_SUBPATH='api/v1/oauth2'
|
27
|
-
|
28
|
-
|
29
|
-
'ssh_port' => Node::SSH_PORT_DEFAULT,
|
30
|
-
'fasp_port' => Node::UDP_PORT_DEFAULT
|
31
|
-
}
|
36
|
+
# minimum fields for user info if retrieval fails
|
37
|
+
USER_INFO_FIELDS_MIN=['name','email','id','default_workspace_id','organization_id']
|
32
38
|
|
33
|
-
private_constant :
|
39
|
+
private_constant :MAX_REDIRECT,:GLOBAL_CLIENT_APPS,:DATA_REPO_INDEX_START,:COOKIE_PREFIX,:PUBLIC_LINK_PATHS,:JWT_AUDIENCE,:OAUTH_API_SUBPATH,:USER_INFO_FIELDS_MIN
|
34
40
|
|
35
|
-
public
|
36
41
|
# various API scopes supported
|
37
42
|
SCOPE_FILES_SELF='self'
|
38
43
|
SCOPE_FILES_USER='user:all'
|
@@ -44,109 +49,142 @@ module Aspera
|
|
44
49
|
PATH_SEPARATOR='/'
|
45
50
|
FILES_APP='files'
|
46
51
|
PACKAGES_APP='packages'
|
52
|
+
API_V1='api/v1'
|
47
53
|
|
48
|
-
|
49
|
-
|
50
|
-
raise "no such pre-defined client: #{client_name}" if client_index.nil?
|
51
|
-
# strings /Applications/Aspera\ Drive.app/Contents/MacOS/AsperaDrive|grep -E '.{100}==$'|base64 --decode
|
52
|
-
return client_name,Base64.urlsafe_encode64(DataRepository.instance.get_bin(DATA_REPO_INDEX_START+client_index))
|
53
|
-
end
|
54
|
+
# class instance variable, access with accessors on class
|
55
|
+
@use_standard_ports = true
|
54
56
|
|
55
|
-
#
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
Log.log.debug("organization=#{organization}")
|
65
|
-
raise "expecting a public FQDN for #{PRODUCT_NAME}" if instance_domain.nil?
|
66
|
-
return organization,instance_domain
|
67
|
-
end
|
57
|
+
# class static methods
|
58
|
+
class << self
|
59
|
+
attr_accessor :use_standard_ports
|
60
|
+
# strings /Applications/Aspera\ Drive.app/Contents/MacOS/AsperaDrive|grep -E '.{100}==$'|base64 --decode
|
61
|
+
def get_client_info(client_name=GLOBAL_CLIENT_APPS.first)
|
62
|
+
client_index=GLOBAL_CLIENT_APPS.index(client_name)
|
63
|
+
raise "no such pre-defined client: #{client_name}" if client_index.nil?
|
64
|
+
return client_name,Base64.urlsafe_encode64(DataRepository.instance.data(DATA_REPO_INDEX_START+client_index))
|
65
|
+
end
|
68
66
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
67
|
+
# @param url of AoC instance
|
68
|
+
# @return organization id in url and AoC domain: ibmaspera.com, asperafiles.com or qa.asperafiles.com, etc...
|
69
|
+
def parse_url(aoc_org_url)
|
70
|
+
uri=URI.parse(aoc_org_url.gsub(/\/+$/,''))
|
71
|
+
instance_fqdn=uri.host
|
72
|
+
Log.log.debug("instance_fqdn=#{instance_fqdn}")
|
73
|
+
raise "No host found in URL.Please check URL format: https://myorg.#{PROD_DOMAIN}" if instance_fqdn.nil?
|
74
|
+
organization,instance_domain=instance_fqdn.split('.',2)
|
75
|
+
Log.log.debug("instance_domain=#{instance_domain}")
|
76
|
+
Log.log.debug("organization=#{organization}")
|
77
|
+
raise "expecting a public FQDN for #{PRODUCT_NAME}" if instance_domain.nil?
|
78
|
+
return organization,instance_domain
|
79
|
+
end
|
73
80
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
})
|
79
|
-
end
|
81
|
+
# base API url depends on domain, which could be "qa.xxx"
|
82
|
+
def api_base_url(api_domain=PROD_DOMAIN)
|
83
|
+
return "https://api.#{api_domain}"
|
84
|
+
end
|
80
85
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
86
|
+
def metering_api(entitlement_id,customer_id,api_domain=PROD_DOMAIN)
|
87
|
+
return Rest.new({
|
88
|
+
base_url: "#{api_base_url(api_domain)}/metering/v1",
|
89
|
+
headers: {'X-Aspera-Entitlement-Authorization' => Rest.basic_creds(entitlement_id,customer_id)}
|
90
|
+
})
|
91
|
+
end
|
85
92
|
|
86
|
-
|
87
|
-
|
88
|
-
|
93
|
+
# node API scopes
|
94
|
+
def node_scope(access_key,scope)
|
95
|
+
return "node.#{access_key}:#{scope}"
|
96
|
+
end
|
89
97
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
raise ArgumentError,
|
98
|
+
# check option "link"
|
99
|
+
# if present try to get token value (resolve redirection if short links used)
|
100
|
+
# then set options url/token/auth
|
101
|
+
def resolve_pub_link(a_auth,a_opt)
|
102
|
+
public_link_url=a_opt[:link]
|
103
|
+
return if public_link_url.nil?
|
104
|
+
raise 'do not use both link and url options' unless a_opt[:url].nil?
|
105
|
+
redirect_count=0
|
106
|
+
while redirect_count <= MAX_REDIRECT
|
107
|
+
uri=URI.parse(public_link_url)
|
108
|
+
# detect if it's an expected format
|
109
|
+
if PUBLIC_LINK_PATHS.include?(uri.path)
|
110
|
+
url_param_token_pair=URI.decode_www_form(uri.query).select{|e|e.first.eql?('token')}.first
|
111
|
+
raise ArgumentError,'link option must be URL with "token" parameter' if url_param_token_pair.nil?
|
112
|
+
# ok we get it !
|
113
|
+
a_opt[:url]='https://'+uri.host
|
114
|
+
a_auth[:crtype]=:aoc_pub_link
|
115
|
+
a_auth[:aoc_pub_link]={
|
116
|
+
url: {grant_type: 'url_token'}, # URL args
|
117
|
+
json: {url_token: url_param_token_pair.last} # JSON body
|
118
|
+
}
|
119
|
+
# password protection of link
|
120
|
+
a_auth[:aoc_pub_link][:json][:password]=a_opt[:password] unless a_opt[:password].nil?
|
121
|
+
return # SUCCESS
|
104
122
|
end
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
return
|
110
|
-
end
|
111
|
-
Log.log.debug("no expected format: #{public_link_url}")
|
112
|
-
raise "exceeded max redirection: #{MAX_REDIRECT}" if redirect_count > MAX_REDIRECT
|
113
|
-
r = Net::HTTP.get_response(uri)
|
114
|
-
if r.code.start_with?("3")
|
123
|
+
Log.log.debug("no expected format: #{public_link_url}")
|
124
|
+
r = Net::HTTP.get_response(uri)
|
125
|
+
# not a redirection
|
126
|
+
raise ArgumentError,'link option must be redirect or have token parameter' unless r.code.start_with?('3')
|
115
127
|
public_link_url = r['location']
|
116
|
-
raise
|
128
|
+
raise 'no location in redirection' if public_link_url.nil?
|
117
129
|
Log.log.debug("redirect to: #{public_link_url}")
|
118
|
-
|
119
|
-
|
120
|
-
|
130
|
+
end # loop
|
131
|
+
raise "exceeded max redirection: #{MAX_REDIRECT}"
|
132
|
+
end
|
133
|
+
|
134
|
+
# additional transfer spec (tags) for package information
|
135
|
+
def package_tags(package_info,operation)
|
136
|
+
return {'tags'=>{'aspera'=>{'files'=>{
|
137
|
+
'package_id' => package_info['id'],
|
138
|
+
'package_name' => package_info['name'],
|
139
|
+
'package_operation' => operation
|
140
|
+
}}}}
|
141
|
+
end
|
142
|
+
|
143
|
+
# add details to show in analytics
|
144
|
+
def analytics_ts(app,direction,ws_id,ws_name)
|
145
|
+
# translate transfer to operation
|
146
|
+
operation=
|
147
|
+
case direction
|
148
|
+
when Fasp::TransferSpec::DIRECTION_SEND then 'upload'
|
149
|
+
when Fasp::TransferSpec::DIRECTION_RECEIVE then 'download'
|
150
|
+
else raise "ERROR: unexpected value: #{direction}"
|
121
151
|
end
|
122
|
-
end # loop
|
123
152
|
|
124
|
-
|
125
|
-
|
153
|
+
return {
|
154
|
+
'tags' => {
|
155
|
+
'aspera' => {
|
156
|
+
'usage_id' => "aspera.files.workspace.#{ws_id}", # activity tracking
|
157
|
+
'files' => {
|
158
|
+
'files_transfer_action' => "#{operation}_#{app.gsub(/s$/,'')}",
|
159
|
+
'workspace_name' => ws_name, # activity tracking
|
160
|
+
'workspace_id' => ws_id
|
161
|
+
}
|
162
|
+
}
|
163
|
+
}
|
164
|
+
}
|
165
|
+
end
|
166
|
+
end # static methods
|
126
167
|
|
127
168
|
# @param :link,:url,:auth,:client_id,:client_secret,:scope,:redirect_uri,:private_key,:username,:subpath,:password (for pub link)
|
128
169
|
def initialize(opt)
|
170
|
+
raise ArgumentError,'Missing mandatory option: scope' if opt[:scope].nil?
|
171
|
+
|
129
172
|
# access key secrets are provided out of band to get node api access
|
130
173
|
# key: access key
|
131
174
|
# value: associated secret
|
132
175
|
@key_chain=nil
|
176
|
+
@user_info=nil
|
133
177
|
|
134
178
|
# init rest params
|
135
|
-
aoc_rest_p={:
|
179
|
+
aoc_rest_p={auth: {type: :oauth2}}
|
136
180
|
# shortcut to auth section
|
137
181
|
aoc_auth_p=aoc_rest_p[:auth]
|
138
182
|
|
139
|
-
# sets [:
|
140
|
-
self.class.resolve_pub_link(
|
183
|
+
# sets opt[:url], aoc_rest_p[:auth][:crtype], [:auth][:aoc_pub_link] if there is a link
|
184
|
+
self.class.resolve_pub_link(aoc_auth_p,opt)
|
141
185
|
|
142
|
-
|
143
|
-
|
144
|
-
opt[:url] = aoc_rest_p[:org_url]
|
145
|
-
aoc_rest_p.delete(:org_url)
|
146
|
-
else
|
147
|
-
# else url is mandatory
|
148
|
-
raise ArgumentError,"Missing mandatory option: url" if opt[:url].nil?
|
149
|
-
end
|
186
|
+
# test here because link may set url
|
187
|
+
raise ArgumentError,'Missing mandatory option: url' if opt[:url].nil?
|
150
188
|
|
151
189
|
# get org name and domain from url
|
152
190
|
organization,instance_domain=self.class.parse_url(opt[:url])
|
@@ -158,107 +196,98 @@ module Aspera
|
|
158
196
|
aoc_auth_p[:base_url] = "#{api_url_base}/#{OAUTH_API_SUBPATH}/#{organization}"
|
159
197
|
aoc_auth_p[:client_id]=opt[:client_id]
|
160
198
|
aoc_auth_p[:client_secret] = opt[:client_secret]
|
199
|
+
aoc_auth_p[:scope] = opt[:scope]
|
161
200
|
|
162
|
-
if
|
163
|
-
|
164
|
-
|
201
|
+
# filled if pub link
|
202
|
+
if !aoc_auth_p.has_key?(:crtype)
|
203
|
+
raise ArgumentError,'Missing mandatory option: auth' if opt[:auth].nil?
|
204
|
+
aoc_auth_p[:crtype] = opt[:auth]
|
165
205
|
end
|
166
206
|
|
167
207
|
if aoc_auth_p[:client_id].nil?
|
168
208
|
aoc_auth_p[:client_id],aoc_auth_p[:client_secret] = self.class.get_client_info()
|
169
209
|
end
|
170
210
|
|
171
|
-
raise ArgumentError,"Missing mandatory option: scope" if opt[:scope].nil?
|
172
|
-
aoc_auth_p[:scope] = opt[:scope]
|
173
|
-
|
174
211
|
# fill other auth parameters based on Oauth method
|
175
|
-
case aoc_auth_p[:
|
212
|
+
case aoc_auth_p[:crtype]
|
176
213
|
when :web
|
177
|
-
raise ArgumentError,
|
178
|
-
aoc_auth_p[:
|
214
|
+
raise ArgumentError,'Missing mandatory option: redirect_uri' if opt[:redirect_uri].nil?
|
215
|
+
aoc_auth_p[:web]={redirect_uri: opt[:redirect_uri]}
|
179
216
|
when :jwt
|
217
|
+
raise ArgumentError,'Missing mandatory option: private_key' if opt[:private_key].nil?
|
218
|
+
raise ArgumentError,'Missing mandatory option: username' if opt[:username].nil?
|
219
|
+
aoc_auth_p[:jwt]={
|
220
|
+
private_key_obj: OpenSSL::PKey::RSA.new(opt[:private_key]),
|
221
|
+
payload: {
|
222
|
+
iss: aoc_auth_p[:client_id], # issuer
|
223
|
+
sub: opt[:username], # subject
|
224
|
+
aud: JWT_AUDIENCE
|
225
|
+
}
|
226
|
+
}
|
180
227
|
# add jwt payload for global ids
|
181
|
-
if
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
private_key_PEM_string=opt[:private_key]
|
187
|
-
aoc_auth_p[:jwt_audience] = JWT_AUDIENCE
|
188
|
-
aoc_auth_p[:jwt_subject] = opt[:username]
|
189
|
-
aoc_auth_p[:jwt_private_key_obj] = OpenSSL::PKey::RSA.new(private_key_PEM_string)
|
190
|
-
when :url_token
|
191
|
-
aoc_auth_p[:password]=opt[:password] unless opt[:password].nil?
|
192
|
-
# nothing more
|
193
|
-
else raise "ERROR: unsupported auth method: #{aoc_auth_p[:grant]}"
|
228
|
+
aoc_auth_p[:jwt][:payload][:org]=organization if GLOBAL_CLIENT_APPS.include?(aoc_auth_p[:client_id])
|
229
|
+
when :aoc_pub_link
|
230
|
+
# basic auth required for /token
|
231
|
+
aoc_auth_p[:auth]={type: :basic, username: aoc_auth_p[:client_id],password: aoc_auth_p[:client_secret]}
|
232
|
+
else raise "ERROR: unsupported auth method: #{aoc_auth_p[:crtype]}"
|
194
233
|
end
|
195
234
|
super(aoc_rest_p)
|
196
235
|
end
|
197
236
|
|
198
|
-
def
|
199
|
-
|
200
|
-
|
201
|
-
|
237
|
+
def url_token_data
|
238
|
+
return nil unless params[:auth][:crtype].eql?(:aoc_pub_link)
|
239
|
+
# TODO: can there be several in list ?
|
240
|
+
return read('url_tokens')[:data].first
|
202
241
|
end
|
203
242
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
'package_name' => package_info['name'],
|
209
|
-
'package_operation' => operation
|
210
|
-
}}}}
|
243
|
+
def key_chain=(keychain)
|
244
|
+
raise 'keychain already set' unless @key_chain.nil?
|
245
|
+
raise 'keychain must have get_secret' unless keychain.respond_to?(:get_secret)
|
246
|
+
@key_chain=keychain
|
211
247
|
end
|
212
248
|
|
213
|
-
#
|
214
|
-
def
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
249
|
+
# cached user information
|
250
|
+
def user_info
|
251
|
+
if @user_info.nil?
|
252
|
+
# get our user's default information
|
253
|
+
@user_info=
|
254
|
+
begin
|
255
|
+
read('self')[:data]
|
256
|
+
rescue StandardError => e
|
257
|
+
Log.log.debug("ignoring error: #{e}")
|
258
|
+
{}
|
259
|
+
end
|
260
|
+
USER_INFO_FIELDS_MIN.each{|f|@user_info[f]='unknown' if @user_info[f].nil?}
|
220
261
|
end
|
221
|
-
|
222
|
-
return {
|
223
|
-
'tags' => {
|
224
|
-
'aspera' => {
|
225
|
-
'usage_id' => "aspera.files.workspace.#{ws_id}", # activity tracking
|
226
|
-
'files' => {
|
227
|
-
'files_transfer_action' => "#{operation}_#{app.gsub(/s$/,'')}",
|
228
|
-
'workspace_name' => ws_name, # activity tracking
|
229
|
-
'workspace_id' => ws_id,
|
230
|
-
}
|
231
|
-
}
|
232
|
-
}
|
233
|
-
}
|
262
|
+
return @user_info
|
234
263
|
end
|
235
264
|
|
236
265
|
# build ts addon for IBM Aspera Console (cookie)
|
237
|
-
def
|
238
|
-
|
239
|
-
elements.
|
240
|
-
|
241
|
-
return {
|
242
|
-
'cookie'=>elements.join(':')
|
243
|
-
}
|
266
|
+
def console_ts(app)
|
267
|
+
# we are sure that fields are not nil
|
268
|
+
elements=[app,user_info['name'],user_info['email']].map{|e|Base64.strict_encode64(e)}
|
269
|
+
elements.unshift(COOKIE_PREFIX)
|
270
|
+
return {'cookie'=>elements.join(':')}
|
244
271
|
end
|
245
272
|
|
246
273
|
# build "transfer info", 2 elements array with:
|
247
274
|
# - transfer spec for aspera on cloud, based on node information and file id
|
248
275
|
# - source and token regeneration method
|
249
276
|
def tr_spec(app,direction,node_file,ts_add)
|
250
|
-
#
|
251
|
-
|
277
|
+
# get node api
|
278
|
+
node_api=get_node_api(node_file[:node_info])
|
279
|
+
# this lambda returns the bearer token for node, if
|
280
|
+
token_generation_lambda=lambda{|do_refresh|node_api.oauth_token(force_refresh: do_refresh)}
|
252
281
|
# prepare transfer specification
|
253
282
|
# note xfer_id and xfer_retry are set by the transfer agent itself
|
254
283
|
transfer_spec={
|
255
284
|
'direction' => direction,
|
256
|
-
'token' =>
|
285
|
+
'token' => token_generation_lambda.call(false), # first time, use cache
|
257
286
|
'tags' => {
|
258
287
|
'aspera' => {
|
259
288
|
'app' => app,
|
260
289
|
'files' => {
|
261
|
-
'node_id' => node_file[:node_info]['id']
|
290
|
+
'node_id' => node_file[:node_info]['id']
|
262
291
|
}, # files
|
263
292
|
'node' => {
|
264
293
|
'access_key' => node_file[:node_info]['access_key'],
|
@@ -269,36 +298,40 @@ module Aspera
|
|
269
298
|
} # tags
|
270
299
|
}
|
271
300
|
# add remote host info
|
272
|
-
if
|
273
|
-
|
301
|
+
if self.class.use_standard_ports
|
302
|
+
# get default TCP/UDP ports and transfer user
|
303
|
+
transfer_spec.merge!(Fasp::TransferSpec::AK_TSPEC_BASE)
|
304
|
+
# by default: same address as node API
|
274
305
|
transfer_spec['remote_host']=node_file[:node_info]['host']
|
306
|
+
# 30 it's necessarily https scheme: webui does not allow anything else
|
307
|
+
if node_file[:node_info]['transfer_url'].is_a?(String) && !node_file[:node_info]['transfer_url'].empty?
|
308
|
+
transfer_spec['remote_host']=URI.parse(node_file[:node_info]['transfer_url']).host
|
309
|
+
end
|
275
310
|
else
|
276
311
|
# retrieve values from API
|
277
|
-
std_t_spec=
|
312
|
+
std_t_spec=node_api.create('files/download_setup',{transfer_requests: [{ transfer_request: {paths: [{'source'=>'/'}] } }] })[:data]['transfer_specs'].first['transfer_spec']
|
278
313
|
['remote_host','remote_user','ssh_port','fasp_port'].each {|i| transfer_spec[i]=std_t_spec[i]}
|
279
314
|
end
|
280
315
|
# add caller provided transfer spec
|
281
316
|
transfer_spec.deep_merge!(ts_add)
|
282
317
|
# additional information for transfer agent
|
283
318
|
source_and_token_generator={
|
284
|
-
|
285
|
-
:
|
319
|
+
src: :node_gen4,
|
320
|
+
regenerate_token: token_generation_lambda
|
286
321
|
}
|
287
322
|
return transfer_spec,source_and_token_generator
|
288
323
|
end
|
289
324
|
|
290
325
|
# returns a node API for access key
|
326
|
+
# @param node_info [Hash] with 'url' and 'access_key'
|
291
327
|
# @param scope e.g. SCOPE_NODE_USER
|
292
328
|
# no scope: requires secret
|
293
329
|
# if secret provided beforehand: use it
|
294
|
-
def get_node_api(node_info,
|
295
|
-
raise
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
if ak_secret.nil? and !options.has_key?(:scope)
|
300
|
-
raise "There must be at least one of: 'secret' or 'scope' for access key #{node_info['access_key']}"
|
301
|
-
end
|
330
|
+
def get_node_api(node_info, scope: SCOPE_NODE_USER, use_secret: true)
|
331
|
+
raise 'internal error' unless node_info.is_a?(Hash) && node_info.has_key?('url') && node_info.has_key?('access_key')
|
332
|
+
# get optional secret unless :use_secret is false
|
333
|
+
ak_secret=@key_chain.get_secret(url: node_info['url'], username: node_info['access_key'], mandatory: false) if use_secret && !@key_chain.nil?
|
334
|
+
raise "There must be at least one of: 'secret' or 'scope' for access key #{node_info['access_key']}" if ak_secret.nil? && scope.nil?
|
302
335
|
node_rest_params={base_url: node_info['url']}
|
303
336
|
# if secret is available
|
304
337
|
if !ak_secret.nil?
|
@@ -310,8 +343,8 @@ module Aspera
|
|
310
343
|
else
|
311
344
|
# X-Aspera-AccessKey required for bearer token only
|
312
345
|
node_rest_params[:headers]= {'X-Aspera-AccessKey'=>node_info['access_key']}
|
313
|
-
node_rest_params[:auth]=
|
314
|
-
node_rest_params[:auth][:scope]=self.class.node_scope(node_info['access_key'],
|
346
|
+
node_rest_params[:auth]=params[:auth].clone
|
347
|
+
node_rest_params[:auth][:scope]=self.class.node_scope(node_info['access_key'],scope)
|
315
348
|
end
|
316
349
|
return Node.new(node_rest_params)
|
317
350
|
end
|
@@ -320,7 +353,7 @@ module Aspera
|
|
320
353
|
# @return split values
|
321
354
|
def check_get_node_file(node_file)
|
322
355
|
raise "node_file must be Hash (got #{node_file.class})" unless node_file.is_a?(Hash)
|
323
|
-
raise
|
356
|
+
raise 'node_file must have 2 keys: :file_id and :node_info' unless node_file.keys.sort.eql?([:file_id,:node_info])
|
324
357
|
node_info=node_file[:node_info]
|
325
358
|
file_id=node_file[:file_id]
|
326
359
|
raise "node_info must be Hash (got #{node_info.class}: #{node_info})" unless node_info.is_a?(Hash)
|
@@ -336,28 +369,28 @@ module Aspera
|
|
336
369
|
@find_state[:found].push(entry.merge({'path'=>path})) if @find_state[:test_block].call(entry)
|
337
370
|
# process link
|
338
371
|
if entry[:type].eql?('link')
|
339
|
-
sub_node_info=
|
372
|
+
sub_node_info=read("nodes/#{entry['target_node_id']}")[:data]
|
340
373
|
sub_opt={method: process_find_files, top_file_id: entry['target_id'], top_file_path: path}
|
341
|
-
get_node_api(sub_node_info
|
374
|
+
get_node_api(sub_node_info).crawl(self,sub_opt)
|
342
375
|
end
|
343
|
-
rescue => e
|
376
|
+
rescue StandardError => e
|
344
377
|
Log.log.error("#{path}: #{e.message}")
|
345
378
|
end
|
346
379
|
# process all folders
|
347
380
|
return true
|
348
381
|
end
|
349
382
|
|
350
|
-
def find_files(
|
383
|
+
def find_files(top_node_file, test_block)
|
351
384
|
top_node_info,top_file_id=check_get_node_file(top_node_file)
|
352
385
|
Log.log.debug("find_files: node_info=#{top_node_info}, fileid=#{top_file_id}")
|
353
386
|
@find_state={found: [], test_block: test_block}
|
354
|
-
get_node_api(top_node_info
|
387
|
+
get_node_api(top_node_info).crawl(self,{method: :process_find_files, top_file_id: top_file_id})
|
355
388
|
result=@find_state[:found]
|
356
389
|
@find_state=nil
|
357
390
|
return result
|
358
391
|
end
|
359
392
|
|
360
|
-
def process_resolve_node_file(entry,
|
393
|
+
def process_resolve_node_file(entry,_path)
|
361
394
|
# stop digging here if not in right path
|
362
395
|
return false unless entry['name'].eql?(@resolve_state[:path].first)
|
363
396
|
# ok it matches, so we remove the match
|
@@ -368,11 +401,11 @@ module Aspera
|
|
368
401
|
raise "#{entry['name']} is a file, expecting folder to find: #{@resolve_state[:path]}" unless @resolve_state[:path].empty?
|
369
402
|
@resolve_state[:result][:file_id]=entry['id']
|
370
403
|
when 'link'
|
371
|
-
@resolve_state[:result][:node_info]=
|
404
|
+
@resolve_state[:result][:node_info]=read("nodes/#{entry['target_node_id']}")[:data]
|
372
405
|
if @resolve_state[:path].empty?
|
373
406
|
@resolve_state[:result][:file_id]=entry['target_id']
|
374
407
|
else
|
375
|
-
get_node_api(@resolve_state[:result][:node_info]
|
408
|
+
get_node_api(@resolve_state[:result][:node_info]).crawl(self,{method: :process_resolve_node_file, top_file_id: entry['target_id']})
|
376
409
|
end
|
377
410
|
when 'folder'
|
378
411
|
if @resolve_state[:path].empty?
|
@@ -391,15 +424,15 @@ module Aspera
|
|
391
424
|
# @param top_node_file Array [root node,file id]
|
392
425
|
# @param element_path_string String path of element
|
393
426
|
# supports links to secondary nodes
|
394
|
-
def resolve_node_file(
|
427
|
+
def resolve_node_file(top_node_file, element_path_string)
|
395
428
|
top_node_info,top_file_id=check_get_node_file(top_node_file)
|
396
|
-
path_elements=element_path_string.split(PATH_SEPARATOR).
|
429
|
+
path_elements=element_path_string.split(PATH_SEPARATOR).reject(&:empty?)
|
397
430
|
result={node_info: top_node_info, file_id: nil}
|
398
431
|
if path_elements.empty?
|
399
432
|
result[:file_id]=top_file_id
|
400
433
|
else
|
401
434
|
@resolve_state={path: path_elements, result: result}
|
402
|
-
get_node_api(top_node_info
|
435
|
+
get_node_api(top_node_info).crawl(self,{method: :process_resolve_node_file, top_file_id: top_file_id})
|
403
436
|
not_found=@resolve_state[:path]
|
404
437
|
@resolve_state=nil
|
405
438
|
raise "entry not found: #{not_found}" if result[:file_id].nil?
|
@@ -407,5 +440,25 @@ module Aspera
|
|
407
440
|
return result
|
408
441
|
end
|
409
442
|
|
443
|
+
# @param entity_type path of entuty in API
|
444
|
+
# @param entity_name name of searched entity
|
445
|
+
# @param options additional search options
|
446
|
+
def lookup_entity_by_name(entity_type,entity_name,options={})
|
447
|
+
# returns entities whose name contains value (case insensitive)
|
448
|
+
matching_items=read(entity_type,options.merge({'q'=>entity_name}))[:data]
|
449
|
+
case matching_items.length
|
450
|
+
when 1 then return matching_items.first
|
451
|
+
when 0 then raise 'not found'
|
452
|
+
else
|
453
|
+
# multiple case insensitive partial matches, try case insensitive full match
|
454
|
+
# (anyway AoC does not allow creation of 2 entities with same case insensitive name)
|
455
|
+
icase_matches=matching_items.select{|i|i['name'].casecmp?(entity_name)}
|
456
|
+
case icase_matches.length
|
457
|
+
when 1 then return icase_matches.first
|
458
|
+
when 0 then raise %Q(#{entity_type}: multiple case insensitive partial match for: "#{entity_name}": #{matching_items.map{|i|i['name']}} but no case insensitive full match. Please be more specific or give exact name.)
|
459
|
+
else raise "Two entities cannot have the same case insensitive name: #{icase_matches.map{|i|i['name']}}"
|
460
|
+
end
|
461
|
+
end
|
462
|
+
end
|
410
463
|
end # AoC
|
411
464
|
end # Aspera
|