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

news2024/11/16 1:55:22

一、前端

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 = fileName
  fileItem.fileSize = fileSize
  fileItem.state = 1  //解码中
  fileItem.progress = 0  //进度是0
  fileItem.filePid = 102903232
  fileItem.fileMd5 = ""
  fileItem.uploadSize = 0

  fileUploadList.value.addFile(fileItem)

  //弹框显示
  isVisible.value = true

  //获得文件的md5
  if (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) {
      //文件最后的end
      let 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 2MB
            chunks = 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 buffer
            currentChunk++;

            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.fileCategory

  if (category == 18 || category == 19) {
    return 1
  }
  else if (category == 13)
    return 2

  else if (category == 9 || category == 14 || category == 5) {
    imageList.value.push(getFileUrl())
    return 3
  }
  else if (category == 20 || category == 11 || category == 15)
    return 4

  else 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+md5
            String 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.coloradmin.cn/o/1839725.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【链表专题】深入探索链表:文章索引与知识架构(链表的概念、实现、应用、经典例题大合集)

&#x1f493; 博客主页&#xff1a;倔强的石头的CSDN主页 &#x1f4dd;Gitee主页&#xff1a;倔强的石头的gitee主页 ⏩ 文章专栏&#xff1a;《数据结构与算法》 期待您的关注 目录 一、引言 二、链表的基础概念 &#x1f343;链表的概念 &#x1f343;顺序表和链表的对…

CPN IDE实现分层效果

Shift键鼠标选中要分层的库所和变迁&#xff01;然后create subpage。 Subpage是这样的&#xff0c;不会像CPN tools里面自动生成IN和OUT库所&#xff0c;但是也能正确运行。 虽然父页面在运行中有标红&#xff1a;"port not defined" 错误通常意味着在模型中有一些连…

debug调试高级功能 断点、布局 及Android Studio常用快捷按键使用详情

文章目录 debug断点篇&#xff1a;打临时断点&#xff08;只用一次&#xff09;&#xff1a;alt断点条件断点&#xff1a;在断点上&#xff0c;点击右键&#xff0c;在Condition那里&#xff0c;设置我们需要的值&#xff0c;循环就会自动停到我们设置的那个值那里依赖断点&…

48-2 内网渗透 - 利用Metasploit提权

一、Metasploit提权过程概述 Metasploit是一个开源的安全漏洞检测工具,广泛用于安全和IT专业人士识别、验证和利用安全漏洞,同时也支持专家驱动的安全评估和管理。 提权过程详解 1)生成后门 在Kali Linux上使用msfvenom生成反向连接的后门文件。 # ip 要改成自…

Ceph入门到精通-对象存储的冷热分离实现方法,该如何配置

实现对象存储的冷热分离的基本方法,包括桶创建、冷热池子创建、生命周期配置以及回收设置的步骤概述: 一、桶创建 使用AWS CLI创建S3存储桶,可以通过指定LocationConstraint参数来创建存储桶,并使用--endpoint-url指向RADOS Gateway的地址。 aws s3api create-bucket --…

进化版ChatGPT的Siri今年无缘上线!苹果正打造史上最薄iPhone 17

目录 01 超强Siri助手预计2025年上线 02 集成ChatGPT但没有买单 03 iPhone 17更轻薄 最新报道称&#xff0c;苹果的AI功能将在未来几个月逐步推出&#xff0c;并持续到2025年。 据称&#xff0c;今年夏天结束前&#xff0c;开发者们仍无法试用和体验。 因此&#xff0c;在即…

连获殊荣,天润融通以AI技术重塑企业客户联络体验!

天润融通又获奖了。 2024年3月22日&#xff0c;「ToB行业头条」联合3W集团共同举办的「2024ToB头条行业大会」在北京举行。 为表彰在过去一年中表现卓越、对行业发展作出显著贡献的企业、产品和数字化转型案例&#xff0c;大会颁布了ToB年度榜单【2023中国ToB行业影响力价值榜…

重生之 SpringBoot3 入门保姆级学习(22、场景整合 Swagger 接口文档)

重生之 SpringBoot3 入门保姆级学习&#xff08;22、场景整合 Swagger 接口文档&#xff09; 6.2 Swagger 接口文档 6.2 Swagger 接口文档 1、将 starter 导入 Maven 官网 https://springdoc.org/<dependency><groupId>org.springdoc</groupId><artifact…

基于Redis提高查询性能(保持数据一致性)

