Files
FEDEO/src/routes/files.ts
2025-12-07 22:27:57 +01:00

292 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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" })
}
})
}