politrunc.rb
Windows 版の Polipo の -x オプションは使えない。
http://ely.ath.cx/~piranha/software/polipo_trimcache/
なのでここの polipo-trimcache という Python 製のコマンドを移植してみる。
名前は POLIpo cache TRUNCate ってことで。いまいちしっくりこないけど。
require 'singleton' require 'optparse' module Cache def self.transverse(root, precise_expiry = false, at_verbose = nil, at_debug = nil) transverser = Transverser.new(precise_expiry, at_verbose, at_debug) transverser.transverse(root) return transverser.objects, transverser.directories end class Transverser def initialize(precise_expiry = false, at_verbose = nil, at_debug = nil) @precise_expiry = precise_expiry @at_verbose = at_verbose @at_debug = at_debug @directories = [] @objects = [] @stack = [] end attr_reader :directories attr_reader :objects def verbose(message) @at_verbose.call(message) if @at_verbose end def debug(message) @at_debug.call(message) if @at_debug end def transverse(root, parent = nil) @stack.push(parent) debug "Entering directory: #{root}" (Dir.entries(root) - ['.', '..']).each do |ent| path = File.join(root, ent) if ent.start_with?('.') transverse_event_dotfile(path) else __send__("transverse_event_#{File.ftype(path)}", path) end end ensure @stack.pop end def add_as_child(obj) parent = @stack.last and parent.children.push(obj) end def transverse_event_file(path) file = Object.new(path, @stack.last, @precise_expiry) add_as_child(file) objects << file end def transverse_event_directory(path) dir = Directory.new(path, @stack.last) add_as_child(dir) directories << dir transverse(path, dir) end def transverse_event_else(path) verbose "Skipping special file: #{path}" end alias transverse_event_dotfile transverse_event_else ["characterSpecial", "blockSpecial", "fifo", "link", "socket"].each do |ftype| alias_method "transverse_event_#{ftype}", :transverse_event_else end def transverse_event_unknown(path) raise Exception, "[BUG] must not happen, File.ftype() returns `unknown'" end end class Entry def initialize(path, parent = nil) @path = path @parent = parent end attr_reader :path attr_reader :parent attr_reader :size def each_ancestor return enum_for(__method__) unless block_given? child, parent = self, self.parent while parent yield(parent, child) child, parent = parent, parent.parent end end end class Directory < Entry def initialize(path, parent = nil) super @size = File.size(path) @children = [] end attr_reader :children def entries Dir.entries(path) - ['.', '..'] end def remove Dir.rmdir path end end class Object < Entry def initialize(path, parent = nil, precise_expiry = false) super(path, parent) stat = File.lstat(path) @size = stat.blocks ? stat.blocks * 512 : stat.size @last_access = precise_expiry ? (get_precise_access_time(path) || 0) : stat.mtime end attr_reader :last_access def remove File.unlink path end def disinherite parent.children.delete(self) if parent end end end BYTE_QUANTIFIERS = {} %w[K M G T P E Z Y].inject(1){|r, i| BYTE_QUANTIFIERS[i] = r * 1024 } def parse_rest_args(cache_dir, amount) qua = 1 num = amount if /(#{Regexp.union(*BYTE_QUANTIFIERS.keys)})B?\Z/io =~ amount qua = BYTE_QUANTIFIERS[$1] num = $` end size = (Float(num) * qua).ceil return cache_dir, size end # incomplete def to_disk_size(n) n end # incomplete def to_interval_description(sec) sec end force_p = false dryrun_p = false precise_expiry_p = false verbose_p = false debug_p = false cache_dir = nil target_size = nil me = File.basename($0, '.rb') echo = lambda{|msg| $stderr.puts "#{me}: #{msg}" } verbose = lambda{|msg| echo.call(msg) if verbose_p } debug = lambda{|msg| echo.call(msg) if debug_p } o = OptionParser.new o.on '-f', '--force' do force_p = true end o.on '-n', '--no-op' do dryrun_p = true end o.on '--dryrun' do dryrun_p = true end o.on '-p', '--precise-expiry' do precise_expiry_p = true end o.on '-V', '--verbose' do verbose_p = true end o.on '-D', '--debug' do debug_p = true end o.parse!(ARGV) cache_dir, target_size = parse_rest_args(*ARGV) verbose["Transversing cache..."] objs, dirs = Cache.transverse(cache_dir, precise_expiry_p, verbose, debug) verbose["Sorting and counting..."] objs = objs.sort_by{|obj| obj.last_access } total_size = [objs, dirs].map{|list| list.inject(0){|r, i| r + i.size } }.inject(:+) verbose["Cache is currently #{to_disk_size(total_size)}B (#{objs.size} files, #{dirs.size} directories)."] verbose["Trimming..."] n_file_rm = 0 n_dir_rm = 0 latest_trimmed = nil until objs.empty? || total_size <= target_size victim = objs.pop if dryrun_p puts victim.path else debug["Removing file: #{victim.path}"] victim.remove end total_size -= victim.size n_file_rm += 1 latest_trimmed = victim.last_access victim.each_ancestor do |parent, child| parent.children.delete(child) break unless parent.children.empty? break if parent.entries.empty? if dryrun_p debug["Removing directory [NOOP]: #{parent.path}"] else debug["Removing directory: #{parent.path}"] parent.remove end total_size -= parent.size dirs.delete(parent) n_dir_rm += 1 end end verbose["Cache trimmed (-#{n_file_rm} files, -#{n_dir_rm} dirs) to #{to_disk_size(total_size)}B (#{objs.size} files, #{dirs.size} directories)."] verbose["Latest file removed was #{to_interval_description(Time.now - latest_trimmed)} old"] if latest_trimmed