aspera-cli 4.13.0 → 4.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +81 -7
  4. data/CONTRIBUTING.md +22 -6
  5. data/README.md +2038 -1080
  6. data/bin/ascli +18 -9
  7. data/bin/asession +12 -14
  8. data/examples/dascli +1 -1
  9. data/examples/proxy.pac +1 -1
  10. data/examples/rubyc +24 -0
  11. data/lib/aspera/aoc.rb +219 -159
  12. data/lib/aspera/ascmd.rb +25 -14
  13. data/lib/aspera/cli/basic_auth_plugin.rb +12 -9
  14. data/lib/aspera/cli/error.rb +17 -0
  15. data/lib/aspera/cli/extended_value.rb +47 -12
  16. data/lib/aspera/cli/formatter.rb +260 -179
  17. data/lib/aspera/cli/hints.rb +80 -0
  18. data/lib/aspera/cli/main.rb +104 -156
  19. data/lib/aspera/cli/manager.rb +259 -209
  20. data/lib/aspera/cli/plugin.rb +123 -63
  21. data/lib/aspera/cli/plugins/alee.rb +2 -3
  22. data/lib/aspera/cli/plugins/aoc.rb +341 -261
  23. data/lib/aspera/cli/plugins/ats.rb +22 -21
  24. data/lib/aspera/cli/plugins/bss.rb +5 -5
  25. data/lib/aspera/cli/plugins/config.rb +578 -627
  26. data/lib/aspera/cli/plugins/console.rb +44 -6
  27. data/lib/aspera/cli/plugins/cos.rb +15 -17
  28. data/lib/aspera/cli/plugins/faspex.rb +114 -100
  29. data/lib/aspera/cli/plugins/faspex5.rb +411 -264
  30. data/lib/aspera/cli/plugins/node.rb +354 -259
  31. data/lib/aspera/cli/plugins/orchestrator.rb +61 -29
  32. data/lib/aspera/cli/plugins/preview.rb +82 -90
  33. data/lib/aspera/cli/plugins/server.rb +79 -32
  34. data/lib/aspera/cli/plugins/shares.rb +55 -42
  35. data/lib/aspera/cli/sync_actions.rb +68 -0
  36. data/lib/aspera/cli/transfer_agent.rb +66 -73
  37. data/lib/aspera/cli/transfer_progress.rb +74 -0
  38. data/lib/aspera/cli/version.rb +1 -1
  39. data/lib/aspera/colors.rb +12 -8
  40. data/lib/aspera/command_line_builder.rb +14 -11
  41. data/lib/aspera/cos_node.rb +3 -2
  42. data/lib/aspera/data/6 +0 -0
  43. data/lib/aspera/environment.rb +24 -9
  44. data/lib/aspera/fasp/agent_aspera.rb +126 -0
  45. data/lib/aspera/fasp/agent_base.rb +31 -77
  46. data/lib/aspera/fasp/agent_connect.rb +25 -21
  47. data/lib/aspera/fasp/agent_direct.rb +89 -103
  48. data/lib/aspera/fasp/agent_httpgw.rb +231 -149
  49. data/lib/aspera/fasp/agent_node.rb +41 -34
  50. data/lib/aspera/fasp/agent_trsdk.rb +75 -32
  51. data/lib/aspera/fasp/error_info.rb +4 -2
  52. data/lib/aspera/fasp/faux_file.rb +52 -0
  53. data/lib/aspera/fasp/installation.rb +53 -195
  54. data/lib/aspera/fasp/management.rb +244 -0
  55. data/lib/aspera/fasp/parameters.rb +71 -37
  56. data/lib/aspera/fasp/parameters.yaml +76 -8
  57. data/lib/aspera/fasp/products.rb +162 -0
  58. data/lib/aspera/fasp/resume_policy.rb +3 -3
  59. data/lib/aspera/fasp/transfer_spec.rb +7 -6
  60. data/lib/aspera/fasp/uri.rb +26 -24
  61. data/lib/aspera/faspex_gw.rb +2 -2
  62. data/lib/aspera/faspex_postproc.rb +2 -2
  63. data/lib/aspera/hash_ext.rb +14 -4
  64. data/lib/aspera/json_rpc.rb +49 -0
  65. data/lib/aspera/keychain/macos_security.rb +13 -13
  66. data/lib/aspera/line_logger.rb +23 -0
  67. data/lib/aspera/log.rb +58 -16
  68. data/lib/aspera/node.rb +157 -92
  69. data/lib/aspera/oauth.rb +37 -19
  70. data/lib/aspera/open_application.rb +4 -4
  71. data/lib/aspera/persistency_action_once.rb +1 -1
  72. data/lib/aspera/persistency_folder.rb +2 -2
  73. data/lib/aspera/preview/file_types.rb +4 -2
  74. data/lib/aspera/preview/generator.rb +22 -35
  75. data/lib/aspera/preview/options.rb +2 -0
  76. data/lib/aspera/preview/terminal.rb +73 -16
  77. data/lib/aspera/preview/utils.rb +21 -28
  78. data/lib/aspera/proxy_auto_config.js +2 -2
  79. data/lib/aspera/rest.rb +136 -68
  80. data/lib/aspera/rest_call_error.rb +1 -1
  81. data/lib/aspera/rest_error_analyzer.rb +15 -14
  82. data/lib/aspera/rest_errors_aspera.rb +37 -34
  83. data/lib/aspera/secret_hider.rb +18 -15
  84. data/lib/aspera/ssh.rb +5 -2
  85. data/lib/aspera/sync.rb +127 -119
  86. data/lib/aspera/temp_file_manager.rb +10 -3
  87. data/lib/aspera/web_auth.rb +10 -7
  88. data/lib/aspera/web_server_simple.rb +9 -4
  89. data.tar.gz.sig +0 -0
  90. metadata +34 -17
  91. metadata.gz.sig +0 -0
  92. data/docs/test_env.conf +0 -186
  93. data/lib/aspera/cli/listener/line_dump.rb +0 -19
  94. data/lib/aspera/cli/listener/logger.rb +0 -22
  95. data/lib/aspera/cli/listener/progress.rb +0 -50
  96. data/lib/aspera/cli/listener/progress_multi.rb +0 -84
  97. data/lib/aspera/cli/plugins/sync.rb +0 -44
  98. data/lib/aspera/data/7 +0 -0
  99. data/lib/aspera/fasp/listener.rb +0 -13
