springboot集成minio实现大文件分片上传

之前记录过一篇minio单机安装及Springboot集成minio记录,但是minioClient里带的流传输的上传文件方法虽然是application/octet-stream的contentType却不支持大文件上传,实测一个3.5G的文件只能传上去2G造成文件上传不全,还不会报错。
minio提供了MinioAsyncClient来实现异步大文件上传,主要用到这几个方法:createMultipartUploadAsync:创建异步分片上传请求
uploadPartAsync:执行异步上传分片
listPartsAsync:查询分片数据
completeMultipartUploadAsync:完成异步分片上传合并分片文件

实现的思路也基本是上面的顺序createMultipartUploadAsync->uploadPartAsync->listPartsAsync->completeMultipartUploadAsync

原来的minioClient集成代码这里省略,只记录MinioAsyncClient使用步骤:

创建自定义分片多文件上传工具类 MultipartMinioClient

package com.ly.mp.project.minio;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

import com.google.common.collect.Multimap;

import io.minio.CreateMultipartUploadResponse;
import io.minio.ListPartsResponse;
import io.minio.MinioAsyncClient;
import io.minio.ObjectWriteResponse;
import io.minio.UploadPartResponse;
import io.minio.errors.ErrorResponseException;
import io.minio.errors.InsufficientDataException;
import io.minio.errors.InternalException;
import io.minio.errors.InvalidResponseException;
import io.minio.errors.ServerException;
import io.minio.errors.XmlParserException;
import io.minio.messages.Part;
import ly.mp.project.common.minio.MultipartUploadCreateParam;
import ly.mp.project.common.minio.UploadPartCreateParam;

/**
 * 自定义minio
 * @date 2023-09-15
 */
public class MultipartMinioClient extends MinioAsyncClient {

    public MultipartMinioClient(MinioAsyncClient client) {
        super(client);
    }
    
    /**
     * 创建分片上传请求
     *
     * @param bucketName       存储桶
     * @param region           区域
     * @param objectName       对象名
     * @param headers          消息头
     * @param extraQueryParams 额外查询参数
     */
    public String multipartUpload(String bucket, String region, String object, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) 
            throws IOException, InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException,
            XmlParserException, InvalidResponseException, ErrorResponseException, InterruptedException, ExecutionException {
        CompletableFuture<CreateMultipartUploadResponse> response = this.createMultipartUploadAsync(bucket, region, object, headers, extraQueryParams);

        return response.get().result().uploadId();
    }
    
    public String multipartUpload(MultipartUploadCreateParam param) 
            throws InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException, IOException, InterruptedException, ExecutionException {
        return this.multipartUpload(param.getBucketName(), param.getRegion(), param.getObjectName(), param.getHeaders(), param.getExtraQueryParams());
    }

    /**
     * 分片上传
     * @param bucketName Name of the bucket.
     * @param region Region of the bucket (Optional).
     * @param objectName Object name in the bucket.
     * @param data Object data must be InputStream, RandomAccessFile, byte[] or String.
     * @param length Length of object data.
     * @param uploadId Upload ID.
     * @param partNumber Part number.
     * @param extraHeaders Extra headers for request (Optional).
     * @param extraQueryParams Extra query parameters for request (Optional).
     * @throws IOException 
     * @throws XmlParserException 
     * @throws NoSuchAlgorithmException 
     * @throws InternalException 
     * @throws InsufficientDataException 
     * @throws InvalidKeyException 
     */
    public CompletableFuture<UploadPartResponse> uploadPartAsync(String bucketName, String region, String objectName, Object data, 
            long length, String uploadId, int partNumber, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) 
                    throws InvalidKeyException, InsufficientDataException, InternalException, NoSuchAlgorithmException, XmlParserException, IOException {
        return super.uploadPartAsync(bucketName, region, objectName, data, length, uploadId, partNumber, extraHeaders, extraQueryParams);
    }
    
    public CompletableFuture<UploadPartResponse> uploadPartAsync(UploadPartCreateParam param) 
            throws InvalidKeyException, InsufficientDataException, InternalException, NoSuchAlgorithmException, XmlParserException, IOException{
        return this.uploadPartAsync(param.getBucketName(), param.getRegion(), param.getObjectName(), param.getData(), param.getLength(), 
                param.getUploadId(), param.getPartNumber(),param.getHeaders(),
                param.getExtraQueryParams());
    }

    /**
     * 完成分片上传,执行合并文件
     *
     * @param bucketName       存储桶
     * @param region           区域
     * @param objectName       对象名
     * @param uploadId         上传ID
     * @param parts            分片
     * @param extraHeaders     额外消息头
     * @param extraQueryParams 额外查询参数
     */
    @Override
    public CompletableFuture<ObjectWriteResponse> completeMultipartUploadAsync(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) 
            throws InsufficientDataException, InternalException, InvalidKeyException, IOException, NoSuchAlgorithmException, XmlParserException {
        return super.completeMultipartUploadAsync(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);
    }
    
