MinIO的配置

前言

MinIO 是一款高性能、分布式的对象存储系统,常用于存储非结构化数据(如图片、视频、文档等)。它可以100%的运行在标准硬件。即X86等低成本机器也能够很好的运行MinIO。

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

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

MinIO与传统的存储和其他的对象存储不同的是:它一开始就针对性能要求更高的私有云标准进行软件架构设计。因为MinIO一开始就只为对象存储而设计。所以他采用了更易用的方式进行设计,它能实现对象存储所需要的全部功能,在性能上也更加强劲,它不会为了更多的业务功能而妥协,失去MinIO的易用性、高效性。 这样的结果所带来的好处是:它能够更简单的实现局有弹性伸缩能力的原生对象存储服务。

MinIO在传统对象存储用例(例如辅助存储,灾难恢复和归档)方面表现出色。同时,它在机器学习、大数据、私有云、混合云等方面的存储技术上也独树一帜。当然,也不排除数据分析、高性能应用负载、原生云的支持。

MinIO主要采用Golang语言实现,,客户端与存储服务器之间采用http/https通信协议。

它与 Amazon S3 云存储服务 API 兼容

MinIO的相关信息

中文官网: http://www.minio.org.cn/ 中文文档: http://docs.minio.org.cn/docs/ 中文下载地址:https://min.io/download?platform=windows 英文官网: https://min.io/ 英文文档: https://docs.min.io/ 英文下载地址:https://min.io/download#/linux Github地址:https://github.com/minio/minio/releases/tag/RELEASE.2025-06-13T11-33-47Z

准备工作

我们先找到自己需要放的位置,比如我的就是放在D盘,下面新建立了一个文件夹Minio:

image-20250707145359058

注意,bindatalogs这三个文件夹是需要手动创建的

其中

  • 应用目录bin,存放mc.exe 以及minio.exe 文件 。
  • minio的数据目录data,存放相关数据文件
  • minio的数据目录log,存储相关日志

以上目录名称可以随便命名,不建议中文

下载所需文件

其中,minio.exe 是用于接收文件信息的服务端程序

mac.exe 用于上传文件的客户端文件,如果用程序代码操作文件存储,只启动服务端就ok

image-20250707145252565

启动服务

当然我们有两种方式启动它:

  • 命令启动
  • 新建.bat文件启动

命令启动

命令启动就是使用cmd,首先我们需要设置登录的用户名和密码

1
set MINIO_ROOT_USER=root

其中,后面的等于号就是你的用户名

1
set MINIO_ROOT_PASSWORD=10086

其中,后面的等于号就是你的密码

然后输入指令,启动 minio 的服务

1
.\minio.exe server D:\OtherLanguageSetting\Minio\data --console-address "127.0.0.1:9000" --address "127.0.0.1:9005"

其中,server <数据存储路径>--console-address "127.0.0.1:<控制台端口>--address "127.0.0.1:<服务端口>,默认控制台端口是9000,服务端端口是9005

访问客户端地址:http://127.0.0.1:9000

然后你就进入了这个登录页面

image-20250707150224453

登陆进去之后,会有如下页面

minio 服务用到了 9000 和 9001 端口,如果使用的是云服务器,需要在安全组中开放端口

打开浏览器,访问 域名:9001 或者 IP:9001 ,即可访问 MinIO 服务的管理系统,使用设置好的用户名和密码,登录系统

image-20250707150206105

其中,左侧导航栏大约都是这些内容

  • Create Bucket(创建存储桶)
    • 作用:存储桶(Bucket)是 MinIO 中用于组织对象(Object,可理解为文件)的顶层容器,类似文件系统中的文件夹。点击这个 “+” 按钮,可以创建新的存储桶,后续可在该存储桶中上传、管理对象。
  • Documentation(文档)
    • 作用:点击后会跳转到 MinIO 的官方文档页面,里面包含了 MinIO 的安装、配置、API 使用等详细说明,方便用户查阅相关技术资料。
  • License(许可证)
    • 作用:点击可查看 MinIO 所遵循的许可证信息,了解其开源协议等相关法律授权内容。

在左侧的主要内容区域中,表明当前处于对象浏览的功能模块,用于查看和管理存储桶及其中的对象。

