先记录下,后面有时间再去实现
可参考链接:vue上传大文件/视频前后端(java)代码
前端 + 后端 实现分片上传(断点续传/极速秒传)
前端slice分片上传,后端用表记录分片索引和分片大小和分片总数,当接受完最后一个分片(分片索引等于分片总数,分片索引从1开始),就合并分片成完成的文件。前端需要递归上传,并显示加载动画和根据分片完成数量显示进度条
临时demo
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><form action="http://www.baidu.com/a"><input type="file" type="hidden" id="file"><!-- 隐藏这个原生的上传文件按钮 --><button type="button" id="btn">触发上传</button></button><!-- 使用它来触发选择图片动作 --></form>
</body>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
<script>/* 监听选择图片的事件 */document.querySelector('#file').onchange = (e)=>{console.log('改变了');console.log(this); // 这里的this变成了Window, 因为写成了箭头函数。console.dir(e.target); // 选择了一个文件,所以数组只有一个元素console.log(e.target.files); // FileList {0: File, length: 1}console.log(e.target.files[0]); // File {name: 'GIF 2023-4-1 18-14-01.gif', lastModified: 1680344051705, lastModifiedDate: Sat Apr 01 2023 18:14:11 GMT+0800 (中国标准时间), webkitRelativePath: '', size: 242914, …}upload(e.target.files[0])document.querySelector('#file').value = '' // 让下次即使选择同一个文件仍能触发onchange事件}function upload(file) {console.log(file instanceof Blob); // true, 而Blob中有个slice方法,可以对文件进行分片let formData = new FormData()let shardSize = 10 * 1024 * 1024let shardIndex = 1let start = shardSize * shardIndexlet end = Math.min(file.size, start + shardSize)console.log(start,end);formData.append('mfile', file.slice(start,end))// 携带数据请求后台$.ajax({url: 'http://127.0.0.1:8083/article/uploadImg',type: 'POST',data: formData,contentType: false,processData: false,cache: false,success: function (data) {if (data.success) {alert('添加成功');} else {alert('添加失败');}}});}/* 点的是#btn,但是我们要触发#file文件上传 */document.querySelector('#btn').onclick = function(){document.querySelector('#file').click()}
</script>
</html>
@PostMapping("uploadImg")
public Result uploadImg(@RequestParam("mfile") MultipartFile mfile) throws IOException {String filename = mfile.getOriginalFilename();mfile.transferTo(new File("D:\\Projects\\vue-springboot\\src\\main\\resources\\static\\img\\"+filename));return Result.ok(filename);
}
spring:servlet:multipart:max-file-size: 50MBmax-request-size: 50MB
oss 将前面的分片上传改为oss里的追加上传
public static void main(String[] args) throws IOException {// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。String endpoint = "https://oss-cn-shenzhen.aliyuncs.com";// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。String accessKeyId = "xxx";String accessKeySecret = "yyy";// 填写Bucket名称,例如examplebucket。String bucketName = "test-zzhua";String objectName = "video/juc.mp4";OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);ObjectMetadata meta = new ObjectMetadata();meta.setObjectAcl(CannedAccessControlList.PublicRead);RandomAccessFile raFile = new RandomAccessFile(new File("D:\\Projects\\vue-springboot\\src\\main\\resources\\static\\img\\juc.mp4"), "r");long totalLen = raFile.length();// 定义每次追加上传的大小 3Mlong everyLen = 3 * 1024 * 1024;long accLen = 0;byte[] bytes = new byte[5 * 1024]; // 缓冲数组5kwhile (true) {// 找到上次读取的位置raFile.seek(accLen);boolean finish = false;ByteArrayOutputStream baos = new ByteArrayOutputStream();// 当前读取累积3M, 或不够3M就读完了int currLen = 0;while (true) {int readLen = raFile.read(bytes);if (readLen == -1) {finish = true;break;}currLen += readLen;baos.write(bytes, 0, readLen);if (currLen >= everyLen) {break;}}// 发起追加请求ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());AppendObjectRequest appendObjectRequest = new AppendObjectRequest(bucketName, objectName, bais,meta);appendObjectRequest.setPosition(accLen);ossClient.appendObject(appendObjectRequest);if (finish) {break;}accLen += currLen;}}
md5大文件计算
javascript实现
参考:SpringBoot大文件上传–前端计算文件的MD5
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.js"></script>
</head>
<body><form id="from" method="post" action="/upload" enctype="multipart/form-data"><table><tr><td><input id="md5" name="md5"><input id="file" name="upload" type="file"><input id="submit" type="submit" value="上传"></td></tr></table></form></body>
<script>//注意此方法引用了SparkMD5库 library:https://github.com/satazor/SparkMD5//监听文本框变化document.getElementById("file").addEventListener("change", function() {//声明必要的变量chunks=0;currentChunk=0;var fileReader = new FileReader();//一个用来读取文件的对象//文件分割方法(注意兼容性)blobSlice = File.prototype.mozSlice || File.prototype.webkitSlice || File.prototype.slice,file = document.getElementById("file").files[0],//文件每块分割2M,计算分割详情chunkSize = 2097152,chunks = Math.ceil(file.size / chunkSize),//文件分成了几块currentChunk = 0,//当前处理的第几块spark = new SparkMD5();//创建md5对象(基于SparkMD5)//每块文件读取完毕之后的处理fileReader.onload = function(e) {console.log("读取文件", currentChunk + 1, "/", chunks);//每块交由sparkMD5进行计算spark.appendBinary(e.target.result);currentChunk++;//如果文件处理完成计算MD5,如果还有分片继续处理if (currentChunk < chunks) {loadNext();} else {md5=spark.end();//最终的MD5console.log("MD5:"+md5);}};//处理单片文件的上传function loadNext() {var start = currentChunk * chunkSize,end = start + chunkSize >= file.size ? file.size : start + chunkSize;fileReader.readAsBinaryString(blobSlice.call(file, start, end));//blobSlice.call(file, start, end)每次执行到blobSlice的时候就会跳转到blobSlice定义的地方,可以理解为一个循环}loadNext();});
</script></html>
java实现
参考:详解JAVA中获取文件MD5值的四种方法
须引入commons-codec包
String s = DigestUtils.md5Hex(new FileInputStream(new File("D:\\documents\\尚硅谷谷粒学院项目视频教程\\6 - What If I Want to Move Faster.mp4")));
ystem.out.println(s);
vue上传大文件/视频前后端(java)代码
<template><div><!-- 上传组件 --><el-upload action drag :auto-upload="false" :show-file-list="false" :on-change="handleChange"><i class="el-icon-upload"></i><div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div><div class="el-upload__tip" slot="tip">大小不超过 200M 的视频</div></el-upload><!-- 进度显示 --><div class="progress-box"><span>上传进度:{{ percent.toFixed() }}%</span><el-button type="primary" size="mini" @click="handleClickBtn">{{ upload | btnTextFilter}}</el-button></div></div>
</template><script>import { getUUID } from '@/utils'import axios from 'axios'export default {name: 'singleUpload',props: {value: String},filters: {btnTextFilter(val) {return val ? '暂停' : '继续'}},data() {return {videoUrl: this.value,percent: 0,upload: true,percentCount: 0,suffix: '',fileName: '',preName: ''}},methods: {emitInput(val) {this.$emit('input', val)},async handleChange(file) {if (!file) returnthis.percent = 0this.percentCount = 0// 获取文件并转成 ArrayBuffer 对象const fileObj = file.rawlet buffertry {buffer = await this.fileToBuffer(fileObj)} catch (e) {console.log(e)}// 将文件按固定大小(2M)进行切片,注意此处同时声明了多个常量const chunkSize = 2097152,chunkList = [], // 保存所有切片的数组chunkListLength = Math.ceil(fileObj.size / chunkSize), // 计算总共多个切片suffix = /\.([0-9A-z]+)$/.exec(fileObj.name)[1] // 文件后缀名this.preName = getUUID() //生成文件名前缀this.fileName = this.preName+'.'+suffix //文件名 // 生成切片,这里后端要求传递的参数为字节数据块(chunk)和每个数据块的文件名(fileName)let curChunk = 0 // 切片时的初始位置for (let i = 0; i < chunkListLength; i++) {const item = {chunk: fileObj.slice(curChunk, curChunk + chunkSize),fileName: `${this.preName}_${i}.${suffix}` // 文件名规则按照 filename_1.jpg 命名}curChunk += chunkSizechunkList.push(item)}this.chunkList = chunkList // sendRequest 要用到this.sendRequest()},// 发送请求sendRequest() {const requestList = [] // 请求集合this.chunkList.forEach((item, index) => {const fn = () => {const formData = new FormData()formData.append('chunk', item.chunk)formData.append('filename', item.fileName)return axios({url: 'http://localhost/api/chunk',method: 'post',headers: { 'Content-Type': 'multipart/form-data' },data: formData}).then(response => {if (response.data.errcode === 0) { // 成功if (this.percentCount === 0) { // 避免上传成功后会删除切片改变 chunkList 的长度影响到 percentCount 的值this.percentCount = 100 / this.chunkList.length}if (this.percent >= 100) {this.percent = 100;}else {this.percent += this.percentCount // 改变进度}if (this.percent >= 100) {this.percent = 100;}this.chunkList.splice(index, 1) // 一旦上传成功就删除这一个 chunk,方便断点续传}else{this.$mseeage({type: "error",message: response.data.message})return }})}requestList.push(fn)})let i = 0 // 记录发送的请求个数// 文件切片全部发送完毕后,需要请求 '/merge' 接口,把文件名传递给服务器const complete = () => {axios({url: 'http://localhost/api/merge',method: 'get',params: {filename: this.fileName },timeout: 60000}).then(response => {if (response.data.errcode === 0) { // 请求发送成功// this.videoUrl = res.data.pathconsole.log(response.data)}})}const send = async () => {if (!this.upload) returnif (i >= requestList.length) {// 发送完毕complete()return}await requestList[i]()i++send()}send() // 发送请求this.emitInput(this.fileName)},// 按下暂停按钮handleClickBtn() {this.upload = !this.upload// 如果不暂停则继续上传if (this.upload) this.sendRequest()},// 将 File 对象转为 ArrayBuffer fileToBuffer(file) {return new Promise((resolve, reject) => {const fr = new FileReader()fr.onload = e => {resolve(e.target.result)}fr.readAsArrayBuffer(file)fr.onerror = () => {reject(new Error('转换文件格式发生错误'))}})}}}
</script><style scoped "">.progress-box {box-sizing: border-box;width: 360px;display: flex;justify-content: space-between;align-items: center;margin-top: 10px;padding: 8px 10px;background-color: #ecf5ff;font-size: 14px;border-radius: 4px;}.videoShow{width: 100%;height:600px;padding: 10px 0 50px;position: relative;
}
#videoBox{object-fit:fill;border-radius: 8px;display: inline-block;vertical-align: baseline;
}
.video-img{position: absolute;top: 0;bottom: 0;width: 100%;z-index: 999;background-size:100%;cursor:pointer;}
.video-img img {display:block;width: 60px;height: 60px;position: relative;top:260px;left: 48%;
}
video:focus {outline: -webkit-focus-ring-color auto 0px;}
</style>
/*** 获取uuid*/export function getUUID () {return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {return (c === 'x' ? (Math.random() * 16 | 0) : ('r&0x3' | '0x8')).toString(16)})
}
String dirPath = "D:\\video\\train"
@PostMapping("/chunk")
public Result upLoadChunk(@RequestParam("chunk") MultipartFile chunk,@RequestParam("filename") String fileName) {// 用于存储文件分片的文件夹File folder = new File(dirPath);if (!folder.exists() && !folder.isDirectory())folder.mkdirs();// 文件分片的路径String filePath = dirPath + fileName;try {File saveFile = new File(filePath);// 写入文件中//FileOutputStream fileOutputStream = new FileOutputStream(saveFile);//fileOutputStream.write(chunk.getBytes());//fileOutputStream.close();chunk.transferTo(saveFile);return new Result();} catch (Exception e) {e.printStackTrace();}return new Result();
}@GetMapping("/merge")
public Result MergeChunk(@RequestParam("filename") String filename) {String preName = filename.substring(0,filename.lastIndexOf("."));// 文件分片所在的文件夹File chunkFileFolder = new File(dirPath);// 合并后的文件的路径File mergeFile = new File(dirPath + filename);// 得到文件分片所在的文件夹下的所有文件File[] chunks = chunkFileFolder.listFiles();System.out.println(chunks.length);assert chunks != null;// 排序File[] files = Arrays.stream(chunks).filter(file -> file.getName().startsWith(preName)).sorted(Comparator.comparing(o -> Integer.valueOf(o.getName().split("\\.")[0].split("_")[1]))).toArray(File[]::new);try {// 合并文件RandomAccessFile randomAccessFileWriter = new RandomAccessFile(mergeFile, "rw");byte[] bytes = new byte[1024];for (File chunk : files) {RandomAccessFile randomAccessFileReader = new RandomAccessFile(chunk, "r");int len;while ((len = randomAccessFileReader.read(bytes)) != -1) {randomAccessFileWriter.write(bytes, 0, len);}randomAccessFileReader.close();System.out.println(chunk.getName());chunk.delete(); // 删除已经合并的文件}randomAccessFileWriter.close();} catch (Exception e) {e.printStackTrace();}return new Result();
}