当前位置: 首页>后端>正文

从零开始-文件资源管理器-08-获取文件资源

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

  1. 通过 getFileDetail 方法获取文件信息,并赋值给 headers 的 Content-Type。让浏览器正确识别文件信息
  2. 将文件名配置给 Content-Disposition 属性。当触发浏览器下载时,正确设置下载的默认文件名
  3. 设置 Cache-Control 过期时间 180 天
  4. 将文件大小赋值给 Content-Length 属性
  5. 判断 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,否则视频会播放失败或快进失败。
  6. 使用 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


https://www.xamrdz.com/backend/3a21924790.html

相关文章: