Archives - ZIP, 7Z, RAR and TAR/.tar.gz uploads are indexed in the background into archive_entries (7-Zip compiled to WASM in a worker thread; node-tar for tar). macOS/Windows clutter and paths that could escape the extraction folder are skipped. - Models inside archives are extracted once per archive to hash and geometry-fingerprint them, so duplicates are found inside packs too. Pictures, PDFs and readmes are listed but never matched. - New routes list an archive's entries and serve/convert a single entry, extracted on first use and cached under extracted/. Viewer - Model detail lists what's inside each archive with view/download links; opening an entry adds a viewer tab. - New 3D formats: FBX (with sibling textures), OBJ with its MTL and textures, STEP/STP via occt-import-js, and Rhino 3DM via rhino3dm — the WASM readers are served from node_modules, loaded on first use. - Pictures, PDFs and text files (jpg/png/gif/webp/bmp/svg, pdf, txt/md) can be uploaded and preview in the viewer; a model without an embedded 3MF preview uses its first picture as the card thumbnail. - Inline file responses send nosniff, and SVGs are sandboxed. - GLB conversions write to a temp file and rename, so a half-written preview is never served; a later tab can't be overdrawn by a slow load. - Better 3MF card image: the project thumbnail or plate 1 render, not a slicer pick map. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
82 lines
2.8 KiB
JavaScript
82 lines
2.8 KiB
JavaScript
// Worker thread running 7-Zip (compiled to WASM) for ZIP/7Z/RAR archives.
|
|
// 7-Zip calls are synchronous, so they run here to keep the server responsive.
|
|
|
|
const { parentPort } = require('worker_threads');
|
|
const SevenZip = require('7z-wasm');
|
|
|
|
let sevenZip = null;
|
|
const output = [];
|
|
|
|
// The host filesystem is mounted at /host inside the WASM filesystem.
|
|
const host = (p) => `/host${p}`;
|
|
|
|
async function instance() {
|
|
if (!sevenZip) {
|
|
sevenZip = await SevenZip({ print: (l) => output.push(l), printErr: (l) => output.push(l) });
|
|
sevenZip.FS.mkdir('/host');
|
|
sevenZip.FS.mount(sevenZip.NODEFS, { root: '/' }, '/host');
|
|
}
|
|
return sevenZip;
|
|
}
|
|
|
|
async function run(args) {
|
|
const sz = await instance();
|
|
output.length = 0;
|
|
let code;
|
|
try {
|
|
code = sz.callMain(args);
|
|
} catch (err) {
|
|
code = err && typeof err.status === 'number' ? err.status : 2;
|
|
}
|
|
return { code, lines: output.slice() };
|
|
}
|
|
|
|
function errorText(lines) {
|
|
const line = lines.find((l) => /error|wrong password|can ?not open|unsupported|headers/i.test(l));
|
|
return line ? line.trim() : null;
|
|
}
|
|
|
|
// Parses `7zz l -slt` into [{ path, size }] for files (folders are dropped).
|
|
function parseListing(lines) {
|
|
const start = lines.findIndex((l) => l.startsWith('----------'));
|
|
if (start === -1) return null;
|
|
const entries = [];
|
|
let cur = {};
|
|
const flush = () => {
|
|
const isFolder = cur.Folder === '+' || (cur.Attributes || '').startsWith('D');
|
|
if (cur.Path !== undefined && !isFolder) entries.push({ path: cur.Path, size: Number(cur.Size) || 0 });
|
|
cur = {};
|
|
};
|
|
for (const line of lines.slice(start + 1)) {
|
|
if (!line.trim()) { flush(); continue; }
|
|
const i = line.indexOf(' = ');
|
|
if (i > 0) cur[line.slice(0, i)] = line.slice(i + 3);
|
|
}
|
|
flush();
|
|
return entries;
|
|
}
|
|
|
|
async function list(archive) {
|
|
// A dummy password makes encrypted archives fail fast instead of prompting.
|
|
const { code, lines } = await run(['l', '-slt', '-pprinthive', host(archive)]);
|
|
const entries = parseListing(lines);
|
|
if (code !== 0 || !entries) throw new Error(errorText(lines) || `7-Zip could not read the archive (exit ${code})`);
|
|
return entries;
|
|
}
|
|
|
|
// Extracts the named entries (keeping their folders) under destDir. Callers
|
|
// check which files actually arrived.
|
|
async function extract(archive, entries, destDir) {
|
|
const { code, lines } = await run(
|
|
['x', host(archive), `-o${host(destDir)}`, '-y', '-spd', '-bsp0', '-pprinthive', '--', ...entries]);
|
|
return { code, error: code === 0 ? null : errorText(lines) };
|
|
}
|
|
|
|
parentPort.on('message', async ({ id, op, archive, entries, destDir }) => {
|
|
try {
|
|
const result = op === 'list' ? await list(archive) : await extract(archive, entries, destDir);
|
|
parentPort.postMessage({ id, result });
|
|
} catch (err) {
|
|
parentPort.postMessage({ id, error: err.message });
|
|
}
|
|
});
|