MinIo学习记录

背景介绍

MinIO 是一个基于 Go 实现的轻量高性能、兼容 S3 协议的对象存储。它采用 GNU AGPL v3 开源协议,项目地址是 https://github.com/minio/minio,官网是 https://min.io。下载地址 https://min.io/download

它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据。 例如图片、音频、视频、日志文件等常见文件,备份数据、容器、虚拟机镜像等等,而一个对象文件可以是任意大小,从几 kb 到最大 5T 不等。

国内阿里巴巴、腾讯、百度、华为、中国移动、中国联通等企业在使用 MinIO,甚至不少商业公司二次开发 MinIO 来提供商业化的云存储产品。

安装使用

基于docker进行安装并启动MinIo容器

1.拉取MinIo镜像,学习用的所以拉取镜像时不指定版本直接体验下最新版本

docker pull minio/minio

2.启动容器

docker run -p 9000:9000 -p 9090:9090 \
--name minio \
--privileged=true \
-it -d --restart=always \
-e "MINIO_ACCESS_KEY=minioadmin" \  #创建用户:minioadmin
-e "MINIO_SECRET_KEY=minioadmin" \  #设置密码:minioadmin
-v /dataFile/minio/data:/data \		#挂载数据目录
minio/minio server \
/data --console-address ":9090" -address ":9000"

访问9090端口测试,出现以下界面代表启动成功,启动失败可以使用docker的logs查看日志排查解决

在这里插入图片描述

使用启动容器时添加的账号和密码进行登录,登录成功后需要做以下两件事

1.创建存放文件的存储桶(类似于电脑的盘符)

2.创建用户并且为该用户生成 accessKey、secretKey

整合使用

使用SpringBoot整合MinIo完成文件的上传、下载、删除操作

1.引入MinIo依赖

<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.4.5</version>
       <exclusions>
          <exclusion>
          	<groupId>com.squareup.okhttp3</groupId>
          	<artifactId>okhttp</artifactId>
          </exclusion>
       </exclusions>
</dependency>

<dependency>
   <groupId>com.squareup.okhttp3</groupId>
   <artifactId>okhttp</artifactId>
   <version>4.10.0</version>
</dependency>

2. 定义配置类、工具类

yml文件:

########### MinIO 服务配置 #############
minio:
  #服务器的地址和端口号
  endpoint: 192.168.56.103
  port: 9000
  #上传的密钥
  accessKey: hPe2dUdIYoevY2n7
  secretKey: RjPjKe9UVXp1bP2cjcSsElTDkkIe3max
  #需要注意的一点是 secure 参数是设置 oss 服务请求 minio 是否使用 https 请求默认false使用 http 请求
  #但在 new MinioClient() 构造方法中默认是 true
  secure: false
  #文件存储桶名称
  bucketName: systembucket

spring:
  servlet:
    #设置文件上传大小限制
    multipart:
      max-file-size: 500MB
      max-request-size: 500MB

自定义配置类 MinioProperty:

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @author: YUF
 * @Date: 2023/4/21 14:19
 */
@Data
@Component
@ConfigurationProperties(prefix = "minio")
public class MinIoProperty {
    private String endpoint = "localhost";

    private int port = 9000;

    private String accessKey = "minioadmin";

    private String secretKey = "minioadmin";

    private boolean secure = false;
}

创建工具类:

package com.hp.utils;

import com.alibaba.fastjson.JSON;
import com.hp.config.MinIoProperty;
import com.hp.dto.DelFilesDto;
import com.hp.entity.FileInfoRes;
import com.hp.entity.MinIoItem;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * 8.x.x版
 *
 * @author: YUF
 * @Date: 2023/4/21 14:20
 */
@Component
public class MinIoUtils8 {

    @Autowired
    private MinIoProperty minIoProperty;

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

    /**
     * minio客户端
     */
    private MinioClient minioClient;

    /**
     * 文件的ContentType映射map
     * key-》文件类型 value-》ContentType
     */
    private Map<String, String> map = new HashMap<>(16);

