Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : /**
5 : * @file infrastructure/media_index.c
6 : * @brief media_id → local path index stored at ~/.cache/tg-cli/media.idx
7 : *
8 : * The file is a plain-text tab-delimited table:
9 : * <int64 media_id>\t<absolute path>\n
10 : *
11 : * Reads are O(n) sequential scans — the file is expected to hold at
12 : * most a few thousand entries in typical usage. Writes rewrite the
13 : * entire file after updating the relevant entry to avoid stale lines.
14 : */
15 :
16 : #include "media_index.h"
17 : #include "platform/path.h"
18 : #include "fs_util.h"
19 : #include "logger.h"
20 : #include "raii.h"
21 :
22 : #include <stdio.h>
23 : #include <stdlib.h>
24 : #include <string.h>
25 :
26 : #define MEDIA_IDX_MAX_LINE 2048
27 : #define MEDIA_IDX_MAX_ENTRIES 8192
28 :
29 : /** Returns a heap-allocated path to the index file. Caller must free. */
30 99 : static char *index_file_path(void) {
31 99 : const char *cache = platform_cache_dir();
32 99 : if (!cache) return NULL;
33 99 : char *p = NULL;
34 99 : if (asprintf(&p, "%s/tg-cli/media.idx", cache) == -1) return NULL;
35 99 : return p;
36 : }
37 :
38 : /** Ensure parent directory of the index file exists. */
39 35 : static int ensure_dir(const char *index_path) {
40 35 : RAII_STRING char *dir = strdup(index_path);
41 35 : if (!dir) return -1;
42 35 : char *slash = strrchr(dir, '/');
43 35 : if (slash) *slash = '\0';
44 35 : return fs_mkdir_p(dir, 0700);
45 : }
46 :
47 36 : int media_index_put(int64_t media_id, const char *local_path) {
48 36 : if (!local_path) return -1;
49 :
50 70 : RAII_STRING char *idx = index_file_path();
51 35 : if (!idx) return -1;
52 35 : if (ensure_dir(idx) != 0) {
53 0 : logger_log(LOG_ERROR, "media_index_put: cannot create index directory");
54 0 : return -1;
55 : }
56 :
57 : /* Read existing entries (if any). */
58 35 : char (*lines)[MEDIA_IDX_MAX_LINE] = NULL;
59 35 : int count = 0;
60 35 : int found = 0;
61 :
62 : {
63 70 : RAII_FILE FILE *rf = fopen(idx, "r");
64 35 : if (rf) {
65 7 : lines = calloc(MEDIA_IDX_MAX_ENTRIES,
66 : sizeof(*lines));
67 7 : if (!lines) return -1;
68 : char buf[MEDIA_IDX_MAX_LINE];
69 7 : while (count < MEDIA_IDX_MAX_ENTRIES
70 18 : && fgets(buf, sizeof(buf), rf)) {
71 : /* Strip trailing newline */
72 11 : size_t n = strlen(buf);
73 22 : while (n > 0 && (buf[n-1] == '\n' || buf[n-1] == '\r'))
74 11 : buf[--n] = '\0';
75 11 : if (n == 0) continue;
76 :
77 : /* Check if this line is for our media_id */
78 11 : int64_t id = 0;
79 11 : char *tab = strchr(buf, '\t');
80 11 : if (tab) {
81 11 : *tab = '\0';
82 11 : id = (int64_t)strtoll(buf, NULL, 10);
83 11 : *tab = '\t';
84 : }
85 11 : if (id == media_id) {
86 : /* Overwrite with new path */
87 4 : snprintf(lines[count], MEDIA_IDX_MAX_LINE,
88 : "%lld\t%s", (long long)media_id, local_path);
89 4 : found = 1;
90 : } else {
91 7 : snprintf(lines[count], MEDIA_IDX_MAX_LINE, "%s", buf);
92 : }
93 11 : count++;
94 : }
95 : } else {
96 28 : lines = calloc(MEDIA_IDX_MAX_ENTRIES, sizeof(*lines));
97 28 : if (!lines) return -1;
98 : }
99 : }
100 :
101 35 : if (!found) {
102 31 : if (count < MEDIA_IDX_MAX_ENTRIES) {
103 31 : snprintf(lines[count], MEDIA_IDX_MAX_LINE,
104 : "%lld\t%s", (long long)media_id, local_path);
105 31 : count++;
106 : } else {
107 0 : logger_log(LOG_WARN, "media_index_put: index full (%d entries)",
108 : count);
109 0 : free(lines);
110 0 : return -1;
111 : }
112 : }
113 :
114 : /* Write all entries back. */
115 70 : RAII_FILE FILE *wf = fopen(idx, "w");
116 35 : if (!wf) {
117 0 : logger_log(LOG_ERROR, "media_index_put: cannot open index for writing");
118 0 : free(lines);
119 0 : return -1;
120 : }
121 77 : for (int i = 0; i < count; i++) {
122 42 : fprintf(wf, "%s\n", lines[i]);
123 : }
124 35 : free(lines);
125 35 : return 0;
126 : }
127 :
128 65 : int media_index_get(int64_t media_id, char *out_path, size_t out_cap) {
129 65 : if (!out_path || out_cap == 0) return -1;
130 :
131 128 : RAII_STRING char *idx = index_file_path();
132 64 : if (!idx) return -1;
133 :
134 128 : RAII_FILE FILE *rf = fopen(idx, "r");
135 64 : if (!rf) return 0; /* file doesn't exist → not cached */
136 :
137 : char buf[MEDIA_IDX_MAX_LINE];
138 29 : while (fgets(buf, sizeof(buf), rf)) {
139 27 : char *tab = strchr(buf, '\t');
140 27 : if (!tab) continue;
141 27 : *tab = '\0';
142 27 : int64_t id = (int64_t)strtoll(buf, NULL, 10);
143 27 : if (id != media_id) { *tab = '\t'; continue; }
144 :
145 : /* Found — copy path (strip trailing newline). */
146 18 : char *path = tab + 1;
147 18 : size_t plen = strlen(path);
148 36 : while (plen > 0 && (path[plen-1] == '\n' || path[plen-1] == '\r'))
149 18 : path[--plen] = '\0';
150 :
151 18 : if (plen == 0) return 0; /* empty path → treat as not cached */
152 18 : snprintf(out_path, out_cap, "%s", path);
153 18 : return 1;
154 : }
155 2 : return 0;
156 : }
|