import fssync from 'node:fs'; import path from 'node:path'; import yauzl from 'yauzl'; import type { Project } from '../../shared/types'; export function unzipToDir(zipPath: string, outDir: string): Promise { return new Promise((resolve, reject) => { yauzl.open(zipPath, { lazyEntries: true }, (err, zip) => { if (err) return reject(err); const zipFile = zip; let settled = false; const safeClose = (): void => { try { zipFile.close(); } catch { /* ignore */ } }; const finishOk = (): void => { if (settled) return; settled = true; safeClose(); resolve(); }; const finishErr = (e: unknown): void => { if (settled) return; settled = true; safeClose(); reject(e instanceof Error ? e : new Error(String(e))); }; zipFile.readEntry(); zipFile.on('entry', (entry: yauzl.Entry) => { if (settled) return; const filePath = path.join(outDir, entry.fileName); if (entry.fileName.endsWith('/')) { fssync.mkdirSync(filePath, { recursive: true }); zipFile.readEntry(); return; } fssync.mkdirSync(path.dirname(filePath), { recursive: true }); zipFile.openReadStream(entry, (streamErr, readStream) => { if (streamErr) return finishErr(streamErr); readStream.on('error', finishErr); const writeStream = fssync.createWriteStream(filePath); writeStream.on('error', finishErr); readStream.pipe(writeStream); writeStream.on('close', () => { if (!settled) zipFile.readEntry(); }); }); }); zipFile.on('end', () => finishOk()); zipFile.on('error', finishErr); }); }); } /** Читает `project.json` из zip; всегда закрывает дескриптор yauzl (иначе на Windows возможен EMFILE). */ export async function readProjectJsonFromZip(zipPath: string): Promise { return new Promise((resolve, reject) => { yauzl.open(zipPath, { lazyEntries: true }, (err, zip) => { if (err) return reject(err); const zipFile = zip; let settled = false; const safeClose = (): void => { try { zipFile.close(); } catch { /* ignore */ } }; const finishOk = (project: Project): void => { if (settled) return; settled = true; safeClose(); resolve(project); }; const finishErr = (e: unknown): void => { if (settled) return; settled = true; safeClose(); reject(e instanceof Error ? e : new Error(String(e))); }; zipFile.readEntry(); zipFile.on('entry', (entry: yauzl.Entry) => { if (settled) return; if (entry.fileName !== 'project.json') { zipFile.readEntry(); return; } zipFile.openReadStream(entry, (streamErr, readStream) => { if (streamErr) return finishErr(streamErr); const chunks: Buffer[] = []; readStream.on('data', (c: Buffer) => chunks.push(c)); readStream.on('error', finishErr); readStream.on('end', () => { try { const raw = Buffer.concat(chunks).toString('utf8'); const parsed = JSON.parse(raw) as unknown as Project; finishOk(parsed); } catch (e) { finishErr(e instanceof Error ? e : new Error('Failed to parse project.json')); } }); }); }); zipFile.on('error', finishErr); zipFile.on('end', () => { finishErr(new Error('project.json not found in zip')); }); }); }); }