aspera-cli 4.27.0 → 4.27.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.
Files changed (140) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +29 -0
  4. data/CONTRIBUTING.md +1 -7
  5. data/TODO.md +50 -0
  6. data/docs/README.md +708 -488
  7. data/lib/aspera/agent/base.rb +10 -2
  8. data/lib/aspera/agent/connect.rb +7 -7
  9. data/lib/aspera/agent/desktop.rb +3 -3
  10. data/lib/aspera/agent/direct.rb +48 -19
  11. data/lib/aspera/agent/factory.rb +3 -3
  12. data/lib/aspera/agent/httpgw.rb +55 -14
  13. data/lib/aspera/agent/node.rb +2 -2
  14. data/lib/aspera/agent/transferd.rb +12 -12
  15. data/lib/aspera/api/aoc.rb +28 -28
  16. data/lib/aspera/api/cos_node.rb +10 -9
  17. data/lib/aspera/api/faspex.rb +2 -2
  18. data/lib/aspera/api/httpgw.rb +20 -20
  19. data/lib/aspera/api/node.rb +31 -53
  20. data/lib/aspera/ascmd.rb +13 -13
  21. data/lib/aspera/ascp/installation.rb +43 -36
  22. data/lib/aspera/ascp/management.rb +6 -6
  23. data/lib/aspera/assert.rb +12 -13
  24. data/lib/aspera/cli/ascp_actions.rb +7 -7
  25. data/lib/aspera/cli/async_transfer_store.rb +49 -5
  26. data/lib/aspera/cli/bootstrapper.rb +9 -9
  27. data/lib/aspera/cli/command_registry.rb +1 -1
  28. data/lib/aspera/cli/command_spec.rb +6 -3
  29. data/lib/aspera/cli/context.rb +3 -3
  30. data/lib/aspera/cli/extended_value.rb +26 -26
  31. data/lib/aspera/cli/formatter.rb +76 -54
  32. data/lib/aspera/cli/gem_checker.rb +1 -1
  33. data/lib/aspera/cli/hints.rb +2 -2
  34. data/lib/aspera/cli/http.rb +22 -29
  35. data/lib/aspera/cli/mailer.rb +7 -7
  36. data/lib/aspera/cli/mcp_tool.rb +31 -4
  37. data/lib/aspera/cli/option_declarator.rb +77 -0
  38. data/lib/aspera/cli/options.schema.yaml +273 -3
  39. data/lib/aspera/cli/parser.rb +539 -280
  40. data/lib/aspera/cli/plugins/alee.rb +1 -1
  41. data/lib/aspera/cli/plugins/aoc.rb +297 -313
  42. data/lib/aspera/cli/plugins/ats.rb +25 -21
  43. data/lib/aspera/cli/plugins/base.rb +357 -186
  44. data/lib/aspera/cli/plugins/basic_auth.rb +0 -12
  45. data/lib/aspera/cli/plugins/config.rb +76 -73
  46. data/lib/aspera/cli/plugins/console.rb +8 -8
  47. data/lib/aspera/cli/plugins/cos.rb +4 -3
  48. data/lib/aspera/cli/plugins/factory.rb +2 -2
  49. data/lib/aspera/cli/plugins/faspex5.rb +259 -248
  50. data/lib/aspera/cli/plugins/faspio.rb +7 -2
  51. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  52. data/lib/aspera/cli/plugins/mcp.rb +11 -11
  53. data/lib/aspera/cli/plugins/node.rb +227 -283
  54. data/lib/aspera/cli/plugins/orchestrator.rb +23 -23
  55. data/lib/aspera/cli/plugins/preview.rb +61 -64
  56. data/lib/aspera/cli/plugins/server.rb +36 -31
  57. data/lib/aspera/cli/plugins/shares.rb +174 -135
  58. data/lib/aspera/cli/preset_actions.rb +39 -19
  59. data/lib/aspera/cli/preset_manager.rb +74 -32
  60. data/lib/aspera/cli/result.rb +18 -16
  61. data/lib/aspera/cli/runner.rb +75 -39
  62. data/lib/aspera/cli/secret_finder.rb +1 -1
  63. data/lib/aspera/cli/sync_actions.rb +36 -35
  64. data/lib/aspera/cli/terminal_formatter.rb +2 -2
  65. data/lib/aspera/cli/transfer_actions.rb +17 -10
  66. data/lib/aspera/cli/transfer_agent.rb +44 -44
  67. data/lib/aspera/cli/transfer_progress.rb +7 -7
  68. data/lib/aspera/cli/vault_manager.rb +34 -12
  69. data/lib/aspera/cli/version.rb +1 -1
  70. data/lib/aspera/cli/wizard.rb +17 -20
  71. data/lib/aspera/colors.rb +3 -3
  72. data/lib/aspera/command_line_builder.rb +16 -16
  73. data/lib/aspera/command_line_converter.rb +1 -1
  74. data/lib/aspera/coverage.rb +1 -2
  75. data/lib/aspera/data_repository.rb +1 -1
  76. data/lib/aspera/dot_container.rb +8 -8
  77. data/lib/aspera/environment.rb +12 -12
  78. data/lib/aspera/faspex_gw.rb +4 -4
  79. data/lib/aspera/faspex_postproc.rb +14 -9
  80. data/lib/aspera/graphql.rb +4 -2
  81. data/lib/aspera/hash_ext.rb +2 -2
  82. data/lib/aspera/json_rpc/client.rb +1 -1
  83. data/lib/aspera/keychain/base.rb +10 -3
  84. data/lib/aspera/keychain/encrypted_hash.rb +92 -17
  85. data/lib/aspera/keychain/factory.rb +17 -5
  86. data/lib/aspera/keychain/hashicorp_vault.rb +1 -1
  87. data/lib/aspera/keychain/macos_security.rb +23 -25
  88. data/lib/aspera/keychain/one_password_api.rb +86 -0
  89. data/lib/aspera/keychain/one_password_base.rb +34 -0
  90. data/lib/aspera/keychain/one_password_cli.rb +98 -0
  91. data/lib/aspera/link_header.rb +2 -2
  92. data/lib/aspera/log.rb +13 -13
  93. data/lib/aspera/markdown.rb +5 -5
  94. data/lib/aspera/nagios.rb +8 -8
  95. data/lib/aspera/node_simulator.rb +33 -27
  96. data/lib/aspera/oauth/base.rb +4 -5
  97. data/lib/aspera/oauth/boot.rb +3 -3
  98. data/lib/aspera/oauth/factory.rb +5 -5
  99. data/lib/aspera/oauth/jwt.rb +5 -7
  100. data/lib/aspera/oauth/web.rb +4 -4
  101. data/lib/aspera/persistency_action_once.rb +4 -4
  102. data/lib/aspera/persistency_folder.rb +8 -8
  103. data/lib/aspera/preview/file_types.rb +4 -4
  104. data/lib/aspera/preview/generator.rb +49 -46
  105. data/lib/aspera/preview/options.rb +3 -2
  106. data/lib/aspera/preview/terminal.rb +5 -5
  107. data/lib/aspera/preview/utils.rb +65 -18
  108. data/lib/aspera/products/connect.rb +2 -2
  109. data/lib/aspera/products/desktop.rb +1 -1
  110. data/lib/aspera/products/other.rb +2 -2
  111. data/lib/aspera/products/transferd.rb +3 -3
  112. data/lib/aspera/proxy_auto_config.rb +10 -10
  113. data/lib/aspera/rest.rb +149 -115
  114. data/lib/aspera/rest_error_analyzer.rb +3 -3
  115. data/lib/aspera/rest_list.rb +10 -9
  116. data/lib/aspera/schema/IBM Aspera Node API-4.4.6.yaml +6232 -0
  117. data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +249 -0
  118. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
  119. data/lib/aspera/schema/documentation.rb +17 -11
  120. data/lib/aspera/schema/reader.rb +78 -4
  121. data/lib/aspera/schema/registry.rb +26 -7
  122. data/lib/aspera/secret_hider.rb +5 -5
  123. data/lib/aspera/ssh.rb +64 -31
  124. data/lib/aspera/ssl.rb +3 -3
  125. data/lib/aspera/sync/database.rb +2 -2
  126. data/lib/aspera/sync/operations.rb +13 -13
  127. data/lib/aspera/temp_file_manager.rb +24 -4
  128. data/lib/aspera/transfer/faux_file.rb +2 -2
  129. data/lib/aspera/transfer/parameters.rb +15 -15
  130. data/lib/aspera/transfer/resumer.rb +9 -9
  131. data/lib/aspera/transfer/spec.rb +1 -1
  132. data/lib/aspera/uri_reader.rb +52 -15
  133. data/lib/aspera/web_auth.rb +6 -5
  134. data/lib/aspera/web_server_simple.rb +9 -9
  135. data/lib/aspera/yaml.rb +4 -4
  136. data.tar.gz.sig +0 -0
  137. metadata +10 -4
  138. metadata.gz.sig +0 -0
  139. data/lib/aspera/cli/plugins/faspex.rb +0 -576
  140. data/lib/aspera/transfer/uri.rb +0 -56
