Next.js 返回文件资源
explorer-manager
新增依赖
pnpm i ext-name file-type
- file-type:通过 buffer 来读取文件信息
- ext-name:通过文件后缀名返回文件信息
explorer-manager/src/main.mjs 新增方法
fsStat
单独获取文件 stat
信息
export const fsStat = (path) => {
return fs.statSync(formatPath(path))
}
getFileDetail
获取文件信息,当 file-type 获取失败时使用 ext-name 分析文件后缀名返回信息。
用于设置 http header 的 Content-Type 字段。
export const getFileDetail = async (path = '') => {
const file_path = formatPath(path)
return await fileTypeFromFile(formatPath(path)).then((info) => {
if (info) {
return info
} else {
return extName(file_path)
}
})
}
fsStream
将文件 ReadStream 转换成 Response 可接受的 ReadableStream。
//https://github.com/vercel/next.js/discussions/15453#discussioncomment-6565699
const nodeStreamToIterator = async function* (stream) {
for await (const chunk of stream) {
yield chunk
}
}
const iteratorToStream = (iterator) => {
return new ReadableStream({
async pull(controller) {
const { value, done } = await iterator.next()
if (done) {
controller.close()
} else {
controller.enqueue(value)
}
},
})
}
/**
* @param path {string}
* @param option {StreamOptions}
* @returns {ReadableStream<*>|Buffer}
*/
export const fsStream = (path = '.', option = {}) => {
if (fs.statSync(formatPath(path)).isFile()) {
return iteratorToStream(nodeStreamToIterator(fs.createReadStream(formatPath(path), option)))
} else {
return Buffer.from('')
}
}
explorer
安装依赖
pnpm i lodash etag
pnpm i @types/lodash @types/etag -D
返回文件路由:explorer/src/app/static/[[...path]]/route.ts
- 通过 getFileDetail 方法获取文件信息,并赋值给 headers 的 Content-Type。让浏览器正确识别文件信息
- 将文件名配置给 Content-Disposition 属性。当触发浏览器下载时,正确设置下载的默认文件名
- 设置 Cache-Control 过期时间 180 天
- 将文件大小赋值给 Content-Length 属性
- 判断 req 内 headers 是否包含 Range 属性。当拥有该属性,则为 video 播放读取视频文件。需要对应的设置到 Accept-Ranges、Content-Range、Content-Length 属性。并根据 Range 值的字符串"bytes=2662563840-4810047486"解析出 “bytes=[start]-[end]” start 至 end 的长度数据,传递至 fsStream 方法的 option 内,让 fs.createReadStream 方法只读取该区间内的数据长度。http status 需要设置为 206,否则视频会播放失败或快进失败。
- 使用 stream 的形式将数据返回给客户端。
import { NextRequest, NextResponse } from 'next/server'
import { fsStat, fsStream, getFileDetail } from '@/explorer-manager/src/main.mjs'
import etag from 'etag'
import { extend } from 'lodash'
import sys_path from 'path'
export const GET = async (req: NextRequest, { params: { path } }: { params: { path: string[] } }) => {
const static_path = '/' + path.join('/')
const file_type = await getFileDetail(static_path)
const filename = sys_path.basename(static_path)
const stat = fsStat(static_path)
const headers = {
'Content-Type': file_type.mime,
'Content-Disposition': `filename=${encodeURIComponent(filename || 'download')}`,
'Cache-Control': `public,max-age=${60 * 60 * 24 * 30 * 6},must-revalidate`,
'Content-Length': stat.size.toString(),
ETag: etag(Date.now().toString()),
'Last-Modified': stat.mtime.toString(),
}
const option: { start?: number; end?: number } = {}
const range = req.headers.get('Range') || '' //如果是video标签发起的请求就不会为null
if (range) {
const positions = range.replace(/bytes=/, '').split('-')
const start = positions[0] parseInt(positions[0], 10) : 0
const total = stat.size
const end = positions[1] parseInt(positions[1], 10) : total - 1
const chunk_size = end - start + 1
extend(headers, {
'Accept-Ranges': 'bytes',
'Content-Range': `bytes ${start}-${end}/${total}`,
'Content-Length': chunk_size.toString(),
})
option.start = start
option.end = end
}
const fs_stream = fsStream(static_path, option)
return new NextResponse(fs_stream, {
status: range 206 : 200,
headers: headers,
})
}
到此,Next.js 返回文件的方法完成。
可通过访问 domain/static/xxx1/xxx2 的格式以 stream 的形式获取到文件
git-repo
yangWs29/share-explorer