aspera-cli 4.13.0 → 4.15.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 (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