aspera-cli 4.14.0 → 4.16.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 (104) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +29 -3
  4. data/CHANGELOG.md +300 -185
  5. data/CONTRIBUTING.md +74 -23
  6. data/README.md +2346 -1619
  7. data/bin/ascli +16 -25
  8. data/bin/asession +15 -15
  9. data/examples/dascli +2 -2
  10. data/examples/proxy.pac +1 -1
  11. data/lib/aspera/aoc.rb +216 -150
  12. data/lib/aspera/ascmd.rb +25 -18
  13. data/lib/aspera/assert.rb +45 -0
  14. data/lib/aspera/cli/basic_auth_plugin.rb +9 -6
  15. data/lib/aspera/cli/error.rb +17 -0
  16. data/lib/aspera/cli/extended_value.rb +51 -16
  17. data/lib/aspera/cli/formatter.rb +276 -174
  18. data/lib/aspera/cli/hints.rb +81 -0
  19. data/lib/aspera/cli/main.rb +114 -147
  20. data/lib/aspera/cli/manager.rb +181 -136
  21. data/lib/aspera/cli/plugin.rb +82 -64
  22. data/lib/aspera/cli/plugins/alee.rb +0 -1
  23. data/lib/aspera/cli/plugins/aoc.rb +327 -331
  24. data/lib/aspera/cli/plugins/ats.rb +12 -8
  25. data/lib/aspera/cli/plugins/bss.rb +2 -2
  26. data/lib/aspera/cli/plugins/config.rb +575 -439
  27. data/lib/aspera/cli/plugins/console.rb +40 -0
  28. data/lib/aspera/cli/plugins/cos.rb +4 -5
  29. data/lib/aspera/cli/plugins/faspex.rb +111 -92
  30. data/lib/aspera/cli/plugins/faspex5.rb +245 -182
  31. data/lib/aspera/cli/plugins/node.rb +239 -160
  32. data/lib/aspera/cli/plugins/orchestrator.rb +56 -19
  33. data/lib/aspera/cli/plugins/preview.rb +54 -38
  34. data/lib/aspera/cli/plugins/server.rb +63 -20
  35. data/lib/aspera/cli/plugins/shares.rb +64 -38
  36. data/lib/aspera/cli/sync_actions.rb +68 -0
  37. data/lib/aspera/cli/transfer_agent.rb +64 -67
  38. data/lib/aspera/cli/transfer_progress.rb +73 -0
  39. data/lib/aspera/cli/version.rb +1 -1
  40. data/lib/aspera/colors.rb +3 -1
  41. data/lib/aspera/command_line_builder.rb +27 -22
  42. data/lib/aspera/cos_node.rb +6 -4
  43. data/lib/aspera/coverage.rb +22 -0
  44. data/lib/aspera/data_repository.rb +33 -2
  45. data/lib/aspera/environment.rb +21 -8
  46. data/lib/aspera/fasp/agent_alpha.rb +116 -0
  47. data/lib/aspera/fasp/agent_base.rb +40 -76
  48. data/lib/aspera/fasp/agent_connect.rb +21 -22
  49. data/lib/aspera/fasp/agent_direct.rb +169 -179
  50. data/lib/aspera/fasp/agent_httpgw.rb +200 -195
  51. data/lib/aspera/fasp/agent_node.rb +43 -35
  52. data/lib/aspera/fasp/agent_trsdk.rb +124 -41
  53. data/lib/aspera/fasp/error_info.rb +2 -2
  54. data/lib/aspera/fasp/faux_file.rb +52 -0
  55. data/lib/aspera/fasp/installation.rb +89 -191
  56. data/lib/aspera/fasp/management.rb +249 -0
  57. data/lib/aspera/fasp/parameters.rb +86 -47
  58. data/lib/aspera/fasp/parameters.yaml +75 -8
  59. data/lib/aspera/fasp/products.rb +162 -0
  60. data/lib/aspera/fasp/resume_policy.rb +7 -5
  61. data/lib/aspera/fasp/sync.rb +273 -0
  62. data/lib/aspera/fasp/transfer_spec.rb +10 -8
  63. data/lib/aspera/fasp/uri.rb +6 -6
  64. data/lib/aspera/faspex_gw.rb +11 -8
  65. data/lib/aspera/faspex_postproc.rb +8 -7
  66. data/lib/aspera/hash_ext.rb +2 -2
  67. data/lib/aspera/id_generator.rb +3 -1
  68. data/lib/aspera/json_rpc.rb +51 -0
  69. data/lib/aspera/keychain/encrypted_hash.rb +46 -11
  70. data/lib/aspera/keychain/macos_security.rb +15 -13
  71. data/lib/aspera/line_logger.rb +23 -0
  72. data/lib/aspera/log.rb +61 -19
  73. data/lib/aspera/nagios.rb +7 -2
  74. data/lib/aspera/node.rb +105 -21
  75. data/lib/aspera/node_simulator.rb +214 -0
  76. data/lib/aspera/oauth.rb +57 -36
  77. data/lib/aspera/open_application.rb +4 -4
  78. data/lib/aspera/persistency_action_once.rb +13 -14
  79. data/lib/aspera/persistency_folder.rb +5 -4
  80. data/lib/aspera/preview/file_types.rb +56 -268
  81. data/lib/aspera/preview/generator.rb +28 -39
  82. data/lib/aspera/preview/options.rb +2 -0
  83. data/lib/aspera/preview/terminal.rb +36 -16
  84. data/lib/aspera/preview/utils.rb +23 -29
  85. data/lib/aspera/proxy_auto_config.rb +6 -3
  86. data/lib/aspera/rest.rb +127 -80
  87. data/lib/aspera/rest_call_error.rb +1 -1
  88. data/lib/aspera/rest_error_analyzer.rb +16 -14
  89. data/lib/aspera/rest_errors_aspera.rb +39 -34
  90. data/lib/aspera/secret_hider.rb +18 -17
  91. data/lib/aspera/ssh.rb +10 -5
  92. data/lib/aspera/temp_file_manager.rb +11 -4
  93. data/lib/aspera/web_auth.rb +10 -7
  94. data/lib/aspera/web_server_simple.rb +11 -5
  95. data.tar.gz.sig +0 -0
  96. metadata +108 -39
  97. metadata.gz.sig +0 -0
  98. data/lib/aspera/cli/listener/line_dump.rb +0 -19
  99. data/lib/aspera/cli/listener/logger.rb +0 -22
  100. data/lib/aspera/cli/listener/progress.rb +0 -50
  101. data/lib/aspera/cli/listener/progress_multi.rb +0 -84
  102. data/lib/aspera/cli/plugins/sync.rb +0 -44
  103. data/lib/aspera/fasp/listener.rb +0 -13
  104. data/lib/aspera/sync.rb +0 -213
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # cspell:ignore ffprobe optipng unoconv
4
+ require 'aspera/log'
5
+ require 'aspera/assert'
3
6
  require 'English'
