大文件上传和断点续传
2023/10/8大约 3 分钟
大文件上传和断点续传
什么是断点续传
断点续传是指在文件上传过程中,如果因为网络中断、页面刷新等原因导致上传失败,不需要重新上传整个文件,而是从上次中断的地方继续上传。这对于大文件上传尤为重要,可以显著提升用户体验和上传效率。
核心思路
把一个大文件切割成多个小块(Chunk),每个小块单独上传。如果上传过程中出现网络问题,只需要重新上传未完成的块,无需重新上传整个文件。
核心优势
- 节省带宽:避免重复上传已成功的分片
- 提升效率:支持并发上传多个分片
- 用户体验:网络中断后可以无缝继续
- 可靠性高:单个分片失败不影响其他分片
实现步骤
1. 文件唯一标识生成
在上传前,根据文件内容生成唯一的 Hash 值(通常使用 MD5),用于区分不同的文件。
import SparkMD5 from "spark-md5";
async function calculateFileHash(file) {
return new Promise((resolve) => {
const spark = new SparkMD5.ArrayBuffer();
const reader = new FileReader();
reader.onload = (e) => {
spark.append(e.target.result);
resolve(spark.end());
};
reader.readAsArrayBuffer(file);
});
}2. 文件切片
将大文件切割成多个固定大小的分片。
function createChunks(file, chunkSize = 2 * 1024 * 1024) {
const chunks = [];
let cur = 0;
while (cur < file.size) {
chunks.push({
file: file.slice(cur, cur + chunkSize),
index: cur,
size: Math.min(chunkSize, file.size - cur),
});
cur += chunkSize;
}
return chunks;
}3. 分片上传
支持并发上传多个分片,提升上传效率。
async function uploadChunks(chunks, fileHash, onProgress) {
const concurrent = 3;
let current = 0;
const results = [];
const upload = async (chunk, index) => {
const formData = new FormData();
formData.append("file", chunk.file);
formData.append("hash", fileHash);
formData.append("index", index);
const response = await fetch("/api/upload/chunk", {
method: "POST",
body: formData,
});
return response.json();
};
const worker = async () => {
while (current < chunks.length) {
const index = current++;
const result = await upload(chunks[index], index);
results[index] = result;
onProgress?.(results.filter(Boolean).length / chunks.length);
}
};
const workers = Array(Math.min(concurrent, chunks.length))
.fill(null)
.map(() => worker());
await Promise.all(workers);
return results;
}4. 断点续传实现
记录已上传的分片信息,断网后可恢复上传。
async function checkUploadedChunks(fileHash) {
const response = await fetch(`/api/upload/status?hash=${fileHash}`);
return response.json();
}
async function resumeUpload(file, chunkSize = 2 * 1024 * 1024) {
const fileHash = await calculateFileHash(file);
const { uploadedIndexes = [] } = await checkUploadedChunks(fileHash);
const allChunks = createChunks(file, chunkSize);
const remainingChunks = allChunks.filter(
(_, index) => !uploadedIndexes.includes(index),
);
await uploadChunks(remainingChunks, fileHash, (progress) => {
console.log(`上传进度: ${Math.round(progress * 100)}%`);
});
await mergeChunks(fileHash);
}5. 文件合并
所有分片上传完成后,通知服务器合并分片。
async function mergeChunks(fileHash) {
const response = await fetch("/api/upload/merge", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ hash: fileHash }),
});
return response.json();
}安全性问题
1. 分片内容校验
在切片过程中对每个分片进行 Hash 校验,防止内容篡改。
async function calculateChunkHash(chunk) {
return new Promise((resolve) => {
const spark = new SparkMD5.ArrayBuffer();
const reader = new FileReader();
reader.onload = (e) => {
spark.append(e.target.result);
resolve(spark.end());
};
reader.readAsArrayBuffer(chunk);
});
}2. 切片大小限制
设置切片大小上限,防止切片过大导致请求失败。
- 建议分片大小:1MB - 5MB
- 最小分片大小:100KB
- 最大分片数量:1000 个
3. 文件类型和大小限制
function validateFile(file) {
const ALLOWED_TYPES = ["video/*", "application/zip", "application/pdf"];
const MAX_SIZE = 5 * 1024 * 1024 * 1024; // 5GB
const isAllowedType = ALLOWED_TYPES.some((type) =>
file.type.match(new RegExp(type.replace("*", ".*"))),
);
if (!isAllowedType) {
throw new Error("不支持的文件类型");
}
if (file.size > MAX_SIZE) {
throw new Error("文件大小超过限制");
}
return true;
}常见问题
1. 如何选择分片大小?
- 文件类型:视频大文件建议 5MB+,图片建议 1MB
- 网络环境:网络稳定可较大,网络不稳定建议较小
- 服务器限制:考虑服务器超时和请求体大小限制
2. 如何处理服务器存储?
- 使用临时目录存储分片
- 合并后移动到正式存储位置
- 定期清理未完成的分片(可设置过期时间)
3. 如何保证分片顺序?
服务器端根据分片索引(index)存储,合并时按索引顺序读取。
总结
大文件上传和断点续传的核心在于:
- 分片:将大文件切分为小分片
- 标识:使用 Hash 唯一标识文件和分片
- 并发:支持多分片并发上传
- 记录:记录上传状态,支持断点续传
- 校验:确保数据完整性