其中,介绍了存储桶的作用,即 MinIO 使用存储桶来组织对象,存储桶类似于文件系统中的文件夹或目录,每个存储桶可以容纳任意数量的对象。并提示用户可以通过 “Create a Bucket”(创建一个存储桶)开始使用

整体来说,这个界面是 MinIO 用于管理对象存储的入口,通过它可以创建存储桶,进而在存储桶中上传、下载、删除等操作对象,实现对非结构化数据的管理。

新建 bat 脚本启动minio

确定 MinIO 可执行文件的路径以及你要用于存储数据的目录路径。

假设 MinIO 可执行文件 minio.exe 存放在 D:\MinIO\bin 目录下,数据存储目录为 D:\MinIO\data ,管理控制台端口为 9001 ,服务端口为 9000

新建 .bat 文件,输入如下

1
2
3
4
5
6
7
@echo off
rem 设置MinIO的根用户和密码
set MINIO_ROOT_USER=your_root_user_name
set MINIO_ROOT_PASSWORD=your_root_password
rem 启动MinIO服务,指定数据存储路径、控制台地址和服务地址
D:\MinIO\minio.exe server D:\MinIO\data --console-address "127.0.0.1:9001" --address "127.0.0.1:9000"
pause

请将上述内容中的 your_root_user_name 替换为你想要设置的 MinIO 根用户名,your_root_password 替换为对应的密码。同时,根据实际情况修改 MinIO 可执行文件路径和数据存储路径,以及控制台地址和服务地址。

