44require "active_support/cache"
55require "active_support/notifications"
66require "active_support/core_ext/object/json"
7- require "digest"
87require "fileutils"
98
109module ActiveSupport
@@ -19,23 +18,29 @@ module Cache
1918 # Example usage:
2019 # config.cache_store = :source_control_cache_store, cache_path: "tmp/cache"
2120 class SourceControlCacheStore < Store
22- attr_reader :cache_path
21+ attr_reader :cache_path , :subdirectory_delimiter
2322
2423 # Initialize a new SourceControlCacheStore
2524 #
2625 # @param cache_path [String] The directory where cache files will be stored
26+ # @param subdirectory_delimiter [String, nil] Optional delimiter to split keys into subdirectories
2727 # @param options [Hash] Additional options (currently unused)
28- def initialize ( cache_path :, **options )
28+ def initialize ( cache_path :, subdirectory_delimiter : nil , **options )
2929 super ( options )
3030 @cache_path = cache_path
31+ @subdirectory_delimiter = subdirectory_delimiter
3132 FileUtils . mkdir_p ( @cache_path )
3233 end
3334
3435 # Clear all cache entries
3536 def clear ( options = nil )
3637 if File . directory? ( @cache_path )
37- Dir . glob ( File . join ( @cache_path , "*" ) ) . each do |file |
38- File . delete ( file ) if File . file? ( file )
38+ Dir . glob ( File . join ( @cache_path , "*" ) ) . each do |path |
39+ if File . file? ( path )
40+ File . delete ( path )
41+ elsif File . directory? ( path )
42+ FileUtils . rm_rf ( path )
43+ end
3944 end
4045 end
4146 true
@@ -49,8 +54,7 @@ def clear(options = nil)
4954 # @param options [Hash] Options (unused)
5055 # @return [Object, nil] The cached value or nil if not found
5156 def read_entry ( key , **options )
52- hash = hash_key ( key )
53- value_file = value_path ( hash )
57+ value_file = value_path_for_key ( key )
5458
5559 return nil unless File . exist? ( value_file )
5660
@@ -74,6 +78,23 @@ def read_entry(key, **options)
7478 # @param options [Hash] Options (expiration is ignored)
7579 # @return [Boolean] Returns true on success, false on failure
7680 def write_entry ( key , entry , **options )
81+ if @subdirectory_delimiter
82+ write_entry_with_subdirectories ( key , entry , **options )
83+ else
84+ write_entry_simple ( key , entry , **options )
85+ end
86+ rescue StandardError
87+ # Return false if write fails (permissions, disk space, etc.)
88+ false
89+ end
90+
91+ # Write entry using simple hash-based file structure
92+ #
93+ # @param key [String] The cache key
94+ # @param entry [ActiveSupport::Cache::Entry] The cache entry
95+ # @param options [Hash] Options (expiration is ignored)
96+ # @return [Boolean] Returns true on success
97+ def write_entry_simple ( key , entry , **options )
7798 hash = hash_key ( key )
7899
79100 # Write the key file
@@ -83,9 +104,32 @@ def write_entry(key, entry, **options)
83104 File . write ( value_path ( hash ) , serialize_entry ( entry , **options ) )
84105
85106 true
86- rescue StandardError
87- # Return false if write fails (permissions, disk space, etc.)
88- false
107+ end
108+
109+ # Write entry using subdirectory structure
110+ #
111+ # @param key [String] The cache key
112+ # @param entry [ActiveSupport::Cache::Entry] The cache entry
113+ # @param options [Hash] Options (expiration is ignored)
114+ # @return [Boolean] Returns true on success
115+ def write_entry_with_subdirectories ( key , entry , **options )
116+ chunks = key . to_s . split ( @subdirectory_delimiter )
117+ current_dir = @cache_path
118+
119+ # Create subdirectories for each chunk
120+ chunks . each do |chunk |
121+ chunk_hash = hash_chunk ( chunk )
122+ current_dir = File . join ( current_dir , chunk_hash )
123+ FileUtils . mkdir_p ( current_dir )
124+
125+ # Write _key_chunk file
126+ File . write ( File . join ( current_dir , "_key_chunk" ) , chunk )
127+ end
128+
129+ # Write the value file in the final directory
130+ File . write ( File . join ( current_dir , "value" ) , serialize_entry ( entry , **options ) )
131+
132+ true
89133 end
90134
91135 # Delete an entry from the cache
@@ -94,6 +138,15 @@ def write_entry(key, entry, **options)
94138 # @param options [Hash] Options (unused)
95139 # @return [Boolean] Returns true if any file was deleted
96140 def delete_entry ( key , **options )
141+ if @subdirectory_delimiter
142+ delete_entry_with_subdirectories ( key , **options )
143+ else
144+ delete_entry_simple ( key , **options )
145+ end
146+ end
147+
148+ # Delete entry using simple hash-based file structure
149+ def delete_entry_simple ( key , **options )
97150 hash = hash_key ( key )
98151 key_file = key_path ( hash )
99152 value_file = value_path ( hash )
@@ -115,12 +168,42 @@ def delete_entry(key, **options)
115168 deleted
116169 end
117170
171+ # Delete entry using subdirectory structure
172+ #
173+ # @param key [String] The cache key
174+ # @param options [Hash] Options (unused)
175+ # @return [Boolean] Returns true if the entry was deleted
176+ def delete_entry_with_subdirectories ( key , **options )
177+ value_file = value_path_for_key ( key )
178+
179+ return false unless File . exist? ( value_file )
180+
181+ # Delete only the deepest directory containing this specific entry
182+ current_dir = subdirectory_path_for_key ( key )
183+
184+ begin
185+ # Delete the final directory (containing _key_chunk and value)
186+ FileUtils . rm_rf ( current_dir ) if File . exist? ( current_dir )
187+ true
188+ rescue StandardError
189+ false
190+ end
191+ end
192+
118193 # Generate a hash for the given key
119194 #
120195 # @param key [String] The cache key
121- # @return [String] The SHA256 hash of the key
196+ # @return [String] The hash of the key
122197 def hash_key ( key )
123- ::Digest ::SHA256 . hexdigest ( key . to_s )
198+ ::ActiveSupport ::Digest . hexdigest ( key . to_s )
199+ end
200+
201+ # Generate a hash for a key chunk
202+ #
203+ # @param chunk [String] A chunk of the cache key
204+ # @return [String] The hash of the chunk
205+ def hash_chunk ( chunk )
206+ ::ActiveSupport ::Digest . hexdigest ( chunk . to_s )
124207 end
125208
126209 # Get the path for the key file
@@ -138,6 +221,34 @@ def key_path(hash)
138221 def value_path ( hash )
139222 File . join ( @cache_path , "#{ hash } .value" )
140223 end
224+
225+ # Get the value file path for a given key
226+ #
227+ # @param key [String] The cache key
228+ # @return [String] The full path to the value file
229+ def value_path_for_key ( key )
230+ if @subdirectory_delimiter
231+ File . join ( subdirectory_path_for_key ( key ) , "value" )
232+ else
233+ value_path ( hash_key ( key ) )
234+ end
235+ end
236+
237+ # Get the subdirectory path for a given key
238+ #
239+ # @param key [String] The cache key
240+ # @return [String] The full path to the subdirectory for this key
241+ def subdirectory_path_for_key ( key )
242+ chunks = key . to_s . split ( @subdirectory_delimiter )
243+ current_dir = @cache_path
244+
245+ chunks . each do |chunk |
246+ chunk_hash = hash_chunk ( chunk )
247+ current_dir = File . join ( current_dir , chunk_hash )
248+ end
249+
250+ current_dir
251+ end
141252 end
142253 end
143254end
0 commit comments