java文件上传Minio——文件、文件分片上传

本文主要介绍了文件上传——单\多文件分片上传;当然还有文件的md5值秒传、文件断点续传等内容,本文结合minio文件服务和redis做计数来实现的场景(下方地址是单机情况,即上传至本地demo)。有什么问题可以在留言哦!并在文章末尾附上demo源码下载!

 大文件分片上传博文地址:大文件分片上传

大文件(md5分片)上传博文地址:大文件(md5分片)上传

大文件断点续传博文地址:大文件断点续传 

一、创建前后端项目及演示

创建前后端项目,采用springboot项目结构,前端采用的vue框架搭建(前端完全是边用边百度,这里就不附前面项目结构,就一个页面)

后端控制台的输出日志

minio文件服务上面的结果

关于minio的配置与基本使用:(37条消息) minio的基本使用——java_java使用minio_寒夜憨憨的博客-CSDN博客

二、文件上传minio核心代码 

其中包括了单文件上传minio和多文件分片上传minio;至于文件分片上传采用的是:先保存分片文件——>然后合并分片文件——>再删除分片文件;其实minio有自带的文件分片上传策略。

文件上传至minio,如果单纯的保存文件的话,其实可以直接采用前端去对接minio,然后当上传成功之后,返回上传至minio文件的相关信息给后端进行数据存储处理即可;这样上传文件就不要经过后端服务器了,减少数据处理环节。

1、minio配置信息核心代码

在代码的注释都详细说明了每一步是做什么的,如果不懂的可以详细看看每一步的注释,当然也可以留言!

package com.jdh.fileUpload.config;

import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.Item;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import io.minio.MinioClient;

import javax.annotation.Resource;
import java.io.*;
import java.util.Objects;
import java.util.UUID;

/**
 * @ClassName: minioConfig
 * @Author: jdh
 * @CreateTime: 2022-03-12
 * @Description:
 */
@Slf4j
@Data
@Configuration
@ConditionalOnProperty(prefix = "autoconfigure", value = "isMinio", matchIfMissing = false)
public class MinioUtil {

    private MinioClient minioClient;

    @Resource
    private MinioProperties minioProperties;

//    @Autowired
//    private MyMinioClient myMinioClient;