找到保存好的 .bat 文件,双击它,批处理文件会按照你编写的命令设置环境变量并启动 MinIO 服务。启动成功后,你可以通过浏览器访问指定的控制台地址(如 http://127.0.0.1:9001 ),使用设置好的用户名和密码登录 MinIO 管理界面。

minio 服务用到了 9000 和 9001 端口,如果使用的是云服务器,需要在安全组中开放端口

关于MinIO

MinIO 的优点

  1. 高性能:MinIO 针对海量小文件存储和高并发访问进行了优化,能够提供低延迟、高吞吐量的服务。在处理大量的图片、视频等非结构化数据时,它可以快速地完成上传和下载操作,满足诸如电商产品图片展示、视频平台内容存储等对性能要求较高的应用场景。
  2. 轻量级与易部署:MinIO 是一个单一的二进制文件,部署非常简单,不依赖复杂的安装过程和额外的软件包。无论是在本地开发环境、测试环境,还是生产环境的物理机、虚拟机、容器(如 Docker)中,都能快速启动并运行。
  3. 兼容性好:MinIO 兼容 Amazon S3 接口,这使得原本基于 S3 开发的应用程序可以很方便地迁移到 MinIO 上,无需对代码进行大规模修改。同时,众多支持 S3 协议的工具,也可以直接用于 MinIO 的管理和数据操作,极大地降低了开发和运维成本。
  4. 分布式存储:MinIO 支持分布式部署,可以将多个节点组成一个集群,实现数据的分布式存储和处理。通过这种方式,不仅可以扩展存储容量,还能提升系统的可用性和容错能力。即使部分节点出现故障,也不会影响整个系统的正常运行,数据依然可以被访问和写入。
  5. 数据安全:支持数据加密,包括静态数据加密(存储在磁盘上的数据被加密)和传输中数据加密(数据在网络传输过程中被加密),有效保护数据的安全性和隐私性。此外,它还具备访问控制功能,能够精确控制用户对存储桶和对象的访问权限。

MinIO 相关基础概念

  1. 对象(Object):MinIO 中存储的基本单元,可理解为一个文件,比如一张图片、一个视频文件、一份文档等。每个对象都有一个唯一的标识符,通过该标识符可以对对象进行访问和操作。
  2. 存储桶(Bucket):用来存储 Object 的逻辑空间。是组织对象的容器,类似于文件系统中的文件夹。每个 Bucket 之间的数据是相互隔离的。一个存储桶可以包含任意数量的对象,并且可以为每个存储桶设置不同的访问策略,例如公开访问、私有访问等。
  3. 数据盘Drive:即存储数据的磁盘,在 MinIO 启动时,以参数的方式传入。Minio 中所有的对象数据都会存储在 Drive 里。
  4. 集合Set:即一组 Drive 的集合,分布式部署根据集群规模自动划分一个或多个 Set ,每个 Set 中的Drive 分布在不同位置。一个对象存储在一个 Set 上。
    1. 一个对象存储在一个Set上
    2. 一个集群划分为多个Set
    3. 一个Set包含的Drive数量是固定的,默认由系统根据集群规模自动计算得出
    4. 一个SET中的Drive尽可能分布在不同的节点上
  5. 端点(Endpoint):是 MinIO 服务的网络地址,客户端通过端点来连接 MinIO 服务器。端点包含了服务器的 IP 地址和端口号,如 http://127.0.0.1:9000
  6. 访问密钥(Access Key)和秘密密钥(Secret Key):用于身份验证的凭据。客户端在与 MinIO 进行交互时,需要提供正确的访问密钥和秘密密钥,以证明其有权限访问 MinIO 服务。

纠删码 EC(Erasure Coding)

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

纠删码是一种数据冗余和恢复技术,在 MinIO 中起到重要作用:

  • 工作原理:纠删码将原始数据分割成多个数据块,并通过特定的算法生成一些冗余数据块。例如,将数据分成 n 个数据块,然后再生成 m 个冗余块。在恢复数据时,只要获取到其中一定数量(通常小于 n + m )的数据块,就可以还原出原始数据。
  • 优点:相比传统的冗余方式(如镜像复制),纠删码在提供数据冗余保护的同时,能更高效地利用存储空间。假设使用镜像复制,存储一份数据需要两倍的存储空间;而使用纠删码,在保证一定冗余度和数据恢复能力的情况下,所需的额外存储空间相对较少。此外,它还能提升数据的可用性和容错能力,当部分数据块损坏或丢失时,依然可以恢复出完整的数据。
  • 应用场景:在 MinIO 的分布式存储集群中,纠删码被广泛应用。比如在一个有多个存储节点的集群里,数据以纠删码的形式分散存储在不同节点上。如果个别节点出现故障,导致部分数据块丢失,通过剩余节点上的数据块,依然能够恢复出原始数据,保证了数据的完整性和服务的连续性。

关于纠删码,简单来说就是可以通过数学计算,把丢失的数据进行还原,它可以将n份原始数据,增加m份数据,并能通过n+m份中的任意n份数据,还原为原始数据。即如果有任意小于等于m份的数据失效,仍然能通过剩下的数据还原出来。

存储形式

文件对象上传到 MinIO ,会在对应的数据存储磁盘中,以 Bucket 名称为目录,文件名称为下一级目录,文件名下是 part.1 和 xl.meta(老版本,最新版本如下图),前者是编码数据块及检验块,后者是元数据文件。

类似于 linux 在指定目录下执行 tree 命令

详细分为单机存储和分布式存储

  • 单机存储
    • 基本原理:MinIO 可以以单机模式部署在一台物理机或者虚拟机上。在单机存储中,MinIO 将数据存储在本地磁盘上,通过管理本地磁盘的存储空间来存放对象数据。例如,你在本地开发环境中部署 MinIO 用于测试,它会将数据存储在指定的本地目录(在启动 MinIO 时可以配置存储目录)。
    • 适用场景:适合用于开发测试阶段,方便开发者快速搭建环境进行功能测试,或者在数据量较小、对数据冗余和高可用要求不高的简单场景中使用,比如个人博客的图片存储等。
  • 分布式存储
    • 基本原理:MinIO 的分布式存储是将多个存储节点组成一个集群,这些节点可以分布在不同的物理位置,通过网络进行通信。数据会被分割成多个数据块,然后按照纠删码(Erasure Coding)算法生成冗余数据块,这些数据块会被分散存储到集群中的各个节点上。例如,一个由 4 个节点组成的 MinIO 分布式集群,在上传文件时,文件会被分割成多个小块,一部分小块存储在节点 A,一部分存储在节点 B,同时生成的冗余块会分布在节点 C 和节点 D。这样即使部分节点出现故障,也能通过剩余节点上的数据块恢复原始数据。
    • 适用场景:适用于对存储容量、数据可用性和容错能力要求较高的生产环境,像大型电商平台的商品图片存储、视频网站的视频文件存储等场景。通过分布式存储,不仅可以扩展存储容量,还能保证在部分节点故障时服务不中断,数据不丢失。

文件上传之后的组织方式

那么文件上传后的组织方式再详细展开说一下

基于存储桶(Bucket)的逻辑组织

  • 存储桶的概念:存储桶是 MinIO 中用于组织对象(文件)的容器,类似于文件系统中的文件夹。每个存储桶都有一个唯一的名称,在创建存储桶时,用户可以设置其访问策略,比如设置为公开可访问,那么任何人都可以通过 URL 直接访问存储桶中的文件;也可以设置为私有,只有拥有相应访问权限的用户才能访问。
  • 文件归属:当用户上传文件时,需要指定将文件上传到哪个存储桶中。例如,在一个摄影网站中,可以创建一个名为 “user_photos” 的存储桶,用户上传的照片都会存放在这个存储桶下。不同用户、不同类型的文件可以通过创建不同的存储桶来进行分类管理,方便数据的查找和权限控制。

对象(文件)的标识与元数据管理

  • 对象标识:每个上传到 MinIO 的文件都有一个唯一的对象名称(Object Name),这个名称由用户在上传时指定(或者通过程序生成)。对象名称和存储桶名称一起构成了对象的唯一标识符,用于在 MinIO 中定位和访问该文件。比如,存储桶名称为 “images”,上传的一个图片文件对象名称为 “product_1.jpg”,那么完整的对象标识就是 “images/product_1.jpg” 。
  • 元数据管理:MinIO 会为每个对象存储一些元数据信息,如文件的大小、创建时间、修改时间、内容类型(MIME 类型,用于标识文件是图片、视频还是文档等)等。这些元数据可以帮助用户更好地了解文件的属性,同时在进行文件搜索、过滤等操作时提供依据。例如,在一个新闻网站的文件存储中,通过元数据中的内容类型可以快速筛选出所有的图片文件进行展示。

数据存储的物理布局(以分布式存储为例)

  • 数据分块:当文件上传后,MinIO 会根据配置和算法对文件进行分块处理。比如,对于一个较大的视频文件,可能会被分割成多个固定大小的数据块。
  • 数据分布:结合纠删码技术,这些数据块和冗余数据块会被分散存储到集群中的不同节点上。节点会将数据块存储在本地磁盘的特定目录结构下,通过索引等方式管理这些数据块,以便在需要读取或恢复数据时能够快速定位和获取。

版本控制(如果启用)

  • 版本概念:如果在 MinIO 中为存储桶启用了版本控制功能,那么每次上传同名文件时,MinIO 不会覆盖原来的文件,而是会为该文件创建一个新的版本。
  • 版本组织:每个版本都有对应的版本号,用户可以通过指定版本号来访问特定版本的文件。这在需要保留文件历史版本的场景中非常有用,比如软件开发中的代码库存储,或者设计文件的迭代管理等。
image-20251016191137995

MinIO的启动模式

minio支持多种server启动模式:

image-20251016191344049

MinIO 的启动模式主要有单机模式和分布式模式,具体如下:

  • 单机模式
    • 非纠删码模式(non - erasure code mode):在此模式下,MinIO 直接在指定的数据目录下存储对象数据,不会建立副本,也不启用纠删码机制。这种模式下,服务实例和磁盘都是 “单点”,没有高可用保障,一旦磁盘损坏,数据就会丢失。启动时只需指定一个数据存储目录,例如./minio server /home/data,其中/home/data是自定义的数据存储路径。
    • 纠删码模式(erasure code mode):当为 MinIO server 实例传入多个本地磁盘参数时,会自动启用纠删码模式。纠删码模式对磁盘个数有要求,在 standalone 模式下,要求传给 MinIO server 的 endpoint(即本地磁盘上的目录)至少为 4 个,否则实例启动失败。例如./minio server /data1 /data2 /data3 /data4,这里/data1/data2/data3/data4为 4 个不同的本地磁盘目录。
  • 分布式模式:分布式模式下,MinIO 可以跨多个节点和磁盘进行数据存储和管理,以提供更高的可用性、扩展性和性能。启动分布式 MinIO 时,需要在多个节点上都运行相应的启动命令,并且每个节点都要指定多个磁盘参数。例如,4 节点,每个节点 4 块盘的分布式 MinIO,需要在 4 个节点下都运行类似./minio server http://node1/data1 http://node1/data2 http://node1/data3 http://node1/data4 http://node2/data1 http://node2/data2 http://node2/data3 http://node2/data4 http://node3/data1 http://node3/data2 http://node3/data3 http://node3/data4 http://node4/data1 http://node4/data2 http://node4/data3 http://node4/data4的命令,其中node1node2node3node4为不同节点的地址,/data1/data2/data3/data4为每个节点上的磁盘目录。

此外,根据启动方式的不同,还可以分为直接启动、手动启动和配置自动启动等。直接启动时,程序随启动任务关闭或终端窗口关闭而关闭;手动启动可以在终端中通过特定操作使程序在后台运行,但关闭当前终端窗口程序有时也会关闭;配置自动启动则可以使程序随着系统启动而启动,比如通过在/etc/rc.local文件中添加启动脚本,或者使用systemd管理 MinIO 启停来实现

MinIO实际使用

使用介绍

创建一个存储桶

所有的文件必须要存储到桶中,因此需要先创建存储桶。

注意桶的名字必须是小写

image-20251016192022155

点击左侧的Buckets菜单,即可展示存储桶配置信息。

上传和下载文件

点击Object Browser菜单,可看到刚刚创建的存储桶,点击进入

首先,桶需要指定文件的存储目录

image-20251016192251852

我们来上传文件,MinIO在2025年4月的一次更新中把控制台全部删掉了,扫码了MinIO

image-20251016192411130

MinIO在配置桶的目录的时候,要注意,我们的 MinIO 数据存储目录是通过启动命令 .\minio.exe server xxxx 指定的,直接在桶内输入一个绝对路径,就会导致,实际生成的路径却拼接了另一个磁盘根目录导致最终路径变成 D:\OtherLanguageSetting\Minio\data/testbucket2/D:\MinIO\testbucket2/

image-20251016195203024

我一开始忘了)))))))

其实不配置任何路径即可

image-20251016195301846

最后文件在 minio 的 data目录下能找到桶同名的文件夹,就在里面

image-20251016195441966

欸?为什么我传的是一张图片,到这里变成了一个这种我不知道的 meta 文件

这是因为 MinIO 在存储对象时,会为每个对象生成对应的元数据文件(xl.meta),用于记录对象的相关信息,这是 MinIO 内部的机制

MinIO 元数据文件(xl.meta)的作用

  • 存储对象元信息xl.meta 文件中包含了对象(你上传的图片 test_gal.jpg)的元数据,比如对象的大小、创建时间、内容类型(MIME 类型,这里是图片类型)、校验和等信息。这些元数据对于 MinIO 管理对象(如进行数据恢复、验证数据完整性、支持版本控制等)非常重要。
  • 与纠删码(Erasure Coding)配合:MinIO 采用纠删码技术来提供数据冗余和高可用性,xl.meta 中的信息有助于在数据恢复等场景下,准确地识别和处理对应的对象数据块。

虽然你在文件系统中看到的是 xl.meta 文件,但这并不意味着图片本身没有被正确存储。MinIO 会将对象的数据和元数据分别以特定的方式组织存储。当你通过 MinIO 的客户端(如 Web 控制台、mc 命令行工具等)去访问 test_gal.jpg 时,MinIO 会根据 xl.meta 中的信息,正确地读取和返回图片数据。你可以尝试通过 MinIO 的 Web 控制台下载 test_gal.jpg,或者使用 mc cp 命令将其从 MinIO 下载到本地,查看是否能正常获取到原始图片。

简单来说,xl.meta 是 MinIO 为了自身内部管理对象而生成的元数据文件,它的存在不影响图片本身的存储和使用,图片的实际数据依然被 MinIO 妥善保存,只是在文件系统层面的存储形式包含了这些元数据相关的文件。

所以我们下载一下文件看看

image-20251016195714462

图片是没有问题的,是可以被读取的

设置文件公开访问

默认创建的存储桶,均为私有桶,无法被公开访问。

通常而言,要将数据写入操作进行控制;而读操作,很多不涉及安全问题,希望能被互联网公开访问,以便加快文件的访问速度。

可以在存储桶里面配置,将数据读取权限设置为公开访问

但是话又说回来了,没有控制台,我们怎么设置

要通过命令行将 MinIO 存储桶设置为公开读权限(允许互联网公开访问读取,限制写入权限),需使用 mc 客户端工具操作。

你扫码了 MinIO

首先,配置 mc 连接到你的 MinIO 服务

需要将 mc 与你的 MinIO 服务建立连接(设置别名),执行以下命令:

1
mc alias set myminio http://127.0.0.1:9005 用户名 密码
  • myminio:为你的 MinIO 服务设置的别名(后续命令用这个别名指代服务)。
  • http://127.0.0.1:9005:MinIO 的 API 访问地址(必须与启动时的 --address 一致)。
  • root:你的管理员用户名(RootUser)。
  • zjm10086:你的管理员密码(RootPass)。

