generated from Dicken/dickendock
251 lines
7.9 KiB
TypeScript
251 lines
7.9 KiB
TypeScript
import type { FastifyInstance } from "fastify";
|
|
import * as XLSX from "xlsx";
|
|
import * as deviceRepo from "../db/repositories/devices.js";
|
|
import * as serviceRepo from "../db/repositories/services.js";
|
|
import * as categoryRepo from "../db/repositories/categories.js";
|
|
|
|
interface TransferRow {
|
|
type: "device" | "service";
|
|
displayName: string;
|
|
hostname: string;
|
|
ip: string;
|
|
mac: string;
|
|
manufacturer: string;
|
|
model: string;
|
|
category: string;
|
|
alias: string;
|
|
favorite: string;
|
|
visible: string;
|
|
order: number | string;
|
|
port: string | number;
|
|
https: string;
|
|
url: string;
|
|
deviceHostname: string;
|
|
deviceIp: string;
|
|
}
|
|
|
|
function buildExportRows(): TransferRow[] {
|
|
const devices = deviceRepo.listDevices();
|
|
const services = serviceRepo.listServices();
|
|
const deviceById = new Map(devices.map((d) => [d.id, d]));
|
|
|
|
const deviceRows: TransferRow[] = devices.map((d) => ({
|
|
type: "device",
|
|
displayName: d.hostname,
|
|
hostname: d.hostname,
|
|
ip: d.ip,
|
|
mac: d.mac ?? "",
|
|
manufacturer: d.manufacturer ?? "",
|
|
model: d.model ?? "",
|
|
category: "",
|
|
alias: "",
|
|
favorite: "",
|
|
visible: "",
|
|
order: "",
|
|
port: "",
|
|
https: "",
|
|
url: "",
|
|
deviceHostname: "",
|
|
deviceIp: "",
|
|
}));
|
|
|
|
const serviceRows: TransferRow[] = services.map((s) => {
|
|
const device = deviceById.get(s.deviceId);
|
|
return {
|
|
type: "service",
|
|
displayName: s.displayName,
|
|
hostname: s.hostname,
|
|
ip: "",
|
|
mac: "",
|
|
manufacturer: "",
|
|
model: "",
|
|
category: s.category ?? "",
|
|
alias: s.alias.join(";"),
|
|
favorite: s.favorite ? "true" : "false",
|
|
visible: s.visible ? "true" : "false",
|
|
order: s.order,
|
|
port: s.port,
|
|
https: s.https ? "true" : "false",
|
|
url: s.url,
|
|
deviceHostname: device?.hostname ?? "",
|
|
deviceIp: device?.ip ?? "",
|
|
};
|
|
});
|
|
|
|
return [...deviceRows, ...serviceRows];
|
|
}
|
|
|
|
export async function transferRoutes(app: FastifyInstance): Promise<void> {
|
|
app.get("/api/transfer/export", async (request, reply) => {
|
|
const query = request.query as { format?: string };
|
|
const format = query.format === "xlsx" ? "xlsx" : query.format === "json" ? "json" : "csv";
|
|
const rows = buildExportRows();
|
|
|
|
if (format === "json") {
|
|
reply.header("Content-Disposition", 'attachment; filename="launchpad-export.json"');
|
|
reply.type("application/json");
|
|
return rows;
|
|
}
|
|
|
|
const worksheet = XLSX.utils.json_to_sheet(rows);
|
|
|
|
if (format === "xlsx") {
|
|
const workbook = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, "LaunchPad");
|
|
const buffer = XLSX.write(workbook, { type: "buffer", bookType: "xlsx" }) as Buffer;
|
|
reply.header("Content-Disposition", 'attachment; filename="launchpad-export.xlsx"');
|
|
reply.type("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
|
return reply.send(buffer);
|
|
}
|
|
|
|
const csv = XLSX.utils.sheet_to_csv(worksheet);
|
|
reply.header("Content-Disposition", 'attachment; filename="launchpad-export.csv"');
|
|
reply.type("text/csv; charset=utf-8");
|
|
return reply.send(csv);
|
|
});
|
|
|
|
app.post("/api/transfer/import", async (request, reply) => {
|
|
const body = request.body as { format?: string; content?: string } | undefined;
|
|
if (!body?.content) {
|
|
return reply.code(400).send({ error: "Kein Dateiinhalt übermittelt" });
|
|
}
|
|
|
|
let rows: Record<string, unknown>[];
|
|
try {
|
|
if (body.format === "json") {
|
|
const text = Buffer.from(body.content, "base64").toString("utf-8");
|
|
rows = JSON.parse(text);
|
|
} else {
|
|
const buffer = Buffer.from(body.content, "base64");
|
|
const workbook = XLSX.read(buffer, { type: "buffer" });
|
|
const sheetName = workbook.SheetNames[0];
|
|
rows = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName], { defval: "" });
|
|
}
|
|
} catch (err) {
|
|
return reply.code(400).send({
|
|
error: "Datei konnte nicht gelesen werden",
|
|
detail: err instanceof Error ? err.message : String(err),
|
|
});
|
|
}
|
|
|
|
let devicesImported = 0;
|
|
let devicesSkipped = 0;
|
|
let servicesImported = 0;
|
|
let servicesSkipped = 0;
|
|
const errors: string[] = [];
|
|
|
|
// Geräte-Zeilen zuerst verarbeiten, damit Dienst-Zeilen aus derselben
|
|
// Datei sie bereits über deviceHostname/deviceIp finden können.
|
|
const deviceRows = rows.filter((r) => String(r.type ?? "").trim() === "device");
|
|
const serviceRows = rows.filter((r) => String(r.type ?? "").trim() === "service");
|
|
|
|
for (const row of deviceRows) {
|
|
try {
|
|
const hostname = String(row.hostname ?? row.displayName ?? "").trim();
|
|
const ip = String(row.ip ?? "").trim();
|
|
if (!hostname || !ip) {
|
|
errors.push(`Geräte-Zeile übersprungen (hostname/ip fehlt): ${JSON.stringify(row)}`);
|
|
continue;
|
|
}
|
|
|
|
const allDevices = deviceRepo.listDevices();
|
|
const existing =
|
|
allDevices.find((d) => d.ip === ip) ?? allDevices.find((d) => d.hostname === hostname);
|
|
|
|
if (existing) {
|
|
devicesSkipped++;
|
|
continue;
|
|
}
|
|
|
|
deviceRepo.createDevice({
|
|
hostname,
|
|
ip,
|
|
mac: row.mac ? String(row.mac).trim() : undefined,
|
|
manufacturer: row.manufacturer ? String(row.manufacturer).trim() : undefined,
|
|
model: row.model ? String(row.model).trim() : undefined,
|
|
});
|
|
devicesImported++;
|
|
} catch (err) {
|
|
errors.push(err instanceof Error ? err.message : String(err));
|
|
}
|
|
}
|
|
|
|
for (const row of serviceRows) {
|
|
try {
|
|
const deviceHostname = String(row.deviceHostname ?? "").trim();
|
|
const deviceIp = String(row.deviceIp ?? "").trim();
|
|
const hostname = String(row.hostname ?? deviceHostname).trim();
|
|
const port = Number(row.port);
|
|
const displayName = String(row.displayName ?? "").trim();
|
|
const url = String(row.url ?? "").trim();
|
|
|
|
if (!displayName || !hostname || !port || !url) {
|
|
errors.push(`Dienst-Zeile übersprungen (Pflichtfelder fehlen): ${JSON.stringify(row)}`);
|
|
continue;
|
|
}
|
|
|
|
const allDevices = deviceRepo.listDevices();
|
|
let device = deviceIp ? allDevices.find((d) => d.ip === deviceIp) : undefined;
|
|
if (!device && deviceHostname) {
|
|
device = allDevices.find((d) => d.hostname === deviceHostname);
|
|
}
|
|
if (!device) {
|
|
device = deviceRepo.createDevice({
|
|
hostname: deviceHostname || hostname,
|
|
ip: deviceIp || hostname,
|
|
});
|
|
devicesImported++;
|
|
}
|
|
|
|
const existingService = serviceRepo
|
|
.listServicesByDevice(device.id)
|
|
.find((s) => s.port === port);
|
|
if (existingService) {
|
|
servicesSkipped++;
|
|
continue;
|
|
}
|
|
|
|
const category = row.category ? String(row.category).trim() : undefined;
|
|
|
|
serviceRepo.createService({
|
|
deviceId: device.id,
|
|
displayName,
|
|
hostname,
|
|
url,
|
|
https: String(row.https ?? "").toLowerCase() === "true",
|
|
port,
|
|
category: category || undefined,
|
|
alias: row.alias
|
|
? String(row.alias)
|
|
.split(";")
|
|
.map((a) => a.trim())
|
|
.filter(Boolean)
|
|
: undefined,
|
|
favorite: String(row.favorite ?? "").toLowerCase() === "true",
|
|
order: row.order !== undefined && row.order !== "" ? Number(row.order) : undefined,
|
|
visible:
|
|
row.visible !== undefined && row.visible !== ""
|
|
? String(row.visible).toLowerCase() === "true"
|
|
: undefined,
|
|
});
|
|
|
|
if (category) {
|
|
categoryRepo.ensureCategory(category);
|
|
}
|
|
|
|
servicesImported++;
|
|
} catch (err) {
|
|
errors.push(err instanceof Error ? err.message : String(err));
|
|
}
|
|
}
|
|
|
|
return {
|
|
devicesImported,
|
|
devicesSkipped,
|
|
servicesImported,
|
|
servicesSkipped,
|
|
errors,
|
|
};
|
|
});
|
|
}
|