Java当中实现分片上传

Java当中实现分片上传


一:背景

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

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

二:解决方案

1、整体方案

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

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

2、main方法代码实例

@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);
    }

三:具体到业务中的实现

1.校验文件MD5是否上传过
2.切片文件上传,前端5MB一切片
3.切片文件上传完之后进行合并文件,创建异步合并任务
4.合并文件之后前端定时进行轮循获取上传结果;20mb以内文件每隔3s;100mb以内每隔5s;大于100mb每隔10s

1、dto方法实例

package com.wondertek.oms.controller.business;

import com.wondertek.center.constants.BusinessCenterApi;
import com.wondertek.center.model.dto.business.CheckSliceFileMd5DTO;
import com.wondertek.center.model.dto.business.MergeSliceFileDTO;
import com.wondertek.center.model.dto.business.UploadSliceFileDTO;
import com.wondertek.center.model.vo.business.CheckSliceFileMd5VO;
import com.wondertek.center.model.vo.business.UploadFileVO;
import com.wondertek.oms.service.business.FileOmsService;
import com.wondertek.web.exception.result.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

/**
 * @beLongProjecet: Intelligent_video
 * @beLongPackage: com.wondertek.center.controller
 * @author: xxy
 * @createTime: 2022/06/22 15:37
 * @description: 文件上传控制层
 * @version: v1.0
 */
@RestController
@Api(value = "Web端切片大文件上传", tags = "Web端切片大文件上传")
public class SliceFileOmsController {

    @Autowired
    private FileOmsService fileOmsService;

    @ApiOperation(value = "1.校验文件MD5是否上传过", notes = "1.校验文件MD5是否上传过")
    @PostMapping(value = BusinessCenterApi.WEB_CHECK_FILE_MD5)
    public Result<CheckSliceFileMd5VO> checkFileMd5(@RequestBody @Validated CheckSliceFileMd5DTO dto) {
        CheckSliceFileMd5VO resultVo = this.fileOmsService.checkFileMd5(dto);
        return new Result<>(resultVo);
    }

    @ApiOperation(value = "2.切片文件上传,前端5MB一切片", notes = "2.切片文件上传,前端5MB一切片")
    @PostMapping(value = BusinessCenterApi.WEB_UPLOAD_SLICE_FILE)
    public Result uploadSliceFile(@RequestParam("file") MultipartFile multipartFile, @Validated UploadSliceFileDTO dto) {
        Boolean resultVo= this.fileOmsService.uploadSliceFile(multipartFile,dto);
        return new Result(resultVo);
    }

    @ApiOperation(value = "3.切片文件上传完之后进行合并文件,创建异步合并任务", notes = "3.切片文件上传完之后进行合并文件,创建异步合并任务")
    @PostMapping(value = BusinessCenterApi.WEB_MERGE_SLICE_FILE)
    public Result mergeSliceFile(@RequestBody @Validated MergeSliceFileDTO dto) {
        Boolean resultVo = this.fileOmsService.mergeSliceFile(dto);
        return new Result(resultVo);
    }

    @ApiOperation(value = "4.合并文件之后前端定时进行轮循获取上传结果;20mb以内文件每隔3s;100mb以内每隔5s;大于100mb每隔10s", notes = "4.合并文件之后前端定时进行轮循获取上传结果;20mb以内文件每隔3s;100mb以内每隔5s;大于100mb每隔10s")
    @PostMapping(value = BusinessCenterApi.WEB_GET_MERGE_FILE)
    public Result getMergeFile(@RequestBody @Validated MergeSliceFileDTO dto) {
        UploadFileVO resultVo = this.fileOmsService.getMergeFile(dto);
        return new Result(resultVo);
    }


}
package com.wondertek.center.model.dto.business;

import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

