MinIO

file

# 一、MinIO基础

OSS ——Object Storage Service,对象存储服务

Multi-Cloud Object Storage ——分布式对象存储

MinIO官网 (opens new window)

MinIO中文官网 (opens new window)

MinIO官方文档 (opens new window)

MinIO 是一个基于Go语言实现的高性能的对象存储服务分布式文件存储系统,使用Apache License v2.0开源协议。

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

MinIO是一个非常轻量的服务,可以很简单的和其他应用的结合,类似 NodeJS, Redis 或者 MySQL。

对象存储服务(Object Storage Service,OSS)是一种海量、安全、低成本、高可靠的云存储服务,适合存放任意类型的文件。容量和处理能力弹性扩展,多种存储类型供选择,全面优化存储成本。

# MinIO应用

对于中小型企业,如果不选择存储上云,那么 MinIO 是个不错的选择,麻雀虽小,五脏俱全。

当然MinIO 除了直接作为对象存储使用,还可以作为云上对象存储服务的网关层,无缝对接到 Amazon S3、MicroSoft Azure。

在中国:阿里巴巴、腾讯、百度、中国联通、华为、中国移动等等9000多家企业也都在使用MinIO产品。

# MinIO优点

  • 部署简单: 一个single二进制文件即是一切,还可支持各种平台。
  • minio支持海量存储,可按zone扩展(原zone不受任何影响),支持单个对象最大5TB;
  • 兼容Amazon S3接口,充分考虑开发人员的需求和体验;
  • 低冗余且磁盘损坏高容忍,标准且最高的数据冗余系数为2(即存储一个1M的数据对象,实际占用磁盘空间为2M)。但在任意n/2块disk损坏的情况下依然可以读出数据(n为一个纠删码集合(Erasure Coding Set)中的disk数量)。并且这种损坏恢复是基于单个对象的,而不是基于整个存储卷的。
  • 读写性能优异

# MinIO的基础概念

  • Object

    存储到 Minio 的基本对象,如文件、字节流,Anything...

  • Bucket

    用来存储 Object 的逻辑空间。每个 Bucket 之间的数据是相互隔离的。

    对于客户端而言,就相当于一个存放文件的顶层文件夹

  • Drive

    即存储数据的磁盘,在 MinIO 启动时,以参数的方式传入。Minio 中所有的对象数据都会存储在 Drive 里。

  • Set

    即一组 Drive 的集合,分布式部署根据集群规模自动划分一个或多个 Set ,每个 Set 中的Drive 分布在不同位置。一个对象存储在一个 Set 上。(For example: {1...64} is divided into 4 sets each of size 16.)

    • 一个对象存储在一个Set上 ;

    • 一个集群划分为多个Set ;

    • 一个Set包含的Drive数量是固定的,默认由系统根据集群规模自动计算得出 ;

    • 一个Set中的Drive尽可能分布在不同的节点上。

# 纠删码EC

EC Erasure Code

MinIO 使用纠删码机制来保证高可靠性,使用highwayhash来处理数据损坏( Bit Rot Protection )。

关于纠删码,简单来说就是可以通过数学计算,把丢失的数据进行还原,它可以将n份原始数据,增加m份数据,并能通过n+m份中的任意n份数据,还原为原始数据。

即如果有任意小于等于m份的数据失效,仍然能通过剩下的数据还原出来。

# 存储形式

文件对象上传到 MinIO ,会在对应的数据存储磁盘中,以Bucket名称为目录,文件名称为下一级目录,文件名下是part.1xl.metapart.1是编码数据块及检验块,xl.meta是元数据文件。

# 存储方案

# 二、MinIO权限管理

Minio官方文档-MinIO Identity and Access Management (opens new window)

internal Identity 内部身份

Access Management 访问管理

Policy-Based Access Control(PBAC)基于策略的访问控制

Authentication 认证

Authorization 授权

Policy 策略

MinIO权限管理,即MinIO身份和访问管理。

MinIO提供了一个内部身份访问管理子系统,支持创建用户身份、组和策略,以支持客户端操作的身份验证和授权。

  • Authentication:身份验证是验证连接客户端身份的过程。具体来说,客户机必须提供有效的访问密钥(用户名)和密钥(密码),才能访问任何S3或MinIO管理API,如PUT、GET和DELETE操作。
  • Authorization:授权是限制经过身份验证的客户端可以对部署执行的操作和资源的过程。MinIO使用基于策略的访问控制(PBAC),其中每个策略描述一个或多个规则,这些规则概述了用户或用户组的权限。MinIO在创建策略时支持操作和条件的子集。默认情况下,MinIO拒绝访问用户分配或继承的策略中未明确引用的操作或资源。

# 2.1 身份管理

Identity Management (opens new window)

access key (username) 访问密钥

secret key (password) 密钥

service accounts 服务帐户

MinIO包括一个内置身份提供程序(IDP),提供核心身份管理功能。

MinIO IDP支持在部署上创建任意数量的用户,以支持客户端身份验证。

  • 每个用户由唯一的访问密钥(用户名)和相应的密钥(密码)组成。客户端必须通过指定现有MinIO用户的有效访问密钥(用户名)和相应的密钥(密码)来验证其身份。

  • 每个用户可以有一个或多个分配的策略,这些策略明确列出该用户可以访问的操作和资源。用户还可以从其拥有成员资格的组中继承策略。

    默认情况下,MinIO拒绝访问用户分配或继承的策略未明确允许的所有操作或资源。必须显式分配描述用户授权操作和资源的策略,或者将用户分配给具有关联策略的组。有关详细信息,请参阅**访问管理Access Management **。

MinIO还支持创建服务帐户。服务帐户是经过身份验证的父用户的子身份,并从父用户继承其权限。

# 2.1.1 超级账户root

MinIO root User (opens new window)

MinIO部署有一个根用户,可以访问部署上的所有操作和资源,而不考虑配置的identity manager。

当minio服务器首次启动时,它通过检查以下环境变量的值来设置根用户凭据:

  • MINIO_ROOT_USER (opens new window)

  • MINIO_ROOT_PASSWORD (opens new window)

    说明:

    1. 不要在生产环境中使用默认凭据。MinIO强烈建议为所有环境指定唯一、长且随机的MinIO_ROOT_USER值。
    2. 自2021版本发布以来已弃用MINIO_ACCESS_KEY、MINIO_SECRET_KEY,通知来自Environment Variables (opens new window)
    3. MinIO强烈反对使用超级用户(root)进行常规客户端访问,而不管环境如何(开发、登台或生产)。
    4. MinIO强烈建议创建用户,以便每个客户端都可以访问执行其分配的工作负载所需的最小操作集和资源。
    5. 如果这些变量未设置,minio默认分别将minioadminminioadmin作为访问密钥和密钥。MinIO强烈反对使用默认凭据,无论部署环境如何。

# 2.1.2 用户管理

# 创建用户

管理员使用mc admin user命令创建和管理MinIO 用户。MinIO控制台为创建用户提供了图形界面。

使用mc admin user add命令在MinIO部署上创建新用户:

mc admin user add ALIAS ACCESSKEY SECRETKEY
1

说明:

  1. ALIAS为你的MinIO部署的别名;
  2. ACCESSKEY设置为你的用户名;
  3. SECRETKEY设置为你的密码;MinIO不提供任何方法来检索设置后的密钥。

使用mc admin policy set将基于MinIO策略的访问控制关联到新用户。以下命令指定内置读写策略:

mc admin policy set ALIAS readwrite user=USERNAME
1
# 删除用户

使用mc admin user remove命令删除MinIO部署中的用户:

mc admin user remove ALIAS USERNAME
1

# 2.1.3 服务帐户

Service Accounts (opens new window)

MinIO服务帐户是经过身份验证的MinIO用户的子身份,包括外部管理的身份。每个服务帐户根据附加到其父用户或父用户具有成员资格的组的策略继承其特权。

服务帐户还支持可选的内联策略,该策略进一步限制对父用户可用的操作和资源子集的访问。

MinIO用户可以生成任意数量的服务帐户。这允许应用程序所有者为其应用程序生成任意服务帐户,而无需MinIO管理员的操作。由于生成的服务帐户具有与父用户相同或更少的权限,管理员可以集中精力管理顶级父用户,而无需微观管理生成的服务账户。

您可以使用MinIO控制台或使用mc admin user svcct add命令创建服务帐户。

服务帐户用于编程访问(Service Accounts are for Programmatic Access)

服务帐户支持应用程序的编程访问。您不能使用服务帐户登录MinIO控制台。

# 2.1.4 用户组管理

Group Management (opens new window)

组是用户的集合。组提供了一种简化的方法,用于管理具有常见访问模式和工作负载的用户之间的共享权限。客户端无法使用组作为标识对MinIO部署进行身份验证。

  • 每个组可以有一个或多个分配的策略,这些策略明确列出允许或拒绝组成员访问的操作和资源。

  • 每个组还具有一个或多个分配的用户。每个用户的总权限集由其显式分配的权限和从每个分配组继承的权限组成。

    默认情况下,MinIO拒绝访问附加或继承策略未明确允许的操作或资源。没有显式分配或继承策略的用户无法执行任何S3或MinIO管理API操作。

例:

Group Policy Members
Operations readwrite (opens new window) on finance bucketreadonly (opens new window) on audit bucket john.doe, jane.doe
Auditing readonly (opens new window) on audit bucket jen.doe, joe.doe
Admin admin:* (opens new window) greg.doe, jen.doe

mc admin group命令支持在MinIO部署上创建和管理组。

# 2.2 访问管理

Access Management (opens new window)

MinIO使用基于策略的访问控制(PBAC)来定义经过身份验证的用户可以访问的授权操作和资源。每个策略描述一个或多个操作和条件,这些操作和条件概述了用户或用户组的权限。

# 2.2.1 策略管理

Policy Management (opens new window)

MinIO管理策略的创建和存储。将策略分配给用户或组的过程取决于配置的身份提供程序(IDP)。

# 内置策略

MinIO提供了以下用于分配给用户或组的内置策略:

  • consoleAdmin
  • readonly
  • readwrite
  • diagnostics 诊断
  • writeonly

使用MinIO内部IDP的MinIO部署需要使用mc-admin-policy-set命令将用户显式关联到一个或多个策略。用户还可以继承附加到其具有成员资格的组的策略。

例如,该表描述了如果作为该用户进行身份验证,客户端可以执行的操作子集:

User Policy Operations
Operations readwrite (opens new window) on finance bucketreadonly (opens new window) on audit bucket PUT and GET on finance bucket.PUT on audit bucket
Auditing readonly on audit bucket GET on audit bucket
Admin admin:* All mc admin commands.

默认情况下,MinIO拒绝访问附加或继承策略未明确允许的操作或资源。

每个用户只能访问内置角色明确授予的资源和操作。没有显式分配或继承策略的用户无法执行任何S3或MinIO管理API操作。

# 策略配置

  • Version 标识策略的版本号,Minio 中一般为 2012-10-17
  • Statement 策略授权语句,描述策略的详细信息,包含Effect(效果)、Action(动作)、Principal(用户)、Resource(资源)和Condition(条件)。其中Condition为可选
  • Effect (效果)作用包含两种:Allow(允许)和Deny(拒绝),系统预置策略仅包含允许的授权语句,自定义策略中可以同时包含允许和拒绝的授权语句,当策略中既有允许又有拒绝的授权语句时,遵循Deny优先的原则。
  • Action Action(动作)对资源的具体操作权限,格式为:服务名:资源类型:操作,支持单个或多个操作权限,支持通配符号*,通配符号表示所有。例如 s3:GetObject ,表示获取对象
  • Resource (资源) 策略所作用的资源,支持通配符号*,通配符号表示所有。在JSON视图中,不带Resource表示对所有资源生效。Resource支持以下字符:-_0-9a-zA-Z*./\,如果Resource中包含不支持的字符,请采用通配符号*。例如:arn:aws:s3:::my-bucketname/myobject*\,表示minio中my-bucket/myobject目录下所有对象文件。
  • Condition (条件)您可以在创建自定义策略时,通过Condition元素来控制策略何时生效。Condition包括条件键和运算符,条件键表示策略语句的Condition元素,分为全局级条件键和服务级条件键。全局级条件键(前缀为g:)适用于所有操作,服务级条件键(前缀为服务缩写,如obs:)仅适用于对应服务的操作。运算符与条件键一起使用,构成完整的条件判断语句。

# 三、部署模式

MinIO支持多种server启动模式:

  • 单机模式 standalone mode

    • 无纠删码模式 non-erasure code mode
    • 纠删码模式erasure code mode
  • 分布式模式 distributed mode

    • 无纠删码模式 non-erasure code mode
    • 纠删码模式 erasure code mode

# 单机部署(Docker)

minio server的standalone模式,即要管理的磁盘都在host本地。

该启动模式一般仅用于实验环境、测试环境的验证和学习使用。

在standalone模式下,还可以分为non-erasure code mode和erasure code mode。

  • non-erasure code mode

在此启动模式下,对于每一份对象数据,minio直接在data下面存储这份数据,不会建立副本,也不会启用纠删码机制。

因此,这种模式无论是服务实例还是磁盘都是“单点”,无任何高可用保障,磁盘损坏就表示数据丢失。

  • erasure code mode

此模式为minio server实例传入多个本地磁盘参数。一旦遇到多于一个磁盘参数,minio server会自动启用erasure code mode。

erasure code对磁盘的个数是有要求的,如不满足要求,实例启动将失败。

erasure code启用后,要求传给minio server的endpoint(standalone模式下,即本地磁盘上的目录)至少为4个。

# 无纠删码模式(简单版)

docker-compose.yml

version: '3'
services:
  minio_simple:
    image: minio/minio
    hostname: minio
    container_name: minio_simple
    restart: always
    ports:
      - 9000:9000 # api 端口
      - 9001:9001 # 控制台端口
    environment:
      MINIO_ACCESS_KEY: admin    #管理后台用户名
      MINIO_SECRET_KEY: 12345678 #管理后台密码,最小8个字符
    volumes:
      - ./data:/data               #映射当前目录下的data目录至容器内/data目录
      - ./config:/root/.minio/     #映射配置目录
    command: server --console-address ':9001' /data  #指定容器中的目录 /data
    privileged: true 	#使用该参数,container内的root拥有真正的root权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

说明:

  1. 密码最少需要8个字符;

# minio纠删码模式

Minio使用纠删码 erasure code 和校验和 checksum 来保护数据免受硬件故障和无声数据损坏。 即便丢失一半数量(N/2)的硬盘,仍然可以恢复数据。

docker-compose.yml

