import { createReadStream } from 'fs' import { stat } from 'fs/promises' import { NextResponse } from 'next/server' import { Readable } from 'stream' interface LocalFileStreamResponseOptions { contentType: string range?: string | null headers?: Record } interface ByteRange { start: number end: number } export async function createLocalFileStreamResponse( filePath: string, options: LocalFileStreamResponseOptions ) { const fileStat = await stat(filePath) const fileSize = fileStat.size const baseHeaders: Record = { ...(options.headers || {}), 'Content-Type': options.contentType, 'Accept-Ranges': 'bytes', } const range = options.range?.trim() if (range) { const parsedRange = parseByteRange(range, fileSize) if (!parsedRange) { return new NextResponse(null, { status: 416, headers: { ...baseHeaders, 'Content-Range': `bytes */${fileSize}`, }, }) } const { start, end } = parsedRange const stream = createReadStream(filePath, { start, end }) const chunkSize = end - start + 1 return new NextResponse(Readable.toWeb(stream) as any, { status: 206, headers: { ...baseHeaders, 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Content-Length': chunkSize.toString(), }, }) } const stream = createReadStream(filePath) return new NextResponse(Readable.toWeb(stream) as any, { status: 200, headers: { ...baseHeaders, 'Content-Length': fileSize.toString(), }, }) } function parseByteRange(rangeHeader: string, fileSize: number): ByteRange | null { if (!Number.isFinite(fileSize) || fileSize < 0) return null const match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader) if (!match) return null const [, startText, endText] = match if (!startText && !endText) return null if (!startText) { const suffixLength = Number.parseInt(endText, 10) if (!Number.isFinite(suffixLength) || suffixLength <= 0 || fileSize === 0) return null return { start: Math.max(fileSize - suffixLength, 0), end: fileSize - 1, } } const start = Number.parseInt(startText, 10) const requestedEnd = endText ? Number.parseInt(endText, 10) : fileSize - 1 if ( !Number.isFinite(start) || !Number.isFinite(requestedEnd) || fileSize === 0 || start < 0 || requestedEnd < start || start >= fileSize ) { return null } return { start, end: Math.min(requestedEnd, fileSize - 1), } }