fix(security-classifier): close writer + delete tmp on download error

downloadFile() opens an fs.WriteStream to '<dest>.tmp.<pid>' and drives
it from a fetch body reader, but if reader.read() or writer.write()
throws mid-download the writer is never closed. That leaks an FD per
failed attempt and leaves the half-written tmp on disk. A later retry
can land in renameSync(tmp, dest) with a truncated TestSavantAI /
DeBERTa ONNX file — which then loads but produces garbage classifier
verdicts until the user manually nukes the models cache.

Wrap the download loop in try/catch. On failure, destroy() the writer
and unlink the tmp before rethrowing, so the next attempt starts from a
clean slate.
This commit is contained in:
RagavRida
2026-04-24 00:07:24 +05:30
committed by Garry Tan
parent 3bba467289
commit 321a4e75b5
+8
View File
@@ -144,6 +144,7 @@ async function downloadFile(url: string, dest: string): Promise<void> {
const writer = fs.createWriteStream(tmp); const writer = fs.createWriteStream(tmp);
// @ts-ignore — Node stream compat // @ts-ignore — Node stream compat
const reader = res.body.getReader(); const reader = res.body.getReader();
try {
let done = false; let done = false;
while (!done) { while (!done) {
const chunk = await reader.read(); const chunk = await reader.read();
@@ -154,6 +155,13 @@ async function downloadFile(url: string, dest: string): Promise<void> {
writer.end((err?: Error | null) => (err ? reject(err) : resolve())); writer.end((err?: Error | null) => (err ? reject(err) : resolve()));
}); });
fs.renameSync(tmp, dest); fs.renameSync(tmp, dest);
} catch (err) {
// Close the writer and drop the half-written tmp so we don't leak an FD
// or ship a truncated model file to the renameSync path on retry.
try { writer.destroy(); } catch { /* already destroyed */ }
try { fs.unlinkSync(tmp); } catch { /* nothing to clean */ }
throw err;
}
} }
async function ensureTestsavantStaged(onProgress?: (msg: string) => void): Promise<void> { async function ensureTestsavantStaged(onProgress?: (msg: string) => void): Promise<void> {