data/lib/aspera/aoc.rb CHANGED
@@ -5,6 +5,7 @@ require 'aspera/rest'
5
5
  require 'aspera/hash_ext'
6
6
  require 'aspera/data_repository'
7
7
  require 'aspera/fasp/transfer_spec'
8
+ require 'aspera/node'
8
9
  require 'base64'
9
10
  require 'cgi'
10
11
 
@@ -27,9 +28,9 @@ module Aspera
27
28
  class AoC < Aspera::Rest
28
29
  PRODUCT_NAME = 'Aspera on Cloud'
29
30
  # Production domain of AoC
30
- PROD_DOMAIN = 'ibmaspera.com'
31
+ PROD_DOMAIN = 'ibmaspera.com' # cspell:disable-line
31
32
  # to avoid infinite loop in pub link redirection
32
- MAX_REDIRECT = 10
33
+ MAX_AOC_URL_REDIRECT = 10
33
34
  # Well-known AoC globals client apps
34
35
  GLOBAL_CLIENT_APPS = %w[aspera.global-cli-client aspera.drive].freeze
35
36
  # index offset in data repository of client app
@@ -37,20 +38,25 @@ module Aspera
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
44
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 = ''
45
50
 
46
- private_constant :MAX_REDIRECT,
51
+ private_constant :MAX_AOC_URL_REDIRECT,
47
52
  :GLOBAL_CLIENT_APPS,
48
53
  :DATA_REPO_INDEX_START,
49
54
  :COOKIE_PREFIX_CONSOLE_AOC,
