task(F7): wcadlib as real zip package - jszip export-zip/import-zip endpoints with manifest/blocks/thumbs, frontend zip buttons
This commit is contained in:
@@ -6,6 +6,7 @@ import type { DatabaseInterface } from '../database/DatabaseInterface.js';
|
||||
import { requireAuth } from '../auth/authMiddleware.js';
|
||||
import type { AuthService } from '../auth/AuthService.js';
|
||||
import { validateName, validateIdParam } from '../utils/validation.js';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
export function registerGlobalBlockRoutes(fastify: FastifyInstance, db: DatabaseInterface, authService: AuthService) {
|
||||
// ─── Global Block Folders ──────────────────────────────
|
||||
@@ -229,4 +230,132 @@ export function registerGlobalBlockRoutes(fastify: FastifyInstance, db: Database
|
||||
}
|
||||
return reply.code(201).send({ imported_blocks: imported, imported_folders: folderIdMap.size });
|
||||
});
|
||||
|
||||
// ─── Task F7: .wcadlib als echtes ZIP-Paket ──────────────
|
||||
|
||||
// ZIP-Export: manifest.json + blocks.json + thumbs/ (SVG je Block)
|
||||
fastify.get('/api/global-blocks/export-zip', async (request, reply) => {
|
||||
if (!requireAuth(request, reply, authService)) return;
|
||||
const folders = db.listGlobalFolders();
|
||||
const blocks = db.listGlobalBlocks();
|
||||
|
||||
const zip = new JSZip();
|
||||
zip.file('manifest.json', JSON.stringify({
|
||||
format: 'wcadlib-zip',
|
||||
version: 1,
|
||||
exported_at: new Date().toISOString(),
|
||||
block_count: blocks.length,
|
||||
folder_count: folders.length,
|
||||
}));
|
||||
|
||||
const blockEntries = [] as Array<{
|
||||
name: string;
|
||||
folder_id: string | null;
|
||||
block_data: string;
|
||||
thumbnail: string | null;
|
||||
}>;
|
||||
for (const b of blocks) {
|
||||
let thumbnail: string | null = null;
|
||||
if (b.svg_data) {
|
||||
thumbnail = `thumbs/${b.id}.svg`;
|
||||
zip.file(thumbnail, b.svg_data);
|
||||
}
|
||||
blockEntries.push({
|
||||
name: b.name,
|
||||
folder_id: b.folder_id,
|
||||
block_data: b.block_data,
|
||||
thumbnail,
|
||||
});
|
||||
}
|
||||
zip.file('blocks.json', JSON.stringify({
|
||||
folders: folders.map((f) => ({ id: f.id, name: f.name, parent_id: f.parent_id })),
|
||||
blocks: blockEntries,
|
||||
}));
|
||||
|
||||
const buffer = await zip.generateAsync({ type: 'nodebuffer' });
|
||||
reply.header('content-type', 'application/zip');
|
||||
reply.header('content-disposition', 'attachment; filename="library.wcadlib.zip"');
|
||||
return reply.send(buffer);
|
||||
});
|
||||
|
||||
// ZIP-Import: .wcadlib-ZIP einspielen (Ordner-Mapping, Blöcke, Thumbnails)
|
||||
fastify.post('/api/global-blocks/import-zip', async (request, reply) => {
|
||||
if (!requireAuth(request, reply, authService)) return;
|
||||
const body = request.body;
|
||||
if (!Buffer.isBuffer(body) || body.length === 0) {
|
||||
return reply.code(400).send({ error: 'ZIP body fehlt oder leer' });
|
||||
}
|
||||
|
||||
let zip: JSZip;
|
||||
try {
|
||||
zip = await JSZip.loadAsync(body);
|
||||
} catch {
|
||||
return reply.code(400).send({ error: 'Kein valides ZIP-Format' });
|
||||
}
|
||||
|
||||
const blocksFile = zip.file('blocks.json');
|
||||
if (!blocksFile) {
|
||||
return reply.code(400).send({ error: 'blocks.json fehlt im ZIP' });
|
||||
}
|
||||
|
||||
let pkg: { folders?: Array<{ id: string; name: string; parent_id: string | null }>; blocks?: Array<{ name: string; folder_id: string | null; block_data: string; thumbnail?: string | null }> };
|
||||
try {
|
||||
pkg = JSON.parse(await blocksFile.async('string'));
|
||||
} catch {
|
||||
return reply.code(400).send({ error: 'blocks.json ist kein valides JSON' });
|
||||
}
|
||||
if (!Array.isArray(pkg.blocks)) {
|
||||
return reply.code(400).send({ error: 'blocks.json enthaelt kein blocks-Array' });
|
||||
}
|
||||
|
||||
// Optional: manifest pruefen (falls vorhanden)
|
||||
const manifestFile = zip.file('manifest.json');
|
||||
if (manifestFile) {
|
||||
try {
|
||||
const manifest = JSON.parse(await manifestFile.async('string'));
|
||||
if (manifest.format && manifest.format !== 'wcadlib-zip') {
|
||||
return reply.code(400).send({ error: `Unbekanntes Paket-Format: ${manifest.format}` });
|
||||
}
|
||||
} catch {
|
||||
return reply.code(400).send({ error: 'manifest.json ist kein valides JSON' });
|
||||
}
|
||||
}
|
||||
|
||||
// Ordner mit ID-Mapping importieren (Eltern zuerst):
|
||||
const folderIdMap = new Map<string, string>();
|
||||
const sortedFolders = [...(pkg.folders ?? [])].sort((a, b) => {
|
||||
if (a.parent_id === null) return -1;
|
||||
if (b.parent_id === null) return 1;
|
||||
if (a.parent_id === b.parent_id) return 0;
|
||||
const aRes = folderIdMap.has(a.parent_id!) ? -1 : 0;
|
||||
return aRes;
|
||||
});
|
||||
for (const f of sortedFolders) {
|
||||
const newParent = f.parent_id ? folderIdMap.get(f.parent_id) ?? null : null;
|
||||
const created = db.createGlobalFolder({ name: f.name, parent_id: newParent });
|
||||
folderIdMap.set(f.id, created.id);
|
||||
}
|
||||
|
||||
// Blöcke importieren; Thumbnail aus thumbs/ lesen wenn referenziert:
|
||||
let imported = 0;
|
||||
for (const b of pkg.blocks) {
|
||||
let svgData: string | null = null;
|
||||
if (b.thumbnail) {
|
||||
const thumbFile = zip.file(b.thumbnail);
|
||||
if (thumbFile) {
|
||||
try { svgData = await thumbFile.async('string'); } catch { svgData = null; }
|
||||
}
|
||||
}
|
||||
const newFolderId = b.folder_id ? folderIdMap.get(b.folder_id) ?? null : null;
|
||||
db.createGlobalBlock({
|
||||
name: b.name,
|
||||
folder_id: newFolderId,
|
||||
block_data: b.block_data,
|
||||
svg_data: svgData,
|
||||
});
|
||||
imported++;
|
||||
}
|
||||
|
||||
return reply.code(201).send({ imported_blocks: imported, imported_folders: folderIdMap.size });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,6 +22,11 @@ export interface ServerOptions {
|
||||
export async function createServer(opts: ServerOptions) {
|
||||
const fastify = Fastify({ logger: true });
|
||||
|
||||
// ─── Task F7: binäre Body für .wcadlib ZIP-Import ───
|
||||
fastify.addContentTypeParser('application/zip', { parseAs: 'buffer' }, (_req, body, done) => done(null, body));
|
||||
fastify.addContentTypeParser('application/octet-stream', { parseAs: 'buffer' }, (_req, body, done) => done(null, body));
|
||||
fastify.addContentTypeParser('application/x-zip-compressed', { parseAs: 'buffer' }, (_req, body, done) => done(null, body));
|
||||
|
||||
// CORS — restrictive origin list
|
||||
await fastify.register(cors, {
|
||||
origin: (origin: string | undefined, cb: (err: Error | null, allow: boolean) => void) => {
|
||||
|
||||
Reference in New Issue
Block a user