执行后若提示 Added 'myminio' successfully,表示配置成功。

image-20251016200146038

确认目标存储桶当前的权限,可先执行以下命令(以存储桶 testbucket2 为例):

1
mc anonymous get myminio/testbucket2
image-20251016200317871

私有,需要改

执行以下命令,允许匿名用户读取存储桶内容(限制写入权限):

1
mc anonymous set download myminio/testbucket2
  • downloadmc预定义的权限标识,含义:

    • 允许匿名用户执行读操作(下载文件、访问文件)。

    • 禁止匿名用户执行写操作(上传、删除、修改文件)。

image-20251016200415811

来验证一下权限是否生效

通过 mc 命令验证匿名访问,用匿名方式列出存储桶文件(无需登录):

1
mc ls myminio/testbucket2 --insecure
image-20251016200453286

直接访问一下其 url ,看看什么情况

1
http://127.0.0.1:9005/testbucket2/test_gal.jpg
image-20251016200536328

没有问题,此时文件可以公开访问。

在spring上使用minio

我们写一个简单的 galgame 表情管理的程序,通过操纵 minio 来实现对图片的对象存储和访问

引入依赖

起手引入依赖,minio的依赖如下

1
2
3
4
5
6
<!-- MinIO 客户端 -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.7</version>
</dependency>

进行配置

来创建一个 MinIO 的配置类,并且配置application.properties添加MinIO配置信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
spring.application.name=ErgouTreeGalemjStore

# MinIO 服务地址(API 端点)
minio.endpoint=http://localhost:9005
# MinIO 访问密钥(账号)
minio.access-key=minioadmin
# MinIO 秘密密钥(密码)
minio.secret-key=minioadmin
# 操作的 MinIO 存储桶名称
minio.bucket-name=testbucket2

# 单个文件的最大上传大小
spring.servlet.multipart.max-file-size=10MB
# 整个请求(可能包含多个文件)的最大大小
spring.servlet.multipart.max-request-size=10MB

# 关闭 Thymeleaf 模板缓存(开发环境用)
spring.thymeleaf.cache=false
# 模板文件的存放路径
spring.thymeleaf.prefix=classpath:/templates/
# 模板文件的后缀名
spring.thymeleaf.suffix=.html

写一个 MinIO 的配置类使得这些配置被加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
@ConfigurationProperties(prefix = "minio")
@Data
public class MinioConfig {

private String endpoint;
private String accessKey;
private String secretKey;
private String bucketName;

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

实体类

这个简单设计一下就行

1
2
3
4
5
6
7
8
9
10
11
12
13
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ImageInfo {
private String fileName;
private String url;
private Long size;
private String uploadTime;
}

图片上传的服务类

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
@Service
@Slf4j
public class ImageService {

@Autowired
private MinioClient minioClient;

@Autowired
private MinioConfig minioConfig;

/**
* 上传图片
*/
public String uploadImage(MultipartFile file) {
try {
// 检查桶是否存在
boolean exists = minioClient.bucketExists(
BucketExistsArgs.builder()
.bucket(minioConfig.getBucketName())
.build()
);

if (!exists) {
// 如果桶不存在,创建桶
minioClient.makeBucket(
MakeBucketArgs.builder()
.bucket(minioConfig.getBucketName())
.build()
);
}

// 获取原始文件名
String originalFilename = file.getOriginalFilename();
// 生成唯一文件名
String fileName = UUID.randomUUID().toString() + "_" + originalFilename;

// 上传文件
minioClient.putObject(
PutObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(fileName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build()
);

log.info("文件上传成功: {}", fileName);
return fileName;

} catch (Exception e) {
log.error("文件上传失败", e);
throw new RuntimeException("文件上传失败: " + e.getMessage());
}
}

/**
* 获取所有图片列表
*/
public List<ImageInfo> listAllImages() {
List<ImageInfo> imageList = new ArrayList<>();
try {
Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(minioConfig.getBucketName())
.build()
);

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

for (Result<Item> result : results) {
Item item = result.get();
String fileName = item.objectName();

// 只显示图片文件
if (isImageFile(fileName)) {
String url = getImageUrl(fileName);
String uploadTime = sdf.format(Date.from(item.lastModified().toInstant()));

ImageInfo imageInfo = new ImageInfo(
fileName,
url,
item.size(),
uploadTime
);
imageList.add(imageInfo);
}
}
} catch (Exception e) {
log.error("获取图片列表失败", e);
throw new RuntimeException("获取图片列表失败: " + e.getMessage());
}
return imageList;
}

/**
* 获取图片访问URL
*/
public String getImageUrl(String fileName) {
try {
String url = minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(io.minio.http.Method.GET)
.bucket(minioConfig.getBucketName())
.object(fileName)
.expiry(60 * 60 * 24) // 24小时有效期
.build()
);
return url;
} catch (Exception e) {
log.error("获取图片URL失败", e);
throw new RuntimeException("获取图片URL失败: " + e.getMessage());
}
}

/**
* 下载图片
*/
public InputStream downloadImage(String fileName) {
try {
return minioClient.getObject(
GetObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(fileName)
.build()
);
} catch (Exception e) {
log.error("下载图片失败", e);
throw new RuntimeException("下载图片失败: " + e.getMessage());
}
}

/**
* 删除图片
*/
public void deleteImage(String fileName) {
try {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(fileName)
.build()
);
log.info("删除图片成功: {}", fileName);
} catch (Exception e) {
log.error("删除图片失败", e);
throw new RuntimeException("删除图片失败: " + e.getMessage());
}
}

