大文件分块上传后端服务器

一、背景:

后台系统需要上传大文件、大视频等数据,耗时过长,接口等待超时,故需优化通过前端多线程+分片方式进行文件上传,显著提升上传速度。

二、流程:

前端逻辑

前端使用分片技术,将大文或大视频件进行分片,可使用webupload,将视频进行切片,多线程上传后台服务器。同时计算hash值用于文件完整性校验。同时对每个分片视频或文件进行顺序记录,后面单个分片数据异常可以针对单个文件进行重新上传。

后端逻辑

准备接口1:前端生成大文件的MD5,调用接口把MD5传递来,后端会用这个MD5去创建一个文件夹,一个视频就对应一个文件夹。

接口2:前端带MD5+视频分片信息去调用此接口,后端会根据MD5找到对应的文件夹,判断这个分片是否已经上传过了。同时也可以那这个分片号做异常返回,让前端重传。

接口3:前端根据接口2的调用进行判断,如果该分片已经上传则跳过,如果没有上传则进行该分片上传,传递MD5+视频分片信息,后端根据MD5找到文件夹,将视频分片存储起来

接口4:当所有视频通过多线程传完成,调用接口4,传递MD5+表单信息,后端通过流合并,把所有的分片信息整合成一个视频,此时上传完毕,视频已存储到服务器本地。后端还可以异步通过调用三方文件存储(OSS)进行上传对象服务器,生成访问地址存储数据库,修改对应数据状态位,同时可删除临时服务端文件。

三、后端接口及业务

@RequestMapping("/common/core/bigFile")
@RestController
public class BigFileController {
    /**
     * 创建文件夹
     */
@PostMapping("/createFolder")
    public JsonResult createFolder(@RequestParam("fileMd5") String fileMd5,     // 文件md5
                               @RequestParam("fileName") String fileName,       // 文件名
                               @RequestParam("fileExt") String fileExt	    //文件扩展名
                               ) {   
        return bigFileService.createFolder(fileMd5,fileName,fileSize,mimetype,fileExt);
    }


    /**
     * 检查分片接口
     */
    @PostMapping("/checkChunk")
    public Result checkChunk(
            @RequestParam("fileMd5") String fileMd5, //文件md5
            @RequestParam("chunk") Integer chunk // 当前分块下标
            ){
        return bigFileService.checkChunk(fileMd5,chunk,chunkSize);
    }

    /**
     * 上传分片接口
     */
    @PostMapping("/uploadChunk")
    public Result uploadChunk(
            @RequestParam("file") MultipartFile file, //分块后的文件
            @RequestParam("fileMd5") String fileMd5, // 文件md5
            @RequestParam("chunk") Integer chunk  // 分片标识
            ){
        return bigFileService.uploadChunk(file,fileMd5,chunk);
    }

    /**
     * 合并分片接口
     */
    @PostMapping("/mergeChunks")
    public Result mergeChunks(
            @RequestParam("deptId") Long deptId,  // 部门id
            @RequestParam("remark") String remark, // 备注
            @RequestParam("fileMd5") String fileMd5,// 文件md5
            @RequestParam("fileName") String fileName, // 原文件名
            @RequestParam("fileSize") Long fileSize, // 文件总大小
            @RequestParam("fileType") String fileType, // 文件类型
            @RequestParam("fileExt") String fileExt // 扩展名
            ){

        return bigFileService.mergeChunks(deptId,remark,fileMd5,fileName,fileSize,mimetype,fileExt);
    }
}

@Service
@Slf4j
public class BigFileService {

    @Autowired
    private BigFileMapper bigFileMapper;

    // 临时文件目录
    private String uploadPath = "/data/bigFile/temp/";


    /**
     *  创建文件夹
     */
    public Result createFolder(String fileMd5, String fileName, String fileExt) {
        //检查文件是否存在
        String filePath = getFilePath(fileMd5, fileExt);
        File file = new File(filePath);

        if (file.exists()) {
            log.info("文件夹已存在 {} ", fileName);
            return Result.error("上传文件已存在");
        }

        boolean fileFold = createFileFold(fileMd5);
        if (!fileFold) {
            //上传文件目录创建失败
            log.info("上传文件夹创建失败 {} ,文件夹存在", fileName);
            return Result.error("上传文件夹失败");
        }
        return Result.success();
    }

