Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : #include "cache_store.h"
5 : #include "fs_util.h"
6 : #include "platform/path.h"
7 : #include "raii.h"
8 : #include "logger.h"
9 : #include <stdio.h>
10 : #include <stdlib.h>
11 : #include <string.h>
12 : #include <dirent.h>
13 :
14 : /** @brief Returns a heap-allocated path for a cache entry. Caller must free. */
15 26 : char *cache_path(const char *category, const char *key) {
16 26 : const char *cache_base = platform_cache_dir();
17 26 : if (!cache_base) return NULL;
18 26 : char *path = NULL;
19 26 : if (asprintf(&path, "%s/tg-cli/%s/%s",
20 : cache_base, category, key) == -1)
21 0 : return NULL;
22 26 : return path;
23 : }
24 :
25 12 : int cache_exists(const char *category, const char *key) {
26 24 : RAII_STRING char *path = cache_path(category, key);
27 12 : if (!path) return 0;
28 12 : RAII_FILE FILE *fp = fopen(path, "r");
29 12 : return fp != NULL;
30 : }
31 :
32 8 : int cache_save(const char *category, const char *key,
33 : const char *content, size_t len) {
34 8 : const char *cache_base = platform_cache_dir();
35 8 : if (!cache_base) return -1;
36 :
37 : /* Build directory path: ~/.cache/tg-cli/<category>/
38 : * key may contain slashes (e.g. "chat_id/msg_id") — create parent dirs. */
39 16 : RAII_STRING char *full = cache_path(category, key);
40 8 : if (!full) return -1;
41 :
42 : /* Find last '/' to isolate directory portion */
43 8 : RAII_STRING char *dir = strdup(full);
44 8 : if (!dir) return -1;
45 8 : char *slash = strrchr(dir, '/');
46 8 : if (slash) *slash = '\0';
47 :
48 8 : if (fs_mkdir_p(dir, 0700) != 0) {
49 1 : logger_log(LOG_ERROR, "Failed to create cache directory %s", dir);
50 1 : return -1;
51 : }
52 :
53 14 : RAII_FILE FILE *fp = fopen(full, "w");
54 7 : if (!fp) {
55 0 : logger_log(LOG_ERROR, "Failed to open cache file for writing: %s", full);
56 0 : return -1;
57 : }
58 :
59 7 : if (fwrite(content, 1, len, fp) != len) {
60 0 : logger_log(LOG_ERROR, "Failed to write cache file: %s", full);
61 0 : return -1;
62 : }
63 :
64 7 : logger_log(LOG_DEBUG, "Cached %s/%s (%zu bytes)", category, key, len);
65 7 : return 0;
66 : }
67 :
68 : /* ── Shared file I/O helper ──────────────────────────────────────────── */
69 :
70 4 : static char *load_file(const char *path) {
71 8 : RAII_FILE FILE *fp = fopen(path, "r");
72 4 : if (!fp) return NULL;
73 2 : if (fseek(fp, 0, SEEK_END) != 0) return NULL;
74 2 : long size = ftell(fp);
75 2 : if (size <= 0) return NULL;
76 2 : rewind(fp);
77 2 : char *buf = malloc((size_t)size + 1);
78 2 : if (!buf) return NULL;
79 2 : if ((long)fread(buf, 1, (size_t)size, fp) != size) { free(buf); return NULL; }
80 2 : buf[size] = '\0';
81 2 : return buf;
82 : }
83 :
84 4 : char *cache_load(const char *category, const char *key) {
85 8 : RAII_STRING char *path = cache_path(category, key);
86 4 : if (!path) {
87 0 : logger_log(LOG_ERROR, "cache_load: failed to determine cache path for %s/%s",
88 : category ? category : "(null)", key ? key : "(null)");
89 0 : return NULL;
90 : }
91 4 : char *data = load_file(path);
92 4 : if (!data) {
93 2 : logger_log(LOG_DEBUG, "cache_load: no data at %s", path);
94 : }
95 4 : return data;
96 : }
97 :
98 : /* ── Stale entry eviction ────────────────────────────────────────────── */
99 :
100 5 : static int cmp_str_evict(const void *a, const void *b) {
101 5 : return strcmp(*(const char **)a, *(const char **)b);
102 : }
103 :
104 3 : void cache_evict_stale(const char *category,
105 : const char **keep_keys, int keep_count) {
106 3 : const char *cache_base = platform_cache_dir();
107 4 : if (!cache_base) return;
108 :
109 3 : RAII_STRING char *dir = NULL;
110 3 : if (asprintf(&dir, "%s/tg-cli/%s", cache_base, category) == -1)
111 0 : return;
112 :
113 : /* Sort a local copy of keys for bsearch */
114 3 : const char **sorted = malloc((size_t)keep_count * sizeof(char *));
115 3 : if (!sorted) return;
116 3 : memcpy(sorted, keep_keys, (size_t)keep_count * sizeof(char *));
117 3 : qsort(sorted, (size_t)keep_count, sizeof(char *), cmp_str_evict);
118 :
119 6 : RAII_DIR DIR *d = opendir(dir);
120 3 : if (!d) { free(sorted); return; }
121 :
122 : struct dirent *ent;
123 11 : while ((ent = readdir(d)) != NULL) {
124 9 : const char *name = ent->d_name;
125 9 : if (name[0] == '.') continue; /* skip . and .. */
126 5 : if (!bsearch(&name, sorted, (size_t)keep_count,
127 : sizeof(char *), cmp_str_evict)) {
128 4 : RAII_STRING char *path = NULL;
129 4 : if (asprintf(&path, "%s/%s", dir, name) != -1) {
130 4 : remove(path);
131 4 : logger_log(LOG_DEBUG, "Evicted stale cache entry: %s/%s",
132 : category, name);
133 : }
134 : }
135 : }
136 2 : free(sorted);
137 : }
|