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 : /* upload.fileCdnRedirect#f18cda44 dc_id:int file_token:bytes
120 : * encryption_key:bytes encryption_iv:bytes
121 : * file_hashes:Vector<FileHash>
122 : * CDN DCs use a separate auth key and AES-256-CTR per-chunk
123 : * decryption — not yet implemented. */
124 2 : TlReader cr = tl_reader_init(resp, resp_len);
125 2 : tl_read_uint32(&cr); /* crc */
126 2 : int32_t cdn_dc = tl_reader_ok(&cr) ? tl_read_int32(&cr) : 0;
127 2 : logger_log(LOG_WARN,
128 : "media: CDN redirect to DC%d — CDN download not implemented",
129 : (int)cdn_dc);
130 2 : return -1;
131 : }
132 35 : if (top != CRC_upload_file) {
133 2 : logger_log(LOG_ERROR, "media: unexpected top 0x%08x", top);
134 2 : return -1;
135 : }
136 :
137 33 : TlReader r = tl_reader_init(resp, resp_len);
138 33 : tl_read_uint32(&r); /* top */
139 33 : tl_read_uint32(&r); /* storage.FileType crc */
140 33 : tl_read_int32(&r); /* mtime */
141 33 : size_t bytes_len = 0;
142 66 : RAII_STRING uint8_t *bytes = tl_read_bytes(&r, &bytes_len);
143 33 : if (!bytes && bytes_len != 0) return -1;
144 :
145 33 : if (bytes_len > 0) {
146 33 : if (fwrite(bytes, 1, bytes_len, fp) != bytes_len) {
147 0 : logger_log(LOG_ERROR, "media: fwrite failed");
148 0 : return -1;
149 : }
150 33 : offset += (int64_t)bytes_len;
151 : }
152 :
153 33 : if (bytes_len < CHUNK_SIZE) break;
154 : }
155 :
156 29 : logger_log(LOG_INFO, "media: saved %lld bytes to %s",
157 : (long long)offset, out_path);
158 29 : return 0;
159 : }
160 :
161 23 : int domain_download_photo(const ApiConfig *cfg,
162 : MtProtoSession *s, Transport *t,
163 : const MediaInfo *info,
164 : const char *out_path,
165 : int *wrong_dc) {
166 23 : if (wrong_dc) *wrong_dc = 0;
167 23 : if (!cfg || !s || !t || !info || !out_path) return -1;
168 22 : if (info->kind != MEDIA_PHOTO) {
169 3 : logger_log(LOG_ERROR, "media: download_photo needs MEDIA_PHOTO");
170 3 : return -1;
171 : }
172 19 : if (info->photo_id == 0 || info->access_hash == 0
173 18 : || info->file_reference_len == 0) {
174 1 : logger_log(LOG_ERROR, "media: missing id / access_hash / file_reference");
175 1 : return -1;
176 : }
177 :
178 : /* Cache hit: if the file is already indexed and still exists on disk,
179 : * copy/use the cached path rather than issuing upload.getFile again. */
180 : char cached[4096];
181 18 : if (media_index_get(info->photo_id, cached, sizeof(cached)) == 1) {
182 7 : FILE *fp = fopen(cached, "rb");
183 7 : if (fp) {
184 4 : fclose(fp);
185 : /* If the caller wants the same path that is already cached,
186 : * we're done. Otherwise copy to out_path so the caller can
187 : * rely on it being at the requested location. */
188 4 : if (strcmp(cached, out_path) != 0) {
189 4 : RAII_FILE FILE *src = fopen(cached, "rb");
190 4 : RAII_FILE FILE *dst = fopen(out_path, "wb");
191 2 : if (src && dst) {
192 : uint8_t buf[4096];
193 : size_t n;
194 4 : while ((n = fread(buf, 1, sizeof(buf), src)) > 0)
195 2 : fwrite(buf, 1, n, dst);
196 : }
197 : }
198 4 : logger_log(LOG_INFO, "media: cache hit for photo_id %lld → %s",
199 4 : (long long)info->photo_id, cached);
200 4 : return 0;
201 : }
202 : }
203 :
204 14 : int rc = download_loop(cfg, s, t, info, out_path, wrong_dc);
205 14 : if (rc == 0)
206 10 : media_index_put(info->photo_id, out_path);
207 14 : return rc;
208 : }
209 :
210 44 : int domain_download_document(const ApiConfig *cfg,
211 : MtProtoSession *s, Transport *t,
212 : const MediaInfo *info,
213 : const char *out_path,
214 : int *wrong_dc) {
215 44 : if (wrong_dc) *wrong_dc = 0;
216 44 : if (!cfg || !s || !t || !info || !out_path) return -1;
217 44 : if (info->kind != MEDIA_DOCUMENT) {
218 3 : logger_log(LOG_ERROR, "media: download_document needs MEDIA_DOCUMENT");
219 3 : return -1;
220 : }
221 41 : if (info->document_id == 0 || info->access_hash == 0
222 39 : || info->file_reference_len == 0) {
223 2 : logger_log(LOG_ERROR,
224 : "media: document missing id / access_hash / file_reference");
225 2 : return -1;
226 : }
227 :
228 : /* Cache hit: avoid re-downloading an already cached document. */
229 : char cached[4096];
230 39 : if (media_index_get(info->document_id, cached, sizeof(cached)) == 1) {
231 6 : FILE *fp = fopen(cached, "rb");
232 6 : if (fp) {
233 4 : fclose(fp);
234 4 : if (strcmp(cached, out_path) != 0) {
235 4 : RAII_FILE FILE *src = fopen(cached, "rb");
236 4 : RAII_FILE FILE *dst = fopen(out_path, "wb");
237 2 : if (src && dst) {
238 : uint8_t buf[4096];
239 : size_t n;
240 4 : while ((n = fread(buf, 1, sizeof(buf), src)) > 0)
241 2 : fwrite(buf, 1, n, dst);
242 : }
243 : }
244 4 : logger_log(LOG_INFO, "media: cache hit for document_id %lld → %s",
245 4 : (long long)info->document_id, cached);
246 4 : return 0;
247 : }
248 : }
249 :
250 35 : int rc = download_loop(cfg, s, t, info, out_path, wrong_dc);
251 35 : if (rc == 0)
252 19 : media_index_put(info->document_id, out_path);
253 35 : return rc;
254 : }
255 :
256 : /* Dispatch on MediaKind and call the right per-type entry point so the
257 : * cross-DC wrapper does not need to know the argument validation rules. */
258 12 : static int download_any(const ApiConfig *cfg,
259 : MtProtoSession *s, Transport *t,
260 : const MediaInfo *info,
261 : const char *out_path,
262 : int *wrong_dc) {
263 12 : if (info->kind == MEDIA_PHOTO)
264 2 : return domain_download_photo(cfg, s, t, info, out_path, wrong_dc);
265 10 : if (info->kind == MEDIA_DOCUMENT)
266 8 : return domain_download_document(cfg, s, t, info, out_path, wrong_dc);
267 2 : logger_log(LOG_ERROR, "media: download_any unsupported kind=%d", info->kind);
268 2 : return -1;
269 : }
270 :
271 15 : int domain_download_media_cross_dc(const ApiConfig *cfg,
272 : MtProtoSession *home_s, Transport *home_t,
273 : const MediaInfo *info,
274 : const char *out_path) {
275 15 : if (!cfg || !home_s || !home_t || !info || !out_path) return -1;
276 :
277 10 : int wrong_dc = 0;
278 10 : if (download_any(cfg, home_s, home_t, info, out_path, &wrong_dc) == 0) {
279 3 : return 0; /* home DC had the file */
280 : }
281 7 : if (wrong_dc <= 0) return -1; /* not a migration — hard fail */
282 :
283 4 : logger_log(LOG_INFO, "media: FILE_MIGRATE_%d, retrying on DC%d",
284 : wrong_dc, wrong_dc);
285 :
286 : DcSession xdc;
287 4 : if (dc_session_open(wrong_dc, &xdc) != 0) {
288 2 : logger_log(LOG_ERROR, "media: cannot open DC%d session", wrong_dc);
289 2 : return -1;
290 : }
291 :
292 : /* Freshly handshaked foreign sessions are not yet authorized.
293 : * dc_session_ensure_authorized() runs export/import; on a cached
294 : * session it is a no-op. */
295 2 : if (dc_session_ensure_authorized(&xdc, cfg, home_s, home_t) != 0) {
296 0 : logger_log(LOG_ERROR,
297 : "media: cross-DC authorization setup failed for DC%d",
298 : wrong_dc);
299 0 : dc_session_close(&xdc);
300 0 : return -1;
301 : }
302 :
303 2 : int dummy = 0;
304 2 : int rc = download_any(cfg, &xdc.session, &xdc.transport,
305 : info, out_path, &dummy);
306 2 : if (rc != 0) {
307 2 : logger_log(LOG_ERROR, "media: retry on DC%d still failed", wrong_dc);
308 : }
309 2 : dc_session_close(&xdc);
310 2 : return rc;
311 : }
|