/**
* 判断是否为图片文件
*/
private boolean isImageFile(String fileName) {
String lowerCaseName = fileName.toLowerCase();
return lowerCaseName.endsWith(".jpg") ||
lowerCaseName.endsWith(".jpeg") ||
lowerCaseName.endsWith(".png") ||
lowerCaseName.endsWith(".gif") ||
lowerCaseName.endsWith(".bmp") ||
lowerCaseName.endsWith(".webp");
}
}

控制器

没啥好说的,纯预制化控制器

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
@Controller
@Slf4j
public class ImageController {

@Autowired
private ImageService imageService;

/**
* 首页 - 显示所有图片
*/
@GetMapping("/")
public String index(Model model) {
try {
List<ImageInfo> images = imageService.listAllImages();
model.addAttribute("images", images);
model.addAttribute("message", "共找到 " + images.size() + " 张图片");
} catch (Exception e) {
log.error("加载图片列表失败", e);
model.addAttribute("error", "加载图片列表失败: " + e.getMessage());
}
return "index";
}

/**
* 上传图片
*/
@PostMapping("/upload")
public String uploadImage(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {
try {
if (file.isEmpty()) {
redirectAttributes.addFlashAttribute("error", "请选择要上传的文件");
return "redirect:/";
}

// 检查文件类型
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
redirectAttributes.addFlashAttribute("error", "只能上传图片文件");
return "redirect:/";
}

String fileName = imageService.uploadImage(file);
redirectAttributes.addFlashAttribute("success", "图片上传成功: " + fileName);
} catch (Exception e) {
log.error("上传图片失败", e);
redirectAttributes.addFlashAttribute("error", "上传失败: " + e.getMessage());
}
return "redirect:/";
}

/**
* 下载图片
*/
@GetMapping("/download/{fileName}")
public ResponseEntity<InputStreamResource> downloadImage(@PathVariable String fileName) {
try {
InputStream inputStream = imageService.downloadImage(fileName);

// 设置响应头
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));

return ResponseEntity.ok()
.headers(headers)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(new InputStreamResource(inputStream));
} catch (Exception e) {
log.error("下载图片失败", e);
return ResponseEntity.internalServerError().build();
}
}

/**
* 删除图片
*/
@PostMapping("/delete/{fileName}")
public String deleteImage(@PathVariable String fileName,
RedirectAttributes redirectAttributes) {
try {
imageService.deleteImage(fileName);
redirectAttributes.addFlashAttribute("success", "图片删除成功: " + fileName);
} catch (Exception e) {
log.error("删除图片失败", e);
redirectAttributes.addFlashAttribute("error", "删除失败: " + e.getMessage());
}
return "redirect:/";
}
}

