前端 + 后端 实现分片上传(断点续传/极速秒传)

news/2024/12/1 0:21:41/

先记录下,后面有时间再去实现
可参考链接: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();
}

http://www.ppmy.cn/news/46519.html

相关文章

BUUCTF-PWN-pwn1_sctf_2016

下载 放入 ubuntu里查信息 现在这些保护我都没有遇到 以后慢慢做应该是会遇到的 然后进行发现是32 所以我们记住 如果栈溢出漏洞 我们需要4个字节填满基地址 放入ida32 查看字符串 发现 cat flag 敏感字符串 然后我们就看引用 先记住地址 为 0x8048F0D 然后开始进去 发…

FPGA与ASIC的区别

先来看张图&#xff0c;本图体现出了集成电路产业链&#xff1a;设计业、制造业、封测业。 关于制造、封装测试我们看两张图稍作了解即可&#xff1a; 数字IC ASIC设计流程及EDA工具&#xff1a; &#xff08;1&#xff09;了解数字IC设计&#xff1a;在VLSI时代&#xff…

linux驱动开发 - 04_Linux 设备树学习 - DTS语法

文章目录 Linux 设备树学习 - DTS语法1 什么是设备树&#xff1f;2 DTS、DTB和DTC3 DTS 语法3.1 dtsi 头文件3.2 设备节点3.3 标准属性1、compatible 属性2、model 属性3、status 属性4、#address-cells 和#size-cells 属性5、reg 属性6、ranges 属性7、name 属性8、device_typ…

yolov8 做图片分类和 ResNet 的对比

文章大纲 yolo v8 图片分类简介与原理说明训练代码数据集的组织多尺度训练参考内容ResNet简介与原理说明训练代码与使用说明Usage其他 牛逼 分类模型分类效果不好怎么办?参考文献和学习路径自己实现windows 下基于pytorch 图片分类教程yolo v8 图片分类 简介与原理说明 简单…

【NestJs】使用连接mysql企业级开发规范

本篇将介绍如何建立 NestJs 的数据库连接、并使用数据库联表查询。 简介 Nest 与数据库无关&#xff0c;允许您轻松地与任何 SQL 或 NoSQL 数据库集成。根据您的偏好&#xff0c;您有许多可用的选项。一般来说&#xff0c;将 Nest 连接到数据库只需为数据库加载一个适当的 No…

毕业生招聘信息的发布与管理系统(论文+设计)

前 言 当今&#xff0c;人类社会已经进入信息全球化和全球信息化、网络化的高速发展阶段。丰富的网络信息已经成为人们工作、生活、学习中不可缺少的一部分。人们正在逐步适应和习惯于网上贸易、网上购物、网上支付、网上服务和网上娱乐等活动&#xff0c;人类的许多社会活动…

Node 01-Buffer

Buffer&#xff08;缓冲器&#xff09; 概念 Buffer 是一个类似于数组的 对象 &#xff0c;用于表示固定长度的字节序列 Buffer 本质是一段内存空间&#xff0c;专门用来处理 二进制数据 。 特点 Buffer 大小固定且无法调整Buffer 性能较好&#xff0c;可以直接对计算机内存…

java SimpleDateFormat和Calendar日期类

目录 一、SimpleDateFormat使用二、Calendar使用 一、SimpleDateFormat使用 使用Date直接输出日期时&#xff0c;是使用系统默认的格式输出&#xff0c;所以需要使用SimpleDateFormat来格式化日期。 那么SimpleDateFormat类怎么使用呢&#xff0c;我们需要先了解此类的格式化符…