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.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +29 -3
  4. data/CHANGELOG.md +375 -280
  5. data/CONTRIBUTING.md +71 -18
  6. data/README.md +1978 -1656
  7. data/bin/ascli +13 -31
  8. data/bin/asession +32 -22
  9. data/examples/dascli +2 -2
  10. data/lib/aspera/agent/alpha.rb +117 -0
  11. data/lib/aspera/agent/base.rb +61 -0
  12. data/lib/aspera/{fasp/agent_connect.rb → agent/connect.rb} +13 -11
  13. data/lib/aspera/{fasp/agent_direct.rb → agent/direct.rb} +116 -116
  14. data/lib/aspera/{fasp/agent_httpgw.rb → agent/httpgw.rb} +21 -19
  15. data/lib/aspera/{fasp/agent_node.rb → agent/node.rb} +21 -33
  16. data/lib/aspera/agent/trsdk.rb +188 -0
  17. data/lib/aspera/api/aoc.rb +586 -0
  18. data/lib/aspera/api/ats.rb +46 -0
  19. data/lib/aspera/api/cos_node.rb +95 -0
  20. data/lib/aspera/api/node.rb +344 -0
  21. data/lib/aspera/ascmd.rb +47 -14
  22. data/lib/aspera/{fasp → ascp}/installation.rb +54 -15
  23. data/lib/aspera/{fasp → ascp}/management.rb +14 -14
  24. data/lib/aspera/{fasp → ascp}/products.rb +1 -1
  25. data/lib/aspera/assert.rb +45 -0
  26. data/lib/aspera/cli/basic_auth_plugin.rb +11 -10
  27. data/lib/aspera/cli/extended_value.rb +5 -5
  28. data/lib/aspera/cli/formatter.rb +27 -14
  29. data/lib/aspera/cli/hints.rb +7 -6
  30. data/lib/aspera/cli/main.rb +49 -29
  31. data/lib/aspera/cli/manager.rb +46 -36
  32. data/lib/aspera/cli/plugin.rb +34 -20
  33. data/lib/aspera/cli/plugin_factory.rb +61 -0
  34. data/lib/aspera/cli/plugins/alee.rb +7 -7
  35. data/lib/aspera/cli/plugins/aoc.rb +168 -132
  36. data/lib/aspera/cli/plugins/ats.rb +33 -33
  37. data/lib/aspera/cli/plugins/bss.rb +3 -4
  38. data/lib/aspera/cli/plugins/config.rb +250 -272
  39. data/lib/aspera/cli/plugins/console.rb +8 -6
  40. data/lib/aspera/cli/plugins/cos.rb +20 -19
  41. data/lib/aspera/cli/plugins/faspex.rb +71 -60
  42. data/lib/aspera/cli/plugins/faspex5.rb +212 -133
  43. data/lib/aspera/cli/plugins/node.rb +83 -75
  44. data/lib/aspera/cli/plugins/orchestrator.rb +36 -44
  45. data/lib/aspera/cli/plugins/preview.rb +33 -31
  46. data/lib/aspera/cli/plugins/server.rb +33 -32
  47. data/lib/aspera/cli/plugins/shares.rb +39 -33
  48. data/lib/aspera/cli/sync_actions.rb +9 -9
  49. data/lib/aspera/cli/transfer_agent.rb +45 -25
  50. data/lib/aspera/cli/transfer_progress.rb +2 -3
  51. data/lib/aspera/cli/version.rb +1 -1
  52. data/lib/aspera/colors.rb +5 -0
  53. data/lib/aspera/command_line_builder.rb +16 -14
  54. data/lib/aspera/coverage.rb +21 -0
  55. data/lib/aspera/data_repository.rb +33 -2
  56. data/lib/aspera/environment.rb +5 -4
  57. data/lib/aspera/faspex_gw.rb +13 -11
  58. data/lib/aspera/faspex_postproc.rb +6 -5
  59. data/lib/aspera/id_generator.rb +4 -2
  60. data/lib/aspera/json_rpc.rb +10 -8
  61. data/lib/aspera/keychain/encrypted_hash.rb +46 -11
  62. data/lib/aspera/keychain/macos_security.rb +29 -22
  63. data/lib/aspera/log.rb +5 -4
  64. data/lib/aspera/nagios.rb +7 -2
  65. data/lib/aspera/node_simulator.rb +213 -0
  66. data/lib/aspera/oauth/base.rb +143 -0
  67. data/lib/aspera/oauth/factory.rb +124 -0
  68. data/lib/aspera/oauth/generic.rb +34 -0
  69. data/lib/aspera/oauth/jwt.rb +51 -0
  70. data/lib/aspera/oauth/url_json.rb +31 -0
  71. data/lib/aspera/oauth/web.rb +50 -0
  72. data/lib/aspera/oauth.rb +5 -328
  73. data/lib/aspera/open_application.rb +7 -7
  74. data/lib/aspera/persistency_action_once.rb +13 -14
  75. data/lib/aspera/persistency_folder.rb +3 -2
  76. data/lib/aspera/preview/file_types.rb +53 -267
  77. data/lib/aspera/preview/generator.rb +7 -5
  78. data/lib/aspera/preview/terminal.rb +17 -7
  79. data/lib/aspera/preview/utils.rb +8 -7
  80. data/lib/aspera/proxy_auto_config.rb +6 -3
  81. data/lib/aspera/rest.rb +187 -140
  82. data/lib/aspera/rest_error_analyzer.rb +1 -0
  83. data/lib/aspera/rest_errors_aspera.rb +5 -3
  84. data/lib/aspera/resumer.rb +77 -0
  85. data/lib/aspera/secret_hider.rb +5 -2
  86. data/lib/aspera/ssh.rb +15 -8
  87. data/lib/aspera/temp_file_manager.rb +1 -1
  88. data/lib/aspera/{fasp → transfer}/error.rb +3 -3
  89. data/lib/aspera/{fasp → transfer}/error_info.rb +1 -1
  90. data/lib/aspera/{fasp → transfer}/faux_file.rb +1 -1
  91. data/lib/aspera/{fasp → transfer}/parameters.rb +95 -120
  92. data/lib/aspera/{fasp/transfer_spec.rb → transfer/spec.rb} +23 -19
  93. data/lib/aspera/{fasp/parameters.yaml → transfer/spec.yaml} +4 -99
  94. data/lib/aspera/transfer/sync.rb +273 -0
  95. data/lib/aspera/{fasp → transfer}/uri.rb +10 -9
  96. data/lib/aspera/web_server_simple.rb +12 -3
  97. data.tar.gz.sig +0 -0
  98. metadata +92 -68
  99. metadata.gz.sig +0 -0
  100. data/lib/aspera/aoc.rb +0 -606
  101. data/lib/aspera/ats_api.rb +0 -47
  102. data/lib/aspera/cos_node.rb +0 -93
  103. data/lib/aspera/fasp/agent_aspera.rb +0 -126
  104. data/lib/aspera/fasp/agent_base.rb +0 -48
  105. data/lib/aspera/fasp/agent_trsdk.rb +0 -146
  106. data/lib/aspera/fasp/resume_policy.rb +0 -77
  107. data/lib/aspera/node.rb +0 -338
  108. 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
@@ -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