    /**
     * 获取一个连接minio服务端的客户端
     *
     * @return MinioClient
     */
    @Bean
    public MinioClient getMinioClient() {

        try {
            String url = "http:" + minioProperties.getIp() + ":" + minioProperties.getPort();
            MinioClient minioClient = MinioClient.builder()
                    .endpoint(url)    //两种都可以,这种全路径的其实就是下面分开配置一样的
//                        .endpoint(minioProperties.getIp(),minioProperties.getPort(),minioProperties.getSecure())
                    .credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey())
                    .build();
            this.minioClient = minioClient;
            return minioClient;
        } catch (Exception e) {
            e.printStackTrace();
            log.info("创建minio客户端失败!", e);
            return null;
        }
    }

    /**
     * 创建桶
     *
     * @param bucketName 桶名称
     */
    public Boolean createBucket(String bucketName) throws Exception {
        if (!StringUtils.hasLength(bucketName)) {
            throw new RuntimeException("创建桶的时候,桶名不能为空!");
        }
        try {
            minioClient.makeBucket(MakeBucketArgs.builder()
                    .bucket(bucketName)
                    .build());
            return true;
        } catch (Exception e) {
            log.info("创建桶失败!", e);
            return false;
        }

    }

    /**
     * 检查桶是否存在
     *
     * @param bucketName 桶名称
     * @return boolean true-存在 false-不存在
     */
    public boolean checkBucketExist(String bucketName) throws Exception {
        if (!StringUtils.hasLength(bucketName)) {
            throw new RuntimeException("检测桶的时候,桶名不能为空!");
        }

        return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }

    /**
     * 检测某个桶内是否存在某个文件
     *
     * @param objectName 文件名称
     * @param bucketName 桶名称
     */
    public boolean getBucketFileExist(String objectName, String bucketName) throws Exception {
        if (!StringUtils.hasLength(objectName) || !StringUtils.hasLength(bucketName)) {
            throw new RuntimeException("检测文件的时候,文件名和桶名不能为空!");
        }

        try {
            // 判断文件是否存在
            return (minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()) &&
                    minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build()) != null);

        } catch (ErrorResponseException e) {
            log.info("文件不存在 ! Object does not exist");
            return false;
        } catch (Exception e) {
            throw new Exception(e);
        }
    }

    /**
     * 删除文件夹
     *
     * @param bucketName 桶名
     * @param objectName 文件夹名
     * @param isDeep     是否递归删除
     * @return
     */
    public Boolean deleteBucketFolder(String bucketName, String objectName, Boolean isDeep) {
        if (!StringUtils.hasLength(bucketName) || !StringUtils.hasLength(objectName)) {
            throw new RuntimeException("删除文件夹的时候,桶名或文件名不能为空!");
        }
        try {
            ListObjectsArgs args = ListObjectsArgs.builder().bucket(bucketName).prefix(objectName + "/").recursive(isDeep).build();
            Iterable<Result<Item>> listObjects = minioClient.listObjects(args);
            listObjects.forEach(objectResult -> {
                try {
                    Item item = objectResult.get();
                    System.out.println(item.objectName());
                    minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(item.objectName()).build());
                } catch (Exception e) {
                    log.info("删除文件夹中的文件异常", e);
                }
            });
            return true;
        } catch (Exception e) {
            log.info("删除文件夹失败");
            return false;
        }
    }

    /**
     * 文件上传文件
     *
     * @param file       文件
     * @param bucketName 桶名
     * @param objectName 文件名,如果有文件夹则格式为 "文件夹名/文件名"
     * @return
     */
    public Boolean uploadFile(MultipartFile file, String bucketName, String objectName) {

        if (Objects.isNull(file) || Objects.isNull(bucketName)) {
            throw new RuntimeException("文件或者桶名参数不全!");
        }

        try {
            //资源的媒体类型
            String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//默认未知二进制流
            InputStream inputStream = file.getInputStream();
            PutObjectArgs args = PutObjectArgs.builder()
                    .bucket(bucketName).object(objectName)
                    .stream(inputStream, file.getSize(), -1)
                    .contentType(file.getContentType())
                    .build();
            ObjectWriteResponse response = minioClient.putObject(args);
            inputStream.close();
            return response.etag() != null;
        } catch (Exception e) {
            log.info("单文件上传失败!", e);
            return false;
        }
    }

    /**
     * 分片合并
     *
     * @param bucketName
     * @param folderName
     * @param objectName
     * @param partNum
     * @return
     */
    public Boolean uploadFileComplete(String bucketName, String folderName, String objectName, Integer partNum) {

        try {
            //获取临时文件下的所有文件信息
            Iterable<Result<Item>> listObjects = minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).prefix(folderName + "/").build());

            //计算minio中分片个数
            Integer num = 0;
            for (Result<Item> result : listObjects) {
                num++;
            }
            //在依次校验实际分片数和预计分片数是否一致
            if (!num.equals(partNum)) {
                log.info("文件 {} 分片合并的时候,检测到实际分片数 {} 和预计分片数 {} 不一致", folderName, num, partNum);
                return false;
            }

            InputStream inputStream = null;
            log.info("开始合并文件 {} 分片合并,实际分片数 {} 和预计分片数 {}", folderName, num, partNum);
            for (int i = 0; i < num; i++) {
                String tempName = folderName + "/" + objectName.substring(0, objectName.lastIndexOf(".")) + "_" + i + ".temp";
                try {
                    //获取分片文件流
                    InputStream response = minioClient.getObject(
                            GetObjectArgs.builder().bucket(bucketName).object(tempName).build());
                    //流合并
                    if (inputStream == null) {
                        inputStream = response;
                    } else {
                        inputStream = new SequenceInputStream(inputStream, response);
                    }
                } catch (Exception e) {
                    log.info("读取分片文件失败!", e);
                }
            }
            if (inputStream == null) {
                log.info("合并流数据为空!");
                return false;
            }
            //转换为文件格式
            MockMultipartFile file = new MockMultipartFile(objectName, inputStream);

            //将合并的文件流写入到minio中
            PutObjectArgs args = PutObjectArgs.builder()
                    .bucket(bucketName).object(objectName)
                    .stream(file.getInputStream(), file.getSize(), -1)
