glimr/cache/file/cache
File-Based Cache Backend
Not every project needs Redis or a database just to cache a few values. This backend stores cached data as plain files on disk, using a two-level directory tree derived from SHA256 hashes so the filesystem doesn’t choke on a single directory with thousands of entries. It’s the zero-dependency option — no external services, no pool management, just the filesystem.
Values
pub fn flush(pool: pool.Pool) -> Result(Nil, cache.CacheError)
Nuking the entire directory tree is much faster than walking every subdirectory and deleting files one by one, especially when there are thousands of cached entries. The directory gets recreated automatically the next time something is written, so there’s no setup step needed after a flush.
pub fn forget(
pool: pool.Pool,
key: String,
) -> Result(Nil, cache.CacheError)
Treating “file doesn’t exist” as success makes this idempotent — two concurrent requests can both try to delete the same key without one of them getting a spurious error. Only actual filesystem failures (permissions, disk full) bubble up as errors.
pub fn get(
pool: pool.Pool,
key: String,
) -> Result(String, cache.CacheError)
Expired entries aren’t cleaned up by a background job — instead they get deleted the next time someone asks for them. This “lazy expiration” means you never waste CPU scanning for stale files, and entries that nobody reads again just sit harmlessly until the next flush.
pub fn has(pool: pool.Pool, key: String) -> Bool
Delegating to get() rather than just checking if the file exists means expired entries are cleaned up as a side effect. If we only did a file existence check, has() would return True for entries that get() would reject as expired — and that inconsistency would be really confusing to debug.
pub fn put(
pool: pool.Pool,
key: String,
value: String,
ttl_seconds: Int,
) -> Result(Nil, cache.CacheError)
The file format is dead simple: first line is the Unix timestamp when the entry expires, second line onward is the value. Computing the absolute expiry time here (now + TTL) means reads only need a single comparison against the current time, no duration arithmetic involved.
pub fn put_forever(
pool: pool.Pool,
key: String,
value: String,
) -> Result(Nil, cache.CacheError)
A timestamp of 0 is the sentinel for “never expires.” We could have used a separate file format or a flag, but keeping the same {timestamp}\n{value} layout means the read path doesn’t need to branch on format — it just checks if the timestamp is 0 and skips expiry logic.