需要了解透彻请参考:如何实现大文件上传、断点续传、切片上传_断点上传-CSDN博客
说明
切片:根据指定的大小对文件进行切块上传。
断点上传:每次上传完成一个切片后端保存信息,前端每次上传都判断所上传的文件是否存在,存在后端则返回信息,前端再根据信息进行调整继续上传。
秒传:如果文件已经在后端中存在,直接返回上传成功。可以节磁盘,提高用户体验,这里我用的是文件名来判断,不推荐,一般使用hash或者算法来为文件取一个唯一标识。
前端
没有安装axios就在命令行安装:npm install axios
import axios from 'axios';
const req = (method, url, params) => {
return axios({
method: method,
url: url,
data: params,
headers: {
'Content-Type': 'multipart/form-data',
},
traditional: true,
}).then(res => res.data);
};
const jsonReq = (method, url) => {
return axios({
method: method,
url: url,
traditional: true,
}).then(res => res.data);
};
export { req, jsonReq };
request.js
import { req, jsonReq } from "./axiosFun";
export const upload = (params) => req('post', '/test/upload', params)
export const getUploadFile = (params) => jsonReq('get', '/test/getUploadFile/' + params)
type.js
class FileChunk {
constructor(chunk, fileName, start, end, total) {
// 切片对象
this.chunk = chunk;
// 文件名称
this.fileName = fileName;
// 切片起始位置
this.start = start;
// 切片结束位置
this.end = end;
// 文件总大小
this.total = total;
}
}
export default FileChunk;
HelloWorld.vue
<template>
<el-upload class="upload-demo" ref="upload" action="https://jsonplaceholder.typicode.com/posts/" :file-list="fileList"
:on-change="selectFile" :auto-upload="false" :limit="max_fileNum" :on-exceed="onExceed">
<el-button slot="trigger" size="small" type="primary">选取文件</el-button>
<el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload">上传到服务器</el-button>
</el-upload>
</template>
<script>
import FileChunk from './type.js';
import { upload, getUploadFile } from './request.js';
export default {
name: 'FileChunk',
data() {
return {
fileList: [],
max_fileNum: 1,
fileChunkSize: 10240
};
},
methods: {
createFileChunkList(start, end_) {
let fileChunkList = [];//文件块list
let file = this.fileList[0];//文件
let cur = null;//文件切割当前位置
let end = null;//文件切割结尾位置
//断点续传
if (start != null && end_ != null) {
let temp = start + this.fileChunkSize;
if (temp < file.size) {
cur = temp;
}
end = cur + this.fileChunkSize
if (end >= file.size) {
end = file.size
}
//从头开始切片上传
} else {
cur = 0;
end = this.fileChunkSize
}
//当前文件小于切块文件,直接返回
if (file.size < this.fileChunkSize) {
return fileChunkList.push(new FileChunk(file, file.name, cur, file.size));
} else {
//断点续传已切到最后一块
if (end == file.size) {
let blob = new Blob([file]).slice(cur, end);
const fileChunk = new FileChunk(blob, file.name, cur, end, file.size);
fileChunkList.push(fileChunk);
}
//切片
while (end < file.size) {
let blob = new Blob([file]).slice(cur, end);
const fileChunk = new FileChunk(blob, file.name, cur, end, file.size);
fileChunkList.push(fileChunk);
let temp = cur + this.fileChunkSize;
if (temp < file.size) {
cur = temp;
}
end = cur + this.fileChunkSize
if (end >= file.size) {
end = file.size
}
}
}
//返回切片list
return fileChunkList;
},
//上传
submitUpload() {
let fileChunkList = null;
//判断是否是断点上传的文件
getUploadFile(this.fileList[0].name).then(res => {
if (res.data == null) {//文件不存在,直接传
fileChunkList = this.createFileChunkList(null, null);
} else if (res.data.end == res.data.total) {//该文件已经存在了 -> 秒传
this.$message.success("上传成功")
return;
} else {//断点,文件续传
fileChunkList = this.createFileChunkList(res.data.start, res.data.end);
}
let reqNum = 0;//请求次数
let errCount = 3;//只允许错误三次
while (reqNum < fileChunkList.length && errCount > 0) {
const formData = new FormData();
formData.append("file", fileChunkList[reqNum].chunk);
formData.append("fileChunk", JSON.stringify(fileChunkList[reqNum]));
upload(
formData
).then(res => {
if (res) {
reqNum += 1;
} else {
errCount -= 1;
}
this.$message.success(res.data.msg)
});
}
})
},
selectFile(file) {
this.fileList.push(file.raw)
},
onExceed() {
this.$message.error("一次只能上传一个文件");
}
}
}
</script>./type.js
后端
操作数据库所需要的实体类、接口、mapper参考type.js创建一个表格,代码生成即可
返回值直接拷贝:CSDN
package com.example.demo.controller; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.example.demo.domain.FileChunk; import com.example.demo.res.R; import com.example.demo.service.FilechunkService; import lombok.AllArgsConstructor; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.RandomAccessFile; @RestController @RequestMapping("/test") @AllArgsConstructor public class FileUploadController { private static final String UPLOAD_DIR = "D:\\luo\\code\\demo3\\src\\main\\resources\\files\\"; private final FilechunkService filechunkService; @PostMapping("/upload") public R uploadFileChunk(@RequestParam("file") MultipartFile file, @RequestParam("fileChunk") String fileChunk) { FileChunk f = JSONUtil.toBean(fileChunk, FileChunk.class); String fullPath = UPLOAD_DIR + f.getFileName(); // 模块写入对应的位置,rw表示读写模式 try (RandomAccessFile rf = new RandomAccessFile(fullPath, "rw")) { rf.seek(f.getStart()); rf.write(file.getBytes()); } catch (Exception e) { return R.fail(e.getMessage()); } //存储信息,一般存放在redis中,并设置在一定的时间内删除 f.setId(1L); filechunkService.updateById(f); return R.success("上传成功"); } @GetMapping("/getUploadFile/{name}") public R followUpload(@PathVariable("name") String name) { //查询数据库的信息,继续上传 QueryWrapper<FileChunk> wrapper = new QueryWrapper<>(); wrapper.eq("file_name", name); return R.data(filechunkService.getOne(wrapper)); } }