aspera-cli 4.14.0 → 4.16.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 (104) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +29 -3
  4. data/CHANGELOG.md +300 -185
  5. data/CONTRIBUTING.md +74 -23
  6. data/README.md +2346 -1619
  7. data/bin/ascli +16 -25
  8. data/bin/asession +15 -15
  9. data/examples/dascli +2 -2
  10. data/examples/proxy.pac +1 -1
  11. data/lib/aspera/aoc.rb +216 -150
  12. data/lib/aspera/ascmd.rb +25 -18
  13. data/lib/aspera/assert.rb +45 -0
  14. data/lib/aspera/cli/basic_auth_plugin.rb +9 -6
  15. data/lib/aspera/cli/error.rb +17 -0
  16. data/lib/aspera/cli/extended_value.rb +51 -16
  17. data/lib/aspera/cli/formatter.rb +276 -174
  18. data/lib/aspera/cli/hints.rb +81 -0
  19. data/lib/aspera/cli/main.rb +114 -147
  20. data/lib/aspera/cli/manager.rb +181 -136
  21. data/lib/aspera/cli/plugin.rb +82 -64
  22. data/lib/aspera/cli/plugins/alee.rb +0 -1
  23. data/lib/aspera/cli/plugins/aoc.rb +327 -331
  24. data/lib/aspera/cli/plugins/ats.rb +12 -8
  25. data/lib/aspera/cli/plugins/bss.rb +2 -2
  26. data/lib/aspera/cli/plugins/config.rb +575 -439
  27. data/lib/aspera/cli/plugins/console.rb +40 -0
  28. data/lib/aspera/cli/plugins/cos.rb +4 -5
  29. data/lib/aspera/cli/plugins/faspex.rb +111 -92
  30. data/lib/aspera/cli/plugins/faspex5.rb +245 -182
  31. data/lib/aspera/cli/plugins/node.rb +239 -160
  32. data/lib/aspera/cli/plugins/orchestrator.rb +56 -19
  33. data/lib/aspera/cli/plugins/preview.rb +54 -38
  34. data/lib/aspera/cli/plugins/server.rb +63 -20
  35. data/lib/aspera/cli/plugins/shares.rb +64 -38
  36. data/lib/aspera/cli/sync_actions.rb +68 -0
  37. data/lib/aspera/cli/transfer_agent.rb +64 -67
  38. data/lib/aspera/cli/transfer_progress.rb +73 -0
  39. data/lib/aspera/cli/version.rb +1 -1
  40. data/lib/aspera/colors.rb +3 -1
  41. data/lib/aspera/command_line_builder.rb +27 -22
  42. data/lib/aspera/cos_node.rb +6 -4
  43. data/lib/aspera/coverage.rb +22 -0
  44. data/lib/aspera/data_repository.rb +33 -2
  45. data/lib/aspera/environment.rb +21 -8
  46. data/lib/aspera/fasp/agent_alpha.rb +116 -0
  47. data/lib/aspera/fasp/agent_base.rb +40 -76
  48. data/lib/aspera/fasp/agent_connect.rb +21 -22
  49. data/lib/aspera/fasp/agent_direct.rb +169 -179
  50. data/lib/aspera/fasp/agent_httpgw.rb +200 -195
  51. data/lib/aspera/fasp/agent_node.rb +43 -35
  52. data/lib/aspera/fasp/agent_trsdk.rb +124 -41
  53. data/lib/aspera/fasp/error_info.rb +2 -2
  54. data/lib/aspera/fasp/faux_file.rb +52 -0
  55. data/lib/aspera/fasp/installation.rb +89 -191
  56. data/lib/aspera/fasp/management.rb +249 -0
  57. data/lib/aspera/fasp/parameters.rb +86 -47
  58. data/lib/aspera/fasp/parameters.yaml +75 -8
  59. data/lib/aspera/fasp/products.rb +162 -0
  60. data/lib/aspera/fasp/resume_policy.rb +7 -5
  61. data/lib/aspera/fasp/sync.rb +273 -0
  62. data/lib/aspera/fasp/transfer_spec.rb +10 -8
  63. data/lib/aspera/fasp/uri.rb +6 -6
  64. data/lib/aspera/faspex_gw.rb +11 -8
  65. data/lib/aspera/faspex_postproc.rb +8 -7
  66. data/lib/aspera/hash_ext.rb +2 -2
  67. data/lib/aspera/id_generator.rb +3 -1
  68. data/lib/aspera/json_rpc.rb +51 -0
  69. data/lib/aspera/keychain/encrypted_hash.rb +46 -11
  70. data/lib/aspera/keychain/macos_security.rb +15 -13
  71. data/lib/aspera/line_logger.rb +23 -0
  72. data/lib/aspera/log.rb +61 -19
  73. data/lib/aspera/nagios.rb +7 -2
  74. data/lib/aspera/node.rb +105 -21
  75. data/lib/aspera/node_simulator.rb +214 -0
  76. data/lib/aspera/oauth.rb +57 -36
  77. data/lib/aspera/open_application.rb +4 -4
  78. data/lib/aspera/persistency_action_once.rb +13 -14
  79. data/lib/aspera/persistency_folder.rb +5 -4
  80. data/lib/aspera/preview/file_types.rb +56 -268
  81. data/lib/aspera/preview/generator.rb +28 -39
  82. data/lib/aspera/preview/options.rb +2 -0
  83. data/lib/aspera/preview/terminal.rb +36 -16
  84. data/lib/aspera/preview/utils.rb +23 -29
  85. data/lib/aspera/proxy_auto_config.rb +6 -3
  86. data/lib/aspera/rest.rb +127 -80
  87. data/lib/aspera/rest_call_error.rb +1 -1
  88. data/lib/aspera/rest_error_analyzer.rb +16 -14
  89. data/lib/aspera/rest_errors_aspera.rb +39 -34
  90. data/lib/aspera/secret_hider.rb +18 -17
  91. data/lib/aspera/ssh.rb +10 -5
  92. data/lib/aspera/temp_file_manager.rb +11 -4
  93. data/lib/aspera/web_auth.rb +10 -7
  94. data/lib/aspera/web_server_simple.rb +11 -5
  95. data.tar.gz.sig +0 -0
  96. metadata +108 -39
  97. metadata.gz.sig +0 -0
  98. data/lib/aspera/cli/listener/line_dump.rb +0 -19
  99. data/lib/aspera/cli/listener/logger.rb +0 -22
  100. data/lib/aspera/cli/listener/progress.rb +0 -50
  101. data/lib/aspera/cli/listener/progress_multi.rb +0 -84
  102. data/lib/aspera/cli/plugins/sync.rb +0 -44
  103. data/lib/aspera/fasp/listener.rb +0 -13
  104. data/lib/aspera/sync.rb +0 -213