@@ -57,21 +57,21 @@ module Aspera
57
57
  end
58
58
  end
59
59
  @processing_method = @processing_method.to_sym
60
- Log.log.debug{"method: #{@processing_method}"}
61
- Aspera.assert(respond_to?(@processing_method, true)){"no processing known for #{conversion_type} -> #{@preview_format_sym}"}
60
+ Log.log.debug { "method: #{@processing_method}" }
61
+ Aspera.assert(respond_to?(@processing_method, true)) { "no processing known for #{conversion_type} -> #{@preview_format_sym}" }
62
62
  command = [:magick] + %w[identify -list font]
63
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}"}
64
+ Aspera.assert(magick_fonts[:fonts].any? { |f| f[:name].eql?(@options.thumb_text_font) }) { "Missing font #{@options.thumb_text_font} in #{command}" }
65
65
  end
66
66
 
67
67
  # Creates preview as specified in constructor.
68
68
  def generate
69
- Log.log.debug{"#{@source}->#{@destination} (#{@processing_method})"}
69
+ Log.log.debug { "#{@source}->#{@destination} (#{@processing_method})" }
70
70
  begin
71
71
  send(@processing_method)
72
72
  # Check that generated size does not exceed maximum.
73
73
  result_size = File.size(@destination)
74
- Log.log.warn{"preview size exceeds maximum allowed #{result_size} > #{@options.max_size}"} if result_size > @options.max_size
74
+ Log.log.warn { "preview size exceeds maximum allowed #{result_size} > #{@options.max_size}" } if result_size > @options.max_size
75
75
  ensure
