From c1c35399004e8a3b927c1e775f8b074e2748c71b Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Mon, 26 Oct 2026 04:48:59 -0500 Subject: [PATCH] fix(server): clear unreachable preview downloads after a server restart The server browser indexes saved downloads in memory and server tab ids carry a per-process epoch, so files an earlier run saved under server-browser/downloads can never be opened again. Tab close removes its own folder, but a stop or crash leaves the rest on disk for good. Clear the downloads folder when the service starts, before any tab can save. The server ownership lock guarantees one server per state directory, so no other live server owns these files. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/preview/ServerBrowser.test.ts | 21 +++++++++++++++++++ apps/server/src/preview/ServerBrowser.ts | 6 ++++++ 2 files changed, 27 insertions(+) diff --git a/apps/server/src/preview/ServerBrowser.test.ts b/apps/server/src/preview/ServerBrowser.test.ts index c8061833a80ec..a0b7526f513ed 100644 --- a/apps/server/src/preview/ServerBrowser.test.ts +++ b/apps/server/src/preview/ServerBrowser.test.ts @@ -1057,6 +1057,27 @@ it.live.each(["save failed", "oversized"])( ).pipe(Effect.provide(layer)), ); +it.live("a restarted server removes the previous run's downloads and keeps its profiles", () => + Effect.scoped( + Effect.gen(function* () { + const { stateDir } = yield* ServerConfig.ServerConfig; + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const previousTab = path.join(stateDir, "server-browser", "downloads", "previous-tab"); + const profile = path.join(stateDir, "server-browser", "profiles", "default"); + yield* fs.makeDirectory(previousTab, { recursive: true }); + yield* fs.makeDirectory(profile, { recursive: true }); + yield* fs.writeFileString(path.join(previousTab, "report.csv"), "a,b"); + yield* fs.writeFileString(path.join(profile, "Preferences"), "{}"); + + yield* Layer.build(ServerBrowser.layer); + + expect(yield* fs.exists(previousTab)).toBe(false); + expect(yield* fs.readFileString(path.join(profile, "Preferences"))).toBe("{}"); + }), + ).pipe(Effect.provide(dependencies)), +); + it.live("a page's file picker goes to the controller and takes its uploaded files", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/preview/ServerBrowser.ts b/apps/server/src/preview/ServerBrowser.ts index 93f6849bbb995..d4f05d7a35151 100644 --- a/apps/server/src/preview/ServerBrowser.ts +++ b/apps/server/src/preview/ServerBrowser.ts @@ -903,6 +903,12 @@ const make = Effect.gen(function* () { }; const downloadsRoot = NodePath.join(config.stateDir, "server-browser", "downloads"); + // Saved downloads are indexed in memory, so an earlier run's files can never be opened again. + yield* Effect.tryPromise(() => NodeFSP.rm(downloadsRoot, { recursive: true, force: true })).pipe( + Effect.catch((cause) => + Effect.logWarning("server preview could not clear earlier downloads", { cause }), + ), + ); // Tab ids are opaque server strings; hashing keeps them out of the path. const downloadDir = (tab: ServerTab) => NodePath.join(