Vue + SpringBoot 实现文件的断点上传、秒传,存储到Minio

一、前端

1. 计算文件的md5值

  前端页面使用的elment-plus的el-upload组件。

    <el-upload action="#" :multiple="true" :auto-upload="false" :on-change="handleChange" :show-file-list="false"><FileButton content="上传文件" type="primary" class="file-button" /></el-upload>

当上传文件后,会调用handleChange 方法,可以在这里进行文件相关的操作。

//处理文件上传
const handleChange = async (uploadFile) => {//文件名字let fileName = uploadFile.name//文件的大小const fileSize = uploadFile.size || 0//当前的文件对象let fileItem = {}fileItem.fileName = fileNamefileItem.fileSize = fileSizefileItem.state = 1  //解码中fileItem.progress = 0  //进度是0fileItem.filePid = 102903232fileItem.fileMd5 = ""fileItem.uploadSize = 0fileUploadList.value.addFile(fileItem)//弹框显示isVisible.value = true//获得文件的md5if (uploadFile.raw) {await generateMD5OfFile(uploadFile.raw).then(res => {fileItem.fileMd5 = res})}fileUploadList.value.addMd5(fileItem.fileName, fileItem.fileMd5)fileUploadList.value.changeFileState(fileItem.fileName, 2)//分片上传let chunkTotals = Math.ceil(fileSize / chunkSize);//分片上传if (chunkTotals > 0) {for (let chunkNumber = 0, start = 0; chunkNumber < chunkTotals; chunkNumber++, start += chunkSize) {//文件最后的endlet end = Math.min(fileSize, start + chunkSize);// el-mement - plus中,上传的文件就在raw里面const files = uploadFile.raw?.slice(start, end)//上传的结果const result = await uploadFileToServer(files, chunkNumber + 1, chunkTotals, fileName , getCurrentId(), fileItem.fileMd5,userId)console.log(result.data)console.log(result.data.data)if (result.data.data.status === 1) {// console.log("上传中")//上传的进度fileUploadList.value.changeProgress(fileItem.fileName, ((end / fileSize) * 100).toFixed(1))//修改已经上传完成的文件大小fileUploadList.value.changeUploadSize(fileItem.fileName, end)} else if (result.data.data.status === 3) {// console.log("上传成功!"),这里是弹窗显示的文件上传进度,可以适当修改fileUploadList.value.changeFileState(fileItem.fileName, 3)  //上传完成fileUploadList.value.changeProgress(fileItem.fileName, 100)  // 进度100%//通过main,进行刷新$emit("addChangeNum")return ; //结束} else {message("上传失败", 'error')return;  //结束}}}
}

 计算文件的MD5值