version: '3'
services:
  minio_erasure:
    image: minio/minio
    hostname: minio
    container_name: minio_erasure_8
    restart: always
    ports:
      - 9000:9000 # api 端口
      - 9001:9001 # 控制台端口
    environment:
      MINIO_ACCESS_KEY: admin    #管理后台用户名
      MINIO_SECRET_KEY: 12345678 #管理后台密码,最小8个字符
    volumes:
      # 挂载8块盘
      - ./data/data1:/data1
      - ./data/data2:/data2
      - ./data/data3:/data3
      - ./data/data4:/data4
      - ./data/data5:/data5
      - ./data/data6:/data6
      - ./data/data7:/data7
      - ./data/data8:/data8
      - ./config:/root/.minio/     #映射配置目录
    command: server /data{1...8} --console-address ':9001' /data  #指定容器中的目录 /data
    privileged: true 	#使用该参数,container内的root拥有真正的root权限。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 分布式集群部署

分布式Minio可以让你将多块硬盘(甚至在不同的机器上)组成一个对象存储服务。由于硬盘分布在不同的节点上,分布式Minio避免了单点故障。

# 分布式存储可靠性

分布式存储,很关键的点在于数据的可靠性,即保证数据的完整,不丢失,不损坏。只有在可靠性实现的前提下,才有了追求一致性、高可用、高性能的基础。而对于在存储领域,一般对于保证数据可靠性的方法主要有两类,一类是冗余法,一类是校验法。

  • 冗余

    冗余法最简单直接,即对存储的数据进行副本备份,当数据出现丢失,损坏,即可使用备份内容进行恢复,而副本 备份的多少,决定了数据可靠性的高低。这其中会有成本的考量,副本数据越多,数据越可靠,但需要的设备就越多,成本就越高。可靠性是允许丢失其中一份数据。当前已有很多分布式系统是采用此种方式实现,如 Hadoop 的文件系统(3个副本),Redis 的集群,MySQL 的主备模式等。

  • 校验

    校验法即通过校验码的数学计算的方式,对出现丢失、损坏的数据进行校验、还原。注意,这里有两个作用,一个校验,通过对数据进行校验和( checksum )进行计算,可以检查数据是否完整,有无损坏或更改,在数据传输和保存时经常用到,如 TCP 协议;二是恢复还原,通过对数据结合校验码,通过数学计算,还原丢失或损坏的数据,可以在保证数据可靠的前提下,降低冗余,如单机硬盘存储中的 RAID技术,纠删码(Erasure Code)技术等。MinIO 采用的就是纠删码技术。

# 分布式Minio优势

  • 数据保护

    分布式Minio采用 纠删码来防范多个节点宕机和位衰减 bit rot 。

    分布式Minio至少需要4个硬盘,使用分布式Minio自动引入了纠删码功能。

  • 高可用

    单机Minio服务存在单点故障,相反,如果是一个有N块硬盘的分布式Minio,只要有N/2硬盘在线,你的数据就是安全的。不过你需要至少有N/2+1个硬盘来创建新的对象。

    例如,一个16节点的Minio集群,每个节点16块硬盘,就算8台服務器宕机,这个集群仍然是可读的,不过你需要9台服務器才能写数据。

  • 一致性

    Minio在分布式和单机模式下,所有读写操作都严格遵守read-after-write一致性模型。

# 部署(Docker)

Run Distributed MinIO on Docker Compose

要在Docker Compose上部署分布式MinIO,先下载docker-compose.yaml (opens new window)nginx.conf (opens new window)到你当前的工作目录。

  • docker-compose.yml
version: '3.7'

# Settings and configurations that are common for all containers
x-minio-common: &minio-common
  image: quay.io/minio/minio:RELEASE.2022-12-02T19-19-22Z
  command: server --console-address ":9001" http://minio{1...4}/data{1...2}
  expose:
    - "9000"
    - "9001"
  # environment:
    # MINIO_ROOT_USER: minioadmin
    # MINIO_ROOT_PASSWORD: minioadmin
  healthcheck:
    test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
    interval: 30s
    timeout: 20s
    retries: 3

# starts 4 docker containers running minio server instances.
# using nginx reverse proxy, load balancing, you can access
# it through port 9000.
services:
  minio1:
    <<: *minio-common
    hostname: minio1
    volumes:
      - data1-1:/data1
      - data1-2:/data2

  minio2:
    <<: *minio-common
    hostname: minio2
    volumes:
      - data2-1:/data1
      - data2-2:/data2

  minio3:
    <<: *minio-common
    hostname: minio3
    volumes:
      - data3-1:/data1
      - data3-2:/data2

  minio4:
    <<: *minio-common
    hostname: minio4
    volumes:
      - data4-1:/data1
      - data4-2:/data2

  nginx:
    image: nginx:1.19.2-alpine
    hostname: nginx
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - "9000:9000"
      - "9001:9001"
    depends_on:
      - minio1
      - minio2
      - minio3
      - minio4

## By default this config uses default local driver,
## For custom volumes replace with volume driver configuration.
volumes:
  data1-1:
  data1-2:
  data2-1:
  data2-2:
  data3-1:
  data3-2:
  data4-1:
  data4-2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
  • nginx.conf
