0


拿来即用: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下载地址:https://www.minio.org.cn/download.shtml

选择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文件,文件内容如下:

@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。分别执行如下脚本:

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来进行代理。配置如下:

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中的三个方法。

/**
 * @author: jiangjs
 * @description: 分片上传MinioClient继承MinioClient,主要暴露:
 *       createMultipartUpload:创建分片请求,返回uploadId
 *       listMultipart:查询分片信息
 *       completeMultipartUpload:根据uploadId合并已上传的分片
 * @date: 2023/10/24 14:08
 **/publicclassPearMinioClientextendsMinioClient{protectedPearMinioClient(MinioClient client){super(client);}@OverridepublicObjectWriteResponsecompleteMultipartUpload(String bucketName,String region,String objectName,String uploadId,Part[] parts,Multimap<String,String> extraHeaders,Multimap<String,String> extraQueryParams)throwsNoSuchAlgorithmException,InsufficientDataException,IOException,InvalidKeyException,ServerException,XmlParserException,ErrorResponseException,InternalException,InvalidResponseException{returnsuper.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);}@OverridepublicCreateMultipartUploadResponsecreateMultipartUpload(String bucketName,String region,String objectName,Multimap<String,String> headers,Multimap<String,String> extraQueryParams)throwsNoSuchAlgorithmException,InsufficientDataException,IOException,InvalidKeyException,ServerException,XmlParserException,ErrorResponseException,InternalException,InvalidResponseException{returnsuper.createMultipartUpload(bucketName, region, objectName, headers, extraQueryParams);}publicListPartsResponselistMultipart(String bucketName,String region,String objectName,Integer maxParts,Integer partNumberMarker,String uploadId,Multimap<String,String> extraHeaders,Multimap<String,String> extraQueryParams)throwsServerException,InsufficientDataException,ErrorResponseException,NoSuchAlgorithmException,IOException,InvalidKeyException,XmlParserException,InvalidResponseException,InternalException{returnsuper.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);}}
2、设置minio配置

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

/**
 * @author: jiangjs
 * @description:
 * @date: 2023/10/20 10:27
 **/@Component@Data@ConfigurationProperties(prefix ="minio")publicclassMinioEndPointInfo{/**
     * minio节点地址
     */privateString endpoint;/**
     * 登录用户名
     */privateString accessKey;/**
     * 密码
     */privateString secretKey;}

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

创建minio配置类

/**
 * @author: jiangjs
 * @description: minio相关配置
 * @date: 2023/10/20 10:46
 **/@Configuration@EnableConfigurationProperties(MinioEndPointInfo.class)publicclassMinioConfig{@ResourceprivateMinioEndPointInfo minioEndPointInfo;@BeanpublicPearMinioClientcreatePearMinioClient(){MinioClient minioClient =MinioClient.builder().endpoint(minioEndPointInfo.getEndpoint()).credentials(minioEndPointInfo.getAccessKey(), minioEndPointInfo.getSecretKey()).build();returnnewPearMinioClient(minioClient);}}

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

