大文件切片上传

步骤1:前端文件切片

步骤2-1:后端接口提供:切片文件,切片索引、切片数量【2选1】

步骤2-2:后端接口提供:切片文件包名、所有切片文件,切片数量【2选1】

步骤3:接收切片文件完成后并合并,且删除临时的切片文件

步骤4:上传合并的文件至文件服务Minio

步骤5:Minio返回上传文件地址,上传完成。

1、Controller层

package com.company.pvinspection.controller;

import com.company.common.core.baseweb.domain.AjaxResult;
import com.company.pvinspection.service.IFileUploadMinioService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

/**
 * 大文件上传
 * 备注:大文件切片——切片文件临时存储——合并切片文件——上传MINIO
 * 步骤1:前端将大文件分成n个小文件
 * 步骤2:分别将小文件传给后端(包括:文件夹名、切片文件、文件名、切片总数)
 * 当后端获取到最后一个分片文件时,合并分片文件,并上传至MINIO文件服务。【上传过程考虑断点续传】
 *
 * @author kally
 * @date 2023/8/10
 */
@Api(tags = "大文件切片上传管理")
@RestController
@RequestMapping("/prj/uploadFile")
public class FileUploadController {

    @Autowired
    private IFileUploadMinioService fileService;

    @ApiOperation(value = "切片文件单文件上传【使用】")
    @PostMapping(value = "/upload")
    public AjaxResult singleFilePartUpload(@ApiParam(value = "文件夹名称唯一") String folderName,
                                           @ApiParam(value = "分片文件") MultipartFile filePart,
                                           @ApiParam(value = "原始文件名") String fileName,
                                           @ApiParam(value = "分片总数") Integer partNum,
                                           @ApiParam(value = "作业ID") Long recordId) {
        return fileService.singleFilePartUpload(folderName, filePart, fileName, partNum, recordId);
    }

    @ApiOperation(value = "切片文件所有文件上传【不用】")
    @PostMapping(value = "/upload2")
    public AjaxResult multipleFilePartUpload(@ApiParam(value = "文件夹名称唯一") String folderName,
                                             @ApiParam(value = "分片文件") List<MultipartFile> fileParts,
                                             @ApiParam(value = "原始文件名") String fileName,
                                             @ApiParam(value = "分片总数") Integer partNum,
                                             @ApiParam(value = "作业ID") Long recordId) {
        return fileService.multipleFilePartUpload(folderName, fileParts, fileName, partNum, recordId);
    }

}

2、Service层

package com.company.pvinspection.service;

import com.company.common.core.baseweb.domain.AjaxResult;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

/**
 * 文件上传接口
 *
 * @author kally
 * @date 2023/8/10
 */
public interface IFileUploadMinioService {

    /**
     * 单文件分片上传
     *
     * @param folderName  文件夹名称唯一
     * @param filePart  分片文件
     * @param fileName  原始文件名
     * @param partNum   分片数量
     * @param recordId  作业ID
     * @return 结果
     */
    AjaxResult singleFilePartUpload(String folderName, MultipartFile filePart, String fileName, Integer partNum, Long recordId);

    /**
     * 多文件分片上传
     *
     * @param folderName  文件夹名称唯一
     * @param fileParts  多个分片文件
     * @param fileName  原始文件名
     * @param partNum   分片数量
     * @param recordId  作业ID
     * @return 结果
     */
    AjaxResult multipleFilePartUpload(String folderName, List<MultipartFile> fileParts, String fileName, Integer partNum, Long recordId);

}

3、Service实现

package com.company.project.service.impl;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.company.common.core.baseweb.domain.AjaxResult;
import com.company.common.core.constant.Constants;
import com.company.common.core.utils.file.FileUtils;
import com.company.common.framework.minio.MinioUtil;
import com.company.project.domain.TaskRecord;
import com.company.project.service.IFileUploadMinioService;
import com.company.project.service.ITaskRecordService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.stream.Collectors;

/**
 * 大文件切片上传(断点续传)
 *
 * @author kally
 * @date 2023-08-10
 */