//                    .contentType(file.getContentType())//这里可以不知道类型
                    .build();
            String etag = minioClient.putObject(args).etag();

            // 删除临时文件
            if (etag != null) {
                listObjects.forEach(objectResult -> {
                    try {
                        Item item = objectResult.get();
                        minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(item.objectName()).build());
                    } catch (Exception e) {
                        log.info("删除文件夹中的文件异常", e);
                    }
                });
                log.info("{}:临时文件夹文件已删除!", folderName);
            }

            inputStream.close();
            return etag != null;
        } catch (Exception e) {
            log.info("合并 {} - {} 文件失败!", folderName, objectName, e);
            return false;
        }
    }

    public static String generateUniqueId(int length) {
        UUID uuid = UUID.randomUUID();
        String hash = uuid.toString().replaceAll("-", "");
        return hash.substring(0, Math.min(length, hash.length()));
    }

}

2、文件上传minio的实现核心代码

package com.jdh.fileUpload.service.fileUpload;

import com.alibaba.fastjson2.JSONObject;
import com.jdh.fileUpload.config.MinioUtil;
import com.jdh.fileUpload.utils.Result;
import com.jdh.fileUpload.utils.Uuid;
import com.jdh.fileUpload.vo.PartFileIndexVo;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;

/**
 * @ClassName: FileUploadMinioServiceImpl
 * @Author: jdh
 * @CreateTime: 2022-03-15
 * @Description:
 */
@Slf4j
@Service
@ConditionalOnProperty(prefix = "autoconfigure", value = "isMinio", matchIfMissing = false)
public class FileUploadMinioServiceImpl implements FileUploadMinioService {

    @Autowired
    private MinioUtil minioUtil;

    @Resource
    private RedissonClient redissonClient;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public Result<Boolean> fileUploadMinio(MultipartFile file) {
        String objectName = Uuid.generateUniqueId(8) + "_" + file.getOriginalFilename();

        try {
            Boolean aBoolean = minioUtil.uploadFile(file, "minio-file", objectName);
            log.info("文件上传是否完成:{}", aBoolean);
            return Result.success(aBoolean, "文件上传完成!");
        } catch (Exception e) {
            log.info("文件上传异常!", e);
            return Result.error(false, "文件上传异常!");
        }
    }

    @Override
    public Result<Boolean> filePartUploadMinio(MultipartFile file, String partFile) {

//        String objectName = Uuid.generateUniqueId(8) + "_" + file.getOriginalFilename();
        try {
            PartFileIndexVo partFileIndexVo = JSONObject.parseObject(partFile, PartFileIndexVo.class);
            String objectName = "temp/" + partFileIndexVo.getFileUid() + "/"
                    + partFileIndexVo.getFileName().substring(0, partFileIndexVo.getFileName().lastIndexOf(".")) + "_"
                    + partFileIndexVo.getPartIndex() + ".temp";
            //上传文件分片
            Boolean partState = minioUtil.uploadFile(file, "minio-file", objectName);

            if (partState) {

                Long result = redisTemplate.opsForValue().increment("partCount:" + partFileIndexVo.getFileUid());

                if (result.equals(partFileIndexVo.getPartTotalNum().longValue())) {

                    log.info("{}:开始合并分片请求...", partFileIndexVo.getFileName());
                    //开始合并分片
                    Boolean complete = minioUtil.uploadFileComplete("minio-file", "temp/" + partFileIndexVo.getFileUid(),
                            partFileIndexVo.getFileName(), partFileIndexVo.getPartTotalNum());
                    //移除文件分片上传情况,这个也应该在redis中
                    Boolean delete = false;
                    if (complete) {
                        delete = redisTemplate.delete("partCount:" + partFileIndexVo.getFileUid());
                        log.info("{}:分片文件合并完成!", partFileIndexVo.getFileName());
                    }
                    return Result.success(complete && delete, partFileIndexVo.getFileUid());
                }
            }
            return Result.success(partState, partFileIndexVo.getPartIndex().toString());
        } catch (Exception e) {
            log.info("文件上传异常!", e);
            return Result.error("文件上传异常!");
        }
    }

}

