拿来即用:SpringBoot+Minio+vue-uploader实现分片上传

Hi,大家好,我是抢老婆酸奶的小肥仔。

在日常开发中,我们常常需要文件上传,传统上实现上传是直接将文件保存到本地磁盘,然后通过磁盘路径进行下载查看。这样子会造成磁盘被大量占用,同时一不小心手欠的话就会将文件删除。这时候,我们可能会想到用一些文件存储系统,例如:我们熟悉的FastDFS等,我们今天介绍的主角不是FastDFS,而是Minio。

废话不多说,开整。

1、Minio简介及部署

Minio:一种分布式文件存储,具有高性能,轻量级,速度快,容错率高等特点,兼容亚马逊S3云存储服务接口,并可以作为一个独立的存储后端。

minio提供了纠删码策略,即将数据进行切分,同时计算校验块,采用Reed-Solomon code将对象拆分成N/2数据和N/2奇偶校验块,假如有8块盘,数据则被分成4个数据块,4个奇偶校验块。即使这个对象丢了4块盘,数据依然可以进行恢复,因此即使我们一不小心删除了一些盘,也不用担心数据会丢失。

1.1 Minio部署

我们以win10环境为例,进行Minio部署。

1.1.1 下载Minio

minio下载地址:www.minio.org.cn/download.sh...

选择windows版本进行下载,下载完成后是一个exe文件。

1.1.2 部署minio

在windows下创建一个文件夹用来放置minio执行文件,例如:我将其放在D盘下的soft/minio下。

Minio的部署可以分为:单节点单磁盘,单节点多磁盘,多节点方式。

单节点单磁盘执行执行exe文件即可,我们就说说单节点多次盘,多节点两种部署方式。

1.1.2.1 单节点多磁盘

1.1.2.1.1 创建目录

即通过一个节点进行访问,数据被保存在不同磁盘下。

我们创建4个文件夹来模拟四块不同的磁盘。如图:

1.1.2.1.2 启动minio

minio启动需要编写脚本,在存放minio.exe文件夹下创建一个minio.bat文件,文件内容如下:

bat 复制代码
@echo off
set MINIO_ROOT_USER=admin
set MINIO_ROOT_PASSWORD=12345678
cd "D:\soft\minio"
start minio.exe server --console-address ":9000" D:\soft\minio\dataOne D:\soft\minio\dataTwo D:\soft\minio\dataThree D:\soft\minio\dataFour

--console-address ":9000" :管理页的ip和端口,缺省时默认是127.0.0.1:9000

--address "127.0.0.1:9090" :代表接口的ip和端口

1.1.2.1.3 访问

直接在浏览器上输入:http://localhost:9000即可进行访问。

1.1.2.2 多节点

即端口不一样,数据磁盘路径一致。

例如我们设置四个不同端口:9001,9002,9003,9004。分别执行如下脚本:

bat 复制代码
set MINIO_ROOT_USER=admin
set MINIO_ROOT_PASSWORD=12345678
start minio.exe server --console-address "127.0.0.1:9001"  --address "127.0.0.1:9091"
http://127.0.0.1:9091/D:\soft\minio\dataOne
http://127.0.0.1:9092/D:\soft\minio\dataTwo
http://127.0.0.1:9093/D:\soft\minio\dataThree
http://127.0.0.1:9094/D:\soft\minio\dataFour

其他端口执行,只需要更改端口就好。

启动minio

由于涉及到多个端口,访问资源时,不可能对每个节点进行访问显然不合理,因此我们可以通过nginx来进行代理。配置如下:

config 复制代码
upstream minio{
  server 127.0.0.1:9001;
  server 127.0.0.1:9002;
  server 127.0.0.1:9003;
  server 127.0.0.1:9004;
}

server{
    listen       8888;
    server_name  127.0.0.1;
    ignore_invalid_headers off;
    client_max_body_size 0;
    proxy_buffering off;
    location / {
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-Host  $host:$server_port;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto  $http_x_forwarded_proto;
        proxy_set_header   Host $http_host;
        proxy_connect_timeout 300;
        proxy_http_version 1.1;
        chunked_transfer_encoding off;
        proxy_ignore_client_abort on;
        proxy_pass http://minio;
    }
}