76
76
  FileUtils.rm_rf(@temp_folder)
77
77
  end
@@ -99,7 +99,7 @@ module Aspera
99
99
  # @param index [Integer] Index of part (starts at 1).
100
100
  # @return [Float] Offset in seconds suitable for ffmpeg -ss option.
101
101
  def get_offset(duration, start_offset, total_count, index)
102
- Aspera.assert_type(duration, Float){'duration'}
102
+ Aspera.assert_type(duration, Float) { 'duration' }
103
103
  return start_offset + ((index - 1) * (duration - start_offset) / total_count)
104
104
  end
105
105
 
@@ -127,10 +127,12 @@ module Aspera
127
127
  current_index = last_keyframe + 1 + @options.blend_transframes
128
128
  end
129
129
  Utils.ffmpeg(
130
- in_f: Utils.ffmpeg_fmt(this_tmpdir),
131
- in_p: ['-framerate', @options.blend_fps],
132
- out_f: @destination,
133
- out_p: [
130
+ in: [
131
+ Utils.ffmpeg_fmt(this_tmpdir),
132
+ '-framerate', @options.blend_fps
133
+ ],
134
+ out: [
135
+ @destination,
134
136
  '-filter:v', "scale='trunc(iw/2)*2:trunc(ih/2)*2'",
135
137
  '-codec:v', 'libx264',
136
138
  '-r', frame_rate_hz,
@@ -149,13 +151,15 @@ module Aspera
149
151
  offset_seconds = get_offset(p_duration, @options.video_start_sec.to_i, @options.clips_count.to_i, i)
150
152
  tmp_file_name = format('clip%04d.mp4', i)
151
153
  Utils.ffmpeg(
152
- in_f: @source,
153
- in_p: ['-ss', offset_seconds * 0.9],
154
- out_f: File.join(this_tmpdir, tmp_file_name),
155
- out_p: [
154
+ in: [
155
+ @source,
156
+ '-ss', offset_seconds * 0.9
157
+ ],
158
+ out: [
159
+ File.join(this_tmpdir, tmp_file_name),
156
160
  '-ss', offset_seconds * 0.1,
157
161
  '-t', @options.clips_length,
158
- '-filter:v', "scale=#{@options.video_scale}",
162
+ '-filter:v', "scale='#{@options.video_scale}'",
159
163
  '-codec:a', 'libmp3lame'
160
164
  ]
161
165
  )
@@ -164,10 +168,8 @@ module Aspera
164
168
  end
165
169
  # Concat clips.
166
170
  Utils.ffmpeg(
167
- in_f: file_list_file,
168
- in_p: ['-f', 'concat'],
169
- out_f: @destination,
170
- out_p: ['-codec', 'copy']
171
+ in: [file_list_file, '-f', 'concat'],
172
+ out: [@destination, '-codec', 'copy']
171
173
  )
172
174
  File.delete(file_list_file)
173
175
  end
@@ -176,31 +178,32 @@ module Aspera
176
178
  # Performs a simple re-encoding with configurable ffmpeg options.
177
179
  def convert_video_to_mp4_using_reencode
178
180
  options = @options.reencode_ffmpeg
179
- Aspera.assert_type(options, Hash){'reencode_ffmpeg'}
181
+ Aspera.assert_type(options, Hash) { 'reencode_ffmpeg' }
180
182
  options.each do |k, v|
181
- Aspera.assert_values(k, FFMPEG_OPTIONS_LIST){'key'}
182
- Aspera.assert_type(v, Array){k}
183
+ Aspera.assert_values(k, FFMPEG_OPTIONS_LIST) { 'key' }
184
+ Aspera.assert_type(v, Array) { k }
183
185
  end
186
+ codec = @options.video_codec || Utils.available_h264_encoder
187
+ in_opts = options['in'] || ['-ss', @options.video_start_sec.to_i * 0.9]
188
+ out_opts = options['out'] || [
189
+ '-t', 60,
190
+ '-codec:v', codec,
191
+ '-profile:v', 'high',
192
+ '-pix_fmt', 'yuv420p',
193
+ '-preset', 'slow',
194
+ '-b:v', '500k',
195
+ '-maxrate', '500k',
196
+ '-bufsize', '1000k',
197
+ '-filter:v', "scale='#{@options.video_scale}'",
198
+ '-threads', '0',
199
+ '-codec:a', 'libmp3lame',
200
+ '-ac', '2',
201
+ '-b:a', '128k',
202
+ '-movflags', 'faststart'
203
+ ]
184
204
  Utils.ffmpeg(
185
- in_f: @source,
186
- in_p: options['in'] || ['-ss', @options.video_start_sec.to_i * 0.9],
187
- out_f: @destination,
188
- out_p: options['out'] || [
189
- '-t', 60,
190
- '-codec:v', 'libx264',
191
- '-profile:v', 'high',
192
- '-pix_fmt', 'yuv420p',
193
- '-preset', 'slow',
194
- '-b:v', '500k',
195
- '-maxrate', '500k',
196
- '-bufsize', '1000k',
197
- '-filter:v', "scale=#{@options.video_scale}",
198
- '-threads', '0',
199
- '-codec:a', 'libmp3lame',
200
- '-ac', '2',
201
- '-b:a', '128k',
202
- '-movflags', 'faststart'
203
- ]
205
+ in: [@source, *in_opts],
206
+ out: [@destination, *out_opts]
204
207
  )
205
208
  end
206
209
 
@@ -228,13 +231,13 @@ module Aspera
228
231
  p_max_duration = p_duration
229
232
  end
230
233
  Utils.ffmpeg(
231
- in_f: @source,
232
- in_p: [
234
+ in: [
235
+ @source,
233
236
  '-ss', p_start_offset,
234
237
  '-t', p_max_duration
235
238
  ],
236
- out_f: @destination,
237
- out_p: [
239
+ out: [
240
+ @destination,
238
241
  '-vf', 'fps=5,scale=120:-1:flags=lanczos',
239
242
  '-plays', 0, # Loop forever (0 = infinite loop for APNG).
240
243
  '-f', 'apng'
@@ -265,7 +268,7 @@ module Aspera
265
268
  # soffice creates the file with the source name, so we need to rename it if needed.
266
269
  generated_pdf = File.join(File.dirname(tmp_pdf_file), "#{File.basename(@source, File.extname(@source))}.pdf")
267
270
  FileUtils.mv(generated_pdf, tmp_pdf_file) if generated_pdf != tmp_pdf_file
268
- else Aspera.error_unexpected_value(@options.office_conversion){'office_conversion'}
271
+ else Aspera.error_unexpected_value(@options.office_conversion) { 'office_conversion' }
269
272
  end
270
273
  convert_pdf_to_png(tmp_pdf_file)
271
274
  end
@@ -18,15 +18,16 @@ module Aspera
18
18
  # -x : keep aspect ratio, having value a multiple of x
19
19
  DESCRIPTIONS = [
20
20
  {name: :max_size, default: 1 << 24, description: 'maximum size (in bytes) of preview file'},
21
- {name: :thumb_vid_scale, default: "-1:'min(ih,100)'", description: 'png: video: size (ffmpeg scale argument)'},
21
+ {name: :thumb_vid_scale, default: '-1:min(ih,100)', description: 'png: video: size (ffmpeg scale argument)'},
22
22
  {name: :thumb_vid_fraction, default: 0.1, description: 'png: video: time percent position of snapshot'},
23
23
  {name: :thumb_img_size, default: 800, description: 'png: non-video: height (and width)'},
24
24
  {name: :thumb_text_font, default: 'Courier', description: 'png: plaintext: font for text rendering: `magick identify -list font`'},
25
25
  {name: :office_conversion, default: :soffice, description: 'office: method for office document conversion', values: OFFICE_CONVERSION_METHODS},
26
26
  {name: :video_conversion, default: :reencode, description: 'mp4: method for preview generation', values: VIDEO_CONVERSION_METHODS},
27
27
  {name: :video_png_conv, default: :fixed, description: 'mp4: method for thumbnail generation', values: VIDEO_THUMBNAIL_METHODS},
28
- {name: :video_scale, default: "'min(iw,360)':-2", description: 'mp4: all: video scale (ffmpeg scale argument)'},
28
+ {name: :video_scale, default: 'min(iw,360):-2', description: 'mp4: all: video scale (ffmpeg scale argument)'},
29
29
  {name: :video_start_sec, default: 10, description: 'mp4: all: start offset (seconds) of video preview'},
30
+ {name: :video_codec, default: nil, description: 'mp4: reencode: video codec for ffmpeg (e.g. libx264, h264_videotoolbox, h264_nvenc), default: auto-detect'},
30
31
  {name: :reencode_ffmpeg, default: {}, description: 'mp4: reencode: options to ffmpeg, keys: `in`, `out`'},
31
32
  {name: :blend_keyframes, default: 30, description: 'mp4: blend: # key frames'},
32
33
  {name: :blend_pauseframes, default: 3, description: 'mp4: blend: # pause frames'},
@@ -58,7 +58,7 @@ module Aspera
58
58
  pixel_colors = []
59
59
  @image.scale(*terminal_scaling(@image.rows, @image.columns)).each_pixel do |pixel, col, row|
60
60
  pixel_rgb = [pixel.red, pixel.green, pixel.blue]
61
- pixel_rgb = pixel_rgb.map{ |color| color >> shift_for_8_bit} unless shift_for_8_bit.eql?(0)
61
+ pixel_rgb = pixel_rgb.map { |color| color >> shift_for_8_bit } unless shift_for_8_bit.eql?(0)
62
62
  # Initialize the destination 2D pixel matrix row by row.
63
63
  pixel_colors[row] ||= []
64
64
  pixel_colors[row][col] = pixel_rgb
@@ -87,7 +87,7 @@ module Aspera
87
87
  dst_w, dst_h = terminal_scaling(src_h, src_w)
88
88
  dst_w = [dst_w, 1].max
89
89
  dst_h = [dst_h, 1].max
90
- pixel_colors = Array.new(dst_h){Array.new(dst_w)}
90
+ pixel_colors = Array.new(dst_h) { Array.new(dst_w) }
91
91
  x_ratio = src_w.to_f / dst_w
92
92
  y_ratio = src_h.to_f / dst_h
93
93
  dst_h.times do |dy|
@@ -98,7 +98,7 @@ module Aspera
98
98
  sx = src_w - 1 if sx >= src_w
99
99
  rgba = @png.get_pixel(sx, sy)
100
100
  # ChunkyPNG stores pixels as 0xRRGGBBAA; extract 8-bit RGB channels.
101
- pixel_colors[dy][dx] = %i[r g b].map{ |i| ::ChunkyPNG::Color.send(i, rgba)}
101
+ pixel_colors[dy][dx] = %i[r g b].map { |i| ::ChunkyPNG::Color.send(i, rgba) }
102
102
  end
103
103
  end
104
104
  pixel_colors
@@ -178,7 +178,7 @@ module Aspera
178
178
  size: blob.length
179
179
  # width: image.columns,
180
180
  # height: image.rows
181
- }.map{ |k, v| "#{k}=#{v}"}.join(';')
181
+ }.map { |k, v| "#{k}=#{v}" }.join(';')
182
182
  # `\a` is BEL and `\e` is ESC.
183
183
  # See: https://github.com/ruby/ruby/blob/master/doc/syntax/literals.rdoc#label-Strings
184
184
  # Return the full escape sequence expected by iTerm2-compatible terminals.
@@ -190,7 +190,7 @@ module Aspera
190
190
  # @return [Boolean] `true` when the current terminal advertises iTerm2 image support
191
191
  def iterm_supported?
192
192
  TERM_ENV_VARS.each do |env_var|
193
- return true if ITERM_NAMES.any?{ |term| ENV[env_var]&.include?(term)}
193
+ return true if ITERM_NAMES.any? { |term| ENV[env_var]&.include?(term) }
194
194
  end
195
195
  false
196
196
  end
@@ -20,11 +20,25 @@ module Aspera
20
20
  '-y', # overwrite output without asking
21
21
  '-loglevel', 'error' # show only errors and up
22
22
  ].freeze
23
- private_constant :EXTERNAL_TOOLS, :TEMP_FORMAT, :FFMPEG_DEFAULT_PARAMS
23
+ # Preferred H.264 encoders in order: best quality/compatibility first.
24
+ H264_ENCODER_PREFERENCE = %w[libx264 libopenh264 h264_nvenc h264_amf h264_qsv h264_vaapi h264_v4l2m2m].freeze
25
+ private_constant :EXTERNAL_TOOLS, :TEMP_FORMAT, :FFMPEG_DEFAULT_PARAMS, :H264_ENCODER_PREFERENCE
24
26
 
25
27
  class << self
26
- # Check that external tools can be executed
28
+ # Return the first H.264 encoder available in the local ffmpeg installation.
29
+ # Result is memoized after the first call.
30
+ # @return [String] encoder name (e.g. 'libx264', 'libopenh264')
31
+ # @raise [RuntimeError] if no supported H.264 encoder is found
32
+ def available_h264_encoder
33
+ return @available_h264_encoder if defined?(@available_h264_encoder)
34
+ stdout, = execute(:ffmpeg, '-encoders', mode: :capture, exception: false)
35
+ @available_h264_encoder = H264_ENCODER_PREFERENCE.find { |enc| stdout.include?(enc) } ||
36
+ raise("No supported H.264 encoder found in ffmpeg. Available: #{stdout.lines.grep(/h264/i).map(&:strip).join(', ')}")
37
+ end
38
+
39
+ # Check that external tools can be executed.
27
40
  # @param skip_types [Array<Symbol>] list of tools to skip
41
+ # @raise [RuntimeError] if a required tool binary is missing
28
42
  # @return [nil]
29
43
  def check_tools(skip_types = [])
30
44
  tools_to_check = EXTERNAL_TOOLS.dup
@@ -42,28 +56,46 @@ module Aspera
42
56
  end
43
57
  end
44
58
 
45
- # Execute external command, verify it is in the supported list
59
+ # Execute external command, verify it is in the supported list.
60
+ # @param args [Array] command name followed by CLI arguments
61
+ # @param kwargs [Hash] execution options passed to {Environment.secure_execute}
62
+ # @raise [Aspera::AssertError] if the command is not in {EXTERNAL_TOOLS}
63
+ # @return [Array<String>] captured stdout and stderr lines depending on mode
46
64
  def execute(*args, **kwargs)
47
- Aspera.assert_values(args.first, EXTERNAL_TOOLS){'command'}
65
+ Aspera.assert_values(args.first, EXTERNAL_TOOLS) { 'command' }
48
66
  Environment.secure_execute(*args, **kwargs)
49
67
  end
50
68
 
51
- # Execute external command silently
69
+ # Execute external command, capturing and discarding output unless it fails.
70
+ # On failure, the captured stderr is included in the raised exception message.
71
+ # @param args [Array] command name followed by CLI arguments
52
72
  # @return [nil]
53
73
  def silent_execute(*args)
54
- execute(*args, out: File::NULL, err: File::NULL)
74
+ execute(*args, mode: :capture)
55
75
  nil
56
76
  end
57
77
 
58
- # Execute `ffmpeg`
78
+ # Execute `ffmpeg`, capturing output.
79
+ # On failure, the ffmpeg stderr is logged at debug level and re-raised.
80
+ # @param in [Array] input file path followed by input options
81
+ # @param out [Array] output file path followed by output options
82
+ # @param global [Array<String>] global options for ffmpeg
59
83
  # @return [nil]
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
- silent_execute(:ffmpeg, *gl_p, *in_p, '-i', in_f, *out_p, out_f)
84
+ def ffmpeg(in:, out:, global: FFMPEG_DEFAULT_PARAMS)
85
+ Aspera.assert_type(global, Array)
86
+ # NOTE: cannot use just "in", as it is a reserved word in ruby
87
+ in_args = binding.local_variable_get(:in).dup
88
+ out_args = out.dup
89
+ Aspera.assert_type(in_args, Array)
90
+ Aspera.assert_type(out_args, Array)
91
+ in_file = in_args.shift
92
+ out_file = out_args.shift
93
+ execute(:ffmpeg, *global, *in_args, '-i', in_file, *out_args, out_file, mode: :capture)
94
+ nil
65
95
  end
66
96
 
97
+ # Get duration of a video file using `ffprobe`.
98
+ # @param input_file [String] path to video file
67
99
  # @return [Float] duration in seconds
68
100
  def video_get_duration(input_file)
69
101
  return execute(
@@ -76,23 +108,39 @@ module Aspera
76
108
  ).first.to_f
77
109
  end
78
110
 
79
- # File output format, including temp folder
111
+ # File output pattern for ffmpeg, including temp folder.
112
+ # @param temp_folder [String] path to temp folder
113
+ # @return [String] file path pattern
80
114
  def ffmpeg_fmt(temp_folder)
81
115
  return File.join(temp_folder, TEMP_FORMAT)
82
116
  end
83
117
 
118
+ # Get numbered temporary file path.
119
+ # @param temp_folder [String] path to temp folder
120
+ # @param file_number [Integer] frame index
121
+ # @return [String] file path
84
122
  def get_tmp_num_filepath(temp_folder, file_number)
85
- # Format using {Kernel.format}
86
123
  return File.join(temp_folder, format(TEMP_FORMAT, file_number))
87
124
  end
88
125
 
126
+ # Duplicate a video frame by creating symlinks.
127
+ # @param temp_folder [String] path to temp folder
128
+ # @param index [Integer] frame index to duplicate
129
+ # @param count [Integer] number of duplicate frames to create
130
+ # @return [nil]
89
131
  def video_dupe_frame(temp_folder, index, count)
90
132
  input_file = get_tmp_num_filepath(temp_folder, index)
91
133
  1.upto(count) do |i|
92
134
  FileUtils.ln_s(input_file, get_tmp_num_filepath(temp_folder, index + i))
93
135
  end
136
+ nil
94
137
  end
95
138
 
139
+ # Blend transition frames between two keyframes using ImageMagick.
140
+ # @param temp_folder [String] path to temp folder
141
+ # @param index_begin [Integer] starting frame index
142
+ # @param index_end [Integer] ending frame index
143
+ # @return [nil]
96
144
  def video_blend_frames(temp_folder, index_begin, index_end)
97
145
  img1 = get_tmp_num_filepath(temp_folder, index_begin)
98
146
  img2 = get_tmp_num_filepath(temp_folder, index_end)
@@ -102,6 +150,7 @@ module Aspera
102
150
  filename = get_tmp_num_filepath(temp_folder, index_begin + i)
103
151
  silent_execute(:magick, 'composite', '-blend', percent, img2, img1, filename)
104
152
  end
153
+ nil
105
154
  end
106
155
 
107
156
  # Dump a frame from a video file
@@ -112,10 +161,8 @@ module Aspera
112
161
  # @return [nil]
113
162
  def video_dump_frame(input_file, offset_seconds, scale, output_file)
114
163
  ffmpeg(
115
- in_f: input_file,
116
- in_p: ['-ss', offset_seconds],
117
- out_f: output_file,
118
- out_p: ['-frames:v', 1, '-filter:v', "scale=#{scale}"]
164
+ in: [input_file, '-ss', offset_seconds],
165
+ out: [output_file, '-frames:v', 1, '-filter:v', "scale='#{scale}'"]
119
166
  )
120
167
  end
121
168
 
@@ -47,7 +47,7 @@ module Aspera
47
47
  app_root: File.join(Dir.home, '.aspera', 'connect'),
48
48
  run_root: File.join(Dir.home, '.aspera', 'connect')
49
49
  }]
50
- end.map{ |i| i.merge({expected: APP_NAME})}
50
+ end.map { |i| i.merge({expected: APP_NAME}) }
51
51
  end
52
52
  end
53
53
 
@@ -65,7 +65,7 @@ module Aspera
65
65
  Log.dump(:javascript, connect_versions_javascript)
66
66
  # get javascript object only
67
67
  found = connect_versions_javascript.match(/^.*? = (.*);/)
68
- Aspera.assert(!found.nil?, type: Cli::Error){'Problem when getting connect versions from internet'}
68
+ Aspera.assert(!found.nil?, type: Cli::Error) { 'Problem when getting connect versions from internet' }
69
69
  all_data = JSON.parse(found[1])
70
70
  @connect_versions = all_data['entries']
71
71
  end
@@ -19,7 +19,7 @@ module Aspera
19
19
  sub_bin: File.join('Contents', 'Resources', 'transferd', 'bin')
20
20
  }]
21
21
  else []
22
- end.map{ |i| i.merge({expected: APP_NAME})}
22
+ end.map { |i| i.merge({expected: APP_NAME}) }
23
23
  end
24
24
  end
25
25
  end
@@ -63,9 +63,9 @@ module Aspera
63
63
  found = scan_locations.select do |item|
64
64
  product_names.push(item[:expected]) unless product_names.include?(item[:expected])
65
65
  # skip if not main folder
66
- Log.log.trace1{"Checking #{item[:app_root]}"}
66
+ Log.log.trace1 { "Checking #{item[:app_root]}" }
67
67
  next false unless Dir.exist?(item[:app_root])
68
- Log.log.debug{"Found #{item[:expected]}"}
68
+ Log.log.debug { "Found #{item[:expected]}" }
69
69
  sub_bin = item[:sub_bin] || 'bin'
70
70
  item[:ascp_path] = File.join(item[:app_root], sub_bin, Environment.instance.exe_file('ascp'))
71
71
  # skip if no ascp
@@ -17,12 +17,12 @@ module Aspera
17
17
  [{
18
18
  app_root: sdk_directory,
19
19
  sub_bin: ''
20
- }].map{ |i| i.merge({expected: APP_NAME})}
20
+ }].map { |i| i.merge({expected: APP_NAME}) }
21
21
  end