50
55
  :PUBLIC_LINK_PATHS,
51
56
  :JWT_AUDIENCE,
52
57
  :OAUTH_API_SUBPATH,
53
- :USER_INFO_FIELDS_MIN
58
+ :USER_INFO_FIELDS_MIN,
59
+ :PERMISSIONS_CREATED
54
60
 
55
61
  # various API scopes supported
56
62
  SCOPE_FILES_SELF = 'self'
@@ -58,13 +64,9 @@ module Aspera
58
64
  SCOPE_FILES_ADMIN = 'admin:all'
59
65
  SCOPE_FILES_ADMIN_USER = 'admin-user:all'
60
66
  SCOPE_FILES_ADMIN_USER_USER = "#{SCOPE_FILES_ADMIN_USER}+#{SCOPE_FILES_USER}"
61
- SCOPE_NODE_USER = 'user:all'
62
- SCOPE_NODE_ADMIN = 'admin:all'
63
67
  FILES_APP = 'files'
64
68
  PACKAGES_APP = 'packages'
65
69
  API_V1 = 'api/v1'
66
- # error message when entity not found
67
- ENTITY_NOT_FOUND = 'No such'
68
70
 
69
71
  # class static methods
70
72
  class << self
@@ -75,20 +77,6 @@ module Aspera
75
77
  return client_name, Base64.urlsafe_encode64(DataRepository.instance.data(DATA_REPO_INDEX_START + client_index))
76
78
  end
77
79
 
78
- # @param url of AoC instance
79
- # @return organization id in url and AoC domain: ibmaspera.com, asperafiles.com or qa.asperafiles.com, etc...
80
- def parse_url(aoc_org_url)
81
- uri = URI.parse(aoc_org_url.gsub(%r{/+$}, ''))
82
- instance_fqdn = uri.host
83
- Log.log.debug{"instance_fqdn=#{instance_fqdn}"}
84
- raise "No host found in URL.Please check URL format: https://myorg.#{PROD_DOMAIN}" if instance_fqdn.nil?
85
- organization, instance_domain = instance_fqdn.split('.', 2)
86
- Log.log.debug{"instance_domain=#{instance_domain}"}
87
- Log.log.debug{"organization=#{organization}"}
88
- raise "expecting a public FQDN for #{PRODUCT_NAME}" if instance_domain.nil?
89
- return organization, instance_domain
90
- end
91
-
92
80
  # base API url depends on domain, which could be "qa.xxx"
93
81
  def api_base_url(organization: 'api', api_domain: PROD_DOMAIN)
94
82
  return "https://#{organization}.#{api_domain}"
@@ -97,118 +85,131 @@ module Aspera
97
85
  def metering_api(entitlement_id, customer_id, api_domain=PROD_DOMAIN)
98
86
  return Rest.new({
99
87
  base_url: "#{api_base_url(api_domain: api_domain)}/metering/v1",
100
- headers: {'X-Aspera-Entitlement-Authorization' => Rest.basic_creds(entitlement_id, customer_id)}
88
+ headers: {'X-Aspera-Entitlement-Authorization' => Rest.basic_token(entitlement_id, customer_id)}
101
89
  })
102
90
  end
103
91
 
104
- # node API scopes
105
- def node_scope(access_key, scope)
106
- return "node.#{access_key}:#{scope}"
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
107
98
  end
108
99
 
