aspera-cli 4.21.2 → 4.23.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 +1 -1
- data/CHANGELOG.md +402 -374
- data/CONTRIBUTING.md +6 -10
- data/README.md +1018 -687
- data/lib/aspera/agent/base.rb +9 -5
- data/lib/aspera/agent/connect.rb +30 -28
- data/lib/aspera/agent/desktop.rb +29 -25
- data/lib/aspera/agent/direct.rb +137 -125
- data/lib/aspera/agent/httpgw.rb +22 -26
- data/lib/aspera/agent/node.rb +14 -11
- data/lib/aspera/agent/transferd.rb +6 -2
- data/lib/aspera/api/aoc.rb +15 -18
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/httpgw.rb +15 -7
- data/lib/aspera/api/node.rb +6 -4
- data/lib/aspera/ascmd.rb +17 -9
- data/lib/aspera/ascp/installation.rb +21 -19
- data/lib/aspera/ascp/management.rb +1 -1
- data/lib/aspera/assert.rb +14 -5
- data/lib/aspera/cli/error.rb +2 -2
- data/lib/aspera/cli/extended_value.rb +38 -19
- data/lib/aspera/cli/formatter.rb +48 -48
- data/lib/aspera/cli/hints.rb +10 -2
- data/lib/aspera/cli/main.rb +190 -168
- data/lib/aspera/cli/manager.rb +16 -16
- data/lib/aspera/cli/plugin.rb +24 -21
- data/lib/aspera/cli/plugin_factory.rb +1 -1
- data/lib/aspera/cli/plugins/alee.rb +1 -1
- data/lib/aspera/cli/plugins/aoc.rb +173 -126
- data/lib/aspera/cli/plugins/ats.rb +19 -17
- data/lib/aspera/cli/plugins/config.rb +87 -98
- data/lib/aspera/cli/plugins/console.rb +5 -3
- data/lib/aspera/cli/plugins/faspex.rb +39 -35
- data/lib/aspera/cli/plugins/faspex5.rb +104 -80
- data/lib/aspera/cli/plugins/faspio.rb +13 -1
- data/lib/aspera/cli/plugins/httpgw.rb +13 -1
- data/lib/aspera/cli/plugins/node.rb +336 -205
- data/lib/aspera/cli/plugins/orchestrator.rb +34 -40
- data/lib/aspera/cli/plugins/preview.rb +3 -3
- data/lib/aspera/cli/plugins/server.rb +7 -6
- data/lib/aspera/cli/plugins/shares.rb +5 -5
- data/lib/aspera/cli/sync_actions.rb +19 -18
- data/lib/aspera/cli/transfer_agent.rb +11 -15
- data/lib/aspera/cli/transfer_progress.rb +2 -2
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/command_line_builder.rb +116 -95
- data/lib/aspera/coverage.rb +4 -3
- data/lib/aspera/data_repository.rb +1 -0
- data/lib/aspera/environment.rb +7 -6
- data/lib/aspera/faspex_gw.rb +14 -14
- data/lib/aspera/faspex_postproc.rb +7 -6
- data/lib/aspera/hash_ext.rb +2 -2
- data/lib/aspera/json_rpc.rb +1 -1
- data/lib/aspera/keychain/encrypted_hash.rb +47 -34
- data/lib/aspera/keychain/factory.rb +41 -0
- data/lib/aspera/keychain/hashicorp_vault.rb +71 -0
- data/lib/aspera/keychain/macos_security.rb +19 -11
- data/lib/aspera/log.rb +29 -34
- data/lib/aspera/nagios.rb +6 -6
- data/lib/aspera/node_simulator.rb +8 -8
- data/lib/aspera/oauth/base.rb +10 -6
- data/lib/aspera/oauth/factory.rb +6 -6
- data/lib/aspera/oauth/url_json.rb +6 -6
- data/lib/aspera/persistency_action_once.rb +6 -4
- data/lib/aspera/persistency_folder.rb +2 -2
- data/lib/aspera/preview/file_types.rb +40 -33
- data/lib/aspera/preview/generator.rb +1 -1
- data/lib/aspera/preview/options.rb +16 -16
- data/lib/aspera/preview/terminal.rb +3 -3
- data/lib/aspera/preview/utils.rb +11 -13
- data/lib/aspera/products/connect.rb +2 -1
- data/lib/aspera/products/desktop.rb +1 -1
- data/lib/aspera/products/transferd.rb +1 -1
- data/lib/aspera/proxy_auto_config.rb +2 -2
- data/lib/aspera/rest.rb +70 -50
- data/lib/aspera/rest_error_analyzer.rb +1 -0
- data/lib/aspera/rest_errors_aspera.rb +1 -1
- data/lib/aspera/secret_hider.rb +5 -5
- data/lib/aspera/ssh.rb +5 -5
- data/lib/aspera/temp_file_manager.rb +1 -0
- data/lib/aspera/timer_limiter.rb +7 -5
- data/lib/aspera/transfer/async_conf.schema.yaml +716 -0
- data/lib/aspera/transfer/convert.rb +29 -0
- data/lib/aspera/transfer/error_info.rb +66 -66
- data/lib/aspera/transfer/parameters.rb +13 -68
- data/lib/aspera/transfer/spec.rb +5 -6
- data/lib/aspera/transfer/spec.schema.yaml +753 -0
- data/lib/aspera/transfer/spec_doc.rb +62 -0
- data/lib/aspera/transfer/sync.rb +37 -76
- data/lib/aspera/transfer/sync_instance.schema.yaml +20 -0
- data/lib/aspera/transfer/sync_session.schema.yaml +86 -0
- data/lib/aspera/transfer/uri.rb +6 -6
- data/lib/aspera/uri_reader.rb +1 -1
- data/lib/aspera/web_auth.rb +1 -1
- data/lib/aspera/web_server_simple.rb +53 -44
- data.tar.gz.sig +0 -0
- metadata +38 -7
- metadata.gz.sig +0 -0
- data/examples/build_package.sh +0 -28
- data/examples/dascli +0 -30
- data/examples/get_proto_file.rb +0 -8
- data/examples/proxy.pac +0 -60
- data/lib/aspera/transfer/spec.yaml +0 -718
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'aspera/log'
|
4
|
+
require 'aspera/assert'
|
4
5
|
require 'singleton'
|
5
6
|
require 'mime/types'
|
6
7
|
|
@@ -9,6 +10,7 @@ module Aspera
|
|
9
10
|
# function conversion_type returns one of the types: CONVERSION_TYPES
|
10
11
|
class FileTypes
|
11
12
|
include Singleton
|
13
|
+
|
12
14
|
# values for conversion_type : input format
|
13
15
|
CONVERSION_TYPES = %i[image office pdf plaintext video].freeze
|
14
16
|
|
@@ -22,6 +24,8 @@ module Aspera
|
|
22
24
|
'application/mxf' => :video,
|
23
25
|
'application/mac-binhex40' => :office,
|
24
26
|
'application/msword' => :office,
|
27
|
+
'application/vnd.ms-excel' => :office,
|
28
|
+
'application/vnd.ms-powerpoint' => :office,
|
25
29
|
'application/rtf' => :office,
|
26
30
|
'application/x-abiword' => :office,
|
27
31
|
'application/x-mspublisher' => :office,
|
@@ -56,17 +60,43 @@ module Aspera
|
|
56
60
|
end
|
57
61
|
|
58
62
|
# @param mimetype [String] mime type
|
59
|
-
# @return file type, one of enum CONVERSION_TYPES
|
63
|
+
# @return file type, one of enum CONVERSION_TYPES, or nil if not found
|
60
64
|
def mime_to_type(mimetype)
|
65
|
+
Aspera.assert_type(mimetype, String)
|
61
66
|
return SUPPORTED_MIME_TYPES[mimetype] if SUPPORTED_MIME_TYPES.key?(mimetype)
|
62
|
-
return :office if mimetype.start_with?('application/vnd.')
|
67
|
+
return :office if mimetype.start_with?('application/vnd.ms-')
|
68
|
+
return :office if mimetype.start_with?('application/vnd.openxmlformats-officedocument')
|
63
69
|
return :video if mimetype.start_with?('video/')
|
64
70
|
return :image if mimetype.start_with?('image/')
|
65
71
|
return nil
|
66
72
|
end
|
67
73
|
|
68
|
-
#
|
69
|
-
|
74
|
+
# @param filepath [String] full path to file
|
75
|
+
# @param mimetype [String] provided by node API
|
76
|
+
# @return file type, one of enum CONVERSION_TYPES
|
77
|
+
# @raise [RuntimeError] if no conversion type found
|
78
|
+
def conversion_type(filepath, mimetype)
|
79
|
+
Log.log.debug{"conversion_type(#{filepath},mime=#{mimetype},magic=#{@use_mimemagic})"}
|
80
|
+
mimetype = nil if mimetype.is_a?(String) && (mimetype == 'application/octet-stream' || mimetype.empty?)
|
81
|
+
# Use mimemagic if available
|
82
|
+
mimetype ||= mime_using_mimemagic(filepath)
|
83
|
+
mimetype ||= mime_using_file(filepath)
|
84
|
+
# from extensions, using local mapping
|
85
|
+
mimetype ||= MIME::Types.of(File.basename(filepath)).first
|
86
|
+
raise "no MIME type found for #{File.basename(filepath)}" if mimetype.nil?
|
87
|
+
conversion_type = mime_to_type(mimetype)
|
88
|
+
raise "no conversion type found for #{File.basename(filepath)}" if conversion_type.nil?
|
89
|
+
Log.log.trace1{"conversion_type(#{File.basename(filepath)}): #{conversion_type.class.name} [#{conversion_type}]"}
|
90
|
+
return conversion_type
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
# Use mime magic to find mime type based on file content (magic numbers)
|
96
|
+
# @param filepath [String] full path to file
|
97
|
+
# @return [String] mime type, or nil if not found
|
98
|
+
def mime_using_mimemagic(filepath)
|
99
|
+
return unless @use_mimemagic
|
70
100
|
# moved here, as `mimemagic` can cause installation issues
|
71
101
|
require 'mimemagic'
|
72
102
|
require 'mimemagic/version'
|
@@ -83,35 +113,12 @@ module Aspera
|
|
83
113
|
return detected_mime
|
84
114
|
end
|
85
115
|
|
86
|
-
#
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
# 1- get type from provided mime type, using local mapping
|
93
|
-
conversion_type = mime_to_type(mimetype) if !mimetype.nil?
|
94
|
-
# 2- else, from computed mime type (if available)
|
95
|
-
if conversion_type.nil? && @use_mimemagic
|
96
|
-
detected_mime = file_to_mime(filepath)
|
97
|
-
if !detected_mime.nil?
|
98
|
-
conversion_type = mime_to_type(detected_mime)
|
99
|
-
if !mimetype.nil?
|
100
|
-
if mimetype.eql?(detected_mime)
|
101
|
-
Log.log.debug('matching mime type per magic number')
|
102
|
-
else
|
103
|
-
# NOTE: detected can be nil
|
104
|
-
Log.log.debug{"non matching mime types: node=[#{mimetype}], magic=[#{detected_mime}]"}
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
109
|
-
# 3- else, from extensions, using local mapping
|
110
|
-
mime_by_ext = MIME::Types.of(File.basename(filepath)).first
|
111
|
-
conversion_type = mime_to_type(mime_by_ext.to_s) if conversion_type.nil? && !mime_by_ext.nil?
|
112
|
-
raise "no conversion type found for #{File.basename(filepath)}" if conversion_type.nil?
|
113
|
-
Log.log.trace1{"conversion_type(#{File.basename(filepath)}): #{conversion_type.class.name} [#{conversion_type}]"}
|
114
|
-
return conversion_type
|
116
|
+
# Use 'file' command to find mime type based on file content (Unix)
|
117
|
+
def mime_using_file(filepath)
|
118
|
+
return Environment.secure_capture(exec: 'file', args: ['--mime-type', '--brief', filepath]).strip
|
119
|
+
rescue => e
|
120
|
+
Log.log.error{"error using 'file' command: #{e.message}"}
|
121
|
+
return nil
|
115
122
|
end
|
116
123
|
end
|
117
124
|
end
|
@@ -240,7 +240,7 @@ module Aspera
|
|
240
240
|
# text to png
|
241
241
|
def convert_plaintext_to_png
|
242
242
|
# get 100 first lines of text file
|
243
|
-
first_lines = File.
|
243
|
+
first_lines = File.foreach(@source_file_path).first(100).join
|
244
244
|
Utils.external_command(:magick, [
|
245
245
|
'convert',
|
246
246
|
'-size', "#{@options.thumb_img_size}x#{@options.thumb_img_size}",
|
@@ -15,22 +15,22 @@ module Aspera
|
|
15
15
|
# iw/ih : input width or height
|
16
16
|
# -x : keep aspect ratio, having value a multiple of x
|
17
17
|
DESCRIPTIONS = [
|
18
|
-
{
|
19
|
-
{
|
20
|
-
{
|
21
|
-
{
|
22
|
-
{
|
23
|
-
{
|
24
|
-
{
|
25
|
-
{
|
26
|
-
{
|
27
|
-
{
|
28
|
-
{
|
29
|
-
{
|
30
|
-
{
|
31
|
-
{
|
32
|
-
{
|
33
|
-
{
|
18
|
+
{name: :max_size, default: 1 << 24, description: 'maximum size (in bytes) of preview file'},
|
19
|
+
{name: :thumb_vid_scale, default: "-1:'min(ih,100)'", description: 'png: video: size (ffmpeg scale argument)'},
|
20
|
+
{name: :thumb_vid_fraction, default: 0.1, description: 'png: video: time percent position of snapshot'},
|
21
|
+
{name: :thumb_img_size, default: 800, description: 'png: non-video: height (and width)'},
|
22
|
+
{name: :thumb_text_font, default: 'Courier', description: 'png: plaintext: font for text rendering: `magick identify -list font`'},
|
23
|
+
{name: :video_conversion, default: :reencode, description: 'mp4: method for preview generation', values: VIDEO_CONVERSION_METHODS},
|
24
|
+
{name: :video_png_conv, default: :fixed, description: 'mp4: method for thumbnail generation', values: VIDEO_THUMBNAIL_METHODS},
|
25
|
+
{name: :video_scale, default: "'min(iw,360)':-2", description: 'mp4: all: video scale (ffmpeg scale argument)'},
|
26
|
+
{name: :video_start_sec, default: 10, description: 'mp4: all: start offset (seconds) of video preview'},
|
27
|
+
{name: :reencode_ffmpeg, default: {}, description: 'mp4: reencode: options to ffmpeg'},
|
28
|
+
{name: :blend_keyframes, default: 30, description: 'mp4: blend: # key frames'},
|
29
|
+
{name: :blend_pauseframes, default: 3, description: 'mp4: blend: # pause frames'},
|
30
|
+
{name: :blend_transframes, default: 5, description: 'mp4: blend: # transition blend frames'},
|
31
|
+
{name: :blend_fps, default: 15, description: 'mp4: blend: frame per second'},
|
32
|
+
{name: :clips_count, default: 5, description: 'mp4: clips: number of clips'},
|
33
|
+
{name: :clips_length, default: 5, description: 'mp4: clips: length in seconds of each clips'}
|
34
34
|
].freeze
|
35
35
|
# add accessors
|
36
36
|
DESCRIPTIONS.each do |opt|
|
@@ -50,7 +50,7 @@ module Aspera
|
|
50
50
|
pixel_colors = []
|
51
51
|
image.each_pixel do |pixel, col, row|
|
52
52
|
pixel_rgb = [pixel.red, pixel.green, pixel.blue]
|
53
|
-
pixel_rgb = pixel_rgb.map
|
53
|
+
pixel_rgb = pixel_rgb.map{ |color| color >> shift_for_8_bit} unless shift_for_8_bit.eql?(0)
|
54
54
|
# init 2-dim array
|
55
55
|
pixel_colors[row] ||= []
|
56
56
|
pixel_colors[row][col] = pixel_rgb
|
@@ -82,7 +82,7 @@ module Aspera
|
|
82
82
|
size: blob.length
|
83
83
|
# width: image.columns,
|
84
84
|
# height: image.rows
|
85
|
-
}.map
|
85
|
+
}.map{ |k, v| "#{k}=#{v}"}.join(';')
|
86
86
|
# \a is BEL, \e is ESC : https://github.com/ruby/ruby/blob/master/doc/syntax/literals.rdoc#label-Strings
|
87
87
|
# escape sequence for iTerm2 image display
|
88
88
|
return "\e]1337;File=#{arguments}:#{Base64.encode64(blob)}\a"
|
@@ -91,7 +91,7 @@ module Aspera
|
|
91
91
|
# @return [Boolean] true if the terminal supports iTerm2 image display
|
92
92
|
def iterm_supported?
|
93
93
|
TERM_ENV_VARS.each do |env_var|
|
94
|
-
return true if ITERM_NAMES.any?
|
94
|
+
return true if ITERM_NAMES.any?{ |term| ENV[env_var]&.include?(term)}
|
95
95
|
end
|
96
96
|
false
|
97
97
|
end
|
data/lib/aspera/preview/utils.rb
CHANGED
@@ -16,14 +16,18 @@ module Aspera
|
|
16
16
|
# external binaries used
|
17
17
|
EXTERNAL_TOOLS = %i[ffmpeg ffprobe magick optipng unoconv].freeze
|
18
18
|
TEMP_FORMAT = 'img%04d.jpg'
|
19
|
+
FFMPEG_DEFAULT_PARAMS = [
|
20
|
+
'-y', # overwrite output without asking
|
21
|
+
'-loglevel', 'error' # show only errors and up
|
22
|
+
].freeze
|
19
23
|
private_constant :BASH_SPECIAL_CHARACTERS, :EXTERNAL_TOOLS, :TEMP_FORMAT
|
20
24
|
|
21
25
|
class << self
|
22
26
|
# returns string with single quotes suitable for bash if there is any bash meta-character
|
23
27
|
def shell_quote(argument)
|
24
|
-
return argument unless argument.chars.any?{|c|BASH_SPECIAL_CHARACTERS.include?(c)}
|
28
|
+
return argument unless argument.chars.any?{ |c| BASH_SPECIAL_CHARACTERS.include?(c)}
|
25
29
|
# surround with single quotes, and escape single quotes
|
26
|
-
return %Q{'#{argument.gsub("'"){|_s| %q{'"'"'}}}'}
|
30
|
+
return %Q{'#{argument.gsub("'"){ |_s| %q{'"'"'}}}'}
|
27
31
|
end
|
28
32
|
|
29
33
|
# check that external tools can be executed
|
@@ -53,17 +57,11 @@ module Aspera
|
|
53
57
|
return Environment.secure_capture(exec: command_sym.to_s, args: command_args.map(&:to_s))
|
54
58
|
end
|
55
59
|
|
56
|
-
def ffmpeg(
|
57
|
-
Aspera.assert_type(
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
'-loglevel', 'error' # show only errors and up
|
62
|
-
]
|
63
|
-
a[:in_p] ||= []
|
64
|
-
a[:out_p] ||= []
|
65
|
-
Aspera.assert(%i[gl_p in_f in_p out_f out_p].eql?(a.keys.sort)){"wrong params (#{a.keys.sort})"}
|
66
|
-
external_command(:ffmpeg, [a[:gl_p], a[:in_p], '-i', a[:in_f], a[:out_p], a[:out_f]].flatten)
|
60
|
+
def ffmpeg(gl_p: FFMPEG_DEFAULT_PARAMS, in_p: [], in_f:, out_p: [], out_f:)
|
61
|
+
Aspera.assert_type(gl_p, Array)
|
62
|
+
Aspera.assert_type(in_p, Array)
|
63
|
+
Aspera.assert_type(out_p, Array)
|
64
|
+
external_command(:ffmpeg, gl_p + in_p + ['-i', in_f] + out_p + [out_f])
|
67
65
|
end
|
68
66
|
|
69
67
|
# @return Float in seconds
|
@@ -7,6 +7,7 @@ module Aspera
|
|
7
7
|
module Products
|
8
8
|
class Connect
|
9
9
|
include Singleton
|
10
|
+
|
10
11
|
APP_NAME = 'IBM Aspera Connect'
|
11
12
|
|
12
13
|
class << self
|
@@ -43,7 +44,7 @@ module Aspera
|
|
43
44
|
app_root: File.join(Dir.home, '.aspera', 'connect'),
|
44
45
|
run_root: File.join(Dir.home, '.aspera', 'connect')
|
45
46
|
}]
|
46
|
-
end.map
|
47
|
+
end.map{ |i| i.merge({expected: APP_NAME})}
|
47
48
|
end
|
48
49
|
end
|
49
50
|
|
@@ -13,7 +13,7 @@ module URI
|
|
13
13
|
def register_proxy_finder
|
14
14
|
Aspera.assert(block_given?)
|
15
15
|
# overload the method in URI : call user's provided block and fallback to original method
|
16
|
-
define_method(:find_proxy)
|
16
|
+
define_method(:find_proxy){ |env_vars=ENV| yield(to_s) || find_proxy_orig(env_vars)}
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
@@ -70,7 +70,7 @@ END_OF_JAVASCRIPT
|
|
70
70
|
end
|
71
71
|
|
72
72
|
def register_uri_generic
|
73
|
-
URI::Generic.register_proxy_finder{|url_str|get_proxies(url_str).first}
|
73
|
+
URI::Generic.register_proxy_finder{ |url_str| get_proxies(url_str).first}
|
74
74
|
# allow chaining
|
75
75
|
return self
|
76
76
|
end
|
data/lib/aspera/rest.rb
CHANGED
@@ -6,12 +6,14 @@ require 'aspera/log'
|
|
6
6
|
require 'aspera/assert'
|
7
7
|
require 'aspera/oauth'
|
8
8
|
require 'aspera/hash_ext'
|
9
|
+
require 'aspera/timer_limiter'
|
9
10
|
require 'net/http'
|
10
11
|
require 'net/https'
|
11
12
|
require 'json'
|
12
13
|
require 'base64'
|
13
14
|
require 'singleton'
|
14
15
|
require 'securerandom'
|
16
|
+
require 'fileutils'
|
15
17
|
|
16
18
|
# Cancel method for HTTP
|
17
19
|
class Net::HTTP::Cancel < Net::HTTPRequest # rubocop:disable Style/ClassAndModuleChildren
|
@@ -31,15 +33,18 @@ module Aspera
|
|
31
33
|
class RestParameters
|
32
34
|
include Singleton
|
33
35
|
|
34
|
-
attr_accessor :user_agent, :download_partial_suffix, :retry_on_error, :retry_sleep, :session_cb, :progress_bar
|
36
|
+
attr_accessor :user_agent, :download_partial_suffix, :retry_on_error, :retry_on_timeout, :retry_on_unavailable, :retry_max, :retry_sleep, :session_cb, :progress_bar
|
35
37
|
|
36
38
|
private
|
37
39
|
|
38
40
|
def initialize
|
39
41
|
@user_agent = 'RubyAsperaRest'
|
40
42
|
@download_partial_suffix = '.http_partial'
|
41
|
-
@retry_on_error =
|
42
|
-
@
|
43
|
+
@retry_on_error = false
|
44
|
+
@retry_on_timeout = true
|
45
|
+
@retry_on_unavailable = true
|
46
|
+
@retry_max = 1
|
47
|
+
@retry_sleep = 4
|
43
48
|
@session_cb = nil
|
44
49
|
@progress_bar = nil
|
45
50
|
end
|
@@ -57,8 +62,14 @@ module Aspera
|
|
57
62
|
# error message when entity not found (TODO: use specific exception)
|
58
63
|
ENTITY_NOT_FOUND = 'No such'
|
59
64
|
|
65
|
+
MIME_JSON = 'application/json'
|
66
|
+
MIME_WWW = 'application/x-www-form-urlencoded'
|
67
|
+
MIME_TEXT = 'text/plain'
|
68
|
+
|
60
69
|
# Content-Type that are JSON
|
61
|
-
JSON_DECODE = [
|
70
|
+
JSON_DECODE = [MIME_JSON, 'application/vnd.api+json', 'application/x-javascript'].freeze
|
71
|
+
|
72
|
+
UNAVAILABLE_CODES = ['503']
|
62
73
|
|
63
74
|
class << self
|
64
75
|
# @return [String] Basic auth token
|
@@ -98,7 +109,7 @@ module Aspera
|
|
98
109
|
end
|
99
110
|
end
|
100
111
|
when Array
|
101
|
-
Aspera.assert(query.all?{|i| i.is_a?(Array) && i.length.eql?(2)})
|
112
|
+
Aspera.assert(query.all?{ |i| i.is_a?(Array) && i.length.eql?(2)}){'Query must be array of arrays or 2 elements'}
|
102
113
|
query_array = query
|
103
114
|
else
|
104
115
|
raise "Query must be Hash or Array, not #{query.class}"
|
@@ -163,6 +174,10 @@ module Aspera
|
|
163
174
|
return result
|
164
175
|
end
|
165
176
|
|
177
|
+
# Parse a header string as returned by HTTP
|
178
|
+
# @param header [String] header string, e.g. "application/json; charset=utf-8"
|
179
|
+
# @return [Hash] parsed header with type and parameters
|
180
|
+
# {type: 'application/json', parameters: {charset: 'utf-8'}}
|
166
181
|
def parse_header(header)
|
167
182
|
type, *params = header.split(/;\s*/)
|
168
183
|
parameters = params.map do |param|
|
@@ -171,7 +186,7 @@ module Aspera
|
|
171
186
|
one[1] = one[1].gsub(/\A"|"\z/, '')
|
172
187
|
one
|
173
188
|
end.to_h
|
174
|
-
{
|
189
|
+
{type: type.downcase, parameters: parameters}
|
175
190
|
end
|
176
191
|
end
|
177
192
|
|
@@ -189,6 +204,7 @@ module Aspera
|
|
189
204
|
|
190
205
|
attr_reader :base_url
|
191
206
|
attr_reader :auth_params
|
207
|
+
attr_reader :headers
|
192
208
|
|
193
209
|
# @return creation parameters
|
194
210
|
def params
|
@@ -238,7 +254,7 @@ module Aspera
|
|
238
254
|
@http_session = nil
|
239
255
|
@redirect_max = redirect_max
|
240
256
|
Aspera.assert_type(@redirect_max, Integer)
|
241
|
-
@headers = headers
|
257
|
+
@headers = headers.clone
|
242
258
|
Aspera.assert_type(@headers, Hash)
|
243
259
|
@headers['User-Agent'] ||= RestParameters.instance.user_agent
|
244
260
|
# OAuth object (created on demand)
|
@@ -249,7 +265,7 @@ module Aspera
|
|
249
265
|
def oauth
|
250
266
|
if @oauth.nil?
|
251
267
|
Aspera.assert(@auth_params[:type].eql?(:oauth2)){'no OAuth defined'}
|
252
|
-
oauth_parameters = @auth_params.reject
|
268
|
+
oauth_parameters = @auth_params.reject{ |k, _v| k.eql?(:type)}
|
253
269
|
Log.log.debug{Log.dump('oauth parameters', oauth_parameters)}
|
254
270
|
@oauth = OAuth::Factory.instance.create(**oauth_parameters)
|
255
271
|
end
|
@@ -260,20 +276,20 @@ module Aspera
|
|
260
276
|
# @param operation [String] HTTP operation (GET, POST, PUT, DELETE)
|
261
277
|
# @param subpath [String] subpath of REST API
|
262
278
|
# @param query [Hash] URL parameters
|
279
|
+
# @param content_type [String,nil] Type of body parameters (one of MIME_*) and serialization, else use headers
|
263
280
|
# @param body [Hash, String] body parameters
|
264
|
-
# @param
|
281
|
+
# @param headers [Hash] additional headers (override Content-Type)
|
265
282
|
# @param save_to_file (filepath)
|
266
283
|
# @param return_error (bool)
|
267
|
-
# @param headers [Hash] additional headers
|
268
284
|
def call(
|
269
285
|
operation:,
|
270
286
|
subpath: nil,
|
271
287
|
query: nil,
|
288
|
+
content_type: nil,
|
272
289
|
body: nil,
|
273
|
-
|
290
|
+
headers: nil,
|
274
291
|
save_to_file: nil,
|
275
|
-
return_error: false
|
276
|
-
headers: nil
|
292
|
+
return_error: false
|
277
293
|
)
|
278
294
|
subpath = subpath.to_s if subpath.is_a?(Symbol)
|
279
295
|
subpath = '' if subpath.nil?
|
@@ -317,19 +333,18 @@ module Aspera
|
|
317
333
|
rescue NameError
|
318
334
|
raise "unsupported operation : #{operation}"
|
319
335
|
end
|
320
|
-
case
|
321
|
-
when
|
336
|
+
case content_type
|
337
|
+
when nil # ignore
|
338
|
+
when MIME_JSON
|
322
339
|
req.body = JSON.generate(body) # , ascii_only: true
|
323
|
-
req['Content-Type'] =
|
324
|
-
when
|
340
|
+
req['Content-Type'] = MIME_JSON
|
341
|
+
when MIME_WWW
|
325
342
|
req.body = URI.encode_www_form(body)
|
326
|
-
req['Content-Type'] =
|
327
|
-
when
|
343
|
+
req['Content-Type'] = MIME_WWW
|
344
|
+
when MIME_TEXT
|
328
345
|
req.body = body
|
329
|
-
req['Content-Type'] =
|
330
|
-
|
331
|
-
else
|
332
|
-
raise "unsupported body type : #{body_type}"
|
346
|
+
req['Content-Type'] = MIME_TEXT
|
347
|
+
else Aspera.error_unexpected_value(content_type){'body type'}
|
333
348
|
end
|
334
349
|
# set headers
|
335
350
|
headers.each do |key, value|
|
@@ -338,10 +353,8 @@ module Aspera
|
|
338
353
|
# :type = :basic
|
339
354
|
req.basic_auth(@auth_params[:username], @auth_params[:password]) if @auth_params[:type].eql?(:basic)
|
340
355
|
Log.log.trace1{Log.dump(:req_body, req.body)}
|
341
|
-
# we try the call, and will retry
|
342
|
-
|
343
|
-
timeout_tries ||= 5
|
344
|
-
general_tries ||= 1 + RestParameters.instance.retry_on_error
|
356
|
+
# we try the call, and will retry on some error types
|
357
|
+
error_tries ||= 1 + RestParameters.instance.retry_max
|
345
358
|
# initialize with number of initial retries allowed, nil gives zero
|
346
359
|
tries_remain_redirect = @redirect_max if tries_remain_redirect.nil?
|
347
360
|
Log.log.debug("send request (retries=#{tries_remain_redirect})")
|
@@ -350,19 +363,19 @@ module Aspera
|
|
350
363
|
# make http request (pipelined)
|
351
364
|
http_session.request(req) do |response|
|
352
365
|
result[:http] = response
|
353
|
-
result_mime = self.class.parse_header(result[:http]['Content-Type'] ||
|
366
|
+
result_mime = self.class.parse_header(result[:http]['Content-Type'] || MIME_TEXT)[:type]
|
367
|
+
Log.log.debug{"response: code=#{result[:http].code}, mime=#{result_mime}, mime2= #{response['Content-Type']}"}
|
354
368
|
# JSON data needs to be parsed, in case it contains an error code
|
355
369
|
if !save_to_file.nil? &&
|
356
370
|
result[:http].code.to_s.start_with?('2') &&
|
357
|
-
!result[:http]['Content-Length'].nil? &&
|
358
371
|
!JSON_DECODE.include?(result_mime)
|
359
|
-
total_size = result[:http]['Content-Length']
|
372
|
+
total_size = result[:http]['Content-Length']&.to_i
|
360
373
|
Log.log.debug('before write file')
|
361
374
|
target_file = save_to_file
|
362
375
|
# override user's path to path in header
|
363
376
|
if !response['Content-Disposition'].nil?
|
364
377
|
disposition = self.class.parse_header(response['Content-Disposition'])
|
365
|
-
if disposition[:parameters].key?(:filename)
|
378
|
+
if disposition[:parameters].key?(:filename) && !disposition[:parameters][:filename].eql?('.')
|
366
379
|
target_file = File.join(File.dirname(target_file), disposition[:parameters][:filename])
|
367
380
|
end
|
368
381
|
end
|
@@ -372,12 +385,14 @@ module Aspera
|
|
372
385
|
written_size = 0
|
373
386
|
session_id = SecureRandom.uuid.freeze
|
374
387
|
RestParameters.instance.progress_bar&.event(:session_start, session_id: session_id)
|
375
|
-
RestParameters.instance.progress_bar&.event(:session_size, session_id: session_id, info: total_size)
|
388
|
+
RestParameters.instance.progress_bar&.event(:session_size, session_id: session_id, info: total_size) if total_size
|
389
|
+
FileUtils.mkdir_p(File.dirname(target_file_tmp))
|
390
|
+
limiter = TimerLimiter.new(0.5)
|
376
391
|
File.open(target_file_tmp, 'wb') do |file|
|
377
392
|
result[:http].read_body do |fragment|
|
378
393
|
file.write(fragment)
|
379
394
|
written_size += fragment.length
|
380
|
-
RestParameters.instance.progress_bar&.event(:transfer, session_id: session_id, info: written_size)
|
395
|
+
RestParameters.instance.progress_bar&.event(:transfer, session_id: session_id, info: written_size) if limiter.trigger?
|
381
396
|
end
|
382
397
|
end
|
383
398
|
RestParameters.instance.progress_bar&.event(:end, session_id: session_id)
|
@@ -394,17 +409,22 @@ module Aspera
|
|
394
409
|
when *JSON_DECODE
|
395
410
|
result[:data] = JSON.parse(result[:http].body) rescue result[:http].body
|
396
411
|
Log.log.debug{Log.dump('result_data', result[:data])}
|
397
|
-
else # when
|
412
|
+
else # when MIME_TEXT
|
398
413
|
result[:data] = result[:http].body
|
399
414
|
end
|
400
415
|
RestErrorAnalyzer.instance.raise_on_error(req, result)
|
401
|
-
|
416
|
+
unless file_saved || save_to_file.nil?
|
417
|
+
FileUtils.mkdir_p(File.dirname(save_to_file))
|
418
|
+
File.write(save_to_file, result[:http].body, binmode: true)
|
419
|
+
end
|
402
420
|
rescue RestCallError => e
|
403
421
|
do_retry = false
|
404
422
|
# AoC have some timeout , like Connect to platform.bss.asperasoft.com:443 ...
|
405
|
-
do_retry
|
423
|
+
do_retry ||= true if e.response.body.include?('failed: connect timed out') && RestParameters.instance.retry_on_timeout
|
424
|
+
# AoC sometimes not available
|
425
|
+
do_retry ||= true if RestParameters.instance.retry_on_unavailable && UNAVAILABLE_CODES.include?(result[:http].code.to_s)
|
406
426
|
# possibility to retry anything if it fails
|
407
|
-
do_retry
|
427
|
+
do_retry ||= true if RestParameters.instance.retry_on_error
|
408
428
|
# not authorized: oauth token expired
|
409
429
|
if @not_auth_codes.include?(result[:http].code.to_s) && @auth_params[:type].eql?(:oauth2)
|
410
430
|
begin
|
@@ -417,10 +437,10 @@ module Aspera
|
|
417
437
|
req['Authorization'] = oauth.authorization(cache: false)
|
418
438
|
end
|
419
439
|
Log.log.debug{"using new token=#{headers['Authorization']}"}
|
420
|
-
do_retry
|
440
|
+
do_retry ||= true
|
421
441
|
end
|
422
|
-
if do_retry
|
423
|
-
sleep(RestParameters.instance.retry_sleep) unless RestParameters.instance.retry_sleep.
|
442
|
+
if do_retry && (error_tries -= 1).positive?
|
443
|
+
sleep(RestParameters.instance.retry_sleep) unless RestParameters.instance.retry_sleep.eql?(0)
|
424
444
|
retry
|
425
445
|
end
|
426
446
|
# redirect ? (any code beginning with 3)
|
@@ -436,13 +456,13 @@ module Aspera
|
|
436
456
|
end
|
437
457
|
# forwards the request to the new location
|
438
458
|
return self.class.new(base_url: new_url, redirect_max: tries_remain_redirect).call(
|
439
|
-
operation: operation, query: query, body: body,
|
459
|
+
operation: operation, query: query, body: body, content_type: content_type,
|
440
460
|
save_to_file: save_to_file, return_error: return_error, headers: headers)
|
441
461
|
end
|
442
462
|
# raise exception if could not retry and not return error in result
|
443
463
|
raise e unless return_error
|
444
464
|
end
|
445
|
-
Log.log.debug{"result
|
465
|
+
Log.log.debug{"result=http:#{result[:http]}, data:#{result[:data].class}"}
|
446
466
|
return result
|
447
467
|
end
|
448
468
|
|
@@ -452,23 +472,23 @@ module Aspera
|
|
452
472
|
#
|
453
473
|
|
454
474
|
def create(subpath, params)
|
455
|
-
return call(operation: 'POST', subpath: subpath, headers: {'Accept' =>
|
475
|
+
return call(operation: 'POST', subpath: subpath, headers: {'Accept' => MIME_JSON}, body: params, content_type: MIME_JSON)[:data]
|
456
476
|
end
|
457
477
|
|
458
478
|
def read(subpath, query=nil)
|
459
|
-
return call(operation: 'GET', subpath: subpath, headers: {'Accept' =>
|
479
|
+
return call(operation: 'GET', subpath: subpath, headers: {'Accept' => MIME_JSON}, query: query)[:data]
|
460
480
|
end
|
461
481
|
|
462
482
|
def update(subpath, params)
|
463
|
-
return call(operation: 'PUT', subpath: subpath, headers: {'Accept' =>
|
483
|
+
return call(operation: 'PUT', subpath: subpath, headers: {'Accept' => MIME_JSON}, body: params, content_type: MIME_JSON)[:data]
|
464
484
|
end
|
465
485
|
|
466
486
|
def delete(subpath, params=nil)
|
467
|
-
return call(operation: 'DELETE', subpath: subpath, headers: {'Accept' =>
|
487
|
+
return call(operation: 'DELETE', subpath: subpath, headers: {'Accept' => MIME_JSON}, query: params)[:data]
|
468
488
|
end
|
469
489
|
|
470
490
|
def cancel(subpath)
|
471
|
-
return call(operation: 'CANCEL', subpath: subpath, headers: {'Accept' =>
|
491
|
+
return call(operation: 'CANCEL', subpath: subpath, headers: {'Accept' => MIME_JSON})[:data]
|
472
492
|
end
|
473
493
|
|
474
494
|
# Query entity by general search (read with parameter `q`)
|
@@ -490,11 +510,11 @@ module Aspera
|
|
490
510
|
else
|
491
511
|
# multiple case insensitive partial matches, try case insensitive full match
|
492
512
|
# (anyway AoC does not allow creation of 2 entities with same case insensitive name)
|
493
|
-
name_matches = matching_items.select{|i|i['name'].casecmp?(search_name)}
|
513
|
+
name_matches = matching_items.select{ |i| i['name'].casecmp?(search_name)}
|
494
514
|
case name_matches.length
|
495
515
|
when 1 then return name_matches.first
|
496
|
-
when 0 then raise %Q(#{subpath}: multiple case insensitive partial match for: "#{search_name}": #{matching_items.map{|i|i['name']}} but no case insensitive full match. Please be more specific or give exact name.) # rubocop:disable Layout/LineLength
|
497
|
-
else raise "Two entities cannot have the same case insensitive name: #{name_matches.map{|i|i['name']}}"
|
516
|
+
when 0 then raise %Q(#{subpath}: multiple case insensitive partial match for: "#{search_name}": #{matching_items.map{ |i| i['name']}} but no case insensitive full match. Please be more specific or give exact name.) # rubocop:disable Layout/LineLength
|
517
|
+
else raise "Two entities cannot have the same case insensitive name: #{name_matches.map{ |i| i['name']}}"
|
498
518
|
end
|
499
519
|
end
|
500
520
|
end
|
@@ -35,7 +35,7 @@ module Aspera
|
|
35
35
|
d_t_s.each do |res|
|
36
36
|
r_err = res.dig(*%w[transfer_spec error]) || res['error']
|
37
37
|
next unless r_err.is_a?(Hash)
|
38
|
-
RestErrorAnalyzer.add_error(call_context, type,
|
38
|
+
RestErrorAnalyzer.add_error(call_context, type, r_err.values.join(': '))
|
39
39
|
end
|
40
40
|
end
|
41
41
|
RestErrorAnalyzer.instance.add_simple_handler(name: 'T9:IBM cloud IAM', path: ['errorMessage'])
|