aspera-cli 4.4.0 → 4.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2095 -1503
  3. data/bin/ascli +2 -1
  4. data/bin/asession +4 -5
  5. data/docs/test_env.conf +3 -0
  6. data/examples/aoc.rb +4 -3
  7. data/examples/faspex4.rb +25 -25
  8. data/examples/proxy.pac +1 -1
  9. data/examples/transfer.rb +17 -17
  10. data/lib/aspera/aoc.rb +238 -185
  11. data/lib/aspera/ascmd.rb +93 -83
  12. data/lib/aspera/ats_api.rb +11 -10
  13. data/lib/aspera/cli/basic_auth_plugin.rb +13 -14
  14. data/lib/aspera/cli/extended_value.rb +42 -33
  15. data/lib/aspera/cli/formater.rb +142 -108
  16. data/lib/aspera/cli/info.rb +17 -0
  17. data/lib/aspera/cli/listener/line_dump.rb +3 -2
  18. data/lib/aspera/cli/listener/logger.rb +2 -1
  19. data/lib/aspera/cli/listener/progress.rb +16 -18
  20. data/lib/aspera/cli/listener/progress_multi.rb +18 -21
  21. data/lib/aspera/cli/main.rb +173 -149
  22. data/lib/aspera/cli/manager.rb +163 -168
  23. data/lib/aspera/cli/plugin.rb +43 -31
  24. data/lib/aspera/cli/plugins/alee.rb +6 -6
  25. data/lib/aspera/cli/plugins/aoc.rb +405 -370
  26. data/lib/aspera/cli/plugins/ats.rb +86 -79
  27. data/lib/aspera/cli/plugins/bss.rb +14 -16
  28. data/lib/aspera/cli/plugins/config.rb +580 -362
  29. data/lib/aspera/cli/plugins/console.rb +23 -19
  30. data/lib/aspera/cli/plugins/cos.rb +18 -18
  31. data/lib/aspera/cli/plugins/faspex.rb +201 -158
  32. data/lib/aspera/cli/plugins/faspex5.rb +80 -57
  33. data/lib/aspera/cli/plugins/node.rb +183 -166
  34. data/lib/aspera/cli/plugins/orchestrator.rb +71 -67
  35. data/lib/aspera/cli/plugins/preview.rb +92 -96
  36. data/lib/aspera/cli/plugins/server.rb +79 -75
  37. data/lib/aspera/cli/plugins/shares.rb +35 -19
  38. data/lib/aspera/cli/plugins/sync.rb +20 -22
  39. data/lib/aspera/cli/transfer_agent.rb +76 -113
  40. data/lib/aspera/cli/version.rb +2 -1
  41. data/lib/aspera/colors.rb +35 -27
  42. data/lib/aspera/command_line_builder.rb +48 -34
  43. data/lib/aspera/cos_node.rb +29 -21
  44. data/lib/aspera/data_repository.rb +3 -2
  45. data/lib/aspera/environment.rb +50 -45
  46. data/lib/aspera/fasp/{manager.rb → agent_base.rb} +28 -25
  47. data/lib/aspera/fasp/{connect.rb → agent_connect.rb} +52 -43
  48. data/lib/aspera/fasp/{local.rb → agent_direct.rb} +58 -72
  49. data/lib/aspera/fasp/{http_gw.rb → agent_httpgw.rb} +37 -43
  50. data/lib/aspera/fasp/{node.rb → agent_node.rb} +35 -16
  51. data/lib/aspera/fasp/agent_trsdk.rb +104 -0
  52. data/lib/aspera/fasp/error.rb +2 -1
  53. data/lib/aspera/fasp/error_info.rb +68 -52
  54. data/lib/aspera/fasp/installation.rb +152 -124
  55. data/lib/aspera/fasp/listener.rb +1 -0
  56. data/lib/aspera/fasp/parameters.rb +87 -92
  57. data/lib/aspera/fasp/parameters.yaml +305 -249
  58. data/lib/aspera/fasp/resume_policy.rb +11 -14
  59. data/lib/aspera/fasp/transfer_spec.rb +26 -0
  60. data/lib/aspera/fasp/uri.rb +22 -21
  61. data/lib/aspera/faspex_gw.rb +55 -89
  62. data/lib/aspera/hash_ext.rb +4 -3
  63. data/lib/aspera/id_generator.rb +8 -7
  64. data/lib/aspera/keychain/encrypted_hash.rb +121 -0
  65. data/lib/aspera/keychain/macos_security.rb +90 -0
  66. data/lib/aspera/log.rb +55 -37
  67. data/lib/aspera/nagios.rb +13 -12
  68. data/lib/aspera/node.rb +30 -25
  69. data/lib/aspera/oauth.rb +175 -226
  70. data/lib/aspera/open_application.rb +4 -3
  71. data/lib/aspera/persistency_action_once.rb +6 -6
  72. data/lib/aspera/persistency_folder.rb +5 -9
  73. data/lib/aspera/preview/file_types.rb +6 -5
  74. data/lib/aspera/preview/generator.rb +25 -24
  75. data/lib/aspera/preview/options.rb +16 -14
  76. data/lib/aspera/preview/utils.rb +98 -98
  77. data/lib/aspera/{proxy_auto_config.erb.js → proxy_auto_config.js} +23 -31
  78. data/lib/aspera/proxy_auto_config.rb +111 -20
  79. data/lib/aspera/rest.rb +154 -135
  80. data/lib/aspera/rest_call_error.rb +2 -2
  81. data/lib/aspera/rest_error_analyzer.rb +23 -25
  82. data/lib/aspera/rest_errors_aspera.rb +15 -14
  83. data/lib/aspera/ssh.rb +12 -10
  84. data/lib/aspera/sync.rb +42 -41
  85. data/lib/aspera/temp_file_manager.rb +18 -14
  86. data/lib/aspera/timer_limiter.rb +2 -1
  87. data/lib/aspera/uri_reader.rb +7 -5
  88. data/lib/aspera/web_auth.rb +79 -76
  89. metadata +116 -29
  90. data/docs/Makefile +0 -66
  91. data/docs/README.erb.md +0 -3973
  92. data/docs/README.md +0 -13
  93. data/docs/diagrams.txt +0 -49
  94. data/docs/doc_tools.rb +0 -58
  95. data/lib/aspera/api_detector.rb +0 -60
  96. data/lib/aspera/cli/plugins/shares2.rb +0 -114
  97. data/lib/aspera/secrets.rb +0 -20
