fix(storage): age-gate the boot spool sweep

This commit is contained in:
jubnl
2026-08-16 09:53:26 +02:00
parent 92c87d1f3a
commit db3c5da04e
3 changed files with 32 additions and 2 deletions
@@ -16,6 +16,8 @@ import {
} from '../storage.types';
const SPOOL_DIR_NAME = '.tmp';
/** Boot spool-reap age gate: entries younger than this survive the sweep. */
const SPOOL_REAP_AGE_MS = 60 * 60 * 1000;
function errnoCode(err: unknown): string | undefined {
return (err as NodeJS.ErrnoException).code;
@@ -65,7 +67,18 @@ export class LocalDriver implements StorageDriver {
}
if (opts.cleanSpool) {
for (const entry of fs.readdirSync(this.spoolDir())) {
fs.rmSync(path.join(this.spoolDir(), entry), { recursive: true, force: true });
const entryPath = path.join(this.spoolDir(), entry);
// Age gate: only reap entries older than the threshold. Crash leftovers
// are always old by the next boot; a fresh entry belongs to another
// process spooling into the same tree right now (the vitest integration
// workers share uploads/, and a second worker booting mid-upload must
// not delete the first one's in-flight spool file).
try {
if (Date.now() - fs.statSync(entryPath).mtimeMs < SPOOL_REAP_AGE_MS) continue;
} catch {
continue; // raced away already — nothing to reap
}
fs.rmSync(entryPath, { recursive: true, force: true });
}
}
}
@@ -63,11 +63,14 @@ describe('LocalDriver specifics', () => {
expect(() => driver.getLocalPath('../escape')).toThrow(StorageInvalidKeyError);
});
it('cleans spool leftovers only at boot, never on reload-style init', () => {
it('cleans aged spool leftovers only at boot, never on reload-style init', () => {
const driver = new LocalDriver({ id: 'spool', root: tmp() });
driver.init();
const stray = path.join(driver.getSpoolDir(), 'leftover.part');
fs.writeFileSync(stray, 'in-flight upload');
// Crash leftover: old enough to clear the reap age gate.
const old = new Date(Date.now() - 2 * 60 * 60 * 1000);
fs.utimesSync(stray, old, old);
driver.init(); // reload(): must NOT delete an in-flight upload's spool file
expect(fs.existsSync(stray)).toBe(true);
@@ -76,6 +79,16 @@ describe('LocalDriver specifics', () => {
expect(fs.existsSync(stray)).toBe(false);
});
it('the boot spool sweep spares fresh entries (another process may be mid-upload)', () => {
const driver = new LocalDriver({ id: 'spool-fresh', root: tmp() });
driver.init();
const inflight = path.join(driver.getSpoolDir(), 'inflight.part');
fs.writeFileSync(inflight, 'spooling right now');
driver.init({ cleanSpool: true });
expect(fs.existsSync(inflight)).toBe(true);
});
it('creates its root, spool, and category prefix dirs at init', () => {
const root = path.join(tmp(), 'nested', 'uploads');
const driver = new LocalDriver({ id: 'pfx', root });
@@ -237,6 +237,10 @@ describe('StorageRegistryService reload', () => {
fs.mkdirSync(path.dirname(stray), { recursive: true });
fs.writeFileSync(stray, 'crash leftover');
// Aged past the reap gate: the boot sweep spares fresh entries (another
// process may be spooling into the same tree — see LocalDriver.init).
const old = new Date(Date.now() - 2 * 60 * 60 * 1000);
fs.utimesSync(stray, old, old);
const { registry } = makeRegistry({ uploadsDir: uploadsRoot });
expect(fs.existsSync(stray)).toBe(false); // boot reclaimed it