Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_disk_cache.c

a bounded on-disk cache keyed by content

a bounded on-disk cache keyed by content

Author
Castagnier Mickael
Version
1.0
Date
24/09/2026

Makes a scratch directory, stores and reads entries, checks the path a key maps to, then gives three entries known last-used times and checks the size prune removes the least recently used first, the age expiry removes only the old, and neither ever touches a file the cache did not make. Last, two caches pruned as a group share one budget and one order.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>
#include "nilorea/n_log.h"
#ifdef __windows__
#include <direct.h>
#include <sys/utime.h>
#define ex_mkdir(p) _mkdir(p)
#else
#include <utime.h>
#define ex_mkdir(p) mkdir(p, 0700)
#endif
#define EX_DIR "ex_disk_cache.dir"
static int log_level = LOG_ERR;
static int failures = 0;
static void expect_true(const char* what, int ok) {
if (!ok) {
n_log(LOG_ERR, "FAIL: %s", what);
} else {
n_log(LOG_INFO, "ok: %s", what);
}
}
static int process_args(int argc, char** argv) {
int opt = 0;
while ((opt = getopt(argc, argv, "V:")) != -1) {
if (opt == 'V') {
log_level = atoi(optarg);
} else {
return -1;
}
}
return 0;
}
static int exists(const char* path) {
struct stat st;
return stat(path, &st) == 0;
}
static void write_foreign(const char* name) {
char path[512];
FILE* f = NULL;
snprintf(path, sizeof(path), "%s/%s", EX_DIR, name);
f = fopen(path, "wb");
if (f) {
fputs("not the cache's", f);
fclose(f);
}
}
static void put_aged(const N_DISK_CACHE* cache, const char* key, long ago) {
char data[100];
char path[512];
struct utimbuf when;
memset(data, key[0], sizeof(data));
n_disk_cache_put(cache, key, strlen(key), ".bin", data, sizeof(data));
n_disk_cache_path(cache, key, strlen(key), ".bin", path, sizeof(path));
when.actime = time(NULL) - ago;
when.modtime = when.actime;
utime(path, &when);
}
static int has(const N_DISK_CACHE* cache, const char* key) {
char path[512];
return n_disk_cache_path(cache, key, strlen(key), ".bin", path, sizeof(path)) && exists(path);
}
int main(int argc, char** argv) {
N_DISK_CACHE* cache = NULL;
unsigned char* data = NULL;
size_t len = 0;
uint64_t total = 0;
char p1[512], p2[512], p3[512];
const char* upper = "0123456789ABCDEF0123456789ABCDEF.bin";
if (process_args(argc, argv) != 0) {
fprintf(stderr, "Usage: %s [-V LOG_LEVEL]\n", argv[0]);
return 2;
}
expect_true("a directory that is not there is refused", n_disk_cache_open(EX_DIR "/nope") == NULL);
cache = n_disk_cache_open(EX_DIR "/");
expect_true("an existing directory opens", cache != NULL);
if (!cache) return 1;
expect_true("its name loses the trailing separator", strcmp(n_disk_cache_dir(cache), EX_DIR) == 0);
/* keys to paths */
expect_true("a key has a path", n_disk_cache_path(cache, "k", 1, ".png", p1, sizeof(p1)));
n_disk_cache_path(cache, "k", 1, ".png", p2, sizeof(p2));
n_disk_cache_path(cache, "k", 1, ".jpg", p3, sizeof(p3));
expect_true("the same key has the same path", strcmp(p1, p2) == 0);
expect_true("another extension, another file", strcmp(p1, p3) != 0);
expect_true("the path is the directory, 32 digits and the extension",
strlen(p1) == strlen(EX_DIR) + 1 + N_DISK_CACHE_NAME_LEN + 4 && strcmp(p1 + strlen(p1) - 4, ".png") == 0);
expect_true("an extension with a separator is refused", n_disk_cache_path(cache, "k", 1, "/x", p1, sizeof(p1)) == FALSE);
expect_true("a path that does not fit is refused", n_disk_cache_path(cache, "k", 1, ".png", p1, 8) == FALSE);
/* put and get */
expect_true("put stores", n_disk_cache_put(cache, "hello", 5, NULL, "world", 5));
expect_true("get finds it", n_disk_cache_get(cache, "hello", 5, NULL, 1024, &data, &len) && len == 5 && memcmp(data, "world", 5) == 0 && data[5] == '\0');
free(data);
data = NULL;
expect_true("an entry over the limit is a miss", n_disk_cache_get(cache, "hello", 5, NULL, 4, &data, &len) == FALSE && data == NULL);
expect_true("another key is a miss", n_disk_cache_get(cache, "hellO", 5, NULL, 1024, &data, &len) == FALSE);
expect_true("put replaces", n_disk_cache_put(cache, "hello", 5, NULL, "again!", 6) &&
n_disk_cache_get(cache, "hello", 5, NULL, 1024, &data, &len) && len == 6);
free(data);
data = NULL;
expect_true("remove removes", n_disk_cache_remove(cache, "hello", 5, NULL) && !n_disk_cache_get(cache, "hello", 5, NULL, 1024, &data, &len));
expect_true("removing what is not there is fine", n_disk_cache_remove(cache, "hello", 5, NULL));
/* files the cache did not make */
write_foreign("notes.txt");
write_foreign(upper);
/* least recently used first */
put_aged(cache, "a", 3000);
put_aged(cache, "b", 2000);
put_aged(cache, "c", 1000);
expect_true("size counts only the entries", n_disk_cache_size(cache, &total) == 3 && total == 300);
expect_true("prune to 250 bytes removes one", n_disk_cache_prune(cache, 250) == 1);
expect_true("the least recently used", !has(cache, "a") && has(cache, "b") && has(cache, "c"));
expect_true("prune under budget removes none", n_disk_cache_prune(cache, 1000) == 0);
expect_true("prune to 0 removes the rest", n_disk_cache_prune(cache, 0) == 2 && !has(cache, "b") && !has(cache, "c"));
/* by age */
put_aged(cache, "d", 1000);
put_aged(cache, "e", 0);
expect_true("expire at 500 s removes the old one", n_disk_cache_expire(cache, 500) == 1 && !has(cache, "d") && has(cache, "e"));
expect_true("expire at 0 removes nothing", n_disk_cache_expire(cache, 0) == 0 && has(cache, "e"));
n_disk_cache_prune(cache, 0);
snprintf(p1, sizeof(p1), "%s/notes.txt", EX_DIR);
snprintf(p2, sizeof(p2), "%s/%s", EX_DIR, upper);
expect_true("a file that is not an entry is never removed", exists(p1) && exists(p2));
remove(p1);
remove(p2);
/* two caches, one budget: the oldest entries go whichever directory they
* are in */
{
N_DISK_CACHE* other = NULL;
const N_DISK_CACHE* both[2];
ex_mkdir(EX_DIR "/other");
other = n_disk_cache_open(EX_DIR "/other");
expect_true("a second cache opens", other != NULL);
if (other) {
both[0] = cache;
both[1] = other;
put_aged(cache, "f", 4000);
put_aged(other, "g", 3000);
put_aged(cache, "h", 2000);
put_aged(other, "i", 1000);
expect_true("a group prune to 250 bytes removes two", n_disk_cache_prune_group(both, 2, 250) == 2);
expect_true("the two oldest, one from each", !has(cache, "f") && !has(other, "g") && has(cache, "h") && has(other, "i"));
expect_true("under budget removes none", n_disk_cache_prune_group(both, 2, 1000) == 0);
expect_true("a NULL cache is skipped", n_disk_cache_prune_group((const N_DISK_CACHE* const[]){NULL, other}, 2, 0) == 1 && !has(other, "i") && has(cache, "h"));
expect_true("no caches is an error", n_disk_cache_prune_group(both, 0, 0) == -1);
n_disk_cache_prune(cache, 0);
}
expect_true("the second scratch directory is empty again", rmdir(EX_DIR "/other") == 0);
}
expect_true("close sets the handle to NULL", cache == NULL);
expect_true("the scratch directory is empty again", rmdir(EX_DIR) == 0);
if (failures) {
fprintf(stderr, "ex_disk_cache: %d failure(s)\n", failures);
return 1;
}
printf("ex_disk_cache: ok\n");
return 0;
}
void process_args(int argc, char **argv)
static int failures
Definition ex_atlas.c:51
int main(void)
Definition ex_atlas.c:117
static void expect_true(const char *label, int cond)
Definition ex_atlas.c:53
static int exists(const char *path)
does a file exist
#define ex_mkdir(p)
owner only elsewhere
static void write_foreign(const char *name)
write a small file that is not an entry
#define EX_DIR
the scratch directory, made and removed by this example
static int has(const N_DISK_CACHE *cache, const char *key)
is the entry for key there
static int log_level
static void put_aged(const N_DISK_CACHE *cache, const char *key, long ago)
store 100 bytes under key and say it was last used ago seconds ago
volatile char key[227]
TRUE while the key is held.
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:113
#define LOG_ERR
error conditions
Definition n_log.h:100
void set_log_level(const int log_level)
Set the global log level value ( static int LOG_LEVEL ).
Definition n_log.c:123
#define LOG_INFO
informational
Definition n_log.h:106
const char * n_disk_cache_dir(const N_DISK_CACHE *cache)
The cache's directory, as it was opened.
#define N_DISK_CACHE_NAME_LEN
length of an entry's name before its extension: 128 bits in hexadecimal
int n_disk_cache_get(const N_DISK_CACHE *cache, const void *key, size_t key_len, const char *ext, size_t max_len, unsigned char **data, size_t *len)
Read an entry.
int n_disk_cache_path(const N_DISK_CACHE *cache, const void *key, size_t key_len, const char *ext, char *out, size_t out_len)
The file an entry lives in, whether or not it exists.
void n_disk_cache_close(N_DISK_CACHE **cache)
Forget a cache.
int n_disk_cache_put(const N_DISK_CACHE *cache, const void *key, size_t key_len, const char *ext, const void *data, size_t len)
Store an entry, replacing any under the same key and extension.
int n_disk_cache_prune_group(const N_DISK_CACHE *const *caches, int count, uint64_t max_bytes)
n_disk_cache_prune() over several caches that share one budget.
int n_disk_cache_prune(const N_DISK_CACHE *cache, uint64_t max_bytes)
Remove least recently used entries until the rest fit a budget.
int n_disk_cache_remove(const N_DISK_CACHE *cache, const void *key, size_t key_len, const char *ext)
Remove one entry, for a caller that found it unreadable.
int n_disk_cache_expire(const N_DISK_CACHE *cache, int64_t max_age_seconds)
Remove every entry unused for longer than an age.
int n_disk_cache_size(const N_DISK_CACHE *cache, uint64_t *total)
How many bytes the cache's entries take.
N_DISK_CACHE * n_disk_cache_open(const char *dir)
Use a directory as a cache.
Common headers and low-level functions & define.
a cache: only where it is
A bounded cache of files in one directory, keyed by content.
Generic log system.