3、关于相关前端核心代码

页面代码

 <!-- 单文件上传Minio -->
            <div class="singleFileUploadMinio">
                <el-upload  ref="upload" name="files" action="#"
                    :on-change="selectSingleFileMinio" :on-remove="removeSingleFileMinio" :file-list="singleFileMinio.fileList" :auto-upload="false">
                    <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
                    <el-button style="margin-left: 10px;" size="small" type="success" @click="singleFileUploadMinio">点击进行单文件分片上传Minio</el-button>
                    <div slot="tip" class="el-upload__tip">主要用于测试单文件上传Minio</div>
                </el-upload>
            </div>
             <!-- 多文件分片上传Minio -->
             <div class="multipleFilePartUploadMinio">
                <el-upload ref="upload" name="files" action="#"
                    :on-change="selectMultiplePartFileMinio" :on-remove="removeMultiplePartFileMinio" :file-list="multipleFilePartMinio.fileList" :auto-upload="false">
                    <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
                    <el-button style="margin-left: 10px;" size="small" type="success" @click="multipleFilePartUploadMinio">点击进行多文件分片上传Minio</el-button>
                    <div slot="tip" class="el-upload__tip">主要用于测试多文件分片上传Minio</div>
                </el-upload>
            </div>

数据定义组件代码

data() {
        return {
            urlPrefix: 'http://localhost:8082',
            headers: {
                'Content-Type': 'multipart/form-data'
            },
            singleFileMinio: {
                file: '',
                fileList: []
            },
            multipleFilePartMinio: {
                fileList: [],
                fileData: []
            },
        };
    },

各方法代码

  methods: {
    // 下面是单文件上传Minio的一些方法
    selectSingleFileMinio(file, fileList){
        this.singleFileMinio.file = file
        this.singleFileMinio.fileList = []
        this.singleFileMinio.fileList.push(file)
        console.log("单文件上传Minio->选中的文件:",this.singleFileMinio.file)
    },
    removeSingleFileMinio(file, fileList){
        this.singleFileMinio.file = ''
        console.log("单文件上传Minio->移除选中的文件:",this.singleFileMinio.file)
    },
    singleFileUploadMinio(){
        let url = this.urlPrefix + "/minio/singleFileUploadMinio";
        var fileParam = new FormData();
        fileParam.append("file",this.singleFileMinio.file.raw);

        this.$upFile(url,fileParam,null,resp => {
            let res = resp.data;
            if (res.data){
                this.$message.success(res.msg)
            }else{
                this.$message.error(res.msg)
            }
        })
    },
    // 下面多文件分片上传Minio的一些方法
    selectMultiplePartFileMinio(file, fileList){
        this.multipleFilePartMinio.fileList.push(file)
        console.log("多文件分片上传->选中的文件:",this.multipleFilePartMinio.fileList)
    },
    removeMultiplePartFileMinio(file, fileList){
        this.multipleFilePartMinio.fileList = this.multipleFilePartMinio.fileList.filter(item => item.uid != file.uid);
        console.log("多文件分片上传->移除选中的文件:",this.multipleFilePartMinio.fileList)
    },
    multipleFilePartUploadMinio(){
        let fileUploadCount = 0;
        let url = this.urlPrefix + "/minio/partFileUploadMinio";
        let partSize = 8; //MB
        let partFileUid = [];
        for(let i = 0; i < this.multipleFilePartMinio.fileList.length; i++ ){
            let partNum = Math.ceil(this.multipleFilePartMinio.fileList[i].size / 1024 / 1024 / partSize)
            let partFileIndex = {
                fileName: this.multipleFilePartMinio.fileList[i].name,
                fileUid: this.multipleFilePartMinio.fileList[i].uid,
                partTotalNum: partNum,
                partIndex: ''
            }
            partFileUid.push(this.multipleFilePartMinio.fileList[i].uid + '')
            for(let j = 0; j < partNum; j++ ){
                partFileIndex.partIndex = j
                var fileParam = new FormData();
                fileParam.append('file', this.multipleFilePartMinio.fileList[i].raw.slice(j * 1024 * 1024 * partSize, (j + 1) * 1024 * 1024 * partSize))
                fileParam.append('partFileIndex', JSON.stringify(partFileIndex))
                
                this.$upFile(url,fileParam,null,resp => {
                    let res = resp.data;
                    //判断最后一个分片文件传完之后,后台检验文件上传情况
                    if (res.code == 200){

                        if(!res.data){
                            var info = '当前分片上传Minio失败!uid:' + partFileUid + ';当前分片:' + j + ';返回分片:' + res.msg
                            console.log(info)
                        }

                        let isUid = partFileUid.includes(res.msg)
                        if(isUid){
                            fileUploadCount ++;
                            if(fileUploadCount == this.multipleFilePartMinio.fileList.length){
                                console.log('多文件分片上传Minio完成')
                                this.$message.success('多文件分片上传Minio完成')
                            }
                        }
                    }else{
                        var info = '多文件分片上传Minio失败!当前分片:' + j + ';当前uid:' + partFileUid
                        console.log(info)
                        this.$message.error(res.msg)
                    }
                })
            }
        }
    }
  },

