Я обнаружил этот вопрос, когда писал свой сценарий примерно для той же цели, и ваша идея о кэшировании файлов с их dev+inode+size+mtime показалась мне очень полезной, поэтому я добавил ее. Ваша идея и моя реализация отличаются, так как я поздно наткнулся на эту страницу и решил не переписывать все:
Сценарий для простоты хранит записи кэша в одном файле YAML. Вы по-прежнему можете поделиться этим файлом на нескольких машинах, но есть риск RCE, плюс вам нужно будет написать блокирующую оболочку из-за TOCTOU в файле YAML.
Вероятно, это будет работать только в Linux и, если повезет, в других Unix-системах.
Используйте на свой страх и риск. Содержимое вашего кэша не будет защищено.
Сначала запустите gem install chronic_duration
.
#!/usr/bin/env ruby
# Usage: memoize [-D DATABASE] [-T TIMEOUT] [-F] [--] COMMAND [ARG]...
# or memoize [-D DATABASE] --cleanup
#
# OPTIONS
# -D DATABASE Store entries in YAML format in DATABASE file.
# -T TIMEOUT Invalidate memoized entries older than TIMEOUT.
# -F Track file changes (dev+inode+size+mtime).
# --cleanup Remove all stale entries.
require 'date'
require 'optparse'
require 'digest'
require 'yaml'
require 'chronic_duration'
require 'open3'
MYSELF = File.basename(__FILE__)
DEFAULT_DBFILE = "#{Dir.home}/.config/memoize.yml"
DEFAULT_TIMEOUT = '1 week'
def fc(fpath) # File characteristic
return [:dev, :ino, :size, :mtime].map do |s|
Digest::SHA1.digest(Integer(File.stat(fpath).send(s)).to_s.b)
end.join
end
def cmdline_checksum(cmdline, fchanges)
pre_cksum_bytes = "".b
cmdline.each do |c|
characteristic = (File.exists?(c) and fchanges) ? fc(c) : c
pre_cksum_bytes += Digest::SHA1.digest(characteristic)
end
return Digest::SHA1.digest(pre_cksum_bytes)
end
def timed_out?(entry)
return (entry[:timestamp] + Integer(entry[:timeout])) < Time.now
end
def pluralize(n, singular, plural)
return (n % 100 == 11 || n % 10 != 1) ? plural : singular
end
fail "memoize: FATAL: this is a script, not a library" unless __FILE__ == $0
$dbfile = DEFAULT_DBFILE
$timeout = DEFAULT_TIMEOUT
$fchanges = false
$cleanup = false
$retcode = 0
$replay = false
ARGV.options do |o|
o.version = '2018.06.23'
o.banner = "Usage: memoize [OPTION]... [--] COMMAND [ARG]...\n"+
"Cache results of COMMAND and replay its output"
o.separator ""
o.separator "OPTIONS"
o.summary_indent = " "
o.summary_width = 17
o.on('-D=DATABASE', "Default: #{DEFAULT_DBFILE}") { |d| $dbfile = d }
o.on('-T=TIMEOUT', "Default: #{DEFAULT_TIMEOUT}") { |t| $timeout = t }
o.on('-F', "Track file changes (dev+inode+size+mtime)") { $fchanges = true }
o.on('--cleanup', "Remove all stale entries") { $cleanup = true }
end.parse!
begin
File.open($dbfile, 'a') {}
File.chmod(0600, $dbfile)
end unless File.exists?($dbfile)
db = (YAML.load(File.read($dbfile)) or {})
cmdline = ARGV
cksum = cmdline_checksum(cmdline, $fchanges)
entry = {
cmdline: cmdline,
timestamp: Time.now,
timeout: '1 week',
stdout: "",
stderr: "",
retcode: 0,
}
if $cleanup
entries = db.keys.select{|k| timed_out?(db[k]) }
c = entries.count
entries.each do |k|
db.delete(k)
end
STDERR.puts "memoize: NOTE: #{c} stale #{pluralize(c, "entry", "entries")} removed"
File.open($dbfile, 'w') { |f| f << YAML.dump(db) }
exit
end
$replay = db.key?(cksum) && (not timed_out?(db[cksum]))
if $replay
entry = db[cksum]
else
Open3.popen3(*cmdline) do |i, o, e, t|
i.close
entry[:stdout] = o.read
entry[:stderr] = e.read
entry[:retcode] = t.value.exitstatus
end
entry[:timestamp] = Time.now
entry[:timeout] = Integer(ChronicDuration.parse($timeout))
db[cksum] = entry
end
$retcode = entry[:retcode]
STDOUT.write(entry[:stdout]) # NOTE: we don't record or replay stream timing
STDERR.write(entry[:stderr])
STDOUT.flush
STDERR.flush
File.open($dbfile, 'w') { |f| f << YAML.dump(db) }
exit! $retcode