探索MinIO:高性能、分布式对象存储解决方案
注:本文除代码外多数为AI生成
最近因为有项目需要换成Amazon S3的云存储,所以把之前做过的minio部分做一个记录,后面也会把基于这版改造的S3方法发出来记录。
MinIO简介
MinIO是一款高性能、分布式对象存储服务器,设计用于在大规模环境中存储和检索非结构化数据集,如图像、视频和日志文件。它完全开源,遵循Apache License v2.0,并且与Amazon S3 API兼容,这使得从现有的S3环境迁移变得简单。MinIO支持多租户,确保了数据的安全性和隔离性,同时提供了多种数据持久性和一致性保障机制。由于其出色的性能特性,如高吞吐量和低延迟,MinIO成为了众多企业和组织在构建现代云基础设施时的首选对象存储解决方案。
MinIO架构与技术
去中心化架构
- 无共享架构:MinIO采用了一种去中心化的无共享架构,这意味着没有单一的瓶颈点或中心化的元数据服务器。数据和元数据分布在集群内的所有节点上,提高了系统的整体性能和可靠性。
- 统一命名空间:尽管数据分布在多个节点上,但MinIO对外提供了一个统一的命名空间,用户无需关心数据的具体位置。
分布式特性
- 水平扩展:MinIO可以很容易地通过添加更多的节点来水平扩展,每个节点都是对等的,可以独立运行和存储数据。
- 纠删编码:为了提高数据的可靠性和容错性,MinIO使用纠删编码技术(Erasure Coding)来存储数据。这允许数据在多个节点上以冗余的形式存在,即使部分节点发生故障,数据仍然可读。
高性能
- 并发处理:MinIO的设计考虑到了高并发性,能够同时处理大量的读写请求,提供低延迟和高吞吐量。
- 网络优化:MinIO通过高效的网络协议和数据传输优化,确保数据在网络中的快速流动。
兼容性
- S3 API兼容:MinIO完全兼容Amazon S3 API,这使得它可以无缝地与许多已有的应用程序和服务集成,降低了迁移成本。
安全性
- 加密:MinIO支持静态数据加密,确保数据在存储期间的安全。
- 访问控制:通过IAM(Identity and Access Management)策略,MinIO提供了细粒度的访问控制,保护数据不被未授权访问。
开源与跨平台
- 开源许可证:MinIO遵循Apache License v2.0开源协议,允许自由使用、修改和分发。
- 跨平台:MinIO可以在多种操作系统上运行,包括Linux、Windows和macOS,增强了其部署灵活性。
集群部署
- 多节点集群:MinIO可以部署为一个集群,其中包含多个节点,这些节点共同维护数据的一致性和可用性。
- 负载均衡:在集群中,可以通过DNS轮询或负载均衡器来分配请求到不同的节点,确保负载均匀分布。
技术栈
- Go语言:MinIO使用Go语言编写,这提供了良好的性能和简洁的代码基础。
Minio的安装部署
单节点部署
- 参考https://www.cnblogs.com/ComfortableM/p/17384523.html
集群部署概念
- 节点:MinIO集群由多个节点组成,每个节点都是一个独立运行的MinIO服务器实例。
- 纠删编码:为了实现数据冗余和容错,MinIO使用纠删编码(Erasure Coding)。这种技术允许数据被分割成多个片段,并且每个片段都有额外的校验信息。如果集群中有节点失效,数据仍然可以从剩余的节点中重构出来。
- 数据分布:数据均匀分布在所有节点上,以达到负载均衡和最大化使用所有存储资源的目的。
部署步骤
- 环境准备:
- 确保有足够的物理服务器或虚拟机。
- 准备好足够的磁盘空间,每台服务器可以使用单个磁盘或多个磁盘。
- 配置网络,确保所有节点之间可以互相通信。
- 软件安装:
- 在每台服务器上安装MinIO软件。可以通过二进制包、Docker容器或软件包管理系统完成。
- 配置MinIO服务器,指定数据存储目录和集群相关的配置。
- 启动MinIO服务:
- 在每个节点上启动MinIO服务,使用集群模式的启动参数。
- 指定集群中的其他节点,以便它们可以相互发现并形成集群。
- 验证集群状态:
- 使用MinIO的管理命令或Web UI检查集群状态,确认所有节点是否已成功加入集群。
- 测试读写操作,确保数据可以正确地在集群中分布和访问。
- 监控和维护:
- 设置监控,持续监控集群的健康状况和性能指标。
- 定期进行维护,如数据平衡、硬件升级或替换故障节点。
关键考虑因素
- 节点数量:集群至少需要4个节点来实现数据的冗余和分布。通常,节点数量越多,数据的可用性和性能就越好。
- 数据冗余:根据纠删编码策略,集群可以容忍一定数量的节点故障而不会丢失数据。
- 网络配置:确保网络稳定和高速,因为集群中的节点需要频繁地相互通信。
- 故障恢复:设计故障恢复计划,包括节点替换、数据重建和灾难恢复策略。
- 性能调优:根据实际工作负载,可能需要调整网络带宽、磁盘I/O或其他系统参数来优化性能。
扩展和缩放
- 扩展:通过添加更多节点可以轻松扩展MinIO集群的容量和性能。
- 缩放:移除节点时需要小心,确保数据冗余不会降低到不可接受的水平。
注意事项
- 使用
hosts文件或DNS服务来解决集群内部的域名或IP地址,避免直接在配置文件中硬编码IP地址,这有助于在节点变化时更容易地维护集群。 - 在部署多节点多磁盘的集群时,确保遵循最佳实践,如避免在同一服务器上使用过多的磁盘,以防服务器故障导致大量数据不可用。
下面为例:
- 节点启动方式,每个节点都需要启动
export MINIO_ACCESS_KEY=admin export MINIO_SECRET_KEY=minioadmin #console-address参数可设可不设 ./minio server --address ":9001" --console-address ":9011" "http://ipA:9001/arcfile/minioData/data" "http://ipB:9001/arcfile/minioData/data" "http://ipC:9001/arcfile/minioData/data" "http://ipD:9001/arcfile/minioData/data" >/arcfile/minioData/logs/start.txt 2>&1 &
- 如果要用来测试,可以一个服务器部署四个节点。以上面为例修改端口启动,或者参考下面的启动脚本
RUNNING_USER=root MINIO_HOME=/opt/minio MINIO_HOST=192.168.222.10 #accesskey and secretkey ACCESS_KEY=admin SECRET_KEY=minioadmin for i in {01..04}; do START_CMD="MINIO_ACCESS_KEY=${ACCESS_KEY} MINIO_SECRET_KEY=${SECRET_KEY} nohup ${MINIO_HOME}/minio server --address "${MINIO_HOST}:90${i}" http://${MINIO_HOST}:9001/opt/min-data1 http://${MINIO_HOST}:9002/opt/min-data2 http://${MINIO_HOST}:9003/opt/min-data3 http://${MINIO_HOST}:9004/opt/min-data4 > ${MINIO_HOME}/minio-90${i}.log 2>&1 &" su - ${RUNNING_USER} -c "${START_CMD}" done
集成Spring boot项目
添加依赖
<dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>8.3.3</version> </dependency> <!--如果出现okhttp依赖冲突,添加下面依赖--> <!-- <dependency>--> <!-- <groupId>com.squareup.okhttp3</groupId>--> <!-- <artifactId>okhttp</artifactId>--> <!-- <version>4.9.0</version>--> <!-- </dependency>-->
创建配置类
import io.minio.MinioClient; import io.minio.errors.*; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.logging.Logger; @Configuration public class MinioClientConfig { private final Logger log = Logger.getLogger(this.getClass().getName()); @Value("${config.minio.url}") private String MINIO_URL; @Value("${config.minio.accessKey}") private String MINIO_ACCESS_KEY; @Value("${config.minio.secretKey}") private String MINIO_SECRET_KEY; @Bean public MinioClient getMinioClient() { if (MINIO_URL.length() == 0 || MINIO_URL == null) { throw new IllegalArgumentException("n请正确配置Minio服务器的 URL 连接参数"); } if (MINIO_ACCESS_KEY.length() == 0 || MINIO_ACCESS_KEY == null) { throw new IllegalArgumentException("n请正确配置Minio服务器的 ACCESS_KEY 连接参数"); } if (MINIO_SECRET_KEY.length() == 0 || MINIO_SECRET_KEY == null) { throw new IllegalArgumentException("n请正确配置Minio服务器的 SECRET_KEY 连接参数"); } MinioClient minioClient = MinioClient.builder() .endpoint(MINIO_URL) .credentials(MINIO_ACCESS_KEY, MINIO_SECRET_KEY) .build(); try { minioClient.listBuckets(); } catch (ErrorResponseException e) { e.printStackTrace(); throw new RuntimeException("nMinio服务器连接异常n请检查所配置的Minio连接信息Access-key和Secret-Key是否正确"); } catch (InsufficientDataException e) { e.printStackTrace(); } catch (InternalException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (InvalidResponseException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException("nMinio服务器连接异常n请检查Minio服务器是否已开启或所配置的Minio_url连接信息是否正确"); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (ServerException e) { e.printStackTrace(); } catch (XmlParserException e) { e.printStackTrace(); } log.info("Minio服务器连接成功,URL = " + MINIO_URL); return minioClient; } }
基本操作介绍
- 创建桶
public boolean createBucket(String bucketName) throws RuntimeException { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); }
- 配置桶权限为公开
public void BucketAccessPublic(String bucketName) { String config = "{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:ListBucketMultipartUploads","s3:GetBucketLocation","s3:ListBucket"],"Resource":["arn:aws:s3:::" + bucketName + ""]},{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:ListMultipartUploadParts","s3:PutObject","s3:AbortMultipartUpload","s3:DeleteObject","s3:GetObject"],"Resource":["arn:aws:s3:::" + bucketName + "/*"]}]}"; minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucketName).config(config).build(); }
- 上传对象
uploadObject()
//上传本地文件 public boolean uploadObject(String bucketName, String targetObject, String sourcePath) { try { minioClient.uploadObject(UploadObjectArgs.builder() .bucket(bucketName) .object(targetObject) .filename(sourcePath) .build()); } catch (ErrorResponseException e) { e.printStackTrace(); return false; } catch (InsufficientDataException e) { e.printStackTrace(); } catch (InternalException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (InvalidResponseException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (ServerException e) { e.printStackTrace(); } catch (XmlParserException e) { e.printStackTrace(); } return true; }
putObject()
public boolean putObject(String bucketName, String object, InputStream inputStream) { try { minioClient.putObject(PutObjectArgs.builder() .bucket(bucketName) .object(object) //.contentType("application/pdf")不设置的话默认是"application/stream",这就是为什么某些文件上传上去无法直接预览的问题 .stream(inputStream, -1, 10485760) //.tags(tags)上传可以直接携带标签 .build()); } catch (ErrorResponseException e) { e.printStackTrace(); return false; } catch (InsufficientDataException e) { e.printStackTrace(); } catch (InternalException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (InvalidResponseException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (ServerException e) { e.printStackTrace(); } catch (XmlParserException e) { e.printStackTrace(); } return true; }
- 下载对象
public GetObjectResponse getObject(String bucketName, String object) { GetObjectResponse object1 = null;//这个对象是集成了InputStream的FilterInputStream类,所以是可以直接用流来处理文件的 try { object1 = minioClient.getObject(GetObjectArgs.builder() .bucket(bucketName) .object(object) .build()); } catch (ErrorResponseException e) { e.printStackTrace(); } catch (InsufficientDataException e) { e.printStackTrace(); } catch (InternalException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (InvalidResponseException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (ServerException e) { e.printStackTrace(); } catch (XmlParserException e) { e.printStackTrace(); } return object1; }
- 获取对象预签名Url
/** * 生成预签名URL,允许外部在限定时间内访问指定的MinIO对象。 * * @param bucketName 存储桶名称 * @param object 对象名称 * @param expire 过期时间,单位是分钟 * @param map 用于自定义HTTP响应头的映射,例如设置Content-Type * @return 返回预签名的URL,可以用于直接访问对象 */ public String presignedURLofObject(String bucketName, String object, int expire, Map<String, String> map) { // 设置响应的内容类型为application/json map.put("response-content-type", "application/json"); // 初始化预签名URL字符串 String presignedObjectUrl = ""; // 尝试获取预签名的URL try { // 构建获取预签名URL的参数 presignedObjectUrl = minioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .bucket(bucketName) // 设置存储桶名称 .object(object) // 设置对象名称 .method(Method.GET) // 设置HTTP方法为GET .expiry(expire, TimeUnit.MINUTES) // 设置过期时间为expire分钟后 //.extraQueryParams(map) // 注释掉的代码,用于设置额外的查询参数 .build() // 构建参数对象 ); } catch (ErrorResponseException e) { // 处理错误响应异常 e.printStackTrace(); } catch (InsufficientDataException e) { // 数据不足异常 e.printStackTrace(); } catch (InternalException e) { // MinIO内部异常 e.printStackTrace(); } catch (InvalidKeyException e) { // 无效的访问密钥异常 e.printStackTrace(); } catch (InvalidResponseException e) { // 无效的响应异常 e.printStackTrace(); } catch (IOException e) { // 输入输出异常 e.printStackTrace(); } catch (NoSuchAlgorithmException e) { // 不存在算法异常 e.printStackTrace(); } catch (XmlParserException e) { // XML解析异常 e.printStackTrace(); } catch (ServerException e) { // 服务器异常 e.printStackTrace(); } // 返回预签名的URL return presignedObjectUrl; }
部分方法集
MinioService
import io.minio.ComposeSource; import io.minio.GetObjectResponse; import io.minio.Result; import io.minio.StatObjectResponse; import io.minio.messages.Bucket; import io.minio.messages.Item; import org.springframework.stereotype.Service; import java.io.InputStream; import java.util.List; import java.util.Map; /** * MinioService接口,定义了与MinIO对象存储系统交互的方法集。 */ @Service public interface MinioService { /** * 检查指定的存储桶是否存在。 * * @param bucketName 存储桶名称 * @return 如果存储桶存在则返回true,否则返回false。 */ boolean ifExistsBucket(String bucketName); /** * 创建一个新的存储桶。 * * @param bucketName 要创建的存储桶名称 * @throws RuntimeException 如果创建失败抛出运行时异常 * @return 如果创建成功则返回true,否则返回false。 */ boolean createBucket(String bucketName) throws RuntimeException; /** * 删除指定的存储桶。 * * @param bucketName 要删除的存储桶名称 * @return 如果删除成功则返回true,否则返回false。 */ boolean removeBucket(String bucketName); /** * 列出所有已存在的存储桶。 * * @return 包含所有存储桶信息的列表。 */ List<Bucket> alreadyExistBuckets(); /** * 列出指定存储桶中的对象。 * * @param bucketName 要列出对象的存储桶名称 * @param predir 可选前缀,用于过滤对象名 * @param recursive 是否递归列出子目录的对象 * @return 包含匹配条件的对象列表。 */ List<Result<Item>> listObjects(String bucketName, String predir, boolean recursive); /** * 列出指定存储桶中的对象,用于组成对象操作。 * * @param bucketName 要列出对象的存储桶名称 * @param predir 可选前缀,用于过滤对象名 * @return 包含对象信息的List<ComposeSource>对象列表。 */ List<ComposeSource> listObjects(String bucketName, String predir); /** * 复制一个对象到另一个位置。 * * @param pastBucket 原始存储桶名称 * @param pastObject 原始对象名称 * @param newBucket 目标存储桶名称 * @param newObject 目标对象名称 * @return 如果复制成功则返回true,否则返回false。 */ boolean copyObject(String pastBucket, String pastObject, String newBucket, String newObject); /** * 下载存储桶中的对象到本地文件系统。 * * @param bucketName 存储桶名称 * @param objectName 对象名称 * @param targetPath 目标文件路径 * @return 如果下载成功则返回true,否则返回false。 */ boolean downObject(String bucketName, String objectName, String targetPath); /** * 生成一个预签名的URL,允许外部访问指定的对象。 * * @param bucketName 存储桶名称 * @param object 对象名称 * @param expire URL的有效期(分钟) * @return 预签名的URL。 */ String presignedURLofObject(String bucketName, String object, int expire); /** * 生成一个预签名的URL,允许外部访问指定的对象,并自定义HTTP头部。 * * @param bucketName 存储桶名称 * @param object 对象名称 * @param expire URL的有效期(分钟) * @param map 自定义的HTTP头部信息 * @return 预签名的URL。 */ String presignedURLofObject(String bucketName, String object, int expire, Map<String, String> map); /** * 删除存储桶中的对象。 * * @param bucketName 存储桶名称 * @param object 对象名称 * @return 如果删除成功则返回true,否则返回false。 */ boolean deleteObject(String bucketName, String object); /** * 上传本地文件到存储桶。 * * @param bucketName 存储桶名称 * @param targetObject 目标对象名称 * @param sourcePath 本地文件路径 * @return 如果上传成功则返回true,否则返回false。 */ boolean uploadObject(String bucketName, String targetObject, String sourcePath); /** * 从InputStream上传数据到存储桶。 * * @param bucketName 存储桶名称 * @param object 对象名称 * @param inputStream 数据流 * @return 如果上传成功则返回true,否则返回false。 */ boolean putObject(String bucketName, String object, InputStream inputStream); /** * 从InputStream上传数据到存储桶,并附带标签。 * * @param bucketName 存储桶名称 * @param object 对象名称 * @param inputStream 数据流 * @param tags 对象的标签集合 * @return 如果上传成功则返回true,否则返回false。 */ boolean putObject(String bucketName, String object, InputStream inputStream, Map<String, String> tags); /** * 获取存储桶中的对象信息。 * * @param bucketName 存储桶名称 * @param object 对象名称 * @return 对象的响应信息。 */ GetObjectResponse getObject(String bucketName, String object); /** * 检查存储桶中是否包含指定文件。 * * @param bucketName 存储桶名称 * @param filename 文件名称 * @param recursive 是否递归查找 * @return 如果文件存在则返回true,否则返回false。 */ boolean fileifexist(String bucketName, String filename, boolean recursive); /** * 获取对象的元数据标签。 * * @param bucketName 存储桶名称 * @param object 对象名称 * @return 对象的元数据标签集合。 */ Map<String, String> getTags(String bucketName, String object); /** * 添加或更新对象的标签。 * * @param bucketName 存储桶名称 * @param object 对象名称 * @param addTags 要添加或更新的标签集合 * @return 如果操作成功则返回true,否则返回false。 */ boolean addTags(String bucketName, String object, Map<String, String> addTags); /** * 获取对象的状态信息。 * * @param bucketName 存储桶名称 * @param object 对象名称 * @return 对象的状态信息。 */ StatObjectResponse statObject(String bucketName, String object); /** * 检查存储桶中对象是否存在。 * * @param bucketName 存储桶名称 * @param objectName 对象名称 * @return 如果对象存在则返回true,否则返回false。 */ boolean ifExistObject(String bucketName, String objectName); /** * 从其他对象名中提取元名称。 * * @param objectName 对象名称 * @return 提取的元名称。 */ String getMetaNameFromOther(String objectName); /** * 更改对象的标签。 * * @param object 对象名称 * @param tag 新的标签值 * @return 如果更改成功则返回true,否则返回false。 */ boolean changeTag(String object, String tag); /** * 设置存储桶的公共访问权限。 * * @param bucketName 存储桶名称 */ void BucketAccessPublic(String bucketName); }
MinioServiceImpl
import com.aspose.cad.internal.Y.S; import com.xagxsj.erms.model.BucketName; import com.xagxsj.erms.model.ObjectTags; import com.xagxsj.erms.service.MinioService; import com.xagxsj.erms.utils.FileUtil; import io.minio.*; import io.minio.errors.*; import io.minio.http.Method; import io.minio.messages.Bucket; import io.minio.messages.Item; import io.minio.messages.Tags; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import java.io.IOException; import java.io.InputStream; import java.net.URLDecoder; import java.net.URLEncoder; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import static com.xagxsj.erms.model.BucketName.METADATA; @Service public class MinioServiceImpl implements MinioService { private final Logger log = Logger.getLogger(this.getClass().getName()); @Qualifier("getMinioClient") @Autowired MinioClient minioClient; /** * 检查指定的存储桶是否存在于MinIO服务器上。 * * @param bucketName 要检查的存储桶名称。 * @return 如果存储桶存在,则返回true;否则返回false。 */ @Override public boolean ifExistsBucket(String bucketName) { boolean result = false; try { result = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); } catch (ErrorResponseException e) { e.printStackTrace(); return false; } catch (InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) { e.printStackTrace(); } return result; } /** * 在MinIO服务器上创建一个新的存储桶。 * * @param bucketName 要创建的存储桶名称。 * @throws RuntimeException 如果尝试创建一个已存在的存储桶,则抛出此异常。 * @return 如果存储桶创建成功,则返回true;否则返回false。 */ @Override public boolean createBucket(String bucketName) throws RuntimeException { if (ifExistsBucket(bucketName)) { throw new RuntimeException("桶已存在"); } try { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) { e.printStackTrace(); } return ifExistsBucket(bucketName); } /** * 从MinIO服务器上删除一个存储桶。 * * @param bucketName 要删除的存储桶名称。 * @return 如果存储桶成功删除,则返回true;如果存储桶不存在,则返回true;否则返回false。 */ @Override public boolean removeBucket(String bucketName) { if (!ifExistsBucket(bucketName)) { return true; } try { minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build()); } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) { e.printStackTrace(); } return !ifExistsBucket(bucketName); } /** * 列出MinIO服务器上所有存在的存储桶。 * * @return 返回一个包含所有存储桶信息的列表。 */ @Override public List<Bucket> alreadyExistBuckets() { List<Bucket> buckets = null; try { buckets = minioClient.listBuckets(); } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) { e.printStackTrace(); } return buckets; } /** * 检查指定的文件是否存在于存储桶中。 * * @param bucketName 存储桶名称。 * @param filename 要检查的文件名。 * @param recursive 是否递归搜索子目录。 * @return 如果文件存在,则返回true;否则返回false。 */ @Override public boolean fileifexist(String bucketName, String filename, boolean recursive) { boolean flag = false; Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder() .bucket(bucketName) .prefix(filename) .recursive(recursive) .maxKeys(1000) .build()); for (Result<Item> result : results) { try { Item item = result.get(); if (item.objectName().equals(filename)) { flag = true; break; } } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) { e.printStackTrace(); } } return flag; } /** * 列出存储桶中所有对象,可指定前缀和是否递归搜索子目录。 * * @param bucketName 存储桶名称。 * @param predir 前缀过滤器。 * @param recursive 是否递归搜索子目录。 * @return 返回一个包含所有匹配对象的结果列表。 */ @Override public List<Result<Item>> listObjects(String bucketName, String predir, boolean recursive) { Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder() .bucket(bucketName) .prefix(predir) .recursive(recursive) .maxKeys(1000) .build()); List<Result<Item>> list = new ArrayList<>(); results.forEach(list::add); return list; } /** * 构建存储桶中对象的ComposeSource列表,用于复合对象操作。 * * @param bucketName 存储桶名称。 * @param predir 前缀过滤器。 * @return 返回一个包含所有匹配对象的ComposeSource列表。 */ @Override public List<ComposeSource> listObjects(String bucketName, String predir) { Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder() .bucket(bucketName) .prefix(predir) .recursive(true) .maxKeys(1000) .build()); List<Result<Item>> list = new ArrayList<>(); results.forEach(list::add); List<ComposeSource> sources = new ArrayList<>(); for (Result<Item> itemResult : list) { try { sources.add(ComposeSource.builder().bucket(bucketName).object(itemResult.get().objectName()).build()); } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) { e.printStackTrace(); } } return sources; } /** * 复制一个对象到新的存储桶或新的对象名称。 * * @param pastBucket 原始存储桶名称 * @param pastObject 原始对象名称 * @param newBucket 新的存储桶名称 * @param newObject 新的对象名称 * @return 如果复制成功,则返回true;否则返回false。 */ @Override public boolean copyObject(String pastBucket, String pastObject, String newBucket, String newObject) { try { ObjectWriteResponse response = minioClient.copyObject( CopyObjectArgs.builder() .bucket(newBucket) .object(newObject) .source( CopySource.builder() .bucket(pastBucket) .object(pastObject) .build()) .build()); } catch (ErrorResponseException e) { e.printStackTrace(); return false; } catch (InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) { e.printStackTrace(); } return true; } /** * 下载存储桶中的对象到本地文件系统。 * * @param bucketName 存储桶名称 * @param objectName 对象名称 * @param targetPath 本地目标路径 * @return 如果下载成功,则返回true;否则返回false。 */ @Override public boolean downObject(String bucketName, String objectName, String targetPath) { try { if ("".equals(objectName) || null == objectName) { throw new RuntimeException("检查电子文件FilePath是否为空,下载目标位置为:" + targetPath); } minioClient.downloadObject( DownloadObjectArgs.builder() .bucket(bucketName) .object(objectName) .filename(targetPath) //download local path .build()); } catch (ErrorResponseException e) { e.printStackTrace(); return false; } catch (InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) { e.printStackTrace(); } return true; } /** * 获取存储桶中对象的预签名URL(GET方法)。 * * @param bucketName 存储桶名称 * @param object 对象名称 * @param expire 过期时间(分钟) * @return 预签名URL,如果发生错误则返回null。 */ @Override public String presignedURLofObject(String bucketName, String object, int expire) { String url = null; try { url = minioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.GET) .bucket(bucketName) .object(object) .expiry(expire, TimeUnit.MINUTES) .build()); } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | XmlParserException | ServerException e) { e.printStackTrace(); } return url; } /** * 获取存储桶中对象的预签名URL,允许设置额外的查询参数。 * * @param bucketName 存储桶名称 * @param object 对象名称 * @param expire 过期时间(分钟) * @param map 额外的查询参数 * @return 预签名URL,如果发生错误则返回空字符串。 */ @Override public String presignedURLofObject(String bucketName, String object, int expire, Map<String, String> map) { map.put("response-content-type", "application/json"); String presignedObjectUrl = ""; try { presignedObjectUrl = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder() .bucket(bucketName) .object(object) .method(Method.GET) .expiry(expire, TimeUnit.MINUTES) .extraQueryParams(map) .build()); } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | XmlParserException | ServerException e) { e.printStackTrace(); } return presignedObjectUrl; } /** * 删除存储桶中的对象。 * * @param bucketName 存储桶名称 * @param object 对象名称 * @return 如果删除成功,则返回true;否则返回false。 */ @Override public boolean deleteObject(String bucketName, String object) { try { minioClient.removeObject(RemoveObjectArgs.builder() .bucket(bucketName) .object(object) .build()); } catch (ErrorResponseException e) { e.printStackTrace(); return false; } catch (InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) { e.printStackTrace(); } return true; } /** * 将本地文件上传至MinIO存储桶。 * * @param bucketName 存储桶名称 * @param targetObject 目标对象名称 * @param sourcePath 源文件路径 * @return 如果上传成功,则返回true;否则返回false。 */ @Override public boolean uploadObject(String bucketName, String targetObject, String sourcePath) { try { minioClient.uploadObject(UploadObjectArgs.builder() .bucket(bucketName) .object(targetObject) .filename(sourcePath) .build()); } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) { e.printStackTrace(); return false; } return true; } /** * 将输入流数据写入MinIO存储桶。 * * @param bucketName 存储桶名称 * @param object 对象名称 * @param inputStream 输入流 * @return 如果写入成功,则返回true;否则返回false。 */ @Override public boolean putObject(String bucketName, String object, InputStream inputStream) { try { minioClient.putObject(PutObjectArgs.builder() .bucket(bucketName) .object(object) .stream(inputStream, -1, 10485760) .build()); } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) { e.printStackTrace(); return false; } return true; } /** * 将带有标签的输入流数据写入MinIO存储桶。 * * @param bucketName 存储桶名称 * @param object 对象名称 * @param inputStream 输入流 * @param tags 对象标签 * @return 如果写入成功,则返回true;否则返回false。 */ @Override public boolean putObject(String bucketName, String object, InputStream inputStream, Map<String, String> tags) { try { minioClient.putObject(PutObjectArgs.builder() .bucket(bucketName) .object(object) .stream(inputStream, -1, 10485760) .tags(tags) .build()); } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) { e.printStackTrace(); return false; } return true; } /** * 从MinIO存储桶获取对象。 * * @param bucketName 存储桶名称 * @param object 对象名称 * @return 返回对象响应信息,如果发生错误则返回null。 */ @Override public GetObjectResponse getObject(String bucketName, String object) { GetObjectResponse object1 = null; try { object1 = minioClient.getObject(GetObjectArgs.builder() .bucket(bucketName) .object(object) .build()); } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) { e.printStackTrace(); } return object1; } /** * 获取对象的标签信息。 * * @param bucketName 存储桶名称 * @param object 对象名称 * @return 返回对象的标签映射,如果发生错误则返回null。 */ @Override public Map<String, String> getTags(String bucketName, String object) { Map<String, String> map = new HashMap<>(); try { Tags objectTags = minioClient.getObjectTags(GetObjectTagsArgs.builder() .bucket(bucketName) .object(object) .build()); map = objectTags.get(); } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) { e.printStackTrace(); return null; } return map; } /** * 向对象添加或更新标签。 * * @param bucketName 存储桶名称 * @param object 对象名称 * @param addTags 要添加或更新的标签映射 * @return 如果操作成功,则返回true;否则返回false。 */ @Override public boolean addTags(String bucketName, String object, Map<String, String> addTags) { Map<String, String> oldtags = new HashMap<>(); Map<String, String> newTags = new HashMap<>(); try { oldtags = getTags(bucketName, object); if (oldtags.size() > 0) { newTags.putAll(oldtags); } if (addTags != null && addTags.size() > 0) { newTags.putAll(addTags); } minioClient.setObjectTags(SetObjectTagsArgs.builder() .bucket(bucketName) .object(object) .tags(newTags) .build()); return true; } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) { e.printStackTrace(); return false; } } /** * 获取对象的状态信息。 * * @param bucketName 存储桶名称 * @param object 对象名称 * @return 返回对象状态信息,如果发生错误则返回null。 */ @Override public StatObjectResponse statObject(String bucketName, String object) { StatObjectResponse statObject = null; try { statObject = minioClient.statObject( StatObjectArgs.builder() .bucket(bucketName) .object(object) .build()); } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) { e.printStackTrace(); } return statObject; } /** * 判断存储桶中是否存在指定对象。 * * @param bucketName 存储桶名称 * @param objectName 对象名称 * @return 如果存在,则返回true;否则返回false。 */ @Override public boolean ifExistObject(String bucketName, String objectName) { return listObjects(bucketName, objectName, true).size() >= 1; } /** * 从元数据存储桶中获取与特定对象相关的元数据对象名。 * * @param objectName 原始对象名称 * @return 返回编码后的元数据对象名,如果没有找到对应的元数据则返回其文件名。 */ @Override public String getMetaNameFromOther(String objectName) { String metaobject = ""; List<Result<Item>> results = listObjects(BucketName.METADATA, FileUtil.getPreMeta(objectName), true); if (results.size() == 1) { try { metaobject = results.get(0).get().objectName(); Map<String, String> tags = getTags(BucketName.METADATA, metaobject); String s = tags.get(ObjectTags.FILENAME); // 解码后再编码以确保正确处理特殊字符 // URLDecoder.decode(s,"UTF-8"); return URLEncoder.encode(s, "UTF-8"); } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) { e.printStackTrace(); } } return FileUtil.getFileName(metaobject); } /** * 修改对象的标签信息。 * * @param object 对象名称 * @param tag 新的标签值 * @return 如果修改成功,则返回true;否则返回false。 */ @Override public boolean changeTag(String object, String tag) { try { Map<String, String> map = minioClient.getObjectTags(GetObjectTagsArgs.builder() .bucket(BucketName.METADATA) .object(object) .build()).get(); Map<String, String> map1 = new HashMap<>(); tag = tag + FileUtil.getSuffix(object); map1.put(ObjectTags.FILENAME, tag); map1.put(ObjectTags.OPERATOR, map.get(ObjectTags.OPERATOR)); minioClient.setObjectTags(SetObjectTagsArgs.builder() .bucket(BucketName.METADATA) .object(object) .tags(map1) .build()); return true; } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) { e.printStackTrace(); return false; } } /** * 设置存储桶的访问策略为公开访问。 * * @param bucketName 存储桶名称 */ @Override public void BucketAccessPublic(String bucketName) { String config = "{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:ListBucketMultipartUploads","s3:GetBucketLocation","s3:ListBucket"],"Resource":["arn:aws:s3:::" + bucketName + ""]},{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:ListMultipartUploadParts","s3:PutObject","s3:AbortMultipartUpload","s3:DeleteObject","s3:GetObject"],"Resource":["arn:aws:s3:::" + bucketName + "/*"]}]}"; try { minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucketName).config(config).build()); } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) { e.printStackTrace(); } } }