springboot+vue+elementui大文件分片上传

news2024/12/26 13:42:27

工具类方法:

 /**
     * 大文件分片上传
     * @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;
    }
}
FilePojoVo
@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();
    }
}
PanoramicFileTb

上面两个实体类,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>
前端代码

源码参考地址:bigfileupload: springboot+vue大文件分片上传 (gitee.com)

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

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

相关文章

【数据结构】MapSet

【概念】 Map和Set是一种专门用于搜索的数据结构&#xff0c;其搜索效率与具体实例化的子类数量有关&#xff0c;本质上是一颗二叉搜索树 搜索的关键数据为关键字“Key”&#xff0c;关键字对应的数据为值“Value”&#xff0c;将其称为“Key-Value键值对” 【关于Map】 Ma…

8 种渗透测试类型

渗透测试是对网络、硬件或软件系统进行有计划的攻击&#xff0c;目的是揭露可能破坏系统完整性并危及有价值数据的安全缺陷。虽然渗透测试的类型不同&#xff0c;但它们都旨在利用漏洞和弱点来测试现有安全措施的有效性。 渗透测试 不同类型的渗透测试取决于人们希望在特定系…

初识Linux · O(1)调度算法

目录 前言&#xff1a; O(1)调度算法 前言&#xff1a; 在初识进程的那一块&#xff0c;我们已经知道了进程并不是一直占用cpu资源的&#xff0c;而是存在时间片的概念&#xff0c;即&#xff0c;每个进程都有一定的时间来执行该进程&#xff0c;时间一到&#xff0c;该进程…

会议平台后端优化方案

会议平台后端优化方案 通过RTC的学习&#xff0c;我了解到了端对端技术&#xff0c;就想着做一个节省服务器资源的会议平台 之前做了这个项目&#xff0c;快手二面被问到卡着不知如何介绍&#xff0c;便有了这篇文章 分析当下机制 相对于传统视频平台&#xff08;SFU&#xff…

windows 驱动实例分析系列-定时日志的COM驱动

本文章的前置文章为: windows 驱动编写原则 windows COM驱动 案例 windows COM驱动的I/O处理 在前面的设计中,主要是对windows提供的VirtualSerial源代码的讲解,但是那个驱动其实是一个空壳驱动,用于学习的,在I/O处理中,也讲述了serial I/O处理的本质,接下来会将这些…

PGMP-03战略一致性

1.概要 program strategy alignment&#xff1a;战略一致性 2.详细

css的背景background属性

CSS的background属性是一个简写属性&#xff0c;它允许你同时设置元素的多个背景相关的子属性。使用这个属性可以简化代码&#xff0c;使其更加清晰和易于维护。background属性可以设置不同的子属性。 background子属性 定义背景颜色 使用background-color属性 格式&#x…

经典文献阅读之--WiROS(用于机器人的WiFi感知工具箱)

0. 简介 近期的许多研究探索了使用基于WiFi的感知技术来改善SLAM&#xff08;同时定位与地图构建&#xff09;、机器人操控或探索。此外&#xff0c;WiFi的广泛可用性使其成为最具优势的射频信号。但WiFi传感器缺乏一个准确、易处理、多功能的工具箱&#xff0c;这限制了它们与…

MicoZone-Maven

一、理论 Maven 是 Apache 软件基金会组织维护的一款专门为 Java 项目提供项目构建和依赖管理支持的工具。 通过Maven管理依赖的优势&#xff1a; 1、通过在pom.xml中指定jar包坐标即可自动从仓库下载依赖 2、如果jar包存在子依赖会自动下载子依赖包 3、如果jar包之间存在冲突…

Web安全 - 服务端请求伪造SSRF(Server-Side Request Forgery)

文章目录 OWASP 2023 TOP 10SSRF 导图SSRF 概念SSRF的工作原理SSRF攻击场景SSRF防御策略1. 严格验证用户输入2. 禁用或限制对内部网络的访问3. 强制使用外部API代理4. 禁止直接访问敏感资源5. 输入内容长度限制6. 检测和监控7. 确保对HTTP请求的处理安全 SSRF防御实现方案1. 白…

【鸿蒙开发】05 登录Demo解析

文章目录 一、功能介绍 在鸿蒙开发中&#xff0c;一个完善的登录功能是许多应用程序的基础需求。本文将详细介绍一个鸿蒙 App 登录 Demo&#xff0c;包括其功能介绍、代码解析以及代码 demo 的下载地址。 本文初始代码从华为开发者网站下载&#xff0c;根据该Demo进行内容调整。…

【Fast-LIO系列】Fast-LIO、Fast-LIO2、Faster-LIO系列特点分析

【FAST-LIO】Fast-LIO系列特点分析 1. FAST-LIO核心贡献1.将IMU和Lidar特征点紧耦合在一起2.使用反向传播考虑到了运动补偿3. 基于IESKF中的 卡尔曼增益更新 K 2. FAST-LIO2核心贡献(2021年)1. 不用线&#xff0c;面特征点而使用全局点云2. 使用ikd-tree存储点云3. ikd-Tree 3.…

P3131 [USACO16JAN] Subsequences Summing to Sevens S Python题解

[USACO16JAN] Subsequences Summing to Sevens S 题目描述 Farmer John’s N N N cows are standing in a row, as they have a tendency to do from time to time. Each cow is labeled with a distinct integer ID number so FJ can tell them apart. FJ would like to ta…

C语言 | Leetcode C语言题解之第448题找到所有数组中消失的数字

题目&#xff1a; 题解&#xff1a; int* findDisappearedNumbers(int* nums, int numsSize, int* returnSize) {for (int i 0; i < numsSize; i) {int x (nums[i] - 1) % numsSize;nums[x] numsSize;}int* ret malloc(sizeof(int) * numsSize);*returnSize 0;for (in…

特征工程——一门提高机器学习性能的艺术

当前围绕人工智能(AI)和机器学习(ML)展开的许多讨论以模型为中心&#xff0c;聚焦于 ML和深度学习(DL)的最新进展。这种模型优先的方法往往对用于训练这些模型的数据关注不足&#xff0c;甚至完全忽视。类似MLOps的领域正迅速发展&#xff0c;通过系统性地训练和利用ML模型&…

ZYNQ: GPIO 之 MIO 控制 LED 实验

GPIO 之 MIO 控制 LED 实验目的 使用 GPIO 通过两个 MIO 引脚控制 PS 端两个 LED 的亮灭&#xff0c;实现底板上 PS_LED0、PS_LED1 两个 LED 灯同亮同灭的效果。 简介 ZYNQ PS 中的外设&#xff08;如 USB 控制器、UART 控制器、I2C 控制器以及 GPIO 等等&#xff09;可以通…

亲测无限坐席在线客服系统源码/二开版/基于ThinkPHP+搭建教程

源码简介&#xff1a; 亲测了一款实用的无限坐席在线客服系统源码&#xff0c;这可是基于ThinkPHP框架开发的二开版哦&#xff01;里面还附带了一份超详细的搭建教程。 安装过程简直不能更简单&#xff0c;只需一键操作&#xff0c;启动两个端口就搞定了。而且&#xff0c;它…

动态库的加载全过程

区分一组概念&#xff1a;逻辑地址&#xff0c;虚拟地址&#xff0c;物理地址。 逻辑地址&#xff1a;是我们的代码在编译过程&#xff0c;编译器帮对每一条代码所生成的指令所编写的地址。 物理地址&#xff1a;当程序被放入到内存中时&#xff0c;内存与每一条指令所对应的…

回执单识别-银行回单识别API-文字识别OCR API

银行回单是一种由银行提供的交易凭证&#xff0c;记录了账户资金的交易明细。它通常包括存款、取款、转账、汇款、支付等各种类型的资金往来信息。银行回单可以是纸质的&#xff0c;也可以是电子版的&#xff0c;内容详尽记录了交易的相关信息&#xff0c;具有法律效力&#xf…

CSS 效果:实现动态展示双箭头

最近写了一段 CSS 样式&#xff0c;虽然不难&#xff0c;但实现过程比较繁琐。这个效果结合了两个箭头&#xff0c;一个突出&#xff0c;一个内缩&#xff0c;非常适合用于步骤导航或选项卡切换等场景。样式不仅仅是静态的&#xff0c;还可以通过点击 click 或者 hover 事件&am…