单机版 Minio 的部署和使用

MinIO 基于 Apache License v2.0 开源协议的对象存储服务,兼容亚马逊 S3( Simple Storage Service 简单存储服务)云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、静态页面等,一个对象文件可以是任意大小,文件大小最大支持 5T 。由于采用Golang实现,服务端可以工作在绝大多数主流操作系统上,而且配置和启动服务都非常简单。

本博客主要介绍单机版 Minio 的部署,分别介绍简单部署方式和纠删码部署方式,以及使用 Springboot 程序访问 Minio 实现文件的上传、下载和删除操作,在本篇博客的最后提供源代码下载。有关 Minio 的详细介绍请参考官方文档。

官网地址:https://min.io
中文地址:https://www.minio.org.cn


一、简单部署方式

我的 CentOS7 虚拟机 ip 地址为:192.168.136.128,已经安装好了 docker 和 docker-compose

首先在虚拟机上创建目录 /app/minio,然后创建相关子目录 data 和 docker-compose.yml 文件,具体结构如下:

image

编写 docker-compose.yml 文件内容如下:

version: "3.5"
services:
  minio:
    image: minio/minio
    container_name: minio
    privileged: true
    restart: always
    ports:
      # 对外提供的 api 访问端口
      - 9000:9000
      # 对外提供的 web 管理后台访问端口
      - 9001:9001
    environment:
      # web管理后台用户名
      MINIO_ROOT_USER: jobs
      # web管理后台密码
      MINIO_ROOT_PASSWORD: jobs@123
    volumes:
      # 文件存储目录映射
      - /app/minio/data:/data
    # 运行 minio 服务启动命令,/data 参数是 docker 容器内部的数据目录
    # 由于 web 管理后台是动态端口,因此必须指定为固定的端口
    command: server --console-address ":9001" /data

然后在 docker-compose.yml 文件所在目录下,运行 docker-compose up -d 启动服务即可。


二、纠删码部署方式

Minio 可以使用至少 4 块磁盘,采用 Minio Erasure Code(纠删码)的部署方式,对上传的文件进行保护。即便损坏一半磁盘,仍然可以恢复全部文件。单机版 Minio 纠删码的部署方式,相比简单部署方式,非常简单,只不过是多挂载一些磁盘而已。但是磁盘的总数量必须 2 的 n 次幂(n >= 2),比如 4 块硬盘,8 块硬盘,16 块硬盘等等。我这边没有那么多磁盘,我使用 4 个目录代表 4 个磁盘,实现纠删码的部署方式。

首先在虚拟机上创建目录 /app/minio-erasure ,然后在其内部创建相关的子目录和文件,具体结构如下:

image

编写 docker-compose.yml 文件内容如下:

version: "3.5"
services:
  minio:
    image: minio/minio
    container_name: minio
    privileged: true
    restart: always
    ports:
      # 对外提供的 api 访问端口
      - 9000:9000
      # 对外提供的 web 管理后台访问端口
      - 9001:9001
    environment:
      # web管理后台用户名
      MINIO_ROOT_USER: jobs
      # web管理后台密码
      MINIO_ROOT_PASSWORD: jobs@123
    volumes:
      # 文件存储目录映射
      - /app/minio-erasure/data1:/data1
      - /app/minio-erasure/data2:/data2
      - /app/minio-erasure/data3:/data3
      - /app/minio-erasure/data4:/data4
    # 运行 minio 服务启动命令,/data 参数是 docker 容器内部的数据目录
    # 由于 web 管理后台是动态端口,因此必须指定为固定的端口
    command: server --console-address ":9001" /data{1...4}

然后在 docker-compose.yml 文件所在目录下,运行 docker-compose up -d 启动服务即可。


三、Web 界面简单操作

我们部署的 Minio 的 web 界面访问端口是 9001 ,因此访问 http://192.168.136.128:9001 即可:

image

我们部署时设置的账号是 jobs ,密码是 jobs@123,输入后登录即可。在左侧菜单中选择 Buckets ,在右侧的界面中创建一个 jobstest 的 Bucket 。Bucket 实际上相当于一个目录,用于对不同系统或项目的文件进行隔离。我们可以继续在 Bucket 内部创建文件夹,对文件进行分类管理。

image

点击 Browse 按钮,我们在 jobstest 的 Bucket 下,上传一个图片(注意:上传的文件名称,尽量不要使用包含中文的名称):

image

默认创建的 Bucket 是 private 权限,无法被外部访问。

在上面的 Minio 操作界面第二张图片中,点击 Manage 按钮,进入 Bucket 管理界面:

image

然后点击 Access Policy 的编辑按钮,从弹出的框的下拉列表中,选择 Public ,这样就可以从外部访问文件了

image

访问文件的 url 地址格式为:http://minio 的 api 地址/bucket名称/文件夹及文件名称

因此访问刚刚上传的 qrcode2.jpg 的 url 地址为:http://192.168.136.128:9000/jobstest/qrcode2.jpg

