Migrated admin and media APIs to Nitro

This commit is contained in:
lucast committed 2026-09-29 01:22:29 +02:00
1 parent ee5809a489
commit bca41b5e6f
18 files changed
+786 -1

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
import { readMultipartFormData, type H3Event, type MultiPartData } from "h3"
import { formFieldText, getFormField, getFormFieldValues } from "./upload-files"
export async function readAdminForm(event: H3Event) {
return (await readMultipartFormData(event)) ?? []
}
export function adminFormText(parts: MultiPartData[], key: string) {
return formFieldText(getFormField(parts, key))
}
export function adminFormTexts(parts: MultiPartData[], key: string) {
return getFormFieldValues(parts, key).map((part) => part.data.toString("utf8"))
}
export function adminFormFile(parts: MultiPartData[], key: string) {
const part = getFormField(parts, key)
if (!part?.filename) return undefined
return { name: part.filename, type: part.type ?? "", data: part.data }
}
export function splitIds(value: string | undefined) {
return value === undefined ? undefined : value.split(",").map((id) => id.trim()).filter(Boolean)
}
export function safeFileExtension(fileName: string) {
return fileName.split(".")[1]?.replace(/[^A-Za-z0-9]/g, "") ?? ""
}
export function parseOptionalJson(value: string | undefined) {
if (value === undefined) return { ok: true as const, value: undefined }
try {
return { ok: true as const, value: JSON.parse(value) as unknown }
} catch {
return { ok: false as const }
}
}
+47
View File
@@ -0,0 +1,47 @@
import { unlink } from "node:fs/promises"
import path from "node:path"
import Ffmpeg from "fluent-ffmpeg"
import type { H3Event } from "h3"
import { resolveUploadFile } from "./upload-files"
export async function getAudioDuration(event: H3Event, relativePath: string): Promise<number> {
if (!relativePath) throw new Error("filePath is undefined")
const { path: sourcePath } = await resolveUploadFile(event, relativePath)
return new Promise((resolve, reject) => {
Ffmpeg.ffprobe(sourcePath, (error, metadata) => {
if (error) return reject(0)
if (metadata.format.duration) return resolve(metadata.format.duration)
reject(0)
})
})
}
export async function processAudioFile(event: H3Event, relativePath: string) {
if (!relativePath) throw new Error("filePath is undefined")
const { path: sourcePath } = await resolveUploadFile(event, relativePath)
const directory = path.dirname(sourcePath)
const jobs: Array<[string, string, string]> = [
["128k", "mp3", "libmp3lame"],
["256k", "mp3", "libmp3lame"],
["96k", "mp4", "aac"],
["192k", "mp4", "aac"],
["320k", "mp4", "aac"],
]
const names = ["low.mp3", "mid.mp3", "low.m4a", "mid.m4a", "high.m4a"]
await Promise.all(
jobs.map(([bitrate, format, codec], index) =>
new Promise<void>((resolve, reject) => {
Ffmpeg(sourcePath)
.noVideo()
.audioBitrate(bitrate)
.toFormat(format)
.audioCodec(codec)
.on("end", () => resolve())
.on("error", reject)
.save(path.join(directory, names[index]!))
}),
),
)
await unlink(sourcePath)
}
+121
View File
@@ -0,0 +1,121 @@
import { mkdir, realpath, rm, writeFile } from "node:fs/promises"
import path from "node:path"
import type { H3Event, MultiPartData } from "h3"
import { getBackendRuntimeConfig } from "./backend-config"
export class InvalidUploadPathError extends Error {
constructor() {
super("filePath is invalid")
this.name = "InvalidUploadPathError"
}
}
export class UploadFileNotFoundError extends Error {
code = "ENOENT"
constructor() {
super("File or directory doesn't exist")
this.name = "UploadFileNotFoundError"
}
}
function rootDirectory(event: H3Event) {
const configured = getBackendRuntimeConfig(event).uploadDir
if (!configured) throw new Error("Environment variable UPLOAD_DIR not found")
return path.resolve(configured)
}
function isWithin(root: string, target: string) {
const relative = path.relative(root, target)
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
}
function resolveLexically(root: string, relativePath: string) {
if (!relativePath || path.isAbsolute(relativePath)) throw new InvalidUploadPathError()
const target = path.resolve(root, relativePath)
if (!isWithin(root, target)) throw new InvalidUploadPathError()
return target
}
export async function resolveUploadFile(event: H3Event, relativePath: string) {
const root = rootDirectory(event)
const target = resolveLexically(root, relativePath)
// Resolve the configured root separately: a missing/unreadable upload root is
// an operational failure, while a missing requested file is a normal 404.
const realRoot = await realpath(root)
let realTarget: string
try {
realTarget = await realpath(target)
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") throw new UploadFileNotFoundError()
throw error
}
if (!isWithin(realRoot, realTarget)) throw new InvalidUploadPathError()
return { root: realRoot, path: realTarget }
}
export async function saveUploadFile(event: H3Event, relativePath: string, data: Buffer) {
const root = rootDirectory(event)
const target = resolveLexically(root, relativePath)
await mkdir(path.dirname(target), { recursive: true })
const realRoot = await realpath(root)
const realParent = await realpath(path.dirname(target))
if (!isWithin(realRoot, realParent)) throw new InvalidUploadPathError()
try {
const existing = await realpath(target)
if (!isWithin(realRoot, existing)) throw new InvalidUploadPathError()
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
}
await writeFile(target, data)
}
export async function cleanUploadFileOrDirectory(event: H3Event, relativePath: string) {
const root = rootDirectory(event)
const target = resolveLexically(root, relativePath)
let realTarget: string
try {
const [realRoot, resolved] = await Promise.all([realpath(root), realpath(target)])
if (!isWithin(realRoot, resolved)) throw new InvalidUploadPathError()
realTarget = resolved
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
console.error("File or directory doesn't exist")
return
}
throw error
}
try {
await rm(realTarget, { recursive: true })
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
console.error("File or directory doesn't exist")
return
}
throw error
}
}
export async function tryCleanUploadFileOrDirectory(event: H3Event, relativePath: string) {
try {
await cleanUploadFileOrDirectory(event, relativePath)
} catch (error) {
console.error("Unable to clean uploaded file or directory", error)
}
}
export function getFormField(parts: MultiPartData[] | undefined, name: string) {
return parts?.find((part) => part.name === name)
}
export function getFormFieldValues(parts: MultiPartData[] | undefined, name: string) {
return parts?.filter((part) => part.name === name) ?? []
}
export function formFieldText(part: MultiPartData | undefined) {
return part ? part.data.toString("utf8") : undefined
}
export function formFieldFile(part: MultiPartData | undefined) {
if (!part?.filename) return undefined
return { name: part.filename, type: part.type ?? "", data: part.data }
}