aspera-cli 4.21.1 → 4.22.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 +52 -22
- data/CONTRIBUTING.md +69 -148
- data/README.md +929 -668
- data/bin/ascli +5 -14
- data/bin/asession +1 -3
- data/examples/get_proto_file.rb +4 -3
- data/examples/proxy.pac +20 -20
- data/lib/aspera/agent/base.rb +11 -5
- data/lib/aspera/agent/connect.rb +30 -28
- data/lib/aspera/agent/{alpha.rb → desktop.rb} +35 -31
- data/lib/aspera/agent/direct.rb +141 -121
- data/lib/aspera/agent/httpgw.rb +22 -26
- data/lib/aspera/agent/node.rb +14 -11
- data/lib/aspera/agent/transferd.rb +30 -19
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +6 -6
- data/lib/aspera/api/cos_node.rb +2 -2
- data/lib/aspera/api/httpgw.rb +7 -3
- data/lib/aspera/api/node.rb +10 -8
- data/lib/aspera/ascmd.rb +3 -3
- data/lib/aspera/ascp/installation.rb +53 -72
- data/lib/aspera/ascp/management.rb +1 -1
- data/lib/aspera/assert.rb +11 -2
- data/lib/aspera/cli/error.rb +2 -2
- data/lib/aspera/cli/extended_value.rb +46 -21
- data/lib/aspera/cli/formatter.rb +55 -48
- data/lib/aspera/cli/hints.rb +1 -1
- data/lib/aspera/cli/info.rb +1 -0
- data/lib/aspera/cli/main.rb +192 -170
- data/lib/aspera/cli/manager.rb +18 -18
- data/lib/aspera/cli/plugin.rb +23 -20
- 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 +247 -159
- data/lib/aspera/cli/plugins/ats.rb +19 -17
- data/lib/aspera/cli/plugins/config.rb +76 -113
- 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 +111 -84
- 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 +312 -182
- 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 +6 -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 +5 -5
- 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 +8 -5
- data/lib/aspera/environment.rb +26 -17
- data/lib/aspera/faspex_gw.rb +14 -14
- data/lib/aspera/faspex_postproc.rb +10 -11
- data/lib/aspera/hash_ext.rb +4 -14
- 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 +28 -34
- data/lib/aspera/nagios.rb +6 -6
- data/lib/aspera/node_simulator.rb +8 -8
- data/lib/aspera/oauth/base.rb +14 -7
- data/lib/aspera/oauth/factory.rb +5 -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/generator.rb +13 -10
- data/lib/aspera/preview/options.rb +16 -16
- data/lib/aspera/preview/terminal.rb +4 -4
- data/lib/aspera/preview/utils.rb +15 -17
- data/lib/aspera/products/connect.rb +35 -1
- data/lib/aspera/products/{alpha.rb → desktop.rb} +3 -3
- data/lib/aspera/products/transferd.rb +9 -2
- data/lib/aspera/proxy_auto_config.rb +2 -2
- data/lib/aspera/rest.rb +56 -47
- data/lib/aspera/rest_errors_aspera.rb +1 -1
- data/lib/aspera/secret_hider.rb +12 -5
- data/lib/aspera/ssh.rb +4 -4
- data/lib/aspera/temp_file_manager.rb +5 -4
- 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 +23 -72
- data/lib/aspera/transfer/sync_instance.schema.yaml +13 -0
- data/lib/aspera/transfer/sync_session.schema.yaml +79 -0
- data/lib/aspera/transfer/uri.rb +6 -6
- data/lib/aspera/uri_reader.rb +18 -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 +28 -165
- metadata.gz.sig +0 -0
- data/examples/build_exec +0 -74
- data/examples/build_exec_rubyc +0 -40
- data/examples/build_package.sh +0 -28
- data/lib/aspera/transfer/spec.yaml +0 -718
@@ -64,7 +64,7 @@ module Aspera
|
|
64
64
|
garbage_files = current_files(persist_category)
|
65
65
|
if !max_age_seconds.nil?
|
66
66
|
current_time = Time.now
|
67
|
-
garbage_files.select!
|
67
|
+
garbage_files.select!{ |filepath| (current_time - File.stat(filepath).mtime).to_i > max_age_seconds}
|
68
68
|
end
|
69
69
|
garbage_files.each do |filepath|
|
70
70
|
File.delete(filepath)
|
@@ -79,7 +79,7 @@ module Aspera
|
|
79
79
|
end
|
80
80
|
|
81
81
|
def current_items(persist_category)
|
82
|
-
current_files(persist_category).each_with_object({})
|
82
|
+
current_files(persist_category).each_with_object({}){ |i, h| h[File.basename(i, FILE_SUFFIX)] = File.read(i)}
|
83
83
|
end
|
84
84
|
|
85
85
|
private
|
@@ -4,7 +4,6 @@
|
|
4
4
|
# spellchecker:ignore pauseframes libx264 trunc bufsize muxer apng libmp3lame maxrate posterize movflags faststart
|
5
5
|
# spellchecker:ignore palettegen paletteuse pointsize bordercolor repage lanczos unoconv optipng reencode conv transframes
|
6
6
|
|
7
|
-
require 'open3'
|
8
7
|
require 'aspera/preview/options'
|
9
8
|
require 'aspera/preview/utils'
|
10
9
|
require 'aspera/preview/file_types'
|
@@ -24,17 +23,18 @@ module Aspera
|
|
24
23
|
# one of CONVERSION_TYPES
|
25
24
|
attr_reader :conversion_type
|
26
25
|
|
27
|
-
# @param src source file path
|
28
|
-
# @param dst destination file path
|
29
|
-
# @param api_mime_type optional mime type as provided by node api (or nil)
|
30
26
|
# node API mime types are from: http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
|
31
|
-
# supported preview type is one of Preview::PREVIEW_FORMATS
|
32
27
|
# the resulting preview file type is taken from destination file extension.
|
33
28
|
# conversion methods are provided by private methods: convert_<conversion_type>_to_<preview_format>
|
34
29
|
# -> conversion_type is one of FileTypes::CONVERSION_TYPES
|
35
30
|
# -> preview_format is one of Generator::PREVIEW_FORMATS
|
36
31
|
# the conversion video->mp4 is implemented in methods: convert_video_to_mp4_using_<video_conversion>
|
37
32
|
# -> conversion method is one of Generator::VIDEO_CONVERSION_METHODS
|
33
|
+
# @param src [String] source file path
|
34
|
+
# @param dst [String] destination file path
|
35
|
+
# @param options [Options] All conversion options
|
36
|
+
# @param main_temp_dir [String] Main temp folder, sub folder will be created for generation
|
37
|
+
# @param api_mime_type [String,nil] Optional mime type as provided by node api (or nil)
|
38
38
|
def initialize(src, dst, options, main_temp_dir, api_mime_type)
|
39
39
|
@source_file_path = src
|
40
40
|
@destination_file_path = dst
|
@@ -54,7 +54,7 @@ module Aspera
|
|
54
54
|
end
|
55
55
|
@processing_method = @processing_method.to_sym
|
56
56
|
Log.log.debug{"method: #{@processing_method}"}
|
57
|
-
Aspera.assert(respond_to?(@processing_method, true)){"no processing
|
57
|
+
Aspera.assert(respond_to?(@processing_method, true)){"no processing known for #{conversion_type} -> #{@preview_format_sym}"}
|
58
58
|
end
|
59
59
|
|
60
60
|
# create preview as specified in constructor
|
@@ -215,7 +215,8 @@ module Aspera
|
|
215
215
|
|
216
216
|
def convert_pdf_to_png(source_file_path=nil)
|
217
217
|
source_file_path ||= @source_file_path
|
218
|
-
Utils.external_command(:
|
218
|
+
Utils.external_command(:magick, [
|
219
|
+
'convert',
|
219
220
|
'-size', "x#{@options.thumb_img_size}",
|
220
221
|
'-background', 'white',
|
221
222
|
'-flatten',
|
@@ -224,7 +225,8 @@ module Aspera
|
|
224
225
|
end
|
225
226
|
|
226
227
|
def convert_image_to_png
|
227
|
-
Utils.external_command(:
|
228
|
+
Utils.external_command(:magick, [
|
229
|
+
'convert',
|
228
230
|
'-auto-orient',
|
229
231
|
'-thumbnail', "#{@options.thumb_img_size}x#{@options.thumb_img_size}>",
|
230
232
|
'-quality', 95,
|
@@ -238,8 +240,9 @@ module Aspera
|
|
238
240
|
# text to png
|
239
241
|
def convert_plaintext_to_png
|
240
242
|
# get 100 first lines of text file
|
241
|
-
first_lines = File.open(@source_file_path){|f|Array.new(100){f.readline rescue ''}.join}
|
242
|
-
Utils.external_command(:
|
243
|
+
first_lines = File.open(@source_file_path){ |f| Array.new(100){f.readline rescue ''}.join}
|
244
|
+
Utils.external_command(:magick, [
|
245
|
+
'convert',
|
243
246
|
'-size', "#{@options.thumb_img_size}x#{@options.thumb_img_size}",
|
244
247
|
'xc:white', # define canvas with background color (xc, or canvas) of preceding size
|
245
248
|
'-font', @options.thumb_text_font,
|
@@ -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|
|
@@ -44,13 +44,13 @@ module Aspera
|
|
44
44
|
fit_term_ratio = [term_rows.to_f * font_ratio / image.rows.to_f, term_columns.to_f / image.columns.to_f].min
|
45
45
|
height_ratio = double ? 2.0 : 1.0
|
46
46
|
image = image.scale((image.columns * fit_term_ratio).to_i, (image.rows * fit_term_ratio * height_ratio / font_ratio).to_i)
|
47
|
-
# quantum depth is 8 or 16, see: `
|
47
|
+
# quantum depth is 8 or 16, see: `magick xc: -format "%q" info:`
|
48
48
|
shift_for_8_bit = Magick::MAGICKCORE_QUANTUM_DEPTH - 8
|
49
49
|
# get all pixel colors, adjusted for Rainbow
|
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
@@ -14,16 +14,20 @@ module Aspera
|
|
14
14
|
# from bash manual: meta-character need to be escaped
|
15
15
|
BASH_SPECIAL_CHARACTERS = "|&;()<> \t#\n"
|
16
16
|
# external binaries used
|
17
|
-
EXTERNAL_TOOLS = %i[ffmpeg ffprobe
|
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
|
@@ -32,7 +36,7 @@ module Aspera
|
|
32
36
|
tools_to_check.delete(:unoconv) if skip_types.include?(:office)
|
33
37
|
# Check for binaries
|
34
38
|
tools_to_check.each do |command_sym|
|
35
|
-
external_command(command_sym, ['-h'])
|
39
|
+
external_command(command_sym, ['-h'], out: File::NULL)
|
36
40
|
rescue Errno::ENOENT => e
|
37
41
|
raise "missing #{command_sym} binary: #{e}"
|
38
42
|
rescue
|
@@ -45,7 +49,7 @@ module Aspera
|
|
45
49
|
# @return nil
|
46
50
|
def external_command(command_sym, command_args)
|
47
51
|
Aspera.assert_values(command_sym, EXTERNAL_TOOLS){'command'}
|
48
|
-
Environment.secure_execute(exec: command_sym.to_s, args: command_args.map(&:to_s))
|
52
|
+
Environment.secure_execute(exec: command_sym.to_s, args: command_args.map(&:to_s), out: File::NULL, err: File::NULL)
|
49
53
|
end
|
50
54
|
|
51
55
|
def external_capture(command_sym, command_args)
|
@@ -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
|
@@ -97,7 +95,7 @@ module Aspera
|
|
97
95
|
1.upto(count) do |i|
|
98
96
|
percent = i * 100 / (count + 1)
|
99
97
|
filename = get_tmp_num_filepath(temp_folder, index1 + i)
|
100
|
-
external_command(:
|
98
|
+
external_command(:magick, ['composite', '-blend', percent, img2, img1, filename])
|
101
99
|
end
|
102
100
|
end
|
103
101
|
|
@@ -1,11 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'aspera/environment'
|
4
|
+
require 'singleton'
|
4
5
|
|
5
6
|
module Aspera
|
6
7
|
module Products
|
7
8
|
class Connect
|
9
|
+
include Singleton
|
8
10
|
APP_NAME = 'IBM Aspera Connect'
|
11
|
+
|
9
12
|
class << self
|
10
13
|
# standard folder locations
|
11
14
|
def locations
|
@@ -40,9 +43,40 @@ module Aspera
|
|
40
43
|
app_root: File.join(Dir.home, '.aspera', 'connect'),
|
41
44
|
run_root: File.join(Dir.home, '.aspera', 'connect')
|
42
45
|
}]
|
43
|
-
end.map
|
46
|
+
end.map{ |i| i.merge({expected: APP_NAME})}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def cdn_api
|
51
|
+
Rest.new(base_url: CDN_BASE_URL)
|
52
|
+
end
|
53
|
+
|
54
|
+
# retrieve structure from cloud (CDN) with all versions available
|
55
|
+
def versions
|
56
|
+
if @connect_versions.nil?
|
57
|
+
javascript = cdn_api.call(operation: 'GET', subpath: VERSION_INFO_FILE)
|
58
|
+
# get result on one line
|
59
|
+
connect_versions_javascript = javascript[:http].body.gsub(/\r?\n\s*/, '')
|
60
|
+
Log.log.debug{"javascript=[\n#{connect_versions_javascript}\n]"}
|
61
|
+
# get javascript object only
|
62
|
+
found = connect_versions_javascript.match(/^.*? = (.*);/)
|
63
|
+
raise Cli::Error, 'Problem when getting connect versions from internet' if found.nil?
|
64
|
+
all_data = JSON.parse(found[1])
|
65
|
+
@connect_versions = all_data['entries']
|
44
66
|
end
|
67
|
+
return @connect_versions
|
45
68
|
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def initialize
|
73
|
+
@connect_versions = nil
|
74
|
+
end
|
75
|
+
|
76
|
+
VERSION_INFO_FILE = 'connectversions.js' # cspell: disable-line
|
77
|
+
CDN_BASE_URL = 'https://d3gcli72yxqn2z.cloudfront.net/connect'
|
78
|
+
|
79
|
+
private_constant :VERSION_INFO_FILE, :CDN_BASE_URL
|
46
80
|
end
|
47
81
|
end
|
48
82
|
end
|
@@ -4,8 +4,8 @@ require 'aspera/environment'
|
|
4
4
|
|
5
5
|
module Aspera
|
6
6
|
module Products
|
7
|
-
# Aspera Desktop
|
8
|
-
class
|
7
|
+
# Client Aspera for Desktop
|
8
|
+
class Desktop
|
9
9
|
APP_NAME = 'IBM Aspera for Desktop'
|
10
10
|
APP_IDENTIFIER = 'com.ibm.software.aspera.desktop'
|
11
11
|
class << self
|
@@ -18,7 +18,7 @@ module Aspera
|
|
18
18
|
sub_bin: File.join('Contents', 'Resources', 'transferd', 'bin')
|
19
19
|
}]
|
20
20
|
else []
|
21
|
-
end.map
|
21
|
+
end.map{ |i| i.merge({expected: APP_NAME})}
|
22
22
|
end
|
23
23
|
|
24
24
|
def log_file
|
@@ -4,13 +4,18 @@ module Aspera
|
|
4
4
|
module Products
|
5
5
|
class Transferd
|
6
6
|
APP_NAME = 'IBM Aspera Transfer Daemon'
|
7
|
+
V1_DAEMON_NAME = 'asperatransferd'
|
8
|
+
# from 1.1.5
|
9
|
+
V2_DAEMON_NAME = 'transferd'
|
10
|
+
# folders to extract from SDK archive
|
11
|
+
RUNTIME_FOLDERS = %w[bin lib sbin aspera].freeze
|
7
12
|
class << self
|
8
13
|
# standard folder locations
|
9
14
|
def locations
|
10
15
|
[{
|
11
16
|
app_root: sdk_directory,
|
12
17
|
sub_bin: ''
|
13
|
-
}].map
|
18
|
+
}].map{ |i| i.merge({expected: APP_NAME})}
|
14
19
|
end
|
15
20
|
|
16
21
|
# location of SDK files
|
@@ -28,7 +33,9 @@ module Aspera
|
|
28
33
|
end
|
29
34
|
|
30
35
|
def transferd_path
|
31
|
-
|
36
|
+
v1_path = File.join(sdk_directory, Environment.exe_file(V1_DAEMON_NAME))
|
37
|
+
return v1_path if File.exist?(v1_path)
|
38
|
+
return File.join(sdk_directory, Environment.exe_file(V2_DAEMON_NAME))
|
32
39
|
end
|
33
40
|
|
34
41
|
# Well, the port number is only in log file
|
@@ -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
@@ -31,15 +31,18 @@ module Aspera
|
|
31
31
|
class RestParameters
|
32
32
|
include Singleton
|
33
33
|
|
34
|
-
attr_accessor :user_agent, :download_partial_suffix, :retry_on_error, :retry_sleep, :session_cb, :progress_bar
|
34
|
+
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
35
|
|
36
36
|
private
|
37
37
|
|
38
38
|
def initialize
|
39
39
|
@user_agent = 'RubyAsperaRest'
|
40
40
|
@download_partial_suffix = '.http_partial'
|
41
|
-
@retry_on_error =
|
42
|
-
@
|
41
|
+
@retry_on_error = false
|
42
|
+
@retry_on_timeout = true
|
43
|
+
@retry_on_unavailable = true
|
44
|
+
@retry_max = 1
|
45
|
+
@retry_sleep = 4
|
43
46
|
@session_cb = nil
|
44
47
|
@progress_bar = nil
|
45
48
|
end
|
@@ -57,12 +60,18 @@ module Aspera
|
|
57
60
|
# error message when entity not found (TODO: use specific exception)
|
58
61
|
ENTITY_NOT_FOUND = 'No such'
|
59
62
|
|
63
|
+
MIME_JSON = 'application/json'
|
64
|
+
MIME_WWW = 'application/x-www-form-urlencoded'
|
65
|
+
MIME_TEXT = 'text/plain'
|
66
|
+
|
60
67
|
# Content-Type that are JSON
|
61
|
-
JSON_DECODE = [
|
68
|
+
JSON_DECODE = [MIME_JSON, 'application/vnd.api+json', 'application/x-javascript'].freeze
|
69
|
+
|
70
|
+
UNAVAILABLE_CODES = ['503']
|
62
71
|
|
63
72
|
class << self
|
64
73
|
# @return [String] Basic auth token
|
65
|
-
def
|
74
|
+
def basic_authorization(user, pass); return "Basic #{Base64.strict_encode64("#{user}:#{pass}")}"; end
|
66
75
|
|
67
76
|
# Build a parameter list prefixed with "[]"
|
68
77
|
# @param values [Array] list of values
|
@@ -98,7 +107,7 @@ module Aspera
|
|
98
107
|
end
|
99
108
|
end
|
100
109
|
when Array
|
101
|
-
Aspera.assert(query.all?{|i| i.is_a?(Array) && i.length.eql?(2)})
|
110
|
+
Aspera.assert(query.all?{ |i| i.is_a?(Array) && i.length.eql?(2)}){'Query must be array of arrays or 2 elements'}
|
102
111
|
query_array = query
|
103
112
|
else
|
104
113
|
raise "Query must be Hash or Array, not #{query.class}"
|
@@ -171,7 +180,7 @@ module Aspera
|
|
171
180
|
one[1] = one[1].gsub(/\A"|"\z/, '')
|
172
181
|
one
|
173
182
|
end.to_h
|
174
|
-
{
|
183
|
+
{type: type.downcase, parameters: parameters}
|
175
184
|
end
|
176
185
|
end
|
177
186
|
|
@@ -189,6 +198,7 @@ module Aspera
|
|
189
198
|
|
190
199
|
attr_reader :base_url
|
191
200
|
attr_reader :auth_params
|
201
|
+
attr_reader :headers
|
192
202
|
|
193
203
|
# @return creation parameters
|
194
204
|
def params
|
@@ -238,7 +248,7 @@ module Aspera
|
|
238
248
|
@http_session = nil
|
239
249
|
@redirect_max = redirect_max
|
240
250
|
Aspera.assert_type(@redirect_max, Integer)
|
241
|
-
@headers = headers
|
251
|
+
@headers = headers.clone
|
242
252
|
Aspera.assert_type(@headers, Hash)
|
243
253
|
@headers['User-Agent'] ||= RestParameters.instance.user_agent
|
244
254
|
# OAuth object (created on demand)
|
@@ -249,7 +259,7 @@ module Aspera
|
|
249
259
|
def oauth
|
250
260
|
if @oauth.nil?
|
251
261
|
Aspera.assert(@auth_params[:type].eql?(:oauth2)){'no OAuth defined'}
|
252
|
-
oauth_parameters = @auth_params.reject
|
262
|
+
oauth_parameters = @auth_params.reject{ |k, _v| k.eql?(:type)}
|
253
263
|
Log.log.debug{Log.dump('oauth parameters', oauth_parameters)}
|
254
264
|
@oauth = OAuth::Factory.instance.create(**oauth_parameters)
|
255
265
|
end
|
@@ -260,20 +270,20 @@ module Aspera
|
|
260
270
|
# @param operation [String] HTTP operation (GET, POST, PUT, DELETE)
|
261
271
|
# @param subpath [String] subpath of REST API
|
262
272
|
# @param query [Hash] URL parameters
|
273
|
+
# @param content_type [String,nil] Type of body parameters (one of MIME_*) and serialization, else use headers
|
263
274
|
# @param body [Hash, String] body parameters
|
264
|
-
# @param
|
275
|
+
# @param headers [Hash] additional headers (override Content-Type)
|
265
276
|
# @param save_to_file (filepath)
|
266
277
|
# @param return_error (bool)
|
267
|
-
# @param headers [Hash] additional headers
|
268
278
|
def call(
|
269
279
|
operation:,
|
270
280
|
subpath: nil,
|
271
281
|
query: nil,
|
282
|
+
content_type: nil,
|
272
283
|
body: nil,
|
273
|
-
|
284
|
+
headers: nil,
|
274
285
|
save_to_file: nil,
|
275
|
-
return_error: false
|
276
|
-
headers: nil
|
286
|
+
return_error: false
|
277
287
|
)
|
278
288
|
subpath = subpath.to_s if subpath.is_a?(Symbol)
|
279
289
|
subpath = '' if subpath.nil?
|
@@ -295,7 +305,7 @@ module Aspera
|
|
295
305
|
Log.log.debug('using Basic auth')
|
296
306
|
# done in build_req
|
297
307
|
when :oauth2
|
298
|
-
headers['Authorization'] = oauth.
|
308
|
+
headers['Authorization'] = oauth.authorization unless headers.key?('Authorization')
|
299
309
|
when :url
|
300
310
|
query ||= {}
|
301
311
|
@auth_params[:url_query].each do |key, value|
|
@@ -317,19 +327,18 @@ module Aspera
|
|
317
327
|
rescue NameError
|
318
328
|
raise "unsupported operation : #{operation}"
|
319
329
|
end
|
320
|
-
case
|
321
|
-
when
|
330
|
+
case content_type
|
331
|
+
when nil # ignore
|
332
|
+
when MIME_JSON
|
322
333
|
req.body = JSON.generate(body) # , ascii_only: true
|
323
|
-
req['Content-Type'] =
|
324
|
-
when
|
334
|
+
req['Content-Type'] = MIME_JSON
|
335
|
+
when MIME_WWW
|
325
336
|
req.body = URI.encode_www_form(body)
|
326
|
-
req['Content-Type'] =
|
327
|
-
when
|
337
|
+
req['Content-Type'] = MIME_WWW
|
338
|
+
when MIME_TEXT
|
328
339
|
req.body = body
|
329
|
-
req['Content-Type'] =
|
330
|
-
|
331
|
-
else
|
332
|
-
raise "unsupported body type : #{body_type}"
|
340
|
+
req['Content-Type'] = MIME_TEXT
|
341
|
+
else Aspera.error_unexpected_value(content_type){'body type'}
|
333
342
|
end
|
334
343
|
# set headers
|
335
344
|
headers.each do |key, value|
|
@@ -338,10 +347,8 @@ module Aspera
|
|
338
347
|
# :type = :basic
|
339
348
|
req.basic_auth(@auth_params[:username], @auth_params[:password]) if @auth_params[:type].eql?(:basic)
|
340
349
|
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
|
350
|
+
# we try the call, and will retry on some error types
|
351
|
+
error_tries ||= 1 + RestParameters.instance.retry_max
|
345
352
|
# initialize with number of initial retries allowed, nil gives zero
|
346
353
|
tries_remain_redirect = @redirect_max if tries_remain_redirect.nil?
|
347
354
|
Log.log.debug("send request (retries=#{tries_remain_redirect})")
|
@@ -350,7 +357,7 @@ module Aspera
|
|
350
357
|
# make http request (pipelined)
|
351
358
|
http_session.request(req) do |response|
|
352
359
|
result[:http] = response
|
353
|
-
result_mime = self.class.parse_header(result[:http]['Content-Type'] ||
|
360
|
+
result_mime = self.class.parse_header(result[:http]['Content-Type'] || MIME_TEXT)[:type]
|
354
361
|
# JSON data needs to be parsed, in case it contains an error code
|
355
362
|
if !save_to_file.nil? &&
|
356
363
|
result[:http].code.to_s.start_with?('2') &&
|
@@ -394,7 +401,7 @@ module Aspera
|
|
394
401
|
when *JSON_DECODE
|
395
402
|
result[:data] = JSON.parse(result[:http].body) rescue result[:http].body
|
396
403
|
Log.log.debug{Log.dump('result_data', result[:data])}
|
397
|
-
else # when
|
404
|
+
else # when MIME_TEXT
|
398
405
|
result[:data] = result[:http].body
|
399
406
|
end
|
400
407
|
RestErrorAnalyzer.instance.raise_on_error(req, result)
|
@@ -402,25 +409,27 @@ module Aspera
|
|
402
409
|
rescue RestCallError => e
|
403
410
|
do_retry = false
|
404
411
|
# AoC have some timeout , like Connect to platform.bss.asperasoft.com:443 ...
|
405
|
-
do_retry
|
412
|
+
do_retry ||= true if e.response.body.include?('failed: connect timed out') && RestParameters.instance.retry_on_timeout
|
413
|
+
# AoC sometimes not available
|
414
|
+
do_retry ||= true if RestParameters.instance.retry_on_unavailable && UNAVAILABLE_CODES.include?(result[:http].code.to_s)
|
406
415
|
# possibility to retry anything if it fails
|
407
|
-
do_retry
|
416
|
+
do_retry ||= true if RestParameters.instance.retry_on_error
|
408
417
|
# not authorized: oauth token expired
|
409
418
|
if @not_auth_codes.include?(result[:http].code.to_s) && @auth_params[:type].eql?(:oauth2)
|
410
419
|
begin
|
411
420
|
# try to use refresh token
|
412
|
-
req['Authorization'] = oauth.
|
421
|
+
req['Authorization'] = oauth.authorization(refresh: true)
|
413
422
|
rescue RestCallError => e_tok
|
414
423
|
e = e_tok
|
415
424
|
Log.log.error('refresh failed'.bg_red)
|
416
425
|
# regenerate a brand new token
|
417
|
-
req['Authorization'] = oauth.
|
426
|
+
req['Authorization'] = oauth.authorization(cache: false)
|
418
427
|
end
|
419
428
|
Log.log.debug{"using new token=#{headers['Authorization']}"}
|
420
|
-
do_retry
|
429
|
+
do_retry ||= true
|
421
430
|
end
|
422
|
-
if do_retry
|
423
|
-
sleep(RestParameters.instance.retry_sleep) unless RestParameters.instance.retry_sleep.
|
431
|
+
if do_retry && (error_tries -= 1).positive?
|
432
|
+
sleep(RestParameters.instance.retry_sleep) unless RestParameters.instance.retry_sleep.eql?(0)
|
424
433
|
retry
|
425
434
|
end
|
426
435
|
# redirect ? (any code beginning with 3)
|
@@ -436,7 +445,7 @@ module Aspera
|
|
436
445
|
end
|
437
446
|
# forwards the request to the new location
|
438
447
|
return self.class.new(base_url: new_url, redirect_max: tries_remain_redirect).call(
|
439
|
-
operation: operation, query: query, body: body,
|
448
|
+
operation: operation, query: query, body: body, content_type: content_type,
|
440
449
|
save_to_file: save_to_file, return_error: return_error, headers: headers)
|
441
450
|
end
|
442
451
|
# raise exception if could not retry and not return error in result
|
@@ -452,23 +461,23 @@ module Aspera
|
|
452
461
|
#
|
453
462
|
|
454
463
|
def create(subpath, params)
|
455
|
-
return call(operation: 'POST', subpath: subpath, headers: {'Accept' =>
|
464
|
+
return call(operation: 'POST', subpath: subpath, headers: {'Accept' => MIME_JSON}, body: params, content_type: MIME_JSON)[:data]
|
456
465
|
end
|
457
466
|
|
458
467
|
def read(subpath, query=nil)
|
459
|
-
return call(operation: 'GET', subpath: subpath, headers: {'Accept' =>
|
468
|
+
return call(operation: 'GET', subpath: subpath, headers: {'Accept' => MIME_JSON}, query: query)[:data]
|
460
469
|
end
|
461
470
|
|
462
471
|
def update(subpath, params)
|
463
|
-
return call(operation: 'PUT', subpath: subpath, headers: {'Accept' =>
|
472
|
+
return call(operation: 'PUT', subpath: subpath, headers: {'Accept' => MIME_JSON}, body: params, content_type: MIME_JSON)[:data]
|
464
473
|
end
|
465
474
|
|
466
475
|
def delete(subpath, params=nil)
|
467
|
-
return call(operation: 'DELETE', subpath: subpath, headers: {'Accept' =>
|
476
|
+
return call(operation: 'DELETE', subpath: subpath, headers: {'Accept' => MIME_JSON}, query: params)[:data]
|
468
477
|
end
|
469
478
|
|
470
479
|
def cancel(subpath)
|
471
|
-
return call(operation: 'CANCEL', subpath: subpath, headers: {'Accept' =>
|
480
|
+
return call(operation: 'CANCEL', subpath: subpath, headers: {'Accept' => MIME_JSON})[:data]
|
472
481
|
end
|
473
482
|
|
474
483
|
# Query entity by general search (read with parameter `q`)
|
@@ -490,11 +499,11 @@ module Aspera
|
|
490
499
|
else
|
491
500
|
# multiple case insensitive partial matches, try case insensitive full match
|
492
501
|
# (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)}
|
502
|
+
name_matches = matching_items.select{ |i| i['name'].casecmp?(search_name)}
|
494
503
|
case name_matches.length
|
495
504
|
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']}}"
|
505
|
+
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
|
506
|
+
else raise "Two entities cannot have the same case insensitive name: #{name_matches.map{ |i| i['name']}}"
|
498
507
|
end
|
499
508
|
end
|
500
509
|
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'])
|