mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
fix(storage): age-gate the boot spool sweep
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user