import { FastifyInstance } from "fastify" import multipart from "@fastify/multipart" import { s3 } from "../utils/s3" import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3" import { getSignedUrl } from "@aws-sdk/s3-request-presigner" import archiver from "archiver" import { secrets } from "../utils/secrets" import { eq, inArray } from "drizzle-orm" import { files, createddocuments, customers } from "../../db/schema" export default async function fileRoutes(server: FastifyInstance) { // ------------------------------------------------------------- // MULTIPART INIT // ------------------------------------------------------------- await server.register(multipart, { limits: { fileSize: 20 * 1024 * 1024 } // 20 MB }) // ------------------------------------------------------------- // UPLOAD FILE // ------------------------------------------------------------- server.post("/files/upload", async (req, reply) => { try { const tenantId = req.user?.tenant_id if (!tenantId) return reply.code(401).send({ error: "Unauthorized" }) const data: any = await req.file() if (!data?.file) return reply.code(400).send({ error: "No file uploaded" }) const fileBuffer = await data.toBuffer() const meta = data.fields?.meta?.value ? JSON.parse(data.fields.meta.value) : {} // 1️⃣ DB-Eintrag erzeugen const inserted = await server.db .insert(files) .values({ tenant: tenantId }) .returning() const created = inserted[0] if (!created) throw new Error("Could not create DB entry") // 2️⃣ Datei in S3 speichern const fileKey = `${tenantId}/filesbyid/${created.id}/${data.filename}` await s3.send(new PutObjectCommand({ Bucket: secrets.S3_BUCKET, Key: fileKey, Body: fileBuffer, ContentType: data.mimetype })) // 3️⃣ DB updaten: meta + path await server.db .update(files) .set({ ...meta, path: fileKey }) .where(eq(files.id, created.id)) return { id: created.id, filename: data.filename, path: fileKey } } catch (err) { console.error(err) return reply.code(500).send({ error: "Upload failed" }) } }) // ------------------------------------------------------------- // GET FILE OR LIST FILES // ------------------------------------------------------------- server.get("/files/:id?", async (req, reply) => { try { const { id } = req.params as { id?: string } // 🔹 EINZELNE DATEI if (id) { const rows = await server.db .select() .from(files) .where(eq(files.id, id)) const file = rows[0] if (!file) return reply.code(404).send({ error: "Not found" }) return file } // 🔹 ALLE DATEIEN DES TENANTS (mit createddocument + customer) const tenantId = req.user?.tenant_id if (!tenantId) return reply.code(401).send({ error: "Unauthorized" }) const list = await server.db .select({ ...files, createddocument: createddocuments, customer: customers }) .from(files) .leftJoin( createddocuments, eq(files.createddocument, createddocuments.id) ) .leftJoin( customers, eq(createddocuments.customer, customers.id) ) .where(eq(files.tenant, tenantId)) return { files: list } } catch (err) { console.error(err) return reply.code(500).send({ error: "Could not load files" }) } }) // ------------------------------------------------------------- // DOWNLOAD (SINGLE OR MULTI ZIP) // ------------------------------------------------------------- server.post("/files/download/:id?", async (req, reply) => { try { const { id } = req.params as { id?: string } const ids = req.body?.ids || [] // ------------------------------------------------- // 1️⃣ SINGLE DOWNLOAD // ------------------------------------------------- if (id) { const rows = await server.db .select() .from(files) .where(eq(files.id, id)) const file = rows[0] if (!file) return reply.code(404).send({ error: "File not found" }) const command = new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: file.path! }) const { Body, ContentType } = await s3.send(command) const chunks: any[] = [] for await (const chunk of Body as any) chunks.push(chunk) const buffer = Buffer.concat(chunks) reply.header("Content-Type", ContentType || "application/octet-stream") reply.header("Content-Disposition", `attachment; filename="${file.path?.split("/").pop()}"`) return reply.send(buffer) } // ------------------------------------------------- // 2️⃣ MULTI DOWNLOAD → ZIP // ------------------------------------------------- if (Array.isArray(ids) && ids.length > 0) { const rows = await server.db .select() .from(files) .where(inArray(files.id, ids)) if (!rows.length) return reply.code(404).send({ error: "Files not found" }) reply.header("Content-Type", "application/zip") reply.header("Content-Disposition", `attachment; filename="dateien.zip"`) const archive = archiver("zip", { zlib: { level: 9 } }) for (const entry of rows) { const cmd = new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: entry.path! }) const { Body } = await s3.send(cmd) archive.append(Body as any, { name: entry.path?.split("/").pop() || entry.id }) } await archive.finalize() return reply.send(archive) } return reply.code(400).send({ error: "No id or ids provided" }) } catch (err) { console.error(err) return reply.code(500).send({ error: "Download failed" }) } }) // ------------------------------------------------------------- // GENERATE PRESIGNED URL(S) // ------------------------------------------------------------- server.post("/files/presigned/:id?", async (req, reply) => { try { const { id } = req.params as { id?: string } const { ids } = req.body as { ids?: string[] } const tenantId = req.user?.tenant_id if (!tenantId) return reply.code(401).send({ error: "Unauthorized" }) // ------------------------------------------------- // SINGLE FILE PRESIGNED URL // ------------------------------------------------- if (id) { const rows = await server.db .select() .from(files) .where(eq(files.id, id)) const file = rows[0] if (!file) return reply.code(404).send({ error: "Not found" }) const url = await getSignedUrl( s3, new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: file.path! }), { expiresIn: 900 } ) return { ...file, url } } else { // ------------------------------------------------- // MULTIPLE PRESIGNED URLs // ------------------------------------------------- if (!ids || !Array.isArray(ids) || ids.length === 0) { return reply.code(400).send({ error: "No ids provided" }) } const rows = await server.db .select() .from(files) .where(eq(files.tenant, tenantId)) const selected = rows.filter(f => ids.includes(f.id) && f.path) console.log(selected) const url = await getSignedUrl( s3, new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: selected[0].path! }), { expiresIn: 900 } ) console.log(url) console.log(selected.filter(f => !f.path)) const output = await Promise.all( selected.map(async (file) => { const url = await getSignedUrl( s3, new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: file.path! }), { expiresIn: 900 } ) return { ...file, url } }) ) return { files: output } } } catch (err) { console.error(err) return reply.code(500).send({ error: "Could not create presigned URLs" }) } }) }