minio&前后端分离上传视频/上传大文件——前后端分离断点续传&minio分片上传实现(三)

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: minio&前后端分离上传视频/上传大文件——前后端分离断点续传&minio分片上传实现

6.实战开发 - 业务代码

6.1 检查文件是否已存在

6.1.1 MediaFileController

package com.zhulang.waveedu.service.controller;
import com.alibaba.fastjson.JSONObject;
import com.zhulang.waveedu.common.entity.Result;
import com.zhulang.waveedu.service.service.MediaFileService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.stereotype.Controller;
import javax.annotation.Resource;
/**
 * <p>
 * 第三方服务-媒资文件表 前端控制器
 * </p>
 *
 * @author 狐狸半面添
 * @since 2023-02-08
 */
@Controller
@RequestMapping("/media-file")
public class MediaFileController {
    @Resource
    private MediaFileService mediaFileService;
    /**
     * 文件上传前检查文件是否存在
     *
     * @param object 需要上传的文件的md5值
     * @return 是否存在, false-不存在 true-存在
     */
    @PostMapping("/upload/checkFile")
    public Result checkFile(@RequestBody JSONObject object) {
        return mediaFileService.checkFile(object.getString("fileMd5"));
    }
}

6.1.2 MediaFileService

package com.zhulang.waveedu.service.service;
import com.zhulang.waveedu.common.entity.Result;
import com.zhulang.waveedu.service.po.MediaFile;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * 第三方服务-媒资文件表 服务类
 * </p>
 *
 * @author 狐狸半面添
 * @since 2023-02-08
 */
public interface MediaFileService extends IService<MediaFile> {
    /**
     * 文件上传前检查文件是否存在
     *
     * @param fileMd5 需要上传的文件的md5值
     * @return 是否存在,false-不存在 true-存在
     */
    Result checkFile(String fileMd5);
}

6.1.3 MediaFileServiceImpl

package com.zhulang.waveedu.service.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.zhulang.waveedu.common.constant.HttpStatus;
import com.zhulang.waveedu.common.entity.Result;
import com.zhulang.waveedu.common.util.RegexUtils;
import com.zhulang.waveedu.service.po.MediaFile;
import com.zhulang.waveedu.service.dao.MediaFileMapper;
import com.zhulang.waveedu.service.service.MediaFileService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import io.minio.GetObjectArgs;
import io.minio.MinioClient;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.InputStream;
/**
 * <p>
 * 第三方服务-媒资文件表 服务实现类
 * </p>
 *
 * @author 狐狸半面添
 * @since 2023-02-08
 */
@Service
public class MediaFileServiceImpl extends ServiceImpl<MediaFileMapper, MediaFile> implements MediaFileService {
    @Resource
    private MediaFileMapper mediaFileMapper;
    @Resource
    private MinioClientUtils minioClientUtils;
    @Override
    public Result checkFile(String fileMd5) {
        HashMap<String, Object> resultMap = new HashMap<>();
        // 1.校验 fileMd5 合法性
        if (RegexUtils.isMd5HexInvalid(fileMd5)) {
            return Result.error(HttpStatus.HTTP_BAD_REQUEST.getCode(), "文件md5格式错误");
        }
        // 2.在文件表存在,并且在文件系统存在,此文件才存在
        // 2.1 判断是否在文件表中存在
        LambdaQueryWrapper<MediaFile> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(MediaFile::getFileMd5, fileMd5);
        MediaFile mediaFile = mediaFileMapper.selectOne(wrapper);
        if (mediaFile == null) {
            resultMap.put("exist", false);
            return Result.ok(resultMap);
        }
        // 2.2 判断是否在文件系统存在
        try {
            InputStream inputStream = minioClientUtils.getObject(mediaFile.getBucket(), mediaFile.getFilePath());
            if (inputStream == null) {
                // 文件不存在
                resultMap.put("exist", false);
                return Result.ok(resultMap);
            }
        } catch (Exception e) {
            // 文件不存在
            resultMap.put("exist", false);
            return Result.ok(resultMap);
        }
        // 3.走到这里说明文件已存在,返回true
        resultMap.put("exist", true);
        // 4.封装文件信息
        resultMap.put("info", encodeFileInfo(mediaFile));
        // 5.返回结果
        return Result.ok(resultMap);
    }
    /**
     * 将 部分信息进行封装加密
     *
     * @param mediaFile 媒资对象
     * @return 加密结果
     */
    private String encodeFileInfo(MediaFile mediaFile) {
        HashMap<String, Object> fileMap = new HashMap<>(5);
        fileMap.put("fileType", mediaFile.getFileType());
        fileMap.put("filePath", mediaFile.getBucket() + "/" + mediaFile.getFilePath());
        fileMap.put("fileFormat", mediaFile.getFileFormat());
        fileMap.put("fileByteSize", mediaFile.getFileByteSize());
        fileMap.put("fileFormatSize", mediaFile.getFileFormatSize());
        return CipherUtils.encrypt(JSON.toJSONString(fileMap));
    }
}