2、分片上传

在minio中实现上传会自动进行分块,然后再将分块上传到Minio服务器,最后在进行合并。但是我们在使用时必须要将整个文件上传到系统,然后再调用Minio接口进行文件上传,当文件比较大时,就会占用太多宽带,从而大致上传慢,甚至使服务挂掉。因此我们需要进行优化。

在minio中提供了三个方法completeMultipartUpload,createMultipartUpload,listMultipart通过这三个方法我们可以将文件进行分片,分片后返回分片上传连接,等所有分片上传完成后,再进行分片合并,从而完成整个文件的上传。大致流程:

1、用户在前端使用vue-uploader上传文件,判断文件的大小。

2、如果文件大小小于5M则直接通过接口上传到minio服务器,如果大于5M时,计算分片数,调用分片接口获取每个分片对应的上传地址。

3、根据分片计算每个分片的大小,将文件按大小进行分片,调用分片地址将分片进行上传。

4、分片上传完成后,将分片进行合并,在服务器上形成文件。

2.1 代码实现

2.1.1 后端实现

基于minio的分片上传主要是重写三个接口,即completeMultipartUpload,createMultipartUpload,listMultipart,这三个接口在minio包中是protected的,我们如果想要使用这三个方法,只能重写这三个方法即可。

completeMultipartUpload 即完成分片上传后,进行分片合并。

createMultipartUpload 即返回每个分片对应Id及上传的url。

listMultipart 即查询分片信息。

1、定义PearMinioClient

PearMinioClient主要是集成MinioClient,重写MinioClient中的三个方法。

java 复制代码
/**
 * @author: jiangjs
 * @description: 分片上传MinioClient继承MinioClient,主要暴露:
 *       createMultipartUpload:创建分片请求,返回uploadId
 *       listMultipart:查询分片信息
 *       completeMultipartUpload:根据uploadId合并已上传的分片
 * @date: 2023/10/24 14:08
 **/
public class PearMinioClient extends MinioClient {

    protected PearMinioClient(MinioClient client) {
        super(client);
    }

