Skip to content

Commit

Permalink
feat: 大文件分片上传-异常请求处理
Browse files Browse the repository at this point in the history
  • Loading branch information
songxingguo committed Jun 12, 2024
1 parent c97cd21 commit c3769c0
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 53 deletions.
34 changes: 26 additions & 8 deletions src/.vuepress/public/demo/FileUpload/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,36 @@ server.on("request", async (req, res) => {

multipart.parse(req, async (err, fields, files) => {
if (err) {
console.error(err);
res.status = 500;
res.end(
JSON.stringify({
messaage: "process file chunk failed",
})
);
return;
}

const [chunk] = files.file;
const [hash] = fields.hash;
const [filename] = fields.name;
const [fileHash] = fields.fileHash;
const chunkDir = `${UPLOAD_DIR}/${fileHash}`;

const filePath = path.resolve(
UPLOAD_DIR,
`${fileHash}${extractExt(filename)}`
);
// 文件存在直接返回
if (fse.existsSync(filePath)) {
res.end(
JSON.stringify({
messaage: "file exist",
})
);
return;
}

// 切片目录不存在,创建切片目录
if (!fse.existsSync(chunkDir)) {
await fse.mkdirs(chunkDir);
Expand All @@ -72,8 +95,7 @@ server.on("request", async (req, res) => {
res.status = 200;
res.end(
JSON.stringify({
code: 200,
message: "received file chunk",
messaage: "received file chunk",
})
);
});
Expand All @@ -85,12 +107,8 @@ server.on("request", async (req, res) => {
const ext = extractExt(filename);
const filePath = `${UPLOAD_DIR}/${fileHash}${ext}`;
await mergeFileChunk(filePath, fileHash);
res.end(
JSON.stringify({
code: 200,
message: "file merged success",
})
);
res.status = 200;
res.end(JSON.stringify("file merged success"));
}

if (req.url === "/verify") {
Expand Down
120 changes: 75 additions & 45 deletions src/.vuepress/public/demo/FileUpload/大文件分片上传.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,31 @@
<div>
<input type="file" name="file" id="file" multiple />
<button id="upload" onClick="handleUpload()">上传</button>
<button onClick="handlePause()">暂停</button>
<button onClick="handleResume()">恢复</button>
<button id="pause" onClick="handlePause()">暂停</button>
<button id="resume" onClick="handleResume()">恢复</button>
<p id="total-slice"></p>
<p id="hash-progress"></p>
<p id="progress"></p>
</div>
<script>
let abortControllerList = []; // 取消信号列表
let fileName = "",
fileHash = "",
fileSize = 0,
fileChunkListData = [],
abortControllerList = [];
// 切换按钮状态
const toggleBtn = (isPause = true) => {
const pauseBtn = document.getElementById("pause");
const resuemeBtn = document.getElementById("resume");
if (isPause) {
pauseBtn.style.display = "inline-block";
resuemeBtn.style.display = "none";
} else {
pauseBtn.style.display = "none";
resuemeBtn.style.display = "inline-block";
}
};
toggleBtn();
/**
* @description: 封装fetch
* @param {Object} FetchConfig fetch config
Expand Down Expand Up @@ -59,7 +79,6 @@

// 文件分片
const createFileChunk = (file) => {
const fileSize = file.size; // 文件大小
const chunkList = [];
//计算文件切片总数
const sliceSize = 5 * 1024 * 1024; // 每个文件切片大小定为5MB
Expand All @@ -72,32 +91,41 @@
} else {
chunk = file.slice((i - 1) * sliceSize, i * sliceSize);
}
chunkList.push({ file: chunk, fileSize, size: sliceSize });
chunkList.push({
file: chunk,
fileSize,
size: Math.min(sliceSize, file.size),
});
}
console.log("一共分片:", totalSlice);
const sliceText = `一共分片:${totalSlice}`;
document.getElementById("total-slice").innerHTML = sliceText;
console.log(sliceText);
return chunkList;
};

// 根据分片生成hash
const calculateHash = (fileChunkList) => {
return new Promise((resolve) => {
const spark = new SparkMD5.ArrayBuffer();
let percentage = 0;
let count = 0;
// 计算出hash
const loadNext = (index) => {
const reader = new FileReader(); // 文件阅读对象
reader.readAsArrayBuffer(fileChunkList[index].file);
reader.onload = (e) => {
// 事件
count++;
spark.append(e.target.result);
if (count === fileChunkList.length) {
resolve(spark.end());
} else {
// 还没读完
percentage += 100 / fileChunkList.length;
console.log("计算hash值百分比:", percentage);
const percentage = parseInt(
((count + 1) / fileChunkList.length) * 100
);
const progressText = `计算hash值:${percentage}%`;
document.getElementById("hash-progress").innerHTML =
progressText;
console.log(progressText);
loadNext(count);
}
};
Expand All @@ -108,9 +136,10 @@

const handleUpload = async () => {
const file = document.getElementById("file").files[0];
const fileName = file.name; // 文件名
fileName = file.name; // 文件名
fileSize = file.size; // 文件大小
const fileChunkList = createFileChunk(file);
const fileHash = await calculateHash(fileChunkList); // 文件hash
fileHash = await calculateHash(fileChunkList); // 文件hash
const { shouldUpload, uploadedList } = await verifyUpload(
fileName,
fileHash
Expand All @@ -119,64 +148,63 @@
alert("秒传:上传成功");
return;
}
const fileList = fileChunkList.map(({ file, size }, index) => ({
file,
size,
fileName,
fileHash,
hash: `${fileHash}-${index}`,
percentage: 0,
}));
await uploadChunks({
fileList,
uploadedList,
fileName,
fileHash,
fileSize: file.size,
fileChunkListData = fileChunkList.map(({ file, size }, index) => {
const hash = `${fileHash}-${index}`;
return {
file,
size,
fileName,
fileHash,
hash,
percentage: uploadedList.includes(hash) ? 100 : 0,
};
});
await uploadChunks(uploadedList);
};

//上传分片
const uploadChunks = async ({
fileList,
uploadedList,
fileName,
fileHash,
fileSize,
}) => {
const requestList = fileList
const uploadChunks = async (uploadedList) => {
const requestList = fileChunkListData
.filter(({ hash }) => !uploadedList.includes(hash))
.map(({ file, fileHash, fileName, hash }, index) => {
const formData = new FormData();
formData.append("file", file);
formData.append("fileHash", fileHash);
formData.append("name", fileName);
formData.append("hash", hash);
return { formData };
return { formData, hash };
})
.map(async ({ formData }, index) => {
.map(async ({ formData, hash }) => {
return requestApi({
url: `http://localhost:3000`,
method: "POST",
body: formData,
onProgress: ({ loaded, total }) => {
const percentage = parseInt(String((loaded / total) * 100));
const percentage = parseInt((loaded / total) * 100);
// console.log("分片上传百分比:", percentage);
fileList[index].percentage = percentage;
const totalLoaded = fileList
const curIndex = fileChunkListData.findIndex(
({ hash: h }) => h === hash
);
fileChunkListData[curIndex].percentage = percentage;
const totalLoaded = fileChunkListData
.map((item) => item.size * item.percentage)
.reduce((acc, cur) => acc + cur);
const totalPercentage = parseInt(
(totalLoaded / fileSize).toFixed(2)
);
console.log("上传百分比:", `${totalPercentage}%`);
const progressText = `上传进度:${totalPercentage}%`;
document.getElementById("progress").innerHTML = progressText;
console.log(progressText);
},
});
});
await Promise.all(requestList);
// 之前上传的切片数量 + 本次上传的切片数量 = 所有切片数量时
//合并分片
if (uploadedList.length + requestList.length === fileList.length) {
if (
uploadedList.length + requestList.length ===
fileChunkListData.length
) {
await mergeRequest(fileName, fileHash);
}
};
Expand Down Expand Up @@ -211,16 +239,18 @@
});
return data;
};

//断点续传
//暂停
const handlePause = () => {
toggleBtn(false);
abortControllerList.forEach((controller) => controller?.abort());
abortControllerList = [];
};
// 恢复
const handleResume = async () => {
await uploadChunks();
toggleBtn();
const { uploadedList } = await verifyUpload(fileName, fileHash);
await uploadChunks(uploadedList);
};
//TODO 上传进度
</script>
</body>
</html>

0 comments on commit c3769c0

Please sign in to comment.