From a053d83a600cd35501437e80f68228704f1cd635 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 3 Nov 2026 12:48:20 +0100 Subject: [PATCH] Cancel unfinished DM files when the local subgroup is left --- src/dm/dm_media.c | 31 ++++++++++++++++++++++++++++--- src/dm/dm_media.h | 6 ++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/dm/dm_media.c b/src/dm/dm_media.c index 058a09bc9..e9bf2f16d 100644 --- a/src/dm/dm_media.c +++ b/src/dm/dm_media.c @@ -247,6 +247,27 @@ void dm_media_forget_committed(struct UTUN_INSTANCE* inst, const char* conv) { dm_cleanup_pump(inst->dm_media); } +int dm_media_close_prepare(struct UTUN_INSTANCE* inst) { + if (!inst->dm_media) return 0; + return dm_media_exec(inst, "UPDATE dm_local_files SET generation=0 WHERE conv_id IN" + " (SELECT conv_id FROM dm_conversations WHERE closed=1) AND NOT EXISTS" + " (SELECT 1 FROM dm_files f WHERE f.conv_id=dm_local_files.conv_id AND f.ready=1" + " AND (dm_local_files.path=f.local_path OR dm_local_files.path=substr(f.local_path,1,length(f.local_path)-5)||'.cipher'))"); +} + +void dm_media_close_committed(struct UTUN_INSTANCE* inst) { + struct dm_media_state* media = inst->dm_media; + if (!media) return; + for (struct dm_file_job* job = media->jobs, *next; job; job = next) { + next = job->next; + if (job->transfer && job->work == DM_FILE_RECEIVE && dm_conversation_access(inst, job->conv, NULL, NULL)) { + DEBUG_INFO(DEBUG_CATEGORY_DM, "DM files: cancel after group exit conv=%s", job->conv); + file_transfer_cancel(job->transfer); + } + } + dm_cleanup_pump(media); +} + /* Подписанная роль и живые мемберы в одной области; тип NAT не даёт полномочий. */ int dm_media_scope(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t node, const char* role, uint64_t author, uint64_t recipient) { @@ -554,7 +575,8 @@ static void dm_file_done(void* arg, int error) { } int custody = job->work == DM_FILE_CUSTODY; if (error || job->error || media->closing) goto fail; - if (!custody && !dm_history_current(inst, job->conv, job->generation)) { + if (!custody && (!dm_history_current(inst, job->conv, job->generation) || + dm_conversation_access(inst, job->conv, NULL, NULL))) { DEBUG_INFO(DEBUG_CATEGORY_DM, "DM files: discard deleted preparation conv=%s generation=%llu work=%d", job->conv, (unsigned long long)job->generation, job->work); goto fail; } @@ -734,7 +756,9 @@ int dm_send_file(struct UTUN_INSTANCE* inst, const char* conv, const char* path, job->media = media; job->work = DM_FILE_PREPARE; job->author = inst->node_id; job->generation = dm_history_generation(inst, conv); job->request_id = request_id; - if (!dm_history_current(inst, conv, job->generation)) { u_free(job); return -1; } + if (!dm_history_current(inst, conv, job->generation) || dm_conversation_access(inst, conv, NULL, NULL)) { + u_free(job); return -1; + } snprintf(job->conv, sizeof(job->conv), "%s", conv); job->info = *info; job->remove_source = remove_source; snprintf(job->input, sizeof(job->input), "%s", path); sqlite3_stmt* st = dm_media_prepare(inst, "SELECT peer_x25519 FROM dm_conversations WHERE conv_id=?"); @@ -1076,7 +1100,8 @@ static void dm_files_tick(struct dm_media_state* media, uint64_t now) { struct UTUN_INSTANCE* inst = media->inst; for (int i = 0; i < 8; i++) { sqlite3_stmt* st = dm_media_prepare(inst, "SELECT id,dir,author,recipient,group_id,body,ready,holder,expires,receipt" - " FROM dm_files WHERE retry_at<=? ORDER BY retry_at,seq LIMIT 1"); + " FROM dm_files WHERE retry_at<=? AND conv_id IN" + " (SELECT conv_id FROM dm_conversations WHERE closed=0) ORDER BY retry_at,seq LIMIT 1"); if (!st) return; sqlite3_bind_int64(st, 1, (sqlite3_int64)now); int rc = sqlite3_step(st); diff --git a/src/dm/dm_media.h b/src/dm/dm_media.h index 8851fa6d6..a551a63d0 100644 --- a/src/dm/dm_media.h +++ b/src/dm/dm_media.h @@ -97,6 +97,12 @@ int dm_media_forget_prepare(struct UTUN_INSTANCE* inst, const char* conv, uint64 /* Только после commit: отменить transfer удалённых UUID и начать асинхронную уборку старых поколений. * Callback отмены transfer синхронен; ожидания workers здесь нет, busy защищает их пути. */ void dm_media_forget_committed(struct UTUN_INSTANCE* inst, const char* conv); +/* В транзакции выхода пометить незавершённые файлы closed-бесед для durable уборки. + * Готовые plaintext/ciphertext остаются. 0/-1, без media — пустая операция. */ +int dm_media_close_prepare(struct UTUN_INSTANCE* inst); +/* После commit выхода отменить передачи closed-бесед. Workers завершатся с отказом + * публикации в историю; уборка ждёт их holds. Готовые локальные вложения сохраняются. */ +void dm_media_close_committed(struct UTUN_INSTANCE* inst); /* Зарегистрировать принадлежащий приложению path и увеличить busy до запуска worker/transfer. * conv/generation должны быть текущими и видимыми; 0=захват, -1=ошибка/чужое поколение пути. */ int dm_media_file_hold(struct UTUN_INSTANCE* inst, const char* conv, uint64_t generation, const char* path);