From ae5edabb3763bcf8bc8c7a72b6edcb80434e57d9 Mon Sep 17 00:00:00 2001 From: Mihawk <163614687+mihawk-99@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:20:49 -0400 Subject: [PATCH] WebUI: a batch upload cut short removes its partial file before it answers A batch stream that ends inside a file left that file's .upload- temporary until the request was freed, after the 400 answer was sent: a client reading the folder the moment the answer arrived could still find it. The gate's test_batch_refuses_bad_names_and_bad_streams did, once, with the gates run on four CPUs (taskset -c 0-3). BatchUpload::abandon() now closes and removes the cut file before the answer; the destructor calls the same. Evidence: JOBS=4 taskset -c 0-3 nice -n 10 bash tools/verify.sh: the unit gate failed on that test before (a .upload- file left in content/batch-c) and passed after (197 tests OK, 1 skipped); python3 -m unittest tests.test_webui: 17 tests OK. --- src/webui_ps5.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/webui_ps5.cpp b/src/webui_ps5.cpp index 12ffa40..370c469 100644 --- a/src/webui_ps5.cpp +++ b/src/webui_ps5.cpp @@ -1187,13 +1187,19 @@ struct BatchUpload final : ps5_transfer::BatchSink else fail("cannot finish"); } - ~BatchUpload() override + /* The file a cut stream was in the middle of goes */ + void abandon() { const int fd = writer.release(); if (fd >= 0) close(fd); if (!temporary.empty()) unlink(temporary.c_str()); + temporary.clear(); + } + ~BatchUpload() override + { + abandon(); } }; struct BatchState @@ -1970,6 +1976,10 @@ MHD_Result transfer_route(MHD_Connection *c, const std::string &url, const std:: failed += (failed.size() > 1 ? "," : "") + std::string("{\"name\":") + quote(f.first) + ",\"error\":" + quote(f.second) + '}'; const bool whole = state->reader.finished() && r.received == r.expected; + /* Before the answer, not when the request is freed after it: a client that reads + * the folder the moment the answer arrives must not find the partial file. */ + if (!whole) + b.abandon(); return respond(c, whole ? 200 : 400, "{\"complete\":" + std::string(whole ? "true" : "false") + ",\"written\":" + std::to_string(b.written) + ",\"skipped\":" + std::to_string(b.skipped) +