6.2 检查分块文件是否已存在

6.2.1 MediaFileController

/**
     * 分块文件上传前检测分块文件是否已存在
     *
     * @param chunkFileVO 分块文件的源文件md5和该文件索引
     * @return 是否存在, false-不存在 true-存在
     * @throws Exception
     */
    @PostMapping("/upload/checkChunk")
    public Result checkChunk(@Validated @RequestBody ChunkFileVO chunkFileVO) throws Exception {
        return mediaFileService.checkChunk(chunkFileVO.getFileMd5(),chunkFileVO.getChunkIndex());
    }

6.2.2 MediaFileService

/**
     * 分块文件上传前检测分块文件是否已存在
     *
     * @param fileMd5 分块文件的源文件md5
     * @param chunkIndex 分块文件索引
     * @return 是否存在, false-不存在 true-存在
     */
    Result checkChunk(String fileMd5, Integer chunkIndex);

6.2.3 MediaFileServiceImpl

@Resource
    private MinioClientUtils minioClientUtils;
    @Value("${minio.bucket}")
    private String bucket;
    @Override
    public Result checkChunk(String fileMd5, Integer chunkIndex) {
        // 1.得到分块文件所在目录
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        // 2.分块文件的路径
        String chunkFilePath = chunkFileFolderPath + chunkIndex;
        // 3.查看是否在文件系统存在(注意关闭流)
        try (
                InputStream inputStream = minioClientUtils.getObject(bucket, chunkFilePath)
        ) {
            if (inputStream == null) {
                //文件不存在
                return Result.ok(false);
            }
        } catch (Exception e) {
            //文件不存在
            return Result.ok(false);
        }
        // 4.走到这里说明文件已存在,返回true
        return Result.ok(true);
    }
    /**
     * 得到分块文件的目录
     *
     * @param fileMd5 文件的md5值
     * @return 分块文件所在目录
     */
    private String getChunkFileFolderPath(String fileMd5) {
        return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
    }

6.3 上传分块文件

6.3.1 MediaFileController

/**
     * 上传分块文件
     *
     * @param file       分块文件
     * @param fileMd5    原文件md5值
     * @param chunkIndex 分块文件索引
     * @return 上传情况
     */
    @PostMapping("/upload/uploadChunk")
    public Result uploadChunk(@RequestParam("file") MultipartFile file,
                              @RequestParam("fileMd5") @Pattern(regexp = RegexUtils.RegexPatterns.MD5_HEX_REGEX, message = "文件md5格式错误") String fileMd5,
                              @RequestParam("chunkIndex") @Min(value = 0, message = "索引必须大于等于0") Integer chunkIndex) throws Exception {
        return mediaFileService.uploadChunk(fileMd5, chunkIndex, file.getBytes());
    }

6.3.2 MediaFileService

/**
     * 上传分块文件
     *
     * @param fileMd5    原文件md5值
     * @param chunkIndex 分块文件索引
     * @param bytes      分块文件的字节数组形式
     * @return 上传情况
     */
    Result uploadChunk(String fileMd5, Integer chunkIndex, byte[] bytes);

6.3.3 MediaFileServiceImpl

@Resource
    private MinioClientUtils minioClientUtils;
    @Value("${minio.bucket}")
    private String bucket;
  @Override
    public Result uploadChunk(String fileMd5, Integer chunkIndex, byte[] bytes) {
        // 1.得到分块文件所在目录
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        // 2.分块文件的路径
        String chunkFilePath = chunkFileFolderPath + chunkIndex;
        try {
            // 3.将分块上传到文件系统
            minioClientUtils.uploadChunkFile(bytes, bucket, chunkFilePath);
            // 4.上传成功
            return Result.ok();
        } catch (Exception e) {
            // 上传失败
            return Result.error();
        }
    }
    /**
     * 得到分块文件的目录
     *
     * @param fileMd5 文件的md5值
     * @return 分块文件所在目录
     */
    private String getChunkFileFolderPath(String fileMd5) {
        return fileMd5.charAt(0) + "/" + fileMd5.charAt(1) + "/" + fileMd5 + "/" + "chunk" + "/";
    }