三、源码下载和结尾

源码默认只有上传本地的各种demo方案,如果需要打开上传至minio的话,需要再配置文件修改配置文件,如下:

gitee后端链接:java_fileUpload_demo: 关于文件上传的一些基本介绍(如文件上传、分片上传、md5值秒传、断点续传等)

gitee前端链接:https://gitee.com/java_utils_demo/vue2_demo.git

其实思路很简单也很明确,就是一个大文件分成n个小文件进行小文件依次单独上传,后端检测到所有分片上传完成之后,即对这些分片进行合并,并删除对应临时分片文件。

再次说明下,如果需要上传至minio,那么需要先配置好minio和redis并启动这两个服务,然后再启动demo项目。

  • 9
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 22
    评论
Minio是一个开源的分布式对象存储系统,它允许用户在存储服务上存储和检索数据。它支持S3 API,因此可以与大多数S3兼容的客户端和工具一起使用。 下面是使用Java实现Minio分片上传和下载文件的示例代码: ## 分片上传 ```java import io.minio.MinioClient; import io.minio.errors.MinioException; import io.minio.messages.Part; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; public class MinioUploader { private MinioClient minioClient; private final String bucketName; public MinioUploader(MinioClient minioClient, String bucketName) { this.minioClient = minioClient; this.bucketName = bucketName; } public void upload(String objectName, Path filePath) throws NoSuchAlgorithmException, IOException, MinioException { long fileSize = Files.size(filePath); long partSize = 5 * 1024 * 1024; // 5MB int partCount = (int) Math.ceil((double) fileSize / partSize); List<Part> parts = new ArrayList<>(); for (int i = 0; i < partCount; i++) { int partNumber = i + 1; long offset = i * partSize; long size = Math.min(partSize, fileSize - offset); InputStream inputStream = Files.newInputStream(filePath); inputStream.skip(offset); String uploadId = minioClient.initiateMultipartUpload(bucketName, objectName); Part part = minioClient.uploadPart(bucketName, objectName, uploadId, partNumber, inputStream, size); parts.add(part); } minioClient.completeMultipartUpload(bucketName, objectName, parts); } } ``` ## 分片下载 ```java import io.minio.MinioClient; import io.minio.errors.MinioException; import java.io.BufferedOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; import java.security.NoSuchAlgorithmException; import java.util.List; public class MinioDownloader { private MinioClient minioClient; private final String bucketName; public MinioDownloader(MinioClient minioClient, String bucketName) { this.minioClient = minioClient; this.bucketName = bucketName; } public void download(String objectName, Path filePath) throws NoSuchAlgorithmException, IOException, MinioException { long partSize = 5 * 1024 * 1024; // 5MB List<io.minio.messages.Part> parts = minioClient.listObjectParts(bucketName, objectName, null).getParts(); try (BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(filePath.toFile()))) { for (io.minio.messages.Part part : parts) { InputStream inputStream = minioClient.getObject(bucketName, objectName, part.partNumber(), 0L, partSize); byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } inputStream.close(); } } } } ``` 这是一个基本的示例,实际使用时需要根据具体需求进行修改。
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值