    /**
     * 检查分片
     */
    public Result cheCkchunk(String fileMd5, Integer chunk) {
        //获取块文件文件夹路径
        String chunkfileFolderPath = getChunkFileFolderPath(fileMd5);
        // 块文件的下标 1 2 3 排序使用
        File chunkFile = new File(chunkfileFolderPath + chunk);
        if (!chunkFile.exists()) {
            return Result.error();
        }
        return Result.success();
    }

    /**
     * 上传分片
     */
    public Result uploadChunk(MultipartFile file, String fileMd5, Integer chunk) {

        //块文件存放完整路径
        File chunkfile = new File(getChunkFileFolderPath(fileMd5) + chunk);

        //上传的块文件
        InputStream inputStream = null;
        FileOutputStream outputStream = null;
        try {
            inputStream = file.getInputStream();
            outputStream = new FileOutputStream(chunkfile);
            IOUtils.copy(inputStream, outputStream);
        } catch (Exception e) {
            e.printStackTrace();
            return Result.error("文件上传失败!");
        } finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return Result.success();
    }

    /**
     * 合并分片
     */
    public Result mergeChunks(String deptId, String remark, String fileMd5, String fileName, Long fileSize, String fileType, String fileExt) {

        //获取块文件的路径
        String chunkfileFolderPath = getChunkFileFolderPath(fileMd5);

        //创建文件目录
        File chunkfileFolder = new File(chunkfileFolderPath);

        //目录是否存在, 不存在就创建目录
        if (!chunkfileFolder.exists()) {
            chunkfileFolder.mkdirs();
        }

        //合并文件,创建新的文件对象
        File mergeFile = new File(getFilePath(fileMd5, fileExt));

        // 合并文件存在先删除再创建
        if (mergeFile.exists()) {
            mergeFile.delete();
        }

        boolean newFile = false;

        try {
            //创建文件
            newFile = mergeFile.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (!newFile) {
            //创建失败
            return Result.error("创建文件失败!");
        }

        //获取块文件,此列表是已经排好序的列表
        List<File> chunkFiles = getChunkFiles(chunkfileFolder);
        //合并文件
        mergeFile = mergeFile(mergeFile, chunkFiles);
        if (mergeFile == null) {
            return Result.error("合并文件失败!");
        }
        //校验文件
        boolean checkResult = this.checkFileMd5(mergeFile, fileMd5);
        if (!checkResult) {
            return Result.error("文件校验失败!");
        }
        //将文件信息保存到数据库
        BigFile bigFile = new BigFile();
        //MD5作为文件唯一ID
        bigFile.setFileId(fileMd5);
        //文件名
        bigFile.setFileName(fileMd5 + "." + fileExt);
        //源文件名
        bigFile.setFileOriginalName(fileName);
        //文件路径保存相对路径
        bigFile.setFilePath(getFileFolderRelativePath(fileMd5, fileExt));
        bigFile.setFileSize(fileSize);
        bigFile.setUploadTime(new Date());
        bigFile.setFileType(fileType);
        bigFile.setFileType(fileExt);
        bigFile.setDeptId(deptId);
        bigFile.setRemark(remark);
        // 更新状态
        bigFile.setFileStatus(1);

        bigFileMapper.insert(mediaFile);

        // 后台上传oss完毕再更新云端地址,更新状态
        return Result.success("视频上传成功");
    }


    /*
     *根据文件md5得到文件路径
     */
    private String getFilePath(String fileMd5, String fileExt) {
        String filePath = uploadPath + fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + fileMd5 + "." + fileExt;
        return filePath;
    }

    //得到文件目录相对路径,路径中去掉根目录
    private String getFileFolderRelativePath(String fileMd5, String fileExt) {
        String filePath = fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/";
        return filePath;
    }

    //得到文件所在目录
    private String getFileFolderPath(String fileMd5) {
        String fileFolderPath = uploadPath + fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/";
        return fileFolderPath;
    }

    //创建文件目录
    private boolean createFileFold(String fileMd5) {
        //创建上传文件目录
        String fileFolderPath = getFileFolderPath(fileMd5);
        File fileFolder = new File(fileFolderPath);
        if (!fileFolder.exists()) {
            //创建文件夹
            boolean mkdirs = fileFolder.mkdirs();
            log.info("创建文件目录 {} ,结果 {}", fileFolder.getPath(), mkdirs);
            return mkdirs;
        }
        return true;
    }

    //得到块文件所在目录
    private String getChunkFileFolderPath(String fileMd5) {
        String fileChunkFolderPath = getFileFolderPath(fileMd5) + "/" + "chunks" + "/";
        return fileChunkFolderPath;
    }


    /**
     * 创建块文件目录
     */
    private boolean createChunkFileFolder(String fileMd5) {
        //创建上传文件目录
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        File chunkFileFolder = new File(chunkFileFolderPath);
        if (!chunkFileFolder.exists()) {
            //创建文件夹
            boolean mkdirs = chunkFileFolder.mkdirs();
            return mkdirs;
        }
        return true;
    }

    /**
     * 校验文件完整性
     * @param mergeFile
     * @param md5
     * @return
     */
    private boolean checkFileMd5(File mergeFile, String md5) {
        if (mergeFile == null || StringUtils.isEmpty(md5)) {
            return false;
        }
        //进行md5校验
        FileInputStream mergeFileInputstream = null;
        try {
            mergeFileInputstream = new FileInputStream(mergeFile);
            //得到文件的md5
            String mergeFileMd5 = DigestUtils.md5Hex(mergeFileInputstream);
            //比较md5
            if (md5.equalsIgnoreCase(mergeFileMd5)) {
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();

        } finally {
            try {
                mergeFileInputstream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    /**
     * 获取所有分片文件
     * @param chunkfileFolder
     * @return
     */
    private List<File> getChunkFiles(File chunkfileFolder) {
        //获取路径下的所有块文件
        File[] chunkFiles = chunkfileFolder.listFiles();
        //将文件数组转成list,并排序
        List<File> chunkFileList = new ArrayList<File>();
        chunkFileList.addAll(Arrays.asList(chunkFiles));
        //排序
        Collections.sort(chunkFileList, (o1, o2) -> {
            if (Integer.parseInt(o1.getName()) > Integer.parseInt(o2.getName())) {
                return 1;
            }
            return -1;
        });
        return chunkFileList;
    }

    /**
     * 合并文件 流写入
     * @param mergeFile
     * @param chunkFiles
     * @return
     */
    private File mergeFile(File mergeFile, List<File> chunkFiles) {
        try {
            //创建写文件对象
            RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw");
            //遍历分块文件开始合并
            // 读取文件缓冲区
            byte[] b = new byte[1024];
            for (File chunkFile : chunkFiles) {
                RandomAccessFile raf_read = new RandomAccessFile(chunkFile, "r");
                int len = -1;
                //读取分块文件
                while ((len = raf_read.read(b)) != -1) {
                    //向合并文件中写数据
                    raf_write.write(b, 0, len);
                }
                raf_read.close();
            }
            raf_write.close();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return mergeFile;
    }
}


@Data
public class BigFile  {

    private Long id;
    /**
     * 文件的ID
     */
    private String fileId;
    /**
     * 文件名
     */
    private String fileName;
    /**
     * 源文件名
     */
    private String fileOriginalName;
    /**
     * oss地址
     */
    private String fileUrl;
    /**
     * 临时本地存储目录
     */
    private String filePath;
    /**
     * 文件类型
     */
    private String fileType;
    /**
     * 1待上传 2 上传成功 3上传失败
     */
    private Integer fileStatus;
    /**
     * 文件总大小
     */
    private Long fileSize;
    /**
     * 上传时间
     */
    private Date uploadTime;
    /**
     * 部门id
     */
    private Long deptId;
    /**
     * 备注
     */
    private String remark;
}

数据库

CREATE TABLE `big_file` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `file_id` varchar(255) DEFAULT NULL COMMENT '文件的ID',
  `file_name` varchar(255) DEFAULT NULL COMMENT '文件名',
  `file_original_name` varchar(255) DEFAULT NULL COMMENT '原文件名',
  `file_url` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT 'oss地址',
  `file_path` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '临时本地存储目录',
  `file_type` varchar(255) DEFAULT NULL COMMENT '文件类型',
  `file_status` tinyint DEFAULT NULL COMMENT '1待上传 2 上传成功 3上传失败',
  `file_size` bigint DEFAULT NULL COMMENT '文件总大小',
  `upload_time` datetime DEFAULT NULL COMMENT '上传时间',
  `dept_id` bigint DEFAULT NULL COMMENT '部门id',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3;

四、文件上传服务器本地完毕

上传到本地,可以推送到三方服务器进行存储。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

docker部署seata

1.准备数据库表 Seata支持多种存储模式&#xff0c;但考虑到持久化的需要&#xff0c;我们一般选择基于数据库存储。 先准备seata-tc.sql脚本&#xff0c;在你的数据库中运行&#xff0c;内容复制粘贴即可。 CREATE DATABASE IF NOT EXISTS seata; USE seata;CREATE TABLE I…

java+ssm+mysql美妆论坛

项目介绍&#xff1a; 使用javassmmysql开发的美妆论坛&#xff0c;系统包含超级管理员&#xff0c;系统管理员、用户角色&#xff0c;功能如下&#xff1a; 用户&#xff1a;主要是前台功能使用&#xff0c;包括注册、登录&#xff1b;查看论坛板块和板块下帖子&#xff1b;…

Openlayers基础知识回顾(五)

1、GeoJSON数据的加载 GeoJSON是一种基于JSON的地理空间数据交换格式&#xff0c;它定义了几种类型JSON对象以及它们组合在一起的方法&#xff0c;以表示有关地理要素、属性和它们的空间范围的数据 2、GeoJSON转化为ol要素 new ol.format.GeoJSON().readFeatures() 一、canv…

使用 ASP.NET Core HttpLoggingMiddleware 记录 http 请求/响应

我们发布了一个应用程序&#xff0c;该应用程序运行在一个相当隐蔽的 WAF 后面。他们向我们保证&#xff0c;他们的产品不会以任何方式干扰我们的应用程序。这是错误的。他们删除了我们几乎所有的“自定义”标头。为了“证明”这一点&#xff0c;我构建了一个中间件&#xff0c…

后端工程搭建

后端工程通过maven聚合工程的形式来搭建 1.1创建spzx-parent工程(父工程) 存放公共依赖 锁定公共依赖版本 1.2创建spzx-common工程(公共模块) 存放一些工具类/公共服务 1.3创建spzx-model工程(数据模型) 存放实体类 1.4创建spzx-menager工程(后台管理系统) 后台管理系统服务模…

Flink Python作业快速入门

Flink Python快速入门_实时计算 Flink版(Flink)-阿里云帮助中心 import argparse # 用于处理命令行参数和选项&#xff0c;使程序能够接收用户通过命令行传递的参数 import logging import sysfrom pyflink.common import WatermarkStrategy, Encoder, Types from pyflink.data…

数字图像处理(15):图像平移

&#xff08;1&#xff09;图像平移的基本原理&#xff1a;计算每个像素点的移动向量&#xff0c;并将这些像素按照指定的方向和距离进行移动。 &#xff08;2&#xff09;平移向量包括水平和垂直分量&#xff0c;可以表示为&#xff08;dx&#xff0c;dy&#xff09;&#xff…

JAVA秋招面试题精选-第一天总结

目录 分栏简介&#xff1a; 问题一&#xff1a;订单表每天新增500W条数据&#xff0c;分库分表应该怎么设计&#xff1f; 问题难度以及频率&#xff1a; 问题导向&#xff1a; 满分答案&#xff1a; 举一反三&#xff1a; 问题总结&#xff1a; 问题二&#xff1a;解释…

Rnnoise和SpeexDsp两种降噪方式有什么区别?

在蒙以CourseMaker 7.0软件中&#xff0c;增加了两种降噪模式&#xff0c;一种是Rnnoise&#xff0c;一种是SpeexDsp&#xff0c;这两种降噪模式有什么区别呢&#xff1f; Rnnoise 基于神经网络。当噪声与 rnnoise 的模型训练的噪声匹配时&#xff0c;它的效果非常好。比如说&…

博物馆导览系统方案(一)背景需求分析与核心技术实现

维小帮提供多个场所的室内外导航导览方案&#xff0c;如需获取博物馆导览系统解决方案可前往文章最下方获取&#xff0c;如有项目合作及技术交流欢迎私信我们哦~撒花&#xff01; 一、博物馆导览系统的背景与市场需求 在数字化转型的浪潮中&#xff0c;博物馆作为文化传承和知…

福昕PDF低代码平台

福昕PDF低代码平台简介 福昕PDF 低代码平台是一款创新的工具&#xff0c;旨在简化PDF处理和管理的流程。通过这个平台&#xff0c;用户可以通过简单的拖拽界面上的按钮&#xff0c;轻松完成对Cloud API的调用工作流&#xff0c;而无需编写复杂的代码。这使得即使没有编程经验的…

Linux —— 管理文件

一、Linux的目录结构及用途 /bin&#xff1a;存放最常用的命令&#xff0c;如ls、cat等&#xff0c;所有用户都可以执行的命令。/boot&#xff1a;包含启动Linux系统所需的核心文件&#xff0c;如内核文件和引导加载程序。/dev&#xff1a;设备文件目录&#xff0c;包含系统中的…

NanoLog起步笔记-7-log解压过程初探

nonolog起步笔记-6-log解压过程初探 再看解压过程建立调试工程修改makefile添加新的launch项 注&#xff1a;重新学习nanolog的README.mdPost-Execution Log Decompressor 下面我们尝试了解&#xff0c;解压的过程&#xff0c;是如何得到文件头部的meta信息的。 再看解压过程 …

处理配置System Viewer缺少SFR文件

按照网上的教程&#xff0c;其他的都配好 这里给几个参考 嵌入式开发--Keil MDK仿真时System Viewer不显示寄存器选项_keil system viewer不显示外设寄存器-CSDN博客 keil无法查看外设寄存器&#xff08;生成SFR文件&#xff09;_keil sfr文件-CSDN博客 keil5软件仿真 Logic…

网络安全中大数据和人工智能应用实践

传统的网络安全防护手段主要是通过单点的网络安全设备&#xff0c;随着网络攻击的方式和手段不断的变化&#xff0c;大数据和人工智能技术也在最近十年飞速地发展&#xff0c;网络安全防护也逐渐开始拥抱大数据和人工智能。传统的安全设备和防护手段容易形成数据孤岛&#xff0…

create-react-app react19 搭建项目报错

报错截图 此时运行会报错&#xff1a; 解决方法&#xff1a; 1.根据提示安装依赖法 执行npm i web-vitals然后重新允许 2.删除文件法 在index.js中删除对报错文件的引入&#xff0c;删除报错文件

excel如何让单元格选中时显示提示信息?

现象&#xff1a; 当鼠标放在单元格上&#xff0c;会出现提示信息&#xff1a; 先选中单元格选择上方的【数据】-【数据验证】图标选择【输入信息】勾上【选定单元格时显示输入信息】输入【标题】&#xff0c;如&#xff1a;最上方图中的&#xff1a;姓名&#xff1a;输入【输…

PyCharm+Selenium+Pytest配置小记

1、下载ChromeDriver&#xff1a; Chrome130以后的Driver下载&#xff1a; Chrome for Testing availabilityhttps://googlechromelabs.github.io/chrome-for-testing/ &#xff08;1&#xff09;查看自己Crome浏览器的版本&#xff1a;设置-->关于 Chrome&#xff1b; &…

用最小的代价解决mybatis-plus关于批量保存的性能问题

1.问题说明 问题背景说明&#xff0c;在使用达梦数据库时&#xff0c;mybatis-plus的serviceImpl.saveBatch()方法或者updateBatchById()方法的时候&#xff0c;随着数据量、属性字段的增加&#xff0c;效率越发明显的慢。 serviceImpl.saveBatch(); serviceImpl.updateBatch…

电子商务人工智能指南 4/6 - 内容理解

介绍 81% 的零售业高管表示&#xff0c; AI 至少在其组织中发挥了中等至完全的作用。然而&#xff0c;78% 的受访零售业高管表示&#xff0c;很难跟上不断发展的 AI 格局。 近年来&#xff0c;电子商务团队加快了适应新客户偏好和创造卓越数字购物体验的需求。采用 AI 不再是一…