22
22
 
23
23
  # location of SDK files
24
24
  def sdk_directory=(folder)
25
- Log.log.debug{"sdk_directory=#{folder}"}
25
+ Log.log.debug { "sdk_directory=#{folder}" }
26
26
  @sdk_dir = folder
27
27
  sdk_directory
28
28
  end
@@ -53,7 +53,7 @@ module Aspera
53
53
  end
54
54
  end
55
55
  Aspera.assert(!result.nil?, 'Port not found in daemon logs')
56
- Log.log.debug{"Got port #{result} from log"}
56
+ Log.log.debug { "Got port #{result} from log" }
57
57
  return result
58
58
  end
59
59
  end
@@ -16,7 +16,7 @@ module URI
16
16
  def register_proxy_finder
17
17
  Aspera.assert(block_given?, 'block required for register_proxy_finder')
18
18
  # overload the method in URI : call user's provided block and fallback to original method
19
- define_method(:find_proxy){ |env_vars = ENV| yield(to_s) || find_proxy_orig(env_vars)}
19
+ define_method(:find_proxy) { |env_vars = ENV| yield(to_s) || find_proxy_orig(env_vars) }
20
20
  end
21
21
  end
22
22
  end
@@ -44,7 +44,7 @@ module Aspera
44
44
  context_ip = r_addr.to_s if r_addr.is_a?(Resolv::IPv4)
