104 lines
2.5 KiB
TypeScript
104 lines
2.5 KiB
TypeScript
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<string, string>
|
|
}
|
|
|
|
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<string, string> = {
|
|
...(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),
|
|
}
|
|
}
|