aspera-cli 4.15.0 → 4.17.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
- checksums.yaml.gz.sig +0 -0
- data/BUGS.md +29 -3
- data/CHANGELOG.md +375 -280
- data/CONTRIBUTING.md +71 -18
- data/README.md +1978 -1656
- data/bin/ascli +13 -31
- data/bin/asession +32 -22
- data/examples/dascli +2 -2
- data/lib/aspera/agent/alpha.rb +117 -0
- data/lib/aspera/agent/base.rb +61 -0
- data/lib/aspera/{fasp/agent_connect.rb → agent/connect.rb} +13 -11
- data/lib/aspera/{fasp/agent_direct.rb → agent/direct.rb} +116 -116
- data/lib/aspera/{fasp/agent_httpgw.rb → agent/httpgw.rb} +21 -19
- data/lib/aspera/{fasp/agent_node.rb → agent/node.rb} +21 -33
- data/lib/aspera/agent/trsdk.rb +188 -0
- data/lib/aspera/api/aoc.rb +586 -0
- data/lib/aspera/api/ats.rb +46 -0
- data/lib/aspera/api/cos_node.rb +95 -0
- data/lib/aspera/api/node.rb +344 -0
- data/lib/aspera/ascmd.rb +47 -14
- data/lib/aspera/{fasp → ascp}/installation.rb +54 -15
- data/lib/aspera/{fasp → ascp}/management.rb +14 -14
- data/lib/aspera/{fasp → ascp}/products.rb +1 -1
- data/lib/aspera/assert.rb +45 -0
- data/lib/aspera/cli/basic_auth_plugin.rb +11 -10
- data/lib/aspera/cli/extended_value.rb +5 -5
- data/lib/aspera/cli/formatter.rb +27 -14
- data/lib/aspera/cli/hints.rb +7 -6
- data/lib/aspera/cli/main.rb +49 -29
- data/lib/aspera/cli/manager.rb +46 -36
- data/lib/aspera/cli/plugin.rb +34 -20
- data/lib/aspera/cli/plugin_factory.rb +61 -0
- data/lib/aspera/cli/plugins/alee.rb +7 -7
- data/lib/aspera/cli/plugins/aoc.rb +168 -132
- data/lib/aspera/cli/plugins/ats.rb +33 -33
- data/lib/aspera/cli/plugins/bss.rb +3 -4
- data/lib/aspera/cli/plugins/config.rb +250 -272
- data/lib/aspera/cli/plugins/console.rb +8 -6
- data/lib/aspera/cli/plugins/cos.rb +20 -19
- data/lib/aspera/cli/plugins/faspex.rb +71 -60
- data/lib/aspera/cli/plugins/faspex5.rb +212 -133
- data/lib/aspera/cli/plugins/node.rb +83 -75
- data/lib/aspera/cli/plugins/orchestrator.rb +36 -44
- data/lib/aspera/cli/plugins/preview.rb +33 -31
- data/lib/aspera/cli/plugins/server.rb +33 -32
- data/lib/aspera/cli/plugins/shares.rb +39 -33
- data/lib/aspera/cli/sync_actions.rb +9 -9
- data/lib/aspera/cli/transfer_agent.rb +45 -25
- data/lib/aspera/cli/transfer_progress.rb +2 -3
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/colors.rb +5 -0
- data/lib/aspera/command_line_builder.rb +16 -14
- data/lib/aspera/coverage.rb +21 -0
- data/lib/aspera/data_repository.rb +33 -2
- data/lib/aspera/environment.rb +5 -4
- data/lib/aspera/faspex_gw.rb +13 -11
- data/lib/aspera/faspex_postproc.rb +6 -5
- data/lib/aspera/id_generator.rb +4 -2
- data/lib/aspera/json_rpc.rb +10 -8
- data/lib/aspera/keychain/encrypted_hash.rb +46 -11
- data/lib/aspera/keychain/macos_security.rb +29 -22
- data/lib/aspera/log.rb +5 -4
- data/lib/aspera/nagios.rb +7 -2
- data/lib/aspera/node_simulator.rb +213 -0
- data/lib/aspera/oauth/base.rb +143 -0
- data/lib/aspera/oauth/factory.rb +124 -0
- data/lib/aspera/oauth/generic.rb +34 -0
- data/lib/aspera/oauth/jwt.rb +51 -0
- data/lib/aspera/oauth/url_json.rb +31 -0
- data/lib/aspera/oauth/web.rb +50 -0
- data/lib/aspera/oauth.rb +5 -328
- data/lib/aspera/open_application.rb +7 -7
- data/lib/aspera/persistency_action_once.rb +13 -14
- data/lib/aspera/persistency_folder.rb +3 -2
- data/lib/aspera/preview/file_types.rb +53 -267
- data/lib/aspera/preview/generator.rb +7 -5
- data/lib/aspera/preview/terminal.rb +17 -7
- data/lib/aspera/preview/utils.rb +8 -7
- data/lib/aspera/proxy_auto_config.rb +6 -3
- data/lib/aspera/rest.rb +187 -140
- data/lib/aspera/rest_error_analyzer.rb +1 -0
- data/lib/aspera/rest_errors_aspera.rb +5 -3
- data/lib/aspera/resumer.rb +77 -0
- data/lib/aspera/secret_hider.rb +5 -2
- data/lib/aspera/ssh.rb +15 -8
- data/lib/aspera/temp_file_manager.rb +1 -1
- data/lib/aspera/{fasp → transfer}/error.rb +3 -3
- data/lib/aspera/{fasp → transfer}/error_info.rb +1 -1
- data/lib/aspera/{fasp → transfer}/faux_file.rb +1 -1
- data/lib/aspera/{fasp → transfer}/parameters.rb +95 -120
- data/lib/aspera/{fasp/transfer_spec.rb → transfer/spec.rb} +23 -19
- data/lib/aspera/{fasp/parameters.yaml → transfer/spec.yaml} +4 -99
- data/lib/aspera/transfer/sync.rb +273 -0
- data/lib/aspera/{fasp → transfer}/uri.rb +10 -9
- data/lib/aspera/web_server_simple.rb +12 -3
- data.tar.gz.sig +0 -0
- metadata +92 -68
- metadata.gz.sig +0 -0
- data/lib/aspera/aoc.rb +0 -606
- data/lib/aspera/ats_api.rb +0 -47
- data/lib/aspera/cos_node.rb +0 -93
- data/lib/aspera/fasp/agent_aspera.rb +0 -126
- data/lib/aspera/fasp/agent_base.rb +0 -48
- data/lib/aspera/fasp/agent_trsdk.rb +0 -146
- data/lib/aspera/fasp/resume_policy.rb +0 -77
- data/lib/aspera/node.rb +0 -338
- data/lib/aspera/sync.rb +0 -219
data/lib/aspera/aoc.rb
DELETED
|
@@ -1,606 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'aspera/log'
|
|
4
|
-
require 'aspera/rest'
|
|
5
|
-
require 'aspera/hash_ext'
|
|
6
|
-
require 'aspera/data_repository'
|
|
7
|
-
require 'aspera/fasp/transfer_spec'
|
|
8
|
-
require 'aspera/node'
|
|
9
|
-
require 'base64'
|
|
10
|
-
require 'cgi'
|
|
11
|
-
|
|
12
|
-
Aspera::Oauth.register_token_creator(
|
|
13
|
-
:aoc_pub_link,
|
|
14
|
-
lambda{|o|
|
|
15
|
-
o.api.call({
|
|
16
|
-
operation: 'POST',
|
|
17
|
-
subpath: o.generic_parameters[:path_token],
|
|
18
|
-
headers: {'Accept' => 'application/json'},
|
|
19
|
-
json_params: o.specific_parameters[:json],
|
|
20
|
-
url_params: o.specific_parameters[:url].merge(scope: o.generic_parameters[:scope]) # scope is here because it changes over time (node)
|
|
21
|
-
})
|
|
22
|
-
},
|
|
23
|
-
lambda { |oauth|
|
|
24
|
-
return [oauth.specific_parameters.dig(:json, :url_token)]
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
module Aspera
|
|
28
|
-
class AoC < Aspera::Rest
|
|
29
|
-
PRODUCT_NAME = 'Aspera on Cloud'
|
|
30
|
-
# Production domain of AoC
|
|
31
|
-
PROD_DOMAIN = 'ibmaspera.com' # cspell:disable-line
|
|
32
|
-
# to avoid infinite loop in pub link redirection
|
|
33
|
-
MAX_AOC_URL_REDIRECT = 10
|
|
34
|
-
# Well-known AoC globals client apps
|
|
35
|
-
GLOBAL_CLIENT_APPS = %w[aspera.global-cli-client aspera.drive].freeze
|
|
36
|
-
# index offset in data repository of client app
|
|
37
|
-
DATA_REPO_INDEX_START = 4
|
|
38
|
-
# cookie prefix so that console can decode identity
|
|
39
|
-
COOKIE_PREFIX_CONSOLE_AOC = 'aspera.aoc'
|
|
40
|
-
# path in URL of public links
|
|
41
|
-
PUBLIC_LINK_PATHS = %w[/packages/public/receive /packages/public/send /files/public /public/files /public/send].freeze
|
|
42
|
-
JWT_AUDIENCE = 'https://api.asperafiles.com/api/v1/oauth2/token'
|
|
43
|
-
OAUTH_API_SUBPATH = 'api/v1/oauth2'
|
|
44
|
-
# minimum fields for user info if retrieval fails
|
|
45
|
-
USER_INFO_FIELDS_MIN = %w[name email id default_workspace_id organization_id].freeze
|
|
46
|
-
# types of events for shared folder creation
|
|
47
|
-
# Node events: permission.created permission.modified permission.deleted
|
|
48
|
-
PERMISSIONS_CREATED = ['permission.created'].freeze
|
|
49
|
-
DEFAULT_WORKSPACE = ''
|
|
50
|
-
|
|
51
|
-
private_constant :MAX_AOC_URL_REDIRECT,
|
|
52
|
-
:GLOBAL_CLIENT_APPS,
|
|
53
|
-
:DATA_REPO_INDEX_START,
|
|
54
|
-
:COOKIE_PREFIX_CONSOLE_AOC,
|
|
55
|
-
:PUBLIC_LINK_PATHS,
|
|
56
|
-
:JWT_AUDIENCE,
|
|
57
|
-
:OAUTH_API_SUBPATH,
|
|
58
|
-
:USER_INFO_FIELDS_MIN,
|
|
59
|
-
:PERMISSIONS_CREATED
|
|
60
|
-
|
|
61
|
-
# various API scopes supported
|
|
62
|
-
SCOPE_FILES_SELF = 'self'
|
|
63
|
-
SCOPE_FILES_USER = 'user:all'
|
|
64
|
-
SCOPE_FILES_ADMIN = 'admin:all'
|
|
65
|
-
SCOPE_FILES_ADMIN_USER = 'admin-user:all'
|
|
66
|
-
SCOPE_FILES_ADMIN_USER_USER = "#{SCOPE_FILES_ADMIN_USER}+#{SCOPE_FILES_USER}"
|
|
67
|
-
FILES_APP = 'files'
|
|
68
|
-
PACKAGES_APP = 'packages'
|
|
69
|
-
API_V1 = 'api/v1'
|
|
70
|
-
|
|
71
|
-
# class static methods
|
|
72
|
-
class << self
|
|
73
|
-
# strings /Applications/Aspera\ Drive.app/Contents/MacOS/AsperaDrive|grep -E '.{100}==$'|base64 --decode
|
|
74
|
-
def get_client_info(client_name=GLOBAL_CLIENT_APPS.first)
|
|
75
|
-
client_index = GLOBAL_CLIENT_APPS.index(client_name)
|
|
76
|
-
raise "no such pre-defined client: #{client_name}" if client_index.nil?
|
|
77
|
-
return client_name, Base64.urlsafe_encode64(DataRepository.instance.data(DATA_REPO_INDEX_START + client_index))
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# base API url depends on domain, which could be "qa.xxx"
|
|
81
|
-
def api_base_url(organization: 'api', api_domain: PROD_DOMAIN)
|
|
82
|
-
return "https://#{organization}.#{api_domain}"
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def metering_api(entitlement_id, customer_id, api_domain=PROD_DOMAIN)
|
|
86
|
-
return Rest.new({
|
|
87
|
-
base_url: "#{api_base_url(api_domain: api_domain)}/metering/v1",
|
|
88
|
-
headers: {'X-Aspera-Entitlement-Authorization' => Rest.basic_token(entitlement_id, customer_id)}
|
|
89
|
-
})
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# split host of http://myorg.asperafiles.com in org and domain
|
|
93
|
-
def url_parts(uri)
|
|
94
|
-
raise "No host found in URL.Please check URL format: https://myorg.#{PROD_DOMAIN}" if uri.host.nil?
|
|
95
|
-
parts = uri.host.split('.', 2)
|
|
96
|
-
raise "expecting a public FQDN for #{PRODUCT_NAME}" unless parts.length == 2
|
|
97
|
-
return parts
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# @param url [String] URL of AoC public link
|
|
101
|
-
# @return [Hash] information about public link, or nil if not a public link
|
|
102
|
-
def link_info(url)
|
|
103
|
-
final_uri = Rest.new({base_url: url, redirect_max: MAX_AOC_URL_REDIRECT}).read('')[:http].uri
|
|
104
|
-
raise 'AoC shall redirect to login page' if final_uri.query.nil?
|
|
105
|
-
decoded_query = Rest.decode_query(final_uri.query)
|
|
106
|
-
# is that a public link ?
|
|
107
|
-
if decoded_query.key?('token')
|
|
108
|
-
Log.log.warn{"Unknown pub link path: #{final_uri.path}"} unless PUBLIC_LINK_PATHS.include?(final_uri.path)
|
|
109
|
-
# ok we get it !
|
|
110
|
-
return {
|
|
111
|
-
instance_domain: url_parts(final_uri)[1],
|
|
112
|
-
url: 'https://' + final_uri.host,
|
|
113
|
-
token: decoded_query['token']
|
|
114
|
-
}
|
|
115
|
-
end
|
|
116
|
-
Log.log.debug{"path=#{final_uri.path} does not end with /login"} unless final_uri.path.end_with?('/login')
|
|
117
|
-
if decoded_query['state']
|
|
118
|
-
# can be a private link
|
|
119
|
-
state_uri = URI.parse(decoded_query['state'])
|
|
120
|
-
if state_uri.query && decoded_query['redirect_uri']
|
|
121
|
-
decoded_state = Rest.decode_query(state_uri.query)
|
|
122
|
-
if decoded_state.key?('short_link_url')
|
|
123
|
-
if (m = state_uri.path.match(%r{/files/workspaces/([0-9]+)/all/([0-9]+):([0-9]+)}))
|
|
124
|
-
redirect_uri = URI.parse(decoded_query['redirect_uri'])
|
|
125
|
-
parts = url_parts(redirect_uri)
|
|
126
|
-
return {
|
|
127
|
-
instance_domain: parts[1],
|
|
128
|
-
organization: parts[0],
|
|
129
|
-
url: 'https://' + redirect_uri.host,
|
|
130
|
-
private_link: {
|
|
131
|
-
workspace_id: m[1],
|
|
132
|
-
node_id: m[2],
|
|
133
|
-
file_id: m[3]
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
parts = url_parts(URI.parse(url))
|
|
141
|
-
return {
|
|
142
|
-
instance_domain: parts[1],
|
|
143
|
-
organization: parts[0]
|
|
144
|
-
}
|
|
145
|
-
end
|
|
146
|
-
end # static methods
|
|
147
|
-
|
|
148
|
-
attr_reader :private_link
|
|
149
|
-
|
|
150
|
-
def initialize(subpath: API_V1, url:, auth:, client_id: nil, client_secret: nil, scope: nil, redirect_uri: nil, private_key: nil, passphrase: nil, username: nil,
|
|
151
|
-
password: nil, workspace: nil, secret_finder: nil)
|
|
152
|
-
# test here because link may set url
|
|
153
|
-
raise ArgumentError, 'Missing mandatory option: url' if url.nil?
|
|
154
|
-
raise ArgumentError, 'Missing mandatory option: scope' if scope.nil?
|
|
155
|
-
# default values for client id
|
|
156
|
-
client_id, client_secret = self.class.get_client_info if client_id.nil?
|
|
157
|
-
# access key secrets are provided out of band to get node api access
|
|
158
|
-
# key: access key
|
|
159
|
-
# value: associated secret
|
|
160
|
-
@secret_finder = secret_finder
|
|
161
|
-
@workspace_name = workspace
|
|
162
|
-
@cache_user_info = nil
|
|
163
|
-
@cache_url_token_info = nil
|
|
164
|
-
# init rest params
|
|
165
|
-
aoc_rest_p = {auth: {type: :oauth2}}
|
|
166
|
-
# shortcut to auth section
|
|
167
|
-
aoc_auth_p = aoc_rest_p[:auth]
|
|
168
|
-
# analyze type of url
|
|
169
|
-
url_info = AoC.link_info(url)
|
|
170
|
-
Log.log.debug{Log.dump(:url_info, url_info)}
|
|
171
|
-
@private_link = url_info[:private_link]
|
|
172
|
-
aoc_auth_p[:grant_method] = if url_info.key?(:token)
|
|
173
|
-
:aoc_pub_link
|
|
174
|
-
else
|
|
175
|
-
raise ArgumentError, 'Missing mandatory option: auth' if auth.nil?
|
|
176
|
-
auth
|
|
177
|
-
end
|
|
178
|
-
# this is the base API url
|
|
179
|
-
api_url_base = self.class.api_base_url(api_domain: url_info[:instance_domain])
|
|
180
|
-
# API URL, including subpath (version ...)
|
|
181
|
-
aoc_rest_p[:base_url] = "#{api_url_base}/#{subpath}"
|
|
182
|
-
# auth URL
|
|
183
|
-
aoc_auth_p[:base_url] = "#{api_url_base}/#{OAUTH_API_SUBPATH}/#{url_info[:organization]}"
|
|
184
|
-
aoc_auth_p[:client_id] = client_id
|
|
185
|
-
aoc_auth_p[:client_secret] = client_secret
|
|
186
|
-
aoc_auth_p[:scope] = scope
|
|
187
|
-
|
|
188
|
-
# fill other auth parameters based on Oauth method
|
|
189
|
-
case aoc_auth_p[:grant_method]
|
|
190
|
-
when :web
|
|
191
|
-
raise ArgumentError, 'Missing mandatory option: redirect_uri' if redirect_uri.nil?
|
|
192
|
-
aoc_auth_p[:web] = {redirect_uri: redirect_uri}
|
|
193
|
-
when :jwt
|
|
194
|
-
raise ArgumentError, 'Missing mandatory option: private_key' if private_key.nil?
|
|
195
|
-
raise ArgumentError, 'Missing mandatory option: username' if username.nil?
|
|
196
|
-
aoc_auth_p[:jwt] = {
|
|
197
|
-
private_key_obj: OpenSSL::PKey::RSA.new(private_key, passphrase),
|
|
198
|
-
payload: {
|
|
199
|
-
iss: aoc_auth_p[:client_id], # issuer
|
|
200
|
-
sub: username, # subject
|
|
201
|
-
aud: JWT_AUDIENCE
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
# add jwt payload for global ids
|
|
205
|
-
aoc_auth_p[:jwt][:payload][:org] = url_info[:organization] if GLOBAL_CLIENT_APPS.include?(aoc_auth_p[:client_id])
|
|
206
|
-
when :aoc_pub_link
|
|
207
|
-
aoc_auth_p[:aoc_pub_link] = {
|
|
208
|
-
url: {grant_type: 'url_token'}, # URL arguments
|
|
209
|
-
json: {url_token: url_info[:token]} # JSON body
|
|
210
|
-
}
|
|
211
|
-
# password protection of link
|
|
212
|
-
aoc_auth_p[:aoc_pub_link][:json][:password] = password unless password.nil?
|
|
213
|
-
# basic auth required for /token
|
|
214
|
-
aoc_auth_p[:auth] = {type: :basic, username: aoc_auth_p[:client_id], password: aoc_auth_p[:client_secret]}
|
|
215
|
-
else raise "ERROR: unsupported auth method: #{aoc_auth_p[:grant_method]}"
|
|
216
|
-
end
|
|
217
|
-
super(aoc_rest_p)
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def public_link
|
|
221
|
-
return nil unless params[:auth][:grant_method].eql?(:aoc_pub_link)
|
|
222
|
-
return @cache_url_token_info unless @cache_url_token_info.nil?
|
|
223
|
-
# TODO: can there be several in list ?
|
|
224
|
-
@cache_url_token_info = read('url_tokens')[:data].first
|
|
225
|
-
return @cache_url_token_info
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
def assert_public_link_types(expected)
|
|
229
|
-
raise "public link type is #{public_link['purpose']} but action requires one of #{expected.join(',')}" unless expected.include?(public_link['purpose'])
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
def additional_persistence_ids
|
|
233
|
-
return [current_user_info['id']] if public_link.nil?
|
|
234
|
-
return [] # TODO : public_link['id'] ?
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
# def secret_finder=(secret_finder)
|
|
238
|
-
# raise 'secret finder already set' unless @secret_finder.nil?
|
|
239
|
-
# raise 'secret finder must have lookup_secret' unless secret_finder.respond_to?(:lookup_secret)
|
|
240
|
-
# @secret_finder = secret_finder
|
|
241
|
-
# end
|
|
242
|
-
|
|
243
|
-
# cached user information
|
|
244
|
-
def current_user_info(exception: false)
|
|
245
|
-
return @cache_user_info unless @cache_user_info.nil?
|
|
246
|
-
# get our user's default information
|
|
247
|
-
@cache_user_info =
|
|
248
|
-
begin
|
|
249
|
-
read('self')[:data]
|
|
250
|
-
rescue StandardError => e
|
|
251
|
-
raise e if exception
|
|
252
|
-
Log.log.debug{"ignoring error: #{e}"}
|
|
253
|
-
{}
|
|
254
|
-
end
|
|
255
|
-
USER_INFO_FIELDS_MIN.each{|f|@cache_user_info[f] = 'unknown' if @cache_user_info[f].nil?}
|
|
256
|
-
return @cache_user_info
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
# @param application [Symbol] :files or :packages
|
|
260
|
-
# @return [Hash] current context information: workspace, and home node/file if app is "Files"
|
|
261
|
-
def context(application = nil)
|
|
262
|
-
return @context_cache unless @context_cache.nil?
|
|
263
|
-
raise 'context must be initialized with application' if application.nil?
|
|
264
|
-
ws_id =
|
|
265
|
-
if !public_link.nil?
|
|
266
|
-
Log.log.debug('Using workspace of public link')
|
|
267
|
-
public_link['data']['workspace_id']
|
|
268
|
-
elsif !private_link.nil?
|
|
269
|
-
Log.log.debug('Using workspace of private link')
|
|
270
|
-
private_link[:workspace_id]
|
|
271
|
-
elsif @workspace_name.eql?(DEFAULT_WORKSPACE)
|
|
272
|
-
Log.log.debug('Using default workspace'.green)
|
|
273
|
-
raise 'User does not have default workspace, please specify workspace' if current_user_info['default_workspace_id'].nil?
|
|
274
|
-
current_user_info['default_workspace_id']
|
|
275
|
-
elsif @workspace_name.nil?
|
|
276
|
-
nil
|
|
277
|
-
else
|
|
278
|
-
lookup_by_name('workspaces', @workspace_name)['id']
|
|
279
|
-
end
|
|
280
|
-
ws_info =
|
|
281
|
-
if ws_id.nil?
|
|
282
|
-
nil
|
|
283
|
-
else
|
|
284
|
-
read("workspaces/#{ws_id}")[:data]
|
|
285
|
-
end
|
|
286
|
-
@context_cache = if ws_info.nil?
|
|
287
|
-
{
|
|
288
|
-
workspace_id: nil,
|
|
289
|
-
workspace_name: 'Shared folders'
|
|
290
|
-
}
|
|
291
|
-
else
|
|
292
|
-
{
|
|
293
|
-
workspace_id: ws_info['id'],
|
|
294
|
-
workspace_name: ws_info['name']
|
|
295
|
-
}
|
|
296
|
-
end
|
|
297
|
-
return @context_cache unless application.eql?(:files)
|
|
298
|
-
if !public_link.nil?
|
|
299
|
-
assert_public_link_types(['view_shared_file'])
|
|
300
|
-
@context_cache[:home_node_id] = public_link['data']['node_id']
|
|
301
|
-
@context_cache[:home_file_id] = public_link['data']['file_id']
|
|
302
|
-
elsif !private_link.nil?
|
|
303
|
-
@context_cache[:home_node_id] = private_link[:node_id]
|
|
304
|
-
@context_cache[:home_file_id] = private_link[:file_id]
|
|
305
|
-
elsif ws_info
|
|
306
|
-
@context_cache[:home_node_id] = ws_info['home_node_id']
|
|
307
|
-
@context_cache[:home_file_id] = ws_info['home_file_id']
|
|
308
|
-
else
|
|
309
|
-
# not part of any workspace, but has some folder shared
|
|
310
|
-
user_info = current_user_info(exception: true) rescue {'read_only_home_node_id' => nil, 'read_only_home_file_id' => nil}
|
|
311
|
-
@context_cache[:home_node_id] = user_info['read_only_home_node_id']
|
|
312
|
-
@context_cache[:home_file_id] = user_info['read_only_home_file_id']
|
|
313
|
-
end
|
|
314
|
-
raise "Cannot get user's home node id, check your default workspace or specify one" if @context_cache[:home_node_id].to_s.empty?
|
|
315
|
-
Log.log.debug{Log.dump(:context, @context_cache)}
|
|
316
|
-
return @context_cache
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
# @param node_id [String] identifier of node in AoC
|
|
320
|
-
# @param workspace_id [String] workspace identifier
|
|
321
|
-
# @param workspace_name [String] workspace name
|
|
322
|
-
# @param scope e.g. Aspera::Node::SCOPE_USER, or nil (requires secret)
|
|
323
|
-
# @param package_info [Hash] created package information
|
|
324
|
-
# @returns [Aspera::Node] a node API for access key
|
|
325
|
-
def node_api_from(node_id:, workspace_id: nil, workspace_name: nil, scope: Aspera::Node::SCOPE_USER, package_info: nil)
|
|
326
|
-
raise 'invalid type for node_id' unless node_id.is_a?(String)
|
|
327
|
-
node_info = read("nodes/#{node_id}")[:data]
|
|
328
|
-
if workspace_name.nil? && !workspace_id.nil?
|
|
329
|
-
workspace_name = read("workspaces/#{workspace_id}")[:data]['name']
|
|
330
|
-
end
|
|
331
|
-
app_info = {
|
|
332
|
-
api: self, # for callback
|
|
333
|
-
app: package_info.nil? ? FILES_APP : PACKAGES_APP,
|
|
334
|
-
node_info: node_info,
|
|
335
|
-
workspace_id: workspace_id,
|
|
336
|
-
workspace_name: workspace_name
|
|
337
|
-
}
|
|
338
|
-
if PACKAGES_APP.eql?(app_info[:app])
|
|
339
|
-
raise 'package info required' if package_info.nil?
|
|
340
|
-
app_info[:package_id] = package_info['id']
|
|
341
|
-
app_info[:package_name] = package_info['name']
|
|
342
|
-
end
|
|
343
|
-
node_rest_params = {base_url: node_info['url']}
|
|
344
|
-
# if secret is available
|
|
345
|
-
if scope.nil?
|
|
346
|
-
node_rest_params[:auth] = {
|
|
347
|
-
type: :basic,
|
|
348
|
-
username: node_info['access_key'],
|
|
349
|
-
password: @secret_finder&.lookup_secret(url: node_info['url'], username: node_info['access_key'], mandatory: true)
|
|
350
|
-
}
|
|
351
|
-
else
|
|
352
|
-
# OAuth bearer token
|
|
353
|
-
node_rest_params[:auth] = params[:auth].clone
|
|
354
|
-
node_rest_params[:auth][:scope] = Aspera::Node.token_scope(node_info['access_key'], scope)
|
|
355
|
-
# special header required for bearer token only
|
|
356
|
-
node_rest_params[:headers] = {Aspera::Node::HEADER_X_ASPERA_ACCESS_KEY => node_info['access_key']}
|
|
357
|
-
end
|
|
358
|
-
return Node.new(params: node_rest_params, app_info: app_info)
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
# Check metadata: remove when validation is done server side
|
|
362
|
-
def validate_metadata(pkg_data)
|
|
363
|
-
# validate only for shared inboxes
|
|
364
|
-
return unless pkg_data['recipients'].is_a?(Array) &&
|
|
365
|
-
pkg_data['recipients'].first.is_a?(Hash) &&
|
|
366
|
-
pkg_data['recipients'].first.key?('type') &&
|
|
367
|
-
pkg_data['recipients'].first['type'].eql?('dropbox')
|
|
368
|
-
meta_schema = read("dropboxes/#{pkg_data['recipients'].first['id']}")[:data]['metadata_schema']
|
|
369
|
-
if meta_schema.nil? || meta_schema.empty?
|
|
370
|
-
Log.log.debug('no metadata in shared inbox')
|
|
371
|
-
return
|
|
372
|
-
end
|
|
373
|
-
pkg_meta = pkg_data['metadata']
|
|
374
|
-
raise "package requires metadata: #{meta_schema}" unless pkg_data.key?('metadata')
|
|
375
|
-
raise 'metadata must be an Array' unless pkg_meta.is_a?(Array)
|
|
376
|
-
Log.log.debug{Log.dump(:metadata, pkg_meta)}
|
|
377
|
-
pkg_meta.each do |field|
|
|
378
|
-
raise 'metadata field must be Hash' unless field.is_a?(Hash)
|
|
379
|
-
raise 'metadata field must have name' unless field.key?('name')
|
|
380
|
-
raise 'metadata field must have values' unless field.key?('values')
|
|
381
|
-
raise 'metadata values must be an Array' unless field['values'].is_a?(Array)
|
|
382
|
-
raise "unknown metadata field: #{field['name']}" if meta_schema.select{|i|i['name'].eql?(field['name'])}.empty?
|
|
383
|
-
end
|
|
384
|
-
meta_schema.each do |field|
|
|
385
|
-
provided = pkg_meta.select{|i|i['name'].eql?(field['name'])}
|
|
386
|
-
raise "only one field with name #{field['name']} allowed" if provided.count > 1
|
|
387
|
-
raise "missing mandatory field: #{field['name']}" if field['required'] && provided.empty?
|
|
388
|
-
end
|
|
389
|
-
end
|
|
390
|
-
|
|
391
|
-
# Normalize package creation recipient lists as expected by AoC API
|
|
392
|
-
# AoC expects {type: , id: }, but ascli allows providing either the native values or just a name
|
|
393
|
-
# in that case, the name is resolved and replaced with {type: , id: }
|
|
394
|
-
# @param package_data The whole package creation payload
|
|
395
|
-
# @param recipient_list_field The field in structure, i.e. recipients or bcc_recipients
|
|
396
|
-
# @return nil package_data is modified
|
|
397
|
-
def resolve_package_recipients(package_data, ws_id, recipient_list_field, new_user_option)
|
|
398
|
-
return unless package_data.key?(recipient_list_field)
|
|
399
|
-
raise "#{recipient_list_field} must be an Array" unless package_data[recipient_list_field].is_a?(Array)
|
|
400
|
-
new_user_option = {'package_contact' => true} if new_user_option.nil?
|
|
401
|
-
raise 'new_user_option must be a Hash' unless new_user_option.is_a?(Hash)
|
|
402
|
-
# list with resolved elements
|
|
403
|
-
resolved_list = []
|
|
404
|
-
package_data[recipient_list_field].each do |short_recipient_info|
|
|
405
|
-
case short_recipient_info
|
|
406
|
-
when Hash # native API information, check keys
|
|
407
|
-
raise "#{recipient_list_field} element shall have fields: id and type" unless short_recipient_info.keys.sort.eql?(%w[id type])
|
|
408
|
-
when String # CLI helper: need to resolve provided name to type/id
|
|
409
|
-
# email: user, else dropbox
|
|
410
|
-
entity_type = short_recipient_info.include?('@') ? 'contacts' : 'dropboxes'
|
|
411
|
-
begin
|
|
412
|
-
full_recipient_info = lookup_by_name(entity_type, short_recipient_info, {'current_workspace_id' => ws_id})
|
|
413
|
-
rescue RuntimeError => e
|
|
414
|
-
raise e unless e.message.start_with?(ENTITY_NOT_FOUND)
|
|
415
|
-
# dropboxes cannot be created on the fly
|
|
416
|
-
raise "No such shared inbox in workspace #{ws_id}" if entity_type.eql?('dropboxes')
|
|
417
|
-
# unknown user: create it as external user
|
|
418
|
-
full_recipient_info = create('contacts', {
|
|
419
|
-
'current_workspace_id' => ws_id,
|
|
420
|
-
'email' => short_recipient_info
|
|
421
|
-
}.merge(new_user_option))[:data]
|
|
422
|
-
end
|
|
423
|
-
short_recipient_info = if entity_type.eql?('dropboxes')
|
|
424
|
-
{'id' => full_recipient_info['id'], 'type' => 'dropbox'}
|
|
425
|
-
else
|
|
426
|
-
{'id' => full_recipient_info['source_id'], 'type' => full_recipient_info['source_type']}
|
|
427
|
-
end
|
|
428
|
-
else # unexpected extended value, must be String or Hash
|
|
429
|
-
raise "#{recipient_list_field} item must be a String (email, shared inbox) or Hash (id,type)"
|
|
430
|
-
end # type of recipient info
|
|
431
|
-
# add original or resolved recipient info
|
|
432
|
-
resolved_list.push(short_recipient_info)
|
|
433
|
-
end
|
|
434
|
-
# replace with resolved elements
|
|
435
|
-
package_data[recipient_list_field] = resolved_list
|
|
436
|
-
return nil
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
# CLI allows simplified format for metadata: transform if necessary for API
|
|
440
|
-
def update_package_metadata_for_api(pkg_data)
|
|
441
|
-
case pkg_data['metadata']
|
|
442
|
-
when Array, NilClass # no action
|
|
443
|
-
when Hash
|
|
444
|
-
api_meta = []
|
|
445
|
-
pkg_data['metadata'].each do |k, v|
|
|
446
|
-
api_meta.push({
|
|
447
|
-
# 'input_type' => 'single-dropdown',
|
|
448
|
-
'name' => k,
|
|
449
|
-
'values' => v.is_a?(Array) ? v : [v]
|
|
450
|
-
})
|
|
451
|
-
end
|
|
452
|
-
pkg_data['metadata'] = api_meta
|
|
453
|
-
else raise "metadata field if not of expected type: #{pkg_meta.class}"
|
|
454
|
-
end
|
|
455
|
-
return nil
|
|
456
|
-
end
|
|
457
|
-
|
|
458
|
-
# create a package
|
|
459
|
-
# @param package_data [Hash] package creation (with extensions...)
|
|
460
|
-
# @param validate_meta [TrueClass,FalseClass] true to validate parameters locally
|
|
461
|
-
# @param new_user_option [Hash] options if an unknown user is specified
|
|
462
|
-
# @return transfer spec, node api and package information
|
|
463
|
-
def create_package_simple(package_data, validate_meta, new_user_option)
|
|
464
|
-
update_package_metadata_for_api(package_data)
|
|
465
|
-
# list of files to include in package, optional
|
|
466
|
-
# package_data['file_names']||=[..list of filenames to transfer...]
|
|
467
|
-
|
|
468
|
-
# lookup users
|
|
469
|
-
resolve_package_recipients(package_data, package_data['workspace_id'], 'recipients', new_user_option)
|
|
470
|
-
resolve_package_recipients(package_data, package_data['workspace_id'], 'bcc_recipients', new_user_option)
|
|
471
|
-
|
|
472
|
-
validate_metadata(package_data) if validate_meta
|
|
473
|
-
|
|
474
|
-
# create a new package container
|
|
475
|
-
created_package = create('packages', package_data)[:data]
|
|
476
|
-
|
|
477
|
-
package_node_api = node_api_from(
|
|
478
|
-
node_id: created_package['node_id'],
|
|
479
|
-
workspace_id: created_package['workspace_id'],
|
|
480
|
-
package_info: created_package)
|
|
481
|
-
|
|
482
|
-
# tell AoC what to expect in package: 1 transfer (can also be done after transfer)
|
|
483
|
-
# TODO: if multi session was used we should probably tell
|
|
484
|
-
# also, currently no "multi-source" , i.e. only from client-side files, unless "node" agent is used
|
|
485
|
-
update("packages/#{created_package['id']}", {'sent' => true, 'transfers_expected' => 1})[:data]
|
|
486
|
-
|
|
487
|
-
return {
|
|
488
|
-
spec: package_node_api.transfer_spec_gen4(created_package['contents_file_id'], Fasp::TransferSpec::DIRECTION_SEND),
|
|
489
|
-
node: package_node_api,
|
|
490
|
-
info: created_package
|
|
491
|
-
}
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
# Add transfer spec
|
|
495
|
-
# callback in Aspera::Node (transfer_spec_gen4)
|
|
496
|
-
def add_ts_tags(transfer_spec:, app_info:)
|
|
497
|
-
# translate transfer direction to upload/download
|
|
498
|
-
transfer_type = Fasp::TransferSpec.action(transfer_spec)
|
|
499
|
-
# Analytics tags
|
|
500
|
-
################
|
|
501
|
-
transfer_spec.deep_merge!({
|
|
502
|
-
'tags' => {
|
|
503
|
-
Fasp::TransferSpec::TAG_RESERVED => {
|
|
504
|
-
'usage_id' => "aspera.files.workspace.#{app_info[:workspace_id]}", # activity tracking
|
|
505
|
-
'files' => {
|
|
506
|
-
'files_transfer_action' => "#{transfer_type}_#{app_info[:app].gsub(/s$/, '')}",
|
|
507
|
-
'workspace_name' => app_info[:workspace_name], # activity tracking
|
|
508
|
-
'workspace_id' => app_info[:workspace_id]
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
})
|
|
513
|
-
# Console cookie
|
|
514
|
-
################
|
|
515
|
-
# we are sure that fields are not nil
|
|
516
|
-
cookie_elements = [app_info[:app], current_user_info['name'], current_user_info['email']].map{|e|Base64.strict_encode64(e)}
|
|
517
|
-
cookie_elements.unshift(COOKIE_PREFIX_CONSOLE_AOC)
|
|
518
|
-
transfer_spec['cookie'] = cookie_elements.join(':')
|
|
519
|
-
# Application tags
|
|
520
|
-
##################
|
|
521
|
-
case app_info[:app]
|
|
522
|
-
when FILES_APP
|
|
523
|
-
file_id = transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['node']['file_id']
|
|
524
|
-
transfer_spec.deep_merge!({'tags' => {Fasp::TransferSpec::TAG_RESERVED => {'files' => {'parentCwd' => "#{app_info[:node_info]['id']}:#{file_id}"}}}}) \
|
|
525
|
-
unless transfer_spec.key?('remote_access_key')
|
|
526
|
-
when PACKAGES_APP
|
|
527
|
-
transfer_spec.deep_merge!({
|
|
528
|
-
'tags' => {
|
|
529
|
-
Fasp::TransferSpec::TAG_RESERVED => {
|
|
530
|
-
'files' => {
|
|
531
|
-
'package_id' => app_info[:package_id],
|
|
532
|
-
'package_name' => app_info[:package_name],
|
|
533
|
-
'package_operation' => transfer_type
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
})
|
|
538
|
-
end
|
|
539
|
-
transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['files']['node_id'] = app_info[:node_info]['id']
|
|
540
|
-
transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['app'] = app_info[:app]
|
|
541
|
-
end
|
|
542
|
-
|
|
543
|
-
ID_AK_ADMIN = 'ASPERA_ACCESS_KEY_ADMIN'
|
|
544
|
-
# Callback from Plugins::Node
|
|
545
|
-
# add application specific tags to permissions creation
|
|
546
|
-
# @param create_param [Hash] parameters for creating permissions
|
|
547
|
-
# @param app_info [Hash] application information
|
|
548
|
-
def permissions_set_create_params(create_param:, app_info:)
|
|
549
|
-
# workspace shared folder:
|
|
550
|
-
# access_id = "#{ID_AK_ADMIN}_WS_#{app_info[:workspace_id]}"
|
|
551
|
-
default_params = {
|
|
552
|
-
# 'access_type' => 'user', # mandatory: user or group
|
|
553
|
-
# 'access_id' => access_id, # id of user or group
|
|
554
|
-
'tags' => {
|
|
555
|
-
Fasp::TransferSpec::TAG_RESERVED => {
|
|
556
|
-
'files' => {
|
|
557
|
-
'workspace' => {
|
|
558
|
-
'id' => app_info[:workspace_id],
|
|
559
|
-
'workspace_name' => app_info[:workspace_name],
|
|
560
|
-
'user_name' => current_user_info['name'],
|
|
561
|
-
'shared_by_user_id' => current_user_info['id'],
|
|
562
|
-
'shared_by_name' => current_user_info['name'],
|
|
563
|
-
'shared_by_email' => current_user_info['email'],
|
|
564
|
-
# 'shared_with_name' => access_id,
|
|
565
|
-
'access_key' => app_info[:node_info]['access_key'],
|
|
566
|
-
'node' => app_info[:node_info]['name']
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
create_param.deep_merge!(default_params)
|
|
573
|
-
if create_param.key?('with')
|
|
574
|
-
contact_info = lookup_by_name(
|
|
575
|
-
'contacts',
|
|
576
|
-
create_param['with'],
|
|
577
|
-
{'current_workspace_id' => app_info[:workspace_id], 'context' => 'share_folder'})
|
|
578
|
-
create_param.delete('with')
|
|
579
|
-
create_param['access_type'] = contact_info['source_type']
|
|
580
|
-
create_param['access_id'] = contact_info['source_id']
|
|
581
|
-
create_param['tags'][Fasp::TransferSpec::TAG_RESERVED]['files']['workspace']['shared_with_name'] = contact_info['email']
|
|
582
|
-
end
|
|
583
|
-
# optional
|
|
584
|
-
app_info[:opt_link_name] = create_param.delete('link_name')
|
|
585
|
-
end
|
|
586
|
-
|
|
587
|
-
# Callback from Plugins::Node
|
|
588
|
-
# send shared folder event to AoC
|
|
589
|
-
# @param created_data [Hash] response from permission creation
|
|
590
|
-
# @param app_info [Hash] hash with app info
|
|
591
|
-
# @param types [Array] event types
|
|
592
|
-
def permissions_send_event(created_data:, app_info:, types: PERMISSIONS_CREATED)
|
|
593
|
-
raise "INTERNAL: (assert) Invalid event types: #{types}" unless types.is_a?(Array) && !types.empty?
|
|
594
|
-
event_creation = {
|
|
595
|
-
'types' => types,
|
|
596
|
-
'node_id' => app_info[:node_info]['id'],
|
|
597
|
-
'workspace_id' => app_info[:workspace_id],
|
|
598
|
-
'data' => created_data
|
|
599
|
-
}
|
|
600
|
-
# (optional). The name of the folder to be displayed to the destination user.
|
|
601
|
-
# Use it if its value is different from the "share_as" field.
|
|
602
|
-
event_creation['link_name'] = app_info[:opt_link_name] unless app_info[:opt_link_name].nil?
|
|
603
|
-
create('events', event_creation)
|
|
604
|
-
end
|
|
605
|
-
end # AoC
|
|
606
|
-
end # Aspera
|
data/lib/aspera/ats_api.rb
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'aspera/log'
|
|
4
|
-
require 'aspera/rest'
|
|
5
|
-
|
|
6
|
-
module Aspera
|
|
7
|
-
class AtsApi < Aspera::Rest
|
|
8
|
-
# currently supported clouds
|
|
9
|
-
# Note to Aspera: shall be an API call
|
|
10
|
-
CLOUD_NAME = {
|
|
11
|
-
aws: 'Amazon Web Services',
|
|
12
|
-
azure: 'Microsoft Azure',
|
|
13
|
-
google: 'Google Cloud',
|
|
14
|
-
limelight: 'Limelight',
|
|
15
|
-
rackspace: 'Rackspace',
|
|
16
|
-
softlayer: 'IBM Cloud'
|
|
17
|
-
}.freeze
|
|
18
|
-
|
|
19
|
-
private_constant :CLOUD_NAME
|
|
20
|
-
|
|
21
|
-
class << self
|
|
22
|
-
def base_url; 'https://ats.aspera.io'; end
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def initialize
|
|
26
|
-
super({base_url: AtsApi.base_url + '/pub/v1'})
|
|
27
|
-
# cache of server data
|
|
28
|
-
@all_servers_cache = nil
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def cloud_names; CLOUD_NAME; end
|
|
32
|
-
|
|
33
|
-
# all available ATS servers
|
|
34
|
-
# NOTE to Aspera: an API shall be created to retrieve all servers at once
|
|
35
|
-
def all_servers
|
|
36
|
-
if @all_servers_cache.nil?
|
|
37
|
-
@all_servers_cache = []
|
|
38
|
-
CLOUD_NAME.each_key do |name|
|
|
39
|
-
read("servers/#{name.to_s.upcase}")[:data].each do |i|
|
|
40
|
-
@all_servers_cache.push(i)
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
return @all_servers_cache
|
|
45
|
-
end
|
|
46
|
-
end # AtsApi
|
|
47
|
-
end # Aspera
|