Redis实战篇 | Kyles Blog (cyborg2077.github.io) 目录 背景 商户查询缓存(根据ID查询&#xff09; 根据店铺类型查询&#xff08;List型&#xff09; 缓存更新策略&#xff08;保证数据一致性&#xff09; 案例&#xff08;利用缓存更新策略&#xff09; 背景 起初客户端…

2024年【T电梯修理】考试内容及T电梯修理考试总结

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 T电梯修理考试内容参考答案及T电梯修理考试试题解析是安全生产模拟考试一点通题库老师及T电梯修理操作证已考过的学员汇总&#xff0c;相对有效帮助T电梯修理考试总结学员顺利通过考试。 1、【多选题】TSGT7005-2012《…

amov无人机连接;+数据传输;啊啊啊啊啊

socket传输数据: 局域网连接 连接---通信(命令行直接;)--- 传输数据(socket)--传输内容:launch文件; qgc连接; 1.局域网下的通信 1.1 局域网 厂家提供的方式是通过Homer图数传工具(硬件)构建的amov局域网实现通信连接. 好处是通信距离足够长,支持150m;坏处是"局部&qu…

03 - matlab m_map地学绘图工具基础函数 - 设置坐标系(m_coord)

03 - matlab m_map地学绘图工具基础函数 - 设置坐标系&#xff08;m_coord&#xff09; 0. 引言1. m_proj使用方法2. 结语 0. 引言 上一篇介绍了m_proj函数用于初始化投影&#xff0c;本篇介绍的函数m_coord用于初始化地理坐标系或地磁坐标系&#xff0c;地理/地磁坐标系和投影…

图解Linux内核(基于6.x):解读Linux内存反向映射之匿名映射

文章目录 &#x1f4d1;前言一、匿名映射的mapping二、推荐阅读2.1 一图速览2.2 内容简介 &#x1f4d1;前言 内存映射中&#xff0c;我们经常讨论的是由虚拟内存定位物理内存&#xff08;也就是folio或者page&#xff09;&#xff0c;实际上在很多场景中&#xff08;比如内存回…

在Ubuntu中创建Ruby on Rails项目并搭建数据库

新建Rails项目 先安装bundle Ruby gem依赖项工具&#xff1a; sudo apt install bundle 安装Node.js: sudo apt install nodejs 安装npm 包管理器&#xff1a; sudo apt install npm 安装yarn JavaScript包管理工具&#xff1a; sudo apt install yarn 安装webpacker: …

微信小程序毕业设计-电影院订票选座系统项目开发实战(附源码+论文)

大家好&#xff01;我是程序猿老A&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;微信小程序毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计…

基于riscv架构的DAYU800开发板套件介绍

一、简介 润和-SCDAYU800 开发平台基于平头哥高性能 RISC-V 开源架构曳影 TH1520 芯片&#xff0c;集成4核高性能RISC-V处理器玄铁C910的平头哥曳影1520&#xff0c;AI算力达4TOPs支持蓝牙、音频、视频和摄像头等功能,支持多种视频输入输出接口,并提供丰富的扩展接口&#xff…

即时到账支付系统源码第四方支付平台源码(支付宝/QQ钱包/微信二维码收款+附配套软件)

即时到账支付系统源码第四方支付平台源码价值10万&#xff0c;支付宝/QQ钱包/微信二维码收款&#xff0c;附配套软件 开发语言&#xff1a;phpmysql 这个是一个可以跟码支付一样用自己的二维码收款的网站 还可以作为即时到账 代收款 或者易支付使用后台配置好就行&#xff…

Java基础 - 练习(三)打印空心菱形

Java基础练习 打印空心菱形&#xff0c;先上代码&#xff1a; public static void diamond() {//控制行数for (int i 1; i < 4; i) {//空格的个数for (int k 1; k < 4 - i; k) {System.out.print(" ");}//控制星星个数的时候和行有关for (int j 1; j <…

网络层 IP协议【计算机网络】【协议格式 || 分片 || 网段划分 || 子网掩码】

博客主页&#xff1a;花果山~程序猿-CSDN博客 文章分栏&#xff1a;Linux_花果山~程序猿的博客-CSDN博客 关注我一起学习&#xff0c;一起进步&#xff0c;一起探索编程的无限可能吧&#xff01;让我们一起努力&#xff0c;一起成长&#xff01; 目录 一&#xff0c;前提 二&…

3.什么是计算机语言

什么是计算机语言 ? 计算机语言&#xff08;Computer Language&#xff09;指用于人与计算机之间通讯的语言。计算机语言是人与计算机之间传递信息的媒介。计算机系统最大特征是指令通过一种语言传达给机器。为了使电子计算机进行各种工作&#xff0c;就需要有一套用以编写计…