//计算文件的md5
function generateMD5OfFile(file) {return new Promise((resolve, reject) => {let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,                        // Read in chunks of 2MBchunks = Math.ceil(file.size / chunkSize),currentChunk = 0,spark = new SparkMD5.ArrayBuffer(),fileReader = new FileReader();fileReader.onload = function (e) {console.log('read chunk nr', currentChunk + 1, 'of', chunks);spark.append(e.target.result);                   // Append array buffercurrentChunk++;if (currentChunk < chunks) {loadNext();} else {resolve(spark.end())}};fileReader.onerror = function () {reject('MD5 calc error')};function loadNext() {let start = currentChunk * chunkSize,end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));}loadNext();})
}

2.计算文件切片数量

自定义文件切片大小 

//默认分片大小
const chunkSize = 5 * 1024 * 1024

3.分片上传文件

上传文件到服务器 

// 上传文件到服务器
const uploadFileToServer = async (file, chunkNumber, chunkTotal, fileName,filePid, fileMd5,userId) => {const form = new FormData();// 这里的data是文件form.append("file", file);form.append("chunkNumber", chunkNumber);form.append("chunkTotal", chunkTotal);form.append("fileName", fileName)form.append("fileMd5", fileMd5)form.append("filePid", filePid)form.append("userId", userId)var result = await axios({url: env_server_production + '/file/upload',headers: { 'Content-Type': 'multipart/form-data' },method: "post",timeout: 1000000,data: form})return result
}

4.实现相关文件的预览

可以简单的实现对一些文件的预览,比如图片、视频、word、pdf等等。

pdf:

等等

这里使用的是vue-office 

<template><div class="preview-body"><!-- word --><vue-office-docx v-if="getFileType() == 1" :src="getFileUrl()" style="height: 400px;" @rendered="renderedHandler"@error="errorHandler" /><!-- pdf --><vue-office-pdf v-else-if="getFileType() == 2" :src="getFileUrl()" style="height: 400px;"@rendered="renderedHandler" @error="errorHandler" /><!-- iamge --><div v-else-if="getFileType() == 3"><el-image :src="getFileUrl()" style="height: 100px; width: 100px;" :zoom-rate="1.2" :max-scale="7":min-scale="0.2" :preview-src-list="imageList" :initial-index="4" /><br><el-text style="margin-left: 0px;" link type="primary">点击图片查看详情</el-text></div><!-- 不支持显示 --><div v-else-if="getFileType() == 4"><br>该文件不支持在线浏览,请下载后查看!</div><!-- 视频 --><div v-else-if="getFileType() == 5"><video autoplay width="1200px" height="400px" controls :src="getFileUrl()"id="myVideo"></video></div><!-- 文本显示 --><div v-else><el-scrollbar height="400px" class="document-preview"><pre>{{ documentContent }}</pre></el-scrollbar></div></div>
</template><script setup>
//引入相关样式
import VueOfficeDocx from '@vue-office/docx'
import VueOfficePdf from '@vue-office/pdf'
import '@vue-office/docx/lib/index.css'
import { ref } from 'vue'
import axios from 'axios';const props = defineProps(['file'])
const video = document.getElementById("myVideo")const getFileUrl = () => {return "http://60.205.141.200:9000/" + props.file.filePath;
}
const getFileType = () => {let category = props.file.fileCategoryif (category == 18 || category == 19) {return 1}else if (category == 13)return 2else if (category == 9 || category == 14 || category == 5) {imageList.value.push(getFileUrl())return 3}else if (category == 20 || category == 11 || category == 15)return 4else if (category == 12) {//视频return 5} else {//文本readDocumentContent();}}const readDocumentContent = async () => {var res = await axios.get(getFileUrl(), {responseType: 'text',})documentContent.value = `\n${res.data}\n`
}
//文件中的内容
const documentContent = ref('')
//图片列表
const imageList = ref([])const renderedHandler = () => {console.log("渲染成功")
}
const errorHandler = () => {console.log("渲染失败")
}</script><style lang="scss" scoped>
.document-preview {margin-right: 100px;background-color: #ccc;width: 1164px;border: 2px solid #ccc;height: 400px;border-radius: 0 0 10px 10px;text-align: left;
}
pre {font-family: 'Microsoft YaHei';
}
</style>

二、后端

后端使用minio,minio先接收分片文件,上传完成所有的分片文件后,在合并分片文件,删除中间文件即可。

1.接收分片文件、合并文件。

/*** 上传文件方法。* 该方法负责检查文件是否已存在,如果存在,则返回已存在标志;如果不存在且是完整文件,则上传文件到MinIO并保存文件信息到数据库。** @param fileVO 文件相关信息VO,包含文件本身、MD5、文件名等。* @return 如果文件已存在,返回秒传状态码;如果文件上传完成,返回上传完成状态码;否则返回null。* @throws GeneralException 如果文件为空,抛出通用异常。*/@Override@Transactional(rollbackFor = Exception.class)  //所有的操作都在一个事务里面。public HashMap<Object, Object> uploadFile(FileVO fileVO) {if(fileVO.getFile().isEmpty())throw  new GeneralException("文件上传异常");FileInfo insertItem = new FileInfo();Date now = new Date();HashMap<Object, Object> map = new HashMap<>();//第一片文件if(fileVO.getChunkNumber() == 1){//先去数据库看看有没有这个文件QueryWrapper<FileInfo> queryWrapper = new QueryWrapper<>();queryWrapper.eq("file_md5", fileVO.getFileMd5());//通过Md5查询,别人是不是已经传过这个文件了(文件名不影响文件的MD5值)。List<FileInfo> fileInfoList = fileInfoMapper.selectList(queryWrapper);FileInfo fileInfo = null;if(fileInfoList.size() > 0){fileInfo = fileInfoList.get(0);}//别人已经上传过这个文件了,直接秒传if(fileInfo != null){log.info("服务器中有相同的文件,直接秒传");//说明minIO中有对应的文件insertItem.setUserId(fileVO.getUserId());insertItem.setFileMd5(fileVO.getFileMd5());insertItem.setFileName(fileInfo.getFileName());insertItem.setFileCategory(fileInfo.getFileCategory());insertItem.setFileId(StringUtil.getRandomString(10));insertItem.setDelFlag(FileDelFlagEnums.USING.getFlag());insertItem.setFilePid(fileVO.getFilePid());insertItem.setFilePath(fileInfo.getFilePath());insertItem.setCreateTime(now);insertItem.setFileSize(fileInfo.getFileSize());insertItem.setState(UploadStatus.UPLOAD_FINISH.getStatus());fileInfoMapper.insert(insertItem);System.err.println(insertItem);map.put("status",UploadStatus.UPLOAD_FINISH.getStatus());map.put("fileId",insertItem.getFileId());return map;}//插入 一个切片redisUtil.set(fileVO.getFileMd5(),0);}if(Integer.parseInt(redisUtil.get(fileVO.getFileMd5()).toString()) >= fileVO.getChunkNumber()){//说明这片文件已经上传过了。map.put("status",UploadStatus.UPLOADING.getStatus());return map;}//只有一段,直接放到服务器就行if(fileVO.getChunkTotal() == 1){int lastDotIndex = fileVO.getFileName().lastIndexOf(".");String type = fileVO.getFileName().substring(lastDotIndex + 1);String url = minioUtils.uploadFile(MessageConstant.MINIO_BUCKET,fileVO.getFileName(), fileVO.getFile());insertItem.setUserId(fileVO.getUserId());insertItem.setFileMd5(fileVO.getFileMd5());insertItem.setFileName(fileVO.getFileName());insertItem.setFileCategory(FileCategoryEnums.getByCode(type).getCategory());insertItem.setFileId(StringUtil.getRandomString(10));insertItem.setDelFlag(FileDelFlagEnums.USING.getFlag());insertItem.setFilePid(fileVO.getFilePid());insertItem.setFilePath(url);insertItem.setCreateTime(now);insertItem.setFileSize(fileVO.getFile().getSize());insertItem.setState(UploadStatus.UPLOAD_FINISH.getStatus());fileInfoMapper.insert(insertItem);//删除redis中的切片上传信息redisUtil.del(fileVO.getFileMd5());map.put("status",UploadStatus.UPLOAD_FINISH.getStatus());map.put("fileId",insertItem.getFileId());return map;}log.info("分片上传====> md5 :{} ,=====> index :{}",fileVO.getFileMd5(),fileVO.getChunkNumber());//不止一片,继续上传//放切片文件的目录是 文件的userId + md5值,这个是唯一的。String objectName = fileVO.getUserId() + fileVO.getFileMd5() ;try {minioUtils.putChunkObject(fileVO.getFile().getInputStream(), MessageConstant.MINIO_BUCKET, objectName + "/" + fileVO.getChunkNumber());} catch (IOException e) {throw new GeneralException("文件上传异常!");}//最后一片,进行合并if(Objects.equals(fileVO.getChunkNumber(), fileVO.getChunkTotal())){//获得文件类型int lastDotIndex = fileVO.getFileName().lastIndexOf(".");String type = fileVO.getFileName().substring(lastDotIndex + 1);//objectName : userId+md5String filePath = minioUtils.composeObject(MessageConstant.MINIO_BUCKET,MessageConstant.MINIO_BUCKET,objectName, type);insertItem.setUserId(fileVO.getUserId());insertItem.setFileMd5(fileVO.getFileMd5());insertItem.setFileName(fileVO.getFileName());insertItem.setFileCategory(FileCategoryEnums.getByCode(type).getCategory());insertItem.setFileId(StringUtil.getRandomString(10));insertItem.setDelFlag(FileDelFlagEnums.USING.getFlag());insertItem.setFilePid(fileVO.getFilePid());insertItem.setFilePath(filePath);insertItem.setCreateTime(now);Long fileSize = MessageConstant.DEFAULT_CHUNK_SIZE * (fileVO.getChunkTotal() - 1) + fileVO.getFile().getSize();insertItem.setFileSize(fileSize);insertItem.setState(UploadStatus.UPLOAD_FINISH.getStatus());//插入一条数据System.out.println(fileInfoMapper.insert(insertItem));//删除minio中的临时文件目录System.out.println(minioUtils.deleteFolder(MessageConstant.MINIO_BUCKET, objectName));//删除redis中的切片上传信息redisUtil.del(fileVO.getFileMd5());map.put("status",UploadStatus.UPLOAD_FINISH.getStatus());map.put("fileId",insertItem.getFileId());return map;}//更新redis中的切片上传信息redisUtil.incrby(fileVO.getFileMd5(),1);//上传中map.put("status",UploadStatus.UPLOADING.getStatus());return map;}

如何做到秒传?

一个文件有个不重复的md5值,所谓的秒传其实就是你要上传的文件,别人已经上传过了,minio中已经有这个文件了,再解析完文件的md5值之后,后端发现数据库中md5存在了,所以就不用上传文件了,直接在数据库中创建一个信息即可,也就实现了秒传。

如何做到断点传递?

传统传递过程是一整个文件上传,如果中断了下次传的时候,需要重新上传;断点传递,每次传递的时候,可以把分片信息放到redis中,同时下一次传分片的时候,判断一下,redis中时候已经有了这个分片,如果有就不用上传此分片文件,即断点传递。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/359921.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python中的<class ‘complex‘>

一般编程里面不怎么会讲&#xff0c;但是还是挺强大的一个类。 在 Python 中&#xff0c;<class complex> 表示复数类型。复数是一种包含实部和虚部的数学数&#xff0c;可以用 a bj 的形式表示&#xff0c;其中 a 表示实部&#xff0c;b 表示虚部&#xff0c;j 是虚数…

kylinos 国产操作系统离线安装firefox 麒麟操作系统安装新版本firefox

1. 火狐地址&#xff1a; 下载 Firefox 浏览器&#xff0c;这里有简体中文及其他 90 多种语言版本供您选择 2. 选择&#xff1a; 3. 下载完之后&#xff0c;上传到离线机器 4. 解压缩&#xff1a; tar -xvjf firefox-127.0.1.tar.bz2 5. 去点击解压后的文件夹&#xff0c;找…

Redis持久化(RDB、AOF)详解

Redis持久化详解 一、Redis为什么需要持久化&#xff1f; Redis 是一个基于内存的数据库&#xff0c;拥有极高的读写性能&#xff0c;但是内存中的数据在断电或服务器重启时会全部丢失&#xff0c;因此需要一种持久化机制来将数据保存到硬盘上&#xff0c;以便在需要时进行恢复…

Latex学习之“usefont”用法

Latex学习之“\usefont”用法 一、通俗的解释 \usefont 是 LaTeX 中的一个命令&#xff0c;用于在文档中临时改变字体&#xff0c;其基本语法如下&#xff1a; \usefont{字体编码}{字体族}{字体系列}{字体形状}这样看起来好像蛮抽象&#xff0c;你可能以及晕了&#xff0c;什…

《代码大模型安全风险防范能力要求及评估方法》正式发布

​代码大模型在代码生成、代码翻译、代码补全、错误定位与修复、自动化测试等方面为研发人员带来了极大便利的同时&#xff0c;也带来了对安全风险防范能力的挑战。基于此&#xff0c;中国信通院依托中国人工智能产业发展联盟&#xff08;AIIA&#xff09;&#xff0c;联合开源…

Midway + TypeORM项目部署到BT后启动失败,MySQL报错

Midway TypeORM项目部署到BT后启动失败&#xff0c;MySQL报错 前沿 您需要先了解这篇文章&#xff1a;https://blog.csdn.net/weixin_45687201/article/details/139336111 错误日志 服务状态开启后就失败项目日志&#xff0c;输出 \> my-midway-project1.0.0 start \&…

前端vue-cli相关知识与搭建过程(项目创建,组件路由)very 详细

一.关于vue-cli 1.什么是vue Vue (读音 /vju ː /&#xff0c;类似于 view) 是一套用于构建用户界面的渐进式框架。Vue 的核心库只关注视图层&#xff0c;不仅易于上手&#xff0c;还便于与第三方库或既有项目整合。 Vue.js 是前端的主流框架之一&#xff0c;和 Angular.js…

19、删除链表的倒数第

1、题目描述 给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5], n 2 输出&#xff1a;[1,2,3,5]示例 2&#xff1a; 输入&#xff1a;head [1], n 1 输出&#xff1a;[]示例 …

多商户零售外卖超市外卖商品系统源码

构建你的数字化零售王国 一、引言&#xff1a;数字化零售的崛起 在数字化浪潮的推动下&#xff0c;零售业务正经历着前所未有的变革。多商户零售外卖超市商品系统源码应运而生&#xff0c;为商户们提供了一个全新的数字化零售解决方案。通过该系统源码&#xff0c;商户们可以…

基于Openmv的追小球的云台

介绍 在这篇文章&#xff0c;我会先介绍需要用到且需要注意的函数&#xff0c;之后再给出整体代码 在追小球的云台中&#xff0c;比较重要的部分就是云台&#xff08;实质上就是舵机&#xff09;的控制以及对识别的色块位置进行处理得到相应信息后控制云台进行运动 1、舵机模…

qt.qpa.xcb: could not connect to display问题解决

1、问题描述 以服务器pi5作为远程解释器&#xff0c;本地win11使用vscode远程调试视觉时报错如下&#xff1a; qt.qpa.xcb: could not connect to display qt.qpa.plugin: Could not load the Qt platform plugin "xcb" in "xxxxx" even though it was …

Docker Compose--安装Nginx--方法/实例

原文网址&#xff1a;Docker Compose--安装Nginx--方法/实例_IT利刃出鞘的博客-CSDN博客 简介 说明 本文介绍Docker Compose如何安装Nginx。 目录结构 ├── config │ ├── cert │ │ ├── xxx_bundle.pem │ │ └── xxx.key │ ├── conf.d │ …

JAVA笔试题目

1.标识符的使用 2.类名和java文件名的关系 3.java数据类型关系

第3章 小功能大用处-事务与Lua

为了保证多条命令组合的原子性&#xff0c;Redis提供了简单的事务功能以及集成Lua脚本来解决这个问题。 首先简单介绍Redis中事务的使用方法以及它的局限性&#xff0c;之后重点介绍Lua语言的基本使用方法&#xff0c;以及如何将Redis和Lua脚本进行集成&#xff0c;最后给出Red…

React+TS 从零开始教程(3):useState

源码链接&#xff1a;下载 在开始今天的内容之前呢&#xff0c;我们需要先看一个上一节遗留的问题&#xff0c;就是给属性设置默认值。 我们不难发现&#xff0c;这个defaultProps已经被废弃了&#xff0c;说明官方并不推荐这样做。其实&#xff0c;这个写法是之前类组件的时候…

AI金融投资:批量下载巨潮资讯基金招募说明书

打开巨潮资讯的基金招募说明书页面&#xff1a; http://www.cninfo.com.cn/new/fulltextSearch/full?searchkey%E5%B0%81%E9%97%AD%E5%BC%8F%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E8%AF%81%E5%88%B8%E6%8A%95%E8%B5%84%E5%9F%BA%E9%87%91%E6%8B%9B%E5%8B%9F%E8%AF%B4%E6%98%…

计算机网络 访问控制列表以及NAT

一、理论知识 1. 单臂路由 单臂路由是一种在路由器上配置多个子接口的方法&#xff0c;每个子接口代表不同的 VLAN&#xff0c;用于在一个物理接口上支持多 VLAN 通信。此方法使得不同 VLAN 之间可以通过路由器进行通信。 2. NAT (网络地址转换) NAT 是一种在私有网络和公共…

可变分区管理 分区分配算法

First Fit Algorithm Best Fit Algorithm FFA&#xff1a;按照起始地址从小到大&#xff08;本题为分区编号&#xff09;找到第一个能装下进程的起始地址填入第二个表 此时 原表中将起始地址进程大小 分区大小-进程大小 如此继续 BFA&#xff1a;按分区大小排序 从小到大 找到…

【ONLYOFFICE震撼8.1】ONLYOFFICE8.1版本桌面编辑器测评

随着远程工作的普及和数字化办公的发展&#xff0c;越来越多的人开始寻找一款具有强大功能和便捷使用的办公软件。在这个时候&#xff0c;ONLYOFFICE 8.1应运而生&#xff0c;成为了许多用户的新选择。ONLYOFFICE 8.1是一种办公套件软件&#xff0c;它提供了文档处理、电子表格…

面试-细聊synchronized

1.线程安全问题的主要诱因&#xff1a; 存在多条共享数据(临界资源) 存在多条线程共同操作这些共享数据 解决问题的根本方法&#xff1a; 同一时刻有且仅有一个线程在操作共享数据&#xff0c;其他线程必须等到该线程处理完数据后在对共享数据进行操作。 2.synchroized锁 分…