File: //usr/local/rvm/gems/ruby-2.5.9/gems/sprockets-4.0.2/lib/sprockets/cache/file_store.rb
# frozen_string_literal: true
require 'fileutils'
require 'logger'
require 'sprockets/encoding_utils'
require 'sprockets/path_utils'
require 'zlib'
module Sprockets
class Cache
# Public: A file system cache store that automatically cleans up old keys.
#
# Assign the instance to the Environment#cache.
#
# environment.cache = Sprockets::Cache::FileStore.new("/tmp")
#
# See Also
#
# ActiveSupport::Cache::FileStore
#
class FileStore
# Internal: Default key limit for store.
DEFAULT_MAX_SIZE = 25 * 1024 * 1024
EXCLUDED_DIRS = ['.', '..'].freeze
GITKEEP_FILES = ['.gitkeep', '.keep'].freeze
# Internal: Default standard error fatal logger.
#
# Returns a Logger.
def self.default_logger
logger = Logger.new($stderr)
logger.level = Logger::FATAL
logger
end
# Public: Initialize the cache store.
#
# root - A String path to a directory to persist cached values to.
# max_size - A Integer of the maximum size the store will hold (in bytes).
# (default: 25MB).
# logger - The logger to which some info will be printed.
# (default logger level is FATAL and won't output anything).
def initialize(root, max_size = DEFAULT_MAX_SIZE, logger = self.class.default_logger)
@root = root
@max_size = max_size
@gc_size = max_size * 0.75
@logger = logger
end
# Public: Retrieve value from cache.
#
# This API should not be used directly, but via the Cache wrapper API.
#
# key - String cache key.
#
# Returns Object or nil or the value is not set.
def get(key)
path = File.join(@root, "#{key}.cache")
value = safe_open(path) do |f|
begin
EncodingUtils.unmarshaled_deflated(f.read, Zlib::MAX_WBITS)
rescue Exception => e
@logger.error do
"#{self.class}[#{path}] could not be unmarshaled: " +
"#{e.class}: #{e.message}"
end
nil
end
end
if value
FileUtils.touch(path)
value
end
end
# Public: Set a key and value in the cache.
#
# This API should not be used directly, but via the Cache wrapper API.
#
# key - String cache key.
# value - Object value.
#
# Returns Object value.
def set(key, value)
path = File.join(@root, "#{key}.cache")
# Ensure directory exists
FileUtils.mkdir_p File.dirname(path)
# Check if cache exists before writing
exists = File.exist?(path)
# Serialize value
marshaled = Marshal.dump(value)
# Compress if larger than 4KB
if marshaled.bytesize > 4 * 1024
deflater = Zlib::Deflate.new(
Zlib::BEST_COMPRESSION,
Zlib::MAX_WBITS,
Zlib::MAX_MEM_LEVEL,
Zlib::DEFAULT_STRATEGY
)
deflater << marshaled
raw = deflater.finish
else
raw = marshaled
end
# Write data
PathUtils.atomic_write(path) do |f|
f.write(raw)
@size = size + f.size unless exists
end
# GC if necessary
gc! if size > @max_size
value
end
# Public: Pretty inspect
#
# Returns String.
def inspect
"#<#{self.class} size=#{size}/#{@max_size}>"
end
# Public: Clear the cache
#
# adapted from ActiveSupport::Cache::FileStore#clear
#
# Deletes all items from the cache. In this case it deletes all the entries in the specified
# file store directory except for .keep or .gitkeep. Be careful which directory is specified
# as @root because everything in that directory will be deleted.
#
# Returns true
def clear(options=nil)
if File.exist?(@root)
root_dirs = Dir.entries(@root).reject { |f| (EXCLUDED_DIRS + GITKEEP_FILES).include?(f) }
FileUtils.rm_r(root_dirs.collect{ |f| File.join(@root, f) })
end
true
end
private
# Internal: Get all cache files along with stats.
#
# Returns an Array of [String filename, File::Stat] pairs sorted by
# mtime.
def find_caches
Dir.glob(File.join(@root, '**/*.cache')).reduce([]) { |stats, filename|
stat = safe_stat(filename)
# stat maybe nil if file was removed between the time we called
# dir.glob and the next stat
stats << [filename, stat] if stat
stats
}.sort_by { |_, stat| stat.mtime.to_i }
end
def size
@size ||= compute_size(find_caches)
end
def compute_size(caches)
caches.inject(0) { |sum, (_, stat)| sum + stat.size }
end
def safe_stat(fn)
File.stat(fn)
rescue Errno::ENOENT
nil
end
def safe_open(path, &block)
if File.exist?(path)
File.open(path, 'rb', &block)
end
rescue Errno::ENOENT
end
def gc!
start_time = Time.now
caches = find_caches
size = compute_size(caches)
delete_caches, keep_caches = caches.partition { |filename, stat|
deleted = size > @gc_size
size -= stat.size
deleted
}
return if delete_caches.empty?
FileUtils.remove(delete_caches.map(&:first), force: true)
@size = compute_size(keep_caches)
@logger.warn do
secs = Time.now.to_f - start_time.to_f
"#{self.class}[#{@root}] garbage collected " +
"#{delete_caches.size} files (#{(secs * 1000).to_i}ms)"
end
end
end
end
end