前端代码(文件名需要判断是否存在 - ,存在会后台出错)
<script setup>
import { ref } from 'vue'
import SparkMD5 from 'spark-md5'
import {uploadFile,mergeChunks} from './utils/request'
const DefualtChunkSize = 5 * 1024 * 1024
const fileChunkList= {
value:[]
}
const currFile = {
value:{
}
}
// 获取文件分块
const getFileChunk = (file, chunkSize = DefualtChunkSize) => {
return new Promise((resovle) => {
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
chunks = Math.ceil(file.size / chunkSize),
currentChunk = 0,
spark = new SparkMD5.ArrayBuffer(),
fileReader = new FileReader();
fileReader.onload = function (e) {
console.log('read chunk nr', currentChunk + 1, 'of');
const chunk = e.target.result;
spark.append(chunk);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
let fileHash = spark.end();
console.info('finished computed hash', fileHash);
resovle({ fileHash });
}
};
fileReader.onerror = function () {
console.warn('oops, something went wrong.');
};
function loadNext() {
let start = currentChunk * chunkSize,
end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
let chunk = blobSlice.call(file, start, end);
fileChunkList.value.push({ chunk, size: chunk.size, name: currFile.value.name });
fileReader.readAsArrayBuffer(chunk);
}
loadNext();
});
}
// 上传请求
const uploadChunks = (fileHash) => {
const requests = fileChunkList.value.map((item, index) => {
const formData = new FormData();
formData.append(`${currFile.value.name}-${fileHash}-${index}`, item.chunk);
formData.append("filename", currFile.value.name);
formData.append("hash", `${fileHash}-${index}`);
formData.append("fileHash", fileHash);
return uploadFile('/file/upload', formData, onUploadProgress(item));
});
Promise.all(requests).then(() => {
mergeChunks('/file/mergeChunks', { size: DefualtChunkSize, filename: currFile.value.name });
});
}
// 总进度条
const totalPercentage = () => {
if (!fileChunkList.value.length) return 0;
const loaded = fileChunkList.value
.map(item => item.size * item.percentage)
.reduce((curr, next) => curr + next);
return parseInt((loaded / currFile.value.size).toFixed(2));
}
// 分块进度条
const onUploadProgress = (item) => (e) => {
item.percentage = parseInt(String((e.loaded / e.total) * 100));
}
const getFile = async(e)=>{
const files = e.target.files
currFile.value = files[0]
console.log(files[0])
const {fileHash} = await getFileChunk(files[0])
await uploadChunks(fileHash)
console.log(fileChunkList)
console.log(totalPercentage())
}
</script>
<template>
<div>
<input type="file" ref="file" @change="getFile"/>
</div>
</template>
<style>
</style>
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
浙公网安备 33010602011771号