data/lib/aspera/aoc.rb CHANGED
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'aspera/log'
4
+ require 'aspera/assert'
4
5
  require 'aspera/rest'
5
6
  require 'aspera/hash_ext'
6
7
  require 'aspera/data_repository'
7
8
  require 'aspera/fasp/transfer_spec'
9
+ require 'aspera/node'
8
10
  require 'base64'
9
11
  require 'cgi'
10
12
 
@@ -27,17 +29,16 @@ module Aspera
27
29
  class AoC < Aspera::Rest
28
30
  PRODUCT_NAME = 'Aspera on Cloud'
29
31
  # Production domain of AoC
30
- PROD_DOMAIN = 'ibmaspera.com'
32
+ PROD_DOMAIN = 'ibmaspera.com' # cspell:disable-line
31
33
  # to avoid infinite loop in pub link redirection
32
- MAX_REDIRECT = 10
34
+ MAX_AOC_URL_REDIRECT = 10
35
+ CLIENT_ID_PREFIX = 'aspera.'
33
36
  # Well-known AoC globals client apps
34
- GLOBAL_CLIENT_APPS = %w[aspera.global-cli-client aspera.drive].freeze
35
- # index offset in data repository of client app
36
- DATA_REPO_INDEX_START = 4
37
+ GLOBAL_CLIENT_APPS = DataRepository::ELEMENTS.select{|i|i.to_s.start_with?(CLIENT_ID_PREFIX)}.freeze
37
38
  # cookie prefix so that console can decode identity
38
39
  COOKIE_PREFIX_CONSOLE_AOC = 'aspera.aoc'
39
40
  # path in URL of public links
40
- PUBLIC_LINK_PATHS = %w[/packages/public/receive /packages/public/send /files/public].freeze
41
+ PUBLIC_LINK_PATHS = %w[/packages/public/receive /packages/public/send /files/public /public/files /public/send].freeze
41
42
  JWT_AUDIENCE = 'https://api.asperafiles.com/api/v1/oauth2/token'
42
43
  OAUTH_API_SUBPATH = 'api/v1/oauth2'
43
44
  # minimum fields for user info if retrieval fails
@@ -45,10 +46,10 @@ module Aspera
45
46
  # types of events for shared folder creation
46
47
  # Node events: permission.created permission.modified permission.deleted
47
48
  PERMISSIONS_CREATED = ['permission.created'].freeze