    /**
     * 创建 MinioClient 客户端
     */
    @SuppressWarnings("all")
    @PostConstruct
    public void createMinIoClient() throws Exception {
        // 拼接地址
        String adders = "http://" + minIoProperty.getEndpoint() + ":" + minIoProperty.getPort();
        this.minioClient = MinioClient.builder().endpoint(adders)
                .credentials(minIoProperty.getAccessKey(), minIoProperty.getSecretKey())
                .build();
        if (null == bucketName) {
            // 配置文件中未设置时赋予默认值
            bucketName = "asiatrip";
        }
        /**
         * 读取文件类型映射JSON文件,内容如下展示:
         * {"jpe":"image/jpeg","jpeg":"image/jpeg","jpg":"image/jpeg","jpgm":"video/jpm","mp4":"video/mp4",......}
         */
        String jsonStr = readJsonFile("json", "fileType.json");
        if (null != jsonStr) {
            // 反序列化文件类型映射JSON
            this.map = JSON.parseObject(jsonStr, Map.class);
        }
    }

    /**
     * 上传文件方法
     *
     * @param file 上传的文件
     * @return 封装后返回的文件信息实体类
     */
    public FileInfoRes uploadFile(MultipartFile file, String prefix) throws Exception {
        //获取文件的输入流
        try (InputStream is = file.getInputStream()) {
            // 创建文件信息返回实体类
            FileInfoRes fileInfo = new FileInfoRes();
            // 检查存储桶是否已经存在
            boolean isExist = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
            if (!isExist) {
                // 创建一个名为asiatrip的默认存储桶
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            }

            // 创建日期格式类
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd/");
            // 获取当前日期
            Date date = new Date();
            // 文件信息设置上传时间
            fileInfo.setCreateDate(date);
            //格式化日期为字符串
            String dateStr = sdf.format(date);
            if (StringUtils.isNotBlank(prefix)) {
                dateStr = prefix.concat("/").concat(dateStr);
            }
            //获取原文件名称
            String fileName = file.getOriginalFilename();
            //文件信息设置原文件名称
            fileInfo.setOriginalFilename(fileName);
            /**
             * 将待上传的文件的路径设置成 prefix/年/月/日/uuid.png
             * 你也可以自定义上传文件的路径,这样设置是为了更好的管理
             */
            String uuid = UUID.randomUUID()
                    .toString()
                    .replace("-", "")
                    .substring(0, 6);
            assert fileName != null;
            //生成保存文件时的uid文件名
            String uIdName = uuid + fileName.substring(fileName.lastIndexOf("."));
            //保存属性
            fileInfo.setUuIdFileName(uIdName);
            //拼接文件存储的完整路径
            String fileUploadPath = dateStr + uIdName;
            //保存属性
            fileInfo.setFileUploadPath(fileUploadPath);
            // 使用分片的形式上传文件,设置每个分片大小为5MB
            PutObjectArgs put = PutObjectArgs.builder()
                    .contentType(getContentTypeByFileName(fileName))
                    .stream(is, file.getSize(), 10485760)
                    .bucket(bucketName).object(fileUploadPath).build();
            minioClient.putObject(put);

            // 获取永久的访问地址
            GetPresignedObjectUrlArgs foreverUrlArg = GetPresignedObjectUrlArgs.builder()
                    .bucket(bucketName)
                    .object(fileUploadPath)
                    .method(Method.GET)
                    .build();
            String webPath = minioClient.getPresignedObjectUrl(foreverUrlArg);
            //获取带有效期的访问地址
            HashMap<String, String> reqMap = new HashMap<>(16);
            // 添加请求参数,生成路径时会拼接在其中
            reqMap.put("uid", "1174847034");
            reqMap.put("sessionId", "14718103074");
            GetPresignedObjectUrlArgs temporaryUrlArg = GetPresignedObjectUrlArgs.builder()
                    .bucket(bucketName)
                    .expiry(1, TimeUnit.DAYS)
                    .extraQueryParams(reqMap)
                    .object(fileUploadPath)
                    .method(Method.GET)
                    .build();
            /**
             * 获取临时路径: expires有效期秒为单位,可以看到下面生成的路径中是包含了设置的reqMap值的
             * http://192.168.56.103:9000/systembucket/2023/04/21/d9a98a.bat?
             * sessionId=1174847034&uid=14718103074&X-Amz-Algorithm=......
             */
            String temporaryUrl = minioClient.getPresignedObjectUrl(temporaryUrlArg);

            // 保存属性
            fileInfo.setWebPath(webPath);
            fileInfo.setTemporaryUrl(temporaryUrl);
            fileInfo.setBucketName(bucketName);
            return fileInfo;
        } catch (Exception e) {
            throw new Exception("MinIo文件上传失败");
        }
    }

