forked from BRETT/BRETT
159 lines
5.4 KiB
TypeScript
159 lines
5.4 KiB
TypeScript
import { mkdir, writeFile } from 'node:fs/promises'
|
|
import { basename, extname, join } from 'node:path'
|
|
import { z } from 'zod'
|
|
import { uploads } from '../../db/schema'
|
|
|
|
const MAX_SIZE = MAX_UPLOAD_SIZE
|
|
|
|
const MIME_EXT: Record<string, string> = {
|
|
'image/jpeg': '.jpg',
|
|
'image/png': '.png',
|
|
'image/gif': '.gif',
|
|
'image/webp': '.webp',
|
|
'image/avif': '.avif',
|
|
'video/mp4': '.mp4',
|
|
'video/webm': '.webm',
|
|
'video/quicktime': '.mov',
|
|
'audio/mpeg': '.mp3',
|
|
'audio/mp3': '.mp3',
|
|
'audio/flac': '.flac',
|
|
'audio/x-flac': '.flac',
|
|
}
|
|
|
|
// Fallback, wenn der Browser keinen MIME-Type mitschickt
|
|
const EXT_MIME: Record<string, string> = {
|
|
'.jpg': 'image/jpeg',
|
|
'.jpeg': 'image/jpeg',
|
|
'.png': 'image/png',
|
|
'.gif': 'image/gif',
|
|
'.webp': 'image/webp',
|
|
'.avif': 'image/avif',
|
|
'.svg': 'image/svg+xml',
|
|
'.mp4': 'video/mp4',
|
|
'.webm': 'video/webm',
|
|
'.mov': 'video/quicktime',
|
|
'.mp3': 'audio/mpeg',
|
|
'.flac': 'audio/flac',
|
|
}
|
|
|
|
const urlSchema = z.object({
|
|
url: z.url().max(2048),
|
|
})
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const user = await requireUser(event)
|
|
const db = useDb()
|
|
|
|
if ((getRequestHeader(event, 'content-type') ?? '').includes('application/json')) {
|
|
const body = await readValidatedBody(event, urlSchema.parse)
|
|
if (!/^https?:\/\//i.test(body.url)) {
|
|
throw createError({ statusCode: 400, statusMessage: 'Nur http(s)-URLs erlaubt' })
|
|
}
|
|
const type = classifyUrl(body.url)
|
|
const id = await newUploadId(db)
|
|
|
|
// Direkte Bild-/Video-URLs werden heruntergeladen und lokal gespeichert,
|
|
// als wäre die Datei direkt hochgeladen worden
|
|
if (type === 'image' || type === 'video' || type === 'audio') {
|
|
const media = await downloadExternalMedia(body.url)
|
|
const urlExt = extname(new URL(body.url).pathname).toLowerCase().replace(/[^a-z0-9.]/g, '').slice(0, 10)
|
|
const ext = MIME_EXT[media.mimeType] ?? urlExt
|
|
const fileName = `${id}${ext}`
|
|
await mkdir(uploadDir(), { recursive: true })
|
|
await writeFile(join(uploadDir(), fileName), media.data)
|
|
const audioMeta = media.type === 'audio'
|
|
? await extractAudioMeta(id, media.data, media.mimeType)
|
|
: null
|
|
await db.insert(uploads).values({
|
|
id,
|
|
userId: user.id,
|
|
type: media.type,
|
|
fileName,
|
|
originalName: decodeURIComponent(basename(new URL(body.url).pathname) || body.url).slice(0, 255),
|
|
mimeType: media.mimeType.slice(0, 100),
|
|
size: media.data.length,
|
|
previewImage: audioMeta?.cover ?? null,
|
|
audioMeta: audioMeta?.meta ?? null,
|
|
})
|
|
await ensureThreadSubscription(id, user.id)
|
|
return { id, type: media.type, url: `/uploads/${fileName}` }
|
|
}
|
|
|
|
const twitchTarget = type === 'twitch' ? getTwitchTarget(body.url) : null
|
|
const [preview, twitchPreview] = await Promise.all([
|
|
type === 'link' || type === 'twitch' ? fetchLinkPreview(body.url) : null,
|
|
twitchTarget ? fetchTwitchPreview(twitchTarget) : null,
|
|
])
|
|
const remotePreviewImage = twitchPreview?.image ?? preview?.image ?? null
|
|
const previewImage = remotePreviewImage
|
|
? (await savePreviewImage(id, remotePreviewImage)) ?? remotePreviewImage
|
|
: null
|
|
await db.insert(uploads).values({
|
|
id,
|
|
userId: user.id,
|
|
type,
|
|
externalUrl: body.url,
|
|
originalName: body.url.slice(0, 255),
|
|
previewImage,
|
|
previewTitle: twitchPreview?.title ?? preview?.title ?? null,
|
|
})
|
|
await ensureThreadSubscription(id, user.id)
|
|
return { id, type, url: body.url }
|
|
}
|
|
|
|
const parts = await readMultipartFormData(event)
|
|
const file = parts?.find(p => p.name === 'file')
|
|
|
|
if (!file?.data?.length) {
|
|
throw createError({ statusCode: 400, statusMessage: 'Keine Datei hochgeladen' })
|
|
}
|
|
|
|
// Manche Formate kommen ohne MIME-Type (z. B. .flac) — dann per Endung klassifizieren
|
|
const fileExt = extname(file.filename ?? '').toLowerCase()
|
|
const rawMime = file.type ?? ''
|
|
const mimeType = rawMime && rawMime !== 'application/octet-stream' ? rawMime : (EXT_MIME[fileExt] ?? rawMime)
|
|
const type = mimeType.startsWith('image/') ? 'image'
|
|
: mimeType.startsWith('video/') ? 'video'
|
|
: mimeType.startsWith('audio/') ? 'audio'
|
|
: IMAGE_EXT.test(fileExt) ? 'image'
|
|
: VIDEO_EXT.test(fileExt) ? 'video'
|
|
: AUDIO_EXT.test(fileExt) ? 'audio'
|
|
: null
|
|
if (!type) {
|
|
throw createError({ statusCode: 415, statusMessage: 'Nur Bilder, Videos und Audiodateien sind erlaubt' })
|
|
}
|
|
if (file.data.length > MAX_SIZE) {
|
|
throw createError({ statusCode: 413, statusMessage: 'Datei ist zu groß (max. 200 MB)' })
|
|
}
|
|
|
|
const ext = MIME_EXT[mimeType]
|
|
?? fileExt.replace(/[^a-z0-9.]/g, '').slice(0, 10)
|
|
?? ''
|
|
const id = await newUploadId(db)
|
|
const fileName = `${id}${ext}`
|
|
|
|
await mkdir(uploadDir(), { recursive: true })
|
|
await writeFile(join(uploadDir(), fileName), file.data)
|
|
const audioMeta = type === 'audio'
|
|
? await extractAudioMeta(id, file.data, mimeType)
|
|
: null
|
|
await db.insert(uploads).values({
|
|
id,
|
|
userId: user.id,
|
|
type,
|
|
fileName,
|
|
originalName: (file.filename ?? fileName).slice(0, 255),
|
|
mimeType: (mimeType || 'application/octet-stream').slice(0, 100),
|
|
size: file.data.length,
|
|
previewImage: audioMeta?.cover ?? null,
|
|
audioMeta: audioMeta?.meta ?? null,
|
|
})
|
|
|
|
await ensureThreadSubscription(id, user.id)
|
|
return {
|
|
id,
|
|
type,
|
|
url: `/uploads/${fileName}`,
|
|
}
|
|
})
|