49
+ DEFAULT_WORKSPACE = ''
48
50
 
49
- private_constant :MAX_REDIRECT,
51
+ private_constant :MAX_AOC_URL_REDIRECT,
50
52
  :GLOBAL_CLIENT_APPS,
51
- :DATA_REPO_INDEX_START,
52
53
  :COOKIE_PREFIX_CONSOLE_AOC,
53
54
  :PUBLIC_LINK_PATHS,
54
55
  :JWT_AUDIENCE,
@@ -62,8 +63,6 @@ module Aspera
62
63
  SCOPE_FILES_ADMIN = 'admin:all'
63
64
  SCOPE_FILES_ADMIN_USER = 'admin-user:all'
64
65
  SCOPE_FILES_ADMIN_USER_USER = "#{SCOPE_FILES_ADMIN_USER}+#{SCOPE_FILES_USER}"
65
- SCOPE_NODE_USER = 'user:all'
66
- SCOPE_NODE_ADMIN = 'admin:all'
67
66
  FILES_APP = 'files'
68
67
  PACKAGES_APP = 'packages'
69
68
  API_V1 = 'api/v1'
@@ -71,24 +70,9 @@ module Aspera
71
70
  # class static methods
72
71
  class << self
73
72
  # 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
- # @param url of AoC instance
81
- # @return organization id in url and AoC domain: ibmaspera.com, asperafiles.com or qa.asperafiles.com, etc...
82
- def parse_url(aoc_org_url)
83
- uri = URI.parse(aoc_org_url.gsub(%r{/+$}, ''))
84
- instance_fqdn = uri.host
85
- Log.log.debug{"instance_fqdn=#{instance_fqdn}"}
86
- raise "No host found in URL.Please check URL format: https://myorg.#{PROD_DOMAIN}" if instance_fqdn.nil?
87
- organization, instance_domain = instance_fqdn.split('.', 2)
88
- Log.log.debug{"instance_domain=#{instance_domain}"}
89
- Log.log.debug{"organization=#{organization}"}
90
- raise "expecting a public FQDN for #{PRODUCT_NAME}" if instance_domain.nil?
91
- return organization, instance_domain
73
+ def get_client_info(client_name=nil)
74
+ client_key = client_name.nil? ? GLOBAL_CLIENT_APPS.first : client_name.to_sym
75
+ return client_key, DataRepository.instance.item(client_key)
92
76
  end
93
77
 
94
78
  # base API url depends on domain, which could be "qa.xxx"
@@ -99,126 +83,140 @@ module Aspera
99
83
  def metering_api(entitlement_id, customer_id, api_domain=PROD_DOMAIN)
100
84
  return Rest.new({
101
85
  base_url: "#{api_base_url(api_domain: api_domain)}/metering/v1",
102
- headers: {'X-Aspera-Entitlement-Authorization' => Rest.basic_creds(entitlement_id, customer_id)}
86
+ headers: {'X-Aspera-Entitlement-Authorization' => Rest.basic_token(entitlement_id, customer_id)}
103
87
  })
104
88
  end
105
89
 
106
- # node API scopes
107
- def node_scope(access_key, scope)
108
- return "node.#{access_key}:#{scope}"
90
+ # split host of http://myorg.asperafiles.com in org and domain
91
+ def url_parts(uri)
92
+ raise "No host found in URL.Please check URL format: https://myorg.#{PROD_DOMAIN}" if uri.host.nil?
93
+ parts = uri.host.split('.', 2)
94
+ assert(parts.length == 2){"expecting a public FQDN for #{PRODUCT_NAME}"}
95
+ return parts
109
96
  end
110
97
 
111
- # check option "link"
112
- # if present try to get token value (resolve redirection if short links used)
113
- # then set options url/token/auth
114
- def resolve_pub_link(a_auth, a_opt)
115
- public_link_url = a_opt[:link]
116
- return if public_link_url.nil?
117
- raise 'do not use both link and url options' unless a_opt[:url].nil?
118
- redirect_count = 0
119
- while redirect_count <= MAX_REDIRECT
120
- uri = URI.parse(public_link_url)
121
- # detect if it's an expected format
122
- if PUBLIC_LINK_PATHS.include?(uri.path)
123
- url_param_token_pair = URI.decode_www_form(uri.query).find{|e|e.first.eql?('token')}
124
- raise ArgumentError, 'link option must be URL with "token" parameter' if url_param_token_pair.nil?
125
- # ok we get it !
126
- a_opt[:url] = 'https://' + uri.host
127
- a_auth[:grant_method] = :aoc_pub_link
128
- a_auth[:aoc_pub_link] = {
129
- url: {grant_type: 'url_token'}, # URL args
130
- json: {url_token: url_param_token_pair.last} # JSON body
131
- }
132
- # password protection of link
133
- a_auth[:aoc_pub_link][:json][:password] = a_opt[:password] unless a_opt[:password].nil?
134
- return # SUCCESS
98
+ # @param url [String] URL of AoC public link
99
+ # @return [Hash] information about public link, or nil if not a public link
100
+ def link_info(url)
101
+ final_uri = Rest.new({base_url: url, redirect_max: MAX_AOC_URL_REDIRECT}).read('')[:http].uri
102
+ raise 'AoC shall redirect to login page' if final_uri.query.nil?
103
+ decoded_query = Rest.decode_query(final_uri.query)
104
+ # is that a public link ?
105
+ if decoded_query.key?('token')
106
+ Log.log.warn{"Unknown pub link path: #{final_uri.path}"} unless PUBLIC_LINK_PATHS.include?(final_uri.path)
107
+ # ok we get it !
108
+ return {
109
+ instance_domain: url_parts(final_uri)[1],
110
+ url: 'https://' + final_uri.host,
111
+ token: decoded_query['token']
112
+ }
113
+ end
114
+ Log.log.debug{"path=#{final_uri.path} does not end with /login"} unless final_uri.path.end_with?('/login')
115
+ if decoded_query['state']
116
+ # can be a private link
117
+ state_uri = URI.parse(decoded_query['state'])
118
+ if state_uri.query && decoded_query['redirect_uri']
119
+ decoded_state = Rest.decode_query(state_uri.query)
120
+ if decoded_state.key?('short_link_url')
121
+ if (m = state_uri.path.match(%r{/files/workspaces/([0-9]+)/all/([0-9]+):([0-9]+)}))
122
+ redirect_uri = URI.parse(decoded_query['redirect_uri'])
123
+ parts = url_parts(redirect_uri)
124
+ return {
125
+ instance_domain: parts[1],
126
+ organization: parts[0],
127
+ url: 'https://' + redirect_uri.host,
128
+ private_link: {
129
+ workspace_id: m[1],
130
+ node_id: m[2],
131
+ file_id: m[3]
132
+ }
133
+ }
134
+ end
135
+ end
135
136
  end
136
- Log.log.debug{"no expected format: #{public_link_url}"}
137
- r = Net::HTTP.get_response(uri)
138
- # not a redirection
139
- raise ArgumentError, 'link option must be redirect or have token parameter' unless r.code.start_with?('3')
140
- public_link_url = r['location']
141
- raise 'no location in redirection' if public_link_url.nil?
142
- Log.log.debug{"redirect to: #{public_link_url}"}
143
- end # loop
144
- raise "exceeded max redirection: #{MAX_REDIRECT}"
137
+ end
138
+ parts = url_parts(URI.parse(url))
139
+ return {
140
+ instance_domain: parts[1],
141
+ organization: parts[0]
142
+ }
145
143
  end
146
144
  end # static methods
147
145
 
148
- # CLI options that are also options to initialize
149
- OPTIONS_NEW = %i[link url auth client_id client_secret scope redirect_uri private_key passphrase username password].freeze
150
-
151
- # @param any of OPTIONS_NEW + subpath
152
- def initialize(opt)
153
- raise ArgumentError, 'Missing mandatory option: scope' if opt[:scope].nil?
146
+ attr_reader :private_link
154
147
 
148
+ 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,
149
+ password: nil, workspace: nil, secret_finder: nil)
150
+ # test here because link may set url
151
+ raise ArgumentError, 'Missing mandatory option: url' if url.nil?
152
+ raise ArgumentError, 'Missing mandatory option: scope' if scope.nil?
153
+ # default values for client id
154
+ client_id, client_secret = self.class.get_client_info if client_id.nil?
155
155
  # access key secrets are provided out of band to get node api access
156
156
  # key: access key
157
157
  # value: associated secret
158
- @secret_finder = nil
158
+ @secret_finder = secret_finder
159
+ @workspace_name = workspace
159
160
  @cache_user_info = nil
160
161
  @cache_url_token_info = nil
161
-
162
+ @context_cache = nil
162
163
  # init rest params
163
164
  aoc_rest_p = {auth: {type: :oauth2}}
164
165
  # shortcut to auth section
165
166
  aoc_auth_p = aoc_rest_p[:auth]