4
7
  require 'tmpdir'
5
8
  require 'fileutils'
6
- require 'aspera/log'
7
9
  require 'open3'
8
10
 
9
11
  module Aspera
@@ -11,18 +13,17 @@ module Aspera
11
13
  class Utils
12
14
  # from bash manual: meta-character need to be escaped
13
15
  BASH_SPECIAL_CHARACTERS = "|&;()<> \t#\n"
14
- # shell exit code when command is not found
15
- BASH_EXIT_NOT_FOUND = 127
16
16
  # external binaries used
17
17
  EXTERNAL_TOOLS = %i[ffmpeg ffprobe convert composite optipng unoconv].freeze
18
18
  TEMP_FORMAT = 'img%04d.jpg'
19
- private_constant :BASH_SPECIAL_CHARACTERS, :BASH_EXIT_NOT_FOUND, :EXTERNAL_TOOLS, :TEMP_FORMAT
19
+ private_constant :BASH_SPECIAL_CHARACTERS, :EXTERNAL_TOOLS, :TEMP_FORMAT
20
20
 
21
21
  class << self
22
22
  # returns string with single quotes suitable for bash if there is any bash meta-character
23
23
  def shell_quote(argument)
24
24
  return argument unless argument.chars.any?{|c|BASH_SPECIAL_CHARACTERS.include?(c)}
25
- return "'" + argument.gsub(/'/){|_s| "'\"'\"'"} + "'"
25
+ # surround with single quotes, and escape single quotes
26
+ return %Q{'#{argument.gsub("'"){|_s| %q{'"'"'}}}'}
26
27
  end
27
28
 
28
29
  # check that external tools can be executed
@@ -30,42 +31,35 @@ module Aspera
30
31
  tools_to_check = EXTERNAL_TOOLS.dup
31
32
  tools_to_check.delete(:unoconv) if skip_types.include?(:office)
32
33
  # Check for binaries
33
- tools_to_check.each do |command_symb|
34
- external_command(command_symb, ['-h'])
34
+ tools_to_check.each do |command_sym|
35
+ external_command(command_sym, ['-h'], check_code: false)
36
+ rescue Errno::ENOENT => e
37
+ raise "missing #{command_sym} binary: #{e}"
38
+ rescue
39
+ nil
35
40
  end