3、创建minio分片工具类
/**
 * @author: jiangjs
 * @description: minio分片上传工具
 * @date: 2023/10/30 9:34
 **/@Component@Slf4jpublicclassMinioPearUploadUtil{@ResourceprivatePearMinioClient pearMinioClient;/**
     * 校验当前bucket是否存在,不存在则创建
     * @param bucketName 桶
     */privatevoidexistBucket(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 返回分片信息
     */@SneakyThrowspublicMinioPearVocreateMultipartUploadUrl(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 =newHashMap<>(2);
        reqParams.put("uploadId",uploadResponse.result().uploadId());MinioPearVo pearVo =newMinioPearVo();
        pearVo.setUploadId(uploadResponse.result().uploadId());List<MinioPearVo.PearUploadData> uploads =newArrayList<>();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 =newMinioPearVo.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 合并结果
     */@SneakyThrowspublicBooleancompleteMultipart(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 =newPart[chunkNum+10];int partNum =0;for(Part part : listMultipart.result().partList()){
                parts[partNum]=newPart(partNum,part.etag());
                partNum++;}
            pearMinioClient.completeMultipartUpload(MinioBucketEnum.EMAIL.getBucket(),null, fileName,
                    uploadId, parts, headerMap,null);returnBoolean.TRUE;}returnBoolean.FALSE;}/**
     * 文件流上传
     * @param ism 文件流
     * @param bucketName 桶名称
     * @param fileName 文件名称
     * @return 执行结果
     */publicBooleanupLoadInputStream(InputStream ism,String bucketName,String fileName){try{existBucket(bucketName);
            pearMinioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(fileName).stream(ism,ism.available(),-1).build());returnBoolean.TRUE;}catch(Exception e){
            log.error("文件流上传报错:"+e.getMessage());
            e.printStackTrace();returnBoolean.FALSE;}}/**
     * 下载文件
     * @param fileName 文件名称
     * @param bucketName 桶名称
     * @return 文件流
     */publicInputStreamdownLoadFile(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实体:

/**
 * @author: jiangjs
 * @description:
 * @date: 2023/10/30 9:36
 **/@DatapublicclassMinioPearVo{/**
     * 上传Id
     */privateString uploadId;/**
     * 获取分片上传URL
     */privateList<PearUploadData> parts;@Data@Accessors(chain =true)publicstaticclassPearUploadData{/**
         * 分片编号,从1开始
         */privateint parkNum;/**
         * 分片上传Url
         */privateString uploadUrl;}}

MinioBucketEnum桶枚举类:

/**
 * @author: jiangjs
 * @description:
 * @date: 2023/10/20 10:51
 **/publicenumMinioBucketEnum{/**
     * email
     */EMAIL("email");privatefinalString bucket;MinioBucketEnum(String bucket){this.bucket = bucket;}publicMinioBucketEnumgetMinioBucket(String bucket){returnMinioBucketEnum.valueOf(bucket);}publicStringgetBucket(){return bucket;}}
4、重传校验

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

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

创建记录文件的表:

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小伙伴们自行实现。

@OverridepublicBooleanisExistFile(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代码:

/**
 * @author: jiangjs
 * @description: 分片上传文件
 * @date: 2023/10/25 16:05
 **/@RestController@RequestMapping("/multipart")publicclassUploadMultipartFileController{@AutowiredprivateUploadMultipartFileService multipartFileService;@GetMapping("/create.do")publicJsonResult<?>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")publicJsonResult<?>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代码:

/**
 * @author: jiangjs
 * @description:
 * @date: 2023/10/23 11:27
 **/@RestController@RequestMapping("/sysFile")publicclassOpuSysFilesController{@ResourceprivateOpuSysFilesService opuSysFilesService;@PostMapping("/uploadSingleFile.do")publicJsonResult<String>uploadSingleFile(@RequestBodyMultipartFile file){return opuSysFilesService.uploadSingleFile(file,MinioBucketEnum.EMAIL);}@GetMapping("/downFile.do/{md5Code}")publicvoiddownFile(@PathVariable("md5Code")String md5Code,HttpServletResponse response){
        opuSysFilesService.downFile(md5Code,MinioBucketEnum.EMAIL,response);}@GetMapping("/existFile.do/{md5Code}")publicJsonResult<Boolean>judgeExistFile(@PathVariable("md5Code")String md5Code){Boolean existFile = opuSysFilesService.isExistFile(md5Code,MinioBucketEnum.EMAIL.getBucket());returnJsonResult.success(existFile);}}

分片上传service代码:

/**
 * @author: jiangjs
 * @description:
 * @date: 2023/10/25 16:47
 **/@ServicepublicclassUploadMultipartFileServiceImplimplementsUploadMultipartFileService{@ResourceprivateMinioPearUploadUtil minioPearUploadUtil;@ResourceprivateOpuSysFilesMapper opuSysFilesMapper;@OverridepublicJsonResult<?>createMultipartUploadUrl(Integer chunkNum,String fileName,String contentType){returnJsonResult.success(minioPearUploadUtil.createMultipartUploadUrl(chunkNum,fileName,contentType,MinioBucketEnum.EMAIL));}@OverridepublicJsonResult<?>completeMultipart(Integer chunkNum,String fileName,String contentType,String uploadId,String fileMd5){if(minioPearUploadUtil.completeMultipart(chunkNum,fileName,contentType,uploadId,MinioBucketEnum.EMAIL)){OpuSysFiles sysFiles =newOpuSysFiles();String suffix =StringUtils.isNoneBlank(fileName)? fileName.substring(fileName.indexOf(".")+1):"";
            sysFiles.setFileName(fileName).setBucketName(MinioBucketEnum.EMAIL.getBucket()).setMd5Code(fileMd5).setSuffix(suffix);
            opuSysFilesMapper.insert(sysFiles);returnJsonResult.success("上传成功");}returnJsonResult.fails("上传失败");}}

系统文件对应service代码:

/**
 * @author: jiangjs
 * @description:
 * @date: 2023/10/20 15:19
 **/@Service@AllArgsConstructor(onConstructor =@__(@Autowired))@Slf4jpublicclassOpuSysFilesServiceImplimplementsOpuSysFilesService{privatefinalOpuSysFilesMapper opuSysFilesMapper;privatefinalMinioPearUploadUtil minioPearUploadUtil;@OverridepublicBooleanisExistFile(String md5Code,String bucketName){Long count = opuSysFilesMapper.selectCount(Wrappers.<OpuSysFiles>lambdaQuery().eq(OpuSysFiles::getMd5Code, md5Code).eq(OpuSysFiles::getBucketName, bucketName));return count >0;}@OverridepublicJsonResult<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 =newOpuSysFiles();String suffix =StringUtils.isNoneBlank(fileName)? fileName.substring(fileName.indexOf(".")+1):"";
                    sysFiles.setFileName(fileName).setBucketName(bucketEnum.getBucket()).setMd5Code(md5Str).setSuffix(suffix);
                    opuSysFilesMapper.insert(sysFiles);}}returnJsonResult.success(md5Str);}catch(Exception e){
            log.error("单个文件上传报错:"+e.getMessage());
            e.printStackTrace();returnJsonResult.fails("上传文件失败");}}@OverridepublicvoiddownFile(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 =newbyte[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();}}privateStringgetFileContentType(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项目,引入相关的依赖在这就不说了,相信小伙伴们也知道怎么弄。我们直接贴代码吧。

<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的分片上传。

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


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

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

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

后端地址:https://gitee.com/lovequeena/minio.git

前端地址:https://gitee.com/lovequeena/minio_pear.git


本文转载自: https://blog.csdn.net/jiashn123/article/details/137083123
版权归原作者 抢老婆酸奶的小肥仔 所有, 如有侵权,请联系我们删除。

“拿来即用:SpringBoot+Minio+vue-uploader实现分片上传”的评论:

还没有评论