166
-
167
- # sets opt[:url], aoc_rest_p[:auth][:grant_method], [:auth][:aoc_pub_link] if there is a link
168
- self.class.resolve_pub_link(aoc_auth_p, opt)
169
-
170
- # test here because link may set url
171
- raise ArgumentError, 'Missing mandatory option: url' if opt[:url].nil?
172
-
173
- # get org name and domain from url
174
- organization, instance_domain = self.class.parse_url(opt[:url])
167
+ # analyze type of url
168
+ url_info = AoC.link_info(url)
169
+ Log.log.debug{Log.dump(:url_info, url_info)}
170
+ @private_link = url_info[:private_link]
171
+ aoc_auth_p[:grant_method] = if url_info.key?(:token)
172
+ :aoc_pub_link
173
+ else
174
+ raise ArgumentError, 'Missing mandatory option: auth' if auth.nil?
175
+ auth
176
+ end
175
177
  # this is the base API url
176
- api_url_base = self.class.api_base_url(api_domain: instance_domain)
178
+ api_url_base = self.class.api_base_url(api_domain: url_info[:instance_domain])
177
179
  # API URL, including subpath (version ...)
178
- aoc_rest_p[:base_url] = "#{api_url_base}/#{opt[:subpath]}"
179
- # base auth URL
180
- aoc_auth_p[:base_url] = "#{api_url_base}/#{OAUTH_API_SUBPATH}/#{organization}"
181
- aoc_auth_p[:client_id] = opt[:client_id]
182
- aoc_auth_p[:client_secret] = opt[:client_secret]
183
- aoc_auth_p[:scope] = opt[:scope]
184
-
185
- # filled if pub link
186
- if !aoc_auth_p.key?(:grant_method)
187
- raise ArgumentError, 'Missing mandatory option: auth' if opt[:auth].nil?
188
- aoc_auth_p[:grant_method] = opt[:auth]
189
- end
190
-
191
- if aoc_auth_p[:client_id].nil?
192
- aoc_auth_p[:client_id], aoc_auth_p[:client_secret] = self.class.get_client_info
193
- end
180
+ aoc_rest_p[:base_url] = "#{api_url_base}/#{subpath}"
181
+ # auth URL
182
+ aoc_auth_p[:base_url] = "#{api_url_base}/#{OAUTH_API_SUBPATH}/#{url_info[:organization]}"
183
+ aoc_auth_p[:client_id] = client_id
184
+ aoc_auth_p[:client_secret] = client_secret
185
+ aoc_auth_p[:scope] = scope
194
186
 
195
187
  # fill other auth parameters based on Oauth method
196
188
  case aoc_auth_p[:grant_method]
197
189
  when :web
198
- raise ArgumentError, 'Missing mandatory option: redirect_uri' if opt[:redirect_uri].nil?
199
- aoc_auth_p[:web] = {redirect_uri: opt[:redirect_uri]}
190
+ raise ArgumentError, 'Missing mandatory option: redirect_uri' if redirect_uri.nil?
191
+ aoc_auth_p[:web] = {redirect_uri: redirect_uri}
200
192
  when :jwt
201
- raise ArgumentError, 'Missing mandatory option: private_key' if opt[:private_key].nil?
202
- raise ArgumentError, 'Missing mandatory option: username' if opt[:username].nil?
193
+ raise ArgumentError, 'Missing mandatory option: private_key' if private_key.nil?
194
+ raise ArgumentError, 'Missing mandatory option: username' if username.nil?
203
195
  aoc_auth_p[:jwt] = {
204
- private_key_obj: OpenSSL::PKey::RSA.new(opt[:private_key], opt[:passphrase]),
196
+ private_key_obj: OpenSSL::PKey::RSA.new(private_key, passphrase),
205
197
  payload: {
206
- iss: aoc_auth_p[:client_id], # issuer
207
- sub: opt[:username], # subject
198
+ iss: aoc_auth_p[:client_id], # issuer
199
+ sub: username, # subject
208
200
  aud: JWT_AUDIENCE
209
201
  }
210
202
  }
211
203
  # add jwt payload for global ids
212
- aoc_auth_p[:jwt][:payload][:org] = organization if GLOBAL_CLIENT_APPS.include?(aoc_auth_p[:client_id])
204
+ aoc_auth_p[:jwt][:payload][:org] = url_info[:organization] if GLOBAL_CLIENT_APPS.include?(aoc_auth_p[:client_id])
213
205
  when :aoc_pub_link
206
+ aoc_auth_p[:aoc_pub_link] = {
207
+ url: {grant_type: 'url_token'}, # URL arguments
208
+ json: {url_token: url_info[:token]} # JSON body
209
+ }
210
+ # password protection of link
211
+ aoc_auth_p[:aoc_pub_link][:json][:password] = password unless password.nil?
214
212
  # basic auth required for /token
