Cascade attachment records with channel deletion before cleaning files
This commit is contained in:
1 parent
34e6fecbd0
commit
cd6ca2388f
2 files changed
+63
-22
No files matched your search
+59
-20
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user