工具类方法:
/**
* 大文件分片上传
* @param fileName 文件名
* @param file 文件
* @param fileKey 文件key
* @param shardIndex 当前分片下标
* @param shardTotal 分片总量
*/
public static void bigUpload(String fileName,MultipartFile file, String fileKey, Long shardIndex, Long shardTotal) throws Exception {
String fileDir = getDefaultBaseDir() +"/"+ DateUtils.datePath() + "/" + fileKey;
File dir=new File(fileDir);
if (!dir.exists()) {
dir.mkdirs();
}
File dest = new File(fileDir+"/" + fileKey + "." + shardIndex);
// 分片文件保存到文件目录
file.transferTo(dest);
if (shardIndex == shardTotal) {
merge(fileName, shardTotal, fileKey);
}
}
/**
* 分片大文件上传,文件合并
*
* @param fileName 文件名比如123.mp4
* @param shardTotal 分片总量
* @param fileKey 文件key
* @throws Exception
*/
private static void merge(String fileName, Long shardTotal, String fileKey) throws Exception {
String mergeFilePath = getDefaultBaseDir()+"/" + DateUtils.datePath() + "/" + fileKey + "/" + fileName;
File newFile = new File(mergeFilePath);
if (newFile.exists()) {
newFile.delete();
}
FileOutputStream outputStream = new FileOutputStream(newFile, true);//文件追加写入
FileInputStream fileInputStream = null;//分片文件
byte[] byt = new byte[10 * 1024 * 1024];
int len;
try {
for (int i = 0; i < shardTotal; i++) {
// 读取第i个分片
String shardFilePath = getDefaultBaseDir() +"/"+ DateUtils.datePath() + "/" + fileKey + "/" + fileKey + "." + (i + 1);
fileInputStream = new FileInputStream(shardFilePath);
while ((len = fileInputStream.read(byt)) != -1) {
outputStream.write(byt, 0, len);//一直追加到合并的新文件中
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fileInputStream != null) {
fileInputStream.close();
}
outputStream.close();
System.gc();
} catch (Exception e) {
}
}
}
controller需要实现两个接口:上传文件和分片文件状态检查。
@GetMapping("/check")
public AjaxResult check(@RequestParam String key) {
PanoramicFileTb fileTb = panoramicFileTbService.selectLatestIndex(key);
log.info("检查分片:{}", key);
return AjaxResult.success(fileTb);
}
/**
* 大文件上传
*
* @param file
* @param filePojo
* @return
* @throws Exception
*/
@PreAuthorize("@ss.hasPermi('system:BusinessFile:add')")
@Log(title = "文件记录", businessType = BusinessType.INSERT)
@PostMapping("/big-upload")
public AjaxResult bigUpload(@RequestParam(value = "file") MultipartFile file,
FilePojoVo filePojo) throws Exception {
FileUploadUtils.bigUpload(filePojo.getFileName(),file, filePojo.getKey(), filePojo.getShardIndex(), filePojo.getShardTotal());
log.info("文件分片 {} 保存完成", filePojo.getShardIndex());
PanoramicFileTb fileTb = PanoramicFileTb.builder()
.fKey(filePojo.getKey())
.fIndex(filePojo.getShardIndex())
.fTotal(filePojo.getShardTotal())
.fName(filePojo.getFileName())
.build();
if (panoramicFileTbService.isNotExist(filePojo.getKey())) {
panoramicFileTbService.saveFile(fileTb);
} else {
panoramicFileTbService.UpdateFile(fileTb);
}
return AjaxResult.success();
}
public class FilePojoVo {
private String key;
private String fileName;
private Long shardIndex;
private Long shardSize;
private Long shardTotal;
private Long size;
private String suffix;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public Long getShardIndex() {
return shardIndex;
}
public void setShardIndex(Long shardIndex) {
this.shardIndex = shardIndex;
}
public Long getShardSize() {
return shardSize;
}
public void setShardSize(Long shardSize) {
this.shardSize = shardSize;
}
public Long getShardTotal() {
return shardTotal;
}
public void setShardTotal(Long shardTotal) {
this.shardTotal = shardTotal;
}
public Long getSize() {
return size;
}
public void setSize(Long size) {
this.size = size;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
}
@Builder
public class PanoramicFileTb extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** $column.columnComment */
private Integer id;
/** 文件唯一标识 */
@Excel(name = "文件唯一标识")
private String fKey;
/** 第几个分片 */
@Excel(name = "第几个分片")
private Long fIndex;
/** 共有几个分片 */
@Excel(name = "共有几个分片")
private Long fTotal;
/** 文件名称,后面可以返回出去 */
@Excel(name = "文件名称,后面可以返回出去")
private String fName;
public void setId(Integer id)
{
this.id = id;
}
public Integer getId()
{
return id;
}
public void setfKey(String fKey)
{
this.fKey = fKey;
}
public String getfKey()
{
return fKey;
}
public void setfIndex(Long fIndex)
{
this.fIndex = fIndex;
}
public Long getfIndex()
{
return fIndex;
}
public void setfTotal(Long fTotal)
{
this.fTotal = fTotal;
}
public Long getfTotal()
{
return fTotal;
}
public void setfName(String fName)
{
this.fName = fName;
}
public String getfName()
{
return fName;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("id", getId())
.append("fKey", getfKey())
.append("fIndex", getfIndex())
.append("fTotal", getfTotal())
.append("fName", getfName())
.toString();
}
}
上面两个实体类,FilePojoVo是必须的,需要和页面做数据交互,PanoramicFileTb是非必须的,可以选择把FilePojoVo存储到数据库、内存、redis等都可以,只要能验证到对应文件的md5值是否已存在。我这里存到数据库是因为可以做急速上传,已上传的文件md5值可能会一样,加上其他验证方式,这样已上传过的文件再上传其实就不需要再传了。
下面附上对应的service方法,其中mapper方法无非就是用key去查数据或更新数据。就不提供出来了:
@Override
public void saveFile(PanoramicFileTb fileTb) {
panoramicFileTbMapper.insertPanoramicFileTb(fileTb);
}
@Override
public void UpdateFile(PanoramicFileTb fileTb) {
panoramicFileTbMapper.UpdateFile(fileTb);
}
@Override
public boolean isNotExist(String key){
Integer id = panoramicFileTbMapper.isExist(key);
if (ObjectUtils.isEmpty(id)) {
return true;
}
return false;
}
@Override
public PanoramicFileTb selectLatestIndex(String key) {
PanoramicFileTb fileTb = panoramicFileTbMapper.selectLatestIndex(key);
if (ObjectUtils.isEmpty(fileTb)) {
fileTb = PanoramicFileTb.builder().fKey(key).fIndex(-1L).fName("").build();
}
return fileTb;
}
以上就是后台相关代码,可以根据自己的需求扩展功能。
下面是前端代码,需要npm install --save js-md5安装,引用import md5 from 'js-md5';
<template>
<div class="file-upload">
<h1>大文件分片上传、极速秒传</h1>
<div class="file-upload-el">
<el-upload
class="upload-demo"
drag
ref="upload"
:limit=1
:action="actionUrl"
:on-exceed="handleExceed"
:http-request="handUpLoad"
:auto-upload="false"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
</el-upload>
<el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload">上传到服务器</el-button>
</div>
<div>
<!-- autoplay-->
<el-card class="v-box-card">
<video :src="videoUrl"
controls
autoplay
class="video"
width="100%">
</video>
</el-card>
</div>
</div>
</template>
<script>
export default {
name: "FileUpload",
data() {
return {
actionUrl: 'http://localhost:8098/upload',//上传的后台地址
shardSize: 10 * 1024 * 1024,
videoUrl: ''
};
},
methods: {
handleExceed(files, fileList) {
this.$message.warning(`当前限制选择 1个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`);
},
submitUpload() {
this.$refs.upload.submit();
},
async check(key) {
var res = await this.$http.get('/check', {
params: {'key': key}
})
let resData = res.data;
return resData.data;
},
async recursionUpload(param, file) {
//FormData私有类对象,访问不到,可以通过get判断值是否传进去
let _this = this;
let key = param.key;
let shardIndex = param.shardIndex;
let shardTotal = param.shardTotal;
let shardSize = param.shardSize;
let size = param.size;
let fileName = param.fileName;
let suffix = param.suffix;
let fileShard = _this.getFileShard(shardIndex, shardSize, file);
//param.append("file", fileShard);//文件切分后的分片
//param.file = fileShard;
let totalParam = new FormData();
totalParam.append('file', fileShard);
totalParam.append("key", key);
totalParam.append("shardIndex", shardIndex);
totalParam.append("shardSize", shardSize);
totalParam.append("shardTotal", shardTotal);
totalParam.append("size", size);
totalParam.append("fileName", fileName);
totalParam.append("suffix", suffix);
let config = {
//添加请求头
headers: {"Content-Type": "multipart/form-data"}
};
console.log(param);
var res = await this.$http.post('/upload', totalParam, config)
var resData = res.data;
if (resData.status) {
if (shardIndex < shardTotal) {
this.$notify({
title: '成功',
message: '分片' + shardIndex + '上传完成。。。。。。',
type: 'success'
});
} else {
this.videoUrl = resData.data;//把地址赋值给视频标签
this.$notify({
title: '全部成功',
message: '文件上传完成。。。。。。',
type: 'success'
});
}
if (shardIndex < shardTotal) {
console.log('下一份片开始。。。。。。');
// 上传下一个分片
param.shardIndex = param.shardIndex + 1;
_this.recursionUpload(param, file);
}
}
},
async handUpLoad(req) {
let _this = this;
var file = req.file;
/* console.log('handUpLoad', req)
console.log(file);*/
//let param = new FormData();
//通过append向form对象添加数据
//文件名称和格式,方便后台合并的时候知道要合成什么格式
let fileName = file.name;
let suffix = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length).toLowerCase();
//这里判断文件格式,有其他格式的自行判断
if (suffix != 'mp4') {
this.$message.error('文件格式错了哦。。');
return;
}
// 文件分片
// let shardSize = 10 * 1024 * 1024; //以10MB为一个分片
// let shardSize = 50 * 1024; //以50KB为一个分片
let shardSize = _this.shardSize;
let shardIndex = 1; //分片索引,1表示第1个分片
let size = file.size;
let shardTotal = Math.ceil(size / shardSize); //总片数
// 生成文件标识,标识多次上传的是不是同一个文件
let key = this.$md5(file.name + file.size + file.type);
let param = {
key: key,
shardIndex: shardIndex,
shardSize: shardSize,
shardTotal: shardTotal,
size: size,
fileName: fileName,
suffix: suffix
}
/*param.append("uid", key);
param.append("shardIndex", shardIndex);
param.append("shardSize", shardSize);
param.append("shardTotal", shardTotal);
param.append("size", size);
param.append("fileName", fileName);
param.append("suffix", suffix);
*/
let checkIndexData = await _this.check(key);//得到文件分片索引
let checkIndex = checkIndexData.findex;
//console.log(checkIndexData)
if (checkIndex == -1) {
this.recursionUpload(param, file);
} else if (checkIndex < shardTotal) {
param.shardIndex = param.shardIndex + 1;
this.recursionUpload(param, file);
} else {
this.videoUrl = checkIndexData.fname;//把地址赋值给视频标签
this.$message({
message: '极速秒传成功。。。。。',
type: 'success'
});
}
//console.log('结果:', res)
},
getFileShard(shardIndex, shardSize, file) {
let _this = this;
let start = (shardIndex - 1) * shardSize; //当前分片起始位置
let end = Math.min(file.size, start + shardSize); //当前分片结束位置
let fileShard = file.slice(start, end); //从文件中截取当前的分片数据
return fileShard;
},
}
}
</script>
<style scoped lang="less">
.file-upload {
.file-upload-el {
}
}
.v-box-card{
width: 50%;
}
</style>
源码参考地址:bigfileupload: springboot+vue大文件分片上传 (gitee.com)