@Slf4j
@Service
public class FileUploadMinioServiceImpl implements IFileUploadMinioService {

    @Autowired
    private MinioUtil minioUtil;

    @Autowired
    private ITaskRecordService recordService;

    @Override
    public AjaxResult singleFilePartUpload(String folderName, MultipartFile filePart, String fileName, Integer partNum, Long recordId) {
        // 实际情况下,这些路径都应该是服务器上面存储文件的路径
        // 文件存放路径
        String filePath = Constants.WINDPVELEC_TEMP_FILE_PATH + "uploadFile/";
        // 临时文件存放路径
        String tempPath = filePath + "temp/" + folderName;
        File dir = new File(tempPath);
        if (!dir.exists()) {
            dir.mkdirs();
        }

        // 生成一个临时文件名
        try {
            // 将分片存储到临时文件夹中
            File file = new File(tempPath, filePart.getOriginalFilename());
            if (file.exists()) {
                log.info(filePart.getOriginalFilename() + "文件已存在!");
            } else {
                // 断点续传
                uploadFileResume(filePart, file);
            }

            File tempDir = new File(tempPath);
            File[] tempFiles = tempDir.listFiles();
            Map<String, Object> isMergePart = new HashMap<>();
            one:
            if (partNum.equals(Objects.requireNonNull(tempFiles).length)) {
                // 需要校验一下,表示已有异步程序正在合并了;如果是分布式这个校验可以加入redis的分布式锁来完成
                if (isMergePart.get(folderName) != null) {
                    break one;
                }
                isMergePart.put(folderName, tempFiles.length);
                log.info("所有分片上传完成,预计总分片:" + partNum + "; 实际总分片:" + tempFiles.length);
                FileOutputStream fileOutputStream = new FileOutputStream(filePath + fileName);
                // 这里如果分片很多的情况下,可以采用多线程来执行
                for (int i = 1; i <= partNum; i++) {
                    // 读取分片数据,进行分片合并
                    FileInputStream fileInputStream = new FileInputStream(tempPath + "/" + fileName + "_" + i + ".part");
                    // 8MB
                    byte[] buf = new byte[1024 * 10];
                    int length;
                    // 读取fis文件输入字节流里面的数据
                    while ((length = fileInputStream.read(buf)) != -1) {
                        // 通过fos文件输出字节流写出去
                        fileOutputStream.write(buf, 0, length);
                    }
                    fileInputStream.close();
                }
                fileOutputStream.flush();
                fileOutputStream.close();


                // 删除临时文件夹里面的分片文件 如果使用流操作且没有关闭输入流,可能导致删除失败
                for (int i = 1; i <= partNum; i++) {
                    boolean delete = new File(tempPath + "/" + fileName + "_" + i + ".part").delete();
                    File fileDelete = new File(tempPath + "/" + fileName + "_" + i + ".part");
                    log.info(i + " ; 是否删除:" + delete + " ; 是否还存在:" + fileDelete.exists());
                }
                // 在删除对应的临时文件夹
                if (Objects.requireNonNull(tempDir.listFiles()).length == 0) {
                    boolean delete = tempDir.delete();
                    log.info("文件夹 " + tempPath + "是否删除:" + delete);
                }
                isMergePart.remove(folderName);

                // 返回文件上传结果:成功或失败
                String imgUrl = minioUtil.uploadObjectAndObjectUrl(fileName, filePath + fileName);
                log.info("分片上传=" + imgUrl);
                FileUtils.deleteFile(filePath + fileName);
                updateRecord(recordId, imgUrl);
                return AjaxResult.success();
            }

        } catch (Exception e) {
            log.error("单文件分片上传失败!", e);
            return AjaxResult.error("单文件分片上传失败");
        }
        // 通过返回成功的分片值,来验证分片是否有丢失
        // AjaxResult.success(filePart.getOriginalFilename());
        return AjaxResult.success();
    }