    @Override
    public ObjectWriteResponse completeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {
        return super.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);
    }

    @Override
    public CreateMultipartUploadResponse createMultipartUpload(String bucketName, String region, String objectName, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {
        return super.createMultipartUpload(bucketName, region, objectName, headers, extraQueryParams);
    }

    public ListPartsResponse listMultipart(String bucketName,String region,String objectName,Integer maxParts,Integer partNumberMarker,
                                           String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, IOException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {
        return super.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
    }
}

2、设置minio配置

获取配置文件中配置的minio相关属性,如地址等。

java 复制代码
/**
 * @author: jiangjs
 * @description:
 * @date: 2023/10/20 10:27
 **/
@Component
@Data
@ConfigurationProperties(prefix = "minio")
public class MinioEndPointInfo {
    /**
     * minio节点地址
     */
    private String endpoint;
    /**
     * 登录用户名
     */
    private String accessKey;
    /**
     * 密码
     */
    private String secretKey;

}

其中accessKey,secretKey分别在创建服务用户时创建。

创建minio配置类

java 复制代码
/**
 * @author: jiangjs
 * @description: minio相关配置
 * @date: 2023/10/20 10:46
 **/
@Configuration
@EnableConfigurationProperties(MinioEndPointInfo.class)
public class MinioConfig {
    @Resource
    private MinioEndPointInfo minioEndPointInfo;

    @Bean
    public PearMinioClient createPearMinioClient(){
        MinioClient minioClient = MinioClient.builder()
                .endpoint(minioEndPointInfo.getEndpoint())
                .credentials(minioEndPointInfo.getAccessKey(), minioEndPointInfo.getSecretKey())
                .build();
        return new PearMinioClient(minioClient);
    }
}

上述minio配置类中,创建了minioClient来初始化我们定义的PearMinioClient。

3、创建minio分片工具类

java 复制代码
/**
 * @author: jiangjs
 * @description: minio分片上传工具
 * @date: 2023/10/30 9:34
 **/
@Component
@Slf4j
public class MinioPearUploadUtil {

    @Resource
    private PearMinioClient pearMinioClient;

    /**
     * 校验当前bucket是否存在,不存在则创建
     * @param bucketName 桶
     */
    private void existBucket(String bucketName){
        try {
            boolean isExist = pearMinioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
            if (!isExist){
                pearMinioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 创建分片上传信息
     * @param chunkNum 分片数量
     * @param fileName 文件名称
     * @param contentType 文件类型
     * @param bucketEnum 桶
     * @return 返回分片信息
     */
    @SneakyThrows
    public MinioPearVo createMultipartUploadUrl(Integer chunkNum, String fileName, String contentType,MinioBucketEnum bucketEnum){
        //设置分片文件类型
        Multimap<String, String> headerMap = HashMultimap.create();
        headerMap.put("Content-Type",contentType);
        CreateMultipartUploadResponse uploadResponse = pearMinioClient.createMultipartUpload(bucketEnum.getBucket(), null, fileName,
                headerMap, null);
        Map<String, String> reqParams = new HashMap<>(2);
        reqParams.put("uploadId",uploadResponse.result().uploadId());
        MinioPearVo pearVo = new MinioPearVo();
        pearVo.setUploadId(uploadResponse.result().uploadId());
        List<MinioPearVo.PearUploadData> uploads = new ArrayList<>();
        for (int i = 1; i <= chunkNum; i++) {
            reqParams.put("partNumber",String.valueOf(i));
            String objectUrl = pearMinioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.PUT)
                            .bucket(MinioBucketEnum.EMAIL.getBucket())
                            .object(fileName)
                            .expiry(1, TimeUnit.DAYS)
                            .extraQueryParams(reqParams)
                            .build());
            MinioPearVo.PearUploadData uploadData = new MinioPearVo.PearUploadData();
            uploadData.setUploadUrl(objectUrl).setParkNum(i);
            uploads.add(uploadData);
        }
        pearVo.setParts(uploads);
        return pearVo;
    }

    /**
     * 合并文件分片
     * @param chunkNum 分片数量
     * @param fileName 文件名称
     * @param contentType 文件类型
     * @param uploadId 分片上传时的Id
     * @param bucketEnum 桶
     * @return 合并结果
     */
    @SneakyThrows
    public Boolean completeMultipart(Integer chunkNum, String fileName, String contentType,String uploadId,MinioBucketEnum bucketEnum){
        Multimap<String, String> headerMap = HashMultimap.create();
        headerMap.put("Content-Type",contentType);
        ListPartsResponse listMultipart = pearMinioClient.listMultipart(bucketEnum.getBucket(), null, fileName, chunkNum + 10,
                0, uploadId, headerMap, null);
        if (Objects.nonNull(listMultipart)){
            Part[] parts = new Part[chunkNum+10];
            int partNum = 0;
            for (Part part : listMultipart.result().partList()) {
                parts[partNum] = new Part(partNum,part.etag());
                partNum++;
            }
            pearMinioClient.completeMultipartUpload(MinioBucketEnum.EMAIL.getBucket(), null, fileName,
                    uploadId, parts, headerMap, null);
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }

    /**
     * 文件流上传
     * @param ism 文件流
     * @param bucketName 桶名称
     * @param fileName 文件名称
     * @return 执行结果
     */
    public Boolean upLoadInputStream(InputStream ism, String bucketName, String fileName){
        try {
            existBucket(bucketName);
            pearMinioClient.putObject(PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(fileName)
                    .stream(ism,ism.available(),-1)
                    .build());
            return Boolean.TRUE;
        }catch (Exception e){
            log.error("文件流上传报错:"+e.getMessage());
            e.printStackTrace();
            return Boolean.FALSE;
        }
    }

    /**
     * 下载文件
     * @param fileName 文件名称
     * @param bucketName 桶名称
     * @return 文件流
     */
    public InputStream downLoadFile(String fileName,String bucketName){
        InputStream ism = null;
        try {
            ism = pearMinioClient.getObject(GetObjectArgs.builder()
                    .bucket(bucketName)
                    .object(fileName).build());
        } catch (Exception e) {
            log.error(String.format("下载文件(%s)报错:%s",fileName,e.getMessage()));
            e.printStackTrace();
        }
        return ism;
    }
}

MinioPearVo:创建分片返回回来的实体,包含了uploadId及分片数量、地址。

MinioBucketEnum:创建桶的枚举类,为后期方便扩展。

MinioPearVo实体:

java 复制代码
/**
 * @author: jiangjs
 * @description:
 * @date: 2023/10/30 9:36
 **/
@Data
public class MinioPearVo {
    /**
     * 上传Id
     */
    private String uploadId;
    /**
     * 获取分片上传URL
     */
    private List<PearUploadData> parts;

    @Data
    @Accessors(chain = true)
    public static class PearUploadData{
        /**
         * 分片编号,从1开始
         */
        private int parkNum;
        /**
         * 分片上传Url
         */
        private String uploadUrl;
    }

}

MinioBucketEnum桶枚举类:

java 复制代码
/**
 * @author: jiangjs
 * @description:
 * @date: 2023/10/20 10:51
 **/
public enum MinioBucketEnum {

    /**
     * email
     */
    EMAIL("email");

    private final String bucket;

    MinioBucketEnum(String bucket){
        this.bucket = bucket;
    }

    public MinioBucketEnum getMinioBucket(String bucket){
       return MinioBucketEnum.valueOf(bucket);
    }

    public String getBucket(){
        return bucket;
    }
}

4、重传校验

由于minio服务是不会校验当前文件是否已经上传过,因此即使是相同文件、相同名称的文件可以重复上传,为了避免资源的浪费,我们将上传的文件进行校验。

其思路:每次上传文件的时候都对文件内容进行md5加密,使用加密后的md5去数据库查询,查询当前文件在当前桶里面是否已经上传。

创建记录文件的表:

SQL 复制代码
create table opu_sys_files
(
  id          bigint auto_increment comment '主键Id'
  primary key,
  bucket_name varchar(100)                       not null comment '桶名称',
  file_name   varchar(200)                       not null comment '文件名称',
  suffix      varchar(8)                         not null comment '文件后缀名',
  md5_code    varchar(32)                        not null comment '加密md5编码',
  create_time datetime default CURRENT_TIMESTAMP null comment '创建时间'
)
    comment '系统文件表';

引入Mybatis-plus,实现根据md5、桶名称查询数据,其他的mapper,service小伙伴们自行实现。

java 复制代码
@Override
public Boolean isExistFile(String md5Code, String bucketName) {
    Long count = opuSysFilesMapper.selectCount(Wrappers.<OpuSysFiles>lambdaQuery()
            .eq(OpuSysFiles::getMd5Code, md5Code)
            .eq(OpuSysFiles::getBucketName, bucketName));
    return count > 0;
}

注:文件内容的md5加密,小伙伴们可以使用Spring中自带的DigestUtils进行加密

String md5Str = DigestUtils.md5DigestAsHex(file.getInputStream());

5、创建接口

万事具备,只欠东风了。上面封装的方法都有了,我们创建接口给前端调用即可。

分片上传controller代码:

java 复制代码
/**
 * @author: jiangjs
 * @description: 分片上传文件
 * @date: 2023/10/25 16:05
 **/
@RestController
@RequestMapping("/multipart")
public class UploadMultipartFileController {

    @Autowired
    private UploadMultipartFileService multipartFileService;

    @GetMapping("/create.do")
    public JsonResult<?> createMultipartUploadUrl(@RequestParam(value = "chunkNum",defaultValue = "0") Integer chunkNum,
                                                  @RequestParam("fileName") String fileName,
                                                  @RequestParam("contentType") String contentType){
       return multipartFileService.createMultipartUploadUrl(chunkNum,fileName,contentType);
    }
    @GetMapping("/complete.do")
    public JsonResult<?> completeMultipart(@RequestParam(value = "chunkNum",defaultValue = "0") Integer chunkNum,
                                           @RequestParam("fileName") String fileName,
                                           @RequestParam("contentType") String contentType,
                                           @RequestParam("uploadId") String uploadId,
                                           @RequestParam("fileMd5") String fileMd5){
       return multipartFileService.completeMultipart(chunkNum,fileName,contentType,uploadId,fileMd5);
    }
}

系统文件对应controller代码:

java 复制代码
/**
 * @author: jiangjs
 * @description:
 * @date: 2023/10/23 11:27
 **/
@RestController
@RequestMapping("/sysFile")
public class OpuSysFilesController {

    @Resource
    private OpuSysFilesService opuSysFilesService;


    @PostMapping("/uploadSingleFile.do")
    public JsonResult<String> uploadSingleFile(@RequestBody MultipartFile file){
        return opuSysFilesService.uploadSingleFile(file,MinioBucketEnum.EMAIL);
    }

    @GetMapping("/downFile.do/{md5Code}")
    public void downFile(@PathVariable("md5Code") String md5Code, HttpServletResponse response){
        opuSysFilesService.downFile(md5Code,MinioBucketEnum.EMAIL,response);
    }

    @GetMapping("/existFile.do/{md5Code}")
    public JsonResult<Boolean> judgeExistFile(@PathVariable("md5Code") String md5Code){
        Boolean existFile = opuSysFilesService.isExistFile(md5Code, MinioBucketEnum.EMAIL.getBucket());
        return JsonResult.success(existFile);

    }
}

分片上传service代码:

java 复制代码
/**
 * @author: jiangjs
 * @description:
 * @date: 2023/10/25 16:47
 **/
@Service
public class UploadMultipartFileServiceImpl implements UploadMultipartFileService {
    @Resource
    private MinioPearUploadUtil minioPearUploadUtil;
    @Resource
    private OpuSysFilesMapper opuSysFilesMapper;

    @Override
    public JsonResult<?> createMultipartUploadUrl(Integer chunkNum, String fileName, String contentType) {
        return JsonResult.success(minioPearUploadUtil.createMultipartUploadUrl(chunkNum,fileName,contentType,MinioBucketEnum.EMAIL));
    }

    @Override
    public JsonResult<?> completeMultipart(Integer chunkNum, String fileName, String contentType,String uploadId,String fileMd5) {
        if (minioPearUploadUtil.completeMultipart(chunkNum,fileName,contentType,uploadId,MinioBucketEnum.EMAIL)){
            OpuSysFiles sysFiles = new OpuSysFiles();
            String suffix = StringUtils.isNoneBlank(fileName) ? fileName.substring(fileName.indexOf(".")+1) : "" ;
            sysFiles.setFileName(fileName)
                    .setBucketName(MinioBucketEnum.EMAIL.getBucket())
                    .setMd5Code(fileMd5).setSuffix(suffix);
            opuSysFilesMapper.insert(sysFiles);
            return JsonResult.success("上传成功");
        }
        return JsonResult.fails("上传失败");
    }
}

系统文件对应service代码:

java 复制代码
/**
 * @author: jiangjs
 * @description:
 * @date: 2023/10/20 15:19
 **/
@Service
@AllArgsConstructor(onConstructor = @__(@Autowired))
@Slf4j
public class OpuSysFilesServiceImpl implements OpuSysFilesService {

    private final OpuSysFilesMapper opuSysFilesMapper;
    private final MinioPearUploadUtil minioPearUploadUtil;

    @Override
    public Boolean isExistFile(String md5Code, String bucketName) {
        Long count = opuSysFilesMapper.selectCount(Wrappers.<OpuSysFiles>lambdaQuery()
                .eq(OpuSysFiles::getMd5Code, md5Code)
                .eq(OpuSysFiles::getBucketName, bucketName));
        return count > 0;
    }

    @Override
    public JsonResult<String> uploadSingleFile(MultipartFile file, MinioBucketEnum bucketEnum) {
        try (InputStream ism = file.getInputStream()){
            String md5Str = DigestUtils.md5DigestAsHex(ism);
            Boolean existFile = this.isExistFile(md5Str, bucketEnum.getBucket());
            if (!existFile){
                String fileName = md5Str + "_" + file.getOriginalFilename();
                Boolean uploadFlag = minioPearUploadUtil.upLoadInputStream(ism, bucketEnum.getBucket(),fileName);
                if (uploadFlag){
                    OpuSysFiles sysFiles = new OpuSysFiles();
                    String suffix = StringUtils.isNoneBlank(fileName) ? fileName.substring(fileName.indexOf(".")+1) : "" ;
                    sysFiles.setFileName(fileName)
                            .setBucketName(bucketEnum.getBucket())
                            .setMd5Code(md5Str).setSuffix(suffix);
                    opuSysFilesMapper.insert(sysFiles);
                }
            }
            return JsonResult.success(md5Str);
        } catch (Exception e) {
            log.error("单个文件上传报错:"+e.getMessage());
            e.printStackTrace();
            return JsonResult.fails("上传文件失败");
        }
    }

    @Override
    public void downFile(String md5Code, MinioBucketEnum bucketEnum,HttpServletResponse response) {
        OpuSysFiles opuSysFiles = opuSysFilesMapper.selectOne(Wrappers.<OpuSysFiles>lambdaQuery()
                .eq(OpuSysFiles::getMd5Code, md5Code)
                .eq(OpuSysFiles::getBucketName, bucketEnum.getBucket()));
        String fileName = Objects.nonNull(opuSysFiles) ? opuSysFiles.getFileName() : "";
        try (InputStream ism = minioPearUploadUtil.downLoadFile(fileName, bucketEnum.getBucket())){
            String contentType = this.getFileContentType(opuSysFiles.getSuffix());
            response.setContentType(contentType);
            response.setHeader("Content-disposition", "inline; filename="+URLEncoder.encode(fileName, "UTF-8"));
            byte[] bytes = new byte[1024];
            OutputStream osm = response.getOutputStream();
            int count;
            while((count = ism.read(bytes)) !=-1){
                osm.write(bytes, 0, count);
            }
            osm.flush();
            osm.close();
        } catch (Exception e){
            log.error("下载文件报错:"+e.getMessage());
            e.printStackTrace();
        }
    }

    private String getFileContentType(String suffix){
        switch (suffix){
            case "jpg":
            case "jpeg":
                return "image/jpeg";
            case "png":
                return "image/png";
            case "gif":
                return "image/gif";
            case "xml":
                return "application/xml";
            case "pdf":
                return "application/pdf";
            case "xls":
                return "application/vnd.ms-excel";
            case "xlsx":
                return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
            case "doc":
                return "application/msword";
            case "docx":
                return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
            case "ppt":
                return "application/vnd.ms-powerpoint";
            case "pptx":
                return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
            default:
                return "";
        }
    }


}

至此后端的代码基本已经完成,接下来我们看看前端的代码。

2.1.2 前端实现

前端主要采用的是vuejs,elementui。至于创建vue项目,引入相关的依赖在这就不说了,相信小伙伴们也知道怎么弄。我们直接贴代码吧。

VU 复制代码
<template>
  <div>
    <el-form ref="form" label-width="80px">
      <el-upload
        class="upload-demo"
        v-loading="loading"
        drag
        action=""
        :auto-upload="false"
        :show-file-list="true"
        :on-change="changeFile"
        multiple>
        <i class="el-icon-upload"></i>
        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
        <div class="el-upload__tip" slot="tip">文件大小不超过50MB</div>
      </el-upload>
      <el-progress :text-inside="true" :stroke-width="20" :percentage="percent" status="success"></el-progress>
    </el-form>
  </div>
</template>

<script>
  import SparkMD5 from "spark-md5";

  export default {
    name: 'Home',
    data(){
      return {
        chunkSize: 5 * 1024 *1024,
        chunkUploadParam: {
          "data":null,
          "contentType":true,
          "processData":false
        },
        uploadId:"",
        loading: false,
        percent:0
      }
    },
    methods: {
      async changeFile(file){
        this.loading = true;
        debugger;
        if (!file) return;
        const fileSize = file.size;
        const isLt2M = fileSize / 1024 / 1024 < 50;
        if (!isLt2M) {
          this.$message.error('上传文件大小不能超过 50MB!');
          this.loading = false;
          return;
        }
        if (fileSize <= this.chunkSize){
          this.$postReq("/sysFile/uploadSingleFile.do",{"file":file.raw},"multipart/form-data").then(resp => {
            if (resp.data.code === 200){
              this.$message.success("文件已上传");
            } else {
              this.$message.error(resp.msg.data);
            }
          }).catch(error => {
            this.$message.error("文件失败");
          })
          this.loading = false;
        } else {
          //分片上传
          //1、计算分片数量 剩余
          const chunkNum = Math.floor(fileSize / this.chunkSize);
            //获取文件内容MD5
            const fileMd5 = await this.getFileMd5(file);
            let isExist = false;
            //判断当前文件在桶中是否已存在
            await this.$req.get("/sysFile/existFile.do/" + fileMd5).then(resp => {
            if (resp.code === 200 && resp.data){
            isExist = true;
            }
            });
            if (isExist){
            this.$message.success("文件已上传");
            this.loading = false;
            return;
            }
            //向后端请求获取本次分片上传初始化
            await this.$req.get("/multipart/create.do?chunkNum="+chunkNum+"&fileName="+file.name+"&contentType="+file.raw.type)
            .then(resp => {
            if(resp.code === 200){
            const parts = resp.data.parts;
            this.uploadId = resp.data.uploadId;
            let item;
            for (item of parts){
            //分片开始位置
            let startSize = (item.parkNum - 1) * this.chunkSize;
            //分片结束位置
            let endSize = item.parkNum === chunkNum ? fileSize : startSize + this.chunkSize;
            //获取当前分片的byte信息
            let chunkFile = file.raw instanceof File ? file.raw.slice(startSize,endSize) : null;
            this.uploadFilePear(item.uploadUrl,chunkFile,file.raw.type,item.parkNum);
            }
              } else {
                this.$message.error("文件调用后端进行分片失败");
              }

            });
        await this.completeChunkFile(chunkNum,file.name,file.raw.type,this.uploadId,fileMd5);
        this.loading = false;
      }
    },
    completeChunkFile(chunkNum,fileName,contentType,uploadId,fileMd5){
      this.$req.get("/multipart/complete.do?chunkNum="+chunkNum+"&fileName="+fileName+"&contentType="
          +contentType+"&uploadId="+uploadId+"&fileMd5="+fileMd5)
          .then(resp => {
            if (resp.code === 200){
              this.$message.success("文件上传成功");
              console.log("文件合并成功");
            }else {
              this.$message.error("文件上传失败");
            }
          });
    },
     uploadFilePear(uploadUrl,chunkFile,contentType,partNum){
      this.$putReq(uploadUrl,chunkFile,contentType).then(resp => {
        if (resp.data.status === 200){
          console.log("第" + partNum + "个分片上传完成");
        }
      }).catch(error => {
        console.log('分片:' + partNum + ' 上传失败,' + error)
      });
      },
      
    getFileMd5(file){
      const reader = new FileReader();
      reader.readAsBinaryString(file.raw);
      const sparkMD5 = new SparkMD5();
      return new Promise((resolve) => {
        reader.onload = (e) => {
          sparkMD5.append(e.target.result);
          resolve(sparkMD5.end());
        }
      })
    }
  }
}
</script>

前端实现跟其他分片上传一样,还是比较简单的,主要是:

1、设置分片的大小,然后根据大小进行分片;

2、得到分片的数量,再调用后端的创建分片连接的接口,获取对应分片编号与上传的url;

3、然后将文件按照分片进行拆分,上传到minio;

4、上传完成后进行合并即可。

具体步骤小伙伴们可参考代码。

注:在进行分片时,最后一个分片的大小不能小于设置分片大小,否则minio会报错

至此,SpringBoot基于minio进行分片上传的功能基本已经实现。

【总结】

minio的分片主要是依赖completeMultipartUpload,createMultipartUpload,listMultipart三个方法,只需要弄懂这三个方法,就可以实现minio的分片上传。

至于前端,则跟正常的分片一样,按照设置的分片大小进行分片,调用分片相关接口即可。


上述功能,已经经过测试,小伙伴们可以拿来即用,语言组织能力有限,说的比较泛泛,有说不到位的请见谅。

我是抢老婆酸奶的小肥仔,我们下次见。

如果对你有用的话记得点赞、收藏哦。

后端地址:gitee.com/lovequeena/...

前端地址:gitee.com/lovequeena/...

相关推荐
NiNg_1_2342 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
种树人202408192 小时前
如何在 Spring Boot 中启用定时任务
spring boot
Chrikk3 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*3 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue3 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man3 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
cs_dn_Jie3 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic4 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿4 小时前
webWorker基本用法
前端·javascript·vue.js
苹果醋35 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx