大文件上传
前端代码(文件名需要判断是否存在 - ,存在会后台出错)
axios
import axios from "axios";
const baseURL = 'http://localhost:3001';
export const uploadFile = (url, formData, onUploadProgress = () => { }) => {
return axios({
method: 'post',
url,
baseURL,
headers: {
'Content-Type': 'multipart/form-data'
},
data: formData,
onUploadProgress
});
}
export const mergeChunks = (url, data) => {
return axios({
method: 'post',
url,
baseURL,
headers: {
'Content-Type': 'application/json'
},
data
});
}
后端代码
const router = require('koa-router')()
const koaBody = require('koa-body')
const fs = require('fs')
const path = require('path')
router.prefix('/file')
const outputPath = './src/server/resources/'
// 上传请求
router.post(
'/upload',
// 处理文件 form-data 数据
koaBody({
multipart: true,
formidable: {
uploadDir: outputPath,
onFileBegin: (name, file) => {
console.log(name)
const [filename, fileHash, index] = name.split('-');
const dir = path.join(outputPath, filename);
// 保存当前 chunk 信息,发生错误时进行返回
currChunk = {
filename,
fileHash,
index
};
// 检查文件夹是否存在如果不存在则新建文件夹
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
// 覆盖文件存放的完整路径
file.path = `${dir}/${fileHash}-${index}`;
console.log(file.path)
},
onError: (error) => {
app.status = 400;
app.body = { code: 400, msg: "上传失败", data: currChunk };
return;
},
},
}),
// 处理响应
async (ctx) => {
ctx.set("Content-Type", "application/json");
ctx.body = JSON.stringify({
code: 2000,
message: 'upload successfully!'
});
});
// 合并请求
router.post('/mergeChunks', async (ctx) => {
const { filename, size } = ctx.request.body;
// 合并 chunks
await mergeFileChunk(path.join(outputPath, '_' + filename), filename, size);
// 处理响应
ctx.set("Content-Type", "application/json");
ctx.body = JSON.stringify({
data: {
code: 2000,
filename,
size,
path:`/src/server/resources/_${filename}`
},
message: 'merge chunks successful!'
});
});
// 通过管道处理流
const pipeStream = (path, writeStream) => {
return new Promise(resolve => {
const readStream = fs.createReadStream(path);
readStream.pipe(writeStream);
readStream.on("end", () => {
fs.unlinkSync(path);
resolve();
});
});
}
// 合并切片
const mergeFileChunk = async (filePath, filename, size) => {
const chunkDir = path.join(outputPath, filename);
const chunkPaths = fs.readdirSync(chunkDir);
if (!chunkPaths.length) return;
// 根据切片下标进行排序,否则直接读取目录的获得的顺序可能会错乱
chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
console.log("chunkPaths = ", chunkPaths);
await Promise.all(
chunkPaths.map((chunkPath, index) =>
pipeStream(
path.resolve(chunkDir, chunkPath),
// 指定位置创建可写流
fs.createWriteStream(filePath, {
start: index * size,
end: (index + 1) * size
})
)
)
);
// 合并后删除保存切片的目录
fs.rmdirSync(chunkDir);
};
module.exports = router