aspera-cli 4.16.0 → 4.17.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. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +50 -19
  4. data/CONTRIBUTING.md +3 -1
  5. data/README.md +965 -793
  6. data/bin/asession +29 -21
  7. data/lib/aspera/{fasp/agent_alpha.rb → agent/alpha.rb} +26 -25
  8. data/lib/aspera/{fasp/agent_base.rb → agent/base.rb} +15 -12
  9. data/lib/aspera/{fasp/agent_connect.rb → agent/connect.rb} +13 -11
  10. data/lib/aspera/{fasp/agent_direct.rb → agent/direct.rb} +49 -53
  11. data/lib/aspera/{fasp/agent_httpgw.rb → agent/httpgw.rb} +20 -19
  12. data/lib/aspera/{fasp/agent_node.rb → agent/node.rb} +20 -33
  13. data/lib/aspera/{fasp/agent_trsdk.rb → agent/trsdk.rb} +11 -11
  14. data/lib/aspera/api/aoc.rb +586 -0
  15. data/lib/aspera/api/ats.rb +46 -0
  16. data/lib/aspera/api/cos_node.rb +95 -0
  17. data/lib/aspera/api/node.rb +344 -0
  18. data/lib/aspera/ascmd.rb +46 -10
  19. data/lib/aspera/{fasp → ascp}/installation.rb +5 -5
  20. data/lib/aspera/{fasp → ascp}/management.rb +3 -8
  21. data/lib/aspera/{fasp → ascp}/products.rb +1 -1
  22. data/lib/aspera/assert.rb +30 -30
  23. data/lib/aspera/cli/basic_auth_plugin.rb +11 -10
  24. data/lib/aspera/cli/extended_value.rb +1 -1
  25. data/lib/aspera/cli/formatter.rb +13 -13
  26. data/lib/aspera/cli/hints.rb +5 -5
  27. data/lib/aspera/cli/main.rb +35 -28
  28. data/lib/aspera/cli/manager.rb +25 -24
  29. data/lib/aspera/cli/plugin.rb +22 -15
  30. data/lib/aspera/cli/plugin_factory.rb +61 -0
  31. data/lib/aspera/cli/plugins/alee.rb +7 -7
  32. data/lib/aspera/cli/plugins/aoc.rb +83 -77
  33. data/lib/aspera/cli/plugins/ats.rb +32 -33
  34. data/lib/aspera/cli/plugins/bss.rb +3 -4
  35. data/lib/aspera/cli/plugins/config.rb +169 -186
  36. data/lib/aspera/cli/plugins/console.rb +8 -6
  37. data/lib/aspera/cli/plugins/cos.rb +19 -18
  38. data/lib/aspera/cli/plugins/faspex.rb +61 -54
  39. data/lib/aspera/cli/plugins/faspex5.rb +150 -103
  40. data/lib/aspera/cli/plugins/node.rb +68 -73
  41. data/lib/aspera/cli/plugins/orchestrator.rb +34 -44
  42. data/lib/aspera/cli/plugins/preview.rb +31 -31
  43. data/lib/aspera/cli/plugins/server.rb +31 -33
  44. data/lib/aspera/cli/plugins/shares.rb +13 -11
  45. data/lib/aspera/cli/sync_actions.rb +8 -8
  46. data/lib/aspera/cli/transfer_agent.rb +32 -19
  47. data/lib/aspera/cli/transfer_progress.rb +1 -1
  48. data/lib/aspera/cli/version.rb +1 -1
  49. data/lib/aspera/colors.rb +5 -0
  50. data/lib/aspera/command_line_builder.rb +14 -14
  51. data/lib/aspera/coverage.rb +1 -2
  52. data/lib/aspera/data_repository.rb +1 -1
  53. data/lib/aspera/environment.rb +2 -3
  54. data/lib/aspera/faspex_gw.rb +5 -6
  55. data/lib/aspera/faspex_postproc.rb +1 -1
  56. data/lib/aspera/id_generator.rb +2 -2
  57. data/lib/aspera/json_rpc.rb +5 -5
  58. data/lib/aspera/keychain/encrypted_hash.rb +6 -6
  59. data/lib/aspera/keychain/macos_security.rb +27 -22
  60. data/lib/aspera/log.rb +2 -2
  61. data/lib/aspera/nagios.rb +3 -3
  62. data/lib/aspera/node_simulator.rb +5 -6
  63. data/lib/aspera/oauth/base.rb +143 -0
  64. data/lib/aspera/oauth/factory.rb +124 -0
  65. data/lib/aspera/oauth/generic.rb +34 -0
  66. data/lib/aspera/oauth/jwt.rb +51 -0
  67. data/lib/aspera/oauth/url_json.rb +31 -0
  68. data/lib/aspera/oauth/web.rb +50 -0
  69. data/lib/aspera/oauth.rb +5 -331
  70. data/lib/aspera/open_application.rb +7 -7
  71. data/lib/aspera/persistency_action_once.rb +4 -4
  72. data/lib/aspera/persistency_folder.rb +2 -2
  73. data/lib/aspera/preview/generator.rb +5 -5
  74. data/lib/aspera/preview/terminal.rb +3 -2
  75. data/lib/aspera/preview/utils.rb +3 -3
  76. data/lib/aspera/proxy_auto_config.rb +4 -4
  77. data/lib/aspera/rest.rb +175 -144
  78. data/lib/aspera/rest_errors_aspera.rb +3 -3
  79. data/lib/aspera/resumer.rb +77 -0
  80. data/lib/aspera/ssh.rb +6 -1
  81. data/lib/aspera/{fasp → transfer}/error.rb +3 -3
  82. data/lib/aspera/{fasp → transfer}/error_info.rb +1 -1
  83. data/lib/aspera/{fasp → transfer}/faux_file.rb +1 -1
  84. data/lib/aspera/{fasp → transfer}/parameters.rb +58 -89
  85. data/lib/aspera/{fasp/transfer_spec.rb → transfer/spec.rb} +18 -16
  86. data/lib/aspera/{fasp/parameters.yaml → transfer/spec.yaml} +4 -99
  87. data/lib/aspera/{fasp → transfer}/sync.rb +32 -32
  88. data/lib/aspera/{fasp → transfer}/uri.rb +9 -8
  89. data/lib/aspera/web_server_simple.rb +11 -3
  90. data.tar.gz.sig +0 -0
  91. metadata +36 -63
  92. metadata.gz.sig +0 -0
  93. data/lib/aspera/aoc.rb +0 -601
  94. data/lib/aspera/ats_api.rb +0 -47
  95. data/lib/aspera/cos_node.rb +0 -94
  96. data/lib/aspera/fasp/resume_policy.rb +0 -79
  97. data/lib/aspera/node.rb +0 -339
