如何在NestJS项目中为存放在其他服务器的视频创建createReadStream?
NestJS异机存储视频创建createReadStream实现方案
你当前的核心问题是应用服务器无法直接读取另一台服务器的本地文件系统路径,MySQL中存储的路径是视频存储服务器的本地路径,对应用服务器来说无效,必须先打通两台服务器的文件访问通道,再实现流式读取。以下是两种可直接落地的实现方案:
方案1:存储服务器部署轻量文件服务(最常用)
适合所有部署场景,权限可控性高
- 首先在存储视频+MySQL的服务器上,部署一个支持范围请求的HTTP只读文件接口,仅开放应用服务器内网IP访问权限,避免资源泄露
- 应用服务器端从MySQL拿到视频路径后,向存储服务器的文件接口发起流式请求,将响应体直接转为ReadStream即可使用
应用服务器代码示例(NestJS)
import { Controller, Get, Param, Req, Res } from '@nestjs/common'; import { Request, Response } from 'express'; import axios from 'axios'; @Controller('video') export class VideoController { @Get(':id') async getVideoStream(@Param('id') id: string, @Req() req: Request, @Res() res: Response) { // 1. 从MySQL查询视频在存储服务器的存储路径 const videoRelativePath = await this.videoService.getVideoPathById(id); // 2. 向存储服务器的文件服务发起流式请求,透传range头支持视频拖拽进度 const storageServerApi = `http://存储服务器内网IP:服务端口/serve-file?path=${encodeURIComponent(videoRelativePath)}`; const streamResp = await axios.get(storageServerApi, { responseType: 'stream', headers: { range: req.headers.range || '' } }); // 3. 透传响应头和流到客户端,也可直接将streamResp.data作为createReadStream的替代传入其他业务逻辑 res.set(streamResp.headers); streamResp.data.pipe(res); } }
存储服务器文件服务代码示例(NestJS)
import { Controller, Get, Query, Req, Res } from '@nestjs/common'; import { Request, Response } from 'express'; import * as fs from 'fs'; import * as path from 'path'; @Controller() export class FileServeController { // 提前定义视频存储的根目录,用于路径校验防止目录遍历攻击 private readonly VIDEO_ROOT = '/opt/video-storage'; @Get('serve-file') async serveVideo(@Query('path') filePath: string, @Req() req: Request, @Res() res: Response) { // 路径安全校验,禁止访问根目录外的文件 const safePath = path.resolve(this.VIDEO_ROOT, filePath.replace(/^(\.\.(\/|\\|$))+/, '')); if (!safePath.startsWith(this.VIDEO_ROOT) || !fs.existsSync(safePath)) { return res.status(403).send('非法路径'); } const fileStat = fs.statSync(safePath); const range = req.headers.range; if (range) { // 处理范围请求,支持视频进度拖拽 const [startStr, endStr] = range.replace('bytes=', '').split('-'); const start = parseInt(startStr, 10); const end = endStr ? parseInt(endStr, 10) : fileStat.size - 1; const chunkSize = end - start + 1; const readStream = fs.createReadStream(safePath, { start, end }); res.writeHead(206, { 'Content-Range': `bytes ${start}-${end}/${fileStat.size}`, 'Accept-Ranges': 'bytes', 'Content-Length': chunkSize, 'Content-Type': 'video/mp4', }); readStream.pipe(res); } else { // 全量请求 res.writeHead(200, { 'Content-Length': fileStat.size, 'Content-Type': 'video/mp4', }); fs.createReadStream(safePath).pipe(res); } } }
方案2:网络目录挂载(适合内网部署场景)
代码改造成本极低,传输性能优于HTTP方案
- 如果两台服务器都在内网环境,可通过NFS、Samba等网络文件共享协议,将存储服务器的视频存储目录挂载到应用服务器的本地路径下
- 挂载完成后,应用服务器可以直接访问挂载目录下的文件,和读取本地文件逻辑完全一致,直接调用原生
fs.createReadStream即可 - Linux下NFS挂载示例命令(应用服务器执行):
# 需提前在存储服务器配置NFS目录导出规则 mount -t nfs 存储服务器内网IP:/opt/video-storage /mnt/nfs-video
- 之后读取视频只需拼接路径即可:
const videoPath = `/mnt/nfs-video/${从数据库读取的视频相对路径}`; const readStream = fs.createReadStream(videoPath);
注意事项
- 所有方案都必须做路径合法性校验,防止恶意用户构造
../类路径读取服务器敏感文件 - 视频流式传输必须支持range请求,否则无法实现进度拖拽,会严重影响用户体验
- 跨服务器传输尽量走内网链路,避免公网带宽损耗和安全风险
内容的提问来源于stack exchange,提问作者Med
相关产品推荐
相关产品推荐

