aspera-cli 4.14.0 → 4.16.0

Sign up to get free protection for your applications and to get access to all the features.
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