Module: Metaclean

Defined in:
lib/metaclean.rb,
lib/metaclean/cli.rb,
lib/metaclean/mat2.rb,
lib/metaclean/qpdf.rb,
lib/metaclean/ffmpeg.rb,
lib/metaclean/runner.rb,
lib/metaclean/display.rb,
lib/metaclean/version.rb,
lib/metaclean/exiftool.rb,
lib/metaclean/file_ops.rb,
lib/metaclean/strategy.rb,
lib/metaclean/committer.rb,
lib/metaclean/discovery.rb

Defined Under Namespace

Modules: Display, Exiftool, Ffmpeg, FileOps, Mat2, Qpdf, Strategy Classes: CLI, Committer, Discovery, Error, Runner, ToolsMissing

Constant Summary collapse

COMMAND_TIMEOUT =
120
MAX_OUTPUT_BYTES =
64 * 1024 * 1024
PROBE_TIMEOUT =
10
PROBE_MAX_OUTPUT_BYTES =
1024 * 1024
READ_CHUNK =
64 * 1024
TMP_MARKER =
'.metaclean.tmp.'
CLEAN_SUFFIX =
'_clean'
CLEAN_OUTPUT_RE =
/#{Regexp.escape(CLEAN_SUFFIX)}(?:_\d+)?(?:\.[^.]+)?\z/
VERSION =
'4.3.0'

Class Method Summary collapse

Class Method Details

.capture3(*cmd, timeout: command_timeout, max_output: MAX_OUTPUT_BYTES) ⇒ Object



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/metaclean.rb', line 39

def self.capture3(*cmd, timeout: command_timeout, max_output: MAX_OUTPUT_BYTES)
  Open3.popen3(*cmd, pgroup: true) do |stdin, stdout, stderr, wait_thr|
    out_t = err_t = deadline = nil
    begin
      stdin.close
      deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
      out_t = read_capped(stdout, max_output, wait_thr)
      err_t = read_capped(stderr, max_output, wait_thr)

      if wait_thr.join(timeout).nil?
        kill_group(wait_thr)
        drain_readers(out_t, err_t, deadline)
        raise Error, "#{cmd.first} timed out after #{timeout}s"
      end

      unless drain_readers(out_t, err_t, deadline)
        kill_group(wait_thr)
        drain_readers(out_t, err_t, deadline)
        raise Error, "#{cmd.first} timed out after #{timeout}s"
      end

      out, out_over = out_t.value
      err, err_over = err_t.value
      raise Error, "#{cmd.first} exceeded the #{max_output}-byte output limit" if out_over || err_over

      [out, err, wait_thr.value]
    rescue Interrupt
      kill_group(wait_thr)
      drain_readers(out_t, err_t, deadline) if out_t && err_t && deadline
      raise
    end
  end
end

.command_timeoutObject



34
35
36
37
# File 'lib/metaclean.rb', line 34

def self.command_timeout
  override = ENV['METACLEAN_TIMEOUT'].to_i
  override.positive? ? override : COMMAND_TIMEOUT
end

.drain_readers(out_t, err_t, deadline) ⇒ Object



73
74
75
76
77
78
79
# File 'lib/metaclean.rb', line 73

def self.drain_readers(out_t, err_t, deadline)
  [out_t, err_t].all? do |t|
    remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
    remaining = 0.1 if remaining < 0.1
    !t.join(remaining).nil?
  end
end

.ensure_tools!(in_place: false) ⇒ Object

Raises:



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/metaclean.rb', line 116

def self.ensure_tools!(in_place: false)
  missing = []
  missing << 'exiftool' unless Exiftool.available?
  missing << 'mat2'     unless Mat2.available?
  missing << 'qpdf'     unless Qpdf.available?
  missing << 'ffmpeg'   unless Ffmpeg.available?
  missing << 'cp with metadata preservation' if in_place && !FileOps.
  return if missing.empty?

  raise ToolsMissing, <<~MSG
    Missing required tool(s): #{missing.join(', ')}

    metaclean needs ExifTool, mat2, qpdf and ffmpeg together. Install all four:
      macOS:          brew install exiftool mat2 qpdf ffmpeg
      Debian/Ubuntu:  sudo apt install libimage-exiftool-perl mat2 qpdf ffmpeg
      Fedora:         sudo dnf install perl-Image-ExifTool mat2 qpdf ffmpeg
      Arch:           sudo pacman -S perl-image-exiftool mat2 qpdf ffmpeg
      Windows:        use WSL2 (https://learn.microsoft.com/windows/wsl/install) + the Debian/Ubuntu line

    --in-place additionally requires the POSIX cp utility supplied by macOS or coreutils on Linux.
  MSG
end

.ext_of(path) ⇒ Object



106
107
108
# File 'lib/metaclean.rb', line 106

def self.ext_of(path)
  File.extname(path.to_s).downcase.delete('.')
end

.kill_group(wait_thr) ⇒ Object



99
100
101
102
103
104
# File 'lib/metaclean.rb', line 99

def self.kill_group(wait_thr)
  Process.kill('-TERM', wait_thr.pid)
  Process.kill('-KILL', wait_thr.pid) unless wait_thr.join(2)
rescue Errno::ESRCH, Errno::EPERM
  nil
end

.read_capped(io, limit, wait_thr) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/metaclean.rb', line 81

def self.read_capped(io, limit, wait_thr)
  Thread.new do
    buf = +''
    over = false
    while (chunk = io.read(READ_CHUNK))
      next if over

      buf << chunk
      next unless buf.bytesize > limit

      over = true
      buf = buf.byteslice(0, limit)
      kill_group(wait_thr)
    end
    [buf, over]
  end
end

.safe_path(path) ⇒ Object



23
24
25
26
# File 'lib/metaclean.rb', line 23

def self.safe_path(path)
  s = path.to_s
  s.start_with?('-') ? File.join('.', s) : s
end