6.4 合并前下载分块文件(多线程下载)

合并分块前要检查分块文件是否全部上传完成,如果完成则将已经上传的分块文件从minio下载下来,然后再进行合并。

/**
     * 创建10个线程数量的线程池
     */
    private final ExecutorService threadPool = Executors.newFixedThreadPool(10);
    /**
     * 下载所有的块文件
     *
     * @param fileMd5    源文件的md5值
     * @param chunkTotal 块总数
     * @return 所有块文件
     */
    private File[] downloadChunkFilesFromMinio(String fileMd5, int chunkTotal) throws Exception {
        // 1.得到分块文件所在目录
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        // 2.分块文件数组
        File[] chunkFiles = new File[chunkTotal];
        // 3.设置计数器
        CountDownLatch countDownLatch = new CountDownLatch(chunkTotal);
        // 4.开始逐个下载
        for (int i = 0; i < chunkTotal; i++) {
            int index = i;
            threadPool.execute(() -> {
                // 4.1 得到分块文件的路径
                String chunkFilePath = chunkFileFolderPath + index;
                // 4.2 下载分块文件
                try {
                    chunkFiles[index] = minioClientUtils.downloadFile("chunk", null, bucket, chunkFilePath);
                } catch (Exception e) {
                    // 计数器减1
                    countDownLatch.countDown();
                    throw new RuntimeException(e);
                }
                // 计数器减1
                countDownLatch.countDown();
            });
        }
        /*
            阻塞到任务执行完成,当countDownLatch计数器归零,这里的阻塞解除等待,
            给一个充裕的超时时间,防止无限等待,到达超时时间还没有处理完成则结束任务
         */
        countDownLatch.await(30, TimeUnit.MINUTES);
        // 5.返回所有块文件
        return chunkFiles;
    }

6.5 合并分块文件并上传

6.5.1 MediaFileController

/**
     * 合并分块文件
     *
     * @param fileMd5    文件的md5十六进制值
     * @param fileName   文件名
     * @param tag        文件标签
     * @param chunkTotal 文件块总数
     * @return 合并与上传情况
     */
    @PostMapping("/upload/uploadMergeChunks")
    public Result uploadMergeChunks(@RequestParam("fileMd5") @Pattern(regexp = RegexUtils.RegexPatterns.MD5_HEX_REGEX, message = "文件md5格式错误") String fileMd5,
                                    @RequestParam("fileName") @Pattern(regexp = RegexUtils.RegexPatterns.FILE_NAME_REGEX, message = "文件名最多255个字符") String fileName,
                                    @RequestParam("tag") @Pattern(regexp = RegexUtils.RegexPatterns.FILE_TAG_REGEX, message = "文件标签最多32个字符") String tag,
                                    @RequestParam("chunkTotal") @Min(value = 1, message = "块总数必须大于等于1") Integer chunkTotal) {
        return mediaFileService.uploadMergeChunks(fileMd5, fileName, tag, chunkTotal);
    }

6.5.2 MediaFileService

/**
     * 合并分块文件
     *
     * @param fileMd5    文件的md5十六进制值
     * @param fileName   文件名
     * @param tag        文件标签
     * @param chunkTotal 文件块总数
     * @return 合并与上传情况
     */
    Result uploadMergeChunks(String fileMd5, String fileName, String tag, Integer chunkTotal);

6.5.3 MediaFileServiceImpl

