前端:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.6/axios.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
</head>
<body>
<input type="file" id="fileInput"/>
<progress
value="0"
max="100"
id="progress"
></progress>
<button onClick={upload()}>
上传文件
</button>
</body>
<script>
// 计算文件哈希值的函数
const computeFileHash = (file) => { //传入文件内容 对文件内容进行哈希值的计算
return new Promise((resolve, reject) => {
const chunkSize = 1 * 1024 * 1024; // 1MB
const fileReader = new FileReader() // 创建一个FileReader对象,用于读取文件内容
const spark = new SparkMD5.ArrayBuffer(); // 创建一个SparkMD5对象,用于计算文件哈希值
let currentChunk = 0; // 当前处理的分片索引
// 文件读取成功时的回调函数
fileReader.onload = function(e) {
spark.append(e.target.result); //将文件块的数据添加到哈希计算中
currentChunk++;
if(currentChunk < totalChunks){
loadNextChunk(); //继续加载下一个文件块
}else{
const hash = spark.end(); //完成哈希值计算
resolve(hash); //返回计算得到的哈希值
}
};
// 文件读取失败时的回调函数
fileReader.onerror = function (e) {
reject(e.target.error); // 返回读取错误
};
// 加载下一个文件块
function loadNextChunk() {
const start = currentChunk * chunkSize; //当前文件块的起始位置
const end = Math.min(start + chunkSize, file.size); //当前文件块的结束位置
const chunk = file.slice(start, end); // 提取当前文件块的数据
fileReader.readAsArrayBuffer(chunk); // 以ArrayBuffer形式读取文件块的数据
}
const totalChunks = Math.ceil(file.size / chunkSize); // 总的文件块数量
loadNextChunk(); // 开始加载第一个块
})
}
async function upload(){
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const chunkSize = 1 * 1024 * 1024; // 设置每个分片的大小为1MB
const totalChunks = Math.ceil(file.size / chunkSize); // 计算文件总分片数
let currentChunk = 0; // 当前处理的分片索引
const resumeKey = file.name + '-currentChunk'; // 用于断点续传的本地存储键名
const resumeIndex = localStorage.getItem(resumeKey); // 当前的索引值
if (resumeIndex != null) {
currentChunk = parseInt(resumeIndex); // 如果存在已处理的分片索引,则更新currentChunk
} else {
localStorage.removeItem(resumeKey); // 否则移除本地存储的键值对
}
// 计算文件哈希值
const fileHash = await computeFileHash(file);
// 检查服务器是否已存在相同的文件
try{
const response = await axios.head(
'http://localhost:3000/check-file?filehash=' + fileHash
);
if(response.status === 200){
// 文件已存在,直接完成上传
console.log('文件已存在,秒传成功');
return;
}
}catch(error){
console.log('检查文件失败',error)
}
while (currentChunk < totalChunks) {
const start = currentChunk * chunkSize; //计算当前块的起始位置
const end = Math.min(start + chunkSize, file.size); //计算当前块的结束位置
const chunk = file.slice(start, end); //切割文件为当前块
const formData = new FormData();
formData.append('file', chunk); //添加当前块到FormData对象
formData.append('filename', file.name); //添加文件名到FormData对象
formData.append('totalChunks', totalChunks); //添加总块数到FormData对象
formData.append('currentChunk', currentChunk); //添加当前块数到FormData对象
try{
const res = await axios.post('http://localhost:3000/upload',formData,{
headers:{
'Content-Type':'multipart/form-data',
},
}); //发送当前块的上传请求
const { progress } = res.data; //获取当前块的上传进度
document.getElementById('progress').value = progress; //更新进度
localStorage.setItem(resumeKey,currentChunk);
}catch(error){
console.error(error);
return;
}
currentChunk++; //增加当前块数,继续下一块的上传
}
localStorage.removeItem(resumeKey);
try{
const postData = { filename:file.name,totalChunks:totalChunks,fileHash:fileHash }; //构造合并请求的数据
await http.post('http://localhost:3000/merge', postData,{
headers: {
'Content-Type': 'application/json'
}
}); //发送合并请求
}catch(error){
console.error(error);
}
}
</script>
</html>
后端:
const express = require('express');
const cors = require('cors');
const path = require('path');
const fs = require('fs');
const multer = require('multer');
const upload = multer({dest:'uploads/'});
const bodyParser = require('body-parser');
const app = express();
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended:false}));
// 存储文件哈希值的对象
const fileHashes = {};
app.post('/upload',upload.single('file'),(req,res) => {
const file = req.file; // 获取上传的文件对象
const filename = req.body.filename; // 获取文件名
const totalChunks = parseInt(req.body.totalChunks); // 获取总块数
const currentChunk = parseInt(req.body.currentChunk); //获取当前块数
const chunkPath = path.join(
"uploads/",
`${filename}-chunk-${currentChunk}`
); //生成当前存储路径
const chunkStream = fs.createReadStream(file.path); //创建读取文件块的可读流
const writeStream = fs.createWriteStream(chunkPath); //创建写入当前块的可写流
chunkStream.pipe(writeStream); //将读取的文件块内容通过管道写入当前块的文件
chunkStream.on("end", () => {
fs.unlinkSync(file.path); //读取文件块的流结束后,删除临时文件
const progress = ((currentChunk + 1) / totalChunks) * 100;
res.json({ progress }); //相应上传成功的状态
});
});
router.post("/merge", (req, res) => {
const filename = req.body.filename; //获取文件名
const totalChunks = parseInt(req.body.totalChunks); //获取总块数
const fileHash = req.body.fileHash //接收到哈希值
const mergedPath = path.join("uploads", filename); //生成合并后文件的存储路径
const writeStream = fs.createWriteStream(mergedPath); //创建写入合并后文件的可写流
const mergeChunks = (index) => {
if (index === totalChunks) {
writeStream.end(); //所有块都合并完成后,关闭写入流
res.sendStatus(200); //响应合并成功的状态
return;
}
const chunkPath = path.join("uploads", `${filename}-chunk-${index}`); //获取当前块的存储路径
const chunk = fs.readFileSync(chunkPath); //同步读取当前块的内容
fs.unlinkSync(chunkPath); //删除已合并的块文件
// 存储文件哈希值,以便进行响应式的检测操作
fileHashes[fileHash] = true;
writeStream.write(chunk,() => {
mergeChunks(index + 1); //递归合并下一块
});
};
mergeChunks(0); //从第一块开始合并
});
// 检查文件是否已存在
app.head('/check-file',(req,res) => {
const fileHash = req.query.filehash;
console.log(fileHash,fileHashes);
if(fileHashes[fileHash]){
res.sendStatus(200); //文件已存在
}else{
res.sendStatus(404); //文件不存在
}
})
app.listen(3000,() => {
console.log('Server started on port 3000');
});