SpringBoot+MinIO实现上传下载等功能自定义MinioUtil

概述

MinIO为对象存储服务,用来存储大容量非结构化的数据,用于分布式文件存储。与云存储不同,云储存是将数据存储在云服务器上,用户通过互联网即可获取;分布式文件存储是将文件保存在集群的磁盘上,使用网络的方式通过地址来获取。

创建服务

MinIO官网
下载服务,推荐使用Docker或Linux,此次只做简单使用,采用Windows即可,MINIO CLIENT为客户端,暂且不使用。
下载服务
下载完成后,存放在bin目录下,在bin同级创建data文件夹,用来存储数据信息。MinIO服务下载
注意:直接双击.exe文件并不能启动,在bin目录下使用cmd打开,每次启动服务需注册账号和密码

set MINIO_ROOT_USER=admin
set MINIO_ROOT_PASSWORD=12345678

然后通过:minio.exe server D:\Environment_Construction\MinIO\data
注意后面路径为你的data文件路径,后面可设置端口号,不指定时默认为9000
完整为:minio.exe server D:\Environment_Construction\MinIO\data --console-address "127.0.0.1:9000" --address "127.0.0.1:9005"
启动MinIO服务
可以看到有两个端口号,上方为服务器地址,下方Web端管理后台地址,其中127.0.0.1为本机,无网络也可使用,其他为网络分配IP地址,都可使用。
在浏览器中输入该网址即可查看,然后输入刚才设置的账号和密码即可登录。注:服务断开后该账号失效。
MinIO登录
登陆后,初次没有桶和数据,需添加一个默认的桶。桶:类似于文件夹,但级别更高,他是用来存放数据的。
MinIO创建桶
MinIO创建桶
桶不能重复,创建后即可在本地磁盘查看到该信息,所以本质上依然是硬盘介质。
MinIO创建桶
在后台可上传文件,同样在磁盘中可查看到该信息,只不过已经经过序列化处理。
上传文件
上传文件
创建服务和Web端的了解先到此。

整合SpringBoot

在创建之前,需先给MinIO服务创建密钥,以便于访问,点击后会自动生成密钥,可自定义修改。
创建密钥
在下载MinIO服务界面下方可看到MINIO SDK中的Java可获取依赖信息。
在springboot项目中添加依赖

<!--minio-->
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.2.2</version>
</dependency>
<!--zip打包-->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.5.7</version>
</dependency>
<!--lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

配置application.yml信息,其中bucketName不能为空

server:
  port: 8080
spring:
  # 配置文件上传大小限制
  servlet:
    multipart:
      max-file-size: 200MB
      max-request-size: 200MB
#MinIO配置
minio:
  endpoint: http://127.0.0.1:9000
  accessKey: wanglang
  secretKey: wanglang
  bucketName: create-default

在config文件夹下创建MinioConfig.java进行配置服务:

import io.minio.MinioClient;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {

    /**
     * 访问api Url
     */
    @Value("${minio.endpoint}")
    private String endpoint;

    /**
     * 访问密钥
     */
    @Value("${minio.accessKey}")
    private String accessKey;

    /**
     * 密钥
     */
    @Value("${minio.secretKey}")
    private String secretKey;


    /**
     * 捅名称
     */
    @Value("${minio.bucketName}")
    private String bucketName;

    /**
     * 创建Minio客户端
     * @return MinioClient
     */
    @Bean
    public MinioClient minioClient() {

        return MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .build();
    }
}

在utils包下面编写统一返回格式类R.java

import lombok.Data;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

/**
 * 通用返回结果类,服务器响应的数据将被封装成此对象
 * @param <T>
 */

@Data
@Accessors(chain = true)
public class R<T> implements Serializable {

    private Integer code; //编码:1成功,0和其它数字为失败

    private String msg = "成功"; //错误信息

    private T data; //数据

    private Map map = new HashMap(); //动态数据

    public static <T> R<T> success(T object) {
        R<T> r = new R<T>();
        r.data = object;
        r.code = 1;
        return r;
    }

    public static <T> R<T> error(String msg) {
        R r = new R();
        r.msg = msg;
        r.code = 0;
        return r;
    }

    public R<T> add(String key, Object value) {
        this.map.put(key, value);
        return this;
    }
}

在entity包下面创建ObjectItem.java实体类,用做文件信息遍历接收:

import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
public class ObjectItem {

    private String objectName;

    private String size;