109
- # check option "link"
110
- # if present try to get token value (resolve redirection if short links used)
111
- # then set options url/token/auth
112
- def resolve_pub_link(a_auth, a_opt)
113
- public_link_url = a_opt[:link]
114
- return if public_link_url.nil?
115
- raise 'do not use both link and url options' unless a_opt[:url].nil?
116
- redirect_count = 0
117
- while redirect_count <= MAX_REDIRECT
118
- uri = URI.parse(public_link_url)
119
- # detect if it's an expected format
120
- if PUBLIC_LINK_PATHS.include?(uri.path)
121
- url_param_token_pair = URI.decode_www_form(uri.query).find{|e|e.first.eql?('token')}
122
- raise ArgumentError, 'link option must be URL with "token" parameter' if url_param_token_pair.nil?
123
- # ok we get it !
124
- a_opt[:url] = 'https://' + uri.host
125
- a_auth[:grant_method] = :aoc_pub_link
126
- a_auth[:aoc_pub_link] = {
127
- url: {grant_type: 'url_token'}, # URL args
128
- json: {url_token: url_param_token_pair.last} # JSON body
129
- }
130
- # password protection of link
131
- a_auth[:aoc_pub_link][:json][:password] = a_opt[:password] unless a_opt[:password].nil?
132
- return # SUCCESS
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
133
138
  end
134
- Log.log.debug{"no expected format: #{public_link_url}"}
135
- r = Net::HTTP.get_response(uri)
136
- # not a redirection
137
- raise ArgumentError, 'link option must be redirect or have token parameter' unless r.code.start_with?('3')
138
- public_link_url = r['location']
139
- raise 'no location in redirection' if public_link_url.nil?
140
- Log.log.debug{"redirect to: #{public_link_url}"}
141
- end # loop
142
- raise "exceeded max redirection: #{MAX_REDIRECT}"
139
+ end
140
+ parts = url_parts(URI.parse(url))
141
+ return {
142
+ instance_domain: parts[1],
143
+ organization: parts[0]
144
+ }
143
145
  end
144
146
  end # static methods
145
147
 
146
- # CLI options that are also options to initialize
147
- OPTIONS_NEW = %i[link url auth client_id client_secret scope redirect_uri private_key passphrase username password].freeze
148
-
149
- # @param any of OPTIONS_NEW + subpath
150
- def initialize(opt)
151
- raise ArgumentError, 'Missing mandatory option: scope' if opt[:scope].nil?
148
+ attr_reader :private_link
152
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?
153
157
  # access key secrets are provided out of band to get node api access
154
158
  # key: access key
155
159
  # value: associated secret
156
- @secret_finder = nil
160
+ @secret_finder = secret_finder
161
+ @workspace_name = workspace
157
162
  @cache_user_info = nil
158
163
  @cache_url_token_info = nil
159
-
160
164
  # init rest params
161
165
  aoc_rest_p = {auth: {type: :oauth2}}
162
166
  # shortcut to auth section
163
167
  aoc_auth_p = aoc_rest_p[:auth]
164
-
165
- # sets opt[:url], aoc_rest_p[:auth][:grant_method], [:auth][:aoc_pub_link] if there is a link
166
- self.class.resolve_pub_link(aoc_auth_p, opt)
167
-
168
- # test here because link may set url
169
- raise ArgumentError, 'Missing mandatory option: url' if opt[:url].nil?
170
-
171
- # get org name and domain from url
172
- organization, instance_domain = self.class.parse_url(opt[:url])
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
173
178
  # this is the base API url
174
- api_url_base = self.class.api_base_url(api_domain: instance_domain)
179
+ api_url_base = self.class.api_base_url(api_domain: url_info[:instance_domain])
175
180
  # API URL, including subpath (version ...)
176
- aoc_rest_p[:base_url] = "#{api_url_base}/#{opt[:subpath]}"
177
- # base auth URL
178
- aoc_auth_p[:base_url] = "#{api_url_base}/#{OAUTH_API_SUBPATH}/#{organization}"
179
- aoc_auth_p[:client_id] = opt[:client_id]
180
- aoc_auth_p[:client_secret] = opt[:client_secret]
181
- aoc_auth_p[:scope] = opt[:scope]
182
-
183
- # filled if pub link
184
- if !aoc_auth_p.key?(:grant_method)
185
- raise ArgumentError, 'Missing mandatory option: auth' if opt[:auth].nil?
186
- aoc_auth_p[:grant_method] = opt[:auth]
187
- end
188
-
189
- if aoc_auth_p[:client_id].nil?
190
- aoc_auth_p[:client_id], aoc_auth_p[:client_secret] = self.class.get_client_info
191
- end
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
192
187
 
