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