@@ -35,7 +35,7 @@ module Aspera
35
35
  end
36
36
 
37
37
  def put(object_id, value)
38
- assert_type(value, String)
38
+ Aspera.assert_type(value, String)
39
39
  persist_filepath = id_to_filepath(object_id)
40
40
  Log.log.debug{"persistency saving: #{persist_filepath}"}
41
41
  FileUtils.rm_f(persist_filepath)
@@ -68,7 +68,7 @@ module Aspera
68
68
 
69
69
  # @param object_id String or Array
70
70
  def id_to_filepath(object_id)
71
- assert_type(object_id, String)
71
+ Aspera.assert_type(object_id, String)
72
72
  FileUtils.mkdir_p(@folder)
73
73
  Environment.restrict_file_access(@folder)
74
74
  return File.join(@folder, "#{object_id}#{FILE_SUFFIX}")
@@ -54,7 +54,7 @@ module Aspera
54
54
  end
55
55
  @processing_method = @processing_method.to_sym
56
56
  Log.log.debug{"method: #{@processing_method}"}
57
- assert(respond_to?(@processing_method, true)){"no processing know for #{conversion_type} -> #{@preview_format_sym}"}
57
+ Aspera.assert(respond_to?(@processing_method, true)){"no processing know for #{conversion_type} -> #{@preview_format_sym}"}
58
58
  end
59
59
 
60
60
  # create preview as specified in constructor
@@ -88,7 +88,7 @@ module Aspera
88
88
  # @param total_count of parts
89
89
  # @param index of part (start at 1)
90
90
  def get_offset(duration, start_offset, total_count, index)
91
- assert_type(duration, Float){'duration'}
91
+ Aspera.assert_type(duration, Float){'duration'}
92
92
  return start_offset + ((index - 1) * (duration - start_offset) / total_count)
93
93
  end
94
94
 
@@ -151,10 +151,10 @@ module Aspera
151
151
  # do a simple re-encoding
152
152
  def convert_video_to_mp4_using_reencode
153
153
  options = @options.reencode_ffmpeg
154
- assert_type(options, Hash){'reencode_ffmpeg'}
154
+ Aspera.assert_type(options, Hash){'reencode_ffmpeg'}
155
155
  options.each do |k, v|
156
- assert_values(k, FFMPEG_OPTIONS_LIST){'key'}
157
- assert_type(v, Array){k}
156
+ Aspera.assert_values(k, FFMPEG_OPTIONS_LIST){'key'}
157
+ Aspera.assert_type(v, Array){k}
158
158
  end
159
159
  Utils.ffmpeg(
160
160
  in_f: @source_file_path,
@@ -4,6 +4,7 @@
4
4
 
5
5
  require 'rainbow'
6
6
  require 'io/console'
7
+ require 'aspera/log'
7
8
  module Aspera
8
9
  module Preview
9
10
  # Display a picture in the terminal, either using coloured characters or iTerm2
@@ -40,9 +41,9 @@ module Aspera
40
41
  (term_rows, term_columns) = IO.console.winsize
41
42
  term_rows -= reserve
42
43
  # compute scaling to fit terminal
43
- fit_term_ratio = [term_rows / image.rows.to_f, term_columns / image.columns.to_f].min
44
+ fit_term_ratio = [term_rows.to_f * font_ratio / image.rows.to_f, term_columns.to_f / image.columns.to_f].min
44
45
  height_ratio = double ? 2.0 : 1.0
45
- image = image.scale((image.columns * fit_term_ratio * font_ratio).to_i, (image.rows * fit_term_ratio * height_ratio).to_i)
46
+ image = image.scale((image.columns * fit_term_ratio).to_i, (image.rows * fit_term_ratio * height_ratio / font_ratio).to_i)
46
47
  # quantum depth is 8 or 16, see: `convert xc: -format "%q" info:`
47
48
  shift_for_8_bit = Magick::MAGICKCORE_QUANTUM_DEPTH - 8
48
49
  # get all pixel colors, adjusted for Rainbow
@@ -44,7 +44,7 @@ module Aspera
44
44
  # one could use "system", but we would need to redirect stdout/err
45
45
  # @return true if su
46
46
  def external_command(command_sym, command_args, check_code: true)
47
- assert_values(command_sym, EXTERNAL_TOOLS){'command'}
47
+ Aspera.assert_values(command_sym, EXTERNAL_TOOLS){'command'}
48
48
  # build command line, and quote special characters
49
49
  command_line = command_args.clone.unshift(command_sym).map{|i| shell_quote(i.to_s)}.join(' ')
50
50
  Log.log.debug{"cmd=#{command_line}".blue}
@@ -59,7 +59,7 @@ module Aspera
59
59
  end
60
60
 
61
61
  def ffmpeg(a)
62
- assert_type(a, Hash)
62
+ Aspera.assert_type(a, Hash)
63
63
  # input_file,input_args,output_file,output_args
64
64
  a[:gl_p] ||= [
65
65
  '-y', # overwrite output without asking
@@ -67,7 +67,7 @@ module Aspera
67
67
  ]
68
68
  a[:in_p] ||= []
69
69
  a[:out_p] ||= []
70
- assert(%i[gl_p in_f in_p out_f out_p].eql?(a.keys.sort)){"wrong params (#{a.keys.sort})"}
70
+ Aspera.assert(%i[gl_p in_f in_p out_f out_p].eql?(a.keys.sort)){"wrong params (#{a.keys.sort})"}
71
71
  external_command(:ffmpeg, [a[:gl_p], a[:in_p], '-i', a[:in_f], a[:out_p], a[:out_f]].flatten)
72
72
  end
73
73
 
@@ -11,7 +11,7 @@ module URI
11
11
  alias_method :find_proxy_orig, :find_proxy
12
12
  class << self
13
13
  def register_proxy_finder
14
- assert(block_given?)
14
+ Aspera.assert(block_given?)
15
15
  # overload the method in URI : call user's provided block and fallback to original method
16
16
  define_method(:find_proxy) {|env_vars=ENV| yield(to_s) || find_proxy_orig(env_vars)}
17
17
  end
@@ -109,12 +109,12 @@ END_OF_JAVASCRIPT
109
109
  parts = item.strip.split
110
110
  case parts.shift
111
111
  when 'DIRECT'
112
- assert(parts.empty?){'DIRECT has no param'}
112
+ Aspera.assert(parts.empty?){'DIRECT has no param'}
113
113
  Log.log.debug('ignoring proxy DIRECT')
114
114
  when 'PROXY'
115
115
  addr_port = parts.shift
116
- assert_type(addr_port, String)
117
- assert(parts.empty?){'PROXY shall have one param'}
116
+ Aspera.assert_type(addr_port, String)
117
+ Aspera.assert(parts.empty?){'PROXY shall have one param'}
118
118
  begin
119
119
  # PAC proxy addresses are <host>:<port>
120
120
  if /:[0-9]+$/.match?(addr_port)
data/lib/aspera/rest.rb CHANGED
@@ -56,19 +56,19 @@ module Aspera
56
56
  return values.first.eql?(ARRAY_PARAMS)
57
57
  end
58
58
 
59
- # build URI from URL and parameters and check it is http or https
60
- def build_uri(url, params=nil)
59
+ # build URI from URL and parameters and check it is http or https, encode array [] parameters
60
+ def build_uri(url, query_hash=nil)
61
61
  uri = URI.parse(url)
62
- assert(%w[http https].include?(uri.scheme)){"REST endpoint shall be http/s not #{uri.scheme}"}
63
- return uri if params.nil?
64
- Log.log.debug{Log.dump('params', params)}
65
- assert_type(params, Hash)
66
- return uri if params.empty?
62
+ Aspera.assert(%w[http https].include?(uri.scheme)){"REST endpoint shall be http/s not #{uri.scheme}"}
63
+ return uri if query_hash.nil?
64
+ Log.log.debug{Log.dump('query', query_hash)}
65
+ Aspera.assert_type(query_hash, Hash)
66
+ return uri if query_hash.empty?
67
67
  query = []
68
- params.each do |k, v|
68
+ query_hash.each do |k, v|
69
69
  case v
70
70
  when Array
71
- # support array url params, there is no standard. Either p[]=1&p[]=2, or p=1&p=2
71
+ # support array for query parameter, there is no standard. Either p[]=1&p[]=2, or p=1&p=2
72
72
  suffix = array_params?(v) ? v.shift : ''
73
73
  v.each do |e|
74
74
  query.push(["#{k}#{suffix}", e])
@@ -86,7 +86,7 @@ module Aspera
86
86
  URI.decode_www_form(query).each_with_object({}){|v, h|h[v.first] = v.last }
87
87
  end
88
88
 
89
- # start a HTTP/S session, also used for web sockets
89
+ # Start a HTTP/S session, also used for web sockets
90
90
  # @param base_url [String] base url of HTTP/S session
91
91
  # @return [Net::HTTP] a started HTTP session
92
92
  def start_http_session(base_url)
@@ -105,31 +105,34 @@ module Aspera
105
105
  # little hack, handy because HTTP debug, proxy, etc... will be available
106
106
  # used implement web sockets after `start_http_session`
107
107
  def io_http_session(http_session)
108
- assert_type(http_session, Net::HTTP)
108
+ Aspera.assert_type(http_session, Net::HTTP)
109
109
  # Net::BufferedIO in net/protocol.rb
110
110
  result = http_session.instance_variable_get(:@socket)
111
- assert(!result.nil?){"no socket for #{http_session}"}
111
+ Aspera.assert(!result.nil?){"no socket for #{http_session}"}
112
112
  return result
113
113
  end
114
114
 
115
115
  # @return [String] PEM certificates of remote server
116
- def remote_certificates(url)
116
+ def remote_certificate_chain(url, as_string: true)
117
+ result = []
117
118
  # initiate a session to retrieve remote certificate
118
119
  http_session = Rest.start_http_session(url)
119
120
  begin
120
121
  # retrieve underlying openssl socket
121
- return Rest.io_http_session(http_session).io.peer_cert_chain.reverse.map(&:to_pem).join("\n")
122
+ result = Rest.io_http_session(http_session).io.peer_cert_chain
122
123
  rescue
123
- return http_session.peer_cert.to_pem
124
+ result = http_session.peer_cert
124
125
  ensure
125
126
  http_session.finish
126
127
  end
128
+ result = result.map(&:to_pem).join("\n") if as_string
129
+ return result
127
130
  end
128
131
 
129
132
  # set global parameters
130
133
  def set_parameters(**options)
131
134
  options.each do |key, value|
132
- assert(@@global.key?(key)){"unknown Rest option #{key}"}
135
+ Aspera.assert(@@global.key?(key)){"Unknown Rest option #{key}"}
133
136
  @@global[key] = value
134
137
  end
135
138
  end
@@ -145,135 +148,169 @@ module Aspera
145
148
  # create and start keep alive connection on demand
146
149
  def http_session
147
150
  if @http_session.nil?
148
- @http_session = self.class.start_http_session(@params[:base_url])
151
+ @http_session = self.class.start_http_session(@base_url)
149
152
  end
150
153
  return @http_session
151
154
  end
152
155
 
153
- public
154
-
155
- attr_reader :params
156
-
157
- def oauth
158
- if @oauth.nil?
159
- assert(@params[:auth][:type].eql?(:oauth2)){'no OAuth defined'}
160
- @oauth = Oauth.new(@params[:auth])
161
- end
162
- return @oauth
163
- end
164
-
165
- # @param a_rest_params [Hash] default call parameters (merged at call)
166
- def initialize(a_rest_params)
167
- assert_type(a_rest_params, Hash)
168
- assert_type(a_rest_params[:base_url], String)
169
- @params = a_rest_params.clone
170
- Log.log.debug{Log.dump('REST params', @params)}
171
- # base url without trailing slashes (note: string may be frozen)
172
- @params[:base_url] = @params[:base_url].gsub(%r{/+$}, '')
173
- @http_session = nil
174
- # default is no auth
175
- @params[:auth] ||= {type: :none}
176
- @params[:not_auth_codes] ||= ['401']
177
- @oauth = nil
178
- Log.log.debug{Log.dump('REST params(2)', @params)}
179
- end
180
-
181
- def oauth_token(force_refresh: false)
182
- assert_values(force_refresh, [true, false])
183
- return oauth.get_authorization(use_refresh_token: force_refresh)
184
- end
185
-
186
- def build_request(call_data)
156
+ def build_request(
157
+ operation:,
158
+ subpath:,
159
+ url_params:,
160
+ json_params:,
161
+ www_body_params:,
162
+ text_body_params:,
163
+ headers:
164
+ )
187
165
  # TODO: shall we percent encode subpath (spaces) test with access key delete with space in id
188
166
  # URI.escape()
189
- uri = self.class.build_uri("#{call_data[:base_url]}#{['', '/'].include?(call_data[:subpath]) ? '' : '/'}#{call_data[:subpath]}", call_data[:url_params])
167
+ separator = !['', '/'].include?(subpath) || @base_url.end_with?('/') ? '/' : ''
168
+ uri = self.class.build_uri("#{@base_url}#{separator}#{subpath}", url_params)
190
169
  Log.log.debug{"URI=#{uri}"}
191
170
  begin
192
171
  # instantiate request object based on string name
193
- req = Net::HTTP.const_get(call_data[:operation].capitalize).new(uri)
172
+ req = Net::HTTP.const_get(operation.capitalize).new(uri)
194
173
  rescue NameError
195
- raise "unsupported operation : #{call_data[:operation]}"
174
+ raise "unsupported operation : #{operation}"
196
175
  end
197
- if call_data.key?(:json_params) && !call_data[:json_params].nil?
198
- req.body = JSON.generate(call_data[:json_params]) # , ascii_only: true
199
- Log.log.debug{Log.dump('body JSON data', call_data[:json_params])}
176
+ if !json_params.nil?
177
+ req.body = JSON.generate(json_params) # , ascii_only: true
200
178
  req['Content-Type'] = 'application/json'
201
- # call_data[:headers]['Accept']='application/json'
202
179
  end
203
- if call_data.key?(:www_body_params)
204
- req.body = URI.encode_www_form(call_data[:www_body_params])
205
- Log.log.debug{"body www data=#{req.body.chomp}"}
180
+ if !www_body_params.nil?
181
+ req.body = URI.encode_www_form(www_body_params)
206
182
  req['Content-Type'] = 'application/x-www-form-urlencoded'
207
183
  end
208
- if call_data.key?(:text_body_params)
209
- req.body = call_data[:text_body_params]
210
- Log.log.debug{"body data=#{req.body.chomp}"}
184
+ if !text_body_params.nil?
185
+ req.body = text_body_params
211
186
  end
212
187
  # set headers
213
- if call_data.key?(:headers)
214
- call_data[:headers].each_key do |key|
215
- req[key] = call_data[:headers][key]
216
- end
188
+ headers.each do |key, value|
189
+ req[key] = value
217
190
  end
218
191
  # :type = :basic
219
- req.basic_auth(call_data[:auth][:username], call_data[:auth][:password]) if call_data[:auth][:type].eql?(:basic)
192
+ req.basic_auth(@auth_params[:username], @auth_params[:password]) if @auth_params[:type].eql?(:basic)
220
193
  Log.log.debug{Log.dump(:req_body, req.body)}
221
194
  return req
222
195
  end
223
196
 
197
+ public
198
+
199
+ attr_reader :auth_params
200
+ attr_reader :base_url
201
+
202
+ def params
203
+ return {
204
+ base_url: @base_url,
205
+ auth: @auth_params,
206
+ not_auth_codes: @not_auth_codes,
207
+ redirect_max: @redirect_max,
208
+ headers: @headers
209
+ }
210
+ end
211
+
212
+ # @param base_url [String] base URL of REST API
213
+ # @param auth [Hash] authentication parameters:
214
+ # :type (:none, :basic, :url, :oauth2)
215
+ # :username [:basic]
216
+ # :password [:basic]
217
+ # :url_query [:url] a hash
218
+ # :* [:oauth2] see OAuth::Factory class
219
+ # @param not_auth_codes [Array] codes that trigger a refresh/regeneration of bearer token
220
+ # @param redirect_max [int] max redirections allowed
221
+ def initialize(
222
+ base_url:,
223
+ auth: nil,
224
+ not_auth_codes: nil,
225
+ redirect_max: 0,
226
+ headers: nil
227
+ )
228
+ Aspera.assert_type(base_url, String)
229
+ # base url with max one trailing slashes (note: string may be frozen)
230
+ @base_url = base_url.gsub(%r{//+$}, '/')
231
+ # default is no auth
232
+ @auth_params = auth.nil? ? {type: :none} : auth
233
+ Aspera.assert_type(@auth_params, Hash)
234
+ Aspera.assert(@auth_params.key?(:type)){'no auth type defined'}
235
+ @not_auth_codes = not_auth_codes.nil? ? ['401'] : not_auth_codes
236
+ Aspera.assert_type(@not_auth_codes, Array)
237
+ # persistent session
238
+ @http_session = nil
239
+ # OAuth object (created on demand)
240
+ @oauth = nil
241
+ @redirect_max = redirect_max
242
+ @headers = headers.nil? ? {} : headers
243
+ Aspera.assert_type(@headers, Hash)
244
+ @headers['User-Agent'] ||= @@global[:user_agent]
245
+ end
246
+
247
+ # @return the OAuth object (create, or cached if already created)
248
+ def oauth
249
+ if @oauth.nil?
250
+ Aspera.assert(@auth_params[:type].eql?(:oauth2)){'no OAuth defined'}
251
+ oauth_parameters = @auth_params.reject { |k, _v| k.eql?(:type) }
252
+ Log.log.debug{Log.dump('oauth parameters', oauth_parameters)}
253
+ @oauth = OAuth::Factory.instance.create(**oauth_parameters)
254
+ end
255
+ return @oauth
256
+ end
257
+
258
+ def oauth_token(force_refresh: false)
259
+ Aspera.assert_values(force_refresh, [true, false])
260
+ return oauth.get_authorization(use_refresh_token: force_refresh)
261
+ end
262
+
224
263
  # HTTP/S REST call
225
- # call_data has keys:
226
- # :auth
227
- # :operation
228
- # :subpath
229
- # :headers
230
- # :json_params
231
- # :url_params
232
- # :www_body_params
233
- # :text_body_params
234
- # :save_to_file (filepath) default: nil
235
- # :return_error (bool) default: nil
236
- # :redirect_max (int) default: 0
237
- # :not_auth_codes (array) codes that trigger a refresh/regeneration of bearer token
238
- # ----
239
- # authentication (:auth) :
240
- # :type (:none, :basic, :oauth2, :url)
241
- # :username [:basic]
242
- # :password [:basic]
243
- # :url_query [:url] a hash
244
- # :* [:oauth2] see Oauth class
245
- def call(call_data)
246
- assert_type(call_data, Hash)
247
- call_data[:subpath] = '' if call_data[:subpath].nil?
248
- Log.log.debug{"accessing #{call_data[:subpath]}".red.bold.bg_green}
249
- call_data[:headers] ||= {}
250
- call_data[:headers]['User-Agent'] ||= @@global[:user_agent]
251
- # defaults from @params are overridden by call data
252
- call_data = @params.deep_merge(call_data)
253
- case call_data[:auth][:type]
264
+ # @param save_to_file (filepath)
265
+ # @param return_error (bool)
266
+ def call(
267
+ operation:,
268
+ subpath: nil,
269
+ json_params: nil,
270
+ url_params: nil,
271
+ www_body_params: nil,
272
+ text_body_params: nil,
273
+ save_to_file: nil,
274
+ return_error: false,
275
+ headers: nil
276
+ )
277
+ subpath = subpath.to_s if subpath.is_a?(Symbol)
278
+ subpath = '' if subpath.nil?
279
+ Aspera.assert_type(subpath, String)
280
+ if headers.nil?
281
+ headers = @headers.clone
282
+ else
283
+ h = headers
284
+ headers = @headers.clone
285
+ headers.merge!(h)
286
+ end
287
+ Aspera.assert_type(headers, Hash)
288
+ Log.log.debug{"#{operation} [#{subpath}]".red.bold.bg_green}
289
+ case @auth_params[:type]
254
290
  when :none
255
291
  # no auth
256
292
  when :basic
257
293
  Log.log.debug('using Basic auth')
258
294
  # done in build_req
259
295
  when :oauth2
260
- call_data[:headers]['Authorization'] = oauth_token unless call_data[:headers].key?('Authorization')
296
+ headers['Authorization'] = oauth_token unless headers.key?('Authorization')
261
297
  when :url
262
- call_data[:url_params] ||= {}
263
- call_data[:auth][:url_query].each do |key, value|
264
- call_data[:url_params][key] = value
298
+ url_params ||= {}
299
+ @auth_params[:url_query].each do |key, value|
300
+ url_params[key] = value
265
301
  end
266
- else error_unexpected_value(call_data[:auth][:type])
302
+ else Aspera.error_unexpected_value(@auth_params[:type])
267
303
  end
268
- req = build_request(call_data)
269
- Log.log.debug{"call_data = #{call_data}"}
270
304
  result = {http: nil}
271
305
  # start a block to be able to retry the actual HTTP request
272
306
  begin
307
+ req = build_request(
308
+ operation: operation, subpath: subpath, json_params: json_params, url_params: url_params, www_body_params: www_body_params,
309
+ text_body_params: text_body_params, headers: headers)
273
310
  # we try the call, and will retry only if oauth, as we can, first with refresh, and then re-auth if refresh is bad
274
311
  oauth_tries ||= 2
275
312
  # initialize with number of initial retries allowed, nil gives zero
276
- tries_remain_redirect = call_data[:redirect_max].to_i if tries_remain_redirect.nil?
313
+ tries_remain_redirect = @redirect_max.to_i if tries_remain_redirect.nil?
277
314
  Log.log.debug("send request (retries=#{tries_remain_redirect})")
278
315
  result_mime = nil
279
316
  file_saved = false
@@ -282,13 +319,13 @@ module Aspera
282
319
  result[:http] = response
283
320
  result_mime = (result[:http]['Content-Type'] || 'text/plain').split(';').first.downcase
284
321
  # JSON data needs to be parsed, in case it contains an error code
285
- if !call_data[:save_to_file].nil? &&
322
+ if !save_to_file.nil? &&
286
323
  result[:http].code.to_s.start_with?('2') &&
287
324
  !result[:http]['Content-Length'].nil? &&
288
325
  !JSON_DECODE.include?(result_mime)
289
326
  total_size = result[:http]['Content-Length'].to_i
290
327
  Log.log.debug('before write file')
291
- target_file = call_data[:save_to_file]
328
+ target_file = save_to_file
292
329
  # override user's path to path in header
293
330
  if !response['Content-Disposition'].nil? && (m = response['Content-Disposition'].match(/filename="([^"]+)"/))
294
331
  target_file = File.join(File.dirname(target_file), m[1])
@@ -321,13 +358,13 @@ module Aspera
321
358
  else # when 'text/plain'
322
359
  result[:http].body
323
360
  end
324
- Log.log.debug{Log.dump("result: parsed: #{result_mime}", result[:data])}
325
- Log.log.debug{"result: code=#{result[:http].code}"}
361
+ Log.log.debug{"result: code=#{result[:http].code} mime=#{result_mime}"}
362
+ Log.log.debug{Log.dump('data', result[:data])}
326
363
  RestErrorAnalyzer.instance.raise_on_error(req, result)
327
- File.write(call_data[:save_to_file], result[:http].body) unless file_saved || call_data[:save_to_file].nil?
364
+ File.write(save_to_file, result[:http].body) unless file_saved || save_to_file.nil?
328
365
  rescue RestCallError => e
329
366
  # not authorized: oauth token expired
330
- if call_data[:not_auth_codes].include?(result[:http].code.to_s) && call_data[:auth][:type].eql?(:oauth2)
367
+ if @not_auth_codes.include?(result[:http].code.to_s) && @auth_params[:type].eql?(:oauth2)
331
368
  begin
332
369
  # try to use refresh token
333
370
  req['Authorization'] = oauth_token(force_refresh: true)
@@ -337,35 +374,28 @@ module Aspera
337
374
  # regenerate a brand new token
338
375
  req['Authorization'] = oauth_token(force_refresh: true)
339
376
  end
340
- Log.log.debug{"using new token=#{call_data[:headers]['Authorization']}"}
341
- retry unless (oauth_tries -= 1).zero?
377
+ Log.log.debug{"using new token=#{headers['Authorization']}"}
378
+ retry if (oauth_tries -= 1).nonzero?
342
379
  end # if oauth
343
380
  # redirect ? (any code beginning with 3)
344
- if tries_remain_redirect.positive? && e.response.is_a?(Net::HTTPRedirection)
381
+ if e.response.is_a?(Net::HTTPRedirection) && tries_remain_redirect.positive?
345
382
  tries_remain_redirect -= 1
346
- current_uri = URI.parse(call_data[:base_url])
383
+ current_uri = URI.parse(@base_url)
347
384
  new_url = e.response['location']
348
385
  # special case: relative redirect
349
386
  if URI.parse(new_url).host.nil?
350
387
  # we don't manage relative redirects with non-absolute path
351
- assert(new_url.start_with?('/')){"redirect location is relative: #{new_url}, but does not start with /."}
352
- new_url = current_uri.scheme + '://' + current_uri.host + new_url
353
- end
354
- Log.log.info{"URL is moved: #{new_url}"}
355
- redirection_uri = URI.parse(new_url)
356
- call_data[:base_url] = new_url
357
- call_data[:subpath] = ''
358
- if current_uri.host.eql?(redirection_uri.host) && current_uri.port.eql?(redirection_uri.port)
359
- req = build_request(call_data)
360
- retry
361
- else
362
- # change host
363
- Log.log.info{"Redirect changes host: #{current_uri.host} -> #{redirection_uri.host}"}
364
- return self.class.new(call_data).call(call_data)
388
+ Aspera.assert(new_url.start_with?('/')){"redirect location is relative: #{new_url}, but does not start with /."}
389
+ new_url = "#{current_uri.scheme}://#{current_uri.host}#{new_url}"
365
390
  end
391
+ # forwards the request to the new location
392
+ return self.class.new(base_url: new_url, redirect_max: tries_remain_redirect).call(
393
+ operation: operation, json_params: json_params,
394
+ url_params: url_params, www_body_params: www_body_params, text_body_params: text_body_params,
395
+ save_to_file: save_to_file, return_error: return_error, headers: headers)
366
396
  end
367
397
  # raise exception if could not retry and not return error in result
368
- raise e unless call_data[:return_error]
398
+ raise e unless return_error
369
399
  end # begin request
370
400
  Log.log.debug{"result=#{result}"}
371
401
  return result
@@ -377,36 +407,37 @@ module Aspera
377
407
 
378
408
  # @param encoding : one of: :json_params, :url_params
379
409
  def create(subpath, params, encoding=:json_params)
380
- return call({operation: 'POST', subpath: subpath, headers: {'Accept' => 'application/json'}, encoding => params})
410
+ return call(operation: 'POST', subpath: subpath, headers: {'Accept' => 'application/json'}, encoding => params)
381
411
  end
382
412
 
383
- def read(subpath, options=nil)
384
- return call({operation: 'GET', subpath: subpath, headers: {'Accept' => 'application/json'}, url_params: options})
413
+ def read(subpath, query=nil)
414
+ return call(operation: 'GET', subpath: subpath, headers: {'Accept' => 'application/json'}, url_params: query)
385
415
  end
386
416
 
387
417
  def update(subpath, params)
388
- return call({operation: 'PUT', subpath: subpath, headers: {'Accept' => 'application/json'}, json_params: params})
418
+ return call(operation: 'PUT', subpath: subpath, headers: {'Accept' => 'application/json'}, json_params: params)
389
419
  end
390
420
 
391
421
  def delete(subpath, params=nil)
392
- return call({operation: 'DELETE', subpath: subpath, headers: {'Accept' => 'application/json'}, url_params: params})
422
+ return call(operation: 'DELETE', subpath: subpath, headers: {'Accept' => 'application/json'}, url_params: params)
393
423
  end
394
424
 
395
425
  def cancel(subpath)
396
- return call({operation: 'CANCEL', subpath: subpath, headers: {'Accept' => 'application/json'}})
426
+ return call(operation: 'CANCEL', subpath: subpath, headers: {'Accept' => 'application/json'})
397
427
  end
398
428
 
399
- # Query by name and returns a single result, else it throws an exception (no or multiple results)
429
+ # Query entity by general search (read with parameter `q`)
430
+ # TODO: not generic enough ? move somewhere ? inheritance ?
400
431
  # @param subpath path of entity in API
401
432
  # @param search_name name of searched entity
402
- # @param options additional search options
403
- def lookup_by_name(subpath, search_name, options={})
404
- # returns entities whose name contains value (case insensitive)
405
- matching_items = read(subpath, options.merge({'q' => search_name}))[:data]
433
+ # @param query additional search query parameters
434
+ # @returns [Hash] A single entity matching the search, or an exception if not found or multiple found
435
+ def lookup_by_name(subpath, search_name, query={})
436
+ # returns entities matching the query (it matches against several fields in case insensitive way)
437
+ matching_items = read(subpath, query.merge({'q' => search_name}))[:data]
406
438
  # API style: {totalcount:, ...} cspell: disable-line
407
- # TODO: not generic enough ? move somewhere ? inheritance ?
408
439
  matching_items = matching_items[subpath] if matching_items.is_a?(Hash)
409
- assert_type(matching_items, Array)
440
+ Aspera.assert_type(matching_items, Array)
410
441
  case matching_items.length
411
442
  when 1 then return matching_items.first
412
443
  when 0 then raise %Q{#{ENTITY_NOT_FOUND} #{subpath}: "#{search_name}"}
@@ -15,10 +15,10 @@ module Aspera
15
15
  RestErrorAnalyzer.instance.add_simple_handler(name: 'Type 1: error:user_message', path: %w[error user_message], always: true)
16
16
  RestErrorAnalyzer.instance.add_simple_handler(name: 'Type 2: error:description', path: %w[error description])
17
17
  RestErrorAnalyzer.instance.add_simple_handler(name: 'Type 3: error:internal_message', path: %w[error internal_message])
18
- # AoC Automation
19
- RestErrorAnalyzer.instance.add_simple_handler(name: 'AoC Automation', path: ['error'])
20
18
  RestErrorAnalyzer.instance.add_simple_handler(name: 'Type 5', path: ['error_description'])
21
19
  RestErrorAnalyzer.instance.add_simple_handler(name: 'Type 6', path: ['message'])
20
+ # AoC Automation
21
+ RestErrorAnalyzer.instance.add_simple_handler(name: 'AoC Automation', path: ['error'])
22
22
  RestErrorAnalyzer.instance.add_handler('Type 7: errors[]') do |type, call_context|
23
23
  next unless call_context[:data].is_a?(Hash) && call_context[:data]['errors'].is_a?(Hash)
24
24
  # special for Shares: false positive ? (update global transfer_settings)
@@ -33,7 +33,7 @@ module Aspera
33
33
  d_t_s = call_context[:data]['transfer_specs']
34
34
  next unless d_t_s.is_a?(Array)
35
35
  d_t_s.each do |res|
36
- r_err = res.dig(*%w[transfer_spec error])
36
+ r_err = res.dig(*%w[transfer_spec error]) || res['error']
37
37
  next unless r_err.is_a?(Hash)
38
38
  RestErrorAnalyzer.add_error(call_context, type, "#{r_err['code']}: #{r_err['reason']}: #{r_err['user_message']}")
39
39
  end