193
188
  # fill other auth parameters based on Oauth method
194
189
  case aoc_auth_p[:grant_method]
195
190
  when :web
196
- raise ArgumentError, 'Missing mandatory option: redirect_uri' if opt[:redirect_uri].nil?
197
- aoc_auth_p[:web] = {redirect_uri: opt[:redirect_uri]}
191
+ raise ArgumentError, 'Missing mandatory option: redirect_uri' if redirect_uri.nil?
192
+ aoc_auth_p[:web] = {redirect_uri: redirect_uri}
198
193
  when :jwt
199
- raise ArgumentError, 'Missing mandatory option: private_key' if opt[:private_key].nil?
200
- raise ArgumentError, 'Missing mandatory option: username' if opt[:username].nil?
194
+ raise ArgumentError, 'Missing mandatory option: private_key' if private_key.nil?
195
+ raise ArgumentError, 'Missing mandatory option: username' if username.nil?
201
196
  aoc_auth_p[:jwt] = {
202
- private_key_obj: OpenSSL::PKey::RSA.new(opt[:private_key], opt[:passphrase]),
197
+ private_key_obj: OpenSSL::PKey::RSA.new(private_key, passphrase),
203
198
  payload: {
204
- iss: aoc_auth_p[:client_id], # issuer
205
- sub: opt[:username], # subject
199
+ iss: aoc_auth_p[:client_id], # issuer
200
+ sub: username, # subject
206
201
  aud: JWT_AUDIENCE
207
202
  }
208
203
  }
209
204
  # add jwt payload for global ids
210
- aoc_auth_p[:jwt][:payload][:org] = organization if GLOBAL_CLIENT_APPS.include?(aoc_auth_p[:client_id])
205
+ aoc_auth_p[:jwt][:payload][:org] = url_info[:organization] if GLOBAL_CLIENT_APPS.include?(aoc_auth_p[:client_id])
211
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?
212
213
  # basic auth required for /token
213
214
  aoc_auth_p[:auth] = {type: :basic, username: aoc_auth_p[:client_id], password: aoc_auth_p[:client_secret]}
214
215
  else raise "ERROR: unsupported auth method: #{aoc_auth_p[:grant_method]}"
@@ -216,7 +217,7 @@ module Aspera
216
217
  super(aoc_rest_p)
217
218
  end
218
219
 
219
- def url_token_data
220
+ def public_link
220
221
  return nil unless params[:auth][:grant_method].eql?(:aoc_pub_link)
221
222
  return @cache_url_token_info unless @cache_url_token_info.nil?
222
223
  # TODO: can there be several in list ?
@@ -224,41 +225,104 @@ module Aspera
224
225
  return @cache_url_token_info
225
226
  end
226
227
 
227
- def additional_persistence_ids
228
- return [current_user_info['id']] if url_token_data.nil?
229
- return [] # TODO : url_token_data['id'] ?
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
230
  end
231
231
 
232
- def secret_finder=(secret_finder)
233
- raise 'secret finder already set' unless @secret_finder.nil?
234
- raise 'secret finder must have lookup_secret' unless secret_finder.respond_to?(:lookup_secret)
235
- @secret_finder = secret_finder
232
+ def additional_persistence_ids
233
+ return [current_user_info['id']] if public_link.nil?
234
+ return [] # TODO : public_link['id'] ?
236
235
  end
237
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
+
238
243
  # cached user information
239
244
  def current_user_info(exception: false)
240
- if @cache_user_info.nil?
241
- # get our user's default information
242
- @cache_user_info =
243
- begin
244
- read('self')[:data]
245
- rescue StandardError => e
246
- raise e if exception
247
- Log.log.debug{"ignoring error: #{e}"}
248
- {}
249
- end
250
- USER_INFO_FIELDS_MIN.each{|f|@cache_user_info[f] = 'unknown' if @cache_user_info[f].nil?}
251
- end
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?}
252
256
  return @cache_user_info
