1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
|
require 'digest'
require 'fileutils'
module RGen
module Util
# Implements a cache for storing and loading data associated with arbitrary files.
# The data is stored in cache files within a subfolder of the folder where
# the associated files exists.
# The cache files are protected with a checksum and loaded data will be
# invalid in case either the associated file are the cache file has changed.
#
class FileCacheMap
# optional program version info to be associated with the cache files
# if the program version changes, cached data will also be invalid
attr_accessor :version_info
# +cache_dir+ is the name of the subfolder where cache files are created
# +postfix+ is an extension appended to the original file name for
# creating the name of the cache file
def initialize(cache_dir, postfix)
@postfix = postfix
@cache_dir = cache_dir
end
# load data associated with file +key_path+
# returns :invalid in case either the associated file or the cache file has changed
#
# options:
# :invalidation_reasons:
# an array which will receive symbols indicating why the cache is invalid:
# :no_cachefile, :cachefile_corrupted, :keyfile_changed
#
def load_data(key_path, options={})
reasons = options[:invalidation_reasons] || []
cf = cache_file(key_path)
result = nil
begin
File.open(cf, "rb") do |f|
header = f.read(41)
if !header
reasons << :cachefile_corrupted
return :invalid
end
checksum = header[0..39]
data = f.read
if calc_sha1(data) == checksum
if calc_sha1_keydata(key_path) == data[0..39]
result = data[41..-1]
else
reasons << :keyfile_changed
result = :invalid
end
else
reasons << :cachefile_corrupted
result = :invalid
end
end
rescue Errno::ENOENT
reasons << :no_cachefile
result = :invalid
end
result
end
# store data +value_data+ associated with file +key_path+
def store_data(key_path, value_data)
data = calc_sha1_keydata(key_path) + "\n" + value_data
data = calc_sha1(data) + "\n" + data
cf = cache_file(key_path)
FileUtils.mkdir(File.dirname(cf)) rescue Errno::EEXIST
File.open(cf, "wb") do |f|
f.write(data)
end
end
# remove cache files which are not associated with any file in +key_paths+
# will only remove files within +root_path+
def clean_unused(root_path, key_paths)
raise "key paths must be within root path" unless key_paths.all?{|p| p.index(root_path) == 0}
used_files = key_paths.collect{|p| cache_file(p)}
files = Dir[root_path+"/**/"+@cache_dir+"/*"+@postfix]
files.each do |f|
FileUtils.rm(f) unless used_files.include?(f)
end
end
private
def cache_file(path)
File.dirname(path) + "/"+@cache_dir+"/" + File.basename(path) + @postfix
end
def calc_sha1(data)
sha1 = Digest::SHA1.new
sha1.update(data)
sha1.hexdigest
end
def keyData(path)
File.read(path)+@version_info.to_s
end
# this method is much faster than calling +keyData+ and putting the result in +calc_sha1+
# reason is probably that there are not so many big strings being created
def calc_sha1_keydata(path)
begin
sha1 = Digest::SHA1.new
sha1.file(path)
sha1.update(@version_info.to_s)
sha1.hexdigest
rescue Errno::ENOENT
"<missing_key_file>"
end
end
end
end
end
|