Minio大文件上传、文件秒传、断点续传

初步流程:
选择上传文件 -> 提取md5 -> 请求后端校验此md5的文件是否已经上传过 -> 如果有上传就返回信息告诉前端上传完成(秒传) -> 如果没有则根据此md5获取已上传的分片有哪些,未上传的分片有多少个就返回多少个上传url

如何获取已上传的分片有哪些呢?
minio api有一个生成上传url的api,这个api可以指定接下来要上传文件的文件名,也就是说,在上传步骤,我们只要保证上传的分片文件是有规则的,那么我们就可以很轻松的获取到

举个例子:上传文件的大小为10m,分片2个,md5为 asdfghjkl,则我们生成2个上传url,每个url指定的文件名为 md5/分片序号.chunk(分片文件要不要文件后缀都可以) 的格式,也就是 asdfghjkl/1.chunk;asdfghjkl/2.chunk

所有分片文件上传完成后,通过 listObjects 获取到 asdfghjkl前缀开头的所有文件,即是所有分片文件的path,最后合并文件即可

为什么不所有步骤都采用前端直连Minio?毕竟官方的sdk也提供了前端的示例
这样做会直接暴露有关于minio的敏感信息,并且不管怎样,始终也需要后端去记录最终文件上传完成后的信息,所以一些敏感的操作,还是由后端程序来协助

这个流程的好处:

省掉每个分片的md5计算
省掉每上传一个分片文件就请求后端记录一次分片信息
省掉每个分片的数据库存储,合并文件后只需要删除分片文件,不需要再删除数据库数据
可以并发上传
文件上传直连于minio服务,不经过后端程序
分片文件更方便管理,比如分片文件上传到一个B桶里,合并的文件合并到A桶去
不暴露敏感信息
后端程序在整个流程里,仅仅充当了md5校验、生成url、通知minio合并文件的角色

后端代码实现如下

1、配置文件

spring.servlet.multipart.max-file-size=1000MB
spring.servlet.multipart.max-request-size=1000MB
spring.servlet.multipart.file-size-threshold=1000MB
spring.jackson.default-property-inclusion=non_null

minio.server.url=http://192.168.5.202:9000
minio.server.accessKey=admin
minio.server.secretKey=admin123
minio.bucket.img=img-library
minio.bucket.chunk=chunk

2、工具类

package com.anx.minio.controller;

import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.SneakyThrows;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.*;
import java.util.stream.Collectors;


@Component
public class MinIoUtils {

    @Value("${minio.server.url}")
    private String url;


    @Value("${minio.server.accessKey}")
    private String accessKey;


    @Value("${minio.server.secretKey}")
    private String secretKey;
    /**
     * 合并后存储的桶名称
     */
    @Value("${minio.bucket.img}")
    private String imgBucket;
    /**
     * 分片存储的桶名称
     */
    @Value("${minio.bucket.chunk}")
    private String chunkBucKet;

    private static MinioClient minioClient;

    /**
     * 排序
     */
    public final static boolean SORT = true;
    /**
     * 不排序
     */
    public final static boolean NOT_SORT = false;
    /**
     * 默认过期时间(分钟)
     */
    private final static Integer DEFAULT_EXPIRY = 60;

    /**
     * 初始化MinIo对象
     * 非此工具类请勿使用该方法
     */
    @PostConstruct
    public void init() {
        minioClient = MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build();
        //方便管理分片文件,则单独创建一个分片文件的存储桶
        if (!isBucketExist(chunkBucKet)) {
            createBucket(chunkBucKet);
        }
        if (!isBucketExist(imgBucket)) {
            createBucket(imgBucket);
        }
    }

    /**
     * 存储桶是否存在
     *
     * @param bucketName 存储桶名称
     * @return true/false
     */
    @SneakyThrows
    public static boolean isBucketExist(String bucketName) {
        return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }

    /**
     * 创建存储桶
     *
     * @param bucketName 存储桶名称
     * @return true/false
     */
    @SneakyThrows
    public static boolean createBucket(String bucketName) {
        minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        return true;
    }

