mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-10-25 13:11:05 +00:00
fix(filesystem): destroy the upload source when the staged write fails early
A source stream opens its descriptor when it is created, so a refused staging open left it open until exit. Destroy it on the way out of the atomic writer; destroying a finished stream is a no-op.
This commit is contained in:
@@ -9,6 +9,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vites
|
||||
import { promises as fsp } from 'fs';
|
||||
import fsSync from 'fs';
|
||||
import path from 'path';
|
||||
import { Readable } from 'stream';
|
||||
import { setupTestDb, cleanupTestDb } from './helpers/setupTestDb';
|
||||
|
||||
let tmpDir: string;
|
||||
@@ -212,6 +213,24 @@ describe('compose save publication', () => {
|
||||
expect(fsSync.readFileSync(victim, 'utf8')).toBe('untouched');
|
||||
});
|
||||
|
||||
it('releases the upload source when the staging file cannot be created', async () => {
|
||||
const source = Readable.from([Buffer.from(composeContent(['web']))]);
|
||||
const realOpen = fsp.open.bind(fsp);
|
||||
vi.spyOn(fsp, 'open').mockImplementation((async (...args: Parameters<typeof fsp.open>) => {
|
||||
if (String(args[0]).includes('sencho-tmp')) throw Object.assign(new Error('refused'), { code: 'EACCES' });
|
||||
return realOpen(...args);
|
||||
}) as typeof fsp.open);
|
||||
const writer = FileSystemService.getInstance() as unknown as {
|
||||
writeStackFileAtomic(target: string, data: Readable): Promise<void>;
|
||||
};
|
||||
|
||||
await expect(writer.writeStackFileAtomic(path.join(stackDir, 'compose.yaml'), source))
|
||||
.rejects.toThrow('refused');
|
||||
|
||||
expect(source.destroyed).toBe(true);
|
||||
expect(stagingFiles()).toEqual([]);
|
||||
});
|
||||
|
||||
it('removes the staging file when restricted content fails to publish', async () => {
|
||||
const target = path.join(stackDir, 'compose.yaml');
|
||||
fsSync.chmodSync(target, 0o600);
|
||||
|
||||
@@ -2479,6 +2479,11 @@ export class FileSystemService {
|
||||
if (staged) await recordAtomicPublication(safePath, staged);
|
||||
}
|
||||
} finally {
|
||||
// A source stream holds its own descriptor from the moment it is
|
||||
// created, so a failure before it is consumed (the staging open
|
||||
// refused) would otherwise leave it open. Destroying a finished stream
|
||||
// is a no-op.
|
||||
if (data instanceof Readable) data.destroy();
|
||||
if (stagedTmp) {
|
||||
await fsPromises.unlink(tmpPath).catch(() => {});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user