/**
 * @beLongProjecet: metacosmic_conference
 * @beLongPackage: com.wondertek.center.model.dto.business
 * @author: xxy
 * @createTime: 2022/11/17 11:14
 * @description:
 * @version: v1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor

public class CheckSliceFileMd5DTO {

    @ApiModelProperty(value = "文件唯一标识MD5值")
    @NotBlank(message = "文件唯一标识MD5值 不能为空")
    private String fileKey;

    @ApiModelProperty(value = "文件名称加后缀,格式为:test.png")
    @NotBlank(message = "文件名称 不能为空")
    private String fileName;

    @ApiModelProperty(value = "文件后缀,不加.")
    @NotBlank(message = "文件后缀 不能为空")
    private String fileSuffix;

    @ApiModelProperty(value = "文件字节大小")
    @NotNull(message = "文件字节大小 不能为空")
    private Long fileSize;

}

package com.wondertek.center.model.dto.business;

import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

/**
 * @beLongProjecet: metacosmic_conference
 * @beLongPackage: com.wondertek.center.model.dto.business
 * @author: xxy
 * @createTime: 2022/11/17 11:14
 * @description:
 * @version: v1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor

public class UploadSliceFileDTO {

    @ApiModelProperty(value = "文件总分片数")
    @NotNull(message = "文件总分片数 不能为空")
    @Min(value = 0,message = "最小为0")
    private Long sliceTotal;

    @ApiModelProperty(value = "分片文件索引下标,定义从1开始")
    @NotNull(message = "分片文件索引下标 不能为空")
    @Min(value = 0,message = "最小为0")
    private Long sliceIndex;

    @ApiModelProperty(value = "未分片原文件唯一标识MD5值")
    @NotBlank(message = "文件唯一标识MD5值 不能为空")
    private String fileKey;

    @ApiModelProperty(value = "未分片原文件名称,格式为:test.png")
    @NotBlank(message = "文件名称 不能为空")
    private String fileName;

    @ApiModelProperty(value = "未分片原文件名称不加后缀,格式为:test")
    @NotBlank(message = "文件名称无后缀 不能为空")
    private String fileNameNoSuffix;

    @ApiModelProperty(value = "未分片原文件后缀")
    @NotBlank(message = "文件后缀 不能为空")
    private String fileSuffix;

    @ApiModelProperty(value = "未分片原文件字节大小")
    @NotNull(message = "文件字节大小 不能为空")
    private Long fileSize;

}

package com.wondertek.center.model.dto.business;

import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

/**
 * @beLongProjecet: metacosmic_conference
 * @beLongPackage: com.wondertek.center.model.dto.business
 * @author: xxy
 * @createTime: 2022/11/17 11:14
 * @description:
 * @version: v1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor

public class MergeSliceFileDTO {

    @ApiModelProperty(value = "未分片原文件唯一标识MD5值")
    @NotBlank(message = "文件唯一标识MD5值 不能为空")
    private String fileKey;

    @ApiModelProperty(value = "未分片原文件名称,格式为:test.png")
    @NotBlank(message = "文件名称 不能为空")
    private String fileName;

    @ApiModelProperty(value = "未分片原文件名称不加后缀,格式为:test")
    @NotBlank(message = "文件名称无后缀 不能为空")
    private String fileNameNoSuffix;

    @ApiModelProperty(value = "未分片原文件后缀")
    @NotBlank(message = "文件后缀 不能为空")
    private String fileSuffix;

    @ApiModelProperty(value = "未分片原文件字节大小")
    @NotNull(message = "文件字节大小 不能为空")
    private Long fileSize;

}

2、serviceImpl实现方法

2.1检查当前文件是否在系统中存在,如果存在直接返回url
    
    @Override
    public CheckSliceFileMd5VO checkFileMd5(CheckSliceFileMd5DTO dto) {
        CheckSliceFileMd5VO resultVo = new CheckSliceFileMd5VO();

        //检查上传文件类型
        Long resourceTypeId = this.checkFileType(dto.getFileSuffix(), dto.getFileSize());
        //检查fileKey是否支持秒传
        Result<UploadFileRecordVO> uploadFileRecordVOResult = webUploadFileRecordFeignService.selectOneByFileKey(dto.getFileKey());
        uploadFileRecordVOResult.assertSuccess();
        UploadFileRecordVO data = uploadFileRecordVOResult.getData();
        Integer sliceFileType = ObjectUtil.isEmpty(data.getId()) ? SliceFileTypeEnum.NO_UPLOAD.getCode() : SliceFileTypeEnum.FAST_UPLOAD.getCode();
        if (ObjectUtil.isNotEmpty(data.getId())) {
            UploadFileVO uploadFileVO = new UploadFileVO();
            String url = "";
            String previewFileUrl = "";
            log.debug("当前的环境为:{}", ProfileUtil.getActiveProfile());
            if (StringUtils.isNotBlank(ProfileUtil.getActiveProfile()) && "prod".equals(ProfileUtil.getActiveProfile())) {
                url = data.getFileUrl();
                previewFileUrl = EosUtil.addOuterChainUrl(url);
            } else {
                url = data.getFileUrl();
                previewFileUrl = url;
            }
            uploadFileVO.setFileUrl(url);
            uploadFileVO.setPreviewFileUrl(previewFileUrl);
            uploadFileVO.setFileName(dto.getFileName());
            uploadFileVO.setFileSize(FileSizeUtil.getByteFileSize(dto.getFileSize(), 3) + "MB");
            uploadFileVO.setResourceTypeId(resourceTypeId);
            uploadFileVO.setMd5Data(dto.getFileKey());
            resultVo.setUploadFileVO(uploadFileVO);
        }
        resultVo.setSliceFileType(sliceFileType);
        return resultVo;
    }
2.2上传切片文件
    
    @Override
    public Boolean uploadSliceFile(MultipartFile multipartFile, UploadSliceFileDTO dto) {

        String fileFolderPath = getFileFolderPath(dto.getFileKey());
        log.info("文件分片上传请求开始,请求参数: {}", JSON.toJSONString(dto));
        //如果目标文件夹不存在,则直接创建一个
        FileUtil.createFolder(fileFolderPath);
        //本地文件全路径
        String sliceFilePath = fileFolderPath + File.separator + dto.getSliceIndex() + "." + dto.getFileSuffix();
        log.info("本地切片文件全路径,sliceFilePath的值为:{}", sliceFilePath);
        try {
            //将分片文件保存到指定路径
            multipartFile.transferTo(new File(sliceFilePath));
        } catch (IOException e) {
            log.error("保存分片文件异常", e);
            e.printStackTrace();
            return false;
        }
        //边上传边随机合并文件
        String mergedFilePath = getMergeFilePath(dto.getFileName(), fileFolderPath);
        // 切片大小,这里设置为5MB
        long sliceSize = 5 * 1024 * 1024;
        //合并方式一:随机合并randomAccessFile
        //使用了自动资源管理 (try-with-resources) 的方式来打开流,这是一个很好的做法,
        //因为它会在代码块结束时自动关闭这些资源,而不需要显式调用 close() 方法,在以下代码中,try 块内的资源在离开 try 块后会自动关闭
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(mergedFilePath, "rw")) {
            // 写入该分片数据
            long startPosition;
            if (dto.getSliceIndex() - 1 == dto.getSliceTotal() - 1) {
                //根据下标计算剩余的大小
                startPosition = dto.getFileSize() - new File(sliceFilePath).length();
            } else {
                //默认从下标0开始
                long l = dto.getSliceIndex() - 1;
                startPosition = l * sliceSize;
            }
            log.info("偏移量startPosition的值为:{}", startPosition);
            randomAccessFile.seek(startPosition);
            try (FileInputStream inputStream = new FileInputStream(sliceFilePath)) {
                byte[] buf = new byte[1024];
                int len;
                while (-1 != (len = inputStream.read(buf))) {
                    randomAccessFile.write(buf, 0, len);
                }
            }
        } catch (IOException e) {
            log.error("随机合并文件异常", e);
            e.printStackTrace();
            return false;
        }
        return true;
    }
    @Value("${saveFilePath.windows}")
    private String windowsPath;
    @Value("${saveFilePath.linux}")
    private String linuxPath;   
   
   /**
     * 获取存储文件夹路径
     *
     * @param fileKey 文件唯一标识md5
     * @return
     */
    private String getFileFolderPath(String fileKey) {
        String fileFolderPath = null;
        //获取本地文件夹地址
        String savePath = "";
        if (SystemUtil.SystemNameEnum.TYPE1.getDesc().equals(SystemUtil.getSystemName())) {
            savePath = windowsPath;
        } else {
            savePath = linuxPath + "/";
        }
        fileFolderPath = savePath + fileKey;
        log.info("本地文件夹地址,fileFolder的值为:{}", fileFolderPath);
        return fileFolderPath;
    }
    
        /**
     * @param fileName       原始文件名称加后缀
     * @param fileFolderPath
     * @return
     */
    private static String getMergeFilePath(String fileName, String fileFolderPath) {
        String mergedFilePath = fileFolderPath + File.separator + fileName;
        return mergedFilePath;
    }
    
