Java当中实现分片上传

news2024/11/25 3:06:09

Java当中实现分片上传

文章目录

  • Java当中实现分片上传
    • 一:背景
    • 二:解决方案
      • 1、整体方案
      • 2、代码实例
      • 3、说明
      • 4、FileUtil中的方法

一:背景

Web端实现大文件上传下载的需求,要求将文件上传到对象存储当中,大文件上传有以下痛点:

  1. 文件上传超时:原因是前端请求框架限制最大请求时长,后端设置了接口访问的超时时间,或者是 nginx(或其它代理/网关) 限制了最大请求时长。
  2. 文件大小超限:原因在于后端对单个请求大小做了限制,一般 nginx 和 server 都会做这个限制。
  3. 上传时间过久
  4. 由于各种网络原因上传失败,且失败之后需要从头开始。

二:解决方案

1、整体方案

1.前端根据代码中设置好的分片大小将上传的文件切成若干个小文件,分多次请求依次上传,后端再将文件碎片拼接为一个完整的文件,然后再去进行上传

2.如果需要某个碎片上传失败,也不会影响其它文件碎片,只需要重新上传失败的部分就可以了,则需要设计一个表去维护上传的切片相关的一些信息

2、代码实例

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ShardingFileDTO {
    //文件名称(包含文件后缀)
    private String fileName;
    //文件总大小 MB
    private String size;
    //文件总分片数
    private int shardTotal;
    //分片文件索引下标
    private int shardIndex;
    //文件后缀,视频后缀为mp4,图片则为jpg等
    private String suffix;
    //唯一标识
    private String onlyCode;
}

image-20230904093947026

package com.xxy.demotest.controller.ShardingFile;

import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson.JSON;
import com.xxy.demotest.controller.ShardingFile.model.ShardingFileDTO;
import com.xxy.demotest.haikang.aliyun.ALiYun;
import com.xxy.demotest.result.baseresult.BaseResponse;
import com.xxy.demotest.utils.WorkUtil.FileUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @beLongProjecet: demo-test
 * @beLongPackage: com.xxy.demotest.controller.ShardingFile
 * @author: xxx
 * @createTime: 2023/09/01 15:16
 * @description: 分片文件上传
 * @version: v1.0
 */
@RestController
@RequestMapping("sharding")
@RequiredArgsConstructor
@Slf4j
public class ShardingFileController {

    public static final String shardPath="D:\\test\\sharding\\";
    public static final String savePath="D:\\test\\save\\";

    private static void excuteFile(ShardingFileDTO dto, MultipartFile multipartFile) throws IOException {
        log.info("文件分片上传请求开始,请求参数: {}", JSON.toJSONString(dto));
        //获取本地文件夹地址
        String fileFolderPath = savePath + dto.getOnlyCode();
        log.info("本地文件夹地址,fileFolder的值为:{}", fileFolderPath);
        //如果目标文件夹不存在,则直接创建一个
        FileUtil.createFolder(fileFolderPath);

        //本地文件全路径
        String fileFullPath =fileFolderPath + File.separator+ dto.getFileName()+"_"+ dto.getShardIndex()+"."+ dto.getSuffix();
        log.info("本地文件全路径,fileFullPath的值为:{}", fileFullPath);
        //将分片文件保存到指定路径
        multipartFile.transferTo(new File(fileFullPath));
        //更新到文件上传表中
        //判断当前分片索引是否等于分片总数,如果等于分片总数则执行文件合并
        if (dto.getShardIndex()==dto.getShardTotal()) {
            //文件合并
            log.info("文件分片合并开始");
            File dirFile = new File(fileFolderPath);
            if (!dirFile.exists()) {
                throw new RuntimeException("文件不存在");
            }
            //分片上传的文件已经位于同一个文件夹下,方便寻找和遍历(当文件数大于十的时候记得排序用冒泡排序确保顺序是正确的)
            List<String> filePaths = FileUtil.listFiles(fileFolderPath);
            if (CollectionUtils.isNotEmpty(filePaths)) {
                //将此里面文件按照索引进行排序
                log.info("filePaths的值为:{}", filePaths);
                // 使用自定义的Comparator来对文件路径进行排序
                Collections.sort(filePaths, new FilePathComparator());
                //进行合并,顺序按照索引进行合并
                String mergedFilePath =fileFolderPath+File.separator+dto.getFileName();
                log.info("生成新的文件的路径,mergedFilePath的值为:{}", mergedFilePath);
                mergeFiles(filePaths, mergedFilePath);
                //合并完成将新文件上传到对象存储中
                String upload = ALiYun.upload(FileUtil.fileToMultipartFile(new File(mergedFilePath)));
                log.info("文件最终访问地址,upload的值为:{}", upload);
                //可以异步
                //删除所有临时切片文件
                deleteFolderAndSubfolders(fileFolderPath);
                //删除所有切片
                deleteFolderAndSubfolders(shardPath+dto.getOnlyCode());
            }

        }
    }