user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  4096;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;
    sendfile        on;
    keepalive_timeout  65;

    # include /etc/nginx/conf.d/*.conf;

    upstream minio {
        server minio1:9000;
        server minio2:9000;
        server minio3:9000;
        server minio4:9000;
    }

    upstream console {
        ip_hash;
        server minio1:9001;
        server minio2:9001;
        server minio3:9001;
        server minio4:9001;
    }

    server {
        listen       9000;
        listen  [::]:9000;
        server_name  localhost;

        # To allow special characters in headers
        ignore_invalid_headers off;
        # Allow any size file to be uploaded.
        # Set to a value such as 1000m; to restrict file size to a specific value
        client_max_body_size 0;
        # To disable buffering
        proxy_buffering off;
        proxy_request_buffering off;

        location / {
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_connect_timeout 300;
            # Default is HTTP/1, keepalive is only enabled in HTTP/1.1
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            chunked_transfer_encoding off;

            proxy_pass http://minio;
        }
    }

    server {
        listen       9001;
        listen  [::]:9001;
        server_name  localhost;

        # To allow special characters in headers
        ignore_invalid_headers off;
        # Allow any size file to be uploaded.
        # Set to a value such as 1000m; to restrict file size to a specific value
        client_max_body_size 0;
        # To disable buffering
        proxy_buffering off;
        proxy_request_buffering off;

        location / {
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-NginX-Proxy true;

            # This is necessary to pass the correct IP to be hashed
            real_ip_header X-Real-IP;

            proxy_connect_timeout 300;
            
            # To support websocket
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            
            chunked_transfer_encoding off;

            proxy_pass http://console;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106

# Windows部署

  1. 下载地址:https://dl.min.io/server/minio/release/windows-amd64/minio.exe (opens new window)

  2. 进入到minio.exe所在的目录,使用如下命令启动minio服务。

    minio.exe server D:\minio\data
    
    1

    将D:\替换为希望 MinIO 存储数据的驱动器或目录的路径。

    如下图所示,minio服务已启动:

  3. 在浏览器输入:http://localhost:9000 (opens new window) ,进入minIO登录界面。

  4. 使用默认的RootUser和RootPass,进入MinIO控制台。

    MinIO 部署使用默认的 root 凭据RootUser和RootPass都为minioadmin

可以使用 MinIO 控制台测试部署是否成功,这是一个内置在 MinIO Server 中的嵌入式基于 Web 的对象浏览器。将主机上运行的 Web 浏览器指向http://127.0.0.1:9000,并使用 root 凭据登录。

可以使用该控制台创建存储桶、上传对象和浏览 MinIO 服务器的内容。

# Linux部署

创建目录

mkdir -p /minio/data
cd minio
1
2

下载并授权

wget https://dl.min.io/server/minio/release/linux-amd64/minio
chmod +x minio
1
2

设置账号密码(可选)

  • minio 默认账号密码为 minioadmin/minioadmin

  • 新增账号

    • 账号(最少3位)
    • 密码(最少8位)
    export MINIO_ACCESS_KEY=admin
    export MINIO_SECRET_KEY=12345678
    
    1
    2
  • 直接设置管理员账号密码

    编辑 /etc/profile 文件即可

    vim /etc/profile
    
    #===============================Minio=============================================
    # set minio environment
    export MINIO_ROOT_USER=admin
    export MINIO_ROOT_PASSWORD=admin123
    
    1
    2
    3
    4
    5
    6

启动

进入执行文件目录,自定义端口启动(默认端口:9000)

nohup /usr/local/minio/minio server --address :9001 --console-address :9002 /usr/local/minio/data >/usr/local/minio/minio.log 2>&1 &
1

说明:

  1. nohup 后台启动
  2. ./minio server 启动命令
  3. --address :9001 指定API端口
  4. --console-address :9002 指定控制台端口
  5. /usr/local/minio/data 指定存储目录
  6. >/usr/local/minio/minio.log 2>&1 控制台日志重定向到/usr/local/minio/minio.log文件中
  7. & 后台运行

设置开机自启动

新建shell脚本文件minio.sh,设置Minio服务器宕机后自动重启。

cd /etc/rc.d/init.d
vim minio.sh
1
2
#!/bin/bash
#chkconfig: 2345 10 90
#description: ping10
nohup /usr/local/minio/minio server --address :9001 --console-address :9002 /usr/local/minio/data >/usr/local/minio/minio.log 2>&1 &
# 给shell脚本赋权
chmod +x minio.sh
# 添加到开机自启动服务中
chkconfig --add minio.sh
# 设置开机自启动
chkconfig minio.sh on
# 查看是否添加成功
chkconfig --list
1
2
3
4
5
6
7
8
9
10
11
12

卸载

Minio卸载很简单,删除其目录即可。

# 四、MinIO客户端使用

MinIO Client 简称MC,为ls,cat,cp,mirror,diff,find等UNIX命令提供了一种替代方案。它支持文件系统和兼容Amazon S3的云存储服务。

# mc命令

命令 描述
ls 列出存储桶和对象
mb 创建存储桶
cat 合并对象
cp 拷贝对象
rm 删除对象
pipe Pipe到一个对象
share 共享
mirror 存储桶镜像
find 查找文件和对象
diff 比较存储桶差异
policy 给存储桶或前缀设置访问策略
config 管理配置文件
watch 事件监听
events 管理存储桶事件
update 管理软件更新
version 显示版本信息

# 五、SpringBoot整合MinIO

# MinIO Java Client使用

MinIO Java Client SDK提供简单的API来访问任何与Amazon S3兼容的对象存储服务。

Minio-java 官方demo (opens new window)

Minio Java Client API 官方文档 (opens new window)

Minio Java Client API 中文参考文档 (opens new window)

# Console设置

# 添加服务账户

  1. 可自定义,指定固定的服务账户和密钥。

  2. 下载保存好生成的json文件,因为密钥只展示一次,后续再无法进行查看。所以需要保留credentials.json进行备份记录。

    Write these down, as this is the only time the secret will be displayed.

    写下这些,因为这是唯一一次显示机密。

# 添加bucket

手动添加bucket。

后续通过代码可实现自动创建。但此处需要进行配置永久分享链接,设置其对应的策略,则进行手动添加bucket。

# 配置bucket策略

  1. 手动设置bucket的访问策略

    点击 BucketsManageAccess RulesAdd Access Rule

  2. 依次将所有Bucket都配置该访问策略。

# 代码实现

# 引入依赖pom.xml

<properties>
    <minio.version>8.3.1</minio.version>
    <okhttp.version>4.9.2</okhttp.version>
</properties>

<!-- ********  MinIO Begin  ********** -->
<!-- MinIO分布式文件存储 -->
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>${minio.version}</version>
    <exclusions>
        <exclusion>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!--  okhttp3 (轻量级Http Client)  -->
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <!-- 说明:此处指定版本号。
        由于在springboot的dependencyManagement中指定的是okhttp版本号为3.14.9,而minio需要使用okhttp>=4.8.1。
        所以需要明确指定minio需要的版本,若不指定则会默认依赖dependencyManagement中的低版本,造成启动报错。 -->
    <version>${okhttp.version}</version>
    <scope>provided</scope>
</dependency>
<!-- ********  MinIO End  ********** -->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# application.yml

base:
  config:
    minio:
      endpoint: http://minio.ccc.com:9000
      accessKey: WMQMV43NWZI8Z118WSF9
      secretKey: njKsw3sMptmbw0g6K5pCOoco7oBejTS6fKoLGybO
      chunkBucKet: ccc

spring:
  # 配置文件上传大小限制
  servlet:
    multipart:
      max-file-size: 20MB
      max-request-size: 20MB
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# MinioProperties.java

/**
 * Minio相关配置属性
 */

public class MinioProperties {
    /**
     * endPoint是一个URL,域名,IPv4或者IPv6地址
     */
    @Value("${base.config.minio.endpoint}")
    private String endpoint;
    /**
     * accessKey类似于用户ID,用于唯一标识你的账户
     */
    @Value("${base.config.minio.accessKey}")
    private String accessKey;
    /**
     * secretKey是你账户的密码
     */
    @Value("${base.config.minio.secretKey}")
    private String secretKey;
    /**
     * 默认存储桶名称
     */
    @Value("${base.config.minio.chunkBucKet:ccc}")
    private String chunkBucKet;

    /*  ********* getter/setter *********** */

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

构建MinioClient对象,并交给spring管理。

# MinioUploadResDTO.java

/**
 * 文件上传返回对象
 */
@Data
public class MinioUploadResDTO implements Serializable {
    private static final long serialVersionUID = 475040120689218785L;

    /**
     * 文件名,不是上传时候的文件名,是minio自动生成的;
     */
    private String minFileName;

    /**
     * 文件的下载url
     */
    private String minFileUrl;

    public MinioUploadResDTO(String minFileName, String minFileUrl) {
        this.minFileName = minFileName;
        this.minFileUrl = minFileUrl;
    }

    /*  ********* getter *********** */
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# MinioAutoConfiguration.java

/**
 * Minio分布式文件存储自动配置
 */

@Configuration
@ComponentScan({"com.ccc.core.minio"})
public class MinioAutoConfiguration {
    @Bean
    public MinioProperties minioProperties() {
        return new MinioProperties();
    }

    @Bean
    public MinioClient minioClient() {
        MinioProperties properties = minioProperties();

        return MinioClient.builder()
                .endpoint(properties.getEndpoint())
                .credentials(properties.getAccessKey(), properties.getSecretKey())
                .build();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# MinioUtils.java

/**
 * MinIO工具类
 */
@Component
public class MinioUtils {

    protected static final Logger logger = LoggerFactory.getLogger(MinioUtils.class);

    @Autowired
    private MinioProperties minioProperties;

    @Autowired
    private MinioClient minioClient;

    private static final String SEPARATOR_DOT = ".";

    private static final String SEPARATOR_ACROSS = "-";

    private static final String SEPARATOR_STR = "";

    /**
     * 不排序
     */
    public final static boolean NOT_SORT = false;

    /**
     * 排序
     */
    public final static boolean SORT = true;

    /**
     * 签名有效时间,默认过期时间(分钟)
     */
    private final static Integer DEFAULT_EXPIRY = 7 * 24 * 60;

    /**
     * 删除分片
     */
    public final static boolean DELETE_CHUNK_OBJECT = true;
    /**
     * 不删除分片
     */
    public final static boolean NOT_DELETE_CHUNK_OBJECT = false;


    /* *******************  桶bucket start  ******************** */
    /**
     * 判断bucket是否存在
     *
     * @param bucketName 存储bucket名称
     */
    public boolean bucketExists(String bucketName) {
        try {
            return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            logger.error("【捕获异常-minio操作】\r\n异常记录:", e);
            throw new RuntimeException("minio操作失败");
        }
    }

    /**
     * 创建存储桶bucket。检查bucket不存在则创建。
     * bucketName 桶名称,即顶层文件夹.
     * 长度在[3,63]的范围,需要满足^[a-z0-9][a-z0-9\\.\\-]+[a-z0-9]$
     *
     * @param bucketName 存储bucket名称
     */
    public void makeBucket(String bucketName) {
        try {
            if (!bucketExists(bucketName)) {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            }
        } catch (Exception e) {
            logger.error("【捕获异常-minio操作-创建桶】\r\n异常记录:", e);
            throw new RuntimeException("创建桶失败");
        }
    }

    /**
     * 删除存储bucket
     *
     * @param bucketName 存储bucket名称
     * @return Boolean
     */
    public Boolean removeBucket(String bucketName) {
        try {
            minioClient.removeBucket(RemoveBucketArgs.builder()
                    .bucket(bucketName)
                    .build());
            return true;
        } catch (Exception e) {
            logger.error("【捕获异常-minio操作-删除桶】\r\n异常记录:", e);
            throw new RuntimeException("删除桶失败");
        }
    }

    /**
     * 获取文件存储服务的所有存储桶名称
     */
    public List<String> listBucketNames() {
        List<Bucket> bucketList = listBuckets();
        List<String> bucketListName = new ArrayList<>();
        for (Bucket bucket : bucketList) {
            bucketListName.add(bucket.name());
        }
        return bucketListName;
    }

    /**
     * 列出所有存储桶
     */
    @SneakyThrows
    private List<Bucket> listBuckets() {
        return minioClient.listBuckets();
    }

    /**
     * 在桶下创建文件夹,文件夹层级结构根据参数决定
     *
     * @param bucketName 存储桶名称
     * @param dir        格式为 xxx/xxx/xxx/
     */
    @SneakyThrows
    public String createDirectory(String bucketName, String dir) {
        if (null == bucketName) {
            bucketName = minioProperties.getChunkBucKet();
        }
        if (!this.bucketExists(bucketName)) {
            return null;
        }
        minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(dir).stream(
                new ByteArrayInputStream(new byte[]{}), 0, -1)
                .build());
        return dir;
    }
    /* *******************  桶bucket end  ******************** */

    /* *******************  文件 start  ******************** */
    /**
     * 删除一个文件
     *
     * @param bucketName 存储桶名称
     * @param objectName 文件对象的名称(文件夹目录+文件名称)
     */
    @SneakyThrows
    public boolean removeObject(String bucketName, String objectName) {
        if (null == bucketName) {
            bucketName = minioProperties.getChunkBucKet();
        }
        if (!bucketExists(bucketName)) {
            return false;
        }
        minioClient.removeObject(
                RemoveObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .build());
        return true;
    }

    /**
     * 删除指定桶的多个文件对象, 返回删除错误的对象列表,全部删除成功,返回空列表
     *
     * @param bucketName  存储桶名称
     * @param objectNames 文件对象的名称集合(文件夹目录+文件名称)
     */
    @SneakyThrows
    public List<String> removeObjects(String bucketName, List<String> objectNames) {
        if (null == bucketName) {
            bucketName = minioProperties.getChunkBucKet();
        }
        if (!bucketExists(bucketName)) {
            return new ArrayList<>();
        }
        List<DeleteObject> deleteObjects = new ArrayList<>(objectNames.size());
        for (String objectName : objectNames) {
            deleteObjects.add(new DeleteObject(objectName));
        }
        List<String> deleteErrorNames = new ArrayList<>();
        Iterable<Result<DeleteError>> results = minioClient.removeObjects(
                RemoveObjectsArgs.builder()
                        .bucket(bucketName)
                        .objects(deleteObjects)
                        .build());
        for (Result<DeleteError> result : results) {
            DeleteError error = result.get();
            deleteErrorNames.add(error.objectName());
        }
        return deleteErrorNames;
    }

    /**
     * 获取对象文件名称列表
     *
     * @param bucketName 存储桶名称
     * @param prefix     对象名称前缀(文件夹 /xx/xx/xxx.jpg 中的 /xx/xx/)
     * @return objectNames
     */
    public List<String> listObjectNames(String bucketName, String prefix) {
        return listObjectNames(bucketName, prefix, NOT_SORT);
    }

    /**
     * 获取对象文件名称列表
     *
     * @param bucketName 存储桶名称
     * @param prefix     对象名称前缀(文件夹 /xx/xx/xxx.jpg 中的 /xx/xx/)
     * @param sort       是否排序(升序)
     * @return objectNames
     */
    @SneakyThrows
    public List<String> listObjectNames(String bucketName, String prefix, Boolean sort) {

        boolean flag = bucketExists(bucketName);
        if (flag) {

            ListObjectsArgs listObjectsArgs;
            if (null == prefix) {
                listObjectsArgs = ListObjectsArgs.builder()
                        .bucket(bucketName)
                        .recursive(true)
                        .build();
            } else {
                listObjectsArgs = ListObjectsArgs.builder()
                        .bucket(bucketName)
                        .prefix(prefix)
                        .recursive(true)
                        .build();
            }
            Iterable<Result<Item>> chunks = minioClient.listObjects(listObjectsArgs);
            List<String> chunkPaths = new ArrayList<>();
            for (Result<Item> item : chunks) {
                chunkPaths.add(item.get().objectName());
            }
            if (sort) {
                chunkPaths.sort(new Str2IntComparator(false));
            }
            return chunkPaths;
        }
        return new ArrayList<>();
    }
    /* *******************  文件 end  ******************** */

    /* *******************  URL start  ******************** */
    /**
     * 获取访问对象的外链地址。(即生成一个GET请求的分享链接)
     * 失效时间默认是7天。
     *
     * @param bucketName 存储桶名称
     * @param objectName 存储桶里的对象名称
     * @return 获取文件的下载url
     */
    @SneakyThrows
    public String getPresignedObjectUrl(String bucketName, String objectName) {
        return minioClient.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .method(Method.GET)
                        .bucket(bucketName)
                        .object(objectName)
                        .build()
        );
    }

    /**
     * 获取访问对象的外链地址。(即生成一个GET请求的分享链接)
     *
     * @param bucketName 存储桶名称
     * @param objectName 存储桶里的对象名称
     * @param expiry     过期时间(分钟) 最大为7天 超过7天则默认最大值
     * @return 获取文件的下载url
     */
    @SneakyThrows
    public String getPresignedObjectUrl(String bucketName, String objectName, Integer expiry) {
        // 将分钟数转换为秒数
        expiry = expiryHandle(expiry);
        return minioClient.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .method(Method.GET)
                        .bucket(bucketName)
                        .object(objectName)
                        .expiry(expiry)
                        .build()
        );
    }

    /**
     * 获取访问对象的外链地址,不带任何限定参数。(即生成一个GET请求的分享链接)
     *
     * @param bucketName 存储桶名称
     * @param objectName 存储桶里的对象名称
     * @return 获取文件的下载url
     */
    @SneakyThrows
    public String getCleanObjectUrl(String bucketName, String objectName) {
        String endpoint = minioProperties.getEndpoint();
        HttpUrl url = HttpUrl.parse(endpoint);
        if (url == null) {
            url = new HttpUrl.Builder().scheme("https").host(endpoint).build();
        }
        HttpUrl.Builder urlBuilder = url.newBuilder();
        urlBuilder.addEncodedPathSegment(MyS3Escaper.encode(bucketName));
        urlBuilder.addEncodedPathSegments(MyS3Escaper.encodePath(objectName));
        return urlBuilder.toString();
    }

    /**
     * 创建上传文件对象的外链
     *
     * @param bucketName 存储桶名称
     * @param objectName 欲上传文件对象的名称
     * @return uploadUrl
     */
    public String createUploadUrl(String bucketName, String objectName) {
        return createUploadUrl(bucketName, objectName, DEFAULT_EXPIRY);
    }

    /**
     * 创建上传文件对象的外链
     *
     * @param bucketName 存储桶名称
     * @param objectName 欲上传文件对象的名称
     * @param expiry     过期时间(分钟) 最大为7天 超过7天则默认最大值
     * @return uploadUrl
     */
    @SneakyThrows
    public String createUploadUrl(String bucketName, String objectName, Integer expiry) {
        expiry = expiryHandle(expiry);
        return minioClient.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .method(Method.PUT)
                        .bucket(bucketName)
                        .object(objectName)
                        .expiry(expiry)
                        .build()
        );
    }
    /* *******************  URL end  ******************** */

    /* *******************  上传 start  ******************** */
    /**
     * 文件上传
     *
     * @param multipartFile 文件
     * @param bucketName    桶名称
     * @return {@link MinioUploadResponseDTO}
     */
    public MinioUploadResponseDTO upload(MultipartFile multipartFile, String bucketName) throws Exception {
        return upload(multipartFile, bucketName, null);
    }

    /**
     * 文件上传
     *
     * @param multipartFile 文件
     * @param bucketName    桶名称
     * @param directory     文件夹目录(文件夹 /xx/xx/xxx.jpg 中的 /xx/xx/)
     * @return {@link MinioUploadResponseDTO}
     */
    public MinioUploadResponseDTO upload(MultipartFile multipartFile, String bucketName, String directory) throws Exception {
        String fileName = doUpload(multipartFile, bucketName, directory);
        // 返回生成文件名、访问路径
        return new MinioUploadResponseDTO(fileName, getCleanObjectUrl(bucketName, fileName));
    }

    /**
     * 文件上传(设置过期时间)
     *
     * @param multipartFile 文件
     * @param bucketName    桶名称
     * @param directory     文件夹目录(文件夹 /xx/xx/xxx.jpg 中的 /xx/xx/)
     * @param expiryMinutes 过期时间(分钟) [最大为7天 超过7天则默认最大值]
     * @return {@link MinioUploadResponseDTO}
     */
    public MinioUploadResponseDTO upload(MultipartFile multipartFile, String bucketName, String directory, int expiryMinutes) throws Exception {
        String fileName = doUpload(multipartFile, bucketName, directory);
        // 返回生成文件名、访问路径
        return new MinioUploadResponseDTO(fileName, getPresignedObjectUrl(bucketName, fileName, expiryMinutes));
    }

    private String doUpload(MultipartFile multipartFile, String bucketName, String directory) throws Exception {
        if (null == bucketName) {
            bucketName = minioProperties.getChunkBucKet();
        }
        if (!this.bucketExists(bucketName)) {
            this.makeBucket(bucketName);
        }
        InputStream inputStream = multipartFile.getInputStream();
        directory = Optional.ofNullable(directory).orElse("");

        String suffix = "";
        String originalFileName = Objects.requireNonNull(multipartFile.getOriginalFilename());
        if (originalFileName.contains(SEPARATOR_DOT)) {
            suffix = originalFileName.substring(originalFileName.lastIndexOf(SEPARATOR_DOT));
        }
        String fileName = directory + minFileName(suffix);

        //上传文件到指定目录
        minioClient.putObject(PutObjectArgs.builder()
                .bucket(bucketName)
                .object(fileName)
                .contentType(multipartFile.getContentType())
                .stream(inputStream, inputStream.available(), -1)
                .build());
        inputStream.close();
        return fileName;
    }

    /**
     * 文件上传
     *
     * @param bucketName  桶名称
     * @param directory   文件夹目录(文件夹 /xx/xx/xxx.jpg 中的 /xx/xx/)
     * @param inputStream input stream of bytes
     * @param suffix      后缀名
     * @param contentType 文件类型
     * @return {@link MinioUploadResponseDTO}
     */
    public MinioUploadResponseDTO upload(String bucketName, String directory, InputStream inputStream, String suffix, String contentType) throws Exception {
        String fileName = doUpload(bucketName, directory, inputStream, suffix, contentType);
        // 返回生成文件名、访问路径
        return new MinioUploadResponseDTO(fileName, getCleanObjectUrl(bucketName, fileName));
    }

    /**
     * 文件上传(设置过期时间)
     *
     * @param bucketName    桶名称
     * @param directory     文件夹目录(文件夹 /xx/xx/xxx.jpg 中的 /xx/xx/)
     * @param inputStream   input stream of bytes
     * @param suffix        后缀名
     * @param contentType   文件类型
     * @param expiryMinutes 过期时间(分钟) [最大为7天 超过7天则默认最大值]
     * @return {@link MinioUploadResponseDTO}
     */
    public MinioUploadResponseDTO upload(String bucketName, String directory, InputStream inputStream, String suffix, String contentType, int expiryMinutes) throws Exception {
        String fileName = doUpload(bucketName, directory, inputStream, suffix, contentType);
        // 返回生成文件名、访问路径
        return new MinioUploadResponseDTO(fileName, getPresignedObjectUrl(bucketName, fileName, expiryMinutes));
    }

    private String doUpload(String bucketName, String directory, InputStream inputStream, String suffix, String contentType) throws Exception {
        if (null == bucketName) {
            bucketName = minioProperties.getChunkBucKet();
        }
        if (!this.bucketExists(bucketName)) {
            this.makeBucket(bucketName);
        }
        directory = Optional.ofNullable(directory).orElse("");
        String fileName = directory + minFileName(suffix);

        //上传文件到指定目录
        minioClient.putObject(PutObjectArgs.builder()
                .bucket(bucketName)
                .object(fileName)
                .contentType(contentType)
                .stream(inputStream, inputStream.available(), -1)
                .build());
        inputStream.close();
        return fileName;
    }
    /* *******************  上传 end  ******************** */

    /* *******************  下载 start  ******************** */
    /**
     * 下载文件
     *
     * @param response
     * @param bucketName  存储桶名称
     * @param minFileName 文件名
     * @throws Exception
     */
    public void download(HttpServletResponse response, String bucketName, String minFileName) throws Exception {
        if (null == bucketName) {
            bucketName = minioProperties.getChunkBucKet();
        }
        InputStream fileInputStream = minioClient.getObject(GetObjectArgs.builder()
                .bucket(bucketName)
                .object(minFileName).build());
        response.setHeader("Content-Disposition", "attachment;filename=" + minFileName);
        response.setContentType("application/force-download");
        response.setCharacterEncoding("UTF-8");
        IOUtils.copy(fileInputStream, response.getOutputStream());
    }

    /**
     * 批量下载
     *
     * @param bucketName 存储桶名称
     * @param directory  文件夹目录
     * @return
     */
    @SneakyThrows
    public List<String> downLoadBatch(String bucketName, String directory) {
        Iterable<Result<Item>> objs = minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).prefix(directory).useUrlEncodingType(false).build());
        List<String> list = new ArrayList<>();
        for (Result<Item> result : objs) {
            String objectName = null;
            objectName = result.get().objectName();
            StatObjectResponse statObject = minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
            if (statObject != null && statObject.size() > 0) {
                String fileurl = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().bucket(bucketName).object(statObject.object()).method(Method.GET).build());
                list.add(fileurl);
            }
        }
        return list;
    }
    /* *******************  下载 end  ******************** */

    /* *******************  分片 start  ******************** */
    /**
     * 批量创建分片上传外链
     *
     * @param bucketName 存储桶名称
     * @param objectMD5  欲上传分片文件主文件的MD5
     * @param chunkCount 分片数量
     * @return uploadChunkUrls
     */
    public List<String> createUploadChunkUrlList(String bucketName, String objectMD5, Integer chunkCount) {
        if (null == bucketName) {
            bucketName = minioProperties.getChunkBucKet();
        }
        if (null == objectMD5) {
            return null;
        }
        objectMD5 += "/";
        if (null == chunkCount || 0 == chunkCount) {
            return null;
        }
        List<String> urlList = new ArrayList<>(chunkCount);
        for (int i = 1; i <= chunkCount; i++) {
            String objectName = objectMD5 + i + ".chunk";
            urlList.add(createUploadUrl(bucketName, objectName, DEFAULT_EXPIRY));
        }
        return urlList;
    }

    /**
     * 创建指定序号的分片文件上传外链
     *
     * @param bucketName 存储桶名称
     * @param objectMD5  欲上传分片文件主文件的MD5
     * @param partNumber 分片序号
     * @return uploadChunkUrl
     */
    public String createUploadChunkUrl(String bucketName, String objectMD5, Integer partNumber) {
        if (null == bucketName) {
            bucketName = minioProperties.getChunkBucKet();
        }
        if (null == objectMD5) {
            return null;
        }
        objectMD5 += "/" + partNumber + ".chunk";
        return createUploadUrl(bucketName, objectMD5, DEFAULT_EXPIRY);
    }

    /**
     * 获取分片文件名称列表
     *
     * @param bucketName 存储桶名称
     * @param ObjectMd5  对象Md5
     * @return objectChunkNames
     */
    public List<String> listChunkObjectNames(String bucketName, String ObjectMd5) {
        if (null == bucketName) {
            bucketName = minioProperties.getChunkBucKet();
        }
        if (null == ObjectMd5) {
            return null;
        }
        return listObjectNames(bucketName, ObjectMd5, SORT);
    }

    /**
     * 获取分片名称地址HashMap key=分片序号 value=分片文件地址
     *
     * @param bucketName 存储桶名称
     * @param ObjectMd5  对象Md5
     * @return objectChunkNameMap
     */
    public Map<Integer, String> mapChunkObjectNames(String bucketName, String ObjectMd5) {
        if (null == bucketName) {
            bucketName = minioProperties.getChunkBucKet();
        }
        if (null == ObjectMd5) {
            return null;
        }
        List<String> chunkPaths = listObjectNames(bucketName, ObjectMd5);
        if (null == chunkPaths || chunkPaths.size() == 0) {
            return null;
        }
        Map<Integer, String> chunkMap = new HashMap<>(chunkPaths.size());
        for (String chunkName : chunkPaths) {
            Integer partNumber = Integer.parseInt(chunkName.substring(chunkName.indexOf("/") + 1, chunkName.lastIndexOf(".")));
            chunkMap.put(partNumber, chunkName);
        }
        return chunkMap;
    }

    /**
     * 合并分片文件成对象文件
     *
     * @param chunkBucKetName   分片文件所在存储桶名称
     * @param composeBucketName 合并后的对象文件存储的存储桶名称
     * @param chunkNames        分片文件名称集合
     * @param objectName        合并后的对象文件名称
     * @return true/false
     */
    @SneakyThrows
    public boolean composeObject(String chunkBucKetName, String composeBucketName, List<String> chunkNames, String objectName, boolean isDeleteChunkObject) {
        if (null == chunkBucKetName) {
            chunkBucKetName = minioProperties.getChunkBucKet();
        }
        List<ComposeSource> sourceObjectList = new ArrayList<>(chunkNames.size());
        for (String chunk : chunkNames) {
            sourceObjectList.add(
                    ComposeSource.builder()
                            .bucket(chunkBucKetName)
                            .object(chunk)
                            .build()
            );
        }
        minioClient.composeObject(
                ComposeObjectArgs.builder()
                        .bucket(composeBucketName)
                        .object(objectName)
                        .sources(sourceObjectList)
                        .build()
        );
        if (isDeleteChunkObject) {
            removeObjects(chunkBucKetName, chunkNames);
        }
        return true;
    }

    /**
     * 合并分片文件成对象文件
     *
     * @param bucketName 存储桶名称
     * @param chunkNames 分片文件名称集合
     * @param objectName 合并后的对象文件名称
     * @return true/false
     */
    public boolean composeObject(String bucketName, List<String> chunkNames, String objectName) {
        return composeObject(minioProperties.getChunkBucKet(), bucketName, chunkNames, objectName, NOT_DELETE_CHUNK_OBJECT);
    }

    /**
     * 合并分片文件成对象文件
     *
     * @param bucketName 存储桶名称
     * @param chunkNames 分片文件名称集合
     * @param objectName 合并后的对象文件名称
     * @return true/false
     */
    public boolean composeObject(String bucketName, List<String> chunkNames, String objectName, boolean isDeleteChunkObject) {
        return composeObject(minioProperties.getChunkBucKet(), bucketName, chunkNames, objectName, isDeleteChunkObject);
    }

    /**
     * 合并分片文件,合并成功后删除分片文件
     *
     * @param bucketName 存储桶名称
     * @param chunkNames 分片文件名称集合
     * @param objectName 合并后的对象文件名称
     * @return true/false
     */
    public boolean composeObjectAndRemoveChunk(String bucketName, List<String> chunkNames, String objectName) {
        return composeObject(minioProperties.getChunkBucKet(), bucketName, chunkNames, objectName, DELETE_CHUNK_OBJECT);
    }
    /* *******************  分片 end  ******************** */

    /**
     * 生成上传文件名
     *
     * @param suffix 扩展名
     */
    private String minFileName(String suffix) {
        return UUID.randomUUID().toString().replace(SEPARATOR_ACROSS, SEPARATOR_STR).toUpperCase() + suffix;
    }

    /**
     * 将分钟数转换为秒数
     *
     * @param expiry 过期时间(分钟数)
     * @return expiry
     */
    private static int expiryHandle(Integer expiry) {
        expiry = expiry * 60;
        if (expiry > 604800) {
            return 604800;
        }
        return expiry;
    }

    static class Str2IntComparator implements Comparator<String> {
        private final boolean reverseOrder; // 是否倒序

        public Str2IntComparator(boolean reverseOrder) {
            this.reverseOrder = reverseOrder;
        }

        @Override
        public int compare(String arg0, String arg1) {
            Integer intArg0 = Integer.parseInt(arg0.substring(arg0.indexOf("/") + 1, arg0.lastIndexOf(".")));
            Integer intArg1 = Integer.parseInt(arg1.substring(arg1.indexOf("/") + 1, arg1.lastIndexOf(".")));
            if (reverseOrder) {
                return intArg1 - intArg0;
            } else {
                return intArg0 - intArg1;
            }
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714

# 接口测试

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

    @Autowired
    private MinioUtils minioUtils;

    // 存储桶名称
    private static final String MINIO_BUCKET = "ccc";

    @PostMapping("/upload")
    public Result upload(@RequestParam(value = "files") MultipartFile files){
        try {
            return Result.ok(minioUtils.upload(files,MINIO_BUCKET,null));
        } catch (Exception e) {
            return Result.error(e.getMessage());
        }
    }

    @GetMapping("/download")
    public void download(@RequestParam("minFileName")String minFileName,HttpServletResponse response){
            minioUtils.download(response,"img",minFileName);
    }
    
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 六、Minio开发相关问题

# 1、okhttp版本依赖问题

问题描述

springboot项目集成minio时okhttp版本依赖问题。

问题背景

在基于Spring Boot 2.5.5的项目中引入minio 8.3.1的时候,启动项目报okhttp中的方法不存在。

错误信息中提示的okhttp版本号为3.14.9,而minio中的依赖版本为4.8.1

排查思路

看到报错,第一反应是jar包冲突了,于是在idea中打开maven的依赖树,在里面搜索minio,未发现有其它地方引用了。

通过show Effective POM 查看到的版本号却是3.14.9。

但是没说为何这样操作就可以了,而且为何会提示okhttp版本号为3.14.9。

在好奇心的驱使下,我开始了原因探索之旅,顺着网上的思路,有说minio这个版本的包有问题,也有说是maven的问题。maven去构建实际依赖的时候,是根据项目和依赖的jar包的pom文件进行构建的。

# 原因

spring-boot-dependencies中有用到okhttp。根据这条线索,我找到了这个版本号的出处。那么剩下的就是分析为什么会用这个版本号了。

罪魁祸首出在了dependencyManagement这个标签上。它是用来管理jar包版本的,当子项目有依赖相关jar包时,如果是间接依赖或直接依赖时没有指定版本号时,会使用其规定的版本号。

但是在不会引入相关依赖jar包时,此标签里面规定的jar包也不会被引入。而Spring boot项目的父级pom都是spring boot管理的。于是,就出现了上面的灵异事件。

因为dependencyManagement是用来管理jar包版本,如果后面的jar包没有申明版本,会以这里面的版本为主,此处并不会引入jar包,一般是在父级pom文件申明。

我们的项目也使用了dependencyManagement进行了版本管理时,也会遵循同样的规则,而且其优先级似乎比父级pom的优先级要高。所以默认就会加载3.14.9。

# 解决

解决方案都是排除minio中的依赖,再在项目里面重新引入okhttp依赖。

<properties>
    <minio.version>8.3.1</minio.version>
    <okhttp.version>4.9.2</okhttp.version>
</properties>

<!-- ********  MinIO Begin  ********** -->
<!-- MinIO分布式文件存储 -->
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>${minio.version}</version>
    <exclusions>
        <exclusion>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!--  okhttp3 (轻量级Http Client)  -->
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <!-- 说明:此处指定版本号。
        由于在springboot的dependencyManagement中指定的是okhttp版本号为3.14.9,而minio需要使用okhttp>=4.8.1。
        所以需要明确指定minio需要的版本,若不指定则会默认依赖dependencyManagement中的低版本,造成启动报错。 -->
    <version>${okhttp.version}</version>
    <scope>provided</scope>
</dependency>
<!-- ********  MinIO End  ********** -->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 2、MinIO设置永久分享链接

问题描述

MinIO 可以被当做一个轻量级的云盘或文件数据库,默认存储在桶里的文件只能分享7天,但是当我想把它当做文件数据库时,就需要将文件分享设置为永久。

解决思路

第一步:设置对应的bucket 可通过路径直接访问。

第二步:调用JAVA API时,生成的URL不带任何参数,直接用IP:端口/bucket/文件名访问即可。

实现

  • 服务端设置Bucket访问策略

存储空间(Bucket)默认策略有如下几种:

  1. Read Only - download
  2. Write Only - upload
  3. Read and Write - public
  4. 不设置 - none

从管理界面新建一个存储空间,然后查看策略,默认是为空的。

  1. 手动设置bucket的访问策略

    点击 BucketsManageAccess RulesAdd Access Rule

  1. 依次将所有Bucket都配置该访问策略。
  • 代码层面实现

参考minio源码(minioClient.getPresignedObjectUrl)生成的带参数的URL链接,自定义组装不带任何限定参数的分享链接。

  1. MinioUtils.java
/**
 * 获取访问对象的外链地址,不带任何限定参数。(即生成一个GET请求的分享链接)
 *
 * @param bucketName 存储桶名称
 * @param objectName 存储桶里的对象名称
 * @return 获取文件的下载url
 */
@SneakyThrows
public String getCleanObjectUrl(String bucketName, String objectName) {
    String endpoint = minioProperties.getEndpoint();
    HttpUrl url = HttpUrl.parse(endpoint);
    if (url == null) {
        url = new HttpUrl.Builder().scheme("https").host(endpoint).build();
    }
    HttpUrl.Builder urlBuilder = url.newBuilder();
    urlBuilder.addEncodedPathSegment(MyS3Escaper.encode(bucketName));
    urlBuilder.addEncodedPathSegments(MyS3Escaper.encodePath(objectName));
    return urlBuilder.toString();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  1. MyS3Escaper.java

    将Minio源码的该文件,拷贝至自己项目。

# 3、MinIO内网映射问题

问题描述

当项目有网络限制,只暴露一个前端项目的端口,其余端口都需在内网进行使用的情况下,MinIO则需要进行内网映射访问。

解决思路

第一步:获取到外网访问的前端项目地址IP+端口。

第二步:重新设置前端项目访问MinIO的地址,追加前缀路径,用于Nginx转发内网映射。

第三步:配置Nginx映射。

实现

1、后端JAVA程序配置文件中,配置前端访问的ossUrl地址:

base:
    config:
        minio:
            url: http://外网IP:端口/oss
1
2
3
4

2、前端VUE项目正常使用,通过拼接后端返回的url+文件路径;

3、Nginx配置转发代理映射:

location /oss/ {
	proxy_pass http://内网IP:9000/;
	proxy_set_header Host $http_host;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header REMOTE-HOST $remote_addr;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Credentials' 'true';
    add_header 'Access-Control-Allow-Methods' 'GET, PUT, POST, DELETE, OPTIONS';
    add_header 'Access-Control-Allow-Headers' 'Content-Type,*';
}
1
2
3
4
5
6
7
8
9
10
11

# 七、MinIO相关资料

MinIO官网 (opens new window)

MinIO中文官网 (opens new window)

MinIO官方文档 (opens new window)

Minio-java 官方demo - GitHub (opens new window)

Minio Java Client API 官方文档 (opens new window)

Minio Java Client API 中文参考文档 (opens new window)

官方demo链接 (opens new window)