    @Override
    public AjaxResult multipleFilePartUpload(String folderName, List<MultipartFile> fileParts, String fileName, Integer partNum, Long recordId) {
        // 文件存放路径(服务器的存储文件的路径)
        String filePath = Constants.WINDPVELEC_TEMP_FILE_PATH + "uploadFile/";
        // 临时文件存放路径
        String tempPath = filePath + "temp/" + folderName;
        File dir = new File(tempPath);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        Map<String, Object> isMergePart = new HashMap<>();
        File tempDir = new File(tempPath);

        /** 创建线程池 **/
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("bigFilePart-pool-%d").build();
        ExecutorService executorService = Executors.newFixedThreadPool(10, namedThreadFactory);

        /** 大文件切片上传异步处理 **/
        List<CompletableFuture<Void>> taskAsync = fileParts.stream()
                .map(filePart -> CompletableFuture.runAsync(() -> {
                    // 将分片存储到临时文件夹中
                    File file = new File(tempDir, filePart.getOriginalFilename());
                    if (file.exists()) {
                        log.info(filePart.getOriginalFilename() + "文件已存在!");
                    } else {
//                         // 方式1:将文件内容从一个位置传到另一个位置
//                        try {
//                            filePart.transferTo(new File(tempPath + "/" + filePart.getOriginalFilename()));
//                            log.info("文件不存在,上传文件="+tempPath + "/" + filePart.getOriginalFilename());
//                        } catch (IOException e) {
//                            e.printStackTrace();
//                        }

                        // 方式3:断点续传
                        uploadFileResume(filePart, file);
                    }

                }, executorService))
                .collect(Collectors.toList());

        /** 等待所有任务执行完成 **/
        CompletableFuture.allOf(taskAsync.toArray(new CompletableFuture[0])).join();

        /** 关闭线程池 **/
        executorService.shutdown();

        try {
            File[] tempFiles = tempDir.listFiles();

            one:
            if (partNum.equals(Objects.requireNonNull(tempFiles).length)) {
                // 需要校验一下,表示已有异步程序正在合并了;如果是分布式这个校验可以加入redis的分布式锁来完成
                if (isMergePart.get(folderName) != null) {
                    break one;
                }
                isMergePart.put(folderName, tempFiles.length);
                log.info("所有分片上传完成,预计总分片:" + partNum + "; 实际总分片:" + tempFiles.length);
                FileOutputStream fileOutputStream = new FileOutputStream(filePath + fileName);
                // 这里如果分片很多的情况下,可以采用多线程来执行
                for (int i = 1; i <= partNum; i++) {
                    // 读取分片数据,进行分片合并
                    FileInputStream fileInputStream = new FileInputStream(tempPath + "/" + fileName + "_" + i + ".part");
                    // 8MB
                    byte[] buf = new byte[1024 * 8];
                    int length;
                    // 读取fis文件输入字节流里面的数据
                    while ((length = fileInputStream.read(buf)) != -1) {
                        // 通过fos文件输出字节流写出去
                        fileOutputStream.write(buf, 0, length);
                    }
                    fileInputStream.close();
                }
                fileOutputStream.flush();
                fileOutputStream.close();

                // 删除临时文件夹里面的分片文件 如果使用流操作且没有关闭输入流,可能导致删除失败
                for (int i = 1; i <= partNum; i++) {
                    boolean delete = new File(tempPath + "/" + fileName + "_" + i + ".part").delete();
                    File file = new File(tempPath + "/" + fileName + "_" + i + ".part");
                    log.info(i + " ; 是否删除:" + delete + " ; 是否还存在:" + file.exists());
                }
                // 在删除对应的临时文件夹
                if (Objects.requireNonNull(tempDir.listFiles()).length == 0) {
                    boolean delete = tempDir.delete();
                    log.info("文件夹 " + tempPath + "是否删除:" + delete);
                }

                isMergePart.remove(folderName);

                // 返回文件上传结果:成功或失败
                String imgUrl = minioUtil.uploadObjectAndObjectUrl(fileName, filePath + fileName);
                log.info("总体上传=" + imgUrl);
                FileUtils.deleteFile(filePath + fileName);
                updateRecord(recordId, imgUrl);
                return AjaxResult.success();
            }

        } catch (Exception e) {
            log.error("单文件分片上传失败!", e);
            return AjaxResult.error("单文件分片上传失败");
        }

        return AjaxResult.success();
    }