    public CompletableFuture<ObjectWriteResponse> completeMultipartUploadAsync(MultipartUploadCreateParam param) 
            throws InvalidKeyException, InsufficientDataException, InternalException, NoSuchAlgorithmException, XmlParserException, IOException{
        return this.completeMultipartUploadAsync(param.getBucketName(), param.getRegion(), param.getObjectName(), param.getUploadId(), param.getParts(),
                param.getHeaders(),param.getExtraQueryParams());
    }

    /**
     * 查询分片数据
     *
     * @param bucketName       存储桶
     * @param region           区域
     * @param objectName       对象名
     * @param uploadId         上传ID
     * @param extraHeaders     额外消息头
     * @param extraQueryParams 额外查询参数
     */
    @Override
    public CompletableFuture<ListPartsResponse> listPartsAsync(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws InsufficientDataException, InternalException, InvalidKeyException, IOException, NoSuchAlgorithmException, XmlParserException {
        return super.listPartsAsync(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
    }
    
    public CompletableFuture<ListPartsResponse> listPartsAsync(MultipartUploadCreateParam param) 
            throws InvalidKeyException, InsufficientDataException, InternalException, NoSuchAlgorithmException, XmlParserException, IOException{
        return this.listPartsAsync(param.getBucketName(), param.getRegion(), param.getObjectName(), param.getMaxParts(), param.getPartNumberMarker(),
                param.getUploadId(), param.getHeaders(), param.getExtraQueryParams());
    }
}

补充参数类MultipartUploadCreateParam

package ly.mp.project.common.minio;

import com.google.common.collect.Multimap;

import io.minio.messages.Part;

public class MultipartUploadCreateParam {
    private String bucketName;

    private String region;

    private String objectName;

    private Multimap<String, String> headers;

    private Multimap<String, String> extraQueryParams;

    private String uploadId;

    private Integer maxParts;

    private Part[] parts;

    private Integer partNumberMarker;

    public String getBucketName() {
        return bucketName;
    }

    public void setBucketName(String bucketName) {
        this.bucketName = bucketName;
    }

    public String getRegion() {
        return region;
    }

    public void setRegion(String region) {
        this.region = region;
    }

    public String getObjectName() {
        return objectName;
    }

    public void setObjectName(String objectName) {
        this.objectName = objectName;
    }

    public Multimap<String, String> getHeaders() {
        return headers;
    }

    public void setHeaders(Multimap<String, String> headers) {
        this.headers = headers;
    }

    public Multimap<String, String> getExtraQueryParams() {
        return extraQueryParams;
    }

    public void setExtraQueryParams(Multimap<String, String> extraQueryParams) {
        this.extraQueryParams = extraQueryParams;
    }

    public String getUploadId() {
        return uploadId;
    }

    public void setUploadId(String uploadId) {
        this.uploadId = uploadId;
    }

    public Integer getMaxParts() {
        return maxParts;
    }

    public void setMaxParts(Integer maxParts) {
        this.maxParts = maxParts;
    }

    public Part[] getParts() {
        return parts;
    }

    public void setParts(Part[] parts) {
        this.parts = parts;
    }

    public Integer getPartNumberMarker() {
        return partNumberMarker;
    }

    public void setPartNumberMarker(Integer partNumberMarker) {
        this.partNumberMarker = partNumberMarker;
    }
}

补充参数类UploadPartCreateParam:

package ly.mp.project.common.minio;

import java.io.InputStream;

import com.google.common.collect.Multimap;

public class UploadPartCreateParam {
    private String bucketName;

    private String region;

    private String objectName;

    private InputStream data;

    private long length;

    private String uploadId;

    private int partNumber;

    private Multimap<String, String> headers;

    private Multimap<String, String> extraQueryParams;

    public String getBucketName() {
        return bucketName;
    }

    public void setBucketName(String bucketName) {
        this.bucketName = bucketName;
    }

    public String getRegion() {
        return region;
    }

    public void setRegion(String region) {
        this.region = region;
    }

    public String getObjectName() {
        return objectName;
    }

    public void setObjectName(String objectName) {
        this.objectName = objectName;
    }

    public InputStream getData() {
        return data;
    }

    public void setData(InputStream data) {
        this.data = data;
    }

    public long getLength() {
        return length;
    }

    public void setLength(long length) {
        this.length = length;
    }

    public String getUploadId() {
        return uploadId;
    }

    public void setUploadId(String uploadId) {
        this.uploadId = uploadId;
    }

    public int getPartNumber() {
        return partNumber;
    }

    public void setPartNumber(int partNumber) {
        this.partNumber = partNumber;
    }

    public Multimap<String, String> getHeaders() {
        return headers;
    }

    public void setHeaders(Multimap<String, String> headers) {
        this.headers = headers;
    }

    public Multimap<String, String> getExtraQueryParams() {
        return extraQueryParams;
    }

    public void setExtraQueryParams(Multimap<String, String> extraQueryParams) {
        this.extraQueryParams = extraQueryParams;
    }
}

创建MultipartMinioConfiguration将MultipartMinioClient注入到Spring

package com.ly.mp.project.minio;

import java.lang.reflect.Field;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import io.minio.MinioAsyncClient;
import io.minio.MinioClient;

@Configuration
public class MultipartMinioConfiguration {

    @Bean
    @ConditionalOnMissingBean({ MultipartMinioClient.class })
    public MultipartMinioClient multipartMinioClient(MinioClient minioClient) throws Throwable {
        try {
            Field field = minioClient.getClass().getDeclaredField("asyncClient");
            field.setAccessible(true);
            return new MultipartMinioClient((MinioAsyncClient) field.get(minioClient));
        } catch (Throwable ex) {
            throw ex;
        }
    }
}

在MinioHandler里增加大文件上传方法:

public MinioFile write(InputStream in, String fileName, String minioCustomDir, String contentType, boolean shard) {
        MinioFile minioFile = new MinioFile();
        //如果文件大于1G 则切分分片上传
        if(shard) {
            try {
                this.bigFileUpload(in, fileName, minioCustomDir, minioFile, contentType);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        } else {
            ObjectWriteResponse res = null;
            try {
                res = minioTemplate.upload(Paths.get(minioCustomDir + fileName), in, contentType);
            } catch (MyMinioException e) {
                throw new RuntimeException("minio上传出错:{}", e);
            }
            
            minioFile.setObjectName(res.object());
            minioFile.setFileUrl(MinioPathUtils.getBaseUrl() + res.object());
            minioFile.setInnerUrl(MinioPathUtils.getInnerBaseUrl() + res.object());
        }
        
        return minioFile;
    }
    
    private void bigFileUpload(InputStream in, String fileName, String minioCustomDir, MinioFile minioFile, String contentType) throws FileNotFoundException {
        //文件切分,50M一个文件
        String localPath = CommonUtils.getLocalRandomPath();
        CommonUtils.createDirs(localPath);
        FileUtil.cutFile(in, fileName, localPath, 50*1024*1024);
        if(null != in) {
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        
        /**构建分片请求,拿到uploadId**/
        MultipartUploadCreateParam param = new MultipartUploadCreateParam();
        param.setBucketName(minioProperties.getPublicBucket());
        param.setObjectName(MinioOperation.getPathToLinuxStr(Paths.get(minioCustomDir + fileName)));
        Multimap<String, String> headers = HashMultimap.create();
        headers.put("Content-Type", contentType);
        param.setHeaders(headers);
        //分片数量自己计算
//      param.setExtraQueryParams(null);
//      param.setMaxParts(null);
//      param.setParts(null);
//      param.setPartNumberMarker(null);
        String uploadId = "";
        try {
            uploadId = multipartMinioClient.multipartUpload(param);
            param.setUploadId(uploadId);
        } catch (Exception e) {
            e.printStackTrace();
            CommonUtils.deleteFileAll(localPath);
            throw new RuntimeException("multipartUpload接口报错:" + e.getMessage());
        }
        
        
        File targetDirFile = new File(localPath);
        int nameNum = 1;
        if(targetDirFile.exists() && targetDirFile.isDirectory()) {
            String nameNumDirPath = localPath + nameNum + "/";
            String targetFilePath =  nameNumDirPath + fileName;
            File nameNumDir = new File(nameNumDirPath);
            while(null != nameNumDir && nameNumDir.exists()) {
                
                /**分片上传**/
                UploadPartCreateParam createParam = new UploadPartCreateParam();
                createParam.setBucketName(param.getBucketName());
                createParam.setObjectName(param.getObjectName());
                createParam.setData(new FileInputStream(targetFilePath));
                createParam.setLength(new File(targetFilePath).length());
                createParam.setUploadId(param.getUploadId());
                createParam.setPartNumber(nameNum);
                createParam.setHeaders(param.getHeaders());
                createParam.setExtraQueryParams(param.getExtraQueryParams());
                
                try {
                    multipartMinioClient.uploadPartAsync(createParam).get();
                } catch (Exception e) {
                    e.printStackTrace();
                    CommonUtils.deleteFileAll(localPath);
                    throw new RuntimeException("uploadPartAsync接口报错:" + e.getMessage());
                }
                
                nameNum++;
                nameNumDirPath = localPath + nameNum + "/";
                targetFilePath =  nameNumDirPath + fileName;
                nameNumDir = new File(nameNumDirPath);
            }
        }
        
        /**分片合并**/
        param.setMaxParts(nameNum + 10);
        param.setPartNumberMarker(0);
        param.setParts(listParts(param));
        try {
            ObjectWriteResponse res = multipartMinioClient.completeMultipartUploadAsync(param).get();
            minioFile.setObjectName(res.object());
            minioFile.setFileUrl(MinioPathUtils.getBaseUrl() + res.object());
            minioFile.setInnerUrl(MinioPathUtils.getInnerBaseUrl() + res.object());
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("completeMultipartUploadAsync接口报错:" + e.getMessage());
        }finally {
            //删不干净 后面加定时任务删除 tmp文件夹下超一天的文件
            CommonUtils.deleteFileAll(localPath);
        }
        
    }
    
    public Part[] listParts(MultipartUploadCreateParam param) {
        ListPartsResponse listMultipart;
        try {
            listMultipart = multipartMinioClient.listPartsAsync(param).get();
//          LogUtils.info("listMultipart:{}", JsonUtils.writeValue(listMultipart.result().partList()));
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("listPartsAsync接口报错:" + e.getMessage());
        }
        
        return listMultipart.result().partList().toArray(new Part[]{});
    }

主要实现代码在上面的bigFileUpload里,将本地的大文件先切分成小文件,再一个一个按minio的步骤创建分片上传,执行分片上传,合并分片返回最终链接,超过1g的才走大文件上传,低于1g的还是走原来的upload方法,大文件上传成功后,不用担心大文件下载问题,原来的minioClient读取文件流的方法可以下载完整的大文件,这个已经实证过。如果有需要http下载大文件的需求,可参考之前记录的一篇:java大文件断点续传下载分段下载nginx206问题
下面附上完整的MinioHandler以及FileUtil:

package com.ly.mp.project.minio;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.lianyou.minioutils.MinioTemplate;
import com.lianyou.minioutils.config.MinioProperties;
import com.lianyou.minioutils.exception.MyMinioException;
import com.lianyou.minioutils.util.MinioOperation;
import com.ly.mp.project.common.CommonUtils;
import com.ly.mp.project.utils.FileUtil;

import io.minio.ListPartsResponse;
import io.minio.ObjectWriteResponse;
import io.minio.StatObjectResponse;
import io.minio.messages.Item;
import io.minio.messages.Part;
import ly.mp.project.common.minio.MinioFile;
import ly.mp.project.common.minio.MinioPathUtils;
import ly.mp.project.common.minio.MultipartUploadCreateParam;
import ly.mp.project.common.minio.UploadPartCreateParam;
import ly.mp.project.common.otautils.DateUtil;

@Service
public class MinioHandler {
    @Autowired
    MinioTemplate minioTemplate;
    
    @Autowired
    MultipartMinioClient multipartMinioClient;
    
    @Autowired
    MinioProperties minioProperties;
    
    public MinioFile write(InputStream in, String fileName, String minioCustomDir, String contentType, boolean shard) {
        MinioFile minioFile = new MinioFile();
        //如果文件大于1G 则切分分片上传
        if(shard) {
            try {
                this.bigFileUpload(in, fileName, minioCustomDir, minioFile, contentType);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        } else {
            ObjectWriteResponse res = null;
            try {
                res = minioTemplate.upload(Paths.get(minioCustomDir + fileName), in, contentType);
            } catch (MyMinioException e) {
                throw new RuntimeException("minio上传出错:{}", e);
            }
            
            minioFile.setObjectName(res.object());
            minioFile.setFileUrl(MinioPathUtils.getBaseUrl() + res.object());
            minioFile.setInnerUrl(MinioPathUtils.getInnerBaseUrl() + res.object());
        }
        
        return minioFile;
    }
    
    private void bigFileUpload(InputStream in, String fileName, String minioCustomDir, MinioFile minioFile, String contentType) throws FileNotFoundException {
        //文件切分,50M一个文件
        String localPath = CommonUtils.getLocalRandomPath();
        CommonUtils.createDirs(localPath);
        FileUtil.cutFile(in, fileName, localPath, 50*1024*1024);
        if(null != in) {
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        
        /**构建分片请求,拿到uploadId**/
        MultipartUploadCreateParam param = new MultipartUploadCreateParam();
        param.setBucketName(minioProperties.getPublicBucket());
        param.setObjectName(MinioOperation.getPathToLinuxStr(Paths.get(minioCustomDir + fileName)));
        Multimap<String, String> headers = HashMultimap.create();
        headers.put("Content-Type", contentType);
        param.setHeaders(headers);
        //分片数量自己计算
//      param.setExtraQueryParams(null);
//      param.setMaxParts(null);
//      param.setParts(null);
//      param.setPartNumberMarker(null);
        String uploadId = "";
        try {
            uploadId = multipartMinioClient.multipartUpload(param);
            param.setUploadId(uploadId);
        } catch (Exception e) {
            e.printStackTrace();
            CommonUtils.deleteFileAll(localPath);
            throw new RuntimeException("multipartUpload接口报错:" + e.getMessage());
        }
        
        
        File targetDirFile = new File(localPath);
        int nameNum = 1;
        if(targetDirFile.exists() && targetDirFile.isDirectory()) {
            String nameNumDirPath = localPath + nameNum + "/";
            String targetFilePath =  nameNumDirPath + fileName;
            File nameNumDir = new File(nameNumDirPath);
            while(null != nameNumDir && nameNumDir.exists()) {
                
                /**分片上传**/
                UploadPartCreateParam createParam = new UploadPartCreateParam();
                createParam.setBucketName(param.getBucketName());
                createParam.setObjectName(param.getObjectName());
                createParam.setData(new FileInputStream(targetFilePath));
                createParam.setLength(new File(targetFilePath).length());
                createParam.setUploadId(param.getUploadId());
                createParam.setPartNumber(nameNum);
                createParam.setHeaders(param.getHeaders());
                createParam.setExtraQueryParams(param.getExtraQueryParams());
                
                try {
                    multipartMinioClient.uploadPartAsync(createParam).get();
                } catch (Exception e) {
                    e.printStackTrace();
                    CommonUtils.deleteFileAll(localPath);
                    throw new RuntimeException("uploadPartAsync接口报错:" + e.getMessage());
                }
                
                nameNum++;
                nameNumDirPath = localPath + nameNum + "/";
                targetFilePath =  nameNumDirPath + fileName;
                nameNumDir = new File(nameNumDirPath);
            }
        }
        
        /**分片合并**/
        param.setMaxParts(nameNum + 10);
        param.setPartNumberMarker(0);
        param.setParts(listParts(param));
        try {
            ObjectWriteResponse res = multipartMinioClient.completeMultipartUploadAsync(param).get();
            minioFile.setObjectName(res.object());
            minioFile.setFileUrl(MinioPathUtils.getBaseUrl() + res.object());
            minioFile.setInnerUrl(MinioPathUtils.getInnerBaseUrl() + res.object());
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("completeMultipartUploadAsync接口报错:" + e.getMessage());
        }finally {
            //删不干净 后面加定时任务删除 tmp文件夹下超一天的文件
            CommonUtils.deleteFileAll(localPath);
        }
        
    }
    
    public Part[] listParts(MultipartUploadCreateParam param) {
        ListPartsResponse listMultipart;
        try {
            listMultipart = multipartMinioClient.listPartsAsync(param).get();
//          LogUtils.info("listMultipart:{}", JsonUtils.writeValue(listMultipart.result().partList()));
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("listPartsAsync接口报错:" + e.getMessage());
        }
        
        return listMultipart.result().partList().toArray(new Part[]{});
    }

    public MinioFile write(String localFilePath, String fileName, String minioCustomDir) {
        
        return write(localFilePath, fileName, minioCustomDir, "application/octet-stream");
    }
    
    public MinioFile write(String localFilePath, String fileName, String minioCustomDir, String contentType) {
        File file = new File(localFilePath);
        boolean shard = false;
        if(file.length() > 1*1024*1024*1024) {
            shard = true;
        }
        
        try (InputStream in = new FileInputStream(localFilePath)){
            return write(in, fileName, minioCustomDir, contentType, shard);
        } catch (FileNotFoundException e) {
            throw new RuntimeException("minio文件不存在:" + localFilePath);
        } catch (IOException e) {
            throw new RuntimeException("minio文件关闭失败");
        }
    }
    
    public MinioFile write(InputStream in, String fileName, boolean shard) {
        return write(in, fileName, MinioPathUtils.getMinioDefaultDir() + fileName, "application/octet-stream", shard);
    }
    
    /**
     * 
     * @param file 本地文件路径
     * @param fileName
     * @return
     */
    public MinioFile write(String filePath, String fileName) {
        File file = new File(filePath);
        boolean shard = false;
        if(file.length() > 1*1024*1024*1024) {
            shard = true;
        }
        try (InputStream in = new FileInputStream(file)){
            return write(in, fileName, shard);
        } catch (FileNotFoundException e) {
            throw new RuntimeException("minio文件不存在:" + file);
        } catch (IOException e) {
            throw new RuntimeException("minio文件关闭失败");
        }
    }
    
    public void read(String objectName, File outFile) {
        InputStream ins = null;
        try {
            ins = minioTemplate.getObject(Paths.get(objectName));
        } catch (MyMinioException e) {
            throw new RuntimeException("minio文件读取失败");
        }
        try {
            FileUtils.copyInputStreamToFile(ins, outFile);
        } catch (IOException e) {
            throw new RuntimeException("minio文件读取失败1");
        } finally {
            if(null != ins) {
                try {
                    ins.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    /**
     * 
     * @param fileUrl
     * @param outFilePath
     * @return
     */
    public void read(String fileUrl, String outFilePath) {
        read(fileUrl.replace(MinioPathUtils.getBaseUrl(), ""), new File(outFilePath));
    }
    
    
    public void delete(String objectName) {
        try {
            minioTemplate.remove(Paths.get(objectName));
        } catch (MyMinioException e) {
            throw new RuntimeException("minio文件删除失败:{}", e);
        }
    }
    
    public void delete(String fileUrl, boolean url) {
        if(url) {
            fileUrl = fileUrl.replace(MinioPathUtils.getBaseUrl(), ""); 
        }
        delete(fileUrl);
    }
    
    public boolean exists(String objectName) {
        boolean exist = true;
        try {
            minioTemplate.getMetadata(Paths.get(objectName));
        } catch (Exception e) {
            exist = false;
        }
        return exist;
    }
    
    public boolean exists(String objectName, boolean url) {
        if(url) {
            objectName = objectName.replace(MinioPathUtils.getBaseUrl(), "");
        }
        return exists(objectName);
    }
    
    public MinioFile getFileInfo(String objectName) {
        StatObjectResponse res = null;
        try {
            res = minioTemplate.getMetadata(Paths.get(objectName));
            
        } catch (MyMinioException e) {
            throw new RuntimeException("minio文件读取文件详情失败:{}", e);
        }
        MinioFile minioFile = new MinioFile();
        minioFile.setObjectName(res.object());
        minioFile.setSize(res.size());
        minioFile.setFileUrl(MinioPathUtils.getBaseUrl() + objectName);
        minioFile.setInnerUrl(MinioPathUtils.getInnerBaseUrl() + objectName);
        minioFile.setLastModifiedTimeStamp(DateUtil.datatimeToTimestamp(res.lastModified().toLocalDateTime()));
        minioFile.setLastModifiedStr(DateUtil.dateToStr(res.lastModified().toLocalDateTime()));
        return minioFile;
    }
    
    public MinioFile getFileInfo(String objectName, boolean url) {
        if(url) {
            objectName = objectName.replace(MinioPathUtils.getBaseUrl(), "");
        }
        return getFileInfo(objectName);
    }
    
    public List<MinioFile> getFileInfosByPath(String dir){
        List<MinioFile> resultList = new ArrayList<>();
        
        List<Item> list = minioTemplate.listFullPathObjects(Paths.get(dir));
        
        if(!CollectionUtils.isEmpty(list)) {
            for(Item item : list) {
                MinioFile minioFile = new MinioFile();
                minioFile.setObjectName(item.objectName());
                minioFile.setSize(item.size());
                minioFile.setFileUrl(MinioPathUtils.getBaseUrl() + item.objectName());
                minioFile.setInnerUrl(MinioPathUtils.getInnerBaseUrl() + item.objectName());
                minioFile.setLastModifiedTimeStamp(DateUtil.datatimeToTimestamp(item.lastModified().toLocalDateTime()));
                minioFile.setLastModifiedStr(DateUtil.dateToStr(item.lastModified().toLocalDateTime()));
                resultList.add(minioFile);
            }
        }
        return resultList;
    }
    
    public List<MinioFile> getFileInfosByPath(String dir, boolean url){
        if(url) {
            dir = dir.replace(MinioPathUtils.getBaseUrl(), "");
        }
        return getFileInfosByPath(dir);
    }
    
    public static void main(String[] args) {
        String localPath = "D:\\work\\workspace4\\zna-l2-fota\\ly.mp.zna.ota.platform.service\\tmp\\267ec8204c3241d2b3ce7a369a032690\\";
        CommonUtils.deleteFileAll(localPath);
    }
}
package com.ly.mp.project.utils;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;

import ly.mp.project.common.otautils.DownloadClient;

public class FileUtil {
    /**
     * txt格式转String
     * 
     * @param txtPath
     * @return
     * @throws IOException
     */
    public static String txtToStr(String txtPath) throws IOException {
        StringBuilder buffer = new StringBuilder();
        BufferedReader bf = null;
        try {
            bf = new BufferedReader(new InputStreamReader(new FileInputStream(txtPath), "UTF-8"));
            String str = null;
            while ((str = bf.readLine()) != null) {// 使用readLine方法,一次读一行
                buffer.append(new String(str.getBytes(), "UTF-8"));
            }
        } finally {
            if(null != bf) bf.close();
        }
        String xml = buffer.toString();

        return xml;
    }
    
    public static void downloadNet(String urlPath, String filePath) throws Exception {
        DownloadClient.beginDowanload(0, urlPath, filePath);
        
    }
    
    public static void downloadBytes(byte[] bytes, String filePath) throws Exception {
        File file = new File(filePath);
        if(!file.exists()) {
            file.createNewFile();
        }
        FileOutputStream fs = null;
        try {
            fs = new FileOutputStream(filePath);
            fs.write(bytes);
        } finally {
            fs.close();
        }
    }

    public static void appendFile(byte[] bytes, String fileName) throws IOException {
        // 打开一个随机访问文件流,按读写方式
        RandomAccessFile randomFile = new RandomAccessFile(fileName, "rw");
        // 文件长度,字节数
        long fileLength = randomFile.length();
        // 将写文件指针移到文件尾。
        randomFile.seek(fileLength);
        randomFile.write(bytes);
        randomFile.close();
    }

    public static String getTmpDir() {
        String tmpDir = "/tmp/";
        String os = System.getProperty("os.name");
        if (os.toLowerCase().contains("windows")) {
            tmpDir = "D://tmp/";
            File file = new File(tmpDir);
            if (!file.exists())
                file.mkdir();
        }
        return tmpDir;
    }

    public static void mkdir(String path) {
        File file = new File(path);
        if (!file.exists()) {
            file.mkdir();
        } else if (!file.isDirectory()) {
            file.mkdir();
        }
    }

    public static void deleteDir(String dirPath) {
        File file = new File(dirPath);
        File[] fileList = file.listFiles();
        for (File f : fileList) {
            if (f.exists()) {
                if (f.isDirectory()) {
                    deleteDir(f.getAbsolutePath());
                } else {
                    f.delete();
                }
            }
        }
        if (file.exists())
            file.delete();
    }
    
    /**
     * 
     * @param in 源file 流
     * @param fileName 源文件名称
     * @param endDir 目标文件目录
     * @param num 分割大小(字节)
     */
    public static void cutFile(InputStream in, String fileName, String endDir, int byteNum) {
        try {
            // 创建规定大小的byte数组
            byte[] b = new byte[byteNum];
            int len = 0;
            // name为以后的小文件命名做准备
            int nameNum = 1;
            // 遍历将大文件读入byte数组中,当byte数组读满后写入对应的小文件中
            while ((len = in.read(b)) != -1) {
                File nameDir = new File(endDir + nameNum + "/");
                if(!nameDir.exists()) {
                    nameDir.mkdirs();
                }
                // 分别找到原大文件的文件名和文件类型,为下面的小文件命名做准备
                FileOutputStream fos = new FileOutputStream(endDir + nameNum + "/" + fileName);
                // 将byte数组写入对应的小文件中
                fos.write(b, 0, len);
                // 结束资源
                fos.close();
                nameNum++;
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (in != null) {
                    // 结束资源
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

===20240610补充MinioFile和CommonUtils两个类===

MinioFile:

package ly.mp.project.common.minio;

public class MinioFile {
    private String fileUrl;
    
    private String objectName;
    
    private long size;
    
    private long lastModifiedTimeStamp;//13位时间戳
    
    private String lastModifiedStr;//yyyy-MM-dd HH:mm:ss
    
    private String innerUrl;

    public String getFileUrl() {
        return fileUrl;
    }

    public void setFileUrl(String fileUrl) {
        this.fileUrl = fileUrl;
    }

    public String getObjectName() {
        return objectName;
    }

    public void setObjectName(String objectName) {
        this.objectName = objectName;
    }

    public long getSize() {
        return size;
    }

    public void setSize(long size) {
        this.size = size;
    }

    public long getLastModifiedTimeStamp() {
        return lastModifiedTimeStamp;
    }

    public void setLastModifiedTimeStamp(long lastModifiedTimeStamp) {
        this.lastModifiedTimeStamp = lastModifiedTimeStamp;
    }

    public String getLastModifiedStr() {
        return lastModifiedStr;
    }

    public void setLastModifiedStr(String lastModifiedStr) {
        this.lastModifiedStr = lastModifiedStr;
    }

    public String getInnerUrl() {
        return innerUrl;
    }

    public void setInnerUrl(String innerUrl) {
        this.innerUrl = innerUrl;
    }
}

CommonUtils:

package com.ly.mp.project.common;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.UUID;

import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;

import com.alibaba.fastjson.JSON;
import com.ly.mp.component.helper.StringHelper;
import com.ly.mp.springcloudnacos.nacos.NacosConfigs;

import ly.mp.project.common.otautils.DateUtil;

/**
 * @description:
 * @author: ly-yanzj
 * @date: 2021/3/3 11:21
 */
public class CommonUtils {
    public static final String LOCAL_TEMP_PATH = System.getProperty("user.dir") + "/tmp/";
    /**
     * 对象转化成json字符串
     */
    public static String toJsonStr(Object object) {
        try {
            return JSON.toJSONString(object);
        } catch (Exception e) {
            return object.toString();
        }
    }

    /**
     * 获取当前项目下的
     *
     * @return
     */
    public static String getLocalRandomPath() {
        String uid = UUID.randomUUID().toString().replaceAll("-", "");
        return pathSeparateTransfer(LOCAL_TEMP_PATH + uid + "/");
    }

    /**
     * 替换文件间隔路径符
     *
     * @param filePath
     * @return
     */
    public static String pathSeparateTransfer(String filePath) {
        if (File.separator.equals("\\")) {
            filePath = filePath.replaceAll("/", "\\\\");
        } else {
            filePath = filePath.replaceAll("\\\\", "/");
        }
        return filePath;
    }

    /**
     * 删除文件下所有文件夹和文件
     * file:文件对象
     */
    public static void deleteFileAll(File file) {
        if (file.exists()) {
            File files[] = file.listFiles();
            for (File file1: files) {
                if (file1.isDirectory()) {
                    deleteFileAll(file1);
                } else {
                    file1.delete();
                }
            }
            file.delete();
        }
    }

    /**
     * 删除文件下所有文件夹和文件
     * path:文件名
     */
    public static void deleteFileAll(String path) {
        File file = new File(path);
        deleteFileAll(file);
    }

    /**
     * 创建多层级文件夹
     *
     * @param path
     * @return
     */
    public static boolean createDirs(String path) {
        File fileDir = new File(path);
        if (!fileDir.exists()) {
            return fileDir.mkdirs();
        }
        return true;
    }

    /**
     * 字节合并
     * @param byte_1
     * @param byte_2
     * @return
     */
    public static byte[] byteMerger(byte[] byte_1, byte[] byte_2){
        byte[] byte_3 = new byte[byte_1.length+byte_2.length];
        System.arraycopy(byte_1, 0, byte_3, 0, byte_1.length);
        System.arraycopy(byte_2, 0, byte_3, byte_1.length, byte_2.length);
        return byte_3;
    }

    /**
     * 16进制的字符串转成字节
     * @param hexStr 样例-3031300d060960864801650304020105000420
     * @return
     * @throws Exception
     */
    public static byte[] hexStringToBytes(String hexStr) throws Exception {
        byte[] baKeyword = new byte[hexStr.length() / 2];
        for (int i = 0; i < baKeyword.length; i++) {
            try {
                baKeyword[i] = (byte) (0xff & Integer.parseInt(hexStr.substring(i * 2, i * 2 + 2), 16));
            } catch (Exception e) {
                throw new Exception("前置固定加密十六进制字符串处理异常!");
            }
        }
        return baKeyword;
    }

    /**
     * 整数转字节
     *
     * @param i
     * @return
     */
    public static byte[] intToByte4(int i) {
        byte[] targets = new byte[4];
        targets[3] = (byte) (i & 0xFF);
        targets[2] = (byte) (i >> 8 & 0xFF);
        targets[1] = (byte) (i >> 16 & 0xFF);
        targets[0] = (byte) (i >> 24 & 0xFF);
        return targets;
    }

    public static byte[] unsignedShortToByte2(int s) {
        byte[] targets = new byte[2];
        targets[0] = (byte) (s >> 8 & 0xFF);
        targets[1] = (byte) (s & 0xFF);
        return targets;
    }

    /**
     * 字节打印成二进制字符
     *
     * @param toByteArray
     * @return
     */
    public static String showBytes(byte[] toByteArray) {
        StringBuilder sb = new StringBuilder();
        for (byte tByte : toByteArray) {
            sb.append(Integer.toBinaryString((tByte & 0xFF) + 0x100).substring(1));
        }
        return sb.toString();
    }

    public static String getCurrentDay() {
        SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd");
        return format.format(new Date());
    }

    public static String checkSuperUserDomain(String paramDomainId, String headDomainId) {
        String domainId = StringUtils.isNotBlank(paramDomainId) ? paramDomainId : headDomainId;
        if (StringUtils.isBlank(domainId)) {
            return null;
        }
        return domainId.equals(NacosConfigs.getProp("super.user.domain.id")) ? null : domainId;
    }

    /**
     * 读取文本文件转为字符
     *
     * @param filePath      文件全路径
     * @param charset       编码符
     * @param lineSeparator 是否保留原换行符
     * @return
     * @throws IOException
     */
    public static String readTextFile(String filePath, String charset, boolean lineSeparator) throws IOException {
        charset = StringUtils.isBlank(charset) ? "UTF-8" : charset;
        StringBuilder sb;
        try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filePath), charset))) {
            sb = new StringBuilder();
            String line;
            while ((line = br.readLine()) != null) {
                sb.append(line);
                if (lineSeparator) {
                    sb.append(System.lineSeparator());
                }
            }
            // 去除末尾换行符
            int sbLen = sb.length();
            if (lineSeparator && sbLen > 0) {
                sb.delete(sbLen - System.lineSeparator().length(), sbLen);
            }
        } catch (IOException e) {
            throw e;
        }
        return sb.toString();
    }

    /**
     * 将字符串输出到文件
     *
     * @param filePath 输出的文件夹路径
     * @param fileName 输出的文件名称
     * @param content  输出的内容
     * @param charset  字符编码
     */
    public static void writeTextFile(String filePath, String fileName, String content, String charset) throws IOException {
        charset = StringUtils.isBlank(charset) ? "UTF-8" : charset;
        createDirs(filePath);
        try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(filePath + fileName), charset))) {
            bw.write(content);
        } catch (IOException e) {
            throw e;
        }
    }

    /**
     * 将字符串输出到文件
     * @param filePathName
     * @param content
     * @param charset
     * @throws IOException
     */
    public static void writeTextFile(String filePathName, String content, String charset) throws IOException {
        charset = StringUtils.isBlank(charset) ? "UTF-8" : charset;
        try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(filePathName), charset))) {
            bw.write(content);
        } catch (IOException e) {
            throw e;
        }
    }

    public static void main(String[] args) {
        //hexStringToBytes
    }

    public static String getStringByList(String split, List<String> list) {
        String result = "";
        StringBuilder builder = new StringBuilder();
        if(!CollectionUtils.isEmpty(list)) {
            for(String str : list) {
                builder.append(str + split);
            }
            if(StringUtils.isNotBlank(builder.toString())) {
                result = builder.toString().substring(0, builder.toString().length() - 1);
            }
        }
        return result;
    }

    public static String getRemoteTmpDir() {
        return NacosConfigs.getProp("tk.minio.path") +"/tmp/"+ DateUtil.getSystemDate("yyyyMM") + "/" + StringHelper.GetGUID() + "/";
    }

    /**
     * 判断是否是字母或数字字符,是返回true,否则返回false
     * @param reference
     * @return
     */
    public static boolean isLetterOrDigit(String reference) {
        for(int i = 0; i < reference.length(); i++) {
             if(!Character.isLetterOrDigit(reference.charAt(i))) return false;
        }
        return true;
    }

    public static String formatFileSize(Long fileLength) {
        String fileSizeString = "";
        if (fileLength == null) {
            return fileSizeString;
        }
        DecimalFormat df = new DecimalFormat("#.00");
        if (fileLength < 1024) {
            fileSizeString = df.format((double) fileLength) + "B";
        }
        else if (fileLength < 1048576) {
            fileSizeString = df.format((double) fileLength / 1024) + "K";
        }
        else if (fileLength < 1073741824) {
            fileSizeString = df.format((double) fileLength / 1048576) + "M";
        }
        else {
            fileSizeString = df.format((double) fileLength / 1073741824) + "G";
        }
        return fileSizeString;
    }

    public static String getRemoteTmpDir(String dir) {
        return dir + DateUtil.getSystemDate("yyyyMM") + "/" + StringHelper.GetGUID() + "/";
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容