springboot+vue+elementui大文件分片上传
工具类方法:
/** * 大文件分片上传 * @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>
本文来自博客园,作者:Rolay,转载请注明原文链接:https://www.cnblogs.com/rolayblog/p/18439702