36
41
  end
37
42
 
38
43
  # execute external command
39
44
  # one could use "system", but we would need to redirect stdout/err
40
45
  # @return true if su
41
- def external_command(command_symb, command_args)
42
- raise "unexpected command #{command_symb}" unless EXTERNAL_TOOLS.include?(command_symb)
46
+ def external_command(command_sym, command_args, check_code: true)
47
+ assert_values(command_sym, EXTERNAL_TOOLS){'command'}
43
48
  # build command line, and quote special characters
44
- command = command_args.clone.unshift(command_symb).map{|i| shell_quote(i.to_s)}.join(' ')
45
- Log.log.debug{"cmd=#{command}".blue}
46
- # capture3: only in ruby2+
47
- if Open3.respond_to?(:capture3)
48
- stdout, stderr, exit_status = Open3.capture3(command)
49
- else
50
- stderr = '<merged with stdout>'
51
- stdout = %x(#{command} 2>&1)
52
- exit_status = $CHILD_STATUS
53
- end
54
- if BASH_EXIT_NOT_FOUND.eql?(exit_status)
55
- raise "Error: #{command_symb} is not in the PATH"
56
- end
57
- unless exit_status.success?
58
- Log.log.error{"command line: #{command}"}
59
- Log.log.error{"Error code: #{exit_status}"}
49
+ command_line = command_args.clone.unshift(command_sym).map{|i| shell_quote(i.to_s)}.join(' ')
50
+ Log.log.debug{"cmd=#{command_line}".blue}
51
+ stdout, stderr, status = Open3.capture3(command_line)
52
+ if check_code && !status.success?
53
+ Log.log.error{"status: #{status}"}
60
54
  Log.log.error{"stdout: #{stdout}"}
61
55
  Log.log.error{"stderr: #{stderr}"}
62
- raise "#{command_symb} error #{exit_status}"
56
+ raise "#{command_sym} error #{status}"
63
57
  end
64
- return {status: exit_status, stdout: stdout}
58
+ return {status: status, stdout: stdout}
65
59
  end
66
60
 
67
61
  def ffmpeg(a)
68
- raise 'error: hash expected' unless a.is_a?(Hash)
62
+ assert_type(a, Hash)
69
63
  # input_file,input_args,output_file,output_args
70
64
  a[:gl_p] ||= [
71
65
  '-y', # overwrite output without asking
@@ -73,7 +67,7 @@ module Aspera
73
67
  ]
74
68
  a[:in_p] ||= []
75
69
  a[:out_p] ||= []
76
- raise "wrong params (#{a.keys.sort})" unless %i[gl_p in_f in_p out_f out_p].eql?(a.keys.sort)
70
+ assert(%i[gl_p in_f in_p out_f out_p].eql?(a.keys.sort)){"wrong params (#{a.keys.sort})"}
77
71
  external_command(:ffmpeg, [a[:gl_p], a[:in_p], '-i', a[:in_f], a[:out_p], a[:out_f]].flatten)
78
72
  end
79
73
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'aspera/log'
4
+ require 'aspera/assert'
3
5
  require 'uri'
4
6
  require 'resolv'
5
7
 
@@ -9,7 +11,7 @@ module URI
9
11
  alias_method :find_proxy_orig, :find_proxy
10
12
  class << self
11
13
  def register_proxy_finder
12
- raise 'mandatory block missing' unless Kernel.block_given?
14
+ assert(block_given?)
13
15
  # overload the method in URI : call user's provided block and fallback to original method
14
16
  define_method(:find_proxy) {|env_vars=ENV| yield(to_s) || find_proxy_orig(env_vars)}
15
17
  end
@@ -107,11 +109,12 @@ END_OF_JAVASCRIPT
107
109
  parts = item.strip.split
108
110
  case parts.shift
109
111
  when 'DIRECT'
110
- raise 'DIRECT has no param' unless parts.empty?
112
+ assert(parts.empty?){'DIRECT has no param'}
111
113
  Log.log.debug('ignoring proxy DIRECT')
112
114
  when 'PROXY'
113
115
  addr_port = parts.shift
114
- raise 'PROXY shall have one param' unless addr_port.is_a?(String) && parts.empty?
116
+ assert_type(addr_port, String)
117
+ assert(parts.empty?){'PROXY shall have one param'}
115
118
  begin
116
119
  # PAC proxy addresses are <host>:<port>
117
120
  if /:[0-9]+$/.match?(addr_port)
data/lib/aspera/rest.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'aspera/log'
4
+ require 'aspera/assert'
4
5
  require 'aspera/oauth'
5
6
  require 'aspera/rest_error_analyzer'
6
7
  require 'aspera/hash_ext'
@@ -10,7 +11,6 @@ require 'net/https'
10
11
  require 'json'
11
12
  require 'base64'
12
13
  require 'cgi'
13
- require 'ruby-progressbar'
14
14
 
15
15
  # add cancel method to http
16
16
  class Net::HTTP::Cancel < Net::HTTPRequest # rubocop:disable Style/ClassAndModuleChildren
@@ -26,34 +26,25 @@ module Aspera
26
26
  class Rest
27
27
  # global settings also valid for any subclass
28
28
  @@global = { # rubocop:disable Style/ClassVars
29
- debug: false,
30
- # true if https ignore certificate
31
- user_agent: 'Ruby',
32
- download_partial_suffix: '.http_partial',
33
- # a lambda which takes the Net::HTTP as arg, use this to change parameters
34
- session_cb: nil,
35
- proxy_user: nil,
36
- proxy_pass: nil
29
+ user_agent: 'Ruby', # goes to HTTP request header: 'User-Agent'
30
+ download_partial_suffix: '.http_partial', # suffix for partial download
31
+ session_cb: nil, # a lambda which takes the Net::HTTP as arg, use this to change parameters
32
+ progress_bar: nil # progress bar object
37
33
  }
38
34
 
35
+ # flag for array parameters prefixed with []
39
36
  ARRAY_PARAMS = '[]'
40
37
 
41
38
  private_constant :ARRAY_PARAMS
42
39
 
43
- # error message when entity not found
40
+ # error message when entity not found (TODO: use specific exception)
44
41
  ENTITY_NOT_FOUND = 'No such'
45
42
 
46
- class << self
47
- # define accessors
48
- @@global.each_key do |p|
49
- define_method(p){@@global[p]}
50
- define_method("#{p}=") do |val|
51
- Log.log.debug{"#{p} => #{val}".red}
52
- @@global[p] = val
53
- end
54
- end
43
+ # Content-Type that are JSON
44
+ JSON_DECODE = ['application/json', 'application/vnd.api+json', 'application/x-javascript'].freeze
55
45
 
56
- def basic_creds(user, pass); return "Basic #{Base64.strict_encode64("#{user}:#{pass}")}"; end
46
+ class << self
47
+ def basic_token(user, pass); return "Basic #{Base64.strict_encode64("#{user}:#{pass}")}"; end
57
48
 
58
49
  # used to build a parameter list prefixed with "[]"
59
50
  # @param values [Array] list of values
@@ -61,47 +52,92 @@ module Aspera
61
52
  return [ARRAY_PARAMS].concat(values)
62
53
  end
63
54
 
55
+ def array_params?(values)
56
+ return values.first.eql?(ARRAY_PARAMS)
57
+ end
58
+
64
59
  # build URI from URL and parameters and check it is http or https
65
60
  def build_uri(url, params=nil)
66
61
  uri = URI.parse(url)
67
- raise "REST endpoint shall be http/s not #{uri.scheme}" unless %w[http https].include?(uri.scheme)
68
- if !params.nil?
69
- # support array url params, there is no standard. Either p[]=1&p[]=2, or p=1&p=2
70
- if params.is_a?(Hash)
71
- orig = params
72
- params = []
73
- orig.each do |k, v|
74
- case v
75
- when Array
76
- suffix = v.first.eql?(ARRAY_PARAMS) ? v.shift : ''
77
- v.each do |e|
78
- params.push([k.to_s + suffix, e])
79
- end
80
- else
81
- params.push([k, v])
82
- end
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?
67
+ query = []
68
+ params.each do |k, v|
69
+ case v
70
+ when Array
71
+ # support array url params, there is no standard. Either p[]=1&p[]=2, or p=1&p=2
72
+ suffix = array_params?(v) ? v.shift : ''
73
+ v.each do |e|
74
+ query.push(["#{k}#{suffix}", e])
83
75
  end
76
+ else
77
+ query.push([k, v])
84
78
  end
85
- # CGI.unescape to transform back %5D into []
86
- uri.query = CGI.unescape(URI.encode_www_form(params))
87
79
  end
80
+ # [] is allowed in url parameters
81
+ uri.query = URI.encode_www_form(query).gsub('%5B%5D=', '[]=')
88
82
  return uri
89
83
  end
90
84
 
85
+ def decode_query(query)
86
+ URI.decode_www_form(query).each_with_object({}){|v, h|h[v.first] = v.last }
87
+ end
88
+
89
+ # start a HTTP/S session, also used for web sockets
90
+ # @param base_url [String] base url of HTTP/S session
91
+ # @return [Net::HTTP] a started HTTP session
91
92
  def start_http_session(base_url)
92
93
  uri = build_uri(base_url)
93
94
  # this honors http_proxy env var
94
95
  http_session = Net::HTTP.new(uri.host, uri.port)
95
- http_session.proxy_user = proxy_user
96
- http_session.proxy_pass = proxy_pass
97
96
  http_session.use_ssl = uri.scheme.eql?('https')
98
- http_session.set_debug_output($stdout) if debug
99
97
  # set http options in callback, such as timeout and cert. verification
100
- session_cb&.call(http_session)
98
+ @@global[:session_cb]&.call(http_session)
101
99
  # manually start session for keep alive (if supported by server, else, session is closed every time)
102
100
  http_session.start
103
101
  return http_session
104
102
  end
103
+
104
+ # get Net::HTTP underlying socket i/o
105
+ # little hack, handy because HTTP debug, proxy, etc... will be available
106
+ # used implement web sockets after `start_http_session`
107
+ def io_http_session(http_session)
108
+ assert_type(http_session, Net::HTTP)
109
+ # Net::BufferedIO in net/protocol.rb
110
+ result = http_session.instance_variable_get(:@socket)
111
+ assert(!result.nil?){"no socket for #{http_session}"}
112
+ return result
113
+ end
114
+
115
+ # @return [String] PEM certificates of remote server
116
+ def remote_certificates(url)
117
+ # initiate a session to retrieve remote certificate
118
+ http_session = Rest.start_http_session(url)
119
+ begin
120
+ # retrieve underlying openssl socket
121
+ return Rest.io_http_session(http_session).io.peer_cert_chain.reverse.map(&:to_pem).join("\n")
122
+ rescue
123
+ return http_session.peer_cert.to_pem
124
+ ensure
125
+ http_session.finish
126
+ end
127
+ end
128
+
129
+ # set global parameters
130
+ def set_parameters(**options)
131
+ options.each do |key, value|
132
+ assert(@@global.key?(key)){"unknown Rest option #{key}"}
133
+ @@global[key] = value
134
+ end
135
+ end
136
+
137
+ # @return [String] HTTP agent name
138
+ def user_agent
139
+ return @@global[:user_agent]
140
+ end
105
141
  end
106
142
 
107
143
  private
@@ -120,7 +156,7 @@ module Aspera
120
156
 
121
157
  def oauth
122
158
  if @oauth.nil?
123
- raise 'ERROR: no OAuth defined' unless @params[:auth][:type].eql?(:oauth2)
159
+ assert(@params[:auth][:type].eql?(:oauth2)){'no OAuth defined'}
124
160
  @oauth = Oauth.new(@params[:auth])
125
161
  end
126
162
  return @oauth
@@ -128,10 +164,10 @@ module Aspera
128
164
 
129
165
  # @param a_rest_params [Hash] default call parameters (merged at call)
130
166
  def initialize(a_rest_params)
131
- raise 'ERROR: expecting Hash' unless a_rest_params.is_a?(Hash)
132
- raise 'ERROR: expecting base_url' unless a_rest_params[:base_url].is_a?(String)
167
+ assert_type(a_rest_params, Hash)
168
+ assert_type(a_rest_params[:base_url], String)
133
169
  @params = a_rest_params.clone
134
- Log.dump('REST params', @params)
170
+ Log.log.debug{Log.dump('REST params', @params)}
135
171
  # base url without trailing slashes (note: string may be frozen)
136
172
  @params[:base_url] = @params[:base_url].gsub(%r{/+$}, '')
137
173
  @http_session = nil
@@ -139,11 +175,11 @@ module Aspera
139
175
  @params[:auth] ||= {type: :none}
140
176
  @params[:not_auth_codes] ||= ['401']
141
177
  @oauth = nil
142
- Log.dump('REST params(2)', @params)
178
+ Log.log.debug{Log.dump('REST params(2)', @params)}
143
179
  end
144
180
 
145
181
  def oauth_token(force_refresh: false)
146
- raise "ERROR: expecting boolean, have #{force_refresh}" unless [true, false].include?(force_refresh)
182
+ assert_values(force_refresh, [true, false])
147
183
  return oauth.get_authorization(use_refresh_token: force_refresh)
148
184
  end
149
185
 
@@ -159,8 +195,8 @@ module Aspera
159
195
  raise "unsupported operation : #{call_data[:operation]}"
160
196
  end
161
197
  if call_data.key?(:json_params) && !call_data[:json_params].nil?
162
- req.body = JSON.generate(call_data[:json_params])
163
- Log.dump('body JSON data', call_data[:json_params])
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])}
164
200
  req['Content-Type'] = 'application/json'
165
201
  # call_data[:headers]['Accept']='application/json'
166
202
  end
@@ -181,6 +217,7 @@ module Aspera
181
217
  end
182
218
  # :type = :basic
183
219
  req.basic_auth(call_data[:auth][:username], call_data[:auth][:password]) if call_data[:auth][:type].eql?(:basic)
220
+ Log.log.debug{Log.dump(:req_body, req.body)}
184
221
  return req
185
222
  end
186
223
 
@@ -203,14 +240,14 @@ module Aspera
203
240
  # :type (:none, :basic, :oauth2, :url)
204
241
  # :username [:basic]
205
242
  # :password [:basic]
206
- # :url_creds [:url] a hash
243
+ # :url_query [:url] a hash
207
244
  # :* [:oauth2] see Oauth class
208
245
  def call(call_data)
209
- raise "Hash call parameter is required (#{call_data.class})" unless call_data.is_a?(Hash)
246
+ assert_type(call_data, Hash)
210
247
  call_data[:subpath] = '' if call_data[:subpath].nil?
211
248
  Log.log.debug{"accessing #{call_data[:subpath]}".red.bold.bg_green}
212
249
  call_data[:headers] ||= {}
213
- call_data[:headers]['User-Agent'] ||= self.class.user_agent
250
+ call_data[:headers]['User-Agent'] ||= @@global[:user_agent]
214
251
  # defaults from @params are overridden by call data
