feat(dwg): DWG→DXF via sidecar microservice (libredwg)

Add dedicated dwg2dxf container (Debian slim + libredwg-tools + Flask)
instead of modifying the Alpine base image. The ArchiTools API route
proxies to the sidecar over Docker internal network.

- dwg2dxf-api/: Dockerfile + Flask app (POST /convert, GET /health)
- docker-compose.yml: dwg2dxf service, healthcheck, depends_on
- route.ts: rewritten from local exec to HTTP proxy
- .dockerignore: exclude sidecar from main build context

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-09 10:51:27 +02:00
parent 7ed653eaec
commit 5209fd5dd0
6 changed files with 161 additions and 36 deletions
+27 -35
View File
@@ -1,18 +1,12 @@
import { NextRequest, NextResponse } from "next/server";
import { writeFile, readFile, unlink, mkdir } from "fs/promises";
import { exec } from "child_process";
import { promisify } from "util";
import { randomUUID } from "crypto";
import { join } from "path";
import { tmpdir } from "os";
const execAsync = promisify(exec);
const DWG2DXF_URL = process.env.DWG2DXF_URL ?? "http://localhost:5001";
export async function POST(req: NextRequest) {
const tmpDir = join(tmpdir(), `dwg-${randomUUID()}`);
try {
const formData = await req.formData();
const file = formData.get("fileInput") as File | null;
if (!file) {
return NextResponse.json(
{ error: "Lipsește fișierul DWG." },
@@ -28,20 +22,29 @@ export async function POST(req: NextRequest) {
);
}
await mkdir(tmpDir, { recursive: true });
// Re-package for sidecar (field name: "file")
const sidecarForm = new FormData();
sidecarForm.append("file", file, name);
const inputPath = join(tmpDir, name);
const outputPath = inputPath.replace(/\.dwg$/i, ".dxf");
const res = await fetch(`${DWG2DXF_URL}/convert`, {
method: "POST",
body: sidecarForm,
});
const buffer = Buffer.from(await file.arrayBuffer());
await writeFile(inputPath, buffer);
if (!res.ok) {
const data = await res.json().catch(() => ({ error: res.statusText }));
const errorMsg =
typeof data === "object" && data !== null && "error" in data
? String((data as Record<string, unknown>).error)
: `Eroare sidecar DWG: ${res.status}`;
return NextResponse.json({ error: errorMsg }, { status: res.status });
}
await execAsync(`dwg2dxf "${inputPath}"`, { timeout: 60_000 });
const dxfBuffer = await readFile(outputPath);
const blob = await res.blob();
const buffer = Buffer.from(await blob.arrayBuffer());
const dxfName = name.replace(/\.dwg$/i, ".dxf");
return new NextResponse(dxfBuffer, {
return new NextResponse(buffer, {
status: 200,
headers: {
"Content-Type": "application/dxf",
@@ -50,35 +53,24 @@ export async function POST(req: NextRequest) {
});
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
if (
message.includes("ENOENT") ||
message.includes("not found") ||
message.includes("not recognized")
message.includes("ECONNREFUSED") ||
message.includes("fetch failed") ||
message.includes("ENOTFOUND")
) {
return NextResponse.json(
{
error:
"Conversia DWG→DXF este disponibilă doar pe server (Docker). Local nu este instalat dwg2dxf (libredwg).",
"Serviciul de conversie DWG nu este disponibil. Verificați că containerul dwg2dxf este pornit.",
},
{ status: 501 },
{ status: 503 },
);
}
return NextResponse.json(
{ error: `Eroare la conversie DWG→DXF: ${message}` },
{ status: 500 },
);
} finally {
// Clean up temp files
try {
const { readdir } = await import("fs/promises");
const files = await readdir(tmpDir);
for (const f of files) {
await unlink(join(tmpDir, f)).catch(() => {});
}
const { rmdir } = await import("fs/promises");
await rmdir(tmpDir).catch(() => {});
} catch {
// temp cleanup failure is non-critical
}
}
}