flnxi-Wu/lib/local-file-stream.ts
2026-05-25 15:36:51 +08:00

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),
}
}