6 ex__disk__cache_8c
gullradriel edited this page 2026-10-09 00:55:18 +02:00

examples/ex_disk_cache.c

Functions

Name
void write_foreign(const char * name)
write a small file that is not an entry
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
int process_args(int argc, char ** argv)
read -V LOGLEVEL
int main(int argc, char ** argv)
int has(const N_DISK_CACHE * cache, const char * key)
is the entry for key there
void expect_true(const char * what, int ok)
record one check
int exists(const char * path)
does a file exist

Attributes

Name
int log_level
int failures

Defines

Name
ex_mkdir(p)
owner only elsewhere
EX_DIR
the scratch directory, made and removed by this example

Functions Documentation

function write_foreign

static void write_foreign(
    const char * name
)

write a small file that is not an entry

function put_aged

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

function process_args

static int process_args(
    int argc,
    char ** argv
)

read -V LOGLEVEL

function main

int main(
    int argc,
    char ** argv
)

function has

static int has(
    const N_DISK_CACHE * cache,
    const char * key
)

is the entry for key there

function expect_true

static void expect_true(
    const char * what,
    int ok
)

record one check

function exists

static int exists(
    const char * path
)

does a file exist

Attributes Documentation

variable log_level

static int log_level = 3;

variable failures

static int failures = 0;

Macros Documentation

define ex_mkdir

#define ex_mkdir(
    p
)
mkdir(p, 0700)

owner only elsewhere

define EX_DIR

#define EX_DIR "ex_disk_cache.dir"

the scratch directory, made and removed by this example

Source code



#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>

#include "nilorea/n_common.h"
#include "nilorea/n_log.h"
#include "nilorea/n_disk_cache.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);
        failures++;
    } 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";

    set_log_level(LOG_ERR);
    if (process_args(argc, argv) != 0) {
        fprintf(stderr, "Usage: %s [-V LOG_LEVEL]\n", argv[0]);
        return 2;
    }
    set_log_level(log_level);

    expect_true("a directory that is not there is refused", n_disk_cache_open(EX_DIR "/nope") == NULL);
    ex_mkdir(EX_DIR);
    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);
            n_disk_cache_close(&other);
        }
        expect_true("the second scratch directory is empty again", rmdir(EX_DIR "/other") == 0);
    }

    n_disk_cache_close(&cache);
    expect_true("close sets the handle to NULL", cache == NULL);
    n_disk_cache_close(&cache);
    n_disk_cache_close(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;
}