data/lib/aspera/aoc.rb CHANGED
@@ -1,38 +1,43 @@
1
+ # frozen_string_literal: true
1
2
  require 'aspera/log'
2
3
  require 'aspera/rest'
3
4
  require 'aspera/hash_ext'
4
5
  require 'aspera/data_repository'
5
- require 'aspera/node'
6
+ require 'aspera/fasp/transfer_spec'
6
7
  require 'base64'
7
8
 
9
+ Aspera::Oauth.register_token_creator(:aoc_pub_link,lambda{|o|
10
+ o.token_auth_api.call({
11
+ operation: 'POST',
12
+ subpath: o.params[:path_token],
13
+ headers: {'Accept'=>'application/json'},
14
+ json_params: o.params[:aoc_pub_link][:json],
15
+ url_params: o.params[:aoc_pub_link][:url].merge(scope: o.params[:scope]) # scope is here because it changes over time (node)
16
+ })
17
+ })
18
+
8
19
  module Aspera
9
20
  class AoC < Rest
10
- private
11
- @@use_standard_ports = true
12
-
13
- API_V1='api/v1'
14
-
15
21
  PRODUCT_NAME='Aspera on Cloud'
16
22
  # Production domain of AoC
17
23
  PROD_DOMAIN='ibmaspera.com'
18
24
  # to avoid infinite loop in pub link redirection
19
25
  MAX_REDIRECT=10
20
- CLIENT_APPS=['aspera.global-cli-client','aspera.drive']
26
+ # Well-known AoC globals client apps
27
+ GLOBAL_CLIENT_APPS=['aspera.global-cli-client','aspera.drive']
28
+ # index offset in data repository of client app
21
29
  DATA_REPO_INDEX_START = 4
22
-
30
+ # cookie prefix so that console can decode identity
31
+ COOKIE_PREFIX='aspera.aoc'
23
32
  # path in URL of public links
24
- PATHS_PUBLIC_LINK=['/packages/public/receive','/packages/public/send','/files/public']
33
+ PUBLIC_LINK_PATHS=['/packages/public/receive','/packages/public/send','/files/public']
25
34
  JWT_AUDIENCE='https://api.asperafiles.com/api/v1/oauth2/token'
26
35
  OAUTH_API_SUBPATH='api/v1/oauth2'
27
- DEFAULT_TSPEC_INFO={
28
- 'remote_user' => Node::ACCESS_KEY_TRANSFER_USER,
29
- 'ssh_port' => Node::SSH_PORT_DEFAULT,
30
- 'fasp_port' => Node::UDP_PORT_DEFAULT
31
- }
36
+ # minimum fields for user info if retrieval fails
37
+ USER_INFO_FIELDS_MIN=['name','email','id','default_workspace_id','organization_id']
32
38
 
33
- private_constant :PRODUCT_NAME,:PROD_DOMAIN,:MAX_REDIRECT,:CLIENT_APPS,:PATHS_PUBLIC_LINK,:JWT_AUDIENCE,:OAUTH_API_SUBPATH,:DEFAULT_TSPEC_INFO
39
+ private_constant :MAX_REDIRECT,:GLOBAL_CLIENT_APPS,:DATA_REPO_INDEX_START,:COOKIE_PREFIX,:PUBLIC_LINK_PATHS,:JWT_AUDIENCE,:OAUTH_API_SUBPATH,:USER_INFO_FIELDS_MIN
34
40
 
35
- public
36
41
  # various API scopes supported
37
42
  SCOPE_FILES_SELF='self'
38
43
  SCOPE_FILES_USER='user:all'
@@ -44,109 +49,142 @@ module Aspera
44
49
  PATH_SEPARATOR='/'