    public static void main(String[] args) {

        String fastUUID = IdUtil.fastSimpleUUID();
        //String sourceFilePath = "C:\\Users\\wonder\\Desktop\\ai测试图片\\切片文件上传\\metacosmic_conference.zip"; // 源文件路径
        String sourceFilePath = "C:\\Users\\wonder\\Desktop\\ai测试图片\\人像.png"; // 源文件路径
        String outputDirectory = shardPath + fastUUID;
        FileUtil.createFolder(outputDirectory);

        //封装dto参数
        ShardingFileDTO shardingFileDTO = new ShardingFileDTO();
        shardingFileDTO.setFileName(new File(sourceFilePath).getName());
        shardingFileDTO.setSize("10MB");

        shardingFileDTO.setSuffix(getFileExtension(sourceFilePath));
        shardingFileDTO.setOnlyCode(fastUUID);
        long sliceSize = 5 * 1024 * 1024; // 切片大小,这里设置为5MB
        try {
            File sourceFile = new File(sourceFilePath);
            String fileName = sourceFile.getName();
            int lastDotIndex = fileName.lastIndexOf('.');
            String suffix = fileName.substring(lastDotIndex + 1);

            long fileSize = sourceFile.length(); // 获取文件大小
            int sliceNumber = (int) Math.ceil((double) fileSize / sliceSize); // 计算切片数量
            log.info("共切割成 " + sliceNumber + " 个文件切片");
            shardingFileDTO.setShardTotal(sliceNumber);
            FileInputStream fis = new FileInputStream(sourceFile);

            byte[] buffer = new byte[(int) sliceSize];
            int bytesRead;
            List<String> sliceFilePaths = new ArrayList<>();

            for (int i = 0; i < sliceNumber; i++) {
                int num = i + 1;
                shardingFileDTO.setShardIndex(num);

                String sliceFileName = "slice_" + num;
                String sliceFilePath = outputDirectory + File.separator + sliceFileName+"."+suffix;
                // 创建切片文件并写入数据
                FileOutputStream fos = new FileOutputStream(sliceFilePath);
                bytesRead = fis.read(buffer, 0, (int) sliceSize);
                fos.write(buffer, 0, bytesRead);
                fos.close();
                sliceFilePaths.add(sliceFilePath);

                File file = new File(sliceFilePath);
                excuteFile(shardingFileDTO,FileUtil.fileToMultipartFile(file));

            }
            fis.close();


        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 合并成新的文件
     * @param filePaths
     * @param mergedFilePath
     */
    public static void mergeFiles(List<String> filePaths, String mergedFilePath) {
        try (FileOutputStream fos = new FileOutputStream(mergedFilePath);
             BufferedOutputStream bos = new BufferedOutputStream(fos)) {
            for (String filePath : filePaths) {
                try (FileInputStream fis = new FileInputStream(filePath);
                     BufferedInputStream bis = new BufferedInputStream(fis)) {
                    byte[] buffer = new byte[1024];
                    int bytesRead;
                    while ((bytesRead = bis.read(buffer)) != -1) {
                        bos.write(buffer, 0, bytesRead);
                    }
                }
            }
            log.info("文件合并完成");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 获取文件扩展名
     * @param fileName
     * @return
     */
    public static String getFileExtension(String fileName) {
        int lastDotIndex = fileName.lastIndexOf('.');
        if (lastDotIndex > 0) {
            return fileName.substring(lastDotIndex + 1);
        }
        return ""; // 如果文件名中没有点,返回空字符串
    }

    /**
     * 删除文件夹下所有的文件
     * @param folderPath
     */
    public static void deleteFilesInFolder(String folderPath) {
        File folder = new File(folderPath);

        // 检查文件夹是否存在
        if (!folder.exists() || !folder.isDirectory()) {
            System.out.println("指定的路径不是一个有效的文件夹.");
            return;
        }

        File[] files = folder.listFiles();

        if (files != null) {
            for (File file : files) {
                if (file.isFile()) {
                    // 删除文件
                    if (file.delete()) {
                        System.out.println("已删除文件: " + file.getName());
                    } else {
                        System.out.println("无法删除文件: " + file.getName());
                    }
                }
            }
        }
    }

    /**
     * 删除文件夹
     * @param folderPath
     */
    public static void deleteFolder(String folderPath) {
        File folder = new File(folderPath);

        // 删除文件夹
        if (folder.exists() && folder.isDirectory()) {
            if (folder.delete()) {
                System.out.println("已删除文件夹: " + folderPath);
            } else {
                System.out.println("无法删除文件夹: " + folderPath);
            }
        }
    }

    /**
     * 删除文件夹中所有文件和子文件夹
     * @param folderPath
     */
    public static void deleteFolderAndSubfolders(String folderPath) {
        File folder = new File(folderPath);

        // 检查文件夹是否存在
        if (!folder.exists()) {
            System.out.println("文件夹不存在.");
            return;
        }

        if (folder.isDirectory()) {
            File[] files = folder.listFiles();

            if (files != null) {
                for (File file : files) {
                    if (file.isDirectory()) {
                        // 递归删除子文件夹及其内容
                        deleteFolderAndSubfolders(file.getAbsolutePath());
                    } else {
                        // 删除文件
                        if (file.delete()) {
                            System.out.println("已删除文件: " + file.getName());
                        } else {
                            System.out.println("无法删除文件: " + file.getName());
                        }
                    }
                }
            }
        }

        // 删除文件夹本身
        if (folder.delete()) {
            System.out.println("已删除文件夹: " + folderPath);
        } else {
            System.out.println("无法删除文件夹: " + folderPath);
        }
    }

}
class FilePathComparator implements Comparator<String> {
    private static final Pattern NUMBER_PATTERN = Pattern.compile("\\d+");

    @Override
    public int compare(String filePath1, String filePath2) {
        int number1 = extractNumber(filePath1);
        int number2 = extractNumber(filePath2);
        return Integer.compare(number1, number2);
    }

    private int extractNumber(String filePath) {
        Matcher matcher = NUMBER_PATTERN.matcher(filePath);
        if (matcher.find()) {
            return Integer.parseInt(matcher.group());
        }
        return 0; // 如果找不到数字,则返回0或其他适当的默认值
    }
}

3、说明

后端如果一个个调用请求有点麻烦,所以用了一个main方法做下说明,执行的流程为:

程序切片–>保存切片(前端上传)–>上传最后一个切片的时候执行文件合并(后端根据条件索引合并)–>合并完成–>执行上传对象存储–>删除切片文件–>接口响应链接

注意:

正常的接口请求当中需要对excuteFile稍微做下改造,省略掉切片的环节即可;

4、FileUtil中的方法

 /**
     * 创建文件夹
     *
     * @param path
     */
    public static void createFolder(String path) {
        File folder = new File(path);
        if (!folder.exists()) {
            folder.mkdirs();
        }
    }
    
    
     /**
     * 获取当前文件夹下面的文件列表
     *
     * @param folderPath
     * @return
     */
    public static List<String> listFiles(String folderPath) {
        List<String> objects = new ArrayList<>();
        File folder = new File(folderPath);
        File[] files = folder.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isFile()) {
                    objects.add(file.getAbsolutePath());
                } else if (file.isDirectory()) {
                    listFiles(file.getAbsolutePath());
                }
            }
        }
        return objects;
    }
    
