JAVA大文件分段上传+断点续传

  • 大文件分段上传+断点续传
  • 1.思路解析
  • 2.代码实现


大文件分段上传+断点续传

1.思路解析

  思路是为了解决实际业务中大文件上传,中途网络中断和充分利用多请求,加速上传,保存上传记录。
  针对上述问题,我们采用将大文件进行拆分,拆分成若干个临时小文件,进行上传。每个临时文件进行记录。有了这个思路,那么在上传若干个临时文件时,就需要进行告知服务端,本次上传的大文件基本信息,衍生出第一步操作,初始化接口。
  那么接下来,基本思路大概逻辑如下:

-后端接口

  1. 初始化接口—告知服务端,并记录,后端提供该文件唯一标识
  2. 单个拆分后的文件分片上传—若干分片上传,断点续传
  3. 合并接口—完成上传,进行合并,删除临时文件

-前端

  1. 文件拆分
  2. 多个文件上传,采取文件下标
  3. 完成上传

2.代码实现

①接口 “/initMultipart” 实现:
初始化—告知服务端,并记录,后端提供该文件唯一标识

public Object initMultipart(BurstFile burstFile) {
        String digest = SecureUtil.md5(burstFile.getOriginName() + burstFile.getFileSize());
        burstFile.setMdSign(digest);
        burstFile.setIsSuccess("0");
        return fileDomainService.createBurstFile(burstFile);
    }
/**
 * 初始化接口参数
 **/
 @Data
    public static class BurstFileInitDto {

        /**
         * 文件名称
         */
        @NotBlank(message = "文件名称不能为空")
        private String originName;

        /**
         * 后缀
         */
        @NotBlank(message = "文件后缀不能为空")
        private String suffix;

        /**
         * 文件大小
         */
        @NotBlank(message = "文件大小不能为空")
        private String fileSize;

        /**
         * 分片数
         */
        @NotBlank(message = "分片数不能为空")
        private String burstNum;

    }

②接口 “/uploadMultipart” 实现
上传文件分片+断点续传

@SneakyThrows
    public void uploadMultipart(BurstFile burstFile) {
        BurstFile db= fileDomainService.findByPartNumberAndMdSign( burstFile.getPartNumber(),burstFile.getMdSign());
        if (db != null){
            return;
        }
        //上传分片文件
        String dates= ObjectUtil.formatYYYYMMDD();//文件上传的年月目录
        creatDir();
        String path = "/mountfiles/files/"+dates.substring(0, 4)+"/"+dates.substring(4, 6) + "/"+burstFile.getMdSign();
        File dirFile = new File(url+path);
        if (!dirFile.exists()) {
            dirFile.mkdirs();
            System.out.println("成功创建目录:" + burstFile.getMdSign());
        }
        String filename = burstFile.getFile().getOriginalFilename();
        Long size = burstFile.getFile().getSize();
        dirFile = new File(url+path+"/"+filename);
        MultipartFile file = burstFile.getFile();
        file.transferTo(dirFile);
        burstFile.setOriginName(filename);
        burstFile.setIsSuccess("1");
        burstFile.setPartSize(size);
        burstFile.setTempUrl(url+path+"/"+filename);
        fileDomainService.createBurstFile(burstFile);
    }
/**
 * 分片文件接口参数
 **/
@Data 
public class BurstFileDto {

    private MultipartFile file;

    @NotBlank(message = "文件md5不能为空")
    private String mdSign;

    @NotNull(message = "分片索引不能为空")
    private Integer partNumber;

}

③接口 “/completeMultipart” 实现
完成分片上传,进行合并

@SneakyThrows
    @Transactional(rollbackOn = Exception.class)
    public Object completeMultipart(String id) {
        if (StringUtils.isEmpty(id)){
            ValueUtil.isError("文件主键不得为null");
        }
        BurstFile db = fileDomainService.burstFileDetail(id);
        if (db == null){
            ValueUtil.isError("未查询到初始化信息");
        }
        String mdSign = db.getMdSign();
        String burstNum = db.getBurstNum();
        List<BurstFile> burstFileList= fileDomainService.findByMdSign(mdSign);
        if (StringUtils.isEmpty(burstFileList)){
            ValueUtil.isError("分片暂未全部上传完毕");
        }

        List<BurstFile> buDBs = burstFileList.stream().filter(dbFile -> !dbFile.getId().equals(id)).collect(Collectors.toList());
        List<File> collect = buDBs.stream().sorted(Comparator
                .comparing(BurstFile::getPartNumber)).map(burstFile->new File(burstFile.getTempUrl())).collect(Collectors.toList());
        if ( Integer.parseInt(burstNum)!=(collect.size())){
            ValueUtil.isError("分片暂未全部上传完毕");
        }
        File file = collect.get(0).getParentFile();
        String from=file.getPath();
        creatDir();
        String filename = PrimaryUtil.nextId();
        String dates= ObjectUtil.formatYYYYMMDD();//文件上传的年月目录
        String path = "/mountfiles/files/"+dates.substring(0, 4)+"/"+dates.substring(4, 6) + "/"+filename +"."+db.getSuffix() ;
        FileSpiltAndMerge.merge(collect,url+path);
        FileUtil.clean(from);
        file.delete();

        db.setIsSuccess("1");
        db.setTempUrl(path);
        buDBs.forEach(burstFile -> delBurstFile(burstFile.getId()));
        return fileDomainService.createBurstFile(db);
    }
      @SneakyThrows
    private void creatDir() {
        File dirFile = new File(url);
        String dates= ObjectUtil.formatYYYYMMDD();//文件上传的年月目录
        if (!dirFile.exists()) {
            dirFile.mkdirs();
            System.out.println("成功创建目录:" + dirFile.getCanonicalFile());
        }
        dirFile = new File(url +"/mountfiles");
        if (!dirFile.exists()) {
            dirFile.mkdirs();
            System.out.println("成功创建mountfiles目录:" + dirFile.getCanonicalFile());
        }

        dirFile = new File(dirFile +"/files");
        if (!dirFile.exists()) {
            dirFile.mkdirs();
            System.out.println("成功创建files目录:" + dirFile.getCanonicalFile());
        }

        dirFile = new File(dirFile + "/"+dates.substring(0, 4) );
        System.out.println("拼接年的目录:" + dirFile);
        if (!dirFile.exists()) {
            dirFile.mkdirs();
            System.out.println("成功创建目录:" + dirFile.getCanonicalFile());
        }
        dirFile = new File(dirFile +"/"+dates.substring(4, 6) );
        System.out.println("拼接月的目录:" + dirFile);
        if (!dirFile.exists()) {
            dirFile.mkdirs();
            System.out.println("成功创建目录:" + dirFile.getCanonicalFile());
        }
    }