45
50
  FILES_APP='files'
46
51
  PACKAGES_APP='packages'
52
+ API_V1='api/v1'
47
53
 
48
- def self.get_client_info(client_name=CLIENT_APPS.first)
49
- client_index=CLIENT_APPS.index(client_name)
50
- raise "no such pre-defined client: #{client_name}" if client_index.nil?
51
- # strings /Applications/Aspera\ Drive.app/Contents/MacOS/AsperaDrive|grep -E '.{100}==$'|base64 --decode
52
- return client_name,Base64.urlsafe_encode64(DataRepository.instance.get_bin(DATA_REPO_INDEX_START+client_index))
53
- end
54
+ # class instance variable, access with accessors on class
55
+ @use_standard_ports = true
54
56
 
55
- # @param url of AoC instance
56
- # @return organization id in url and AoC domain: ibmaspera.com, asperafiles.com or qa.asperafiles.com, etc...
57
- def self.parse_url(aoc_org_url)
58
- uri=URI.parse(aoc_org_url.gsub(/\/+$/,''))
59
- instance_fqdn=uri.host
60
- Log.log.debug("instance_fqdn=#{instance_fqdn}")
61
- raise "No host found in URL.Please check URL format: https://myorg.#{PROD_DOMAIN}" if instance_fqdn.nil?
62
- organization,instance_domain=instance_fqdn.split('.',2)
63
- Log.log.debug("instance_domain=#{instance_domain}")
64
- Log.log.debug("organization=#{organization}")
65
- raise "expecting a public FQDN for #{PRODUCT_NAME}" if instance_domain.nil?
66
- return organization,instance_domain
67
- end
57
+ # class static methods
58
+ class << self
59
+ attr_accessor :use_standard_ports
60
+ # strings /Applications/Aspera\ Drive.app/Contents/MacOS/AsperaDrive|grep -E '.{100}==$'|base64 --decode
61
+ def get_client_info(client_name=GLOBAL_CLIENT_APPS.first)
62
+ client_index=GLOBAL_CLIENT_APPS.index(client_name)
63
+ raise "no such pre-defined client: #{client_name}" if client_index.nil?
64
+ return client_name,Base64.urlsafe_encode64(DataRepository.instance.data(DATA_REPO_INDEX_START+client_index))
65
+ end
68
66
 
69
- # base API url depends on domain, which could be "qa.xxx"
70
- def self.api_base_url(api_domain=PROD_DOMAIN)
71
- return "https://api.#{api_domain}"
72
- end
67
+ # @param url of AoC instance
68
+ # @return organization id in url and AoC domain: ibmaspera.com, asperafiles.com or qa.asperafiles.com, etc...
69
+ def parse_url(aoc_org_url)
70
+ uri=URI.parse(aoc_org_url.gsub(/\/+$/,''))
71
+ instance_fqdn=uri.host
72
+ Log.log.debug("instance_fqdn=#{instance_fqdn}")
73
+ raise "No host found in URL.Please check URL format: https://myorg.#{PROD_DOMAIN}" if instance_fqdn.nil?
74
+ organization,instance_domain=instance_fqdn.split('.',2)
75
+ Log.log.debug("instance_domain=#{instance_domain}")
76
+ Log.log.debug("organization=#{organization}")
77
+ raise "expecting a public FQDN for #{PRODUCT_NAME}" if instance_domain.nil?
78
+ return organization,instance_domain
79
+ end
73
80
 
74
- def self.metering_api(entitlement_id,customer_id,api_domain=PROD_DOMAIN)
75
- return Rest.new({
76
- :base_url => "#{api_base_url(api_domain)}/metering/v1",
77
- :headers => {'X-Aspera-Entitlement-Authorization' => Rest.basic_creds(entitlement_id,customer_id)}
78
- })
79
- end
81
+ # base API url depends on domain, which could be "qa.xxx"
82
+ def api_base_url(api_domain=PROD_DOMAIN)
83
+ return "https://api.#{api_domain}"
84
+ end
80
85
 
81
- # node API scopes
82
- def self.node_scope(access_key,scope)
83
- return 'node.'+access_key+':'+scope
84
- end
86
+ def metering_api(entitlement_id,customer_id,api_domain=PROD_DOMAIN)
87
+ return Rest.new({
88
+ base_url: "#{api_base_url(api_domain)}/metering/v1",
89
+ headers: {'X-Aspera-Entitlement-Authorization' => Rest.basic_creds(entitlement_id,customer_id)}
90
+ })
91
+ end
85
92
 
86
- def self.set_use_default_ports(val)
87
- @@use_standard_ports=val
88
- end
93
+ # node API scopes
94
+ def node_scope(access_key,scope)
95
+ return "node.#{access_key}:#{scope}"
96
+ end
89
97
 
