Tenant-Importe per Stream in den Object Storage laden
All checks were successful
Build and Push Docker Images / build-frontend (push) Successful in 21s
Build and Push Docker Images / build-central-services-api (push) Successful in 22s
Build and Push Docker Images / build-central-services-admin (push) Successful in 21s
Build and Push Docker Images / build-docs (push) Successful in 21s
Build and Push Docker Images / build-backend (push) Successful in 43s
Build and Push Docker Images / build-website (push) Successful in 21s
All checks were successful
Build and Push Docker Images / build-frontend (push) Successful in 21s
Build and Push Docker Images / build-central-services-api (push) Successful in 22s
Build and Push Docker Images / build-central-services-admin (push) Successful in 21s
Build and Push Docker Images / build-docs (push) Successful in 21s
Build and Push Docker Images / build-backend (push) Successful in 43s
Build and Push Docker Images / build-website (push) Successful in 21s
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||||
import { and, eq, inArray, isNull, sql } from "drizzle-orm";
|
import { and, eq, inArray, isNull, sql } from "drizzle-orm";
|
||||||
import multipart from "@fastify/multipart";
|
import multipart from "@fastify/multipart";
|
||||||
import { GetObjectCommand } from "@aws-sdk/client-s3";
|
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
authTenantUsers,
|
authTenantUsers,
|
||||||
@@ -463,7 +464,7 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
const startTenantImportJob = async (
|
const startTenantImportJob = async (
|
||||||
jobId: string,
|
jobId: string,
|
||||||
currentUser: { id: string; email: string },
|
currentUser: { id: string; email: string },
|
||||||
source: { archiveBuffer: Buffer } | { exportData: TenantFullExport }
|
source: { archiveStoragePath: string } | { exportData: TenantFullExport }
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
await server.db
|
await server.db
|
||||||
@@ -487,9 +488,19 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
.where(eq(tenantExportJobs.id, jobId));
|
.where(eq(tenantExportJobs.id, jobId));
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = "archiveBuffer" in source
|
let result: Awaited<ReturnType<typeof importTenantFullExport>>;
|
||||||
? await importTenantFullExportArchive(server, source.archiveBuffer, { onProgress })
|
if ("archiveStoragePath" in source) {
|
||||||
: await importTenantFullExport(server, source.exportData, { onProgress });
|
const object = await s3.send(new GetObjectCommand({
|
||||||
|
Bucket: secrets.S3_BUCKET,
|
||||||
|
Key: source.archiveStoragePath,
|
||||||
|
}));
|
||||||
|
if (!object.Body) throw new Error("Importarchiv konnte nicht aus dem Speicher gelesen werden");
|
||||||
|
|
||||||
|
const archiveBuffer = Buffer.from(await object.Body.transformToByteArray());
|
||||||
|
result = await importTenantFullExportArchive(server, archiveBuffer, { onProgress });
|
||||||
|
} else {
|
||||||
|
result = await importTenantFullExport(server, source.exportData, { onProgress });
|
||||||
|
}
|
||||||
const access = await completeImportedTenantAccess(currentUser, result);
|
const access = await completeImportedTenantAccess(currentUser, result);
|
||||||
const importResult = { success: true, ...access, ...result };
|
const importResult = { success: true, ...access, ...result };
|
||||||
|
|
||||||
@@ -517,6 +528,15 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
})
|
})
|
||||||
.where(eq(tenantExportJobs.id, jobId));
|
.where(eq(tenantExportJobs.id, jobId));
|
||||||
|
} finally {
|
||||||
|
if ("archiveStoragePath" in source) {
|
||||||
|
await s3.send(new DeleteObjectCommand({
|
||||||
|
Bucket: secrets.S3_BUCKET,
|
||||||
|
Key: source.archiveStoragePath,
|
||||||
|
})).catch((cleanupError) => {
|
||||||
|
console.error("ERROR cleanup tenant import archive:", cleanupError);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1425,12 +1445,13 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
// POST /admin/tenant-imports
|
// POST /admin/tenant-imports
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
server.post("/admin/tenant-imports", { bodyLimit: 1024 * 1024 * 1024 }, async (req, reply) => {
|
server.post("/admin/tenant-imports", { bodyLimit: 1024 * 1024 * 1024 }, async (req, reply) => {
|
||||||
|
let archiveStoragePath: string | null = null;
|
||||||
try {
|
try {
|
||||||
const currentUser = await requireAdmin(req, reply);
|
const currentUser = await requireAdmin(req, reply);
|
||||||
if (!currentUser) return;
|
if (!currentUser) return;
|
||||||
|
|
||||||
const isMultipart = req.headers["content-type"]?.includes("multipart/form-data");
|
const isMultipart = req.headers["content-type"]?.includes("multipart/form-data");
|
||||||
let source!: { archiveBuffer: Buffer } | { exportData: TenantFullExport };
|
let source!: { archiveStoragePath: string } | { exportData: TenantFullExport };
|
||||||
let filename = "tenant-export.json";
|
let filename = "tenant-export.json";
|
||||||
let fileSize: number | null = null;
|
let fileSize: number | null = null;
|
||||||
let targetTenantId: number | null = null;
|
let targetTenantId: number | null = null;
|
||||||
@@ -1439,9 +1460,7 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
const data: any = await req.file();
|
const data: any = await req.file();
|
||||||
if (!data?.file) return reply.code(400).send({ error: "export file required" });
|
if (!data?.file) return reply.code(400).send({ error: "export file required" });
|
||||||
|
|
||||||
const archiveBuffer = await data.toBuffer();
|
|
||||||
filename = data.filename || "tenant-export.fedeo-export.zip";
|
filename = data.filename || "tenant-export.fedeo-export.zip";
|
||||||
fileSize = archiveBuffer.length;
|
|
||||||
targetTenantId = Number(data.fields?.targetTenantId?.value || 0) || null;
|
targetTenantId = Number(data.fields?.targetTenantId?.value || 0) || null;
|
||||||
if (targetTenantId) {
|
if (targetTenantId) {
|
||||||
return reply.code(409).send({
|
return reply.code(409).send({
|
||||||
@@ -1449,7 +1468,15 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
source = { archiveBuffer };
|
archiveStoragePath = `tenant-imports/${randomUUID()}-${filename}`;
|
||||||
|
await s3.send(new PutObjectCommand({
|
||||||
|
Bucket: secrets.S3_BUCKET,
|
||||||
|
Key: archiveStoragePath,
|
||||||
|
Body: data.file,
|
||||||
|
ContentType: data.mimetype || "application/zip",
|
||||||
|
}));
|
||||||
|
fileSize = data.file.bytesRead || null;
|
||||||
|
source = { archiveStoragePath };
|
||||||
} else {
|
} else {
|
||||||
const body = req.body as TenantFullExport | { exportData?: TenantFullExport; targetTenantId?: number };
|
const body = req.body as TenantFullExport | { exportData?: TenantFullExport; targetTenantId?: number };
|
||||||
const exportData = "format" in body ? body : body.exportData;
|
const exportData = "format" in body ? body : body.exportData;
|
||||||
@@ -1493,6 +1520,14 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("ERROR /admin/tenant-imports:", err);
|
console.error("ERROR /admin/tenant-imports:", err);
|
||||||
|
if (archiveStoragePath) {
|
||||||
|
await s3.send(new DeleteObjectCommand({
|
||||||
|
Bucket: secrets.S3_BUCKET,
|
||||||
|
Key: archiveStoragePath,
|
||||||
|
})).catch((cleanupError) => {
|
||||||
|
console.error("ERROR cleanup tenant import archive:", cleanupError);
|
||||||
|
});
|
||||||
|
}
|
||||||
const message = err?.message || "Internal Server Error";
|
const message = err?.message || "Internal Server Error";
|
||||||
const statusCode = message.includes("Tenant mit dieser ID existiert bereits") ? 409 : 500;
|
const statusCode = message.includes("Tenant mit dieser ID existiert bereits") ? 409 : 500;
|
||||||
return reply.code(statusCode).send({ error: message });
|
return reply.code(statusCode).send({ error: message });
|
||||||
|
|||||||
Reference in New Issue
Block a user