You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将Express.Multer.File转换为ReadStream适配Bunny.Net上传?

NestJS中视频上传:将Multer的File转换为Bunny.Net所需的ReadStream

问题描述

在NestJS项目实现视频上传时,Bunny.Net封装库的createAndUploadVideo方法要求传入ReadStream类型参数,但通过Multer获取的Express.Multer.File仅提供buffer和stream属性。由于文件未落地,无法使用fs.createReadStream(需要文件路径),需将buffer或stream转换为ReadStream。

控制器代码

@ApiUpload('', { useAuthGuard: false, description: 'Upload file - max 25mb', type: OutputFileDto })
async create(@User() user: UserEntity, @UploadedFile() uploadedFile: Express.Multer.File) {
  return this._fileService.create(user, uploadedFile);
}

项目启动代码

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const configService: ConfigService<IConfiguration> = app.get(ConfigService);

  // generate REST API documentation
  const documentation = new DocumentBuilder().setTitle('API documentation').setVersion('1.0');
  documentation.addBearerAuth();
  SwaggerModule.setup(
    '',
    app,
    SwaggerModule.createDocument(app, documentation.build(), {
      extraModels: [],
    }),
  );

  // interceptors
  app.useGlobalInterceptors(new LoggerInterceptor());

  // validate DTOs
  app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));

  // handle unique entities error from database
  const { httpAdapter } = app.get(HttpAdapterHost);
  app.useGlobalFilters(new QueryErrorFilter(httpAdapter));

  // enable cors
  app.enableCors();

  // raw body
  app.use(
    json({
      limit: '1mb',
      verify: (req: IncomingMessage, res: ServerResponse, buf: Buffer, encoding: BufferEncoding) => {
        if (buf && buf.length) {
          req['rawBody'] = buf.toString(encoding || 'utf8');
        }
      },
    }),
  );

  // security
  app.use(helmet());

  app.use(
    rateLimit({
      windowMs: 900000,
      max: 50099,
      message: { status: 429, message: 'Too many requests, please try again later.' },
    }),
  );

  await app.listen(configService.get('basic.port', { infer: true }));
}

bootstrap();

服务层create方法代码

async create(user: UserEntity, uploadedFile: Express.Multer.File) {
  let url: string = '';
  let storageKey: string = '';
  let length: number | undefined;
  console.log(uploadedFile);
  if (['video/mp4'].includes(uploadedFile.mimetype)) {
    const stream = new BunnyCdnStream({
      videoLibrary: '...',
      apiKey: '...',
    });

    const readable = Readable.from(uploadedFile.buffer);
    console.log(readable);
    const video = await stream.createAndUploadVideo(readable, { title: '' });

  } else {
    storageKey = `files/${new Date().getTime()}-${slugify(uploadedFile.originalname)}`;
    url = await this._uploadService.save(storageKey, uploadedFile.buffer, uploadedFile.mimetype);
  }

  if (!url) {
    throw new BadRequestException(ErrorMessages.FILE_UPLOAD_FAILED);
  }

  return this._fileRepository.save(
    new FileEntity({
      originalname: uploadedFile.originalname,
      mimetype: uploadedFile.mimetype,
      size: uploadedFile.size,
      url,
      storageKey,
      user,
      length,
    }),
  );
}

解决方案

你的方案的正确性

你使用Readable.from(uploadedFile.buffer)的方式是完全可行且正确的。Node.js的stream.Readable.from()方法可以直接将Buffer转换为符合要求的Readable流,能够被Bunny.Net的createAndUploadVideo方法正常消费。

更优方案:直接复用Multer的stream属性

Express.Multer.File本身自带的stream属性就是一个Readable流,你可以直接传入该属性,无需额外转换:

const video = await stream.createAndUploadVideo(uploadedFile.stream, { title: '' });

这种方式更高效,因为它直接复用了Multer处理上传时的原始流,避免了将Buffer重新转换为流的内存开销,尤其适合大文件上传场景。

注意事项

  • 若使用uploadedFile.stream,需确保Multer的配置不会提前将文件全部缓存到内存(默认情况下,Multer对小于1MB的文件会存到内存,大于则写入临时文件,但无论哪种情况,stream属性都可正常使用)。
  • 无论采用哪种方式,都要确保流被正确消费,避免出现内存泄漏问题。

内容的提问来源于stack exchange,提问作者Korer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 10:34:57