253
257
  end
254
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
+
255
319
  # @param node_id [String] identifier of node in AoC
256
320
  # @param workspace_id [String] workspace identifier
257
321
  # @param workspace_name [String] workspace name
258
- # @param scope e.g. SCOPE_NODE_USER, or nil (requires secret)
322
+ # @param scope e.g. Aspera::Node::SCOPE_USER, or nil (requires secret)
259
323
  # @param package_info [Hash] created package information
260
324
  # @returns [Aspera::Node] a node API for access key
261
- def node_api_from(node_id:, workspace_id: nil, workspace_name: nil, scope: SCOPE_NODE_USER, package_info: nil)
325
+ def node_api_from(node_id:, workspace_id: nil, workspace_name: nil, scope: Aspera::Node::SCOPE_USER, package_info: nil)
262
326
  raise 'invalid type for node_id' unless node_id.is_a?(String)
263
327
  node_info = read("nodes/#{node_id}")[:data]
264
328
  if workspace_name.nil? && !workspace_id.nil?
@@ -287,35 +351,13 @@ module Aspera
287
351
  else
288
352
  # OAuth bearer token
289
353
  node_rest_params[:auth] = params[:auth].clone
290
- node_rest_params[:auth][:scope] = self.class.node_scope(node_info['access_key'], scope)
354
+ node_rest_params[:auth][:scope] = Aspera::Node.token_scope(node_info['access_key'], scope)
291
355
  # special header required for bearer token only
292
356
  node_rest_params[:headers] = {Aspera::Node::HEADER_X_ASPERA_ACCESS_KEY => node_info['access_key']}
293
357
  end
294
358
  return Node.new(params: node_rest_params, app_info: app_info)
295
359
  end
296
360
 
297
- # Query entity type by name and returns the id if a single entry only
298
- # @param entity_type path of entity in API
299
- # @param entity_name name of searched entity
300
- # @param options additional search options
301
- def lookup_entity_by_name(entity_type, entity_name, options={})
302
- # returns entities whose name contains value (case insensitive)
303
- matching_items = read(entity_type, options.merge({'q' => CGI.escape(entity_name)}))[:data]
304
- case matching_items.length
305
- when 1 then return matching_items.first
306
- when 0 then raise %Q{#{ENTITY_NOT_FOUND} #{entity_type}: "#{entity_name}"}
307
- else
308
- # multiple case insensitive partial matches, try case insensitive full match
309
- # (anyway AoC does not allow creation of 2 entities with same case insensitive name)
310
- name_matches = matching_items.select{|i|i['name'].casecmp?(entity_name)}
311
- case name_matches.length
312
- when 1 then return name_matches.first
313
- when 0 then raise %Q(#{entity_type}: multiple case insensitive partial match for: "#{entity_name}": #{matching_items.map{|i|i['name']}} but no case insensitive full match. Please be more specific or give exact name.) # rubocop:disable Layout/LineLength
314
- else raise "Two entities cannot have the same case insensitive name: #{name_matches.map{|i|i['name']}}"
315
- end
316
- end
317
- end
318
-
319
361
  # Check metadata: remove when validation is done server side
320
362
  def validate_metadata(pkg_data)
321
363
  # validate only for shared inboxes
@@ -331,7 +373,7 @@ module Aspera
331
373
  pkg_meta = pkg_data['metadata']
332
374
  raise "package requires metadata: #{meta_schema}" unless pkg_data.key?('metadata')
333
375
  raise 'metadata must be an Array' unless pkg_meta.is_a?(Array)
334
- Log.dump(:metadata, pkg_meta)
376
+ Log.log.debug{Log.dump(:metadata, pkg_meta)}
335
377
  pkg_meta.each do |field|
336
378
  raise 'metadata field must be Hash' unless field.is_a?(Hash)
337
379
  raise 'metadata field must have name' unless field.key?('name')
@@ -367,7 +409,7 @@ module Aspera
367
409
  # email: user, else dropbox
368
410
  entity_type = short_recipient_info.include?('@') ? 'contacts' : 'dropboxes'
369
411
  begin
370
- full_recipient_info = lookup_entity_by_name(entity_type, short_recipient_info, {'current_workspace_id' => ws_id})
412
+ full_recipient_info = lookup_by_name(entity_type, short_recipient_info, {'current_workspace_id' => ws_id})
371
413
  rescue RuntimeError => e
372
414
  raise e unless e.message.start_with?(ENTITY_NOT_FOUND)
373
415
  # dropboxes cannot be created on the fly
@@ -375,7 +417,8 @@ module Aspera
375
417
  # unknown user: create it as external user
376
418
  full_recipient_info = create('contacts', {
377
419
  'current_workspace_id' => ws_id,
378
- 'email' => short_recipient_info}.merge(new_user_option))[:data]
420
+ 'email' => short_recipient_info
421
+ }.merge(new_user_option))[:data]
379
422
  end
380
423
  short_recipient_info = if entity_type.eql?('dropboxes')
381
424
  {'id' => full_recipient_info['id'], 'type' => 'dropbox'}
@@ -448,7 +491,7 @@ module Aspera
448
491
  }
449
492
  end
450
493
 
451
- # Add transferspec
494
+ # Add transfer spec
452
495
  # callback in Aspera::Node (transfer_spec_gen4)
453
496
  def add_ts_tags(transfer_spec:, app_info:)
454
497
  # translate transfer direction to upload/download
@@ -488,7 +531,10 @@ module Aspera
488
531
  'package_id' => app_info[:package_id],
489
532
  'package_name' => app_info[:package_name],
490
533
  'package_operation' => transfer_type
491
- }}}})
534
+ }
535
+ }
536
+ }
537
+ })
492
538
  end
493
539
  transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['files']['node_id'] = app_info[:node_info]['id']
494
540
  transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['app'] = app_info[:app]
@@ -496,7 +542,10 @@ module Aspera
496
542
 
497
543
  ID_AK_ADMIN = 'ASPERA_ACCESS_KEY_ADMIN'
498
544
  # Callback from Plugins::Node
499
- def permissions_create_params(create_param:, app_info:)
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:)
500
549
  # workspace shared folder:
501
550
  # access_id = "#{ID_AK_ADMIN}_WS_#{app_info[:workspace_id]}"
502
551
  default_params = {
@@ -514,10 +563,15 @@ module Aspera
514
563
  'shared_by_email' => current_user_info['email'],
515
564
  # 'shared_with_name' => access_id,
516
565
  'access_key' => app_info[:node_info]['access_key'],
517
- 'node' => app_info[:node_info]['name']}}}}}
566
+ 'node' => app_info[:node_info]['name']
567
+ }
568
+ }
569
+ }
570
+ }
571
+ }
518
572
  create_param.deep_merge!(default_params)
519
573
  if create_param.key?('with')
520
- contact_info = lookup_entity_by_name(
574
+ contact_info = lookup_by_name(
521
575
  'contacts',
522
576
  create_param['with'],
523
577
  {'current_workspace_id' => app_info[:workspace_id], 'context' => 'share_folder'})
@@ -531,14 +585,20 @@ module Aspera
531
585
  end
532
586
 
533
587
  # Callback from Plugins::Node
534
- def permissions_create_event(created_data:, app_info:)
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?
535
594
  event_creation = {
536
- 'types' => ['permission.created'],
595
+ 'types' => types,
537
596
  'node_id' => app_info[:node_info]['id'],
538
597
  'workspace_id' => app_info[:workspace_id],
539
- 'data' => created_data # Response from previous step
598
+ 'data' => created_data
540
599
  }
541
- # (optional). The name of the folder to be displayed to the destination user. Use it if its value is different from the "share_as" field.
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.
542
602
  event_creation['link_name'] = app_info[:opt_link_name] unless app_info[:opt_link_name].nil?
543
603
  create('events', event_creation)
544
604
  end