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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/BUGS.md +29 -3
- data/CHANGELOG.md +300 -185
- data/CONTRIBUTING.md +74 -23
- data/README.md +2346 -1619
- data/bin/ascli +16 -25
- data/bin/asession +15 -15
- data/examples/dascli +2 -2
- data/examples/proxy.pac +1 -1
- data/lib/aspera/aoc.rb +216 -150
- data/lib/aspera/ascmd.rb +25 -18
- data/lib/aspera/assert.rb +45 -0
- data/lib/aspera/cli/basic_auth_plugin.rb +9 -6
- data/lib/aspera/cli/error.rb +17 -0
- data/lib/aspera/cli/extended_value.rb +51 -16
- data/lib/aspera/cli/formatter.rb +276 -174
- data/lib/aspera/cli/hints.rb +81 -0
- data/lib/aspera/cli/main.rb +114 -147
- data/lib/aspera/cli/manager.rb +181 -136
- data/lib/aspera/cli/plugin.rb +82 -64
- data/lib/aspera/cli/plugins/alee.rb +0 -1
- data/lib/aspera/cli/plugins/aoc.rb +327 -331
- data/lib/aspera/cli/plugins/ats.rb +12 -8
- data/lib/aspera/cli/plugins/bss.rb +2 -2
- data/lib/aspera/cli/plugins/config.rb +575 -439
- data/lib/aspera/cli/plugins/console.rb +40 -0
- data/lib/aspera/cli/plugins/cos.rb +4 -5
- data/lib/aspera/cli/plugins/faspex.rb +111 -92
- data/lib/aspera/cli/plugins/faspex5.rb +245 -182
- data/lib/aspera/cli/plugins/node.rb +239 -160
- data/lib/aspera/cli/plugins/orchestrator.rb +56 -19
- data/lib/aspera/cli/plugins/preview.rb +54 -38
- data/lib/aspera/cli/plugins/server.rb +63 -20
- data/lib/aspera/cli/plugins/shares.rb +64 -38
- data/lib/aspera/cli/sync_actions.rb +68 -0
- data/lib/aspera/cli/transfer_agent.rb +64 -67
- data/lib/aspera/cli/transfer_progress.rb +73 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/colors.rb +3 -1
- data/lib/aspera/command_line_builder.rb +27 -22
- data/lib/aspera/cos_node.rb +6 -4
- data/lib/aspera/coverage.rb +22 -0
- data/lib/aspera/data_repository.rb +33 -2
- data/lib/aspera/environment.rb +21 -8
- data/lib/aspera/fasp/agent_alpha.rb +116 -0
- data/lib/aspera/fasp/agent_base.rb +40 -76
- data/lib/aspera/fasp/agent_connect.rb +21 -22
- data/lib/aspera/fasp/agent_direct.rb +169 -179
- data/lib/aspera/fasp/agent_httpgw.rb +200 -195
- data/lib/aspera/fasp/agent_node.rb +43 -35
- data/lib/aspera/fasp/agent_trsdk.rb +124 -41
- data/lib/aspera/fasp/error_info.rb +2 -2
- data/lib/aspera/fasp/faux_file.rb +52 -0
- data/lib/aspera/fasp/installation.rb +89 -191
- data/lib/aspera/fasp/management.rb +249 -0
- data/lib/aspera/fasp/parameters.rb +86 -47
- data/lib/aspera/fasp/parameters.yaml +75 -8
- data/lib/aspera/fasp/products.rb +162 -0
- data/lib/aspera/fasp/resume_policy.rb +7 -5
- data/lib/aspera/fasp/sync.rb +273 -0
- data/lib/aspera/fasp/transfer_spec.rb +10 -8
- data/lib/aspera/fasp/uri.rb +6 -6
- data/lib/aspera/faspex_gw.rb +11 -8
- data/lib/aspera/faspex_postproc.rb +8 -7
- data/lib/aspera/hash_ext.rb +2 -2
- data/lib/aspera/id_generator.rb +3 -1
- data/lib/aspera/json_rpc.rb +51 -0
- data/lib/aspera/keychain/encrypted_hash.rb +46 -11
- data/lib/aspera/keychain/macos_security.rb +15 -13
- data/lib/aspera/line_logger.rb +23 -0
- data/lib/aspera/log.rb +61 -19
- data/lib/aspera/nagios.rb +7 -2
- data/lib/aspera/node.rb +105 -21
- data/lib/aspera/node_simulator.rb +214 -0
- data/lib/aspera/oauth.rb +57 -36
- data/lib/aspera/open_application.rb +4 -4
- data/lib/aspera/persistency_action_once.rb +13 -14
- data/lib/aspera/persistency_folder.rb +5 -4
- data/lib/aspera/preview/file_types.rb +56 -268
- data/lib/aspera/preview/generator.rb +28 -39
- data/lib/aspera/preview/options.rb +2 -0
- data/lib/aspera/preview/terminal.rb +36 -16
- data/lib/aspera/preview/utils.rb +23 -29
- data/lib/aspera/proxy_auto_config.rb +6 -3
- data/lib/aspera/rest.rb +127 -80
- data/lib/aspera/rest_call_error.rb +1 -1
- data/lib/aspera/rest_error_analyzer.rb +16 -14
- data/lib/aspera/rest_errors_aspera.rb +39 -34
- data/lib/aspera/secret_hider.rb +18 -17
- data/lib/aspera/ssh.rb +10 -5
- data/lib/aspera/temp_file_manager.rb +11 -4
- data/lib/aspera/web_auth.rb +10 -7
- data/lib/aspera/web_server_simple.rb +11 -5
- data.tar.gz.sig +0 -0
- metadata +108 -39
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/listener/line_dump.rb +0 -19
- data/lib/aspera/cli/listener/logger.rb +0 -22
- data/lib/aspera/cli/listener/progress.rb +0 -50
- data/lib/aspera/cli/listener/progress_multi.rb +0 -84
- data/lib/aspera/cli/plugins/sync.rb +0 -44
- data/lib/aspera/fasp/listener.rb +0 -13
- data/lib/aspera/sync.rb +0 -213
data/lib/aspera/preview/utils.rb
CHANGED
@@ -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, :
|
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
|
-
|
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 |
|
34
|
-
external_command(
|
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(
|
42
|
-
|
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
|
-
|
45
|
-
Log.log.debug{"cmd=#{
|
46
|
-
|
47
|
-
if
|
48
|
-
|
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 "#{
|
56
|
+
raise "#{command_sym} error #{status}"
|
63
57
|
end
|
64
|
-
return {status:
|
58
|
+
return {status: status, stdout: stdout}
|
65
59
|
end
|
66
60
|
|
67
61
|
def ffmpeg(a)
|
68
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
30
|
-
#
|
31
|
-
|
32
|
-
|
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
|
-
|
47
|
-
|
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
|
-
|
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
|
-
|
68
|
-
if
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
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
|
-
|
132
|
-
|
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
|
-
|
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
|
-
# :
|
243
|
+
# :url_query [:url] a hash
|
207
244
|
# :* [:oauth2] see Oauth class
|
208
245
|
def call(call_data)
|
209
|
-
|
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'] ||=
|
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][:
|
263
|
+
call_data[:auth][:url_query].each do |key, value|
|
227
264
|
call_data[:url_params][key] = value
|
228
265
|
end
|
229
|
-
else
|
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
|
-
|
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}#{
|
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
|
-
|
264
|
-
|
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
|
-
|
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
|
279
|
-
JSON.parse(result[:http].body) rescue
|
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
|
-
|
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,
|
337
|
-
return call({operation: 'GET', subpath: subpath, headers: {'Accept' => 'application/json'}, url_params:
|
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,
|
345
|
-
return call({operation: 'DELETE', subpath: subpath, headers: {'Accept' => 'application/json'}, url_params:
|
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' =>
|
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
|
-
|
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}"}
|
@@ -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[:
|
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
|
-
|
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
|
-
|
71
|
-
# dig and find
|
72
|
-
error_struct = path.
|
73
|
-
|
74
|
-
|
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?(
|
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
|