Cascade attachment records with channel deletion before cleaning files

This commit is contained in:
evgeny committed 2026-10-27 01:50:02 +01:00
1 parent 34e6fecbd0
commit cd6ca2388f
2 files changed
+63 -22

No files matched your search

+59 -20
View File
@@ -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;
}
+4 -2
View File
@@ -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
}