    /**
     * 获取访问对象的外链地址
     *
     * @param bucketName 存储桶名称
     * @param objectName 对象名称
     * @param expiry     过期时间(分钟) 最大为7天 超过7天则默认最大值
     * @return viewUrl
     */
    @SneakyThrows
    public static String getObjectUrl(String bucketName, String objectName, Integer expiry) {
        expiry = expiryHandle(expiry);
        return minioClient.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .method(Method.GET)
                        .bucket(bucketName)
                        .object(objectName)
                        .expiry(expiry)
                        .build()
        );
    }

    /**
     * 创建上传文件对象的外链
     *
     * @param bucketName 存储桶名称
     * @param objectName 欲上传文件对象的名称
     * @param expiry     过期时间(分钟) 最大为7天 超过7天则默认最大值
     * @return uploadUrl
     */
    @SneakyThrows
    public static String createUploadUrl(String bucketName, String objectName, Integer expiry) {
        expiry = expiryHandle(expiry);
        return minioClient.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .method(Method.PUT)
                        .bucket(bucketName)
                        .object(objectName)
                        .expiry(expiry)
                        .build()
        );
    }

    /**
     * 创建上传文件对象的外链
     *
     * @param bucketName 存储桶名称
     * @param objectName 欲上传文件对象的名称
     * @return uploadUrl
     */
    public static String createUploadUrl(String bucketName, String objectName) {
        return createUploadUrl(bucketName, objectName, DEFAULT_EXPIRY);
    }

    /**
     * 批量创建分片上传外链
     *
     * @param bucketName 存储桶名称
     * @param objectMD5  欲上传分片文件主文件的MD5
     * @param chunkCount 分片数量
     * @return uploadChunkUrls
     */
    public static List<String> createUploadChunkUrlList(String bucketName, String objectMD5, Integer chunkCount) {
        if (null == objectMD5) {
            return null;
        }
        objectMD5 += "/";
        if (null == chunkCount || 0 == chunkCount) {
            return null;
        }
        List<String> urlList = new ArrayList<>(chunkCount);
        for (int i = 1; i <= chunkCount; i++) {
            String objectName = objectMD5 + i + ".chunk";
            urlList.add(createUploadUrl(bucketName, objectName, DEFAULT_EXPIRY));
        }
        return urlList;
    }

    /**
     * 创建指定序号的分片文件上传外链
     *
     * @param bucketName 存储桶名称
     * @param objectMD5  欲上传分片文件主文件的MD5
     * @param partNumber 分片序号
     * @return uploadChunkUrl
     */
    public static String createUploadChunkUrl(String bucketName, String objectMD5, Integer partNumber) {
        if (null == objectMD5) {
            return null;
        }
        objectMD5 += "/" + partNumber + ".chunk";
        return createUploadUrl(bucketName, objectMD5, DEFAULT_EXPIRY);
    }

    /**
     * 获取对象文件名称列表
     *
     * @param bucketName 存储桶名称
     * @param prefix     对象名称前缀
     * @param sort       是否排序(升序)
     * @return objectNames
     */
    @SneakyThrows
    public static List<String> listObjectNames(String bucketName, String prefix, Boolean sort) {
        ListObjectsArgs listObjectsArgs;
        if (null == prefix) {
            listObjectsArgs = ListObjectsArgs.builder()
                    .bucket(bucketName)
                    .recursive(true)
                    .build();
        } else {
            listObjectsArgs = ListObjectsArgs.builder()
                    .bucket(bucketName)
                    .prefix(prefix)
                    .recursive(true)
                    .build();
        }
        Iterable<Result<Item>> chunks = minioClient.listObjects(listObjectsArgs);
        List<String> chunkPaths = new ArrayList<>();
        for (Result<Item> item : chunks) {
            chunkPaths.add(item.get().objectName());
        }
        if (sort) {
            return chunkPaths.stream().distinct().collect(Collectors.toList());
        }
        return chunkPaths;
    }

    /**
     * 获取对象文件名称列表
     *
     * @param bucketName 存储桶名称
     * @param prefix     对象名称前缀
     * @return objectNames
     */
    public static List<String> listObjectNames(String bucketName, String prefix) {
        return listObjectNames(bucketName, prefix, NOT_SORT);
    }

    /**
     * 获取分片文件名称列表
     *
     * @param bucketName 存储桶名称
     * @param objectMd5  对象Md5
     * @return objectChunkNames
     */
    public static List<String> listChunkObjectNames(String bucketName, String objectMd5) {
        if (null == objectMd5) {
            return null;
        }
        return listObjectNames(bucketName, objectMd5, SORT);
    }

    /**
     * 获取分片名称地址HashMap key=分片序号 value=分片文件地址
     *
     * @param chunkBucKet 存储桶名称
     * @param objectMd5   对象Md5
     * @return objectChunkNameMap
     */
    public static Map<Integer, String> mapChunkObjectNames(String chunkBucKet, String objectMd5) {
        if (null == objectMd5) {
            return null;
        }
        List<String> chunkPaths = listObjectNames(chunkBucKet, objectMd5);
        if (null == chunkPaths || chunkPaths.size() == 0) {
            return null;
        }
        Map<Integer, String> chunkMap = new HashMap<>(chunkPaths.size());
        for (String chunkName : chunkPaths) {
            Integer partNumber = Integer.parseInt(chunkName.substring(chunkName.indexOf("/") + 1, chunkName.lastIndexOf(".")));
            chunkMap.put(partNumber, chunkName);
        }
        return chunkMap;
    }

    /**
     * 合并分片文件成对象文件
     *
     * @param chunkBucKetName   分片文件所在存储桶名称
     * @param composeBucketName 合并后的对象文件存储的存储桶名称
     * @param chunkNames        分片文件名称集合
     * @param objectName        合并后的对象文件名称
     * @return true/false
     */
    @SneakyThrows
    public static boolean composeObject(String chunkBucKetName, String composeBucketName, List<String> chunkNames, String objectName) {
        //合并文件
        List<ComposeSource> sourceObjectList = new ArrayList<>(chunkNames.size());
  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

非ban必选

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

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

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

打赏作者

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

抵扣说明:

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

余额充值