aspera-cli 4.24.2 → 4.25.0.pre2
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 +1067 -758
- data/CONTRIBUTING.md +93 -120
- data/README.md +817 -510
- data/lib/aspera/agent/direct.rb +14 -12
- data/lib/aspera/agent/transferd.rb +4 -4
- data/lib/aspera/api/aoc.rb +71 -43
- data/lib/aspera/api/cos_node.rb +3 -2
- data/lib/aspera/api/faspex.rb +6 -5
- data/lib/aspera/api/node.rb +10 -12
- data/lib/aspera/ascmd.rb +1 -2
- data/lib/aspera/ascp/installation.rb +55 -41
- data/lib/aspera/ascp/management.rb +9 -5
- data/lib/aspera/assert.rb +28 -6
- data/lib/aspera/cli/error.rb +4 -2
- data/lib/aspera/cli/extended_value.rb +94 -62
- data/lib/aspera/cli/formatter.rb +55 -22
- data/lib/aspera/cli/main.rb +21 -14
- data/lib/aspera/cli/manager.rb +349 -248
- data/lib/aspera/cli/plugins/alee.rb +3 -3
- data/lib/aspera/cli/plugins/aoc.rb +94 -51
- data/lib/aspera/cli/plugins/base.rb +62 -49
- data/lib/aspera/cli/plugins/config.rb +85 -96
- data/lib/aspera/cli/plugins/console.rb +15 -9
- data/lib/aspera/cli/plugins/cos.rb +1 -1
- data/lib/aspera/cli/plugins/faspex.rb +34 -27
- data/lib/aspera/cli/plugins/faspex5.rb +47 -44
- data/lib/aspera/cli/plugins/faspio.rb +7 -6
- data/lib/aspera/cli/plugins/httpgw.rb +3 -2
- data/lib/aspera/cli/plugins/node.rb +132 -120
- data/lib/aspera/cli/plugins/oauth.rb +1 -1
- data/lib/aspera/cli/plugins/orchestrator.rb +116 -33
- data/lib/aspera/cli/plugins/preview.rb +26 -46
- data/lib/aspera/cli/plugins/server.rb +9 -10
- data/lib/aspera/cli/plugins/shares.rb +77 -43
- data/lib/aspera/cli/sync_actions.rb +49 -38
- data/lib/aspera/cli/transfer_agent.rb +16 -34
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +8 -5
- data/lib/aspera/command_line_builder.rb +20 -17
- data/lib/aspera/coverage.rb +6 -2
- data/lib/aspera/environment.rb +71 -84
- data/lib/aspera/faspex_gw.rb +1 -1
- data/lib/aspera/faspex_postproc.rb +1 -1
- data/lib/aspera/keychain/factory.rb +1 -2
- data/lib/aspera/keychain/macos_security.rb +2 -2
- data/lib/aspera/log.rb +2 -1
- data/lib/aspera/markdown.rb +31 -0
- data/lib/aspera/nagios.rb +6 -5
- data/lib/aspera/oauth/base.rb +17 -27
- data/lib/aspera/oauth/factory.rb +1 -1
- data/lib/aspera/oauth/url_json.rb +2 -1
- data/lib/aspera/preview/file_types.rb +23 -37
- data/lib/aspera/preview/terminal.rb +95 -29
- data/lib/aspera/preview/utils.rb +6 -5
- data/lib/aspera/products/connect.rb +3 -3
- data/lib/aspera/rest.rb +51 -39
- data/lib/aspera/rest_error_analyzer.rb +4 -4
- data/lib/aspera/ssh.rb +5 -2
- data/lib/aspera/ssl.rb +41 -0
- data/lib/aspera/sync/conf.schema.yaml +182 -34
- data/lib/aspera/sync/database.rb +2 -1
- data/lib/aspera/sync/operations.rb +128 -72
- data/lib/aspera/transfer/parameters.rb +3 -4
- data/lib/aspera/transfer/spec.rb +2 -3
- data/lib/aspera/transfer/spec.schema.yaml +49 -19
- data/lib/aspera/transfer/spec_doc.rb +14 -14
- data/lib/aspera/uri_reader.rb +1 -1
- data/lib/transferd_pb.rb +2 -2
- data.tar.gz.sig +0 -0
- metadata +33 -6
- metadata.gz.sig +0 -0
|
@@ -49,7 +49,7 @@ module Aspera
|
|
|
49
49
|
options[:path] = uri.path unless ['', '/'].include?(uri.path)
|
|
50
50
|
options[:port] = uri.port unless uri.port.eql?(443) && !url.include?(':443/')
|
|
51
51
|
end
|
|
52
|
-
command_args = [command]
|
|
52
|
+
command_args = [SECURITY_UTILITY, command]
|
|
53
53
|
options&.each do |k, v|
|
|
54
54
|
Aspera.assert(supported.key?(k)){"unknown option: #{k}"}
|
|
55
55
|
next if v.nil?
|
|
@@ -57,7 +57,7 @@ module Aspera
|
|
|
57
57
|
command_args.push(v.shellescape) unless v.empty?
|
|
58
58
|
end
|
|
59
59
|
command_args.push(last_opt) unless last_opt.nil?
|
|
60
|
-
return Environment.
|
|
60
|
+
return Environment.secure_execute(*command_args, mode: :capture)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def key_chains(output)
|
data/lib/aspera/log.rb
CHANGED
|
@@ -81,8 +81,9 @@ module Aspera
|
|
|
81
81
|
# @param object [Hash, nil] Data to dump
|
|
82
82
|
# @param level [Symbol] Debug level
|
|
83
83
|
# @param block [Proc, nil] Give computed object
|
|
84
|
-
def dump(name, object = nil, level: :debug)
|
|
84
|
+
def dump(name, object = nil, level: :debug, &block)
|
|
85
85
|
return unless instance.logger.send(:"#{level}?")
|
|
86
|
+
Aspera.assert(object.nil? || block.nil?){'Use either object, or block, not both'}
|
|
86
87
|
object = yield if block_given?
|
|
87
88
|
instance.logger.send(level, obj_dump(name, object))
|
|
88
89
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aspera
|
|
4
|
+
# Formatting for Markdown
|
|
5
|
+
class Markdown
|
|
6
|
+
# Matches: **bold**, `code`, or an HTML entity (&, ©, 💩)
|
|
7
|
+
FORMATS = /(?:\*\*(?<bold>[^*]+?)\*\*)|(?:`(?<code>[^`]+)`)|&(?<entity>(?:[A-Za-z][A-Za-z0-9]{1,31}|#\d{1,7}|#x[0-9A-Fa-f]{1,6}));/m
|
|
8
|
+
HTML_BREAK = '<br/>'
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
# Generate markdown from the provided 2D table
|
|
12
|
+
def table(table)
|
|
13
|
+
# get max width of each columns
|
|
14
|
+
col_widths = table.transpose.map do |col|
|
|
15
|
+
[col.flat_map{ |c| c.to_s.delete('`').split(HTML_BREAK).map(&:size)}.max, 80].min
|
|
16
|
+
end
|
|
17
|
+
headings = table.shift
|
|
18
|
+
table.unshift(col_widths.map{ |col_width| '-' * col_width})
|
|
19
|
+
table.unshift(headings)
|
|
20
|
+
lines = table.map{ |line| "| #{line.map{ |i| i.to_s.gsub('\\', '\\\\').gsub('|', '\|')}.join(' | ')} |\n"}
|
|
21
|
+
lines[1] = lines[1].tr(' ', '-')
|
|
22
|
+
return lines.join.chomp
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Generate markdown list from the provided list
|
|
26
|
+
def list(items)
|
|
27
|
+
items.map{ |i| "- #{i}"}.join("\n")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/aspera/nagios.rb
CHANGED
|
@@ -21,7 +21,7 @@ module Aspera
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
class << self
|
|
24
|
-
#
|
|
24
|
+
# Process results of a analysis and display status and exit with code
|
|
25
25
|
def process(data)
|
|
26
26
|
Aspera.assert_type(data, Array)
|
|
27
27
|
Aspera.assert(!data.empty?){'data is empty'}
|
|
@@ -54,7 +54,7 @@ module Aspera
|
|
|
54
54
|
@data = []
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
#
|
|
57
|
+
# Compare remote time with local time
|
|
58
58
|
def check_time_offset(remote_date, component)
|
|
59
59
|
# check date if specified : 2015-10-13T07:32:01Z
|
|
60
60
|
remote_time = Time.parse(remote_date)
|
|
@@ -76,10 +76,11 @@ module Aspera
|
|
|
76
76
|
# TODO: check on database if latest version
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
-
#
|
|
80
|
-
|
|
79
|
+
# Readable status list
|
|
80
|
+
# @return [Array] of Hash
|
|
81
|
+
def status_list
|
|
81
82
|
Aspera.assert(!@data.empty?){'missing result'}
|
|
82
|
-
|
|
83
|
+
@data.map{ |i| {'status' => LEVELS[i[:code]].to_s, 'component' => i[:comp], 'message' => i[:msg]}}
|
|
83
84
|
end
|
|
84
85
|
end
|
|
85
86
|
end
|
data/lib/aspera/oauth/base.rb
CHANGED
|
@@ -59,25 +59,13 @@ module Aspera
|
|
|
59
59
|
|
|
60
60
|
attr_reader :scope, :api, :path_token, :client_id
|
|
61
61
|
|
|
62
|
-
#
|
|
62
|
+
# Helper method to create token as per RFC
|
|
63
|
+
# @return [HTTPResponse]
|
|
64
|
+
# @raise RestError if not 2XX code
|
|
63
65
|
def create_token_call(creation_params)
|
|
64
66
|
Log.log.debug{'Generating a new token'.bg_green}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
query: creation_params
|
|
68
|
-
}
|
|
69
|
-
else
|
|
70
|
-
{
|
|
71
|
-
content_type: Rest::MIME_WWW,
|
|
72
|
-
body: creation_params
|
|
73
|
-
}
|
|
74
|
-
end
|
|
75
|
-
return @api.call(
|
|
76
|
-
operation: 'POST',
|
|
77
|
-
subpath: @path_token,
|
|
78
|
-
headers: {'Accept' => Rest::MIME_JSON},
|
|
79
|
-
**payload
|
|
80
|
-
)
|
|
67
|
+
return @api.create(@path_token, nil, query: creation_params, ret: :resp) if @use_query
|
|
68
|
+
return @api.create(@path_token, creation_params, content_type: Rest::MIME_WWW, ret: :resp)
|
|
81
69
|
end
|
|
82
70
|
|
|
83
71
|
# @param add_secret [Boolean] Add secret in default call parameters
|
|
@@ -122,17 +110,18 @@ module Aspera
|
|
|
122
110
|
Factory.instance.persist_mgr.delete(@token_cache_id)
|
|
123
111
|
token_data = nil
|
|
124
112
|
# lets try the existing refresh token
|
|
113
|
+
# NOTE: AoC admin token has no refresh, and lives by default 1800secs
|
|
125
114
|
if !refresh_token.nil?
|
|
126
|
-
Log.log.info{"refresh=[#{refresh_token}]".bg_green}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
json_data = resp[:http].body
|
|
115
|
+
Log.log.info{"refresh token=[#{refresh_token}]".bg_green}
|
|
116
|
+
begin
|
|
117
|
+
http = create_token_call(optional_scope_client_id(add_secret: true).merge(grant_type: 'refresh_token', refresh_token: refresh_token))
|
|
118
|
+
# Save only if success
|
|
119
|
+
json_data = http.body
|
|
132
120
|
token_data = JSON.parse(json_data)
|
|
133
121
|
Factory.instance.persist_mgr.put(@token_cache_id, json_data)
|
|
134
|
-
|
|
135
|
-
|
|
122
|
+
rescue => e
|
|
123
|
+
# Refresh token can fail.
|
|
124
|
+
Log.log.warn{"Refresh failed: #{e}"}
|
|
136
125
|
end
|
|
137
126
|
end
|
|
138
127
|
end
|
|
@@ -140,8 +129,9 @@ module Aspera
|
|
|
140
129
|
|
|
141
130
|
# no cache, nor refresh: generate a token
|
|
142
131
|
if token_data.nil?
|
|
143
|
-
|
|
144
|
-
|
|
132
|
+
# Call the method-specific token creation
|
|
133
|
+
# which returns the result of create_token_call
|
|
134
|
+
json_data = create_token.body
|
|
145
135
|
token_data = JSON.parse(json_data)
|
|
146
136
|
Factory.instance.persist_mgr.put(@token_cache_id, json_data)
|
|
147
137
|
end
|
data/lib/aspera/oauth/factory.rb
CHANGED
|
@@ -156,7 +156,7 @@ module Aspera
|
|
|
156
156
|
def register_token_creator(creator_class)
|
|
157
157
|
Aspera.assert_type(creator_class, Class)
|
|
158
158
|
id = Factory.class_to_id(creator_class)
|
|
159
|
-
Log.log.debug{"registering
|
|
159
|
+
Log.log.debug{"registering creator for #{id}"}
|
|
160
160
|
@token_type_classes[id] = creator_class
|
|
161
161
|
end
|
|
162
162
|
|
|
@@ -25,7 +25,8 @@ module Aspera
|
|
|
25
25
|
query: @query.merge(scope: scope), # scope is here because it may change over time (node)
|
|
26
26
|
content_type: Rest::MIME_JSON,
|
|
27
27
|
body: @body,
|
|
28
|
-
headers: {'Accept' => Rest::MIME_JSON}
|
|
28
|
+
headers: {'Accept' => Rest::MIME_JSON},
|
|
29
|
+
ret: :resp
|
|
29
30
|
)
|
|
30
31
|
end
|
|
31
32
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require 'aspera/log'
|
|
4
4
|
require 'aspera/assert'
|
|
5
5
|
require 'singleton'
|
|
6
|
-
require '
|
|
6
|
+
require 'marcel'
|
|
7
7
|
|
|
8
8
|
module Aspera
|
|
9
9
|
module Preview
|
|
@@ -61,7 +61,7 @@ module Aspera
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
# @param mimetype [String] mime type
|
|
64
|
-
# @return file type, one of enum CONVERSION_TYPES, or nil if not found
|
|
64
|
+
# @return [NilClass,Symbol] file type, one of enum CONVERSION_TYPES, or nil if not found
|
|
65
65
|
def mime_to_type(mimetype)
|
|
66
66
|
Aspera.assert_type(mimetype, String)
|
|
67
67
|
return SUPPORTED_MIME_TYPES[mimetype] if SUPPORTED_MIME_TYPES.key?(mimetype)
|
|
@@ -72,19 +72,17 @@ module Aspera
|
|
|
72
72
|
return
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
# @param filepath [String]
|
|
76
|
-
# @param mimetype [String] provided by node API
|
|
75
|
+
# @param filepath [String] Full path to file
|
|
76
|
+
# @param mimetype [String] MIME typre provided by node API
|
|
77
77
|
# @return file type, one of enum CONVERSION_TYPES
|
|
78
78
|
# @raise [RuntimeError] if no conversion type found
|
|
79
79
|
def conversion_type(filepath, mimetype)
|
|
80
80
|
Log.log.debug{"conversion_type(#{filepath},mime=#{mimetype},magic=#{@use_mimemagic})"}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
mimetype
|
|
84
|
-
mimetype
|
|
85
|
-
|
|
86
|
-
mimetype ||= MIME::Types.of(File.basename(filepath)).first
|
|
87
|
-
raise "no MIME type found for #{File.basename(filepath)}" if mimetype.nil?
|
|
81
|
+
# Default type or empty means no type
|
|
82
|
+
mimetype = TYPE_NOT_FOUND if mimetype.nil? || (mimetype.is_a?(String) && mimetype.empty?)
|
|
83
|
+
mimetype = Marcel::MimeType.for(Pathname.new(filepath), name: File.basename(filepath), declared_type: mimetype)
|
|
84
|
+
mimetype = 'text/plain' if mimetype.eql?(TYPE_NOT_FOUND) && ascii_text_file?(filepath)
|
|
85
|
+
raise "no MIME type found for #{File.basename(filepath)}" if mimetype.eql?(TYPE_NOT_FOUND)
|
|
88
86
|
conversion_type = mime_to_type(mimetype)
|
|
89
87
|
raise "no conversion type found for #{File.basename(filepath)}" if conversion_type.nil?
|
|
90
88
|
Log.log.trace1{"conversion_type(#{File.basename(filepath)}): #{conversion_type.class.name} [#{conversion_type}]"}
|
|
@@ -93,33 +91,21 @@ module Aspera
|
|
|
93
91
|
|
|
94
92
|
private
|
|
95
93
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
# @return [String] mime type, or nil if not found
|
|
99
|
-
def mime_using_mimemagic(filepath)
|
|
100
|
-
return unless @use_mimemagic
|
|
101
|
-
# moved here, as `mimemagic` can cause installation issues
|
|
102
|
-
require 'mimemagic'
|
|
103
|
-
require 'mimemagic/version'
|
|
104
|
-
require 'mimemagic/overlay' if MimeMagic::VERSION.start_with?('0.3.')
|
|
105
|
-
# check magic number inside file (empty string if not found)
|
|
106
|
-
detected_mime = MimeMagic.by_magic(File.open(filepath)).to_s
|
|
107
|
-
# check extension only
|
|
108
|
-
if mime_to_type(detected_mime).nil?
|
|
109
|
-
Log.log.debug{"no conversion for #{detected_mime}, trying extension"}
|
|
110
|
-
detected_mime = MimeMagic.by_extension(File.extname(filepath)).to_s
|
|
111
|
-
end
|
|
112
|
-
detected_mime = nil if detected_mime.empty?
|
|
113
|
-
Log.log.debug{"mimemagic: #{detected_mime.class.name} [#{detected_mime}]"}
|
|
114
|
-
return detected_mime
|
|
115
|
-
end
|
|
94
|
+
TYPE_NOT_FOUND = 'application/octet-stream'
|
|
95
|
+
ACCEPT_CTRL_CHARS = [9, 10, 13]
|
|
116
96
|
|
|
117
|
-
#
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
97
|
+
# Returns true if the file looks like ASCII text (printable ASCII + \t, \r, \n, space).
|
|
98
|
+
# It reads only a small prefix (default: 64KB) and fails fast on the first bad byte.
|
|
99
|
+
def ascii_text_file?(path, sample_size: 64 * 1024)
|
|
100
|
+
File.open(path, 'rb') do |f|
|
|
101
|
+
sample = f.read(sample_size) || ''.b
|
|
102
|
+
sample.each_byte do |b|
|
|
103
|
+
next if b.between?(32, 126) || ACCEPT_CTRL_CHARS.include?(b)
|
|
104
|
+
# Any other control character => not ASCII text
|
|
105
|
+
return false
|
|
106
|
+
end
|
|
107
|
+
true
|
|
108
|
+
end
|
|
123
109
|
end
|
|
124
110
|
end
|
|
125
111
|
end
|
|
@@ -3,11 +3,87 @@
|
|
|
3
3
|
# cspell:words Magick MAGICKCORE ITERM mintty winsize termcap
|
|
4
4
|
|
|
5
5
|
require 'rainbow'
|
|
6
|
+
require 'base64'
|
|
6
7
|
require 'io/console'
|
|
7
8
|
require 'aspera/log'
|
|
8
9
|
require 'aspera/environment'
|
|
10
|
+
|
|
9
11
|
module Aspera
|
|
10
12
|
module Preview
|
|
13
|
+
module Backend
|
|
14
|
+
# provides image pixels scaled to terminal
|
|
15
|
+
class Base
|
|
16
|
+
def initialize(reserve:, double:, font_ratio:)
|
|
17
|
+
@reserve = reserve
|
|
18
|
+
@height_ratio = double ? 2.0 : 1.0
|
|
19
|
+
@font_ratio = font_ratio
|
|
20
|
+
end
|
|
21
|
+
Aspera.require_method!(:terminal_pixels)
|
|
22
|
+
# compute scaling to fit terminal
|
|
23
|
+
def terminal_scaling(rows, columns)
|
|
24
|
+
(term_rows, term_columns) = IO.console.winsize || [24, 80]
|
|
25
|
+
term_rows = [term_rows - @reserve, 2].max
|
|
26
|
+
fit_term_ratio = [term_rows.to_f * @font_ratio / rows.to_f, term_columns.to_f / columns.to_f].min
|
|
27
|
+
[(columns * fit_term_ratio).to_i, (rows * fit_term_ratio * @height_ratio / @font_ratio).to_i]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class RMagick < Base
|
|
32
|
+
def initialize(blob, **kwargs)
|
|
33
|
+
super(**kwargs)
|
|
34
|
+
# do not require statically, as the package is optional
|
|
35
|
+
require 'rmagick' # https://rmagick.github.io/index.html
|
|
36
|
+
@image = Magick::ImageList.new.from_blob(blob)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def terminal_pixels
|
|
40
|
+
# quantum depth is 8 or 16, see: `magick xc: -format "%q" info:`
|
|
41
|
+
shift_for_8_bit = Magick::MAGICKCORE_QUANTUM_DEPTH - 8
|
|
42
|
+
# get all pixel colors, adjusted for Rainbow
|
|
43
|
+
pixel_colors = []
|
|
44
|
+
@image.scale(*terminal_scaling(@image.rows, @image.columns)).each_pixel do |pixel, col, row|
|
|
45
|
+
pixel_rgb = [pixel.red, pixel.green, pixel.blue]
|
|
46
|
+
pixel_rgb = pixel_rgb.map{ |color| color >> shift_for_8_bit} unless shift_for_8_bit.eql?(0)
|
|
47
|
+
# init 2-dim array
|
|
48
|
+
pixel_colors[row] ||= []
|
|
49
|
+
pixel_colors[row][col] = pixel_rgb
|
|
50
|
+
end
|
|
51
|
+
pixel_colors
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class ChunkyPNG < Base
|
|
56
|
+
def initialize(blob, **kwargs)
|
|
57
|
+
super(**kwargs)
|
|
58
|
+
require 'chunky_png'
|
|
59
|
+
@png = ::ChunkyPNG::Image.from_blob(blob)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def terminal_pixels
|
|
63
|
+
src_w = @png.width
|
|
64
|
+
src_h = @png.height
|
|
65
|
+
dst_w, dst_h = terminal_scaling(src_h, src_w)
|
|
66
|
+
dst_w = [dst_w, 1].max
|
|
67
|
+
dst_h = [dst_h, 1].max
|
|
68
|
+
pixel_colors = Array.new(dst_h){Array.new(dst_w)}
|
|
69
|
+
x_ratio = src_w.to_f / dst_w
|
|
70
|
+
y_ratio = src_h.to_f / dst_h
|
|
71
|
+
dst_h.times do |dy|
|
|
72
|
+
sy = (dy * y_ratio).floor
|
|
73
|
+
sy = src_h - 1 if sy >= src_h
|
|
74
|
+
dst_w.times do |dx|
|
|
75
|
+
sx = (dx * x_ratio).floor
|
|
76
|
+
sx = src_w - 1 if sx >= src_w
|
|
77
|
+
rgba = @png.get_pixel(sx, sy)
|
|
78
|
+
# ChunkyPNG stores as 0xRRGGBBAA; extract 8-bit channels
|
|
79
|
+
pixel_colors[dy][dx] = %i[r g b].map{ |i| ::ChunkyPNG::Color.send(i, rgba)}
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
pixel_colors
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
11
87
|
# Display a picture in the terminal.
|
|
12
88
|
# Either use coloured characters or iTerm2 protocol.
|
|
13
89
|
class Terminal
|
|
@@ -22,41 +98,31 @@ module Aspera
|
|
|
22
98
|
private_constant :TERM_ENV_VARS, :ITERM_NAMES, :DEFAULT_FONT_RATIO
|
|
23
99
|
class << self
|
|
24
100
|
# @param blob [String] The image as a binary string
|
|
25
|
-
# @param reserve [Integer] Number of lines to reserve for other text than the image
|
|
26
101
|
# @param text [Boolean] `true` to display the image as text, `false` to use iTerm2 if supported
|
|
102
|
+
# @param reserve [Integer] Number of lines to reserve for other text than the image
|
|
27
103
|
# @param double [Boolean] `true` to use colors on half lines, `false` to use colors on full lines
|
|
28
104
|
# @param font_ratio [Float] ratio = font height / font width
|
|
29
105
|
# @return [String] The image as text, or the iTerm2 escape sequence
|
|
30
|
-
def build(blob,
|
|
106
|
+
def build(blob, text: false, reserve: 3, double: true, font_ratio: DEFAULT_FONT_RATIO)
|
|
31
107
|
return '[Image display requires a terminal]' unless Environment.terminal?
|
|
32
108
|
return iterm_display_image(blob) if iterm_supported? && !text
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
109
|
+
pixel_colors =
|
|
110
|
+
begin
|
|
111
|
+
Log.log.debug('Trying chunky_png')
|
|
112
|
+
Backend::ChunkyPNG.new(blob, reserve: reserve, double: double, font_ratio: font_ratio).terminal_pixels
|
|
113
|
+
rescue => e
|
|
114
|
+
Log.log.debug(e.message)
|
|
115
|
+
begin
|
|
116
|
+
Log.log.debug('Trying rmagick')
|
|
117
|
+
Backend::RMagick.new(blob, reserve: reserve, double: double, font_ratio: font_ratio).terminal_pixels
|
|
118
|
+
rescue => e
|
|
119
|
+
Log.log.debug(e.message)
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
if pixel_colors.nil?
|
|
39
124
|
return iterm_display_image(blob) if iterm_supported?
|
|
40
|
-
|
|
41
|
-
raise e
|
|
42
|
-
end
|
|
43
|
-
image = Magick::ImageList.new.from_blob(blob)
|
|
44
|
-
(term_rows, term_columns) = IO.console.winsize
|
|
45
|
-
term_rows -= reserve
|
|
46
|
-
# compute scaling to fit terminal
|
|
47
|
-
fit_term_ratio = [term_rows.to_f * font_ratio / image.rows.to_f, term_columns.to_f / image.columns.to_f].min
|
|
48
|
-
height_ratio = double ? 2.0 : 1.0
|
|
49
|
-
image = image.scale((image.columns * fit_term_ratio).to_i, (image.rows * fit_term_ratio * height_ratio / font_ratio).to_i)
|
|
50
|
-
# quantum depth is 8 or 16, see: `magick xc: -format "%q" info:`
|
|
51
|
-
shift_for_8_bit = Magick::MAGICKCORE_QUANTUM_DEPTH - 8
|
|
52
|
-
# get all pixel colors, adjusted for Rainbow
|
|
53
|
-
pixel_colors = []
|
|
54
|
-
image.each_pixel do |pixel, col, row|
|
|
55
|
-
pixel_rgb = [pixel.red, pixel.green, pixel.blue]
|
|
56
|
-
pixel_rgb = pixel_rgb.map{ |color| color >> shift_for_8_bit} unless shift_for_8_bit.eql?(0)
|
|
57
|
-
# init 2-dim array
|
|
58
|
-
pixel_colors[row] ||= []
|
|
59
|
-
pixel_colors[row][col] = pixel_rgb
|
|
125
|
+
raise 'Cannot decode picture.'
|
|
60
126
|
end
|
|
61
127
|
# now generate text
|
|
62
128
|
text_pixels = []
|
|
@@ -88,7 +154,7 @@ module Aspera
|
|
|
88
154
|
}.map{ |k, v| "#{k}=#{v}"}.join(';')
|
|
89
155
|
# \a is BEL, \e is ESC : https://github.com/ruby/ruby/blob/master/doc/syntax/literals.rdoc#label-Strings
|
|
90
156
|
# escape sequence for iTerm2 image display
|
|
91
|
-
return "\e]1337;File=#{arguments}:#{Base64.
|
|
157
|
+
return "\e]1337;File=#{arguments}:#{Base64.strict_encode64(blob)}\a"
|
|
92
158
|
end
|
|
93
159
|
|
|
94
160
|
# @return [Boolean] true if the terminal supports iTerm2 image display
|
data/lib/aspera/preview/utils.rb
CHANGED
|
@@ -44,17 +44,18 @@ module Aspera
|
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
# @return nil
|
|
47
|
+
# Execute external command
|
|
48
|
+
# @return [nil]
|
|
50
49
|
def external_command(command_sym, command_args)
|
|
51
50
|
Aspera.assert_values(command_sym, EXTERNAL_TOOLS){'command'}
|
|
52
|
-
Environment.secure_execute(
|
|
51
|
+
Environment.secure_execute(command_sym.to_s, *command_args.map(&:to_s), out: File::NULL, err: File::NULL)
|
|
53
52
|
end
|
|
54
53
|
|
|
54
|
+
# Execute external command and capture output
|
|
55
|
+
# @return [String]
|
|
55
56
|
def external_capture(command_sym, command_args)
|
|
56
57
|
Aspera.assert_values(command_sym, EXTERNAL_TOOLS){'command'}
|
|
57
|
-
return Environment.
|
|
58
|
+
return Environment.secure_execute(command_sym.to_s, *command_args.map(&:to_s), mode: :capture)
|
|
58
59
|
end
|
|
59
60
|
|
|
60
61
|
def ffmpeg(gl_p: FFMPEG_DEFAULT_PARAMS, in_p: [], in_f:, out_p: [], out_f:)
|
|
@@ -56,10 +56,10 @@ module Aspera
|
|
|
56
56
|
# Retrieve structure from cloud (CDN) with all versions available
|
|
57
57
|
def versions
|
|
58
58
|
if @connect_versions.nil?
|
|
59
|
-
|
|
59
|
+
http = cdn_api.read(VERSION_INFO_FILE, ret: :resp)
|
|
60
60
|
# get result on one line
|
|
61
|
-
connect_versions_javascript =
|
|
62
|
-
Log.
|
|
61
|
+
connect_versions_javascript = http.body.gsub(/\r?\n\s*/, '')
|
|
62
|
+
Log.dump(:javascript, connect_versions_javascript)
|
|
63
63
|
# get javascript object only
|
|
64
64
|
found = connect_versions_javascript.match(/^.*? = (.*);/)
|
|
65
65
|
raise Cli::Error, 'Problem when getting connect versions from internet' if found.nil?
|