aspera-cli 4.14.0 → 4.15.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/CHANGELOG.md +54 -3
- data/CONTRIBUTING.md +7 -7
- data/README.md +1457 -880
- data/bin/ascli +18 -9
- data/bin/asession +12 -14
- data/examples/proxy.pac +1 -1
- data/lib/aspera/aoc.rb +198 -127
- data/lib/aspera/ascmd.rb +24 -14
- 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 +47 -12
- data/lib/aspera/cli/formatter.rb +260 -171
- data/lib/aspera/cli/hints.rb +80 -0
- data/lib/aspera/cli/main.rb +101 -147
- data/lib/aspera/cli/manager.rb +160 -124
- data/lib/aspera/cli/plugin.rb +70 -59
- data/lib/aspera/cli/plugins/alee.rb +0 -1
- data/lib/aspera/cli/plugins/aoc.rb +239 -273
- data/lib/aspera/cli/plugins/ats.rb +8 -5
- data/lib/aspera/cli/plugins/bss.rb +2 -2
- data/lib/aspera/cli/plugins/config.rb +516 -375
- 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 +99 -84
- data/lib/aspera/cli/plugins/faspex5.rb +179 -148
- data/lib/aspera/cli/plugins/node.rb +219 -153
- data/lib/aspera/cli/plugins/orchestrator.rb +52 -17
- data/lib/aspera/cli/plugins/preview.rb +46 -32
- data/lib/aspera/cli/plugins/server.rb +57 -17
- data/lib/aspera/cli/plugins/shares.rb +34 -12
- data/lib/aspera/cli/sync_actions.rb +68 -0
- data/lib/aspera/cli/transfer_agent.rb +45 -55
- data/lib/aspera/cli/transfer_progress.rb +74 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/colors.rb +3 -1
- data/lib/aspera/command_line_builder.rb +14 -11
- data/lib/aspera/cos_node.rb +3 -2
- data/lib/aspera/environment.rb +17 -6
- data/lib/aspera/fasp/agent_aspera.rb +126 -0
- data/lib/aspera/fasp/agent_base.rb +31 -77
- data/lib/aspera/fasp/agent_connect.rb +21 -22
- data/lib/aspera/fasp/agent_direct.rb +88 -102
- data/lib/aspera/fasp/agent_httpgw.rb +196 -192
- data/lib/aspera/fasp/agent_node.rb +41 -34
- data/lib/aspera/fasp/agent_trsdk.rb +75 -34
- data/lib/aspera/fasp/error_info.rb +2 -2
- data/lib/aspera/fasp/faux_file.rb +52 -0
- data/lib/aspera/fasp/installation.rb +43 -184
- data/lib/aspera/fasp/management.rb +244 -0
- data/lib/aspera/fasp/parameters.rb +59 -26
- data/lib/aspera/fasp/parameters.yaml +75 -8
- data/lib/aspera/fasp/products.rb +162 -0
- data/lib/aspera/fasp/transfer_spec.rb +1 -1
- data/lib/aspera/fasp/uri.rb +4 -4
- data/lib/aspera/faspex_gw.rb +2 -2
- data/lib/aspera/faspex_postproc.rb +2 -2
- data/lib/aspera/hash_ext.rb +2 -2
- data/lib/aspera/json_rpc.rb +49 -0
- data/lib/aspera/line_logger.rb +23 -0
- data/lib/aspera/log.rb +57 -16
- data/lib/aspera/node.rb +97 -14
- data/lib/aspera/oauth.rb +36 -18
- data/lib/aspera/open_application.rb +4 -4
- data/lib/aspera/persistency_folder.rb +2 -2
- data/lib/aspera/preview/file_types.rb +4 -2
- data/lib/aspera/preview/generator.rb +22 -35
- data/lib/aspera/preview/options.rb +2 -0
- data/lib/aspera/preview/terminal.rb +24 -13
- data/lib/aspera/preview/utils.rb +19 -26
- data/lib/aspera/rest.rb +103 -72
- data/lib/aspera/rest_call_error.rb +1 -1
- data/lib/aspera/rest_error_analyzer.rb +15 -14
- data/lib/aspera/rest_errors_aspera.rb +37 -34
- data/lib/aspera/secret_hider.rb +14 -16
- data/lib/aspera/ssh.rb +4 -1
- data/lib/aspera/sync.rb +128 -122
- data/lib/aspera/temp_file_manager.rb +10 -3
- data/lib/aspera/web_auth.rb +10 -7
- data/lib/aspera/web_server_simple.rb +9 -4
- data.tar.gz.sig +0 -0
- metadata +33 -15
- 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/oauth.rb
CHANGED
@@ -24,18 +24,23 @@ module Aspera
|
|
24
24
|
# OAuth methods supported by default
|
25
25
|
STD_AUTH_TYPES = %i[web jwt].freeze
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
27
|
+
@@globals = { # rubocop:disable Style/ClassVars
|
28
|
+
# remove 5 minutes to account for time offset between client and server (TODO: configurable?)
|
29
|
+
jwt_accepted_offset_sec: 300,
|
30
|
+
# one hour validity (TODO: configurable?)
|
31
|
+
jwt_expiry_offset_sec: 3600,
|
32
|
+
# tokens older than 30 minutes will be discarded from cache
|
33
|
+
token_cache_expiry_sec: 1800,
|
34
|
+
# tokens valid for less than this duration will be regenerated
|
35
|
+
token_expiration_guard_sec: 120
|
36
|
+
}
|
37
|
+
|
35
38
|
# a prefix for persistency of tokens (simplify garbage collect)
|
36
39
|
PERSIST_CATEGORY_TOKEN = 'token'
|
40
|
+
# prefix for bearer token when in header
|
41
|
+
BEARER_PREFIX = 'Bearer '
|
37
42
|
|
38
|
-
private_constant :
|
43
|
+
private_constant :PERSIST_CATEGORY_TOKEN, :BEARER_PREFIX
|
39
44
|
|
40
45
|
# persistency manager
|
41
46
|
@persist = nil
|
@@ -45,10 +50,23 @@ module Aspera
|
|
45
50
|
@id_handlers = {}
|
46
51
|
|
47
52
|
class << self
|
53
|
+
def bearer_build(token)
|
54
|
+
return BEARER_PREFIX + token
|
55
|
+
end
|
56
|
+
|
57
|
+
def bearer_extract(token)
|
58
|
+
raise 'not a bearer token, wrong prefix' unless bearer?(token)
|
59
|
+
return token[BEARER_PREFIX.length..-1]
|
60
|
+
end
|
61
|
+
|
62
|
+
def bearer?(token)
|
63
|
+
return token.start_with?(BEARER_PREFIX)
|
64
|
+
end
|
65
|
+
|
48
66
|
def persist_mgr=(manager)
|
49
67
|
@persist = manager
|
50
68
|
# cleanup expired tokens
|
51
|
-
@persist.garbage_collect(PERSIST_CATEGORY_TOKEN,
|
69
|
+
@persist.garbage_collect(PERSIST_CATEGORY_TOKEN, @@globals[:token_cache_expiry_sec])
|
52
70
|
end
|
53
71
|
|
54
72
|
def persist_mgr
|
@@ -153,9 +171,9 @@ module Aspera
|
|
153
171
|
Log.log.info{"seconds=#{seconds_since_epoch}"}
|
154
172
|
raise 'missing JWT payload' unless oauth.specific_parameters[:payload].is_a?(Hash)
|
155
173
|
jwt_payload = {
|
156
|
-
exp: seconds_since_epoch +
|
157
|
-
nbf: seconds_since_epoch -
|
158
|
-
iat: seconds_since_epoch -
|
174
|
+
exp: seconds_since_epoch + @@globals[:jwt_expiry_offset_sec], # expiration time
|
175
|
+
nbf: seconds_since_epoch - @@globals[:jwt_accepted_offset_sec], # not before
|
176
|
+
iat: seconds_since_epoch - @@globals[:jwt_accepted_offset_sec] + 1, # issued at (we tell a little in the past so that server always accepts)
|
159
177
|
jti: SecureRandom.uuid # JWT id
|
160
178
|
}.merge(oauth.specific_parameters[:payload])
|
161
179
|
Log.log.debug{"JWT jwt_payload=[#{jwt_payload}]"}
|
@@ -213,8 +231,8 @@ module Aspera
|
|
213
231
|
@generic_parameters.delete(:base_url)
|
214
232
|
@generic_parameters.delete(:auth)
|
215
233
|
@generic_parameters.delete(@generic_parameters[:grant_method])
|
216
|
-
Log.dump(:generic_parameters, @generic_parameters)
|
217
|
-
Log.dump(:specific_parameters, @specific_parameters)
|
234
|
+
Log.log.debug{Log.dump(:generic_parameters, @generic_parameters)}
|
235
|
+
Log.log.debug{Log.dump(:specific_parameters, @specific_parameters)}
|
218
236
|
end
|
219
237
|
|
220
238
|
public
|
@@ -259,14 +277,14 @@ module Aspera
|
|
259
277
|
# `direct` agent is equipped with refresh code
|
260
278
|
if !use_refresh_token && !token_data.nil?
|
261
279
|
decoded_token = self.class.decode_token(token_data[@generic_parameters[:token_field]])
|
262
|
-
Log.dump('decoded_token', decoded_token) unless decoded_token.nil?
|
280
|
+
Log.log.debug{Log.dump('decoded_token', decoded_token)} unless decoded_token.nil?
|
263
281
|
if decoded_token.is_a?(Hash)
|
264
282
|
expires_at_sec =
|
265
283
|
if decoded_token['expires_at'].is_a?(String) then DateTime.parse(decoded_token['expires_at']).to_time
|
266
284
|
elsif decoded_token['exp'].is_a?(Integer) then Time.at(decoded_token['exp'])
|
267
285
|
end
|
268
286
|
# force refresh if we see a token too close from expiration
|
269
|
-
use_refresh_token = true if expires_at_sec.is_a?(Time) && (expires_at_sec - Time.now) <
|
287
|
+
use_refresh_token = true if expires_at_sec.is_a?(Time) && (expires_at_sec - Time.now) < @@globals[:token_expiration_guard_sec]
|
270
288
|
Log.log.debug{"Expiration: #{expires_at_sec} / #{use_refresh_token}"}
|
271
289
|
end
|
272
290
|
end
|
@@ -306,7 +324,7 @@ module Aspera
|
|
306
324
|
end # if ! in_cache
|
307
325
|
raise "API error: No such field in answer: #{@generic_parameters[:token_field]}" unless token_data.key?(@generic_parameters[:token_field])
|
308
326
|
# ok we shall have a token here
|
309
|
-
return
|
327
|
+
return self.class.bearer_build(token_data[@generic_parameters[:token_field]])
|
310
328
|
end
|
311
329
|
end # OAuth
|
312
330
|
end # Aspera
|
@@ -27,7 +27,7 @@ module Aspera
|
|
27
27
|
def uri_graphical(uri)
|
28
28
|
case Aspera::Environment.os
|
29
29
|
when Aspera::Environment::OS_X then return system('open', uri.to_s)
|
30
|
-
when Aspera::Environment::OS_WINDOWS then return system('start', 'explorer',
|
30
|
+
when Aspera::Environment::OS_WINDOWS then return system('start', 'explorer', %Q{"#{uri}"})
|
31
31
|
when Aspera::Environment::OS_LINUX then return system('xdg-open', uri.to_s)
|
32
32
|
else
|
33
33
|
raise "no graphical open method for #{Aspera::Environment.os}"
|
@@ -38,7 +38,7 @@ module Aspera
|
|
38
38
|
if ENV.key?('EDITOR')
|
39
39
|
system(ENV['EDITOR'], file_path.to_s)
|
40
40
|
elsif Aspera::Environment.os.eql?(Aspera::Environment::OS_WINDOWS)
|
41
|
-
system('notepad.exe',
|
41
|
+
system('notepad.exe', %Q{"#{file_path}"})
|
42
42
|
else
|
43
43
|
uri_graphical(file_path.to_s)
|
44
44
|
end
|
@@ -59,9 +59,9 @@ module Aspera
|
|
59
59
|
when :text
|
60
60
|
case the_url.to_s
|
61
61
|
when /^http/
|
62
|
-
puts "USER ACTION: please enter this url in a browser:\n
|
62
|
+
puts "USER ACTION: please enter this url in a browser:\n#{the_url.to_s.red}\n"
|
63
63
|
else
|
64
|
-
puts "USER ACTION: open this:\n
|
64
|
+
puts "USER ACTION: open this:\n#{the_url.to_s.red}\n"
|
65
65
|
end
|
66
66
|
else
|
67
67
|
raise StandardError, "unsupported url open method: #{@url_method}"
|
@@ -37,7 +37,7 @@ module Aspera
|
|
37
37
|
raise 'value: only String supported' unless value.is_a?(String)
|
38
38
|
persist_filepath = id_to_filepath(object_id)
|
39
39
|
Log.log.debug{"persistency saving: #{persist_filepath}"}
|
40
|
-
|
40
|
+
FileUtils.rm_f(persist_filepath)
|
41
41
|
File.write(persist_filepath, value)
|
42
42
|
Environment.restrict_file_access(persist_filepath)
|
43
43
|
@cache[object_id] = value
|
@@ -46,7 +46,7 @@ module Aspera
|
|
46
46
|
def delete(object_id)
|
47
47
|
persist_filepath = id_to_filepath(object_id)
|
48
48
|
Log.log.debug{"persistency deleting: #{persist_filepath}"}
|
49
|
-
|
49
|
+
FileUtils.rm_f(persist_filepath)
|
50
50
|
@cache.delete(object_id)
|
51
51
|
end
|
52
52
|
|
@@ -281,7 +281,7 @@ module Aspera
|
|
281
281
|
|
282
282
|
# use mime magic to find mime type based on file content (magic numbers)
|
283
283
|
def mime_from_file(filepath)
|
284
|
-
# moved here, as mimemagic can cause installation issues
|
284
|
+
# moved here, as `mimemagic` can cause installation issues
|
285
285
|
require 'mimemagic'
|
286
286
|
require 'mimemagic/version'
|
287
287
|
require 'mimemagic/overlay' if MimeMagic::VERSION.start_with?('0.3.')
|
@@ -297,9 +297,10 @@ module Aspera
|
|
297
297
|
return detected_mime
|
298
298
|
end
|
299
299
|
|
300
|
-
# return file type, one of enum CONVERSION_TYPES
|
301
300
|
# @param filepath [String] full path to file
|
302
301
|
# @param mimetype [String] provided by node api
|
302
|
+
# @return file type, one of enum CONVERSION_TYPES
|
303
|
+
# @raise [RuntimeError] if no conversion type found
|
303
304
|
def conversion_type(filepath, mimetype)
|
304
305
|
Log.log.debug{"conversion_type(#{filepath},m=#{mimetype},t=#{@use_mimemagic})"}
|
305
306
|
# 1- get type from provided mime type, using local mapping
|
@@ -322,6 +323,7 @@ module Aspera
|
|
322
323
|
# 3- else, from extensions, using local mapping
|
323
324
|
extension = File.extname(filepath.downcase)[1..-1]
|
324
325
|
conversion_type = SUPPORTED_EXTENSIONS[extension] if conversion_type.nil?
|
326
|
+
raise "no conversion type found for extension #{extension}" if conversion_type.nil?
|
325
327
|
Log.log.debug{"conversion_type(#{extension}): #{conversion_type.class.name} [#{conversion_type}]"}
|
326
328
|
return conversion_type
|
327
329
|
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# ffmpeg options:
|
4
4
|
# spellchecker:ignore pauseframes libx264 trunc bufsize muxer apng libmp3lame maxrate posterize movflags faststart
|
5
|
-
# spellchecker:ignore palettegen paletteuse pointsize bordercolor repage lanczos
|
5
|
+
# spellchecker:ignore palettegen paletteuse pointsize bordercolor repage lanczos unoconv optipng reencode conv transframes
|
6
6
|
|
7
7
|
require 'open3'
|
8
8
|
require 'aspera/preview/options'
|
@@ -19,6 +19,7 @@ module Aspera
|
|
19
19
|
FFMPEG_OPTIONS_LIST = %w[in out].freeze
|
20
20
|
|
21
21
|
# CLI needs to know conversion type to know if need skip it
|
22
|
+
# one of CONVERSION_TYPES
|
22
23
|
attr_reader :conversion_type
|
23
24
|
|
24
25
|
# @param src source file path
|
@@ -32,55 +33,40 @@ module Aspera
|
|
32
33
|
# -> preview_format is one of Generator::PREVIEW_FORMATS
|
33
34
|
# the conversion video->mp4 is implemented in methods: convert_video_to_mp4_using_<video_conversion>
|
34
35
|
# -> conversion method is one of Generator::VIDEO_CONVERSION_METHODS
|
35
|
-
def initialize(
|
36
|
-
@options = options
|
36
|
+
def initialize(src, dst, options, main_temp_dir, api_mime_type)
|
37
37
|
@source_file_path = src
|
38
38
|
@destination_file_path = dst
|
39
|
+
@options = options
|
39
40
|
@temp_folder = File.join(main_temp_dir, @source_file_path.split('/').last.gsub(/\s/, '_').gsub(/\W/, ''))
|
40
41
|
# extract preview format from extension of target file
|
41
|
-
@
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
# combination of: conversion type and output format (and video_conversion for video)
|
47
|
-
def processing_method_symb
|
48
|
-
name = "convert_#{@conversion_type}_to_#{@preview_format_symb}"
|
49
|
-
if @conversion_type.eql?(:video)
|
50
|
-
case @preview_format_symb
|
42
|
+
@preview_format_sym = File.extname(@destination_file_path).gsub(/^\./, '').to_sym
|
43
|
+
conversion_type = FileTypes.instance.conversion_type(@source_file_path, api_mime_type)
|
44
|
+
@processing_method = "convert_#{conversion_type}_to_#{@preview_format_sym}"
|
45
|
+
if conversion_type.eql?(:video)
|
46
|
+
case @preview_format_sym
|
51
47
|
when :mp4
|
52
|
-
|
48
|
+
@processing_method = "#{@processing_method}_using_#{@options.video_conversion}"
|
53
49
|
when :png
|
54
|
-
|
50
|
+
@processing_method = "#{@processing_method}_using_#{@options.video_png_conv}"
|
55
51
|
end
|
56
52
|
end
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
# @return true if conversion is supported.
|
62
|
-
# for instance, plaintext to mp4 is not supported
|
63
|
-
def supported?
|
64
|
-
return false if @conversion_type.nil?
|
65
|
-
return respond_to?(processing_method_symb, true)
|
53
|
+
@processing_method = @processing_method.to_sym
|
54
|
+
Log.log.debug{"method: #{@processing_method}"}
|
55
|
+
raise "no processing know for #{conversion_type} -> #{@preview_format_sym}" unless respond_to?(@processing_method, true)
|
66
56
|
end
|
67
57
|
|
68
58
|
# create preview as specified in constructor
|
69
59
|
def generate
|
70
|
-
|
71
|
-
method_symb = processing_method_symb
|
72
|
-
Log.log.info{"#{@source_file_path}->#{@destination_file_path} (#{method_symb})"}
|
60
|
+
Log.log.info{"#{@source_file_path}->#{@destination_file_path} (#{@processing_method})"}
|
73
61
|
begin
|
74
|
-
send(
|
62
|
+
send(@processing_method)
|
75
63
|
# check that generated size does not exceed maximum
|
76
64
|
result_size = File.size(@destination_file_path)
|
77
|
-
if result_size > @options.max_size
|
78
|
-
Log.log.warn{"preview size exceeds maximum allowed #{result_size} > #{@options.max_size}"}
|
79
|
-
end
|
65
|
+
Log.log.warn{"preview size exceeds maximum allowed #{result_size} > #{@options.max_size}"} if result_size > @options.max_size
|
80
66
|
rescue StandardError => e
|
81
67
|
Log.log.error{"Ignoring: #{e.message}"}
|
82
68
|
Log.log.debug(e.backtrace.join("\n").red)
|
83
|
-
FileUtils.cp(File.expand_path(@
|
69
|
+
FileUtils.cp(File.expand_path(@preview_format_sym.eql?(:mp4) ? 'video_error.png' : 'image_error.png', File.dirname(__FILE__)), @destination_file_path)
|
84
70
|
ensure
|
85
71
|
FileUtils.rm_rf(@temp_folder)
|
86
72
|
end
|
@@ -134,8 +120,8 @@ module Aspera
|
|
134
120
|
# generate n clips starting at offset
|
135
121
|
def convert_video_to_mp4_using_clips
|
136
122
|
p_duration = Utils.video_get_duration(@source_file_path)
|
137
|
-
|
138
|
-
File.open(
|
123
|
+
file_list_file = File.join(this_tmpdir, 'clip_files.txt')
|
124
|
+
File.open(file_list_file, 'w+') do |f|
|
139
125
|
1.upto(@options.clips_count.to_i) do |i|
|
140
126
|
offset_seconds = get_offset(p_duration, @options.video_start_sec.to_i, @options.clips_count.to_i, i)
|
141
127
|
tmp_file_name = format('clip%04d.mp4', i)
|
@@ -153,10 +139,11 @@ module Aspera
|
|
153
139
|
end
|
154
140
|
# concat clips
|
155
141
|
Utils.ffmpeg(
|
156
|
-
in_f:
|
142
|
+
in_f: file_list_file,
|
157
143
|
in_p: ['-f', 'concat'],
|
158
144
|
out_f: @destination_file_path,
|
159
145
|
out_p: ['-codec', 'copy'])
|
146
|
+
File.delete(file_list_file)
|
160
147
|
end
|
161
148
|
|
162
149
|
# do a simple re-encoding
|
@@ -7,25 +7,34 @@ require 'rainbow'
|
|
7
7
|
require 'io/console'
|
8
8
|
module Aspera
|
9
9
|
module Preview
|
10
|
-
#
|
10
|
+
# Display a picture in the terminal, either using coloured characters or iTerm2
|
11
11
|
class Terminal
|
12
|
-
# quantum depth is 8 or 16: convert xc: -format "%q" info:
|
13
12
|
# Rainbow only supports 8-bit colors
|
13
|
+
# quantum depth is 8 or 16, see: `convert xc: -format "%q" info:`
|
14
14
|
SHIFT_FOR_8_BIT = Magick::MAGICKCORE_QUANTUM_DEPTH - 8
|
15
|
-
|
15
|
+
# env vars to detect terminal type
|
16
16
|
TERM_ENV_VARS = %w[TERM_PROGRAM LC_TERMINAL].freeze
|
17
|
-
|
17
|
+
# terminal names that support iTerm2 image display
|
18
|
+
ITERM_NAMES = %w[iTerm WezTerm mintty].freeze
|
19
|
+
# TODO: retrieve terminal font ratio using some termcap ?
|
20
|
+
# ratio = font height / font width
|
21
|
+
DEFAULT_FONT_RATIO = 32.0 / 14.0
|
22
|
+
private_constant :SHIFT_FOR_8_BIT, :TERM_ENV_VARS, :ITERM_NAMES, :DEFAULT_FONT_RATIO
|
18
23
|
class << self
|
19
|
-
|
20
|
-
|
24
|
+
# @return [String] the image as text, or the iTerm2 escape sequence
|
25
|
+
# @param blob [String] the image as a binary string
|
26
|
+
# @param reserve [Integer] number of lines to reserve for other text than the image
|
27
|
+
# @param text [Boolean] true to display the image as text, false to use iTerm2
|
28
|
+
# @param double [Boolean] true to use colors on half lines, false to use colors on full lines
|
29
|
+
# @param font_ratio [Float] ratio = font height / font width
|
30
|
+
def build(blob, reserve: 3, text: false, double: true, font_ratio: DEFAULT_FONT_RATIO)
|
31
|
+
return iterm_display_image(blob) if iterm_supported? && !text
|
21
32
|
image = Magick::ImageList.new.from_blob(blob)
|
22
33
|
(term_rows, term_columns) = IO.console.winsize
|
23
|
-
term_rows -=
|
34
|
+
term_rows -= reserve
|
24
35
|
# compute scaling to fit terminal
|
25
36
|
fit_term_ratio = [term_rows / image.rows.to_f, term_columns / image.columns.to_f].min
|
26
|
-
|
27
|
-
font_ratio = 1.7
|
28
|
-
height_ratio = double_precision ? 2.0 : 1.0
|
37
|
+
height_ratio = double ? 2.0 : 1.0
|
29
38
|
image = image.scale((image.columns * fit_term_ratio * font_ratio).to_i, (image.rows * fit_term_ratio * height_ratio).to_i)
|
30
39
|
# get all pixel colors, adjusted for Rainbow
|
31
40
|
pixel_colors = []
|
@@ -39,10 +48,10 @@ module Aspera
|
|
39
48
|
# now generate text
|
40
49
|
text_pixels = []
|
41
50
|
pixel_colors.each_with_index do |row_data, row|
|
42
|
-
next if
|
51
|
+
next if double && (row.odd? || row.eql?(pixel_colors.length - 1))
|
43
52
|
row_data.each_with_index do |pixel_rgb, col|
|
44
53
|
text_pixels.push("\n") if col.eql?(0) && !row.eql?(0)
|
45
|
-
if
|
54
|
+
if double
|
46
55
|
text_pixels.push(Rainbow('▄').background(pixel_rgb).foreground(pixel_colors[row + 1][col]))
|
47
56
|
else
|
48
57
|
text_pixels.push(Rainbow(' ').background(pixel_rgb))
|
@@ -53,8 +62,10 @@ module Aspera
|
|
53
62
|
end
|
54
63
|
|
55
64
|
# display image in iTerm2
|
65
|
+
# https://iterm2.com/documentation-images.html
|
56
66
|
def iterm_display_image(blob)
|
57
67
|
# image = Magick::ImageList.new.from_blob(blob)
|
68
|
+
# parameters for iTerm2 image display
|
58
69
|
arguments = {
|
59
70
|
inline: 1,
|
60
71
|
preserveAspectRatio: 1,
|
@@ -63,7 +74,7 @@ module Aspera
|
|
63
74
|
# height: image.rows
|
64
75
|
}.map { |k, v| "#{k}=#{v}" }.join(';')
|
65
76
|
# \a is BEL, \e is ESC : https://github.com/ruby/ruby/blob/master/doc/syntax/literals.rdoc#label-Strings
|
66
|
-
#
|
77
|
+
# escape sequence for iTerm2 image display
|
67
78
|
return "\e]1337;File=#{arguments}:#{Base64.encode64(blob)}\a"
|
68
79
|
end
|
69
80
|
|
data/lib/aspera/preview/utils.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# cspell:ignore ffprobe optipng unoconv
|
3
4
|
require 'English'
|
4
5
|
require 'tmpdir'
|
5
6
|
require 'fileutils'
|
@@ -11,18 +12,17 @@ module Aspera
|
|
11
12
|
class Utils
|
12
13
|
# from bash manual: meta-character need to be escaped
|
13
14
|
BASH_SPECIAL_CHARACTERS = "|&;()<> \t#\n"
|
14
|
-
# shell exit code when command is not found
|
15
|
-
BASH_EXIT_NOT_FOUND = 127
|
16
15
|
# external binaries used
|
17
16
|
EXTERNAL_TOOLS = %i[ffmpeg ffprobe convert composite optipng unoconv].freeze
|
18
17
|
TEMP_FORMAT = 'img%04d.jpg'
|
19
|
-
private_constant :BASH_SPECIAL_CHARACTERS, :
|
18
|
+
private_constant :BASH_SPECIAL_CHARACTERS, :EXTERNAL_TOOLS, :TEMP_FORMAT
|
20
19
|
|
21
20
|
class << self
|
22
21
|
# returns string with single quotes suitable for bash if there is any bash meta-character
|
23
22
|
def shell_quote(argument)
|
24
23
|
return argument unless argument.chars.any?{|c|BASH_SPECIAL_CHARACTERS.include?(c)}
|
25
|
-
|
24
|
+
# surround with single quotes, and escape single quotes
|
25
|
+
return %Q{'#{argument.gsub("'"){|_s| %q{'"'"'}}}'}
|
26
26
|
end
|
27
27
|
|
28
28
|
# check that external tools can be executed
|
@@ -30,38 +30,31 @@ module Aspera
|
|
30
30
|
tools_to_check = EXTERNAL_TOOLS.dup
|
31
31
|
tools_to_check.delete(:unoconv) if skip_types.include?(:office)
|
32
32
|
# Check for binaries
|
33
|
-
tools_to_check.each do |
|
34
|
-
external_command(
|
33
|
+
tools_to_check.each do |command_sym|
|
34
|
+
external_command(command_sym, ['-h'], log_error: false)
|
35
|
+
rescue Errno::ENOENT => e
|
36
|
+
raise "missing #{command_sym} binary: #{e}"
|
37
|
+
rescue
|
38
|
+
nil
|
35
39
|
end
|
36
40
|
end
|
37
41
|
|
38
42
|
# execute external command
|
39
43
|
# one could use "system", but we would need to redirect stdout/err
|
40
44
|
# @return true if su
|
41
|
-
def external_command(
|
42
|
-
raise "unexpected command #{
|
45
|
+
def external_command(command_sym, command_args, log_error: true)
|
46
|
+
raise "unexpected command #{command_sym}" unless EXTERNAL_TOOLS.include?(command_sym)
|
43
47
|
# 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}"}
|
48
|
+
command_line = command_args.clone.unshift(command_sym).map{|i| shell_quote(i.to_s)}.join(' ')
|
49
|
+
Log.log.debug{"cmd=#{command_line}".blue}
|
50
|
+
stdout, stderr, status = Open3.capture3(command_line)
|
51
|
+
if log_error && !status.success?
|
52
|
+
Log.log.error{"status: #{status}"}
|
60
53
|
Log.log.error{"stdout: #{stdout}"}
|
61
54
|
Log.log.error{"stderr: #{stderr}"}
|
62
|
-
raise "#{
|
55
|
+
raise "#{command_sym} error #{status}"
|
63
56
|
end
|
64
|
-
return {status:
|
57
|
+
return {status: status, stdout: stdout}
|
65
58
|
end
|
66
59
|
|
67
60
|
def ffmpeg(a)
|