215
252
  call_data = @params.deep_merge(call_data)
216
253
  case call_data[:auth][:type]
@@ -223,10 +260,10 @@ module Aspera
223
260
  call_data[:headers]['Authorization'] = oauth_token unless call_data[:headers].key?('Authorization')
224
261
  when :url
225
262
  call_data[:url_params] ||= {}
226
- call_data[:auth][:url_creds].each do |key, value|
263
+ call_data[:auth][:url_query].each do |key, value|
227
264
  call_data[:url_params][key] = value
228
265
  end
229
- else raise "unsupported auth type: [#{call_data[:auth][:type]}]"
266
+ else error_unexpected_value(call_data[:auth][:type])
230
267
  end
231
268
  req = build_request(call_data)
232
269
  Log.log.debug{"call_data = #{call_data}"}
@@ -238,16 +275,18 @@ module Aspera
238
275
  # initialize with number of initial retries allowed, nil gives zero
239
276
  tries_remain_redirect = call_data[:redirect_max].to_i if tries_remain_redirect.nil?
240
277
  Log.log.debug("send request (retries=#{tries_remain_redirect})")
278
+ result_mime = nil
279
+ file_saved = false
241
280
  # make http request (pipelined)
242
281
  http_session.request(req) do |response|
243
282
  result[:http] = response
244
- if !call_data[:save_to_file].nil? && result[:http].code.to_s.start_with?('2')
283
+ result_mime = (result[:http]['Content-Type'] || 'text/plain').split(';').first.downcase
284
+ # JSON data needs to be parsed, in case it contains an error code
285
+ if !call_data[:save_to_file].nil? &&
286
+ result[:http].code.to_s.start_with?('2') &&
287
+ !result[:http]['Content-Length'].nil? &&
288
+ !JSON_DECODE.include?(result_mime)
245
289
  total_size = result[:http]['Content-Length'].to_i
246
- progress = ProgressBar.create(
247
- format: '%a %B %p%% %r KB/sec %e',
248
- rate_scale: lambda{|rate|rate / 1024},
249
- title: 'progress',
250
- total: total_size)
251
290
  Log.log.debug('before write file')
252
291
  target_file = call_data[:save_to_file]
253
292
  # override user's path to path in header
@@ -255,34 +294,37 @@ module Aspera
255
294
  target_file = File.join(File.dirname(target_file), m[1])
256
295
  end
257
296
  # download with temp filename
258
- target_file_tmp = "#{target_file}#{self.class.download_partial_suffix}"
297
+ target_file_tmp = "#{target_file}#{@@global[:download_partial_suffix]}"
259
298
  Log.log.debug{"saving to: #{target_file}"}
299
+ written_size = 0
300
+ @@global[:progress_bar]&.event(session_id: 1, type: :session_start)
301
+ @@global[:progress_bar]&.event(session_id: 1, type: :session_size, info: total_size)
260
302
  File.open(target_file_tmp, 'wb') do |file|
261
303
  result[:http].read_body do |fragment|
262
304
  file.write(fragment)
263
- new_process = progress.progress + fragment.length
264
- new_process = total_size if new_process > total_size
265
- progress.progress = new_process
305
+ written_size += fragment.length
306
+ @@global[:progress_bar]&.event(session_id: 1, type: :transfer, info: written_size)
266
307
  end
267
308
  end
309
+ @@global[:progress_bar]&.event(session_id: 1, type: :end)
268
310
  # rename at the end
269
311
  File.rename(target_file_tmp, target_file)
270
- progress = nil
312
+ file_saved = true
271
313
  end # save_to_file
272
314
  end
273
- # sometimes there is a UTF8 char (e.g. (c) )
274
- result[:http].body.force_encoding('UTF-8') if result[:http].body.is_a?(String)
275
- Log.log.debug{"result: body=#{result[:http].body}"}
276
- result_mime = (result[:http]['Content-Type'] || 'text/plain').split(';').first
315
+ # sometimes there is a UTF8 char (e.g. (c) ), TODO : related to mime type encoding ?
316
+ # result[:http].body.force_encoding('UTF-8') if result[:http].body.is_a?(String)
317
+ # Log.log.debug{"result: body=#{result[:http].body}"}
277
318
  result[:data] = case result_mime
278
- when 'application/json', 'application/vnd.api+json'
279
- JSON.parse(result[:http].body) rescue nil
319
+ when *JSON_DECODE
320
+ JSON.parse(result[:http].body) rescue result[:http].body
280
321
  else # when 'text/plain'
281
322
  result[:http].body
282
323
  end
283
- Log.dump("result: parsed: #{result_mime}", result[:data])
324
+ Log.log.debug{Log.dump("result: parsed: #{result_mime}", result[:data])}
284
325
  Log.log.debug{"result: code=#{result[:http].code}"}
285
326
  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?
286
328
  rescue RestCallError => e
287
329
  # not authorized: oauth token expired
288
330
  if call_data[:not_auth_codes].include?(result[:http].code.to_s) && call_data[:auth][:type].eql?(:oauth2)
@@ -303,7 +345,12 @@ module Aspera
303
345
  tries_remain_redirect -= 1
304
346
  current_uri = URI.parse(call_data[:base_url])
305
347
  new_url = e.response['location']
306
- new_url = "#{current_uri.scheme}:#{new_url}" unless new_url.start_with?('http')
348
+ # special case: relative redirect
349
+ if URI.parse(new_url).host.nil?
350
+ # 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
307
354
  Log.log.info{"URL is moved: #{new_url}"}
308
355
  redirection_uri = URI.parse(new_url)
309
356
  call_data[:base_url] = new_url
@@ -333,16 +380,16 @@ module Aspera
333
380
  return call({operation: 'POST', subpath: subpath, headers: {'Accept' => 'application/json'}, encoding => params})
334
381
  end
335
382
 
336
- def read(subpath, args=nil)
337
- return call({operation: 'GET', subpath: subpath, headers: {'Accept' => 'application/json'}, url_params: args})
383
+ def read(subpath, options=nil)
384
+ return call({operation: 'GET', subpath: subpath, headers: {'Accept' => 'application/json'}, url_params: options})
338
385
  end
339
386
 
340
387
  def update(subpath, params)
341
388
  return call({operation: 'PUT', subpath: subpath, headers: {'Accept' => 'application/json'}, json_params: params})
342
389
  end
343
390
 
344
- def delete(subpath, args=nil)
345
- return call({operation: 'DELETE', subpath: subpath, headers: {'Accept' => 'application/json'}, url_params: args})
391
+ def delete(subpath, params=nil)
392
+ return call({operation: 'DELETE', subpath: subpath, headers: {'Accept' => 'application/json'}, url_params: params})
346
393
  end
347
394
 
348
395
  def cancel(subpath)
@@ -355,11 +402,11 @@ module Aspera
355
402
  # @param options additional search options
356
403
  def lookup_by_name(subpath, search_name, options={})
357
404
  # returns entities whose name contains value (case insensitive)
358
- matching_items = read(subpath, options.merge({'q' => CGI.escape(search_name)}))[:data]
405
+ matching_items = read(subpath, options.merge({'q' => search_name}))[:data]
359
406
  # API style: {totalcount:, ...} cspell: disable-line
360
407
  # TODO: not generic enough ? move somewhere ? inheritance ?
361
408
  matching_items = matching_items[subpath] if matching_items.is_a?(Hash)
362
- raise "Internal error: expecting array, have #{matching_items.class}" unless matching_items.is_a?(Array)
409
+ assert_type(matching_items, Array)
363
410
  case matching_items.length
364
411
  when 1 then return matching_items.first
