Files
OpenSquawk/server/api/atc/audio/[...path].get.ts
itsrubberduck 27e0507bbd tts tut jetz
2025-09-15 11:58:11 +02:00

111 lines
3.5 KiB
TypeScript

// server/api/atc/audio/[...path].get.ts
import { createError } from "h3";
import { readFile, stat } from "node:fs/promises";
import { join } from "node:path";
import { existsSync } from "node:fs";
function outDir() {
const base = process.env.ATC_OUT_DIR?.trim() || join(process.cwd(), "storage", "atc");
return base;
}
export default defineEventHandler(async (event) => {
const path = getRouterParam(event, 'path');
if (!path || typeof path !== 'string') {
throw createError({
statusCode: 400,
statusMessage: "Invalid path"
});
}
// Sicherheitscheck: verhindere Directory Traversal
if (path.includes('..') || path.includes('/./') || path.startsWith('/')) {
throw createError({
statusCode: 400,
statusMessage: "Invalid path"
});
}
const filePath = join(outDir(), path);
if (!existsSync(filePath)) {
throw createError({
statusCode: 404,
statusMessage: "Audio file not found"
});
}
try {
const stats = await stat(filePath);
if (!stats.isFile()) {
throw createError({
statusCode: 400,
statusMessage: "Path is not a file"
});
}
// Nur Audio-Dateien servieren
const allowedExtensions = ['.wav', '.mp3', '.ogg'];
const hasValidExtension = allowedExtensions.some(ext => filePath.toLowerCase().endsWith(ext));
if (!hasValidExtension) {
throw createError({
statusCode: 400,
statusMessage: "Not an audio file"
});
}
const fileBuffer = await readFile(filePath);
// MIME Type basierend auf Dateiendung
let mimeType = 'audio/ogg';
if (filePath.endsWith('.wav')) {
mimeType = 'audio/wav';
} else if (filePath.endsWith('.mp3')) {
mimeType = 'audio/mpeg';
} else if (filePath.endsWith('.ogg')) {
mimeType = 'audio/ogg; codecs=opus';
}
// HTTP Headers für Audio-Streaming
setHeader(event, 'Content-Type', mimeType);
setHeader(event, 'Content-Length', stats.size.toString());
setHeader(event, 'Accept-Ranges', 'bytes');
setHeader(event, 'Cache-Control', 'public, max-age=3600'); // 1 Stunde Cache
setHeader(event, 'Access-Control-Allow-Origin', '*');
// Range-Request Support für Audio-Seeking
const range = getHeader(event, 'range');
if (range) {
const parts = range.replace(/bytes=/, "").split("-");
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : stats.size - 1;
if (start >= stats.size || end >= stats.size) {
setResponseStatus(event, 416); // Range Not Satisfiable
setHeader(event, 'Content-Range', `bytes */${stats.size}`);
return '';
}
const chunkSize = (end - start) + 1;
const chunk = fileBuffer.slice(start, end + 1);
setResponseStatus(event, 206); // Partial Content
setHeader(event, 'Content-Range', `bytes ${start}-${end}/${stats.size}`);
setHeader(event, 'Content-Length', chunkSize.toString());
return chunk;
}
return fileBuffer;
} catch (error) {
throw createError({
statusCode: 500,
statusMessage: `Failed to serve audio file: ${error}`
});
}
});