From f123a1bdebdad7e0c000dc582e5d07048e045cfe Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 22 Oct 2026 15:06:00 +0200 Subject: [PATCH] Document preparation ownership and log exact media index failures --- src/media_delivery/media_index.c | 34 ++++++++++++++++++++++---------- src/media_delivery/media_index.h | 14 ++++++++----- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/media_delivery/media_index.c b/src/media_delivery/media_index.c index 54833080..495471c7 100644 --- a/src/media_delivery/media_index.c +++ b/src/media_delivery/media_index.c @@ -67,7 +67,8 @@ static int mi_location_valid(const char* location) { int media_index_path(const char* base, const char* location, char path[MEDIA_PATH_MAX]) { char clean[MEDIA_BASE_MAX]; - if (!path || mi_clean_path(base, clean, sizeof(clean)) || mi_location_valid(location)) return -1; + if (!path) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index path without output"); return -1; } + if (mi_clean_path(base, clean, sizeof(clean)) || mi_location_valid(location)) return -1; int size = snprintf(path, MEDIA_PATH_MAX, "%s%s%s", clean, clean[strlen(clean) - 1] == '/' ? "" : "/", location); if (size >= MEDIA_PATH_MAX || size < 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index joined path too long base=%s location=%s", clean, location); return -1; @@ -77,7 +78,8 @@ int media_index_path(const char* base, const char* location, char path[MEDIA_PAT int media_index_location(const char* path, const char* base, char location[MEDIA_PATH_MAX]) { char full[MEDIA_PATH_MAX], root[MEDIA_BASE_MAX]; - if (!location || mi_clean_path(path, full, sizeof(full)) || mi_clean_path(base, root, sizeof(root))) return -1; + if (!location) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index location without output"); return -1; } + if (mi_clean_path(path, full, sizeof(full)) || mi_clean_path(base, root, sizeof(root))) return -1; size_t length = strlen(root); const char* relative = NULL; if (!strcmp(root, ".") && full[0] != '/' && !strchr(full, ':')) relative = full; @@ -113,16 +115,23 @@ int media_index_get_base(sqlite3* db, char base[MEDIA_BASE_MAX]) { int media_index_set_base(sqlite3* db, const char* base) { char clean[MEDIA_BASE_MAX]; sqlite3_stmt* st = NULL; - if (!db || mi_clean_path(base, clean, sizeof(clean))) return -1; + if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index set base without DB"); return -1; } + if (mi_clean_path(base, clean, sizeof(clean))) return -1; if (mi_exec(db, "CREATE TABLE IF NOT EXISTS ui_state(key TEXT PRIMARY KEY,value TEXT)")) return -1; int rc = sqlite3_prepare_v2(db, "INSERT INTO ui_state(key,value) VALUES('media_base',?) ON CONFLICT(key) DO NOTHING", -1, &st, NULL); if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, clean, -1, SQLITE_STATIC); if (rc == SQLITE_OK) rc = sqlite3_step(st); int finish = st ? sqlite3_finalize(st) : SQLITE_OK; + if (rc != SQLITE_DONE || finish != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index set base SQL failure requested=%s rc=%d: %s", clean, rc, sqlite3_errmsg(db)); + return -1; + } char stored[MEDIA_BASE_MAX]; - if (rc == SQLITE_DONE && finish == SQLITE_OK && !media_index_get_base(db, stored) && !strcmp(stored, clean)) return 0; - DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index conflicting/invalid media_base requested=%s rc=%d: %s", clean, rc, sqlite3_errmsg(db)); - return -1; + if (media_index_get_base(db, stored)) return -1; + if (strcmp(stored, clean)) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index base change rejected requested=%s stored=%s", clean, stored); return -1; + } + return 0; } int media_index_track_preparation(sqlite3* db, const char* chat_id, const char* path, const char* base) { @@ -182,8 +191,9 @@ static int mi_insert(sqlite3* db, int changed = sqlite3_changes(db), finish = sqlite3_finalize(stmt); if (rc != SQLITE_DONE || finish != SQLITE_OK || changed != 1) { - DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index insert/conflict chat=%s chunk=%d rc=%d changed=%d: %s", - chat_id, chunk, rc, changed, sqlite3_errmsg(db)); + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index %s chat=%s block=%02x%02x%02x%02x chunk=%d path=%s rc=%d: %s", + rc == SQLITE_DONE && finish == SQLITE_OK && !changed ? "conflicting block metadata" : "insert failed", + chat_id, block_id[0], block_id[1], block_id[2], block_id[3], chunk, location, rc, sqlite3_errmsg(db)); return -1; } return 0; @@ -341,7 +351,9 @@ static int mi_result_valid(const struct media_index_result* result) { if (result && result->file_size > 0 && result->block_size > 0 && result->num_blocks > 0 && result->block_ids && (uint64_t)result->num_blocks <= UINT32_MAX / 64 && (result->file_size - 1) / result->block_size + 1 == result->num_blocks) return 0; - DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index invalid block geometry/arrays"); return -1; + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index invalid geometry bytes=%lld block_size=%lld count=%d ids=%p", + (long long)(result ? result->file_size : 0), (long long)(result ? result->block_size : 0), + result ? result->num_blocks : 0, (void*)(result ? result->block_ids : NULL)); return -1; } /* Общий атомарный путь для автора и держателя скачанного файла. */ @@ -369,7 +381,9 @@ static int mi_register_file(sqlite3* db, const struct media_index_result* result int32_t chunk = n; memcpy(message + used, &chunk, 4); used += 4; memcpy(message + used, &offset, 8); used += 8; memcpy(message + used, &node_id, 8); used += 8; - if (ma_sign_block(key, message, used, signature)) { failed = 1; break; } + if (ma_sign_block(key, message, used, signature)) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index signing failed chat=%s chunk=%d", chat_id, n); failed = 1; break; + } } if (mi_insert(db, result->media_id, result->block_ids + (size_t)n * 16, result->content_hash, chat_id, location, node_id, signature, ts, result->file_size, size, n, offset)) { failed = 1; break; } diff --git a/src/media_delivery/media_index.h b/src/media_delivery/media_index.h index 5722811a..46a035b6 100644 --- a/src/media_delivery/media_index.h +++ b/src/media_delivery/media_index.h @@ -2,6 +2,8 @@ * Порядок: init → register_async → callback с UUID, хешем и подписями блоков; затем можно анонсировать файл. * Register_async копирует/хеширует/подписывает файл в worker, затем commit и callback выполняются в uasync. * db/ma/ua заимствованы и должны жить до завершения; доступ к БД выполняется в потоке ядра. + * Назначения учитываются в media_preparations до запуска worker; неуспешная подготовка оставляет их для удаления канала. + * Перед delete_channel вызывающий должен дождаться всех своих workers/completions; функция их сама не отменяет. * Результат callback заимствован только до возврата. result_free освобождает block_ids/block_sigs, но не саму структуру. */ #ifndef MEDIA_INDEX_H #define MEDIA_INDEX_H @@ -36,8 +38,8 @@ int media_index_get_base(sqlite3* db, char base[MEDIA_BASE_MAX]); int media_index_track_preparation(sqlite3* db, const char* chat_id, const char* path, const char* base); struct media_index_result { - uint8_t media_id[16]; - uint8_t content_hash[32]; + uint8_t media_id[16]; // случайный идентификатор целого файла + uint8_t content_hash[32]; // SHA256 полного содержимого int64_t file_size; // полный размер файла, байты int64_t block_size; // размер обычного блока, байты; последний может быть короче int num_blocks; // число блоков и элементов block_ids/block_sigs @@ -47,7 +49,7 @@ struct media_index_result { // ── Инициализация таблицы ── -// Создать таблицу media_files и её индексы, сохранив существующие записи; 0/-1. +// Создать media_files, media_preparations и индексы, сохранив записи; ошибка любого DDL даёт -1, успех 0. int media_index_init(sqlite3* db); // ── Регистрация файлов и блоков ── @@ -63,8 +65,10 @@ int media_index_commit( uint64_t node_id, const uint8_t* ed25519_privkey, const char* chat_id, const char* dest_path, const char* media_base); -/* Зарегистрировать скачанный блок в media_files (location — путь к собранному файлу), - * чтобы узел мог отдавать его как держатель. node_sign = нули (при отдаче не проверяется). */ +/* Зарегистрировать один скачанный блок: location — канонический путь относительно общей базы; 0/-1. + * file_size/chunk_size >0, chunk/offset >=0, блок целиком помещается в файл. + * node_sign новой записи — нули (при отдаче не проверяется); существующая подпись при совпадающем повторе сохраняется. + * Для целого файла использовать register_downloaded_file; одиночные вызовы не образуют общую транзакцию. */ int media_index_register_downloaded(sqlite3* db, const uint8_t* media_id, const uint8_t* block_id, const uint8_t* content_hash, const char* chat_id, const char* location, uint64_t node_id,