aspera-cli 4.14.0 → 4.16.0

Sign up to get free protection for your applications and to get access to all the features.
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'],