90
- # check option "link"
91
- # if present try to get token value (resolve redirection if short links used)
92
- # then set options url/token/auth
93
- def self.resolve_pub_link(rest_opts,public_link_url)
94
- return if public_link_url.nil?
95
- # set to token if available after redirection
96
- url_param_token_pair=nil
97
- redirect_count=0
98
- loop do
99
- uri=URI.parse(public_link_url)
100
- if PATHS_PUBLIC_LINK.include?(uri.path)
101
- url_param_token_pair=URI::decode_www_form(uri.query).select{|e|e.first.eql?('token')}.first
102
- if url_param_token_pair.nil?
103
- raise ArgumentError,"link option must be URL with 'token' parameter"
98
+ # check option "link"
99
+ # if present try to get token value (resolve redirection if short links used)
100
+ # then set options url/token/auth
101
+ def resolve_pub_link(a_auth,a_opt)
102
+ public_link_url=a_opt[:link]
103
+ return if public_link_url.nil?
104
+ raise 'do not use both link and url options' unless a_opt[:url].nil?
105
+ redirect_count=0
106
+ while redirect_count <= MAX_REDIRECT
107
+ uri=URI.parse(public_link_url)
108
+ # detect if it's an expected format
109
+ if PUBLIC_LINK_PATHS.include?(uri.path)
110
+ url_param_token_pair=URI.decode_www_form(uri.query).select{|e|e.first.eql?('token')}.first
111
+ raise ArgumentError,'link option must be URL with "token" parameter' if url_param_token_pair.nil?
112
+ # ok we get it !
113
+ a_opt[:url]='https://'+uri.host
114
+ a_auth[:crtype]=:aoc_pub_link
115
+ a_auth[:aoc_pub_link]={
116
+ url: {grant_type: 'url_token'}, # URL args
117
+ json: {url_token: url_param_token_pair.last} # JSON body
118
+ }
119
+ # password protection of link
120
+ a_auth[:aoc_pub_link][:json][:password]=a_opt[:password] unless a_opt[:password].nil?
121
+ return # SUCCESS
104
122
  end
105
- # ok we get it !
106
- rest_opts[:org_url]='https://'+uri.host
107
- rest_opts[:auth][:grant]=:url_token
108
- rest_opts[:auth][:url_token]=url_param_token_pair.last
109
- return
110
- end
111
- Log.log.debug("no expected format: #{public_link_url}")
112
- raise "exceeded max redirection: #{MAX_REDIRECT}" if redirect_count > MAX_REDIRECT
113
- r = Net::HTTP.get_response(uri)
114
- if r.code.start_with?("3")
123
+ Log.log.debug("no expected format: #{public_link_url}")
124
+ r = Net::HTTP.get_response(uri)
125
+ # not a redirection
126
+ raise ArgumentError,'link option must be redirect or have token parameter' unless r.code.start_with?('3')
115
127
  public_link_url = r['location']
116
- raise "no location in redirection" if public_link_url.nil?
128
+ raise 'no location in redirection' if public_link_url.nil?
117
129
  Log.log.debug("redirect to: #{public_link_url}")
118
- else
119
- # not a redirection
120
- raise ArgumentError,'link option must be redirect or have token parameter'
130
+ end # loop
131
+ raise "exceeded max redirection: #{MAX_REDIRECT}"
132
+ end
133
+
134
+ # additional transfer spec (tags) for package information
135
+ def package_tags(package_info,operation)
136
+ return {'tags'=>{'aspera'=>{'files'=>{
137
+ 'package_id' => package_info['id'],
138
+ 'package_name' => package_info['name'],
139
+ 'package_operation' => operation
140
+ }}}}
141
+ end
142
+
143
+ # add details to show in analytics
144
+ def analytics_ts(app,direction,ws_id,ws_name)
145
+ # translate transfer to operation
146
+ operation=
147
+ case direction
148
+ when Fasp::TransferSpec::DIRECTION_SEND then 'upload'
149
+ when Fasp::TransferSpec::DIRECTION_RECEIVE then 'download'
150
+ else raise "ERROR: unexpected value: #{direction}"
121
151
  end
122
- end # loop
123
152
 
124
- raise RuntimeError,'too many redirections'
125
- end
153
+ return {
154
+ 'tags' => {
155
+ 'aspera' => {
156
+ 'usage_id' => "aspera.files.workspace.#{ws_id}", # activity tracking
157
+ 'files' => {
158
+ 'files_transfer_action' => "#{operation}_#{app.gsub(/s$/,'')}",
159
+ 'workspace_name' => ws_name, # activity tracking
160
+ 'workspace_id' => ws_id
161
+ }
162
+ }
163
+ }
164
+ }
165
+ end
166
+ end # static methods
126
167
 
127
168
  # @param :link,:url,:auth,:client_id,:client_secret,:scope,:redirect_uri,:private_key,:username,:subpath,:password (for pub link)
128
169
  def initialize(opt)
170
+ raise ArgumentError,'Missing mandatory option: scope' if opt[:scope].nil?
171
+
129
172
  # access key secrets are provided out of band to get node api access
130
173
  # key: access key
131
174
  # value: associated secret
132
175
  @key_chain=nil
176
+ @user_info=nil
133
177
 
134
178
  # init rest params
135
- aoc_rest_p={:auth=>{:type =>:oauth2}}
179
+ aoc_rest_p={auth: {type: :oauth2}}
136
180
  # shortcut to auth section
137
181
  aoc_auth_p=aoc_rest_p[:auth]
138
182
 
139
- # sets [:org_url], [:auth][:grant], [:auth][:url_token]
140
- self.class.resolve_pub_link(aoc_rest_p,opt[:link])
183
+ # sets opt[:url], aoc_rest_p[:auth][:crtype], [:auth][:aoc_pub_link] if there is a link
184
+ self.class.resolve_pub_link(aoc_auth_p,opt)
141
185
 
142
- if aoc_rest_p.has_key?(:org_url)
143
- # Pub Link only: get org url from pub link
144
- opt[:url] = aoc_rest_p[:org_url]
145
- aoc_rest_p.delete(:org_url)
146
- else
147
- # else url is mandatory
148
- raise ArgumentError,"Missing mandatory option: url" if opt[:url].nil?
149
- end
186
+ # test here because link may set url
187
+ raise ArgumentError,'Missing mandatory option: url' if opt[:url].nil?
150
188
 
151
189
  # get org name and domain from url
152
190
  organization,instance_domain=self.class.parse_url(opt[:url])
@@ -158,107 +196,98 @@ module Aspera
158
196
  aoc_auth_p[:base_url] = "#{api_url_base}/#{OAUTH_API_SUBPATH}/#{organization}"
159
197
  aoc_auth_p[:client_id]=opt[:client_id]
160
198
  aoc_auth_p[:client_secret] = opt[:client_secret]
199
+ aoc_auth_p[:scope] = opt[:scope]
161
200
 
162
- if !aoc_auth_p.has_key?(:grant)
163
- raise ArgumentError,"Missing mandatory option: auth" if opt[:auth].nil?
164
- aoc_auth_p[:grant] = opt[:auth]
201
+ # filled if pub link
202
+ if !aoc_auth_p.has_key?(:crtype)
203
+ raise ArgumentError,'Missing mandatory option: auth' if opt[:auth].nil?
204
+ aoc_auth_p[:crtype] = opt[:auth]
165
205
  end
166
206
 
167
207
  if aoc_auth_p[:client_id].nil?
168
208
  aoc_auth_p[:client_id],aoc_auth_p[:client_secret] = self.class.get_client_info()
169
209
  end
170
210
 
171
- raise ArgumentError,"Missing mandatory option: scope" if opt[:scope].nil?
172
- aoc_auth_p[:scope] = opt[:scope]
173
-
174
211
  # fill other auth parameters based on Oauth method
175
- case aoc_auth_p[:grant]
212
+ case aoc_auth_p[:crtype]
176
213
  when :web
177
- raise ArgumentError,"Missing mandatory option: redirect_uri" if opt[:redirect_uri].nil?
178
- aoc_auth_p[:redirect_uri] = opt[:redirect_uri]
214
+ raise ArgumentError,'Missing mandatory option: redirect_uri' if opt[:redirect_uri].nil?
215
+ aoc_auth_p[:web]={redirect_uri: opt[:redirect_uri]}
179
216
  when :jwt
217
+ raise ArgumentError,'Missing mandatory option: private_key' if opt[:private_key].nil?
218
+ raise ArgumentError,'Missing mandatory option: username' if opt[:username].nil?
219
+ aoc_auth_p[:jwt]={
220
+ private_key_obj: OpenSSL::PKey::RSA.new(opt[:private_key]),
221
+ payload: {
222
+ iss: aoc_auth_p[:client_id], # issuer
223
+ sub: opt[:username], # subject
224
+ aud: JWT_AUDIENCE
225
+ }
226
+ }
180
227
  # add jwt payload for global ids
181
- if CLIENT_APPS.include?(aoc_auth_p[:client_id])
182
- aoc_auth_p.merge!({:jwt_add=>{org: organization}})
183
- end
184
- raise ArgumentError,"Missing mandatory option: private_key" if opt[:private_key].nil?
185
- raise ArgumentError,"Missing mandatory option: username" if opt[:username].nil?
186
- private_key_PEM_string=opt[:private_key]
187
- aoc_auth_p[:jwt_audience] = JWT_AUDIENCE
188
- aoc_auth_p[:jwt_subject] = opt[:username]
189
- aoc_auth_p[:jwt_private_key_obj] = OpenSSL::PKey::RSA.new(private_key_PEM_string)
190
- when :url_token
191
- aoc_auth_p[:password]=opt[:password] unless opt[:password].nil?
192
- # nothing more
193
- else raise "ERROR: unsupported auth method: #{aoc_auth_p[:grant]}"
228
+ aoc_auth_p[:jwt][:payload][:org]=organization if GLOBAL_CLIENT_APPS.include?(aoc_auth_p[:client_id])
229
+ when :aoc_pub_link
230
+ # basic auth required for /token
231
+ aoc_auth_p[:auth]={type: :basic, username: aoc_auth_p[:client_id],password: aoc_auth_p[:client_secret]}
232
+ else raise "ERROR: unsupported auth method: #{aoc_auth_p[:crtype]}"
194
233
  end