      /**
     * File转换为MultipartFile
     * @param file
     * @return
     */
    public static MultipartFile fileToMultipartFile(File file) {
        FileItem item = new DiskFileItemFactory().createItem("file"
                , MediaType.MULTIPART_FORM_DATA_VALUE
                , true
                , file.getName());
        try (InputStream input = new FileInputStream(file);
             OutputStream os = item.getOutputStream()) {
            // 流转移
            IOUtils.copy(input, os);
        } catch (Exception e) {
            throw new IllegalArgumentException("Invalid file: " + e, e);
        }

        return new CommonsMultipartFile(item);
    }

参考:Java实现文件分片上传

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

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

相关文章

苹果手机微信记录如何备份到电脑?微信聊天记录怎么恢复?

求助&#xff01;手机上的微信聊天记录太多了&#xff0c;导致手机内存严重不足&#xff0c;但是又不舍得把聊天记录全部删除。请问有哪些快速备份微信聊天记录的方法吗&#xff1f;提前感谢大家&#xff01; 随着使用微信时间的增长&#xff0c;微信所保存的照片、视频、音频、…

Golang专题精进

Golang专题精进 Golang单元测试Golang错误处理Golang正则表达式Golang反射Golang验证码Golang日期时间处理库CarbonGolang发送邮件库emailGolang log日志Golang log日志框架logrusGolang加密和解密应用Golang访问权限控制框架casbinGolang使用swagger生成api接口文档Golang jwt…

【Day-28慢就是快】代码随想录-二叉树-完全二叉树的节点个数

给出一个完全二叉树&#xff0c;求出该树的节点个数。 —————————————————————————————————————— 1. 普通二叉树的求法 递归法与求深度类似&#xff0c;但是深度是depth,而此题是计算nodeNum。 迭代法使用层序遍历&#xff0c;记录遍历…

SAP MM学习笔记26- SAP中 振替转记(转移过账)和 在库转送(库存转储)5 - 总结

SAP 中在库移动 不仅有入库&#xff08;GR&#xff09;&#xff0c;出库&#xff08;GI&#xff09;&#xff0c;也可以是单纯内部的转记或转送。 1&#xff0c;振替转记&#xff08;转移过账&#xff09; 具体查看我之前的文章。 SAP MM学习笔记26- SAP中 振替转记&#xff…

大数据可视化大屏实战项目(2)公司季度销售额度可视化展示—HTML+CSS+JS【源码在文末】(可用于比赛项目或者作业参考中)

大数据可视化大屏实战项目&#xff08;2&#xff09;公司季度销售额度可视化展示—HTMLCSSJS【源码在文末】&#xff08;可用于比赛项目或者作业参考中&#x1f415;&#x1f415;&#x1f415;&#xff09; 一&#xff0c;项目概览 ☞☞☞☞☞☞项目演示链接&#xff1a;htt…

Kerberos基础

一. Kerberos概述 Kerberos是一种计算机网络授权协议&#xff0c;用来在非安全网络中&#xff0c;对个人通信以安全的手段进行身份认证。这个词又指麻省理工学院为这个协议开发的一套计算机软件。软件设计上采用客户端/服务器结构&#xff0c;并且能够进行相互认证&#xff0c…

软件工程学术顶会——ESEC/FSE 2022 议题(网络安全方向)清单、摘要与总结

总结 本次会议中网络安全相关议题涵盖区块链、智能合约、符号执行、浏览器API模糊测试等不同研究领域。 热门研究方向: 1. 基于深度学习的漏洞检测与修复 2. 基于AI的自动漏洞修复 3. 模糊测试与漏洞发现 冷门研究方向: 1. 多语言代码的漏洞分析 2. 代码审查中的软件安全 3. 浏…

【类和对象】③友元类

文章目录 1.初始化列表2.static静态成员3.友元 1.初始化列表 我们知道在创建对象时&#xff0c;编译器通过调用构造函数&#xff0c;给对象中各个成员变量一个合适的初始值。虽然调用构造函数之后&#xff0c;对象中已经有了一个初始值&#xff0c;但是不能将其称为对对象中成…

长胜证券:医药逐步走出疫情扰动 潜心静气迎接曙光

8月回顾&#xff1a;2023年8月医药生物板块跌落4.76%&#xff0c;同期沪深300跌落6.21%&#xff0c;医药板块跑赢沪深300约1.45%&#xff0c;位列31个子职业第11位。本月子板块医疗服务、中药、医药商业、化学制药、医疗器械、生物制品分别跌落1.47%、3.20%、3.97%、5.18%、7.2…

关于ChatGPT:4个维度讲透 ChatGPT 技术原理,揭开 ChatGPT 神秘技术黑盒!(文末送书福利5.0)

文章目录 &#x1f4cb;前言&#x1f3af;Tansformer架构模型&#x1f3af;ChatGPT原理&#x1f3af;提示学习与大模型能力的涌现&#x1f9e9;提示学习&#x1f9e9;上下文学习&#x1f9e9;思维链 &#x1f3af;行业参考建议&#xff08;关于本书&#xff09;&#x1f9e9;拥…

Python爬取天气数据并进行分析与预测

随着全球气候的不断变化&#xff0c;对于天气数据的获取、分析和预测显得越来越重要。本文将介绍如何使用Python编写一个简单而强大的天气数据爬虫&#xff0c;并结合相关库实现对历史和当前天气数据进行分析以及未来趋势预测。 1 、数据源选择 选择可靠丰富的公开API或网站作…

QT中闹钟的设置

.h文件 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QPushButton> //按钮 #include <QTextEdit> //文本 #include <QLabel> //标签 #include <QLineEdit> //行编辑器#include <QTimerEvent> //定时器事件类头文件 #…

串联系统与并联系统可靠性计算

串联系统 计算串联系统可靠性&#xff0c;就将所有部分的可靠性相乘即可 并联系统 计算并联系统可靠性&#xff0c;就用每个部分的不可靠性相乘&#xff0c;得到系统的不可靠性&#xff0c;再用1-不可靠性&#xff0c;得到并联系统可靠性 串&#xff0c;并联系统 上图的系统可…

深入剖析Kubernetes之控制器模式

文章目录 Kubernetes 项目中一个重要的设计思想&#xff1a;控制器模式。 nginx-deployment 的例子&#xff1a; apiVersion: apps/v1 kind: Deployment metadata:name: nginx-deployment spec:selector:matchLabels:app: nginxreplicas: 2template:metadata:labels:app: ngin…

做外贸如何引导跟进客户

跟客户聊什么似乎是我们很多小伙伴很头疼的问题&#xff0c;尤其是需要跟客户follow-up 的时候&#xff0c;那种没话找话的感觉有时候都令自己抓狂。 如果我们自己的产品本身比较负责&#xff0c;设计到的工艺或者款式以及规格很多的时候&#xff0c;那可能话题就还容易一些&a…

初级电工电子基础知识部分

欢迎学习初级电工电子基础知识部分 学习可不能像是看小说那样看完就过了&#xff0c;作为电工学的开篇&#xff0c;同时作为电工技术这个特殊作业工种&#xff0c;理解并记住每个知识点是很重要的&#xff0c;因为电工基础里面很多都是理论知识&#xff0c;前面的没掌握好后面的…

算法基础-数学知识-质数、约数

这里写目录标题 质数试除法判定质数晒质数埃及筛线性筛 约数试除法求约数约数个数与约数之和AcWing 870. 约数个数AcWing 871. 约数之和 欧几里德求最大公因数 质数 埃及筛虽然用的不多&#xff0c;大多使用线性筛&#xff0c;但是埃及筛的思想很重要 试除法判定质数 AcWin…

大数据可视化大屏实战项目(4)物流数据云看台(包括动态登陆页面)—HTML+CSS+JS【源码在文末】(可用于比赛项目或者作业参考中)

大数据可视化大屏实战项目&#xff08;4&#xff09;物流数据云看台&#xff08;包括动态登陆页面&#xff09;—HTMLCSSJS【源码在文末】&#xff08;可用于比赛项目或者作业参考中&#x1f415;&#x1f415;&#x1f415;&#xff09; 一&#xff0c;项目概览 ☞☞☞☞☞☞…

软件测试案例 | 某教务管理平台系统的系统测试总结报告

集成测试通过之后&#xff0c;各个模块已经被组装成了一个完整的软件包&#xff0c;这时就需要进行系统测试了。传统的系统测试指的是通过集成测试的软件系统&#xff0c;作为计算机系统的一个重要组成部分&#xff0c;其将与计算机硬件、外部设备、支撑软件等其他系统元素组合…

信息安全性测试的流程

安全测试 一、信息安全性测试的定义 软件安全是一个广泛而复杂的主题&#xff0c;每一个新软件都可能存在安全的缺陷&#xff0c;甚至这个缺陷是前所未见的。信息安全性测试的目的在于通过系统的测试&#xff0c;对所测软件提出安全改进建议&#xff0c;帮助用户将风险控制/转…