Skip to content

Commit e8384a2

Browse files
Copilotnhorton
andcommitted
Replace SHA256 with ActiveSupport::Digest and fix deletion logic
Co-authored-by: nhorton <[email protected]>
1 parent 31246ae commit e8384a2

File tree

3 files changed

+49
-27
lines changed

3 files changed

+49
-27
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ This cache store is designed to be committed to version control, making it ideal
1414
## Features
1515

1616
- **File-based storage**: Each cache entry is stored as separate `.key` and `.value` files
17-
- **Hashed filenames**: Uses SHA256 hashing for keys to create consistent, filesystem-safe filenames
17+
- **Hashed filenames**: Uses ActiveSupport::Digest for keys to create consistent, filesystem-safe filenames
1818
- **No expiration**: Cache entries do NOT honor expiration parameters - they persist until explicitly deleted
1919
- **Rails 7.1+ compatible**: Implements the ActiveSupport::Cache::Store interface
2020

@@ -94,7 +94,7 @@ value = cache.read("foo---bar---boo-ba") # => "27"
9494

9595
When a delimiter is configured:
9696
- The cache key is split by the delimiter into segments
97-
- Each segment creates a subdirectory named `hash(segment)` using SHA256
97+
- Each segment creates a subdirectory named `hash(segment)` using ActiveSupport::Digest
9898
- Each subdirectory contains a `_key_chunk` file with the original segment text
9999
- The cached value is stored in a `value` file in the final subdirectory
100100

@@ -104,7 +104,7 @@ This feature is useful for organizing cache entries hierarchically when keys hav
104104

105105
### Hashed Keys
106106

107-
Keys are hashed using SHA256 to create filesystem-safe filenames. The original key is preserved in the `.key` file, while the hash is used for the filename:
107+
Keys are hashed using ActiveSupport::Digest to create filesystem-safe filenames. The original key is preserved in the `.key` file, while the hash is used for the filename:
108108

