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>
47 lines
2.2 KiB
JavaScript
47 lines
2.2 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const crypto = require('crypto');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
process.env.EXTRACT_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'printhive-extract-'));
|
|
const archives = require('../lib/archives');
|
|
|
|
const fixture = (name) => path.join(__dirname, 'fixtures', name);
|
|
const sha = (p) => crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex');
|
|
const clean = (p) => p.replace(/^\.\//, '');
|
|
|
|
// Each pack holds models/cube.stl, models/inner.3mf and readme.txt, plus macOS
|
|
// junk (._ files, __MACOSX, .DS_Store); the zip also has a ../ path.
|
|
const EXPECTED = ['models/cube.stl', 'models/inner.3mf', 'readme.txt'];
|
|
|
|
test.after(() => fs.rmSync(process.env.EXTRACT_DIR, { recursive: true, force: true }));
|
|
|
|
for (const [name, format] of [['pack.zip', 'zip'], ['pack.7z', '7z'], ['pack.tar.gz', 'tgz']]) {
|
|
const file = { id: `test-${format}`, file_path: fixture(name), format };
|
|
|
|
test(`${name}: lists real files, skipping folders, junk and unsafe paths`, async () => {
|
|
const entries = await archives.listEntries(file);
|
|
assert.deepEqual(entries.map((e) => clean(e.path)).sort(), EXPECTED);
|
|
const cube = entries.find((e) => clean(e.path) === 'models/cube.stl');
|
|
assert.equal(cube.size, fs.statSync(fixture('cube.stl')).size);
|
|
});
|
|
|
|
test(`${name}: extracts one entry byte-for-byte, and caches it`, async () => {
|
|
const entries = await archives.listEntries(file);
|
|
const cube = { ...entries.find((e) => clean(e.path) === 'models/cube.stl'), id: 1 };
|
|
const abs = await archives.extractEntry(file, cube);
|
|
assert.equal(sha(abs), sha(fixture('cube.stl')));
|
|
assert.equal(await archives.extractEntry(file, cube), abs);
|
|
});
|
|
}
|
|
|
|
test('a missing entry fails clearly instead of returning a path', async () => {
|
|
const file = { id: 'test-missing', file_path: fixture('pack.zip'), format: 'zip' };
|
|
await assert.rejects(archives.extractEntry(file, { id: 2, path: 'models/nope.stl' }), /Could not extract/);
|
|
});
|
|
|
|
test('an unreadable archive rejects', async () => {
|
|
await assert.rejects(archives.listEntries({ file_path: fixture('cube.stl'), format: 'zip' }));
|
|
});
|