需要注意的是:

  • 需要通过 api 的端口访问文件,部署时指定的 api 访问端口是 9000。(web管理界面的端口是 9001)
  • 由于没有在 jobstest 下面创建目录,直接在 jobstest 下面上传的图片,因此 qrcode2.jpg 前面没有一级或多级目录路径。

上传的 qrcode2.jpg 图片,是我在博客园的博客首页地址的二维码图片,在浏览器访问如下:

image


四、代码访问 Minio 实现文件上传、下载、删除

新建一个 springboot 工程,名称为 springboot_minio,具体结构如下所示:

image

首先看一下 pom 文件中引入的依赖包:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.jobs</groupId>
    <artifactId>springboot_minio</artifactId>
    <version>1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--引入 minio 的依赖包-->
        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>8.5.7</version>
        </dependency>
        <!--引入 minio 依赖后,必须要引入 okhttp 依赖,版本必须大于等于 4.11.0-->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.12.0</version>
        </dependency>
        <!--使用 knife4j 功能,为了可以使用 web 界面测试接口-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.11.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.4.5</version>
            </plugin>
        </plugins>
    </build>
</project>

需要引入 minio 和 okhttp 这两个主要依赖包,而且有个坑必须注意:okhttp 的版本必须大于等于 4.11.0

由于我们使用 knife4j 文档对所开发的 web 接口进行测试,因此引入了 knife4j-spring-boot-starter 依赖包。

然后看一下 application.yml 配置文件的内容:

server:
  port: 8080

minio:
  accesskey: jobs
  secretkey: jobs@123
  bucket: jobstest
  endpoint: http://192.168.136.128:9000

knife4j:
  # 是否启用增强版功能
  enable: true
  # 如果是生产环境,将此设置为 true,然后就能够禁用了 knife4j 的页面
  production: false

Spring:
  servlet:
    multipart:
      # 单个文件上传大小限制
      max-file-size: 100MB
      # 如果同时上传多个文件,上传的总大小限制
      max-request-size: 100MB

在 WebMvcConfig 中,配置一下 knife4j 文档:

package com.jobs.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

//启用 knife4j 需要添加这个注解 @EnableOpenApi
@EnableOpenApi
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

    //需要配置 knife4j 的静态资源请求映射地址
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/doc.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

    @Bean
    public Docket createDocket() {
        // 文档类型
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.jobs.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("我的 Minio 测试")
                .version("1.0")
                .description("Minio 测试接口文档")
                .build();
    }
}

我们使用 MinioClient 访问 Minio 服务,因此需要在 Springboot 中将 MinioClient 添加到容器中:

package com.jobs.config;

import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MinioConfig {

    @Value("${minio.accesskey}")
    private String accessKey;

    @Value("${minio.secretkey}")
    private String secretKey;

    @Value("${minio.endpoint}")
    private String endPoint;

    @Bean
    public MinioClient buildMinioClient() {
        return MinioClient.builder()
                .credentials(accessKey, secretKey).endpoint(endPoint).build();
    }
}

然后创建一个 MinioService 实现对 Minio 服务的文件上传、下载、删除的操作:

package com.jobs.service;

import io.minio.GetObjectArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

@Slf4j
@Service
public class MinioService {

    @Autowired
    private MinioClient minioClient;

    @Value("${minio.endpoint}")
    private String endPoint;

    @Value("${minio.bucket}")
    private String bucket;

    /**
     * 获取文件在 minio server 上的完整访问路径 url 地址
     *
     * @param minioPath 相对于 bucket 根目录的文件全路径
     */
    public String getFileFullUrl(String minioPath) {
        StringBuilder sb = new StringBuilder();
        sb.append(endPoint);
        sb.append(endPoint.endsWith("/") ? "" : "/");
        sb.append(bucket).append("/");
        sb.append(minioPath);
        return sb.toString();
    }

    /**
     * @param path 相对于 bucket 根目录的目录路径
     *             比如 /book/chinese 表示上传到 book 目录下的 chinese 目录中
     * @param file 上传的文件
     * @return 相对于 bucket 目录的文件全路径
     */
    public String uploadFile(String path, MultipartFile file) {
        try {
            return uploadFile(path, file.getOriginalFilename(), file.getInputStream(), file.getContentType());
        } catch (Exception ex) {
            log.error("minio 上传文件失败:", ex);
            return null;
        }
    }

    //上传文件
    public String uploadFile(String path, File file, String contentType) {
        try {
            FileInputStream inputStream = new FileInputStream(file);
            return uploadFile(path, file.getName(), inputStream, contentType);
        } catch (Exception ex) {
            log.error("minio 上传文件失败:", ex.getMessage());
            return null;
        }
    }

