<commit_before>require 'digest/md5'
require 'fileutils'
require 'pathname'

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 = 1000

      # Public: Initialize the cache store.
      #
      # root     - A String path to a directory to persist cached values to.
      # max_size - A Integer of the maximum number of keys the store will hold.
      #            (default: 1000).
      def initialize(root, max_size = DEFAULT_MAX_SIZE)
        @root = root
        @size = find_caches.size
        @max_size = max_size
      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")

        if File.exist?(path)
          value = File.open(path, 'rb') { |f| Marshal.load(f) }
          ::FileUtils.touch(path)
          value
        else
          nil
        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)

        # Write data
        File.open(path, 'w') { |f| Marshal.dump(value, f) }

        # GC if necessary
        @size += 1 unless exists
        gc! if @size > @max_size

        value
      end

      private
        def find_caches
          Dir.glob(File.join(@root, '**/*.cache'))
        end

        def gc!
          caches = find_caches

          new_size = @max_size * 0.75
          num_to_delete = caches.size - new_size
          return unless num_to_delete > 0

          caches.sort_by! { |path| -File.mtime(path).to_i }
          ::FileUtils.remove(caches[0, num_to_delete], force: true)

          @size = find_caches.size
        end
    end
  end
end<commit_msg>Use atomic write for file store<commit_after>require 'digest/md5'
require 'fileutils'
require 'sprockets/fileutils'

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 = 1000

      # Public: Initialize the cache store.
      #
      # root     - A String path to a directory to persist cached values to.
      # max_size - A Integer of the maximum number of keys the store will hold.
      #            (default: 1000).
      def initialize(root, max_size = DEFAULT_MAX_SIZE)
        @root = root
        @size = find_caches.size
        @max_size = max_size
      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")

        if File.exist?(path)
          value = File.open(path, 'rb') { |f| Marshal.load(f) }
          ::FileUtils.touch(path)
          value
        else
          nil
        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)

        # Write data
        FileUtils.atomic_write(path) { |f| Marshal.dump(value, f) }

        # GC if necessary
        @size += 1 unless exists
        gc! if @size > @max_size

        value
      end

      private
        def find_caches
          Dir.glob(File.join(@root, '**/*.cache'))
        end

        def gc!
          caches = find_caches

          new_size = @max_size * 0.75
          num_to_delete = caches.size - new_size
          return unless num_to_delete > 0

          caches.sort_by! { |path| -File.mtime(path).to_i }
          ::FileUtils.remove(caches[0, num_to_delete], force: true)

          @size = find_caches.size
        end
    end
  end
end
{'fraction_non_alphanumeric': 0.09234365867913501, 'fraction_numerical': 0.0058445353594389245, 'mean_word_length': 1.7932535364526658, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3847429', 'n_tokens_mistral': 1561, 'n_tokens_neox': 1504, 'n_words': 483}