From f09d76e3fdafb46ad7660375a333862d0c44ab43 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 24 Oct 2026 14:43:03 +0200 Subject: [PATCH] Recover owned encrypted attachment temporaries in both storage trees --- src/chat/chat_attachment.c | 48 ++++++++++++++++++++++++++++++++++---- src/chat/chat_msg.c | 4 +--- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/chat/chat_attachment.c b/src/chat/chat_attachment.c index ef779a9b..9e4e8a3f 100644 --- a/src/chat/chat_attachment.c +++ b/src/chat/chat_attachment.c @@ -96,10 +96,8 @@ invalid: /* Разделённые деревья исключают раздачу plaintext и удаление его обходом orphan media. */ static int ak_paths(struct ak_file* job) { - const char* slash = strrchr(CC(job->inst)->db_path, '/'); char base[512], hex[33]; ak_hex(job->id, hex); - if (slash) snprintf(base, sizeof(base), "%.*s", (int)(slash - CC(job->inst)->db_path), CC(job->inst)->db_path); - else snprintf(base, sizeof(base), "."); + if (media_index_get_base(CC(job->inst)->db, base)) return -1; int a = snprintf(job->cipher, sizeof(job->cipher), "%s/media/%s/%s.uenc", base, job->channel, hex); int b = snprintf(job->plain, sizeof(job->plain), "%s/decrypted/%s/%s.file", base, job->channel, hex); if (a < 0 || b < 0 || (size_t)a >= sizeof(job->cipher) || (size_t)b >= sizeof(job->plain)) { @@ -553,9 +551,50 @@ static void ak_tick(void* arg) { if (!CC(inst)->attachment_timer) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: timer allocation failed"); } +struct ak_temp_cleanup { const char* root; const char* extension; unsigned removed; }; + +/* Только /..tmp.; чужие имена и активные owners сохраняются. */ +static void ak_remove_temp(void* arg, const char* path, int64_t size) { + (void)size; + struct ak_temp_cleanup* cleanup = arg; + size_t prefix = strlen(cleanup->root); + if (strncmp(path, cleanup->root, prefix) || path[prefix] != '/') return; + const char* relative = path + prefix + 1; + const char* slash = strchr(relative, '/'); + if (!slash || slash - relative >= 20) return; + char channel[20]; memcpy(channel, relative, (size_t)(slash - relative)); channel[slash - relative] = 0; + if (!ak_channel(channel)) return; + const char* name = slash + 1; + const char* hex = "0123456789abcdef"; + if (strlen(name) != 74 || strspn(name, hex) != 32 || memcmp(name + 32, cleanup->extension, 5) || + memcmp(name + 37, ".tmp.", 5) || strspn(name + 42, hex) != 32) return; + int removed = file_io_remove_orphan(path); + if (removed > 0) cleanup->removed++; + else if (removed < 0) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: temporary recovery failed path=%s", path); +} + +/* И при crash до публикации, и при прерванной расшифровке temp не является готовым вложением. */ +static void ak_recover_temps(struct UTUN_INSTANCE* inst) { + char base[MEDIA_BASE_MAX]; + if (media_index_get_base(CC(inst)->db, base)) return; + const char* trees[] = {"media", "decrypted"}; + for (unsigned i = 0; i < 2; i++) { + char root[MEDIA_BASE_MAX + 16]; snprintf(root, sizeof(root), "%s/%s", base, trees[i]); + struct file_io_info info; + int status = file_io_inspect(root, 0, &info); + if (status == 1) continue; + if (status < 0) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: cannot inspect temporary tree path=%s", root); continue; } + struct ak_temp_cleanup cleanup = {.root = root, .extension = i ? ".file" : ".uenc"}; + if (ma_dir_walk(root, ak_remove_temp, &cleanup)) + DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: temporary recovery incomplete path=%s removed=%u", root, cleanup.removed); + else DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "attachment keys: temporary recovery path=%s removed=%u", root, cleanup.removed); + } +} + /* После crash/отмены убрать только собственные подготовки, не имеющие сообщения. * Вызывается до запуска workers либо после их полного завершения. */ static void ak_recover_preparations(struct UTUN_INSTANCE* inst) { + ak_recover_temps(inst); char** channels = NULL; int count = 0, columns = 0; if (sqlite3_get_table(CC(inst)->db, "SELECT channel_id FROM channels", &channels, &count, &columns, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: recovery channel scan failed: %s", sqlite3_errmsg(CC(inst)->db)); return; @@ -575,8 +614,7 @@ static void ak_recover_preparations(struct UTUN_INSTANCE* inst) { for (int row = 0; row < rows; row++) { for (int field = 0; field < fields; field++) { const char* path = paths[(row + 1) * fields + field]; - char temporary[1040]; snprintf(temporary, sizeof(temporary), "%s.part", path); - if ((remove(path) && errno != ENOENT) || (remove(temporary) && errno != ENOENT)) { + if (remove(path) && errno != ENOENT) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: unfinished file removal failed path=%s: %s", path, strerror(errno)); failed = 1; } } diff --git a/src/chat/chat_msg.c b/src/chat/chat_msg.c index b6b86414..467d71bd 100644 --- a/src/chat/chat_msg.c +++ b/src/chat/chat_msg.c @@ -1683,7 +1683,7 @@ static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst) { for (int i = 0; i < rels.count; i++) u_free(rels.items[i]); u_free(rels.items); return; } - /* Ещё не опубликованные контейнеры и их временные файлы принадлежат crypto worker. */ + /* Конечные контейнеры принадлежат attachment_keys; активные temp защищены file_io-реестром. */ if (cc->attachment_initialized) { sqlite3_stmt* active = NULL; if (sqlite3_prepare_v2(cc->db, "SELECT cipher_path FROM attachment_keys", -1, &active, NULL) == SQLITE_OK) { @@ -1693,8 +1693,6 @@ static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst) { size_t prefix = strlen(media_base); if (path && !strncmp(path, media_base, prefix) && path[prefix] == '/') { cache_rel_add(&rels, path + prefix + 1); - char temporary[1040]; snprintf(temporary, sizeof(temporary), "%s.part", path + prefix + 1); - cache_rel_add(&rels, temporary); } } if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted cache: active scan failed rc=%d", rc);