319 lines
11 KiB
TypeScript
319 lines
11 KiB
TypeScript
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" })
|
||
}
|
||
})
|
||
|
||
}
|