2.3合并切片文件
  //3.合并切片文件
  @Override
    public Boolean mergeSliceFile(MergeSliceFileDTO dto) {
        log.info("开始合并文件,入参为:{}", JSON.toJSONString(dto));
        //封装请求参数
        String fileFolderPath = getFileFolderPath(dto.getFileKey());
        String mergeFilePath = getMergeFilePath(dto.getFileName(), fileFolderPath);
        File mergeFile = new File(mergeFilePath);

        //合并的时候去判断此fileKey是否存在
        Result<UploadFileRecordVO> uploadFileRecordVOResult = webUploadFileRecordFeignService.selectOneByFileKey(dto.getFileKey());
        uploadFileRecordVOResult.assertSuccess();
        UploadFileRecordVO data = uploadFileRecordVOResult.getData();

        if (Objects.isNull(data.getId())) {
            //文件不存在
            if (ObjectUtil.isEmpty(mergeFile)) {
                //删除本地临时文件
                FileUtil.deleteFolderAndSubfolders(fileFolderPath);
                throw new BizException(ErrorCodeEnum.NOT_FILE_NOT_EXIST);
            }
            //文件合并失败,请重新上传
            log.info("入参当中计算的dto.getFileSize()的值为:{}", dto.getFileSize());
            log.info("合并文件后计算的mergeFile.length()的值为:{}", mergeFile.length());
            if (dto.getFileSize() != mergeFile.length()) {
                //删除本地临时文件
                FileUtil.deleteFolderAndSubfolders(fileFolderPath);
                throw new BizException(ErrorCodeEnum.MERGE_FILE_ERROR);
            }
        }
        CompletableFuture.runAsync(() -> {
            if (Objects.isNull(data.getId())) {
                log.info("创建异步执行任务成功!开始执行合并,文件标识为:{}", dto.getFileKey());
                //将文件保存到文件系统,返回文件链接
                UploadFileVO uploadFileVO = this.uploadFile(FileUtil.fileToMultipartFile(mergeFile));
                log.info("文件上传响应,uploadFileVO的值为:{}", JSON.toJSONString(uploadFileVO));
                //将此次记录入库
                AddUploadFileRecordDTO addUploadFileRecordDTO = new AddUploadFileRecordDTO();
                addUploadFileRecordDTO.setFileKey(dto.getFileKey());
                addUploadFileRecordDTO.setFileUrl(uploadFileVO.getFileUrl());
                addUploadFileRecordDTO.setFileName(dto.getFileName());
                addUploadFileRecordDTO.setFileSuffix(dto.getFileSuffix());
                addUploadFileRecordDTO.setFileSize(dto.getFileSize());
                log.info("保存相应记录到系统中,addUploadFileRecordDTO的值为:{}", JSON.toJSONString(addUploadFileRecordDTO));
                Result<Boolean> insertResult = webUploadFileRecordFeignService.insert(addUploadFileRecordDTO);
                insertResult.assertSuccess();
                log.info("保存相应记录到系统中,insertResult的值为:{}", JSON.toJSONString(insertResult));
                //删除本地临时文件
                FileUtil.deleteFolderAndSubfolders(fileFolderPath);
                log.info("删除临时文件完成,fileKey为:{}", dto.getFileKey());
                log.info("创建异步执行任务结束!文件标识为:{}", dto.getFileKey());
            }
        }, poolExecutor);
        return true;
    }