215
213
  aoc_auth_p[:auth] = {type: :basic, username: aoc_auth_p[:client_id], password: aoc_auth_p[:client_secret]}
216
- else raise "ERROR: unsupported auth method: #{aoc_auth_p[:grant_method]}"
214
+ else error_unexpected_value(aoc_auth_p[:grant_method])
217
215
  end
218
216
  super(aoc_rest_p)
219
217
  end
220
218
 
221
- def url_token_data
219
+ def public_link
222
220
  return nil unless params[:auth][:grant_method].eql?(:aoc_pub_link)
223
221
  return @cache_url_token_info unless @cache_url_token_info.nil?
224
222
  # TODO: can there be several in list ?
@@ -226,42 +224,100 @@ module Aspera
226
224
  return @cache_url_token_info
227
225
  end
228
226
 
229
- def additional_persistence_ids
230
- return [current_user_info['id']] if url_token_data.nil?
231
- return [] # TODO : url_token_data['id'] ?
227
+ def assert_public_link_types(expected)
228
+ assert_values(public_link['purpose'], expected){'public link type'}
232
229
  end
233
230
 
234
- def secret_finder=(secret_finder)
235
- raise 'secret finder already set' unless @secret_finder.nil?
236
- raise 'secret finder must have lookup_secret' unless secret_finder.respond_to?(:lookup_secret)
237
- @secret_finder = secret_finder
231
+ def additional_persistence_ids
232
+ return [current_user_info['id']] if public_link.nil?
233
+ return [] # TODO : public_link['id'] ?
238
234
  end
239
235
 
240
236
  # cached user information
241
237
  def current_user_info(exception: false)
242
- if @cache_user_info.nil?
243
- # get our user's default information
244
- @cache_user_info =
245
- begin
246
- read('self')[:data]
247
- rescue StandardError => e
248
- raise e if exception
249
- Log.log.debug{"ignoring error: #{e}"}
250
- {}
251
- end
252
- USER_INFO_FIELDS_MIN.each{|f|@cache_user_info[f] = 'unknown' if @cache_user_info[f].nil?}
253
- end
238
+ return @cache_user_info unless @cache_user_info.nil?
239
+ # get our user's default information
240
+ @cache_user_info =
241
+ begin
242
+ read('self')[:data]
243
+ rescue StandardError => e
244
+ raise e if exception
245
+ Log.log.debug{"ignoring error: #{e}"}
246
+ {}
247
+ end
248
+ USER_INFO_FIELDS_MIN.each{|f|@cache_user_info[f] = nil if @cache_user_info[f].nil?}
254
249
  return @cache_user_info
255
250
  end
256
251
 
252
+ # @param application [Symbol] :files or :packages
253
+ # @return [Hash] current context information: workspace, and home node/file if app is "Files"
254
+ def context(application = nil)
255
+ return @context_cache unless @context_cache.nil?
256
+ assert(!application.nil?){'application must be set once'}
257
+ assert_values(application, %i[files packages])
258
+ ws_id =
259
+ if !public_link.nil?
260
+ Log.log.debug('Using workspace of public link')
261
+ public_link['data']['workspace_id']
262
+ elsif !private_link.nil?
263
+ Log.log.debug('Using workspace of private link')
264
+ private_link[:workspace_id]
265
+ elsif @workspace_name.eql?(DEFAULT_WORKSPACE)
266
+ Log.log.debug('Using default workspace'.green)
267
+ raise 'User does not have default workspace, please specify workspace' if current_user_info['default_workspace_id'].nil?
268
+ current_user_info['default_workspace_id']
269
+ elsif @workspace_name.nil?
270
+ nil
271
+ else
272
+ lookup_by_name('workspaces', @workspace_name)['id']
273
+ end
274
+ ws_info =
275
+ if ws_id.nil?
276
+ nil
277
+ else
278
+ read("workspaces/#{ws_id}")[:data]
279
+ end
280
+ @context_cache = if ws_info.nil?
281
+ {
282
+ workspace_id: nil,
283
+ workspace_name: 'Shared folders'
284
+ }
285
+ else
286
+ {
287
+ workspace_id: ws_info['id'],
288
+ workspace_name: ws_info['name']
289
+ }
290
+ end
291
+ return @context_cache unless application.eql?(:files)
292
+ if !public_link.nil?
293
+ assert_public_link_types(['view_shared_file'])
294
+ @context_cache[:home_node_id] = public_link['data']['node_id']
295
+ @context_cache[:home_file_id] = public_link['data']['file_id']
296
+ elsif !private_link.nil?
297
+ @context_cache[:home_node_id] = private_link[:node_id]
298
+ @context_cache[:home_file_id] = private_link[:file_id]
299
+ elsif ws_info
300
+ @context_cache[:home_node_id] = ws_info['home_node_id']
301
+ @context_cache[:home_file_id] = ws_info['home_file_id']
302
+ else
303
+ # not part of any workspace, but has some folder shared
304
+ user_info = current_user_info(exception: true) rescue {'read_only_home_node_id' => nil, 'read_only_home_file_id' => nil}
305
+ @context_cache[:home_node_id] = user_info['read_only_home_node_id']
306
+ @context_cache[:home_file_id] = user_info['read_only_home_file_id']
307
+ end
308
+ raise "Cannot get user's home node id, check your default workspace or specify one" if @context_cache[:home_node_id].to_s.empty?
309
+ Log.log.debug{Log.dump(:context, @context_cache)}
310
+ return @context_cache
311
+ end
312
+
257
313
  # @param node_id [String] identifier of node in AoC
