8f8eef53c9
- Optimize imported scene preview images (smart WebP/JPEG/PNG, preserve alpha, keep pixel size) - Update converter to re-encode existing image assets with same algorithm - Improve import/export progress overlay and reduce presentation slide stutter Made-with: Cursor
138 lines
4.0 KiB
TypeScript
138 lines
4.0 KiB
TypeScript
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,
|
|
onProgress?: (done: number, total: number) => void,
|
|
): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
yauzl.open(zipPath, { lazyEntries: true }, (err, zip) => {
|
|
if (err) return reject(err);
|
|
const zipFile = zip;
|
|
let settled = false;
|
|
const total = zipFile.entryCount || 0;
|
|
let done = 0;
|
|
|
|
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;
|
|
done += 1;
|
|
if (onProgress && total > 0) {
|
|
try {
|
|
onProgress(done, total);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
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<Project> {
|
|
return new Promise<Project>((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'));
|
|
});
|
|
});
|
|
});
|
|
}
|