2.4前端定时轮循结果

@Override
    public UploadFileVO getMergeFile(MergeSliceFileDTO dto) {
        //合并的时候去判断此fileKey是否存在
        Result<UploadFileRecordVO> uploadFileRecordVOResult = webUploadFileRecordFeignService.selectOneByFileKey(dto.getFileKey());
        uploadFileRecordVOResult.assertSuccess();
        UploadFileRecordVO data = uploadFileRecordVOResult.getData();
        if (Objects.nonNull(data.getId())) {
            UploadFileVO uploadFileVO = new UploadFileVO();
            String url = "";
            String previewFileUrl = "";
            log.debug("当前的环境为:{}", ProfileUtil.getActiveProfile());
            if (StringUtils.isNotBlank(ProfileUtil.getActiveProfile()) && "prod".equals(ProfileUtil.getActiveProfile())) {
                url = data.getFileUrl();
                previewFileUrl = EosUtil.addOuterChainUrl(url);
            } else {
                url = data.getFileUrl();
                previewFileUrl = url;
            }
            uploadFileVO.setFileUrl(url);
            uploadFileVO.setPreviewFileUrl(previewFileUrl);
            uploadFileVO.setFileName(dto.getFileName());
            uploadFileVO.setFileSize(FileSizeUtil.getByteFileSize(dto.getFileSize(), 3) + "MB");
            uploadFileVO.setResourceTypeId(this.checkFileType(dto.getFileSuffix(), dto.getFileSize()));
            uploadFileVO.setMd5Data(dto.getFileKey());
            log.info("fileKey在此系统中已存在,直接返回结果,相应信息为:{}", JSON.toJSONString(uploadFileVO));
            return uploadFileVO;
        }
        return null;
    }
2.4 SQL建表语句
CREATE TABLE `upload_file_record` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `file_key` varchar(255) NOT NULL COMMENT '文件唯一标识MD5值',
  `file_url` text NOT NULL COMMENT '文件上传到对象存储的地址',
  `file_name` varchar(255) NOT NULL COMMENT '文件名称',
  `file_suffix` varchar(20) NOT NULL COMMENT '文件后缀',
  `file_size` bigint(20) NOT NULL COMMENT '文件字节大小',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0:未删除 1:已删除',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=93 DEFAULT CHARSET=utf8mb4 COMMENT='文件上传表';

参考:

Java实现文件分片上传

Java大文件上传(秒传、分片上传、断点续传)

  • 6
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java小白笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值