    /**
     * 根据文件名称获取ContentType类型
     *
     * @param fileName 文件名称
     * @return ContentType类型
     */
    private String getContentTypeByFileName(String fileName) {
        // 根据文件名获取类型
        String fileExtension = "";
        int dotIndex = fileName.lastIndexOf(".");
        if (dotIndex > 0) {
            fileExtension = fileName.substring(dotIndex + 1);
        }
        // 根据文件类型获取ContentType
        String contentType = map.get(fileExtension);
        /**
         * 为空给个默认值
         * application/octet-stream: 二进制流数据(如常见的文件下载,不知道下载文件类型)
         */
        if (null == contentType) {
            contentType = "application/octet-stream";
        }
        return contentType;
    }

    /**
     * 读取某个目录下的json文件,并返回内容字符串
     *
     * @param catalogue 目录
     * @param fileName  文件名
     * @return
     */
    public String readJsonFile(String catalogue, String fileName) {
        String jsonStr = "";
        try {
            ClassPathResource classPathResource = new ClassPathResource(catalogue + "/" + fileName);
            InputStream inputStream = classPathResource.getInputStream();
            Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
            int ch;
            StringBuilder sb = new StringBuilder();
            while ((ch = reader.read()) != -1) {
                sb.append((char) ch);
            }
            reader.close();
            jsonStr = sb.toString();
            return jsonStr;
        } catch (IOException e) {
            return null;
        }
    }

    /**
     * 单个删除文件
     *
     * @param bucketName 存储桶名称
     * @param objectName 文件路径
     * @return
     * @throws Exception
     */
    public boolean delFile(String bucketName, String objectName) throws Exception {
        try {
            minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
            return Boolean.TRUE;
        } catch (Exception e) {
            return Boolean.FALSE;
        }
    }

    /**
     * 删除指定桶的多个文件对象,返回删除错误的对象列表,全部删除成功,返回空列表
     *
     * @param delFilesDto 删除dto,两个属性 存储桶名称:bucketName 文件路径列表:filePaths
     * @return eg: 删除失败的文件名称
     * @throws Exception
     */
    public List<String> delFiles(DelFilesDto delFilesDto) throws Exception {
        List<String> deleteErrorNames = new ArrayList<>();
        List<DeleteObject> objects = new ArrayList<>();
        for (String filePath : delFilesDto.getFilePaths()) {
            objects.add(new DeleteObject(filePath));
        }
        RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket(delFilesDto.getBucketName())
                .objects(objects).build();
        Iterable<Result<DeleteError>> results = minioClient
                .removeObjects(removeObjectsArgs);
        for (Result<DeleteError> result : results) {
            DeleteError error = result.get();
            deleteErrorNames.add(error.objectName());
        }
        return deleteErrorNames;
    }

    /**
     * 获取对象的元数据
     * 该实体类无法直接作为接口返回值,因为其没有get&set方法会导致报406-Not Acceptable的错误
     *
     * @param bucketName 存储桶名称
     * @param objectName 文件路径
     * @return
     */
    public StatObjectResponse getStatObject(String bucketName, String objectName) {
        StatObjectResponse statObject;
        try {
            statObject = minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
        } catch (Exception e) {
            return null;
        }
        return statObject;
    }

    /**
     * 下载文件
     *
     * @param bucketName 存储桶名称
     * @param objectName 文件路径
     * @param response   响应流
     */
    public void downloadFile(String bucketName, String objectName, HttpServletResponse response) {
        try (InputStream in = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
             OutputStream out = response.getOutputStream()) {
            downloadFile(objectName, response, in, out);
        } catch (Exception ignored) {
        }
    }

    /**
     * 下载文件
     *
     * @param bucketName 存储桶名称
     * @param objectName 文件路径
     * @param offSet     开始读取的位置
     * @param length     需要读取的字节
     * @param response   响应流
     */
    public void downloadFile(String bucketName, String objectName, long offSet, Long length, HttpServletResponse response) {
        GetObjectArgs build = GetObjectArgs.builder().bucket(bucketName).object(objectName).offset(offSet).length(length).build();
        try (InputStream in = minioClient.getObject(build);
             OutputStream out = response.getOutputStream()) {
            downloadFile(objectName, response, in, out);
        } catch (Exception ignored) {
        }
    }