/**
     * 将 部分信息进行封装加密
     *
     * @param mediaFile 媒资对象
     * @return 加密结果
     */
    private String encodeFileInfo(MediaFile mediaFile) {
        HashMap<String, Object> fileMap = new HashMap<>(5);
        fileMap.put("fileType", mediaFile.getFileType());
        fileMap.put("filePath", mediaFile.getBucket() + "/" + mediaFile.getFilePath());
        fileMap.put("fileFormat", mediaFile.getFileFormat());
        fileMap.put("fileByteSize", mediaFile.getFileByteSize());
        fileMap.put("fileFormatSize", mediaFile.getFileFormatSize());
        return CipherUtils.encrypt(JSON.toJSONString(fileMap));
    }
  @Override
    public Result uploadMergeChunks(String fileMd5, String fileName, String tag, Integer chunkTotal) {
        try {
            // 1.下载分块
            File[] chunkFiles = downloadChunkFilesFromMinio(fileMd5, chunkTotal);
            // 2.根据文件名得到合并后文件的扩展名
            int index = fileName.lastIndexOf(".");
            String extension = index != -1 ? fileName.substring(index) : "";
            File tempMergeFile = null;
            try {
                try {
                    // 3.创建一个临时文件作为合并文件
                    tempMergeFile = File.createTempFile("merge", extension);
                } catch (IOException e) {
                    return Result.error(HttpStatus.HTTP_INTERNAL_ERROR.getCode(), "创建临时合并文件出错");
                }
                // 4.创建合并文件的流对象
                try (RandomAccessFile rafWrite = new RandomAccessFile(tempMergeFile, "rw")) {
                    byte[] b = new byte[1024];
                    for (File file : chunkFiles) {
                        // 5.读取分块文件的流对象
                        try (RandomAccessFile rafRead = new RandomAccessFile(file, "r");) {
                            int len = -1;
                            while ((len = rafRead.read(b)) != -1) {
                                // 6.向合并文件写数据
                                rafWrite.write(b, 0, len);
                            }
                        }
                    }
                } catch (IOException e) {
                    return Result.error(HttpStatus.HTTP_INTERNAL_ERROR.getCode(), "合并文件过程出错");
                }
                // 7.校验合并后的文件是否正确
                try (
                        // 7.1 获取合并后文件的流对象
                        FileInputStream mergeFileStream = new FileInputStream(tempMergeFile);
                ) {
                    // 7.2 获取合并文件的md5十六进制值
                    String mergeMd5Hex = DigestUtils.md5Hex(mergeFileStream);
                    // 7.3 校验
                    if (!fileMd5.equals(mergeMd5Hex)) {
                        return Result.error(HttpStatus.HTTP_BAD_REQUEST.getCode(), "合并文件校验不通过");
                    }
                } catch (IOException e) {
                    return Result.error(HttpStatus.HTTP_INTERNAL_ERROR.getCode(), "合并文件校验出错");
                }
                // 8.得到 mimetype
                String mimeType = FileTypeUtils.getMimeType(tempMergeFile);
                // 9.拿到合并文件在minio的存储路径
                String mergeFilePath = getFilePathByMd5(fileMd5, extension);
                // 10.将合并后的文件上传到文件系统
                minioClientUtils.uploadFile(tempMergeFile.getAbsolutePath(), bucket, mergeFilePath);
                // 11.设置需要入库的文件信息
                MediaFile mediaFile = new MediaFile();
                // 11.1 文件名
                mediaFile.setFileName(WaveStrUtils.removeBlank(fileName));
                // 11.2 文件类型
                mediaFile.setFileType(FileTypeUtils.getSimpleType(mimeType));
                // 11.3 文件格式
                mediaFile.setFileFormat(mimeType);
                // 11.4 文件标签
                mediaFile.setTag(WaveStrUtils.removeBlank(tag));
                // 11.5 存储桶
                mediaFile.setBucket(bucket);
                // 11.6 存储路径
                mediaFile.setFilePath(mergeFilePath);
                // 11.7 设置md5值
                mediaFile.setFileMd5(fileMd5);
                // 11.8 设置合并文件大小(单位:字节)
                mediaFile.setFileByteSize(tempMergeFile.length());
                // 11.9 设置文件格式大小
                mediaFile.setFileFormatSize(FileFormatUtils.formatFileSize(mediaFile.getFileByteSize()));
                // 11.10 设置上传者
                mediaFile.setUserId(UserHolderUtils.getUserId());
                // 12.保存至数据库
                this.save(mediaFile);
                // 15.返回成功
                return Result.ok(encodeFileInfo(mediaFile));
            } finally {
                // 13.删除临时分块文件
                for (File chunkFile : chunkFiles) {
                    if (chunkFile.exists()) {
                        chunkFile.delete();
                    }
                }
                // 14.删除合并的临时文件
                if (tempMergeFile != null) {
                    tempMergeFile.delete();
                }
            }
        } catch (Exception e) {
            return Result.error(HttpStatus.HTTP_INTERNAL_ERROR.getCode(), e.getMessage());
        }
    }
    /**
     * 得到合并文件的路径
     *
     * @param fileMd5 文件的md5十六进制值
     * @param fileExt 文件的扩展名
     * @return 合并文件路径
     */
    private String getFilePathByMd5(String fileMd5, String fileExt) {
        return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + fileMd5 + fileExt;
    }

