aspera-cli 4.25.6 → 4.26.1
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 +89 -48
- data/CONTRIBUTING.md +1 -1
- data/lib/aspera/api/aoc.rb +120 -79
- data/lib/aspera/api/node.rb +103 -51
- data/lib/aspera/ascp/installation.rb +99 -32
- data/lib/aspera/assert.rb +17 -13
- data/lib/aspera/cli/extended_value.rb +7 -2
- data/lib/aspera/cli/formatter.rb +107 -95
- data/lib/aspera/cli/main.rb +69 -10
- data/lib/aspera/cli/manager.rb +158 -78
- data/lib/aspera/cli/options.schema.yaml +82 -0
- data/lib/aspera/cli/plugins/aoc.rb +247 -144
- data/lib/aspera/cli/plugins/ats.rb +3 -3
- data/lib/aspera/cli/plugins/base.rb +60 -76
- data/lib/aspera/cli/plugins/config.rb +14 -12
- data/lib/aspera/cli/plugins/console.rb +3 -3
- data/lib/aspera/cli/plugins/faspex.rb +6 -6
- data/lib/aspera/cli/plugins/faspex5.rb +24 -23
- data/lib/aspera/cli/plugins/node.rb +67 -71
- data/lib/aspera/cli/plugins/oauth.rb +5 -12
- data/lib/aspera/cli/plugins/orchestrator.rb +13 -13
- data/lib/aspera/cli/plugins/preview.rb +116 -80
- data/lib/aspera/cli/plugins/server.rb +2 -10
- data/lib/aspera/cli/plugins/shares.rb +7 -7
- data/lib/aspera/cli/sync_actions.rb +1 -1
- data/lib/aspera/cli/transfer_agent.rb +17 -15
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/command_line_builder.rb +22 -18
- data/lib/aspera/dot_container.rb +7 -3
- data/lib/aspera/environment.rb +6 -5
- data/lib/aspera/formatter_interface.rb +14 -0
- data/lib/aspera/hash_ext.rb +6 -0
- data/lib/aspera/log.rb +5 -4
- data/lib/aspera/markdown.rb +4 -1
- data/lib/aspera/oauth/factory.rb +1 -1
- data/lib/aspera/preview/file_types.rb +1 -1
- data/lib/aspera/preview/generator.rb +146 -91
- data/lib/aspera/preview/options.rb +4 -1
- data/lib/aspera/preview/terminal.rb +50 -20
- data/lib/aspera/preview/utils.rb +76 -34
- data/lib/aspera/products/transferd.rb +1 -1
- data/lib/aspera/proxy_auto_config.rb +3 -0
- data/lib/aspera/rest.rb +2 -1
- data/lib/aspera/rest_list.rb +23 -16
- data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +62801 -0
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +8898 -0
- data/lib/aspera/schema/documentation.rb +107 -0
- data/lib/aspera/schema/reader.rb +75 -0
- data/lib/aspera/schema/registry.rb +63 -0
- data/lib/aspera/secret_hider.rb +3 -1
- data/lib/aspera/sync/conf.schema.yaml +0 -26
- data/lib/aspera/sync/operations.rb +9 -5
- data/lib/aspera/transfer/faux_file.rb +1 -1
- data/lib/aspera/transfer/resumer.rb +1 -1
- data/lib/aspera/transfer/spec.rb +3 -3
- data/lib/aspera/transfer/spec.schema.yaml +1 -1
- data/lib/aspera/uri_reader.rb +17 -2
- data/lib/aspera/yaml.rb +4 -2
- data.tar.gz.sig +0 -0
- metadata +13 -7
- metadata.gz.sig +0 -0
- data/lib/aspera/transfer/spec_doc.rb +0 -76
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'aspera/log'
|
|
4
4
|
require 'aspera/assert'
|
|
5
|
+
require 'aspera/schema/registry'
|
|
5
6
|
require 'yaml'
|
|
6
7
|
module Aspera
|
|
7
8
|
# Helper class to build command line from a parameter list (key-value hash)
|
|
@@ -35,9 +36,9 @@ module Aspera
|
|
|
35
36
|
|
|
36
37
|
class << self
|
|
37
38
|
# Called by provider of definition before constructor of this class so that schema has all mandatory fields
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
validate_schema(
|
|
39
|
+
# @return [Aspera::Schema::Reader]
|
|
40
|
+
def read_schema(name_sym, ascp: false)
|
|
41
|
+
validate_schema(Schema::Registry.instance.reader(name_sym), ascp: ascp)
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
# @param agent [Symbol] Transfer agent name
|
|
@@ -49,29 +50,32 @@ module Aspera
|
|
|
49
50
|
|
|
50
51
|
private
|
|
51
52
|
|
|
53
|
+
DIRECT_PROPERTIES = %w[x-cli-option x-cli-envvar x-cli-special].freeze
|
|
54
|
+
|
|
52
55
|
# Fill default values for some fields in the schema
|
|
53
|
-
# @param schema [
|
|
54
|
-
# @param ascp
|
|
56
|
+
# @param schema [Schema::Reader] The JSON schema
|
|
57
|
+
# @param ascp [Boolean] `true` if `ascp`
|
|
58
|
+
# @return [Schema::Reader] The JSON schema
|
|
55
59
|
def validate_schema(schema, ascp: false)
|
|
56
|
-
|
|
57
|
-
schema
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
Aspera.assert_type(schema, Schema::Reader){'schema'}
|
|
61
|
+
Aspera.assert(schema.current.key?('properties')){"Schema must have 'properties': #{schema}"}
|
|
62
|
+
schema.each_property do |property_schema, name, _full_name|
|
|
63
|
+
node = property_schema.current
|
|
64
|
+
unsupported_keys = node.keys - PROPERTY_KEYS
|
|
60
65
|
Aspera.assert(unsupported_keys.empty?){"Unsupported definition keys: #{unsupported_keys}"}
|
|
61
|
-
Aspera.assert(
|
|
62
|
-
Aspera.assert(
|
|
63
|
-
|
|
64
|
-
Aspera.assert(
|
|
65
|
-
|
|
66
|
-
validate_schema(info, ascp: ascp) if info['type'].eql?('object') && info['properties']
|
|
67
|
-
validate_schema(info['items'], ascp: ascp) if info['type'].eql?('array') && info['items'] && info['items']['properties']
|
|
66
|
+
Aspera.assert(node.key?('type') || node.key?('enum')){"Missing type for #{name} in #{schema.current.dig('description').current}"}
|
|
67
|
+
Aspera.assert(node['type'].eql?('boolean')){"switch must be bool: #{name}"} if node['x-cli-switch'] && !node['x-cli-special']
|
|
68
|
+
node['x-cli-option'] = "--#{name.to_s.tr('_', '-')}" if node['x-cli-option'].eql?(true) || (node['x-cli-switch'].eql?(true) && !node.key?('x-cli-option'))
|
|
69
|
+
Aspera.assert(DIRECT_PROPERTIES.any?{ |i| node.key?(i)}, type: :warn){name} if ascp && supported_by_agent(:direct, node)
|
|
70
|
+
node.freeze
|
|
68
71
|
end
|
|
69
72
|
schema
|
|
70
73
|
end
|
|
71
74
|
end
|
|
72
75
|
|
|
73
|
-
# @param [Hash] object with parameters
|
|
74
|
-
# @param [
|
|
76
|
+
# @param object [Hash] object with parameters
|
|
77
|
+
# @param schema [Schema::Reader] JSON schema
|
|
78
|
+
# @param convert [Object] function to convert value
|
|
75
79
|
def initialize(object, schema, convert)
|
|
76
80
|
@object = object # keep reference so that it can be modified by caller before calling `process_params`
|
|
77
81
|
@schema = schema
|
data/lib/aspera/dot_container.rb
CHANGED
|
@@ -61,6 +61,7 @@ module Aspera
|
|
|
61
61
|
def to_dotted
|
|
62
62
|
result = {}
|
|
63
63
|
until @stack.empty?
|
|
64
|
+
# path: Array, current: Array or Hash or other
|
|
64
65
|
path, current = @stack.pop
|
|
65
66
|
to_insert = nil
|
|
66
67
|
# empty things are left intact
|
|
@@ -78,8 +79,11 @@ module Aspera
|
|
|
78
79
|
elsif current.all?{ |i| i.is_a?(Hash) && i.keys == ['name']}
|
|
79
80
|
to_insert = current.map{ |i| i['name']}
|
|
80
81
|
# Array of Hashes with only 'name' and 'value' keys -> Hash of key/values
|
|
81
|
-
elsif current.all?{ |i| i.is_a?(Hash) && i.
|
|
82
|
-
|
|
82
|
+
elsif current.all?{ |i| i.is_a?(Hash) && i.key?('name') && i.key?('value') && i.length <= 3}
|
|
83
|
+
# if there is an extra key, other than 'name' and 'value', insert that key as is
|
|
84
|
+
add_elements(path, current.flat_map{ |h| h.except('name', 'value').to_a})
|
|
85
|
+
# Insert name/value pairs as Hash
|
|
86
|
+
add_elements(path, current.to_h{ |h| h.values_at('name', 'value')})
|
|
83
87
|
else
|
|
84
88
|
add_elements(path, current.each_with_index.map{ |v, i| [i, v]})
|
|
85
89
|
end
|
|
@@ -97,7 +101,7 @@ module Aspera
|
|
|
97
101
|
# Add elements of enumerator to the @stack, in reverse order
|
|
98
102
|
def add_elements(path, enum)
|
|
99
103
|
enum.reverse_each do |key, value|
|
|
100
|
-
@stack.push([path + [key], value])
|
|
104
|
+
@stack.push([path + [key.to_s], value])
|
|
101
105
|
end
|
|
102
106
|
nil
|
|
103
107
|
end
|
data/lib/aspera/environment.rb
CHANGED
|
@@ -91,9 +91,9 @@ module Aspera
|
|
|
91
91
|
#
|
|
92
92
|
# @param cmd [Array<#to_s>] The executable and its arguments.
|
|
93
93
|
# @param mode [:execute, :background, :capture] The execution strategy:
|
|
94
|
-
# - `:execute` Uses {Kernel.system}. Returns `true`, `false`, or `nil`.
|
|
94
|
+
# - `:execute` Uses {Kernel.system}. Returns `true`, `false`, or `nil`. (Default)
|
|
95
95
|
# - `:background` Uses {Process.spawn}. Returns the spawned process PID.
|
|
96
|
-
# - `:capture` Uses {Open3.capture3}. Returns captured
|
|
96
|
+
# - `:capture` Uses {Open3.capture3}. Returns captured out, err, and status.
|
|
97
97
|
#
|
|
98
98
|
# @param kwargs [Hash] Additional options forwarded to the underlying call.
|
|
99
99
|
#
|
|
@@ -151,7 +151,7 @@ module Aspera
|
|
|
151
151
|
# @param path [String] the file path
|
|
152
152
|
# @param force [Boolean] if true, overwrite the file
|
|
153
153
|
# @param mode [Integer] the file mode (permissions)
|
|
154
|
-
# @
|
|
154
|
+
# @yieldreturn [String] The content to write to the file
|
|
155
155
|
def write_file_restricted(path, force: false, mode: nil)
|
|
156
156
|
Aspera.assert(block_given?, type: Aspera::InternalError)
|
|
157
157
|
if force || !File.exist?(path)
|
|
@@ -280,7 +280,7 @@ module Aspera
|
|
|
280
280
|
when Environment::OS_MACOS then return self.class.secure_execute('open', uri.to_s)
|
|
281
281
|
when Environment::OS_WINDOWS then return self.class.secure_execute('start', 'explorer', %Q{"#{uri}"})
|
|
282
282
|
when Environment::OS_LINUX then return self.class.secure_execute('xdg-open', uri.to_s)
|
|
283
|
-
else
|
|
283
|
+
else Aspera.error_unexpected_value(os){'no graphical open method'}
|
|
284
284
|
end
|
|
285
285
|
end
|
|
286
286
|
|
|
@@ -316,6 +316,7 @@ module Aspera
|
|
|
316
316
|
|
|
317
317
|
# Replacement character for illegal filename characters
|
|
318
318
|
# Can also be used as safe "join" character
|
|
319
|
+
# @return [String] One character
|
|
319
320
|
def safe_filename_character
|
|
320
321
|
return REPLACE_CHARACTER if @file_illegal_characters.nil? || @file_illegal_characters.empty?
|
|
321
322
|
@file_illegal_characters[0]
|
|
@@ -333,7 +334,7 @@ module Aspera
|
|
|
333
334
|
filename = filename.chop while filename.end_with?(' ', '.')
|
|
334
335
|
if @file_illegal_characters&.size.to_i >= 2
|
|
335
336
|
# replace all illegal characters with safe_char
|
|
336
|
-
filename = filename.tr(@file_illegal_characters[1
|
|
337
|
+
filename = filename.tr(@file_illegal_characters[1..], safe_char)
|
|
337
338
|
end
|
|
338
339
|
# ensure only one safe_char is used at a time
|
|
339
340
|
return filename.gsub(/#{Regexp.escape(safe_char)}+/, safe_char).chomp(safe_char)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/assert'
|
|
4
|
+
|
|
5
|
+
module Aspera
|
|
6
|
+
# Common interface for documentation formatters
|
|
7
|
+
# Used by Schema::Documentation to format tables
|
|
8
|
+
module FormatterInterface
|
|
9
|
+
Aspera.require_method!(:tick)
|
|
10
|
+
Aspera.require_method!(:special_format)
|
|
11
|
+
Aspera.require_method!(:check_row)
|
|
12
|
+
Aspera.require_method!(:markdown_text)
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/aspera/hash_ext.rb
CHANGED
|
@@ -9,6 +9,12 @@ class ::Hash
|
|
|
9
9
|
merge!(second){ |_key, v1, v2| v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.deep_merge!(v2) : v2}
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
# Recursively iterate through hash and execute block on leaf values
|
|
13
|
+
# @param memory [Object, nil] Optional memory object passed to block
|
|
14
|
+
# @yieldparam hash [Hash] The current hash
|
|
15
|
+
# @yieldparam key [Object] The current key
|
|
16
|
+
# @yieldparam value [Object] The current value (non-Hash)
|
|
17
|
+
# @yieldparam memory [Object, nil] The memory object
|
|
12
18
|
def deep_do(memory = nil, &block)
|
|
13
19
|
each do |key, value|
|
|
14
20
|
if value.is_a?(Hash)
|
data/lib/aspera/log.rb
CHANGED
|
@@ -77,7 +77,7 @@ module Aspera
|
|
|
77
77
|
# @param name [String, Symbol] Name of object dumped
|
|
78
78
|
# @param object [Hash, nil] Data to dump
|
|
79
79
|
# @param level [Symbol] Debug level
|
|
80
|
-
# @
|
|
80
|
+
# @yieldreturn [Object] Computed object to dump (alternative to object parameter)
|
|
81
81
|
def dump(name, object = nil, level: :debug, &block)
|
|
82
82
|
return unless instance.logger.send(:"#{level}?")
|
|
83
83
|
Aspera.assert(object.nil? || block.nil?){'Use either object, or block, not both'}
|
|
@@ -93,12 +93,13 @@ module Aspera
|
|
|
93
93
|
JSON.pretty_generate(object) rescue PP.pp(object, +'')
|
|
94
94
|
when :ruby
|
|
95
95
|
PP.pp(object, +'')
|
|
96
|
-
else error_unexpected_value(instance.dump_format){'dump format'}
|
|
96
|
+
else Aspera.error_unexpected_value(instance.dump_format){'dump format'}
|
|
97
97
|
end
|
|
98
|
-
"#{name.to_s.green}
|
|
98
|
+
"#{name.to_s.green}(#{instance.dump_format})#{object.class}=\n#{dump_text}"
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
# Capture the output of $stderr and log it at debug level
|
|
102
|
+
# @yieldreturn [void] Code block whose stderr output will be captured
|
|
102
103
|
def capture_stderr
|
|
103
104
|
real_stderr = $stderr
|
|
104
105
|
$stderr = StringIO.new
|
|
@@ -192,7 +193,7 @@ module Aspera
|
|
|
192
193
|
end
|
|
193
194
|
# Use `local2` facility, like other Aspera components
|
|
194
195
|
@logger = Syslog::Logger.new(@program_name, Syslog::LOG_LOCAL2)
|
|
195
|
-
else error_unexpected_value(new_log_type){"log type (#{LOG_TYPES.join(', ')})"}
|
|
196
|
+
else Aspera.error_unexpected_value(new_log_type){"log type (#{LOG_TYPES.join(', ')})"}
|
|
196
197
|
end
|
|
197
198
|
@logger.level = current_severity_integer
|
|
198
199
|
@logger_type = new_log_type
|
data/lib/aspera/markdown.rb
CHANGED
|
@@ -8,11 +8,14 @@ module Aspera
|
|
|
8
8
|
HTML_BREAK = '<br/>'
|
|
9
9
|
|
|
10
10
|
class << self
|
|
11
|
+
COL_WIDTH = 80
|
|
11
12
|
# Generate markdown from the provided 2D table
|
|
13
|
+
# @param table [Array<Array<String>>] 2D array of strings
|
|
14
|
+
# @return [String] markdown table
|
|
12
15
|
def table(table)
|
|
13
16
|
# get max width of each columns
|
|
14
17
|
col_widths = table.transpose.map do |col|
|
|
15
|
-
[col.flat_map{ |c| c.to_s.delete('`').split(HTML_BREAK).map(&:size)}.max,
|
|
18
|
+
[col.flat_map{ |c| c.to_s.delete('`').split(HTML_BREAK).map(&:size)}.max, COL_WIDTH].min
|
|
16
19
|
end
|
|
17
20
|
headings = table.shift
|
|
18
21
|
table.unshift(col_widths.map{ |col_width| '-' * col_width})
|
data/lib/aspera/oauth/factory.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Aspera
|
|
|
33
33
|
# Extract only token from Authorization (remove scheme)
|
|
34
34
|
def bearer_token(authorization)
|
|
35
35
|
Aspera.assert(bearer_auth?(authorization)){'not a bearer token, wrong prefix scheme'}
|
|
36
|
-
return authorization
|
|
36
|
+
return authorization.delete_prefix(SPACE_BEARER_AUTH_SCHEME)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
# Generate a unique cache id for a token creator
|
|
@@ -14,7 +14,7 @@ module Aspera
|
|
|
14
14
|
# values for conversion_type : input format
|
|
15
15
|
CONVERSION_TYPES = %i[image office pdf plaintext video].freeze
|
|
16
16
|
|
|
17
|
-
#
|
|
17
|
+
# Special cases for MIME types
|
|
18
18
|
# spellchecker:disable
|
|
19
19
|
SUPPORTED_MIME_TYPES = {
|
|
20
20
|
'application/json' => :plaintext,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# ffmpeg options:
|
|
4
|
-
# spellchecker:ignore pauseframes libx264 trunc bufsize muxer apng libmp3lame maxrate posterize movflags faststart
|
|
4
|
+
# spellchecker:ignore soffice 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
7
|
require 'aspera/preview/options'
|
|
@@ -12,37 +12,41 @@ require 'aspera/assert'
|
|
|
12
12
|
|
|
13
13
|
module Aspera
|
|
14
14
|
module Preview
|
|
15
|
-
#
|
|
15
|
+
# Generates one preview file for one format for one file at a time.
|
|
16
16
|
class Generator
|
|
17
|
-
#
|
|
17
|
+
# Values for preview_format: output format.
|
|
18
18
|
PREVIEW_FORMATS = %i[png mp4].freeze
|
|
19
19
|
|
|
20
|
+
# List of valid ffmpeg option keys for reencode configuration.
|
|
20
21
|
FFMPEG_OPTIONS_LIST = %w[in out].freeze
|
|
21
22
|
|
|
22
|
-
# CLI needs to know conversion type to know if need skip it
|
|
23
|
-
#
|
|
24
|
-
attr_reader :conversion_type
|
|
23
|
+
# CLI needs to know conversion type to know if need skip it.
|
|
24
|
+
# One of CONVERSION_TYPES.
|
|
25
|
+
attr_reader :conversion_type, :destination
|
|
25
26
|
|
|
26
|
-
# Node API MIME types are from: http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
|
|
27
|
+
# Node API MIME types are from: http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types.
|
|
27
28
|
# The resulting preview file type is taken from destination file extension.
|
|
28
|
-
# Conversion methods are provided by private methods: convert_<conversion_type>_to_<preview_format
|
|
29
|
-
# -> conversion_type is one of FileTypes::CONVERSION_TYPES
|
|
30
|
-
# -> preview_format is one of Generator::PREVIEW_FORMATS
|
|
31
|
-
# The conversion video->mp4 is implemented in methods: convert_video_to_mp4_using_<video_conversion
|
|
32
|
-
# -> conversion method is one of Generator::VIDEO_CONVERSION_METHODS
|
|
33
|
-
# @param src
|
|
34
|
-
# @param dst
|
|
35
|
-
# @param options
|
|
36
|
-
# @param main_temp_dir [String]
|
|
37
|
-
# @param
|
|
38
|
-
def initialize(src, dst, options, main_temp_dir,
|
|
39
|
-
|
|
40
|
-
@
|
|
29
|
+
# Conversion methods are provided by private methods: convert_<conversion_type>_to_<preview_format>.
|
|
30
|
+
# -> conversion_type is one of FileTypes::CONVERSION_TYPES.
|
|
31
|
+
# -> preview_format is one of Generator::PREVIEW_FORMATS.
|
|
32
|
+
# The conversion video->mp4 is implemented in methods: convert_video_to_mp4_using_<video_conversion>.
|
|
33
|
+
# -> conversion method is one of Generator::VIDEO_CONVERSION_METHODS.
|
|
34
|
+
# @param src [String] Source file path.
|
|
35
|
+
# @param dst [String] Destination file path.
|
|
36
|
+
# @param options [Options] All conversion options.
|
|
37
|
+
# @param main_temp_dir [String] Main temp folder, sub folder will be created for generation.
|
|
38
|
+
# @param mime [String, nil] Optional MIME type as provided by node api (or nil).
|
|
39
|
+
def initialize(src, dst, options, main_temp_dir, mime: nil)
|
|
40
|
+
# Source file path
|
|
41
|
+
@source = src
|
|
42
|
+
# Destination file path
|
|
43
|
+
@destination = dst
|
|
41
44
|
@options = options
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
# temp folder name based on source file
|
|
46
|
+
@temp_folder = File.join(main_temp_dir, @source.split('/').last.gsub(/\s/, '_').gsub(/\W/, ''))
|
|
47
|
+
# Extract preview format from extension of target file.
|
|
48
|
+
@preview_format_sym = File.extname(@destination).gsub(/^\./, '').to_sym
|
|
49
|
+
conversion_type = FileTypes.instance.conversion_type(@source, mime)
|
|
46
50
|
@processing_method = "convert_#{conversion_type}_to_#{@preview_format_sym}"
|
|
47
51
|
if conversion_type.eql?(:video)
|
|
48
52
|
case @preview_format_sym
|
|
@@ -55,82 +59,97 @@ module Aspera
|
|
|
55
59
|
@processing_method = @processing_method.to_sym
|
|
56
60
|
Log.log.debug{"method: #{@processing_method}"}
|
|
57
61
|
Aspera.assert(respond_to?(@processing_method, true)){"no processing known for #{conversion_type} -> #{@preview_format_sym}"}
|
|
62
|
+
command = [:magick] + %w[identify -list font]
|
|
63
|
+
magick_fonts = Utils.parse_magick_fonts(Utils.execute(*command, mode: :capture).first)
|
|
64
|
+
Aspera.assert(magick_fonts[:fonts].any?{ |f| f[:name].eql?(@options.thumb_text_font)}){"Missing font #{@options.thumb_text_font} in #{command}"}
|
|
58
65
|
end
|
|
59
66
|
|
|
60
|
-
#
|
|
67
|
+
# Creates preview as specified in constructor.
|
|
61
68
|
def generate
|
|
62
|
-
Log.log.debug{"#{@
|
|
69
|
+
Log.log.debug{"#{@source}->#{@destination} (#{@processing_method})"}
|
|
63
70
|
begin
|
|
64
71
|
send(@processing_method)
|
|
65
|
-
#
|
|
66
|
-
result_size = File.size(@
|
|
72
|
+
# Check that generated size does not exceed maximum.
|
|
73
|
+
result_size = File.size(@destination)
|
|
67
74
|
Log.log.warn{"preview size exceeds maximum allowed #{result_size} > #{@options.max_size}"} if result_size > @options.max_size
|
|
68
|
-
rescue StandardError => e
|
|
69
|
-
Log.log.error{"Ignoring: #{e.class} #{e.message}"}
|
|
70
|
-
Log.log.debug(e.backtrace.join("\n").red)
|
|
71
|
-
FileUtils.cp(File.expand_path(@preview_format_sym.eql?(:mp4) ? 'video_error.png' : 'image_error.png', File.dirname(__FILE__)), @destination_file_path)
|
|
72
75
|
ensure
|
|
73
76
|
FileUtils.rm_rf(@temp_folder)
|
|
74
77
|
end
|
|
75
78
|
end
|
|
76
79
|
|
|
80
|
+
# Path to error image corresponding to preview type.
|
|
81
|
+
# @return [String] The path to the error image.
|
|
82
|
+
def error_asset
|
|
83
|
+
File.expand_path(@preview_format_sym.eql?(:mp4) ? 'video_error.png' : 'image_error.png', File.dirname(__FILE__))
|
|
84
|
+
end
|
|
85
|
+
|
|
77
86
|
private
|
|
78
87
|
|
|
79
88
|
# Creates a unique temp folder for file.
|
|
89
|
+
# @return [String] The temporary folder path.
|
|
80
90
|
def this_tmpdir
|
|
81
91
|
FileUtils.mkdir_p(@temp_folder)
|
|
82
92
|
return @temp_folder
|
|
83
93
|
end
|
|
84
94
|
|
|
85
|
-
#
|
|
86
|
-
# @param
|
|
87
|
-
# @param
|
|
88
|
-
# @param
|
|
89
|
-
# @
|
|
95
|
+
# Calculates offset in seconds for video frame extraction.
|
|
96
|
+
# @param duration [Float] Duration of video in seconds.
|
|
97
|
+
# @param start_offset [Numeric] Start offset of parts in seconds.
|
|
98
|
+
# @param total_count [Integer] Total count of parts.
|
|
99
|
+
# @param index [Integer] Index of part (starts at 1).
|
|
100
|
+
# @return [Float] Offset in seconds suitable for ffmpeg -ss option.
|
|
90
101
|
def get_offset(duration, start_offset, total_count, index)
|
|
91
102
|
Aspera.assert_type(duration, Float){'duration'}
|
|
92
103
|
return start_offset + ((index - 1) * (duration - start_offset) / total_count)
|
|
93
104
|
end
|
|
94
105
|
|
|
106
|
+
# Converts video to MP4 using blend method.
|
|
107
|
+
# Extracts key frames and blends them with transitions.
|
|
95
108
|
def convert_video_to_mp4_using_blend
|
|
96
|
-
p_duration = Utils.video_get_duration(@
|
|
109
|
+
p_duration = Utils.video_get_duration(@source)
|
|
97
110
|
p_start_offset = @options.video_start_sec.to_i
|
|
98
111
|
p_key_frame_count = @options.blend_keyframes.to_i
|
|
99
112
|
last_keyframe = nil
|
|
100
113
|
current_index = 1
|
|
114
|
+
frame_rate_hz = 30
|
|
101
115
|
1.upto(p_key_frame_count) do |i|
|
|
102
|
-
|
|
103
|
-
|
|
116
|
+
Utils.video_dump_frame(
|
|
117
|
+
@source,
|
|
118
|
+
get_offset(p_duration, p_start_offset, p_key_frame_count, i),
|
|
119
|
+
@options.video_scale,
|
|
120
|
+
Utils.get_tmp_num_filepath(this_tmpdir, current_index)
|
|
121
|
+
)
|
|
104
122
|
Utils.video_dupe_frame(this_tmpdir, current_index, @options.blend_pauseframes)
|
|
105
123
|
Utils.video_blend_frames(this_tmpdir, last_keyframe, current_index) unless last_keyframe.nil?
|
|
106
|
-
#
|
|
124
|
+
# Go to last dupe frame.
|
|
107
125
|
last_keyframe = current_index + @options.blend_pauseframes
|
|
108
|
-
#
|
|
126
|
+
# Go after last dupe frame and keep space to blend.
|
|
109
127
|
current_index = last_keyframe + 1 + @options.blend_transframes
|
|
110
128
|
end
|
|
111
129
|
Utils.ffmpeg(
|
|
112
130
|
in_f: Utils.ffmpeg_fmt(this_tmpdir),
|
|
113
131
|
in_p: ['-framerate', @options.blend_fps],
|
|
114
|
-
out_f: @
|
|
132
|
+
out_f: @destination,
|
|
115
133
|
out_p: [
|
|
116
134
|
'-filter:v', "scale='trunc(iw/2)*2:trunc(ih/2)*2'",
|
|
117
135
|
'-codec:v', 'libx264',
|
|
118
|
-
'-r',
|
|
136
|
+
'-r', frame_rate_hz,
|
|
119
137
|
'-pix_fmt', 'yuv420p'
|
|
120
138
|
]
|
|
121
139
|
)
|
|
122
140
|
end
|
|
123
141
|
|
|
124
|
-
#
|
|
142
|
+
# Converts video to MP4 using clips method.
|
|
143
|
+
# Generates n clips starting at offset and concatenates them.
|
|
125
144
|
def convert_video_to_mp4_using_clips
|
|
126
|
-
p_duration = Utils.video_get_duration(@
|
|
145
|
+
p_duration = Utils.video_get_duration(@source)
|
|
127
146
|
file_list_file = File.join(this_tmpdir, 'clip_files.txt')
|
|
128
147
|
File.open(file_list_file, 'w+') do |f|
|
|
129
148
|
1.upto(@options.clips_count.to_i) do |i|
|
|
130
149
|
offset_seconds = get_offset(p_duration, @options.video_start_sec.to_i, @options.clips_count.to_i, i)
|
|
131
150
|
tmp_file_name = format('clip%04d.mp4', i)
|
|
132
151
|
Utils.ffmpeg(
|
|
133
|
-
in_f: @
|
|
152
|
+
in_f: @source,
|
|
134
153
|
in_p: ['-ss', offset_seconds * 0.9],
|
|
135
154
|
out_f: File.join(this_tmpdir, tmp_file_name),
|
|
136
155
|
out_p: [
|
|
@@ -143,17 +162,18 @@ module Aspera
|
|
|
143
162
|
f.puts("file '#{tmp_file_name}'")
|
|
144
163
|
end
|
|
145
164
|
end
|
|
146
|
-
#
|
|
165
|
+
# Concat clips.
|
|
147
166
|
Utils.ffmpeg(
|
|
148
167
|
in_f: file_list_file,
|
|
149
168
|
in_p: ['-f', 'concat'],
|
|
150
|
-
out_f: @
|
|
169
|
+
out_f: @destination,
|
|
151
170
|
out_p: ['-codec', 'copy']
|
|
152
171
|
)
|
|
153
172
|
File.delete(file_list_file)
|
|
154
173
|
end
|
|
155
174
|
|
|
156
|
-
#
|
|
175
|
+
# Converts video to MP4 using re-encoding method.
|
|
176
|
+
# Performs a simple re-encoding with configurable ffmpeg options.
|
|
157
177
|
def convert_video_to_mp4_using_reencode
|
|
158
178
|
options = @options.reencode_ffmpeg
|
|
159
179
|
Aspera.assert_type(options, Hash){'reencode_ffmpeg'}
|
|
@@ -162,11 +182,11 @@ module Aspera
|
|
|
162
182
|
Aspera.assert_type(v, Array){k}
|
|
163
183
|
end
|
|
164
184
|
Utils.ffmpeg(
|
|
165
|
-
in_f: @
|
|
185
|
+
in_f: @source,
|
|
166
186
|
in_p: options['in'] || ['-ss', @options.video_start_sec.to_i * 0.9],
|
|
167
|
-
out_f: @
|
|
187
|
+
out_f: @destination,
|
|
168
188
|
out_p: options['out'] || [
|
|
169
|
-
'-t',
|
|
189
|
+
'-t', 60,
|
|
170
190
|
'-codec:v', 'libx264',
|
|
171
191
|
'-profile:v', 'high',
|
|
172
192
|
'-pix_fmt', 'yuv420p',
|
|
@@ -184,89 +204,124 @@ module Aspera
|
|
|
184
204
|
)
|
|
185
205
|
end
|
|
186
206
|
|
|
207
|
+
# Converts video to PNG using fixed frame method.
|
|
208
|
+
# Generates a static thumbnail at a specific time offset.
|
|
187
209
|
def convert_video_to_png_using_fixed
|
|
188
210
|
Utils.video_dump_frame(
|
|
189
|
-
@
|
|
190
|
-
Utils.video_get_duration(@
|
|
211
|
+
@source,
|
|
212
|
+
Utils.video_get_duration(@source) * @options.thumb_vid_fraction,
|
|
191
213
|
@options.thumb_vid_scale,
|
|
192
|
-
@
|
|
214
|
+
@destination
|
|
193
215
|
)
|
|
194
216
|
end
|
|
195
217
|
|
|
196
|
-
#
|
|
197
|
-
#
|
|
198
|
-
#
|
|
199
|
-
# ffmpeg output.png
|
|
218
|
+
# Converts video to animated PNG (APNG).
|
|
219
|
+
# Creates an animated thumbnail with looping.
|
|
220
|
+
# @see https://trac.ffmpeg.org/wiki/SponsoringPrograms/GSoC/2015#AnimatedPortableNetworkGraphicsAPNG
|
|
200
221
|
def convert_video_to_png_using_animated
|
|
222
|
+
p_duration = Utils.video_get_duration(@source)
|
|
223
|
+
p_start_offset = @options.video_start_sec.to_i
|
|
224
|
+
p_max_duration = @options.clips_length.to_i
|
|
225
|
+
# If video is shorter than start offset + duration, adjust to capture from start.
|
|
226
|
+
if p_duration <= (p_start_offset + p_max_duration)
|
|
227
|
+
p_start_offset = 0
|
|
228
|
+
p_max_duration = p_duration
|
|
229
|
+
end
|
|
201
230
|
Utils.ffmpeg(
|
|
202
|
-
in_f: @
|
|
231
|
+
in_f: @source,
|
|
203
232
|
in_p: [
|
|
204
|
-
'-ss',
|
|
205
|
-
'-t',
|
|
233
|
+
'-ss', p_start_offset,
|
|
234
|
+
'-t', p_max_duration
|
|
206
235
|
],
|
|
207
|
-
out_f: @
|
|
236
|
+
out_f: @destination,
|
|
208
237
|
out_p: [
|
|
209
|
-
'-vf', 'fps=5,scale=120:-1:flags=lanczos
|
|
210
|
-
'-
|
|
211
|
-
'-f', '
|
|
238
|
+
'-vf', 'fps=5,scale=120:-1:flags=lanczos',
|
|
239
|
+
'-plays', 0, # Loop forever (0 = infinite loop for APNG).
|
|
240
|
+
'-f', 'apng'
|
|
212
241
|
]
|
|
213
242
|
)
|
|
214
243
|
end
|
|
215
244
|
|
|
245
|
+
# Converts office document to PNG.
|
|
246
|
+
# First converts to PDF, then to PNG image.
|
|
216
247
|
def convert_office_to_png
|
|
217
|
-
tmp_pdf_file = File.join(this_tmpdir, "#{File.basename(@
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
248
|
+
tmp_pdf_file = File.join(this_tmpdir, "#{File.basename(@source, File.extname(@source))}.pdf")
|
|
249
|
+
case @options.office_conversion
|
|
250
|
+
when :unoconv
|
|
251
|
+
Utils.silent_execute(
|
|
252
|
+
:unoconv,
|
|
253
|
+
'-f', 'pdf',
|
|
254
|
+
'-o', tmp_pdf_file,
|
|
255
|
+
@source
|
|
256
|
+
)
|
|
257
|
+
when :soffice
|
|
258
|
+
Utils.silent_execute(
|
|
259
|
+
:soffice,
|
|
260
|
+
'--headless',
|
|
261
|
+
'--convert-to', 'pdf',
|
|
262
|
+
'--outdir', File.dirname(tmp_pdf_file),
|
|
263
|
+
@source
|
|
264
|
+
)
|
|
265
|
+
# soffice creates the file with the source name, so we need to rename it if needed.
|
|
266
|
+
generated_pdf = File.join(File.dirname(tmp_pdf_file), "#{File.basename(@source, File.extname(@source))}.pdf")
|
|
267
|
+
FileUtils.mv(generated_pdf, tmp_pdf_file) if generated_pdf != tmp_pdf_file
|
|
268
|
+
else Aspera.error_unexpected_value(@options.office_conversion){'office_conversion'}
|
|
269
|
+
end
|
|
223
270
|
convert_pdf_to_png(tmp_pdf_file)
|
|
224
271
|
end
|
|
225
272
|
|
|
273
|
+
# Converts PDF to PNG image.
|
|
274
|
+
# @param source_file_path [String, nil] Optional source file path, defaults to @source.
|
|
226
275
|
def convert_pdf_to_png(source_file_path = nil)
|
|
227
|
-
source_file_path ||= @
|
|
228
|
-
Utils.
|
|
276
|
+
source_file_path ||= @source
|
|
277
|
+
Utils.silent_execute(
|
|
278
|
+
:magick,
|
|
229
279
|
'convert',
|
|
230
280
|
'-size', "x#{@options.thumb_img_size}",
|
|
231
281
|
'-background', 'white',
|
|
232
282
|
'-flatten',
|
|
233
283
|
"#{source_file_path}[0]",
|
|
234
|
-
@
|
|
235
|
-
|
|
284
|
+
@destination
|
|
285
|
+
)
|
|
236
286
|
end
|
|
237
287
|
|
|
288
|
+
# Converts image to PNG thumbnail.
|
|
289
|
+
# Applies auto-orientation, resizing, and optimization.
|
|
238
290
|
def convert_image_to_png
|
|
239
|
-
Utils.
|
|
291
|
+
Utils.silent_execute(
|
|
292
|
+
:magick,
|
|
240
293
|
'convert',
|
|
241
294
|
'-auto-orient',
|
|
242
295
|
'-thumbnail', "#{@options.thumb_img_size}x#{@options.thumb_img_size}>",
|
|
243
296
|
'-quality', 95,
|
|
244
297
|
'+dither',
|
|
245
298
|
'-posterize', 40,
|
|
246
|
-
"#{@
|
|
247
|
-
@
|
|
248
|
-
|
|
249
|
-
Utils.
|
|
299
|
+
"#{@source}[0]",
|
|
300
|
+
@destination
|
|
301
|
+
)
|
|
302
|
+
Utils.silent_execute(:optipng, @destination)
|
|
250
303
|
end
|
|
251
304
|
|
|
252
|
-
# text to
|
|
305
|
+
# Converts plain text to PNG image.
|
|
306
|
+
# Renders first 100 lines of text file as an image.
|
|
253
307
|
def convert_plaintext_to_png
|
|
254
|
-
#
|
|
255
|
-
first_lines = File.foreach(@
|
|
256
|
-
Utils.
|
|
308
|
+
# Get 100 first lines of text file.
|
|
309
|
+
first_lines = File.foreach(@source).first(100).join
|
|
310
|
+
Utils.silent_execute(
|
|
311
|
+
:magick,
|
|
257
312
|
'convert',
|
|
258
313
|
'-size', "#{@options.thumb_img_size}x#{@options.thumb_img_size}",
|
|
259
|
-
'xc:white', #
|
|
314
|
+
'xc:white', # Define canvas with background color (xc, or canvas) of preceding size.
|
|
260
315
|
'-font', @options.thumb_text_font,
|
|
261
316
|
'-pointsize', 12,
|
|
262
|
-
'-fill', 'black', #
|
|
317
|
+
'-fill', 'black', # Font color.
|
|
263
318
|
'-annotate', '+0+0', first_lines,
|
|
264
|
-
'-trim', #
|
|
319
|
+
'-trim', # Avoid large blank regions.
|
|
265
320
|
'-bordercolor', 'white',
|
|
266
321
|
'-border', 8,
|
|
267
322
|
'+repage',
|
|
268
|
-
@
|
|
269
|
-
|
|
323
|
+
@destination
|
|
324
|
+
)
|
|
270
325
|
end
|
|
271
326
|
end
|
|
272
327
|
end
|