    private void downloadFile(String objectName, HttpServletResponse response, InputStream in, OutputStream out) throws IOException {
        int len;
        byte[] buffer = new byte[1024];
        //重置
        response.reset();
        //设置编码
        response.setCharacterEncoding("UTF-8");
        //设置Mime-Type
        String contentType = getContentTypeByFileName(objectName);
        if (StringUtils.isBlank(contentType)) {
            contentType = "application/octet-stream";
        }
        response.setContentType(contentType);
        //设置下载默认文件名
        response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder
                .encode(objectName, "UTF-8"));
        while ((len = in.read(buffer)) > 0) {
            out.write(buffer, 0, len);
        }
        out.flush();
    }

    /**
     * 根据前缀查询文件列表或文件夹列表
     *
     * @param bucketName bucket名称
     * @param prefix     前缀
     * @param recursive  是否递归查询
     * @return MinioItem 列表
     */
    @SneakyThrows
    public List<MinIoItem> getAllObjectsByPrefix(String bucketName, String prefix, boolean recursive) {
        List<MinIoItem> objectList = new ArrayList<>();
        ListObjectsArgs build = ListObjectsArgs.builder().bucket(bucketName).prefix(prefix).recursive(recursive).build();
        Iterable<Result<Item>> objectsIterator = minioClient
                .listObjects(build);
        for (Result<Item> itemResult : objectsIterator) {
            Item item = itemResult.get();
            String path = item.objectName();
            String objectName = path.replace(prefix, "").replace("/", "");
            objectList.add(new MinIoItem(objectName, item.isDir(), path));
        }
        return objectList;
    }

}

3.调用测试

接口调用测试文档:https://documenter.getpostman.com/view/15741720/2s93Y5Nz4Q#9a8e3ac7-5725-4d1a-8ec1-8209172e3778

在这里插入图片描述

import com.hp.dto.DelFilesDto;
import com.hp.entity.FileInfoRes;
import com.hp.entity.MinIoItem;
import com.hp.utils.MinIoUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

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

/**
 * @author: YUF
 * @Date: 2023/4/21 14:36
 */
@RequestMapping("/file")
@RestController
public class MinIoController {

    @Autowired
    private MinIoUtils minIoUtils;

    /**
     * 文件上传
     *
     * @param file   文件对象
     * @param prefix 路径前缀,用于业务操作
     * @return
     */
    @PostMapping("uploadFile")
    public FileInfoRes uploadFile(MultipartFile file, String prefix) throws Exception {
        return minIoUtils.uploadFile(file, prefix);
    }

    /**
     * 单个删除文件
     * 文件删除之后,如果该文件夹下没有文件存在,MinIO会自动删除该空文件夹及上级空文件夹。
     * @param bucketName 存储桶名称
     * @param objectName 文件路径
     * @return
     */
    @PostMapping("delFile")
    public boolean delFile(String bucketName, String objectName) throws Exception {
        return minIoUtils.delFile(bucketName, objectName);
    }

    /**
     * 批量删除文件
     * 文件删除之后,如果该文件夹下没有文件存在,MinIO会自动删除该空文件夹及上级空文件夹。
     * @param delFilesDto 删除dto
     * @return
     */
    @PostMapping("delFiles")
    public List<String> delFiles(@RequestBody DelFilesDto delFilesDto) throws Exception {
        return minIoUtils.delFiles(delFilesDto);
    }

    /**
     * 普通文件下载
     *
     * @param bucketName 存储桶名称
     * @param objectName 文件路径
     */
    @PostMapping("downloadFile")
    public void downloadFile(String bucketName, String objectName, HttpServletResponse response) {
        minIoUtils.downloadFile(bucketName, objectName, response);
    }

    /**
     * 获取指定前缀的文件或文件夹列表
     *
     * @param bucketName 存储桶名称
     * @param prefix     路径前缀
     * @param recursive  是否递归查询
     * @return
     */
    @PostMapping("getAllObjectsByPrefix")
    public List<MinIoItem> getAllObjectsByPrefix(@RequestParam String bucketName, @RequestParam String prefix, @RequestParam boolean recursive) {
        return minIoUtils.getAllObjectsByPrefix(bucketName, prefix, recursive);
    }

}

注:

1.想查看文件上传后分片情况可以找到启动容器时挂载的目录

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
分成6个分片的原因是在上传时代码中设置了以10MB为单个分片大小,原文件为54.36MB

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值