45
45
  end
46
46
  end
47
- Aspera.assert(!context_ip.nil?){"DNS name not found: #{context_host}"}
47
+ Aspera.assert(!context_ip.nil?) { "DNS name not found: #{context_host}" }
48
48
  # NOTE: Javascript code here with string inclusions
49
49
  javascript = <<END_OF_JAVASCRIPT
50
50
  function dnsResolve(host) {
@@ -73,7 +73,7 @@ END_OF_JAVASCRIPT
73
73
  end
74
74
 
75
75
  def register_uri_generic
76
- URI::Generic.register_proxy_finder{ |url_str| get_proxies(url_str).first}
76
+ URI::Generic.register_proxy_finder { |url_str| get_proxies(url_str).first }
77
77
  # allow chaining
78
78
  return self
79
79
  end
@@ -84,7 +84,7 @@ END_OF_JAVASCRIPT
84
84
  uri = URI.parse(service_url)
85
85
  simple_url = "#{uri.scheme}://#{uri.host}"
86
86
  if !@cache.key?(simple_url)
87
- Log.log.debug{"PAC: starting javascript for #{service_url}"}
87
+ Log.log.debug { "PAC: starting javascript for #{service_url}" }
88
88
  # require at runtime, in case there is no js engine
89
89
  require 'execjs'
90
90
  # read template lib
@@ -93,7 +93,7 @@ END_OF_JAVASCRIPT
93
93
  js_to_execute = "#{pac_dns_functions(uri.host)}#{@pac_functions}#{@proxy_auto_config}"
94
94
  executable_js = ExecJS.compile(js_to_execute)
95
95
  @cache[simple_url] = executable_js.call(PAC_MAIN_FUNCTION, simple_url, uri.host)
96
- Log.log.debug{"PAC: result: #{@cache[simple_url]}"}
96
+ Log.log.debug { "PAC: result: #{@cache[simple_url]}" }
97
97
  end
98
98
  return @cache[simple_url]
99
99
  end
@@ -106,7 +106,7 @@ END_OF_JAVASCRIPT
106
106
  # execute PAC script
107
107
  proxy_list_str = find_proxy_for_url(service_url)
108
108
  if !proxy_list_str.is_a?(String)
109
- Log.log.warn{"PAC: did not return a String, returned #{proxy_list_str.class}"}
109
+ Log.log.warn { "PAC: did not return a String, returned #{proxy_list_str.class}" }
110
110
  return uri_list
111
111
  end
112
112
  proxy_list_str.strip!
@@ -132,15 +132,15 @@ END_OF_JAVASCRIPT
132
132
  uri.password = @proxy_pass
133
133
  uri_list.push(uri)
134
134
  else
135
- Log.log.warn{"PAC: PROXY must be <address>:<port>, ignoring #{addr_port}"}
135
+ Log.log.warn { "PAC: PROXY must be <address>:<port>, ignoring #{addr_port}" }
136
136
  end
137
137
  rescue StandardError => e
138
- Log.log.warn{"PAC: cannot parse #{addr_port} #{e}"}
138
+ Log.log.warn { "PAC: cannot parse #{addr_port} #{e}" }
139
139
  end
140
- else Log.log.warn{"PAC: ignoring proxy type #{proxy_type}: not supported"}
140
+ else Log.log.warn { "PAC: ignoring proxy type #{proxy_type}: not supported" }
141
141
  end
142
142
  end
143
- Log.log.debug{"Proxies: #{uri_list}"}
143
+ Log.log.debug { "Proxies: #{uri_list}" }
144
144
  return uri_list
145
145
  end
146
146
  end