    @Override
    public String toString() {
        return "ObjectItem{" +
                "名称='" + objectName + '\'' +
                ", 大小='" + size + '\'' +
                '}';
    }
}

现在在utils包下面编写自定义的MinioUtil.java工具类,MinIO提供了MinioClient.java,是对该文件的二次封装,其中未使用数据库存储文件相关信息,下载和删除主要使用前端传递的全路径识别,可根据实际情况修改,其他API暂未使用,可根据需要添加:

import cn.hutool.core.io.FastByteArrayOutputStream;
import com.wanglang.minioexercise.entity.ObjectItem;
import io.minio.*;
import io.minio.messages.Item;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import sun.misc.BASE64Encoder;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Component
public class MinioUtil {

    @Autowired
    private MinioClient minioClient;

    /**
     * 默认桶名
     */
    @Value("${minio.bucketName}")
    private String bucketName;

    /**
     * 判断bucket是否存在
     * @param bucketName
     */
    @SneakyThrows
    public boolean existBucket(String bucketName) {
        return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }

    /**
     * 创建存储bucket
     * @param bucketName
     * @return boolean
     */
    public boolean makeBucket(String bucketName) {
        try {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     *  删除存储bucket
     * @param bucketName
     * @return boolean
     */
    public boolean removeBucket(String bucketName) {
        try {
            minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 创建文件路径
     * @param fileName 原始文件名称
     * @return FilePath
     */
    public String createFilePath(String fileName) {
        return new SimpleDateFormat("yyyy/MM/dd").format(new Date()) + "/" + fileName;
    }

    /**
     * 根据文件路径获取文件名称
     * @param filePath 文件路径
     * @return 文件名
     */
    public String getFileNameByPath(String filePath) {
        String[] split = StringUtils.split(filePath, "/");
        return split[split.length - 1];
    }

    /**
     * 查看桶内文件信息
     * @param bucketName 可为空
     * @return List
     */
    @SneakyThrows
    public List<ObjectItem> queryFileListInBucket(String bucketName) {
        // 桶选择
        String thisBucketName = bucketName == null || bucketName.equals("") ? this.bucketName : bucketName;
        List<ObjectItem> objectItemList = new ArrayList<>();
        Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder().bucket(thisBucketName).build());
        for (Result<Item> result : results) {
            Item item = result.get();
            // 不为目录时添加
            if (!item.isDir()) objectItemList.add(new ObjectItem().setObjectName(item.objectName()).setSize(String.valueOf(item.size())));
        }
        return objectItemList;
    }

    /**
     * 判断桶内是否包含该文件
     * @param fileName
     * @param bucketName 可为空
     * @return boolean
     */
    public boolean isContainInBucketByFile(String fileName, String bucketName) {
        List<ObjectItem> objectItemList = queryFileListInBucket(bucketName);
        for (ObjectItem item : objectItemList) if (item.getObjectName().equals(fileName)) return true;
        return false;
    }

    /**
     * 单文件上传
     * @param file
     * @param bucketName 可为空
     * @return String
     */
    @SneakyThrows
    public String upload(MultipartFile file, String bucketName) {
        // 桶名
        String thisBucketName = bucketName == null || bucketName.equals("") ? this.bucketName : bucketName;
        // 文件名
        String fileName = file.getOriginalFilename();
        // 文件流
        InputStream inputStream = file.getInputStream();
        // 文件大小
        long size = file.getSize();
        // 文件路径
        String filePath = createFilePath(fileName);
        System.out.println(fileName + "的文件路径为:" + filePath);
        // 存储到Minio
        minioClient.putObject(PutObjectArgs.builder()
                .bucket(thisBucketName)
                .object(filePath)
                .stream(inputStream, size, -1)
                .contentType(file.getContentType())
                .build());
        return filePath;
    }

    /**
     * 批量文件上传
     * @param file 数组
     * @param bucketName 可为空
     * @return Map key-文件名 value-文件路径
     */
    @SneakyThrows
    public Map<String, String> upload(MultipartFile[] file, String bucketName) {
        Map<String, String> filePaths = new HashMap<>();
        String thisBucketName = bucketName == null || bucketName.equals("") ? this.bucketName : bucketName;
        for (MultipartFile item : file) {
            String fileName = item.getOriginalFilename();
            InputStream inputStream = item.getInputStream();
            long size = item.getSize();
            String filePath = createFilePath(fileName);
            filePaths.put(fileName, filePath);
            System.out.println(fileName + "的文件路径为:" + filePath);
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(thisBucketName)
                    .object(filePath)
                    .stream(inputStream, size, -1)
                    .contentType(item.getContentType())
                    .build());
        }
        return filePaths;
    }

    /**
     * 单文件下载(因为存在多级目录,通过文件路径)
     * @param response
     * @param request
     * @param filePath
     */
    @SneakyThrows
    public void download(HttpServletResponse response, HttpServletRequest request, String filePath) {
        String fileName = getFileNameByPath(filePath);
        // 实际情况的文件路径中不包含桶名,可从数据库中获取
        String bucketName = StringUtils.split(filePath, "/")[0];
        String thisFilePath = StringUtils.split(filePath, "/")[1];
        // 设置响应头信息,告知浏览器下载文件
        response.setCharacterEncoding("UTF-8");
        response.addHeader("Content-Disposition", "attachment;filename=" + fileNameEncoding(fileName, request));
        // 放行Content-Disposition标头,设置权限访问
        response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
        GetObjectResponse getObjectResponse = minioClient.getObject(GetObjectArgs.builder()
                .bucket(bucketName)
                .object(thisFilePath)
                .build());
        int len = -1;
        byte[] buffer = new byte[1024];
        FastByteArrayOutputStream outputStream = new FastByteArrayOutputStream();
        while ((len = getObjectResponse.read(buffer)) != -1) outputStream.write(buffer, 0, len);
        outputStream.flush();
        byte[] bytes = outputStream.toByteArray();
        ServletOutputStream stream = response.getOutputStream();
        stream.write(bytes);
        stream.flush();
    }

    /**
     * 批量文件下载,打包成压缩包(通过文件路径)
     * @param response
     * @param request
     * @param zipName 可为空
     */
    @SneakyThrows
    public void download(HttpServletResponse response, HttpServletRequest request, List<String> filePaths, String zipName) {
        String thisZipName = zipName.equals("") ? "default" : zipName;
        // 清除首部空白行
        response.reset();
        // 设置响应头信息,告知浏览器下载文件
        response.setCharacterEncoding("UTF-8");
        response.addHeader("Content-Disposition", "attachment;filename=" + fileNameEncoding(thisZipName, request));
        // 放行Content-Disposition标头,设置权限访问
        response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
        BufferedOutputStream bufferedOS = new BufferedOutputStream(response.getOutputStream());
        ZipOutputStream zipOS = new ZipOutputStream(bufferedOS);
        response.setHeader("Access-Control-Allow-Origin", "*");
        for (String path : filePaths) {
            String fileName = getFileNameByPath(path);
            // 实际情况的文件路径中不包含桶名,可从数据库中获取
            String bucketName = StringUtils.split(path, "/")[0];
            String filePath = StringUtils.split(path, "/")[1];
            InputStream inputStream = minioClient.getObject(GetObjectArgs.builder()
                    .bucket(bucketName)
                    .object(filePath)
                    .build());
            byte[] buffer = new byte[1024];
            int len = 0;
            zipOS.putNextEntry(new ZipEntry(fileName));
            while ((len = inputStream.read(buffer)) > 0) zipOS.write(buffer, 0, len);
        }
        bufferedOS.close();
        zipOS.close();
    }

    /**
     * 单文件删除(多级目录,使用文件路径)
     * @param filePath
     * @return boolean
     */
    public boolean removeFile(String filePath) {
        String bucketName = StringUtils.split(filePath, "/")[0];
        String thisFilePath = StringUtils.split(filePath, "/")[1];
        try {
            minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(thisFilePath).build());
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 批量文件删除
     * @param filePaths
     * @return Map
     */
    public Map<String, String> removeFile(List<String> filePaths) {
        Map<String,String > result = new HashMap<>();
        for (String path : filePaths) {
            String bucketName = StringUtils.split(path, "/")[0];
            String fileName = StringUtils.split(path, "/")[1];
            try {
                // 这样Minio提供了removeObjects方法,配合,我这里因为桶可能不唯一,暂定
                minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(fileName).build());
                result.put(getFileNameByPath(path), "删除成功");
            } catch (Exception e) {
                e.printStackTrace();
                result.put(getFileNameByPath(path), "网络异常,删除失败");
            }
        }
        return result;
    }

    /**
     * 设置不同浏览器编码
     * @param fileName
     * @param request
     */
    @SneakyThrows
    public static String fileNameEncoding(String fileName, HttpServletRequest request) {
        // 获得请求头中的User-Agent
        String agent = request.getHeader("User-Agent");
        // 根据不同的客户端进行不同的编码
        if (agent.contains("MSIE")) {
            // IE浏览器
            fileName = URLEncoder.encode(fileName, "utf-8");
        } else if (agent.contains("Firefox")) {
            // 火狐浏览器
            BASE64Encoder base64Encoder = new BASE64Encoder();
            fileName = "=?utf-8?B?" + base64Encoder.encode(fileName.getBytes("utf-8")) + "?=";
        } else {
            // 其它浏览器
            fileName = URLEncoder.encode(fileName, "utf-8");
        }
        return fileName;
    }
}

最后在controller包下编写MinioController.java进行测试:

import com.wanglang.minioexercise.utils.MinioUtil;
import com.wanglang.minioexercise.utils.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/minio")
public class MinioController {

    @Autowired
    private MinioUtil minioUtil;

    /**
     * 创建桶
     * @param bucketName
     * @return Boolean
     */
    @GetMapping("/createBucket/{bucketName}")
    public R<Boolean> createBucket(@PathVariable String bucketName) {
        if (minioUtil.existBucket(bucketName)) return R.error("已存在");
        return minioUtil.makeBucket(bucketName) ? R.success(true) : R.error("创建失败");
    }

    /**
     * 删除桶
     * @param bucketName
     * @return Boolean
     */
    @GetMapping("/removeBucketByName/{bucketName}")
    public R<Boolean> removeBucketByName(@PathVariable String bucketName) {
        if (!minioUtil.existBucket(bucketName)) return R.error("未查到该桶");
        return minioUtil.removeBucket(bucketName) ? R.success(true) : R.error("删除失败");
    }

    /**
     * 单文件上传
     * @param file
     * @param bucketName
     * @return String 文件路径
     */
    @PostMapping("/uploadSingleFileByFileOrBucketName")
    public R<String> uploadSingleFileByFileOrBucketName(@RequestParam MultipartFile file,
                                                        @RequestParam String bucketName) {
        return (bucketName.equals("") || minioUtil.existBucket(bucketName))
                ? R.success(minioUtil.upload(file, bucketName))
                : R.error("该桶不存在");
    }

    /**
     * 批量文件上传
     * @param files
     * @param bucketName
     * @return Map 文件文件路径集合
     */
    @PostMapping("/uploadClusterFileByFileOrBucketName")
    public R<Map<String, String>> uploadClusterFileByFileOrBucketName(@RequestParam MultipartFile[] files,
                                                                      @RequestParam String bucketName) {
        return (bucketName.equals("") || minioUtil.existBucket(bucketName))
                ? R.success(minioUtil.upload(files, bucketName))
                : R.error("该桶不存在");
    }

    /**
     * 单文件下载(因为存在多级目录,通过文件路径)
     * @param filePath
     * @param response
     * @param request
     * @return Boolean
     */
    @PostMapping("/downloadSingleFileByFilePath")
    public void downloadSingleFileByFilePath(@RequestParam String filePath,
                                             HttpServletResponse response, HttpServletRequest request) {
        minioUtil.download(response, request, filePath);
    }

    /**
     * 批量文件下载(因为存在多级目录,通过文件路径)
     * @param filePaths
     * @param response 可为空
     * @param response
     * @param request
     */
    @PostMapping("/downloadClusterFileByFilePaths")
    public void downloadClusterFileByFilePaths(@RequestParam List<String> filePaths, @RequestParam String zipName,
                                               HttpServletResponse response, HttpServletRequest request) {
        minioUtil.download(response, request, filePaths, zipName);
    }

    /**
     * 单文件删除(多级目录,使用文件路径)
     * @param filePath
     * @return Boolean
     */
    @PostMapping("/removeSingleFileByFilePath")
    public R<Boolean> removeSingleFileByFilePath(@RequestParam String filePath) {
        return minioUtil.removeFile(filePath) ? R.success(true) : R.error("删除失败");
    }

    /**
     * 文件批量删除(多级目录,使用文件路径)
     * @param filePaths
     * @return Map
     */
    @PostMapping("/removeClusterFileByFilePaths")
    public R<Map<String, String>> removeClusterFileByFilePaths(@RequestParam List<String> filePaths) {
        return R.success(minioUtil.removeFile(filePaths));
    }
}

到此整合结束。

测试

我这里使用的ApiPost7进行测试,提供一个批量上传示例:
批量上传
在本地磁盘和Web端即可查看到变化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值
>