其中,Minio 操作桶涉及到的相关方法如下,在 Spring 项目中需通过 MinioClient 实例调用

  • 检查桶是否存在(bucketExists):判断指定名称的桶是否已存在,避免重复创建或操作不存在的桶。

    1
    2
    3
    4
    // 构建检查参数:指定桶名称
    BucketExistsArgs args = BucketExistsArgs.builder()
    .bucket(bucketName) // 桶名称(必须小写,无特殊字符)
    .build();
    • 桶名称必须符合 MinIO 规范:小写字母、数字、连字符(-),且不能以连字符开头 / 结尾,长度 3-63 字符。
  • 创建桶(makeBucket):创建新桶,需先确保桶不存在

    参数:桶名称(必填),可选区域(region)、对象锁配置等

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public void createBucket(String bucketName) throws Exception {
    // 先检查桶是否存在,不存在再创建
    if (!checkBucketExists(bucketName)) {
    MakeBucketArgs args = MakeBucketArgs.builder()
    .bucket(bucketName) // 桶名称
    .region("us-east-1") // 可选:指定区域(如不指定,使用 MinIO 服务器默认区域)
    .build();
    minioClient.makeBucket(args);
    System.out.println("桶 " + bucketName + " 创建成功");
    }
    }
  • 设置桶访问策略(setBucketPolicy

    控制桶的读写权限(如公开读、私有等),常用于配置前端能否直接访问桶内文件。

    参数:桶名称、JSON 格式的策略配置。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import io.minio.SetBucketPolicyArgs;

    public void setBucketPublicRead(String bucketName) throws Exception {
    // 公开读策略 JSON:允许任何人读取桶内所有文件
    String policyJson = "{\n" +
    " \"Version\": \"2012-10-17\",\n" +
    " \"Statement\": [{\n" +
    " \"Effect\": \"Allow\",\n" +
    " \"Principal\": \"*\", // 所有用户\n" +
    " \"Action\": \"s3:GetObject\", // 允许读取对象\n" +
    " \"Resource\": \"arn:aws:s3:::" + bucketName + "/*\" // 作用范围:桶内所有文件\n" +
    " }]\n" +
    "}";

    SetBucketPolicyArgs args = SetBucketPolicyArgs.builder()
    .bucket(bucketName)
    .config(policyJson) // 策略配置
    .build();
    minioClient.setBucketPolicy(args);
    System.out.println("桶 " + bucketName + " 已设置为公开读");
    }
    • 策略 JSON 遵循 AWS S3 策略语法,Principal: "*" 表示所有用户,s3:GetObject 表示允许下载文件。
    • 公开读适合前端直接通过 URL(如 http://minio地址:9000/桶名/文件名)访问图片,无需后端转发。
  • 列出所有桶(listBuckets

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import io.minio.ListBucketsArgs;
    import io.minio.messages.Bucket;
    import java.util.List;

    public List<Bucket> listAllBuckets() throws Exception {
    // 调用方法获取桶列表
    List<Bucket> buckets = minioClient.listBuckets(ListBucketsArgs.builder().build());
    // 遍历打印桶信息
    for (Bucket bucket : buckets) {
    System.out.println("桶名:" + bucket.name() + ",创建时间:" + bucket.creationDate());
    }
    return buckets;
    }
    • 返回的 Bucket 对象包含 name()(桶名)和 creationDate()(创建时间)方法。
    • 仅能列出当前 Access Key 有权限访问的桶。
  • 删除桶(removeBucket

    删除指定桶,注意:桶必须为空(无任何文件)才能删除

    参数:桶名称。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import io.minio.RemoveBucketArgs;

    public void deleteBucket(String bucketName) throws Exception {
    // 先检查桶是否存在
    if (checkBucketExists(bucketName)) {
    // 注意:需先删除桶内所有文件(此处省略遍历删除逻辑)
    RemoveBucketArgs args = RemoveBucketArgs.builder()
    .bucket(bucketName)
    .build();
    minioClient.removeBucket(args);
    System.out.println("桶 " + bucketName + " 删除成功");
    }
    }
    • 若桶内有文件,调用此方法会抛出 BucketNotEmptyException,需先删除所有文件(通过 removeObject 方法)。
    • 删除桶需要 s3:DeleteBucket 权限。

测试

来运行一下看看效果

image-20251016204518610

还行,能传能查