195
234
  super(aoc_rest_p)
196
235
  end
197
236
 
198
- def key_chain=(keychain)
199
- raise "keychain already set" unless @key_chain.nil?
200
- @key_chain=keychain
201
- nil
237
+ def url_token_data
238
+ return nil unless params[:auth][:crtype].eql?(:aoc_pub_link)
239
+ # TODO: can there be several in list ?
240
+ return read('url_tokens')[:data].first
202
241
  end
203
242
 
204
- # additional transfer spec (tags) for package information
205
- def self.package_tags(package_info,operation)
206
- return {'tags'=>{'aspera'=>{'files'=>{
207
- 'package_id' => package_info['id'],
208
- 'package_name' => package_info['name'],
209
- 'package_operation' => operation
210
- }}}}
243
+ def key_chain=(keychain)
244
+ raise 'keychain already set' unless @key_chain.nil?
245
+ raise 'keychain must have get_secret' unless keychain.respond_to?(:get_secret)
246
+ @key_chain=keychain
211
247
  end
212
248
 
213
- # add details to show in analytics
214
- def self.analytics_ts(app,direction,ws_id,ws_name)
215
- # translate transfer to operation
216
- operation=case direction
217
- when 'send'; 'upload'
218
- when 'receive'; 'download'
219
- else raise "ERROR: unexpected value: #{direction}"
249
+ # cached user information
250
+ def user_info
251
+ if @user_info.nil?
252
+ # get our user's default information
253
+ @user_info=
254
+ begin
255
+ read('self')[:data]
256
+ rescue StandardError => e
257
+ Log.log.debug("ignoring error: #{e}")
258
+ {}
259
+ end
260
+ USER_INFO_FIELDS_MIN.each{|f|@user_info[f]='unknown' if @user_info[f].nil?}
220
261
  end
221
-
222
- return {
223
- 'tags' => {
224
- 'aspera' => {
225
- 'usage_id' => "aspera.files.workspace.#{ws_id}", # activity tracking
226
- 'files' => {
227
- 'files_transfer_action' => "#{operation}_#{app.gsub(/s$/,'')}",
228
- 'workspace_name' => ws_name, # activity tracking
229
- 'workspace_id' => ws_id,
230
- }
231
- }
232
- }
233
- }
262
+ return @user_info
234
263
  end
235
264
 
236
265
  # build ts addon for IBM Aspera Console (cookie)
237
- def self.console_ts(app,user_name,user_email)
238
- elements=[app,user_name,user_email].map{|e|Base64.strict_encode64(e)}
239
- elements.unshift('aspera.aoc')
240
- #Log.dump('elem1'.bg_red,elements[1])
241
- return {
242
- 'cookie'=>elements.join(':')
243
- }
266
+ def console_ts(app)
267
+ # we are sure that fields are not nil
268
+ elements=[app,user_info['name'],user_info['email']].map{|e|Base64.strict_encode64(e)}
269
+ elements.unshift(COOKIE_PREFIX)
270
+ return {'cookie'=>elements.join(':')}
244
271
  end
245
272
 
246
273
  # build "transfer info", 2 elements array with:
247
274
  # - transfer spec for aspera on cloud, based on node information and file id
248
275
  # - source and token regeneration method
249
276
  def tr_spec(app,direction,node_file,ts_add)
250
- # prepare the rest end point is used to generate the bearer token
251
- token_generation_method=lambda {|do_refresh|self.oauth_token(scope: self.class.node_scope(node_file[:node_info]['access_key'],SCOPE_NODE_USER), refresh: do_refresh)}
277
+ # get node api
278
+ node_api=get_node_api(node_file[:node_info])
279
+ # this lambda returns the bearer token for node, if
280
+ token_generation_lambda=lambda{|do_refresh|node_api.oauth_token(force_refresh: do_refresh)}
252
281
  # prepare transfer specification
253
282
  # note xfer_id and xfer_retry are set by the transfer agent itself
