aspera-cli 4.4.0 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|