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
+17 -9
View File
@@ -144,16 +144,24 @@ async function downloadFile(url: string, dest: string): Promise<void> {
const writer = fs.createWriteStream(tmp);
// @ts-ignore — Node stream compat
const reader = res.body.getReader();
let done = false;
while (!done) {
const chunk = await reader.read();
if (chunk.done) { done = true; break; }
writer.write(chunk.value);
try {
let done = false;
while (!done) {
const chunk = await reader.read();
if (chunk.done) { done = true; break; }
writer.write(chunk.value);
}
await new Promise<void>((resolve, reject) => {
writer.end((err?: Error | null) => (err ? reject(err) : resolve()));
});
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;
}
await new Promise<void>((resolve, reject) => {
writer.end((err?: Error | null) => (err ? reject(err) : resolve()));
});
fs.renameSync(tmp, dest);
}
async function ensureTestsavantStaged(onProgress?: (msg: string) => void): Promise<void> {