🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
95 lines
2.6 KiB
TypeScript
95 lines
2.6 KiB
TypeScript
import { createReadStream, statSync } from 'fs'
|
|
import { join } from 'path'
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const url = getRequestURL(event)
|
|
|
|
// Solo manejar rutas que empiecen con /videos/
|
|
if (!url.pathname.startsWith('/videos/')) {
|
|
return
|
|
}
|
|
|
|
// Extraer el path del archivo (puede incluir subcarpetas para HLS)
|
|
const file = url.pathname.substring('/videos/'.length)
|
|
|
|
if (!file) {
|
|
return
|
|
}
|
|
|
|
// Validar que el path solo contenga caracteres seguros (permite subcarpetas con /)
|
|
if (!/^[a-zA-Z0-9_\-\.\/]+$/.test(file)) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: 'Invalid file path'
|
|
})
|
|
}
|
|
|
|
const filePath = join(process.cwd(), 'videos', file)
|
|
|
|
try {
|
|
// Verificar que el archivo existe
|
|
const stats = statSync(filePath)
|
|
|
|
if (!stats.isFile()) {
|
|
throw createError({
|
|
statusCode: 404,
|
|
statusMessage: 'File not found'
|
|
})
|
|
}
|
|
|
|
// Determinar el tipo MIME basado en la extensión
|
|
const ext = file.toLowerCase().substring(file.lastIndexOf('.'))
|
|
const mimeTypes: Record<string, string> = {
|
|
'.mp4': 'video/mp4',
|
|
'.webm': 'video/webm',
|
|
'.ogg': 'video/ogg',
|
|
'.mov': 'video/quicktime',
|
|
'.avi': 'video/x-msvideo',
|
|
'.mkv': 'video/x-matroska',
|
|
'.m3u8': 'application/vnd.apple.mpegurl',
|
|
'.ts': 'video/mp2t'
|
|
}
|
|
const mimeType = mimeTypes[ext] || 'application/octet-stream'
|
|
|
|
// Configurar headers para streaming de video
|
|
setResponseHeaders(event, {
|
|
'Content-Type': mimeType,
|
|
'Content-Length': String(stats.size),
|
|
'Accept-Ranges': 'bytes',
|
|
'Cache-Control': 'public, max-age=604800' // 7 días
|
|
})
|
|
|
|
// Manejar range requests para seeking en el video
|
|
const range = getRequestHeader(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
|
|
const chunkSize = (end - start) + 1
|
|
|
|
setResponseStatus(event, 206) // Partial Content
|
|
setResponseHeaders(event, {
|
|
'Content-Range': `bytes ${start}-${end}/${stats.size}`,
|
|
'Content-Length': String(chunkSize)
|
|
})
|
|
|
|
return createReadStream(filePath, { start, end })
|
|
}
|
|
|
|
// Retornar el archivo completo si no hay range request
|
|
return createReadStream(filePath)
|
|
} catch (error: any) {
|
|
if (error.code === 'ENOENT') {
|
|
throw createError({
|
|
statusCode: 404,
|
|
statusMessage: 'File not found'
|
|
})
|
|
}
|
|
throw createError({
|
|
statusCode: 500,
|
|
statusMessage: 'Error reading file'
|
|
})
|
|
}
|
|
})
|