如何实现大文件上传,切片上传,断点续传
背景
文件上传是个非常普遍的场景,特别是在一些资源管理相关的业务中(比如网盘)。在文件比较大的时候,普通的上传方式可能会遇到以下四个问题。
文件上传超时:原因是前端请求框架认限制最大请求时长,或者是 nginx(或其它代理/网关) 限制了最大请求时长。
文件大小超限:原因在于后端对单个请求大小做了限制,一般 nginx 和 server 都会做这个限制。
上传耗时久。
由于各种网络原因上传失败,且失败之后需要从头开始。
对于前两点,虽说可以通过一定的配置来解决,但有时候也不会那么顺利,毕竟调大这些参数会对后台造成一定的压力,需要兼顾实际场景。只是上传慢的话忍一忍是可以接受的,但是失败后重头开始,在网络环境差的时候简直就是灾难。
思路
针对遇到的这些问题,有比较成熟的解决方案。该方案可以简答的概括为切片上传 + 秒传。
切片上传
切片上传是指将一个大文件切割为若干个小文件,分为多个请求依次上传,后台再将文件碎片拼接为一个完整的文件,即使某个碎片上传失败,也不会影响其它文件碎片,只需要重新上传失败的部分就可以了。而且多个请求一起发送文件,提高了传输速度的上限
切片上传前端代码(可直接使用)
前端代码(vue)
<template>
<div class="upload-section">
<h3 style="display: inline-block; margin-right: 10px;">切片上传</h3>
<input type="file" style="display: inline-block; " @change="handleFileChange($event)">
<button @click="uploadChunkFiles()">上传</button>
</div>
<div v-if="uploadProgress !== null">
<p>上传进度: {{ uploadProgress }}%</p>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "ChunkFile",
data() {
return {
uploadProgress: null,
chunk: null,
chunkSize: 1024 * 1024, // 1MB 切片大小
};
},
methods: {
async uploadChunkFiles() {
const { chunkSize } = this;
let start = 0;
let end = chunkSize;
let totalChunks = Math.ceil(this.chunk.file.size / chunkSize);
let chunkIndex = 0;
while (start < this.chunk.file.size) {
const chunk = this.chunk.file.slice(start, end);
const formData = new FormData();
formData.append("file", chunk);
formData.append("totalChunks", totalChunks);
formData.append("chunkIndex", chunkIndex);
formData.append("start", start);
formData.append("fileName", this.chunk.name);
try {
const config = {
headers: { "Content-Type": "multipart/form-data" },
};
await axios.post("http://localhost:8080/BigFile/uploadChunk", formData, config);
this.updateTotalProgress(chunkIndex);
} catch (error) {
console.error(`Error uploading chunk ${chunkIndex}`, error);
return;
}
start = end;
end = start + chunkSize;
chunkIndex++;
}
},
updateTotalProgress(chunkIndex) {
let totalUploaded = (chunkIndex + 1) * this.chunkSize;
this.uploadProgress = Math.round((totalUploaded / this.chunk.file.size) * 100);
},
handleFileChange(event) {
const file = event.target.files[0];
this.chunk = {
name: file.name,
file: file
};
}
}
};
</script>
<style scoped>
.upload-section {
width: 100%;
margin-bottom: 10px;
}
.upload-section h3 {
margin-bottom: 10px;
font-size: 16px;
color: #333;
}
.upload-section input[type="file"] {
display: inline-block;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #fff;
font-size: 16px;
box-sizing: border-box;
}
.upload-section button {
display: inline-block;
padding: 10px 20px;
margin-left: 10px;
font-size: 16px;
cursor: pointer;
border: 1px solid #007bff;
background-color: #007bff;
color: #fff;
border-radius: 5px;
}
</style>
后端代码
@PostMapping("/uploadChunk")
public void uploadChunk(@RequestParam("file") MultipartFile file,
@RequestParam("totalChunks") Integer totalChunks,
@RequestParam("chunkIndex") Integer chunkIndex,
@RequestParam("start") Long start,
@RequestParam("fileName") String fileName) throws IOException {
String filePath = UPLOAD_DIR + fileName;
// 如果是第一个块,创建新文件
if (chunkIndex == 0) {
try (OutputStream outputStream = new FileOutputStream(filePath)) {
outputStream.write(file.getBytes());
}
} else {
// 如果不是第一个块,追加写入到已有文件
try (OutputStream outputStream = new FileOutputStream(filePath, true)) {
outputStream.write(file.getBytes());
}
}
// 如果是最后一个块,可以在这里进行一些收尾工作,例如关闭流
if (chunkIndex + 1 == totalChunks) {
// 这里可以添加额外的处理逻辑,比如合并文件块等
}
}