    /**
     * 步骤1:将一个大文件切割成指定大小的小文件
     * 备注:对已知文件进行切割操作 –> 得到多个碎片文件
     *
     * @param src    原大文件
     * @param endsrc 切割后存放的小文件的路径
     * @param num    切割规定的内存大小
     */
    private static void cutFile(String src, String endsrc, int num) {
        FileInputStream fis = null;
        File file;

        try {
            fis = new FileInputStream(src);
            file = new File(src);
            //创建规定大小的byte数组
            byte[] b = new byte[num];
            int len = 0;
            //name为以后的小文件命名做准备
            int index = 0;
            //遍历将大文件读入byte数组中,当byte数组读满后写入对应的小文件中
            while ((len = fis.read(b)) != -1) {
                //分别找到原大文件的文件名和文件类型,为下面的小文件命名做准备
                String name2 = file.getName();
                int lastIndexOf = name2.lastIndexOf(".");
                String substring = name2.substring(0, lastIndexOf);
//                String substring2 = name2.substring(lastIndexOf);
                String fileSuffix = ".part";
                //FileOutputStream fos = new FileOutputStream(endsrc + "\\\\" + substring + "_" + name + substring2);
                FileOutputStream fos = new FileOutputStream(endsrc + "\\\\" + name2 + "_" + index + fileSuffix);
                //将byte数组写入对应的小文件中
                fos.write(b, 0, len);
                //结束资源
                fos.close();
                index++;
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fis != null) {
                    //结束资源
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 上传文件(考虑断点续传)--使用
     *
     * @param filePart 分片文件(源文件)
     * @param file     目标文件
     */
    private void uploadFileResume(MultipartFile filePart, File file) {
        long uploadedBytes = 0;

        if (!file.exists()) {
            // 获取源文件大小
            long fileSize = filePart.getSize();

            // 随机访问文件
            try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw")) {
                // 设置文件指针位置
                randomAccessFile.seek(uploadedBytes);

                // 文件输入流
                try (FileInputStream fileInputStream = new FileInputStream(FileUtils.multipartFileToFile(filePart))) {
                    //文件流大小
                    byte[] buffer = new byte[4096];
                    int bytesRead;

                    while ((bytesRead = fileInputStream.read(buffer)) != -1) {
                        // 写入到目标文件
                        randomAccessFile.write(buffer, 0, bytesRead);
                        uploadedBytes += bytesRead;
                    }

                    if (fileSize == uploadedBytes) {
                        log.info("分片文件上传完成," + filePart.getOriginalFilename());
                    }
                } catch (IOException ioException) {
                    ioException.printStackTrace();
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }

    /**
     * 根据作业ID更新历史视频地址
     *
     * @param recordId 作业ID
     * @param imgUrl   图片地址
     */
    private void updateRecord(Long recordId, String imgUrl) {
        recordService.updateRecord(TaskRecord.builder().recordId(recordId).videoUrl(imgUrl).build());
    }

    /**
     * 测试
     *
     * @param args
     */
    public static void main(String[] args) {
        // 步骤1:切片处理文件
        //调用cutFile()函数 传人参数分别为 (原大文件,切割后存放的小文件的路径,切割规定的内存大小)
        //cutFile("D:\\java\\cut\\ForrestGump.avi", "D:\\java\\cuts", 1024 * 1024 * 20);
        //cutFile("G:\\Users\\lenovo\\Desktop\\tangdou.mp4", "E:\\java\\IdeaProjects\\git-new3s\\datangchaoyang\\WindPvElecIntg\\file\\temp", 1024 * 1024 * 1);
        //cutFile("G:\\Users\\lenovo\\Desktop\\DJI_20220504132406_0012_Z.JPG", "E:\\java\\file\\temp", 1024 * 1024 * 2);
        cutFile("G:\\Users\\lenovo\\Desktop\\go3-u5-3.mp4", "E:\\java\\Idea\\file\\temp", 1024 * 1024 * 5);
    }

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值