123456789_123456789_123456789_123456789_123456789_

Class: Sprockets::Cache::FileStore

Relationships & Source Files
Inherits: Object
Defined in: lib/sprockets/cache/file_store.rb

Overview

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

Constant Summary

Class Method Summary

Instance Method Summary

Constructor Details

.new(root, max_size = DEFAULT_MAX_SIZE, logger = self.class.default_logger) ⇒ FileStore

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).
[ GitHub ]

  
# File 'lib/sprockets/cache/file_store.rb', line 42

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

Class Method Details

.default_logger

Internal: Default standard error fatal logger.

Returns a Logger.

[ GitHub ]

  
# File 'lib/sprockets/cache/file_store.rb', line 29

def self.default_logger
  logger = Logger.new($stderr)
  logger.level = Logger::FATAL
  logger
end

Instance Method Details

#clear(options = nil)

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

[ GitHub ]

  
# File 'lib/sprockets/cache/file_store.rb', line 139

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

#compute_size(caches) (private)

[ GitHub ]

  
# File 'lib/sprockets/cache/file_store.rb', line 166

def compute_size(caches)
  caches.inject(0) { |sum, (_, stat)| sum + stat.size }
end

#find_caches (private)

Internal: Get all cache files along with stats.

Returns an Array of [String filename, File::Stat] pairs sorted by mtime.

[ GitHub ]

  
# File 'lib/sprockets/cache/file_store.rb', line 152

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

#gc! (private)

[ GitHub ]

  
# File 'lib/sprockets/cache/file_store.rb', line 183

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

#get(key)

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.

[ GitHub ]

  
# File 'lib/sprockets/cache/file_store.rb', line 56

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

#inspect

Public: Pretty inspect

Returns String.

[ GitHub ]

  
# File 'lib/sprockets/cache/file_store.rb', line 126

def inspect
  "#<#{self.class} size=#{size}/#{@max_size}>"
end

#safe_open(path, &block) (private)

[ GitHub ]

  
# File 'lib/sprockets/cache/file_store.rb', line 176

def safe_open(path, &block)
  if File.exist?(path)
    File.open(path, 'rb', &block)
  end
rescue Errno::ENOENT
end

#safe_stat(fn) (private)

[ GitHub ]

  
# File 'lib/sprockets/cache/file_store.rb', line 170

def safe_stat(fn)
  File.stat(fn)
rescue Errno::ENOENT
  nil
end

#set(key, value)

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.

[ GitHub ]

  
# File 'lib/sprockets/cache/file_store.rb', line 85

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

#size (private)

[ GitHub ]

  
# File 'lib/sprockets/cache/file_store.rb', line 162

def size
  @size ||= compute_size(find_caches)
end