Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : /**
5 : * @file domain/read/media.c
6 : * @brief upload.getFile chunked download (P6-01).
7 : */
8 :
9 : #include "domain/read/media.h"
10 :
11 : #include "app/dc_session.h"
12 : #include "tl_serial.h"
13 : #include "tl_registry.h"
14 : #include "mtproto_rpc.h"
15 : #include "media_index.h"
16 : #include "logger.h"
17 : #include "raii.h"
18 :
19 : #include <stdio.h>
20 : #include <stdlib.h>
21 : #include <string.h>
22 :
23 : #define CRC_upload_getFile 0xbe5335beu
24 : #define CRC_inputPhotoFileLocation 0x40181ffeu
25 : #define CRC_inputDocumentFileLocation 0xbad07584u
26 : #define CRC_upload_file 0x096a18d5u
27 : #define CRC_upload_fileCdnRedirect 0xf18cda44u
28 :
29 : /** Default chunk size — must be a multiple of 4 KB per Telegram spec. */
30 : #define CHUNK_SIZE (128 * 1024)
31 :
32 : /* Build a upload.getFile request whose InputFileLocation is derived
33 : * from @p info.kind. Photos use inputPhotoFileLocation with the
34 : * largest thumb_size; documents use inputDocumentFileLocation with
35 : * an empty thumb_size to fetch the full file. */
36 51 : static int build_getfile_request(const MediaInfo *info,
37 : int64_t offset, int32_t limit,
38 : uint8_t *out, size_t cap, size_t *out_len) {
39 : TlWriter w;
40 51 : tl_writer_init(&w);
41 :
42 51 : tl_write_uint32(&w, CRC_upload_getFile);
43 51 : tl_write_uint32(&w, 0); /* flags */
44 51 : if (info->kind == MEDIA_DOCUMENT) {
45 : /* inputDocumentFileLocation#bad07584 id:long access_hash:long
46 : * file_reference:bytes thumb_size:string */
47 35 : tl_write_uint32(&w, CRC_inputDocumentFileLocation);
48 35 : tl_write_int64 (&w, info->document_id);
49 35 : tl_write_int64 (&w, info->access_hash);
50 35 : tl_write_bytes (&w, info->file_reference,
51 35 : info->file_reference_len);
52 35 : tl_write_string(&w, ""); /* full file */
53 : } else {
54 : /* inputPhotoFileLocation#40181ffe id:long access_hash:long
55 : * file_reference:bytes thumb_size:string */
56 16 : tl_write_uint32(&w, CRC_inputPhotoFileLocation);
57 16 : tl_write_int64 (&w, info->photo_id);
58 16 : tl_write_int64 (&w, info->access_hash);
59 16 : tl_write_bytes (&w, info->file_reference,
60 16 : info->file_reference_len);
61 16 : tl_write_string(&w, info->thumb_type[0] ? info->thumb_type : "y");
62 : }
63 51 : tl_write_int64 (&w, offset);
64 51 : tl_write_int32 (&w, limit);
65 :
66 51 : int rc = -1;
67 51 : if (w.len <= cap) {
68 51 : memcpy(out, w.data, w.len);
69 51 : *out_len = w.len;
70 51 : rc = 0;
71 : }
72 51 : tl_writer_free(&w);
73 51 : return rc;
74 : }
75 :
76 : /* Shared chunked download loop. Caller has already validated @p info. */
77 49 : static int download_loop(const ApiConfig *cfg,
78 : MtProtoSession *s, Transport *t,
79 : const MediaInfo *info,
80 : const char *out_path,
81 : int *wrong_dc) {
82 98 : RAII_FILE FILE *fp = fopen(out_path, "wb");
83 49 : if (!fp) {
84 2 : logger_log(LOG_ERROR, "media: cannot open %s for writing", out_path);
85 2 : return -1;
86 : }
87 :
88 : uint8_t query[1024];
89 47 : RAII_STRING uint8_t *resp = (uint8_t *)malloc(CHUNK_SIZE + 4096);
90 47 : if (!resp) return -1;
91 :
92 47 : int64_t offset = 0;
93 4 : for (;;) {
94 51 : size_t qlen = 0;
95 51 : if (build_getfile_request(info, offset, CHUNK_SIZE,
96 : query, sizeof(query), &qlen) != 0)
97 18 : return -1;
98 :
99 51 : size_t resp_len = 0;
100 51 : if (api_call(cfg, s, t, query, qlen,
101 : resp, CHUNK_SIZE + 4096, &resp_len) != 0) {
102 2 : logger_log(LOG_ERROR, "media: api_call failed at offset %lld",
103 : (long long)offset);
104 2 : return -1;
105 : }
106 49 : if (resp_len < 4) return -1;
107 :
108 : uint32_t top;
109 49 : memcpy(&top, resp, 4);
110 49 : if (top == TL_rpc_error) {
111 : RpcError err;
112 12 : rpc_parse_error(resp, resp_len, &err);
113 12 : if (err.migrate_dc > 0 && wrong_dc) *wrong_dc = err.migrate_dc;
114 12 : logger_log(LOG_ERROR, "media: RPC error %d: %s (migrate=%d)",
115 : err.error_code, err.error_msg, err.migrate_dc);
116 12 : return -1;
117 : }
118 37 : if (top == CRC_upload_fileCdnRedirect) {
119 2 : logger_log(LOG_ERROR, "media: CDN redirect not supported");
120 2 : return -1;
121 : }
122 35 : if (top != CRC_upload_file) {
123 2 : logger_log(LOG_ERROR, "media: unexpected top 0x%08x", top);
124 2 : return -1;
125 : }
126 :
127 33 : TlReader r = tl_reader_init(resp, resp_len);
128 33 : tl_read_uint32(&r); /* top */
129 33 : tl_read_uint32(&r); /* storage.FileType crc */
130 33 : tl_read_int32(&r); /* mtime */
131 33 : size_t bytes_len = 0;
132 66 : RAII_STRING uint8_t *bytes = tl_read_bytes(&r, &bytes_len);
133 33 : if (!bytes && bytes_len != 0) return -1;
134 :
135 33 : if (bytes_len > 0) {
136 33 : if (fwrite(bytes, 1, bytes_len, fp) != bytes_len) {
137 0 : logger_log(LOG_ERROR, "media: fwrite failed");
138 0 : return -1;
139 : }
140 33 : offset += (int64_t)bytes_len;
141 : }
142 :
143 33 : if (bytes_len < CHUNK_SIZE) break;
144 : }
145 :
146 29 : logger_log(LOG_INFO, "media: saved %lld bytes to %s",
147 : (long long)offset, out_path);
148 29 : return 0;
149 : }
150 :
151 23 : int domain_download_photo(const ApiConfig *cfg,
152 : MtProtoSession *s, Transport *t,
153 : const MediaInfo *info,
154 : const char *out_path,
155 : int *wrong_dc) {
156 23 : if (wrong_dc) *wrong_dc = 0;
157 23 : if (!cfg || !s || !t || !info || !out_path) return -1;
158 22 : if (info->kind != MEDIA_PHOTO) {
159 3 : logger_log(LOG_ERROR, "media: download_photo needs MEDIA_PHOTO");
160 3 : return -1;
161 : }
162 19 : if (info->photo_id == 0 || info->access_hash == 0
163 18 : || info->file_reference_len == 0) {
164 1 : logger_log(LOG_ERROR, "media: missing id / access_hash / file_reference");
165 1 : return -1;
166 : }
167 :
168 : /* Cache hit: if the file is already indexed and still exists on disk,
169 : * copy/use the cached path rather than issuing upload.getFile again. */
170 : char cached[4096];
171 18 : if (media_index_get(info->photo_id, cached, sizeof(cached)) == 1) {
172 7 : FILE *fp = fopen(cached, "rb");
173 7 : if (fp) {
174 4 : fclose(fp);
175 : /* If the caller wants the same path that is already cached,
176 : * we're done. Otherwise copy to out_path so the caller can
177 : * rely on it being at the requested location. */
178 4 : if (strcmp(cached, out_path) != 0) {
179 4 : RAII_FILE FILE *src = fopen(cached, "rb");
180 4 : RAII_FILE FILE *dst = fopen(out_path, "wb");
181 2 : if (src && dst) {
182 : uint8_t buf[4096];
183 : size_t n;
184 4 : while ((n = fread(buf, 1, sizeof(buf), src)) > 0)
185 2 : fwrite(buf, 1, n, dst);
186 : }
187 : }
188 4 : logger_log(LOG_INFO, "media: cache hit for photo_id %lld → %s",
189 4 : (long long)info->photo_id, cached);
190 4 : return 0;
191 : }
192 : }
193 :
194 14 : int rc = download_loop(cfg, s, t, info, out_path, wrong_dc);
195 14 : if (rc == 0)
196 10 : media_index_put(info->photo_id, out_path);
197 14 : return rc;
198 : }
199 :
200 44 : int domain_download_document(const ApiConfig *cfg,
201 : MtProtoSession *s, Transport *t,
202 : const MediaInfo *info,
203 : const char *out_path,
204 : int *wrong_dc) {
205 44 : if (wrong_dc) *wrong_dc = 0;
206 44 : if (!cfg || !s || !t || !info || !out_path) return -1;
207 44 : if (info->kind != MEDIA_DOCUMENT) {
208 3 : logger_log(LOG_ERROR, "media: download_document needs MEDIA_DOCUMENT");
209 3 : return -1;
210 : }
211 41 : if (info->document_id == 0 || info->access_hash == 0
212 39 : || info->file_reference_len == 0) {
213 2 : logger_log(LOG_ERROR,
214 : "media: document missing id / access_hash / file_reference");
215 2 : return -1;
216 : }
217 :
218 : /* Cache hit: avoid re-downloading an already cached document. */
219 : char cached[4096];
220 39 : if (media_index_get(info->document_id, cached, sizeof(cached)) == 1) {
221 6 : FILE *fp = fopen(cached, "rb");
222 6 : if (fp) {
223 4 : fclose(fp);
224 4 : if (strcmp(cached, out_path) != 0) {
225 4 : RAII_FILE FILE *src = fopen(cached, "rb");
226 4 : RAII_FILE FILE *dst = fopen(out_path, "wb");
227 2 : if (src && dst) {
228 : uint8_t buf[4096];
229 : size_t n;
230 4 : while ((n = fread(buf, 1, sizeof(buf), src)) > 0)
231 2 : fwrite(buf, 1, n, dst);
232 : }
233 : }
234 4 : logger_log(LOG_INFO, "media: cache hit for document_id %lld → %s",
235 4 : (long long)info->document_id, cached);
236 4 : return 0;
237 : }
238 : }
239 :
240 35 : int rc = download_loop(cfg, s, t, info, out_path, wrong_dc);
241 35 : if (rc == 0)
242 19 : media_index_put(info->document_id, out_path);
243 35 : return rc;
244 : }
245 :
246 : /* Dispatch on MediaKind and call the right per-type entry point so the
247 : * cross-DC wrapper does not need to know the argument validation rules. */
248 12 : static int download_any(const ApiConfig *cfg,
249 : MtProtoSession *s, Transport *t,
250 : const MediaInfo *info,
251 : const char *out_path,
252 : int *wrong_dc) {
253 12 : if (info->kind == MEDIA_PHOTO)
254 2 : return domain_download_photo(cfg, s, t, info, out_path, wrong_dc);
255 10 : if (info->kind == MEDIA_DOCUMENT)
256 8 : return domain_download_document(cfg, s, t, info, out_path, wrong_dc);
257 2 : logger_log(LOG_ERROR, "media: download_any unsupported kind=%d", info->kind);
258 2 : return -1;
259 : }
260 :
261 15 : int domain_download_media_cross_dc(const ApiConfig *cfg,
262 : MtProtoSession *home_s, Transport *home_t,
263 : const MediaInfo *info,
264 : const char *out_path) {
265 15 : if (!cfg || !home_s || !home_t || !info || !out_path) return -1;
266 :
267 10 : int wrong_dc = 0;
268 10 : if (download_any(cfg, home_s, home_t, info, out_path, &wrong_dc) == 0) {
269 3 : return 0; /* home DC had the file */
270 : }
271 7 : if (wrong_dc <= 0) return -1; /* not a migration — hard fail */
272 :
273 4 : logger_log(LOG_INFO, "media: FILE_MIGRATE_%d, retrying on DC%d",
274 : wrong_dc, wrong_dc);
275 :
276 : DcSession xdc;
277 4 : if (dc_session_open(wrong_dc, &xdc) != 0) {
278 2 : logger_log(LOG_ERROR, "media: cannot open DC%d session", wrong_dc);
279 2 : return -1;
280 : }
281 :
282 : /* Freshly handshaked foreign sessions are not yet authorized.
283 : * dc_session_ensure_authorized() runs export/import; on a cached
284 : * session it is a no-op. */
285 2 : if (dc_session_ensure_authorized(&xdc, cfg, home_s, home_t) != 0) {
286 0 : logger_log(LOG_ERROR,
287 : "media: cross-DC authorization setup failed for DC%d",
288 : wrong_dc);
289 0 : dc_session_close(&xdc);
290 0 : return -1;
291 : }
292 :
293 2 : int dummy = 0;
294 2 : int rc = download_any(cfg, &xdc.session, &xdc.transport,
295 : info, out_path, &dummy);
296 2 : if (rc != 0) {
297 2 : logger_log(LOG_ERROR, "media: retry on DC%d still failed", wrong_dc);
298 : }
299 2 : dc_session_close(&xdc);
300 2 : return rc;
301 : }
|