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 37 : static char *index_file_path(void) {
31 37 : const char *cache = platform_cache_dir();
32 37 : if (!cache) return NULL;
33 37 : char *p = NULL;
34 37 : if (asprintf(&p, "%s/tg-cli/media.idx", cache) == -1) return NULL;
35 37 : return p;
36 : }
37 :
38 : /** Ensure parent directory of the index file exists. */
39 12 : static int ensure_dir(const char *index_path) {
40 12 : RAII_STRING char *dir = strdup(index_path);
41 12 : if (!dir) return -1;
42 12 : char *slash = strrchr(dir, '/');
43 12 : if (slash) *slash = '\0';
44 12 : return fs_mkdir_p(dir, 0700);
45 : }
46 :
47 12 : int media_index_put(int64_t media_id, const char *local_path) {
48 12 : if (!local_path) return -1;
49 :
50 24 : RAII_STRING char *idx = index_file_path();
51 12 : if (!idx) return -1;
52 12 : 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 12 : char (*lines)[MEDIA_IDX_MAX_LINE] = NULL;
59 12 : int count = 0;
60 12 : int found = 0;
61 :
62 : {
63 24 : RAII_FILE FILE *rf = fopen(idx, "r");
64 12 : if (rf) {
65 0 : lines = calloc(MEDIA_IDX_MAX_ENTRIES,
66 : sizeof(*lines));
67 0 : if (!lines) return -1;
68 : char buf[MEDIA_IDX_MAX_LINE];
69 0 : while (count < MEDIA_IDX_MAX_ENTRIES
70 0 : && fgets(buf, sizeof(buf), rf)) {
71 : /* Strip trailing newline */
72 0 : size_t n = strlen(buf);
73 0 : while (n > 0 && (buf[n-1] == '\n' || buf[n-1] == '\r'))
74 0 : buf[--n] = '\0';
75 0 : if (n == 0) continue;
76 :
77 : /* Check if this line is for our media_id */
78 0 : int64_t id = 0;
79 0 : char *tab = strchr(buf, '\t');
80 0 : if (tab) {
81 0 : *tab = '\0';
82 0 : id = (int64_t)strtoll(buf, NULL, 10);
83 0 : *tab = '\t';
84 : }
85 0 : if (id == media_id) {
86 : /* Overwrite with new path */
87 0 : snprintf(lines[count], MEDIA_IDX_MAX_LINE,
88 : "%lld\t%s", (long long)media_id, local_path);
89 0 : found = 1;
90 : } else {
91 0 : snprintf(lines[count], MEDIA_IDX_MAX_LINE, "%s", buf);
92 : }
93 0 : count++;
94 : }
95 : } else {
96 12 : lines = calloc(MEDIA_IDX_MAX_ENTRIES, sizeof(*lines));
97 12 : if (!lines) return -1;
98 : }
99 : }
100 :
101 12 : if (!found) {
102 12 : if (count < MEDIA_IDX_MAX_ENTRIES) {
103 12 : snprintf(lines[count], MEDIA_IDX_MAX_LINE,
104 : "%lld\t%s", (long long)media_id, local_path);
105 12 : 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 24 : RAII_FILE FILE *wf = fopen(idx, "w");
116 12 : 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 24 : for (int i = 0; i < count; i++) {
122 12 : fprintf(wf, "%s\n", lines[i]);
123 : }
124 12 : free(lines);
125 12 : return 0;
126 : }
127 :
128 25 : int media_index_get(int64_t media_id, char *out_path, size_t out_cap) {
129 25 : if (!out_path || out_cap == 0) return -1;
130 :
131 50 : RAII_STRING char *idx = index_file_path();
132 25 : if (!idx) return -1;
133 :
134 50 : RAII_FILE FILE *rf = fopen(idx, "r");
135 25 : if (!rf) return 0; /* file doesn't exist → not cached */
136 :
137 : char buf[MEDIA_IDX_MAX_LINE];
138 4 : while (fgets(buf, sizeof(buf), rf)) {
139 4 : char *tab = strchr(buf, '\t');
140 4 : if (!tab) continue;
141 4 : *tab = '\0';
142 4 : int64_t id = (int64_t)strtoll(buf, NULL, 10);
143 4 : if (id != media_id) { *tab = '\t'; continue; }
144 :
145 : /* Found — copy path (strip trailing newline). */
146 4 : char *path = tab + 1;
147 4 : size_t plen = strlen(path);
148 8 : while (plen > 0 && (path[plen-1] == '\n' || path[plen-1] == '\r'))
149 4 : path[--plen] = '\0';
150 :
151 4 : if (plen == 0) return 0; /* empty path → treat as not cached */
152 4 : snprintf(out_path, out_cap, "%s", path);
153 4 : return 1;
154 : }
155 0 : return 0;
156 : }
|