④合并文件工具类FileSpiltAndMerge

package ddd.special.infrastructure.util;

/**
 * @author NJ
 * @create 2022/10/10 15:15
 */

import java.io.*;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class FileSpiltAndMerge {

    public static void main(String[] args) throws IOException {
//        spilt("E:\\home\\test\\a.jpg", 100, "E:\\home\\test\\a");
//        merge("E:\\home\\test\\a", "E:\\home\\test\\b.jpg");
//        mergeUploadFile("E:\\home\\test\\a\\0", 0,"E:\\home\\test\\b.jpg");
        mergeUploadFile("E:\\home\\test\\a\\1", 102400,"E:\\home\\test\\b.jpg");
//        mergeUploadFile("E:\\home\\test\\a\\2", 204800,"E:\\home\\test\\b.jpg");
    }

    public static void spilt(String from, int size, String to) throws IOException {
        File f = new File(from);
        FileInputStream in = new FileInputStream(f);
        FileOutputStream out = null;
        FileChannel inChannel = in.getChannel();
        FileChannel outChannel = null;

        // 将MB单位转为为字节B
        long m = size * 1024 ;
        // 计算最终会分成几个文件
        int count = (int) (f.length() / m);
        for (int i = 0; i <= count; i++) {
            // 生成文件的路径
            String t = to + "/" + i;
            try {
                out = new FileOutputStream(new File(t));
                outChannel = out.getChannel();
                // 从inChannel的m*i处,读取固定长度的数据,写入outChannel
                if (i != count)
                    inChannel.transferTo(m * i, m, outChannel);
                else// 最后一个文件,大小不固定,所以需要重新计算长度
                    inChannel.transferTo(m * i, f.length() - m * count, outChannel);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                out.close();
                outChannel.close();
            }
        }
        in.close();
        inChannel.close();
    }

    public static void merge(List<File> list, String to) throws IOException {
        File t = new File(to);
        FileInputStream in = null;
        FileChannel inChannel = null;

        FileOutputStream out = new FileOutputStream(t, true);
        FileChannel outChannel = out.getChannel();

        // 获取目录下的每一个文件名,再将每个文件一次写入目标文件
        // 记录新文件最后一个数据的位置
        long start = 0;
        for (File file : list) {

            in = new FileInputStream(file);
            inChannel = in.getChannel();

            // 从inChannel中读取file.length()长度的数据,写入outChannel的start处
            outChannel.transferFrom(inChannel, start, file.length());
            start += file.length();
            in.close();
            inChannel.close();
        }
        out.close();
        outChannel.close();
    }
    public static void merge(String from, String to) throws IOException {
        File t = new File(to);
        FileInputStream in = null;
        FileChannel inChannel = null;

        FileOutputStream out = new FileOutputStream(t, true);
        FileChannel outChannel = out.getChannel();

        File f = new File(from);
        // 获取目录下的每一个文件名,再将每个文件一次写入目标文件
        if (f.isDirectory()) {
            List<File> list = getAllFileAndSort(from);
            // 记录新文件最后一个数据的位置
            long start = 0;
            for (File file : list) {

                in = new FileInputStream(file);
                inChannel = in.getChannel();

                // 从inChannel中读取file.length()长度的数据,写入outChannel的start处
                outChannel.transferFrom(inChannel, start, file.length());
                start += file.length();
                in.close();
                inChannel.close();
            }
        }
        out.close();
        outChannel.close();
    }

    private static void mergeUploadFile(String from, int start, String to) throws IOException {
        File t = new File(to);
        FileInputStream in = null;
        FileChannel inChannel = null;

        FileOutputStream out = new FileOutputStream(t, true);
        FileChannel outChannel = out.getChannel();

        File file = new File(from);
        // 获取目录下的每一个文件名,再将每个文件一次写入目标文件
        if (file.exists()) {
            in = new FileInputStream(file);
            inChannel = in.getChannel();
            // 从inChannel中读取file.length()长度的数据,写入outChannel的start处
            outChannel.transferFrom(inChannel, start, file.length());
            start += file.length();
            in.close();
            inChannel.close();
        }
        out.close();
        outChannel.close();
    }

    private static List<File> getAllFileAndSort(String dirPath) {
        File dirFile = new File(dirPath);
        File[] listFiles = dirFile.listFiles();
        List<File> list = Arrays.asList(listFiles);
        Collections.sort(list, (o1, o2) -> {
            return Integer.parseInt(o1.getName()) - Integer.parseInt(o2.getName());
        });
        return list;
    }
}