365
412
  when 0 then raise %Q{#{ENTITY_NOT_FOUND} #{subpath}: "#{search_name}"}
@@ -8,7 +8,7 @@ module Aspera
8
8
  # @param req HTTP Request object
9
9
  # @param resp HTTP Response object
10
10
  # @param msg Error message
11
- def initialize(req, resp, msg)
11
+ def initialize(msg, req=nil, resp=nil)
12
12
  @request = req
13
13
  @response = resp
14
14
  super(msg)
@@ -27,6 +27,7 @@ module Aspera
27
27
  # Analyzes REST call response and raises a RestCallError exception
28
28
  # if HTTP result code is not 2XX
29
29
  def raise_on_error(req, res)
30
+ Log.log.debug{"raise_on_error #{req.method} #{req.path} #{res[:http].code}"}
30
31
  call_context = {
31
32
  messages: [],
32
33
  request: req,
@@ -44,7 +45,7 @@ module Aspera
44
45
  Log.log.error{"ERROR in handler:\n#{e.message}\n#{e.backtrace}"}
45
46
  end
46
47
  end
47
- raise RestCallError.new(call_context[:request], call_context[:response], call_context[:messages].join("\n")) unless call_context[:messages].empty?
48
+ raise RestCallError.new(call_context[:messages].join("\n"), call_context[:request], call_context[:response]) unless call_context[:messages].empty?
48
49
  end
49
50
 
50
51
  # add a new error handler (done at application initialization)
@@ -59,21 +60,21 @@ module Aspera
59
60
  # add a simple error handler
60
61
  # check that key exists and is string under specified path (hash)
61
62
  # adds other keys as secondary information
62
- def add_simple_handler(name, *args)
63
+ # @param name [String] name of error handler (for logs)
64
+ # @param always [boolean] if true, always add error message, even if response code is 2XX
65
+ # @param path [Array] path to error message in response
66
+ def add_simple_handler(name:, always: false, path:)
67
+ path.freeze
63
68
  add_handler(name) do |type, call_context|
64
- # need to clone because we modify and same array is used subsequently
65
- path = args.clone
66
- # Log.log.debug{"path=#{path}"}
67
- # if last in path is boolean it tells if the error is only with http error code or always
68
- always = [true, false].include?(path.last) ? path.pop : false
69
69
  if call_context[:data].is_a?(Hash) && (!call_context[:response].code.start_with?('2') || always)
70
- msg_key = path.pop
71
- # dig and find sub entry corresponding to path in deep hash
72
- error_struct = path.inject(call_context[:data]) { |sub_hash, key| sub_hash.respond_to?(:keys) ? sub_hash[key] : nil }
73
- if error_struct.is_a?(Hash) && error_struct[msg_key].is_a?(String)
74
- RestErrorAnalyzer.add_error(call_context, type, error_struct[msg_key])
70
+ # Log.log.debug{"simple_handler: #{type} #{path} #{path.last}"}
71
+ # dig and find hash containing error message
72
+ error_struct = path.length.eql?(1) ? call_context[:data] : call_context[:data].dig(*path[0..-2])
73
+ # Log.log.debug{"found: #{error_struct.class} #{error_struct}"}
74
+ if error_struct.is_a?(Hash) && error_struct[path.last].is_a?(String)
75
+ RestErrorAnalyzer.add_error(call_context, type, error_struct[path.last])
75
76
  error_struct.each do |k, v|
76
- next if k.eql?(msg_key)
77
+ next if k.eql?(path.last)
77
78
  RestErrorAnalyzer.add_error(call_context, "#{type}(sub)", "#{k}: #{v}") if [String, Integer].include?(v.class)
78
79
  end
79
80
  end
@@ -89,11 +90,12 @@ module Aspera
89
90
  # @param msg one error message to add to list
90
91
  def add_error(call_context, type, msg)
91
92
  call_context[:messages].push(msg)
93
+ Log.log.trace1{"Found error: #{type}: #{msg}"}
92
94
  log_file = instance.log_file
93
95
  # log error for further analysis (file must exist to activate)
94
96
  return if log_file.nil? || !File.exist?(log_file)
95
97
  File.open(log_file, 'a+') do |f|
96
- f.write("\n=#{type}=====\n#{call_context[:request].method} #{call_context[:request].path}\n#{call_context[:response].code}\n"\
98
+ f.write("\n=#{type}=====\n#{call_context[:request].method} #{call_context[:request].path}\n#{call_context[:response].code}\n" \
97
99
  "#{JSON.generate(call_context[:data])}\n#{call_context[:messages].join("\n")}")
98
100
  end
99
101
  end