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" export default async function fileRoutes(server: FastifyInstance) { await server.register(multipart,{ limits: { fileSize: 20 * 1024 * 1024, // 20 MB } }) server.post("/files/upload", async (req, reply) => { const tenantId = req.user?.tenant_id if (!tenantId) return reply.code(401).send({ error: "Unauthorized" }) const data:any = await req.file() const fileBuffer = await data.toBuffer() console.log(data) let meta = JSON.parse(data.fields?.meta?.value) if (!data.file) return reply.code(400).send({ error: "No file uploaded" }) const {data:createdFileData,error:createdFileError} = await server.supabase .from("files") .insert({ tenant: tenantId, }) .select() .single() if(createdFileError) { console.log(createdFileError) return reply.code(500).send({ error: "Internal Server Error" }) } else if(createdFileData && data.file) { const fileKey = `${tenantId}/filesbyid/${createdFileData.id}/${data.filename}` await s3.send(new PutObjectCommand({ Bucket: secrets.S3_BUCKET, Key: fileKey, Body: fileBuffer, ContentType: data.mimetype, })) //Update File with Corresponding Path const {data:updateFileData, error:updateFileError} = await server.supabase .from("files") .update({ ...meta, path: fileKey, }) .eq("id", createdFileData.id) if(updateFileError) { console.log(updateFileError) return reply.code(500).send({ error: "Internal Server Error" }) } else { /*const {data:tagData, error:tagError} = await server.supabase .from("filetagmembers") .insert(tags.map(tag => { return { file_id: createdFileData.id, tag_id: tag } }))*/ return { id: createdFileData.id, filename: data.filename, path: fileKey } } } }) server.get("/files/:id?", async (req, reply) => { const { id } = req.params as { id?: string } if(id) { try { const {data,error} = await server.supabase.from("files").select("*").eq("id", id).single() return {...data} } catch (err) { req.log.error(err); reply.code(500).send({ error: "Could not generate presigned URL" }); } } else { try { const {data:supabaseFileEntries,error} = await server.supabase.from("files").select("*, createddocument(*, customer(*))").eq("tenant",req.user.tenant_id) return { files: supabaseFileEntries } } catch (err) { req.log.error(err) reply.code(500).send({ error: "Could not generate presigned URLs" }) } } }) server.post("/files/download/:id?", async (req, reply) => { const { id } = req.params as { id?: string } // @ts-ignore const ids = req.body?.ids || [] try { if (id) { // 🔹 Einzeldownload const { data, error } = await server.supabase .from("files") .select("*") .eq("id", id) .single() if (error || !data) { return reply.code(404).send({ error: "File not found" }) } const command = new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: data.path, }) const { Body, ContentType } = await s3.send(command) const chunks: any[] = [] // @ts-ignore for await (const chunk of Body) { chunks.push(chunk) } const buffer = Buffer.concat(chunks) reply.header("Content-Type", ContentType || "application/octet-stream") reply.header( "Content-Disposition", `attachment; filename="${data.path.split("/").pop()}"` ) return reply.send(buffer) } console.log(ids) if (Array.isArray(ids) && ids.length > 0) { // 🔹 Multi-Download → ZIP zurückgeben const { data: supabaseFiles, error } = await server.supabase .from("files") .select("*") .in("id", ids) if (error || !supabaseFiles?.length) { return reply.code(404).send({ error: "Files not found" }) } console.log(supabaseFiles) reply.header("Content-Type", "application/zip") reply.header("Content-Disposition", "attachment; filename=dateien.zip") const archive = archiver("zip", { zlib: { level: 9 } }) archive.on("warning", console.warn) for (const entry of supabaseFiles) { const command = new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: entry.path, }) const { Body } = await s3.send(command) const filename = entry.path.split("/").pop() || entry.id console.log(filename) archive.append(Body as any, { name: filename }) } await archive.finalize() return reply.send(archive) } return reply.code(400).send({ error: "No id or ids provided" }) } catch (err) { console.log(err) reply.code(500).send({ error: "Download failed" }) } }) server.post("/files/presigned/:id?", async (req, reply) => { const { id } = req.params as { id: string }; const { ids } = req.body as { ids: string[] } if(id) { try { const {data,error} = await server.supabase.from("files").select("*").eq("id", id).single() const command = new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: data.path, }); // URL für 15 Minuten gültig const url = await getSignedUrl(s3, command, { expiresIn: 900 }); return { ...data, url }; } catch (err) { req.log.error(err); reply.code(500).send({ error: "Could not generate presigned URL" }); } } else { if (!Array.isArray(ids) || ids.length === 0) { return reply.code(400).send({ error: "No file keys provided" }) } try { const {data:supabaseFileEntries,error} = await server.supabase.from("files").select("*, createddocument(*, customer(*))").eq("tenant",req.user.tenant_id).is("archived",false) console.log(error) let filteredFiles = supabaseFileEntries.filter(i => ids.includes(i.id)) filteredFiles = filteredFiles.filter(i => i.path) console.log(filteredFiles.filter(i => !i.path)) let urls = await Promise.all( ids.map(async (id) => { let file = filteredFiles.find(i => i.id === id) if(!file) return let key = file.path if(!key) console.log(file) const command = new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: key, }) const url = await getSignedUrl(s3, command, { expiresIn: 900 }) // 15 min gültig return {...filteredFiles.find(i => i.id === id), url} }) ) urls = urls.filter(i => i) return { files: urls } } catch (err) { console.log(err) reply.code(500).send({ error: "Could not generate presigned URLs" }) } } }) }