258
314
  # @param workspace_id [String] workspace identifier
259
315
  # @param workspace_name [String] workspace name
260
- # @param scope e.g. SCOPE_NODE_USER, or nil (requires secret)
316
+ # @param scope e.g. Aspera::Node::SCOPE_USER, or nil (requires secret)
261
317
  # @param package_info [Hash] created package information
262
318
  # @returns [Aspera::Node] a node API for access key
263
- def node_api_from(node_id:, workspace_id: nil, workspace_name: nil, scope: SCOPE_NODE_USER, package_info: nil)
264
- raise 'invalid type for node_id' unless node_id.is_a?(String)
319
+ def node_api_from(node_id:, workspace_id: nil, workspace_name: nil, scope: Aspera::Node::SCOPE_USER, package_info: nil)
320
+ assert_type(node_id, String)
265
321
  node_info = read("nodes/#{node_id}")[:data]
266
322
  if workspace_name.nil? && !workspace_id.nil?
267
323
  workspace_name = read("workspaces/#{workspace_id}")[:data]['name']
@@ -289,7 +345,7 @@ module Aspera
289
345
  else
290
346
  # OAuth bearer token
291
347
  node_rest_params[:auth] = params[:auth].clone
292
- node_rest_params[:auth][:scope] = self.class.node_scope(node_info['access_key'], scope)
348
+ node_rest_params[:auth][:scope] = Aspera::Node.token_scope(node_info['access_key'], scope)
293
349
  # special header required for bearer token only
294
350
  node_rest_params[:headers] = {Aspera::Node::HEADER_X_ASPERA_ACCESS_KEY => node_info['access_key']}
295
351
  end
@@ -308,16 +364,16 @@ module Aspera
308
364
  Log.log.debug('no metadata in shared inbox')
309
365
  return
310
366
  end
367
+ assert(pkg_data.key?('metadata')){"package requires metadata: #{meta_schema}"}
311
368
  pkg_meta = pkg_data['metadata']
312
- raise "package requires metadata: #{meta_schema}" unless pkg_data.key?('metadata')
313
- raise 'metadata must be an Array' unless pkg_meta.is_a?(Array)
314
- Log.dump(:metadata, pkg_meta)
369
+ assert_type(pkg_meta, Array){'metadata'}
370
+ Log.log.debug{Log.dump(:metadata, pkg_meta)}
315
371
  pkg_meta.each do |field|
316
- raise 'metadata field must be Hash' unless field.is_a?(Hash)
317
- raise 'metadata field must have name' unless field.key?('name')
318
- raise 'metadata field must have values' unless field.key?('values')
319
- raise 'metadata values must be an Array' unless field['values'].is_a?(Array)
320
- raise "unknown metadata field: #{field['name']}" if meta_schema.select{|i|i['name'].eql?(field['name'])}.empty?
372
+ assert_type(field, Hash){'metadata field'}
373
+ assert(field.key?('name')){'metadata field must have name'}
374
+ assert(field.key?('values')){'metadata field must have values'}
375
+ assert_type(field['values'], Array){'metadata field values'}
376
+ assert(!meta_schema.select{|i|i['name'].eql?(field['name'])}.empty?){"unknown metadata field: #{field['name']}"}
321
377
  end
322
378
  meta_schema.each do |field|
323
379
  provided = pkg_meta.select{|i|i['name'].eql?(field['name'])}
@@ -334,15 +390,15 @@ module Aspera
334
390
  # @return nil package_data is modified
335
391
  def resolve_package_recipients(package_data, ws_id, recipient_list_field, new_user_option)
336
392
  return unless package_data.key?(recipient_list_field)
