如何在Adonis JS中实现大文件分片上传,避免请求超时问题?
AdonisJS 大文件分片上传解决方案
问题根因说明
- 你当前的服务端代码仅支持完整单文件上传,没有适配客户端的分片逻辑:客户端将大文件切割为多个独立的blob分片,你直接存储单个分片,重命名后自然无法正常打开
- 大文件走单请求上传必然会触发服务器、反向代理(如Nginx)的超时限制,也容易因网络波动导致整个上传失败
完整实现步骤
1. 调整基础配置放宽上传限制
首先修改config/bodyparser.ts,避免小分片被框架拦截:
// config/bodyparser.ts export default { multipart: { // 单个分片最大大小,按需调整,示例设为100MB maxFileSize: '100mb', // 按需添加允许的文件后缀 allowedExtensions: ['png', 'jpg', 'jpeg', 'mp4', 'avi', 'mov'], } }
如果使用Nginx做反向代理,同步修改Nginx配置:
client_max_body_size 100m; proxy_read_timeout 300s; proxy_send_timeout 300s;
2. 服务端分片逻辑改造
首先要求客户端上传时额外携带3个参数:
file_hash:整个文件的唯一哈希值,用于标识属于同一个文件的所有分片chunk_index:当前分片的序号,从0开始计数total_chunks:当前文件总分片数
路由调整
// routes.ts apiGroup('v1', 'files', Route.group(async () => { // 分片上传接口 Route.post('upload-chunk', 'Files/UploadController.uploadChunk') // 全部分片上传完成后的合并接口 Route.post('merge-chunks', 'Files/UploadController.mergeChunks') }))
UploadController改造
// UploadController.ts 'use strict' import {HttpContextContract} from "@ioc:Adonis/Core/HttpContext"; import Application from "@ioc:Adonis/Core/Application"; import MediaRepositories from "App/Repositories/MediaRepositories"; import fs from 'fs/promises' import path from 'path' export default class UploadController { // 接收单个分片 public async uploadChunk({request, response}:HttpContextContract){ const file = request.file('file') const fileHash = request.input('file_hash') const chunkIndex = request.input('chunk_index') if (!file || !fileHash || chunkIndex === undefined) { return response.badRequest({msg: '参数缺失'}) } // 同一文件的分片统一存放在独立临时目录 const chunkDir = path.join(Application.tmpPath('chunks'), fileHash) await fs.mkdir(chunkDir, {recursive: true}) // 分片按序号命名存储 await file.move(chunkDir, {name: chunkIndex.toString()}) return {msg: '分片上传成功'} } // 合并全部分片生成完整文件 public async mergeChunks({request, response}:HttpContextContract) { const fileHash = request.input('file_hash') const totalChunks = request.input('total_chunks') const fileName = request.input('file_name') // 客户端传递原文件名 if (!fileHash || !totalChunks || !fileName) { return response.badRequest({msg: '参数缺失'}) } const finalPath = path.join(Application.publicPath('media/transientmodels'), fileName) const chunkDir = path.join(Application.tmpPath('chunks'), fileHash) // 按顺序读取所有分片写入最终文件 const writeStream = fs.createWriteStream(finalPath) for (let i = 0; i < totalChunks; i++) { const chunkPath = path.join(chunkDir, i.toString()) const chunkBuffer = await fs.readFile(chunkPath) writeStream.write(chunkBuffer) // 写入完成后删除已处理分片 await fs.unlink(chunkPath) } writeStream.end() // 合并完成后删除空的临时分片目录 await fs.rmdir(chunkDir) // 可调用MediaRepositories做后续的文件校验、数据入库逻辑 const mediaInfo = await MediaRepositories.createMedia(finalPath, fileName) return {msg: '文件上传完成', data: mediaInfo} } }
MediaRepositories调整
// MediaRepositories.ts 'use strict' import Application from "@ioc:Adonis/Core/Application"; export default class MediaRepositories { static async createMedia(filePath: string, fileName: string) { // 此处可扩展文件格式校验、文件信息入库等逻辑 return { fileName, accessUrl: `/media/transientmodels/${fileName}` } } static async updateMediaById(){} static async updateMediaByIds(){} }
避免请求超时的核心方案
- 严格使用分片上传,将单个请求的文件大小控制在10-50MB之间,单个请求耗时远低于常规服务超时阈值
- 可扩展断点续传逻辑:客户端上传前先查询当前文件已上传的分片序号,跳过已上传部分,网络中断后无需重传整个文件
- 针对10GB以上的超大文件,可将分片合并逻辑放到Adonis异步队列中执行,避免合并过程耗时过长触发接口超时
内容的提问来源于stack exchange,提问作者Vladyslav Chalyi
相关产品推荐
相关产品推荐