    //上传文件
    private String uploadFile(String path, String fileName,
                              InputStream inputStream, String contentType) throws Exception {
        //拼接上传文件的目录存储路径,如果目录不存在,则自动创建(支持多级目录的自动创建)
        String filePath = fileName;
        if (StringUtils.isNotBlank(path)) {
            StringBuilder sb = new StringBuilder();
            sb.append(path);
            sb.append(path.endsWith("/") ? "" : "/");
            sb.append(fileName);
            filePath = sb.toString();
        }

        PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                .object(filePath).contentType(contentType).bucket(bucket)
                //inputStream.available() 表示文件的总字节数大小,-1 表示上传所有字节数
                .stream(inputStream, inputStream.available(), -1).build();
        minioClient.putObject(putObjectArgs);
        return filePath;
    }

    /**
     * 下载文件
     *
     * @param minioPath 相对于 bucket 根目录的文件全路径
     */
    public byte[] downloadFile(String minioPath) {
        if (StringUtils.isBlank(minioPath)) {
            return null;
        }

        try {
            GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                    .bucket(bucket).object(minioPath).build();
            InputStream inputStream = minioClient.getObject(getObjectArgs);
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            IOUtils.copy(inputStream, outputStream);
            return outputStream.toByteArray();
        } catch (Exception ex) {
            log.error("minio 下载文件失败,pathUrl:{}/{},失败信息:{}",
                    bucket, minioPath, ex.getMessage());
            return null;
        }
    }

    /**
     * 删除文件
     *
     * @param minioPath 相对于 bucket 根目录的文件全路径
     */
    public boolean deleteFile(String minioPath) {
        if (StringUtils.isBlank(minioPath)) {
            return true;
        }

        try {
            RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder()
                    .bucket(bucket).object(minioPath).build();
            minioClient.removeObject(removeObjectArgs);
            return true;
        } catch (Exception ex) {
            log.error("minio 删除文件失败,pathUrl:{}/{},失败信息:{}",
                    bucket, minioPath, ex.getMessage());
            return false;
        }
    }
}

然后创建 MinioController 对外提供文件的上传、下载、删除接口:

package com.jobs.controller;

import com.jobs.service.MinioService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
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.HashMap;
import java.util.Map;

@Api(tags = "Minio 操作接口")
@RestController
@RequestMapping("/minio")
public class MinioController {

    @Autowired
    private MinioService minioService;

    @ApiOperation("上传文件")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "path", value = "上传目录路径", required = false),
            @ApiImplicitParam(name = "file", value = "上传的目标文件",
                    dataType = "java.io.File", paramType = "query", required = true)})
    @PostMapping("/upload")
    //注意:针对 MultipartFile 需要增加 @RequestPart 注解,否则 knife4j 接口无法显示文件上传操作。
    public Map uploadFile(String path, @RequestPart("file") MultipartFile file) {
        String minioPath = minioService.uploadFile(path, file);
        Map<String, String> map = new HashMap<>();
        if (StringUtils.isNotBlank(minioPath)) {
            map.put("minio_path", minioPath);
            map.put("file_url", minioService.getFileFullUrl(minioPath));
        }
        return map;
    }

    //必须要加上 produces = "application/octet-stream" ,否则 knife4j 接口无法下载文件
    @ApiOperation(value = "下载文件", produces = "application/octet-stream")
    @ApiImplicitParam(name = "minioPath",
            value = "minio文件路径(不包含web域名和bucket名称)", required = true)
    @GetMapping("/download")
    public void downloadFile(String minioPath, HttpServletResponse response) {
        //获取文件名(包含后缀名)
        String fileName = FilenameUtils.getName(minioPath);
        byte[] bytes = minioService.downloadFile(minioPath);
        if (bytes != null) {
            try {
                //下载文件的响应类型,这里统一设置成了文件流
                //你可以根据自己所提供下载的文件类型,使用不同的响应 mime 类型
                response.setContentType("application/octet-stream;charset=utf-8");
                //设置下载弹出框中默认显示的文件名称,如果指定中文名称的话,需要转成 iso8859-1 编码,解决乱码问题
                fileName = new String(fileName.getBytes(), "iso8859-1");
                response.addHeader("Content-Disposition", "attachment;filename=" + fileName);
                IOUtils.write(bytes, response.getOutputStream());
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }

    @ApiOperation("删除文件")
    @ApiImplicitParam(name = "minioPath",
            value = "minio文件路径(不包含web域名和bucket名称)", required = true)
    @PostMapping("/delete")
    public String deleteFile(String minioPath) {
        Boolean flag = minioService.deleteFile(minioPath);
        return flag ? "delete success" : "delete fail";
    }
}

最后启动 springboot 程序,访问 http://localhost:8080/doc.html 即可通过图形化界面调用接口进行测试:

image

有关具体的测试细节,这里就不展示了,我已经测试没问题了。

大家可以下载源代码,自己进行测试体验,结合 Minio 的 web 界面进行验证。


本篇博客的源代码下载地址:https://files.cnblogs.com/files/blogs/699532/springboot_minio.zip

posted @ 2024-02-08 11:40  乔京飞  阅读(6969)  评论(0编辑  收藏  举报