109109
```ruby
110110
cache.write("user:123:profile", { name: "John" })

lib/active_support/cache/source_control_cache_store.rb

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
require "active_support/cache"
55
require "active_support/notifications"
66
require "active_support/core_ext/object/json"
7-
require "digest"
87
require "fileutils"
98

109
module ActiveSupport
@@ -165,13 +164,19 @@ def delete_entry_with_subdirectories(key, **options)
165164

166165
return false unless File.exist?(value_file)
167166

168-
# Delete the entire directory tree for this key
167+
# Delete only the deepest directory containing this specific entry
169168
chunks = key.to_s.split(@subdirectory_delimiter)
170-
first_chunk_hash = hash_chunk(chunks[0])
171-
dir_to_delete = File.join(@cache_path, first_chunk_hash)
169+
170+
# Build the full path to the final directory
171+
current_dir = @cache_path
172+
chunks.each do |chunk|
173+
chunk_hash = hash_chunk(chunk)
174+
current_dir = File.join(current_dir, chunk_hash)
175+
end
172176

173177
begin
174-
FileUtils.rm_rf(dir_to_delete) if File.exist?(dir_to_delete)
178+
# Delete the final directory (containing _key_chunk and value)
179+
FileUtils.rm_rf(current_dir) if File.exist?(current_dir)
175180
true
176181
rescue StandardError
177182
false
@@ -181,17 +186,17 @@ def delete_entry_with_subdirectories(key, **options)
181186
# Generate a hash for the given key
182187
#
183188
# @param key [String] The cache key
184-
# @return [String] The SHA256 hash of the key
189+
# @return [String] The hash of the key
185190
def hash_key(key)
186-
::Digest::SHA256.hexdigest(key.to_s)
191+
::ActiveSupport::Digest.hexdigest(key.to_s)
187192
end
188193

189194
# Generate a hash for a key chunk
190195
#
191196
# @param chunk [String] A chunk of the cache key
192-
# @return [String] The SHA256 hash of the chunk
197+
# @return [String] The hash of the chunk
193198
def hash_chunk(chunk)
194-
::Digest::SHA256.hexdigest(chunk.to_s)
199+
::ActiveSupport::Digest.hexdigest(chunk.to_s)
195200
end
196201

197202
# Get the path for the key file

spec/source_control_cache_store_spec.rb

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
store.write("my_key", "my_value")
4646

4747
# Calculate the expected hash
48-
hash = Digest::SHA256.hexdigest("my_key")
48+
hash = ActiveSupport::Digest.hexdigest("my_key")
4949
key_file = File.join(cache_path, "#{hash}.key")
5050
value_file = File.join(cache_path, "#{hash}.value")
5151

@@ -57,7 +57,7 @@
5757
original_key = "my_special_key"
5858
store.write(original_key, "value")
5959

60-
hash = Digest::SHA256.hexdigest(original_key)
60+
hash = ActiveSupport::Digest.hexdigest(original_key)
6161
key_file = File.join(cache_path, "#{hash}.key")
6262

6363
expect(File.read(key_file)).to eq(original_key)
@@ -82,7 +82,7 @@
8282

8383
it "removes both .key and .value files" do
8484
store.write("key", "value")
85-
hash = Digest::SHA256.hexdigest("key")
85+
hash = ActiveSupport::Digest.hexdigest("key")
8686
key_file = File.join(cache_path, "#{hash}.key")
8787
value_file = File.join(cache_path, "#{hash}.value")
8888

@@ -149,9 +149,9 @@
149149
end
150150

151151
describe "key hashing" do
152-
it "uses SHA256 for hashing keys" do
152+
it "uses ActiveSupport::Digest for hashing keys" do
153153
key = "test_key"
154-
expected_hash = Digest::SHA256.hexdigest(key)
154+
expected_hash = ActiveSupport::Digest.hexdigest(key)
155155

156156
store.write(key, "value")
157157

@@ -215,9 +215,9 @@
215215
store_with_delimiter.write("foo---bar---boo-ba", "27")
216216

217217
# Calculate expected hashes
218-
foo_hash = Digest::SHA256.hexdigest("foo")
219-
bar_hash = Digest::SHA256.hexdigest("bar")
220-
boo_ba_hash = Digest::SHA256.hexdigest("boo-ba")
218+
foo_hash = ActiveSupport::Digest.hexdigest("foo")
219+
bar_hash = ActiveSupport::Digest.hexdigest("bar")
220+
boo_ba_hash = ActiveSupport::Digest.hexdigest("boo-ba")
221221

222222
# Check that directories exist
223223
expect(File.directory?(File.join(cache_path_with_delimiter, foo_hash))).to be true
@@ -228,9 +228,9 @@
228228
it "creates _key_chunk files with correct content" do
229229
store_with_delimiter.write("foo---bar---boo-ba", "27")
230230

231-
foo_hash = Digest::SHA256.hexdigest("foo")
232-
bar_hash = Digest::SHA256.hexdigest("bar")
233-
boo_ba_hash = Digest::SHA256.hexdigest("boo-ba")
231+
foo_hash = ActiveSupport::Digest.hexdigest("foo")
232+
bar_hash = ActiveSupport::Digest.hexdigest("bar")
233+
boo_ba_hash = ActiveSupport::Digest.hexdigest("boo-ba")
234234

235235
# Check _key_chunk files
236236
foo_chunk_file = File.join(cache_path_with_delimiter, foo_hash, "_key_chunk")
@@ -245,9 +245,9 @@
245245
it "stores value in the final directory" do
246246
store_with_delimiter.write("foo---bar---boo-ba", "27")
247247

248-
foo_hash = Digest::SHA256.hexdigest("foo")
249-
bar_hash = Digest::SHA256.hexdigest("bar")
250-
boo_ba_hash = Digest::SHA256.hexdigest("boo-ba")
248+
foo_hash = ActiveSupport::Digest.hexdigest("foo")
249+
bar_hash = ActiveSupport::Digest.hexdigest("bar")
250+
boo_ba_hash = ActiveSupport::Digest.hexdigest("boo-ba")
251251

252252
value_file = File.join(cache_path_with_delimiter, foo_hash, bar_hash, boo_ba_hash, "value")
253253

@@ -263,7 +263,7 @@
263263
it "handles single chunk keys (no delimiter present)" do
264264
store_with_delimiter.write("single_key", "single_value")
265265

266-
single_hash = Digest::SHA256.hexdigest("single_key")
266+
single_hash = ActiveSupport::Digest.hexdigest("single_key")
267267
value_file = File.join(cache_path_with_delimiter, single_hash, "value")
268268

269269
expect(File.exist?(value_file)).to be true
@@ -314,5 +314,22 @@
314314
store_with_delimiter.write(key, "deep_value")
315315
expect(store_with_delimiter.read(key)).to eq("deep_value")
316316
end
317+
318+
it "deletes only the specific entry without affecting others with common prefixes" do
319+
# Write two keys that share the first chunk
320+
store_with_delimiter.write("foo---bar", "value1")
321+
store_with_delimiter.write("foo---baz", "value2")
322+
323+
# Verify both exist
324+
expect(store_with_delimiter.read("foo---bar")).to eq("value1")
325+
expect(store_with_delimiter.read("foo---baz")).to eq("value2")
326+
327+
# Delete the first one
328+
store_with_delimiter.delete("foo---bar")
329+
330+
# Verify only the deleted one is gone
331+
expect(store_with_delimiter.read("foo---bar")).to be_nil
332+
expect(store_with_delimiter.read("foo---baz")).to eq("value2")
333+
end
317334
end
318335
end

0 commit comments

Comments
 (0)