import { FastifyInstance } from "fastify" import multipart from "@fastify/multipart" import { s3 } from "../utils/s3" import { GetObjectCommand } from "@aws-sdk/client-s3" import { getSignedUrl } from "@aws-sdk/s3-request-presigner" import archiver from "archiver" import { secrets } from "../utils/secrets" import { saveFile } from "../utils/files" import { eq, inArray } from "drizzle-orm" import { and } from "drizzle-orm" import { authProfiles, files, createddocuments, customers } from "../../db/schema" export default async function fileRoutes(server: FastifyInstance) { const getPortalCustomerId = async (req: any) => { const tenantId = req.user?.tenant_id const userId = req.user?.user_id if (!tenantId || !userId) return null const [profile] = await server.db .select({ customer_for_portal: authProfiles.customer_for_portal }) .from(authProfiles) .where(and( eq(authProfiles.tenant_id, tenantId), eq(authProfiles.user_id, userId) )) .limit(1) return profile?.customer_for_portal || null } const loadSingleFileForRequest = async (req: any, id: string) => { const tenantId = req.user?.tenant_id if (!tenantId) return null const portalCustomerId = await getPortalCustomerId(req) if (!portalCustomerId) { const rows = await server.db .select() .from(files) .where(and(eq(files.id, id), eq(files.tenant, tenantId))) return rows[0] || null } const rows = await server.db .select({ file: files, }) .from(files) .leftJoin(createddocuments, eq(files.createddocument, createddocuments.id)) .where(and( eq(files.id, id), eq(files.tenant, tenantId), eq(createddocuments.customer, portalCustomerId), eq(createddocuments.availableInPortal, true) )) .limit(1) return rows[0]?.file || null } // ------------------------------------------------------------- // 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) : {} const { folder = null, type = null, ...otherMeta } = meta const created = await saveFile( server, tenantId, null, { filename: data.filename, content: fileBuffer, contentType: data.mimetype }, folder, type, otherMeta ) if (!created) throw new Error("Could not create DB entry") return { id: created.id, filename: created.filename, path: created.key } } 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 file = await loadSingleFileForRequest(req, id) 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 //@ts-ignore .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 } //@ts-ignore const ids = req.body?.ids || [] // ------------------------------------------------- // 1️⃣ SINGLE DOWNLOAD // ------------------------------------------------- if (id) { const file = await loadSingleFileForRequest(req, id) 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 file = await loadSingleFileForRequest(req, id) 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 { files: [] } } 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" }) } }) }