7.补充-面试题

7.1 MinIO是什么?

MinIO一个轻量级的分布式文件系统,由多个个MinIO节点连接组成,可根据文件规模进行扩展,适用于海量文件的存储与访问。

7.2 为什么用MinIO

  1. MinIO开源,使用简单,功能强大。
  2. MinIO使用纠删码算法,只要不超过一半的节点坏掉整个文件系统就可以使用。
  3. 如果将坏的节点重新启动,自动恢复没有上传成功的文件。

7.3 怎么样构建这个独立文件服务?

  1. 我们项目中有很多要上传文件的地方,比如上传图片、上传文档、上传视频等,所以我们要构建一 个独立的文件服务负责上传、下载等功能,负责对文件进行统一管理。
  2. 创建单独的文件服务,提供以下接口:
  • 上传接口
  • 下载接口
  • 我的图库接口
  • 我的文件库接口
  • 删除文件接口
  1. 文件的存储和下载使用MinIO实现。

MinIO是一个分布式的文件系统,性能高,扩展强。

  1. 使用Nginx+MinIO组成一个文件服务器。通过访问Nginx,由nginx代理将请求转发到MinIO去浏览、下载文件。

7.4 断点续传是怎么做的?

我们是基于分块上传的模式实现断点续传的需求,当文件上传一部分断网后前边已经上传过的不再上传。

  1. 前端对文件分块。
  2. 前端使用多线程一块-块上传,上传前给服务端发一 个消息校验该分块是否上传,如果已上传则不再上传。
  3. 等所有分块上传完毕,服务端合并所有分块,校验文件的完整性。因为分块全部上传到了服务器,服务器将所在分块按顺序进行合并,就是写每个分块文件内容按顺序依次写入一个文件中。(使用字节流去读写文件)
  4. 前端给服务传了一个md5值,服务端合并文件后计算合并后文件的md5是否和前端传的一样,如果一样则说文件完整,如果不一样说明可能由于网络丢包导致文件不完整,这时上传失败需要重新上传。

7.5 分块文件清理问题

上传一个文件进行分块上传,上传一半不传了, 之前上传到minio的分块文件要清理吗?怎么做的?

  1. 在数据库中有一张文件表记录minio中存储的文件信息。
  2. 文件开始上传时会写入文件表,状态为,上传中,上传完成会更新状态为上传完成。
  3. 当一个文件传了一半不再上传了说明该文件没有上传完成,会有定时任务去查询文件表中的记录,如果文件未上传完成则删除minio中没有上传成功的文件目录。
相关文章
|
6月前
|
JavaScript 前端开发 Java
springboot整合minio+vue实现大文件分片上传,断点续传(复制可用,包含minio工具类)
springboot整合minio+vue实现大文件分片上传,断点续传(复制可用,包含minio工具类)
1695 0
|
6月前
|
前端开发 NoSQL Redis
如何实现大文件上传:秒传、断点续传、分片上传
如何实现大文件上传:秒传、断点续传、分片上传
511 0
|
6月前
|
存储
fastdfs源码阅读:上传和下载(文件客户端逻辑)
fastdfs源码阅读:上传和下载(文件客户端逻辑)
253 0
|
6月前
|
Kubernetes Windows 容器
minio上传下载
minio上传下载
133 0
|
存储 前端开发 NoSQL
注册java实现文件分片上传并且断点续传
一、简单的分片上传 针对第一个问题,如果文件过大,上传到一半断开了,若重新开始上传的话,会很消耗时间,并且你也并不知道距离上次断开时,已经上传到哪一部分了。因此我们应该先对大文件进行分片处理,防止上面提到的问题。
|
前端开发 Java 数据库
minio&前后端分离上传视频/上传大文件——前后端分离断点续传&minio分片上传实现(一)
minio&前后端分离上传视频/上传大文件——前后端分离断点续传&minio分片上传实现
1091 0
|
数据安全/隐私保护
minio&前后端分离上传视频/上传大文件——前后端分离断点续传&minio分片上传实现(二)
minio&前后端分离上传视频/上传大文件——前后端分离断点续传&minio分片上传实现
421 0
|
存储 分布式计算 网络协议
文件上传下载系列——大文件分片上传
文件上传下载系列——大文件分片上传
|
前端开发
后端处理图片的上传和下载
后端处理图片的上传和下载
166 0
|
前端开发 关系型数据库 MySQL
大文件上传
大文件上传
166 0