254
283
  transfer_spec={
255
284
  'direction' => direction,
256
- 'token' => token_generation_method.call(false), # first time, use cache
285
+ 'token' => token_generation_lambda.call(false), # first time, use cache
257
286
  'tags' => {
258
287
  'aspera' => {
259
288
  'app' => app,
260
289
  'files' => {
261
- 'node_id' => node_file[:node_info]['id'],
290
+ 'node_id' => node_file[:node_info]['id']
262
291
  }, # files
263
292
  'node' => {
264
293
  'access_key' => node_file[:node_info]['access_key'],
@@ -269,36 +298,40 @@ module Aspera
269
298
  } # tags
270
299
  }
271
300
  # add remote host info
272
- if @@use_standard_ports
273
- transfer_spec.merge!(DEFAULT_TSPEC_INFO)
301
+ if self.class.use_standard_ports
302
+ # get default TCP/UDP ports and transfer user
303
+ transfer_spec.merge!(Fasp::TransferSpec::AK_TSPEC_BASE)
304
+ # by default: same address as node API
274
305
  transfer_spec['remote_host']=node_file[:node_info]['host']
306
+ # 30 it's necessarily https scheme: webui does not allow anything else
307
+ if node_file[:node_info]['transfer_url'].is_a?(String) && !node_file[:node_info]['transfer_url'].empty?
308
+ transfer_spec['remote_host']=URI.parse(node_file[:node_info]['transfer_url']).host
309
+ end
275
310
  else
276
311
  # retrieve values from API
277
- std_t_spec=get_node_api(node_file[:node_info],scope: SCOPE_NODE_USER).create('files/download_setup',{:transfer_requests => [ { :transfer_request => {:paths => [ {"source"=>'/'} ] } } ] } )[:data]['transfer_specs'].first['transfer_spec']
312
+ std_t_spec=node_api.create('files/download_setup',{transfer_requests: [{ transfer_request: {paths: [{'source'=>'/'}] } }] })[:data]['transfer_specs'].first['transfer_spec']
278
313
  ['remote_host','remote_user','ssh_port','fasp_port'].each {|i| transfer_spec[i]=std_t_spec[i]}
279
314
  end
280
315
  # add caller provided transfer spec
281
316
  transfer_spec.deep_merge!(ts_add)
282
317
  # additional information for transfer agent
283
318
  source_and_token_generator={
284
- :src => :node_gen4,
285
- :regenerate_token => token_generation_method
319
+ src: :node_gen4,
320
+ regenerate_token: token_generation_lambda
286
321
  }
287
322
  return transfer_spec,source_and_token_generator
288
323
  end
289
324
 
290
325
  # returns a node API for access key
326
+ # @param node_info [Hash] with 'url' and 'access_key'
291
327
  # @param scope e.g. SCOPE_NODE_USER
292
328
  # no scope: requires secret
293
329
  # if secret provided beforehand: use it
294
- def get_node_api(node_info,options={})
295
- raise "INTERNAL ERROR: method parameters: options must ne hash" unless options.is_a?(Hash)
296
- options.keys.each {|k| raise "INTERNAL ERROR: not valid option: #{k}" unless [:scope,:use_secret].include?(k)}
297
- # get optional secret unless :use_secret is false (default is true)
298
- ak_secret=@key_chain.get_secret(node_info['access_key'],false) if !options.has_key?(:use_secret) or options[:use_secret]
299
- if ak_secret.nil? and !options.has_key?(:scope)
300
- raise "There must be at least one of: 'secret' or 'scope' for access key #{node_info['access_key']}"
301
- end
330
+ def get_node_api(node_info, scope: SCOPE_NODE_USER, use_secret: true)
331
+ raise 'internal error' unless node_info.is_a?(Hash) && node_info.has_key?('url') && node_info.has_key?('access_key')
332
+ # get optional secret unless :use_secret is false
333
+ ak_secret=@key_chain.get_secret(url: node_info['url'], username: node_info['access_key'], mandatory: false) if use_secret && !@key_chain.nil?
334
+ raise "There must be at least one of: 'secret' or 'scope' for access key #{node_info['access_key']}" if ak_secret.nil? && scope.nil?
302
335
  node_rest_params={base_url: node_info['url']}
303
336
  # if secret is available
304
337
  if !ak_secret.nil?
@@ -310,8 +343,8 @@ module Aspera
310
343
  else
311
344
  # X-Aspera-AccessKey required for bearer token only
312
345
  node_rest_params[:headers]= {'X-Aspera-AccessKey'=>node_info['access_key']}
313
- node_rest_params[:auth]=self.params[:auth].clone
314
- node_rest_params[:auth][:scope]=self.class.node_scope(node_info['access_key'],options[:scope])
346
+ node_rest_params[:auth]=params[:auth].clone
347
+ node_rest_params[:auth][:scope]=self.class.node_scope(node_info['access_key'],scope)
315
348
  end
316
349
  return Node.new(node_rest_params)
317
350
  end
@@ -320,7 +353,7 @@ module Aspera
320
353
  # @return split values
321
354
  def check_get_node_file(node_file)
322
355
  raise "node_file must be Hash (got #{node_file.class})" unless node_file.is_a?(Hash)
323
- raise "node_file must have 2 keys: :file_id and :node_info" unless node_file.keys.sort.eql?([:file_id,:node_info])
356
+ raise 'node_file must have 2 keys: :file_id and :node_info' unless node_file.keys.sort.eql?([:file_id,:node_info])
324
357
  node_info=node_file[:node_info]
325
358
  file_id=node_file[:file_id]
326
359
  raise "node_info must be Hash (got #{node_info.class}: #{node_info})" unless node_info.is_a?(Hash)
@@ -336,28 +369,28 @@ module Aspera
336
369
  @find_state[:found].push(entry.merge({'path'=>path})) if @find_state[:test_block].call(entry)
337
370
  # process link
338
371
  if entry[:type].eql?('link')
339
- sub_node_info=self.read("nodes/#{entry['target_node_id']}")[:data]
372
+ sub_node_info=read("nodes/#{entry['target_node_id']}")[:data]
340
373
  sub_opt={method: process_find_files, top_file_id: entry['target_id'], top_file_path: path}
341
- get_node_api(sub_node_info,scope: SCOPE_NODE_USER).crawl(self,sub_opt)
374
+ get_node_api(sub_node_info).crawl(self,sub_opt)
342
375
  end
343
- rescue => e
376
+ rescue StandardError => e
344
377
  Log.log.error("#{path}: #{e.message}")
345
378
  end
346
379
  # process all folders
347
380
  return true
348
381
  end
349
382
 
350
- def find_files( top_node_file, test_block )
383
+ def find_files(top_node_file, test_block)
351
384
  top_node_info,top_file_id=check_get_node_file(top_node_file)
352
385
  Log.log.debug("find_files: node_info=#{top_node_info}, fileid=#{top_file_id}")
353
386
  @find_state={found: [], test_block: test_block}
354
- get_node_api(top_node_info,scope: SCOPE_NODE_USER).crawl(self,{method: :process_find_files, top_file_id: top_file_id})
387
+ get_node_api(top_node_info).crawl(self,{method: :process_find_files, top_file_id: top_file_id})
355
388
  result=@find_state[:found]
356
389
  @find_state=nil
357
390
  return result
358
391
  end
359
392
 
360
- def process_resolve_node_file(entry,path)
393
+ def process_resolve_node_file(entry,_path)
361
394
  # stop digging here if not in right path
362
395
  return false unless entry['name'].eql?(@resolve_state[:path].first)
363
396
  # ok it matches, so we remove the match
@@ -368,11 +401,11 @@ module Aspera
368
401
  raise "#{entry['name']} is a file, expecting folder to find: #{@resolve_state[:path]}" unless @resolve_state[:path].empty?
369
402
  @resolve_state[:result][:file_id]=entry['id']
370
403
  when 'link'
371
- @resolve_state[:result][:node_info]=self.read("nodes/#{entry['target_node_id']}")[:data]
404
+ @resolve_state[:result][:node_info]=read("nodes/#{entry['target_node_id']}")[:data]
372
405
  if @resolve_state[:path].empty?
373
406
  @resolve_state[:result][:file_id]=entry['target_id']
374
407
  else
375
- get_node_api(@resolve_state[:result][:node_info],scope: SCOPE_NODE_USER).crawl(self,{method: :process_resolve_node_file, top_file_id: entry['target_id']})
408
+ get_node_api(@resolve_state[:result][:node_info]).crawl(self,{method: :process_resolve_node_file, top_file_id: entry['target_id']})
376
409
  end
377
410
  when 'folder'
378
411
  if @resolve_state[:path].empty?
@@ -391,15 +424,15 @@ module Aspera
391
424
  # @param top_node_file Array [root node,file id]
392
425
  # @param element_path_string String path of element
393
426
  # supports links to secondary nodes
394
- def resolve_node_file( top_node_file, element_path_string )
427
+ def resolve_node_file(top_node_file, element_path_string)
395
428
  top_node_info,top_file_id=check_get_node_file(top_node_file)
396
- path_elements=element_path_string.split(PATH_SEPARATOR).select{|i| !i.empty?}
429
+ path_elements=element_path_string.split(PATH_SEPARATOR).reject(&:empty?)
397
430
  result={node_info: top_node_info, file_id: nil}
398
431
  if path_elements.empty?
399
432
  result[:file_id]=top_file_id
400
433
  else
401
434
  @resolve_state={path: path_elements, result: result}
402
- get_node_api(top_node_info,scope: SCOPE_NODE_USER).crawl(self,{method: :process_resolve_node_file, top_file_id: top_file_id})
435
+ get_node_api(top_node_info).crawl(self,{method: :process_resolve_node_file, top_file_id: top_file_id})
403
436
  not_found=@resolve_state[:path]
404
437
  @resolve_state=nil
405
438
  raise "entry not found: #{not_found}" if result[:file_id].nil?
@@ -407,5 +440,25 @@ module Aspera
407
440
  return result
408
441
  end
409
442
 
443
+ # @param entity_type path of entuty in API
444
+ # @param entity_name name of searched entity
445
+ # @param options additional search options
446
+ def lookup_entity_by_name(entity_type,entity_name,options={})
447
+ # returns entities whose name contains value (case insensitive)
448
+ matching_items=read(entity_type,options.merge({'q'=>entity_name}))[:data]
449
+ case matching_items.length
450
+ when 1 then return matching_items.first
451
+ when 0 then raise 'not found'
452
+ else
453
+ # multiple case insensitive partial matches, try case insensitive full match
454
+ # (anyway AoC does not allow creation of 2 entities with same case insensitive name)
455
+ icase_matches=matching_items.select{|i|i['name'].casecmp?(entity_name)}
456
+ case icase_matches.length
457
+ when 1 then return icase_matches.first
458
+ 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.)
459
+ else raise "Two entities cannot have the same case insensitive name: #{icase_matches.map{|i|i['name']}}"
460
+ end
461
+ end
462
+ end
410
463
  end # AoC
411
464
  end # Aspera