From cd6ca2388fe25c56dfb2ea5fd13af2c767cbf572 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 01:50:02 +0100 Subject: [PATCH] Cascade attachment records with channel deletion before cleaning files --- src/chat/chat_attachment.c | 79 ++++++++++++++++++++++++++++---------- src/chat/chat_attachment.h | 6 ++- 2 files changed, 63 insertions(+), 22 deletions(-) diff --git a/src/chat/chat_attachment.c b/src/chat/chat_attachment.c index 83f9ddc6..425a0645 100644 --- a/src/chat/chat_attachment.c +++ b/src/chat/chat_attachment.c @@ -213,7 +213,7 @@ static int ak_store_file_result(struct ak_file* job) { } stage = "commit completion"; rc = sqlite3_exec(db, "COMMIT /* attachment completion */", NULL, NULL, NULL); if (rc != SQLITE_OK) goto fail; - DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "attachment keys: completion committed channel=%s encrypt=%d present=%d success=%d", + DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment keys: completion committed channel=%s encrypt=%d present=%d success=%d", job->channel, job->encrypt, present, success); return present; fail: @@ -241,8 +241,10 @@ static void ak_finish_file(struct ak_file* job) { memcpy(request.reply_to, job->reply, 32); chat_core_submit_media_message(inst, &request); } else if (!success) { - DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: worker failed channel=%s encrypt=%d error=%d worker=%d current=%d", - job->channel, job->encrypt, job->completion_error, job->error, valid); + if (valid) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: worker failed channel=%s encrypt=%d error=%d worker=%d", + job->channel, job->encrypt, job->completion_error, job->error); + else DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment keys: result discarded channel=%s encrypt=%d closing=%d published=%d", + job->channel, job->encrypt, CC(inst)->attachment_closing, job->published); if (job->encrypt) chat_event_submitted(inst, job->request, 0); } if (job->encrypt && remove(job->source) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: prepared source cleanup failed"); @@ -1165,6 +1167,14 @@ int chat_attachment_init(struct UTUN_INSTANCE* inst) { "incoming INTEGER NOT NULL,state INTEGER NOT NULL,created_at INTEGER NOT NULL,PRIMARY KEY(channel,message_id,peer));" "CREATE TABLE IF NOT EXISTS attachment_key_outbox(channel TEXT NOT NULL,message_id BLOB NOT NULL,peer INTEGER NOT NULL," "command INTEGER NOT NULL,retry_at INTEGER NOT NULL DEFAULT 0,PRIMARY KEY(channel,message_id,peer));" + "CREATE TRIGGER IF NOT EXISTS attachment_channel_delete AFTER DELETE ON channels BEGIN" + " DELETE FROM attachment_key_outbox WHERE channel=OLD.channel_id;" + " DELETE FROM attachment_key_requests WHERE channel=OLD.channel_id;" + " DELETE FROM attachment_keys WHERE channel=OLD.channel_id;" + " SELECT CASE WHEN EXISTS(SELECT 1 FROM attachment_key_outbox WHERE channel=OLD.channel_id)" + " OR EXISTS(SELECT 1 FROM attachment_key_requests WHERE channel=OLD.channel_id)" + " OR EXISTS(SELECT 1 FROM attachment_keys WHERE channel=OLD.channel_id)" + " THEN RAISE(ABORT,'attachment channel cleanup incomplete') END; END;" "UPDATE attachment_keys SET decrypting=0; UPDATE attachment_key_outbox SET retry_at=0;")) return -1; CC(inst)->attachment_closing = 0; CC(inst)->attachment_initialized = 1; @@ -1199,23 +1209,52 @@ void chat_attachment_destroy(struct UTUN_INSTANCE* inst) { ak_recover_preparations(inst); } -/* Удаление ключей отменяет completion активных workers; они удалят свои результаты. */ -void chat_attachment_delete_channel(struct UTUN_INSTANCE* inst, const char* channel) { - if (!CC(inst) || !CC(inst)->attachment_initialized) return; - sqlite3_stmt* st = ak_prepare(inst, "SELECT plain_path FROM attachment_keys WHERE channel=?"); - if (st) { - sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); int rc; - while ((rc = sqlite3_step(st)) == SQLITE_ROW) { - const char* path = (const char*)sqlite3_column_text(st, 0); - if (path && remove(path) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: plaintext removal failed path=%s", path); - } - if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: channel cleanup scan failed rc=%d", rc); - sqlite3_finalize(st); +struct ak_plain_cleanup { const char* root; unsigned removed; int failed; }; + +/* Только канонические UUID.file непосредственно в собственном каталоге канала; чужие имена и temp сохраняются. */ +static void ak_remove_plain(void* arg, const char* path, int64_t size) { + (void)size; + struct ak_plain_cleanup* cleanup = arg; size_t prefix = strlen(cleanup->root); + if (strncmp(path, cleanup->root, prefix) || path[prefix] != '/') return; + const char* name = path + prefix + 1; + if (strlen(name) != 37 || strspn(name, "0123456789abcdef") != 32 || strcmp(name + 32, ".file")) return; + if (!remove(path)) cleanup->removed++; + else if (errno != ENOENT) { + DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: plaintext removal failed path=%s: %s", path, strerror(errno)); + cleanup->failed = 1; } - const char* tables[] = { "attachment_key_outbox", "attachment_key_requests", "attachment_keys" }; - for (unsigned i = 0; i < 3; i++) { - char sql[128]; snprintf(sql, sizeof(sql), "DELETE FROM %s WHERE channel=?", tables[i]); st = ak_prepare(inst, sql); - if (st) { sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); ak_step(inst, st); } +} + +/* Записи удаляет каскадный trigger вместе с каналом; файловая очистка допустима только после его commit. */ +int chat_attachment_delete_channel(struct UTUN_INSTANCE* inst, const char* channel) { + if (!inst || !CC(inst) || !CC(inst)->attachment_initialized || !ak_channel(channel)) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: invalid channel file cleanup state/arguments"); return -1; } - DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment keys: channel keys and requests removed channel=%s", channel); + sqlite3* db = CC(inst)->db; sqlite3_stmt* st = NULL; + if (!sqlite3_get_autocommit(db)) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: channel file cleanup rejected inside transaction channel=%s", channel); return -1; + } + int rc = sqlite3_prepare_v2(db, "SELECT 1 FROM channels WHERE channel_id=?", -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + int final_rc = sqlite3_finalize(st); + if (rc != SQLITE_DONE || final_rc != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: channel still present or cleanup lookup failed channel=%s step=%d finalize=%d db=%s", + channel, rc, final_rc, sqlite3_errmsg(db)); return -1; + } + char base[MEDIA_BASE_MAX], root[MEDIA_BASE_MAX + 64]; + if (media_index_get_base(db, base)) return -1; + int length = snprintf(root, sizeof(root), "%s/decrypted/%s", base, channel); + if (length < 0 || (size_t)length >= sizeof(root)) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: channel plaintext directory too long channel=%s", channel); return -1; + } + struct file_io_info info; int status = file_io_inspect(root, 0, &info); + if (status == 1) return 0; + if (status < 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: cannot inspect plaintext directory path=%s", root); return -1; } + struct ak_plain_cleanup cleanup = {.root = root}; + int error = ma_dir_walk(root, ak_remove_plain, &cleanup); + if (error || cleanup.failed) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: channel plaintext cleanup incomplete channel=%s removed=%u", + channel, cleanup.removed); + else DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment keys: channel plaintext removed after commit channel=%s count=%u", channel, cleanup.removed); + return error || cleanup.failed ? -1 : 0; } diff --git a/src/chat/chat_attachment.h b/src/chat/chat_attachment.h index fff4ffa0..84af2ec7 100644 --- a/src/chat/chat_attachment.h +++ b/src/chat/chat_attachment.h @@ -89,8 +89,10 @@ void chat_attachment_decision_trampoline(void* arg); // ── Локальное удаление канала ── -// Канал удаляется локально: отменить очереди, стереть ключи и частные копии. -void chat_attachment_delete_channel(struct UTUN_INSTANCE* inst, const char* channel); +/* После commit удаления канала убрать собственные UUID.file; 0/-1, при I/O-ошибке можно повторить. + * Ключи/запросы/outbox удаляет SQLite-trigger атомарно с channels; SQL-отказ сохраняет все записи. + * Внешняя транзакция или существующий канал запрещают файловую очистку; завершения workers видят удаление через БД. */ +int chat_attachment_delete_channel(struct UTUN_INSTANCE* inst, const char* channel); #ifdef __cplusplus }