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.
- 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
|