337
- raise "#{recipient_list_field} must be an Array" unless package_data[recipient_list_field].is_a?(Array)
393
+ assert_type(package_data[recipient_list_field], Array){recipient_list_field}
338
394
  new_user_option = {'package_contact' => true} if new_user_option.nil?
339
- raise 'new_user_option must be a Hash' unless new_user_option.is_a?(Hash)
395
+ assert_type(new_user_option, Hash){'new_user_option'}
340
396
  # list with resolved elements
341
397
  resolved_list = []
342
398
  package_data[recipient_list_field].each do |short_recipient_info|
343
399
  case short_recipient_info
344
400
  when Hash # native API information, check keys
345
- raise "#{recipient_list_field} element shall have fields: id and type" unless short_recipient_info.keys.sort.eql?(%w[id type])
401
+ assert(short_recipient_info.keys.sort.eql?(%w[id type])){"#{recipient_list_field} element shall have fields: id and type"}
346
402
  when String # CLI helper: need to resolve provided name to type/id
347
403
  # email: user, else dropbox
348
404
  entity_type = short_recipient_info.include?('@') ? 'contacts' : 'dropboxes'
@@ -355,7 +411,8 @@ module Aspera
355
411
  # unknown user: create it as external user
356
412
  full_recipient_info = create('contacts', {
357
413
  'current_workspace_id' => ws_id,
358
- 'email' => short_recipient_info}.merge(new_user_option))[:data]
414
+ 'email' => short_recipient_info
415
+ }.merge(new_user_option))[:data]
359
416
  end
360
417
  short_recipient_info = if entity_type.eql?('dropboxes')
361
418
  {'id' => full_recipient_info['id'], 'type' => 'dropbox'}
@@ -387,7 +444,7 @@ module Aspera
387
444
  })
388
445
  end
389
446
  pkg_data['metadata'] = api_meta
390
- else raise "metadata field if not of expected type: #{pkg_meta.class}"
447
+ else error_unexpected_value(pkg_meta.class)
391
448
  end
392
449
  return nil
393
450
  end
@@ -428,7 +485,7 @@ module Aspera
428
485
  }
429
486
  end
430
487
 
431
- # Add transferspec
488
+ # Add transfer spec
432
489
  # callback in Aspera::Node (transfer_spec_gen4)
433
490
  def add_ts_tags(transfer_spec:, app_info:)
434
491
  # translate transfer direction to upload/download
@@ -450,7 +507,7 @@ module Aspera
450
507
  # Console cookie
451
508
  ################
452
509
  # we are sure that fields are not nil
453
- cookie_elements = [app_info[:app], current_user_info['name'], current_user_info['email']].map{|e|Base64.strict_encode64(e)}
510
+ cookie_elements = [app_info[:app], current_user_info['name'] || 'public link', current_user_info['email'] || 'none'].map{|e|Base64.strict_encode64(e)}
454
511
  cookie_elements.unshift(COOKIE_PREFIX_CONSOLE_AOC)
455
512
  transfer_spec['cookie'] = cookie_elements.join(':')
456
513
  # Application tags
@@ -468,7 +525,10 @@ module Aspera
468
525
  'package_id' => app_info[:package_id],
469
526
  'package_name' => app_info[:package_name],
470
527
  'package_operation' => transfer_type
471
- }}}})
528
+ }
529
+ }
530
+ }
531
+ })
472
532
  end
473
533
  transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['files']['node_id'] = app_info[:node_info]['id']
474
534
  transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['app'] = app_info[:app]
@@ -497,7 +557,12 @@ module Aspera
497
557
  'shared_by_email' => current_user_info['email'],
498
558
  # 'shared_with_name' => access_id,
499
559
  'access_key' => app_info[:node_info]['access_key'],
500
- 'node' => app_info[:node_info]['name']}}}}}
560
+ 'node' => app_info[:node_info]['name']
561
+ }
562
+ }
563
+ }
564
+ }
565
+ }
501
566
  create_param.deep_merge!(default_params)
502
567
  if create_param.key?('with')
503
568
  contact_info = lookup_by_name(
@@ -519,7 +584,8 @@ module Aspera
519
584
  # @param app_info [Hash] hash with app info
520
585
  # @param types [Array] event types
521
586
  def permissions_send_event(created_data:, app_info:, types: PERMISSIONS_CREATED)
522
- raise "INTERNAL: (assert) Invalid event types: #{types}" unless types.is_a?(Array) && !types.empty?
587
+ assert_type(types, Array)
588
+ assert(!types.empty?)
523
589
  event_creation = {
524
590
  'types' => types,
525
591
  'node_id' => app_info[:node_info]['id'],