MinIo跟对象存储服务
# MinIo跟对象存储服务
# 存储服务的类型
我们进行数据存储的时候,主要有三种不同的技术选型,分别对应了文件存储、块存储和对象存储。他们有独立的应用场景以及所构建出来的上层的应用。
文件存储,它是一个统称的概念。因为从使用者的角度来说,无论是对象存储还是块存储,它们最终展现出来的其实都是一个一个的文件。虽然我们看到的展现的样式是一样的,但是底层的逻辑却非常的不同。
说起文件式的存储,其实最著名的产品就是我们常说的FTP,NFS或者是NAS。
# 文件存储
文件存储:它是站在使用者角度来说,我们构建的是一个文件。在咱们进行数据保存的时候,你传一个文件在咱们的硬盘上,它就有一个这样的文件的存储单位。所以文件是在咱们进行数据存储时的一个基本的单位,它主要面对的是,比如说常见的FTP文件传输协议,还有NFS我们的网络文件系统。
# 块存储
块存储咱们相对就比较陌生了。其实块存储它是一种相对底层的存储的机制
。要知道无论是我们的硬盘也好,还是SSD也好,它在进行数据保存的时候,我们都是块儿一块的,以固定块来进行的存储。那你比如说在咱们机械硬盘上有扇区,对吧?这不就是1块1块的吗?每一块都有固定的大小,那么这个块存储更多的是表达咱们这个存储的方式。
我们创建了文件系统以后,在将一个文件通过操作系统写入到磁盘上的时候,它实际上是被打散到了
多个不同的数据块
来进行保存的,这就是我们说到的块存储,你可以把它看成是一个更为基础的存储单元。有些人也说,其实块存储也是一种文件存储。
# mysql中的存储是哪种
MySQL的数据存储本质上属于文件存储,但其具体实现依赖于存储引擎(如InnoDB、MyISAM)。以下是关键点:
- 文件系统存储:
- MySQL将数据、索引和日志存储在文件系统中。例如:
- InnoDB:数据存储在
.ibd
文件中,索引与数据一起存储(聚簇索引)。 - MyISAM:数据存储在
.MYD
文件中,索引存储在.MYI
文件中(非聚簇索引)。
- InnoDB:数据存储在
- 数据库文件(如
.frm
)保存表的元数据(结构定义)。
- MySQL将数据、索引和日志存储在文件系统中。例如:
- 存储引擎的作用:
- 存储引擎决定了数据在文件系统中的组织方式(如B+树索引、事务日志等),但最终数据仍以文件形式存储在磁盘上。
那它为什么不是块存储呢?
- 块存储是底层存储技术,直接操作磁盘块(如硬盘分区),通常用于虚拟机磁盘(VMDK)、数据库裸设备等场景。
- MySQL不直接使用块存储,而是通过文件系统(如ext4、NTFS)管理数据文件。即使使用块存储作为底层介质,MySQL仍然以文件形式组织数据。
# 对象存储
对象存储是这几年在云原生和大规模的这种互联网应用下的一个主流的软件产品。这个对象存储,它的一个含义是你在使用的时候,你既不需要关心文件是长什么样子的,也不需要关心数据是存在哪些块上面的。
我们只需要安装这一个叫做对象存储的软件,你把你的文件或者数据,通过咱们对应的相应的软件上传到到这个对象存储上。至于底层怎么存的,存在哪儿,这些细节你都不需要去关心。所以作为这个对象存储,是特别适合大规模的海量数据保存和处理的底层数据如何存储的。
不同的对象存储的软件系统,他们有着自己的规则。其中比较有代表性的就是咱们常说的MinIO。
# 对象存储的特点
- 扁平化结构:数据以“对象”为单位存储,每个对象包含数据、元数据(如名称、大小、时间戳)和唯一标识符(如UUID),没有传统的目录树结构。
- 访问方式:通过RESTful API或专有协议访问,适合大规模分布式存储(如AWS S3、阿里云OSS)。
- 适用场景:海量非结构化数据(图片、视频、日志)和需要高扩展性的场景(如云计算、大数据分析)。
# 存储的核心
对象存储本质上仍然是以字节流的方式进行数据存储的,但它在字节流之上提供了一层更高级别的抽象和管理方式。
其实,无论是哪种存储类型(对象存储、文件存储、块存储),最终数据在磁盘或SSD上保存的形式都是字节流(byte stream)
。
# 对象存储的流程
虽然对象存储底层使用的是字节流,但它不是简单地将字节写入文件系统,而是:
- 封装为对象
一个对象(Object)通常包含三个核心部分:
组成部分 | 描述 |
---|---|
Data(数据) | 实际内容,即字节流(如图片、视频等) |
Metadata(元数据) | 自定义或系统元数据(如Content-Type、标签、权限等) |
Key(唯一标识符) | 对象名称或路径(如 photos/2025/photo1.jpg ) |
例如,当你上传一张图片到 MinIO 或 AWS S3,你实际上是在创建一个对象,它包括:
- 字节流形式的图像内容
- 元数据(如 MIME 类型是 image/jpeg)
- 唯一标识 Key(如 bucket/object-key)
才外,它还有一个特点,那就是对象存储不使用传统的目录树结构(如 /home/user/file.txt
),而是通过唯一的 Key 来定位对象,这使得它可以轻松扩展到 EB 级别数据。
最后总结下,对象存储确实是通过一个系统,将每个数据封装为一个“对象”进行存储。这种存储方式与传统的文件存储和块存储有显著区别。
# 为什么说对象存储比文件存储更高级?
因为对象存储:
- 把字节流封装成对象(Object)
- 提供了统一的访问接口(RESTful API)
- 支持大规模分布式架构
- 支持丰富的元数据管理
- 更容易实现高可用、高并发、跨地域同步
# 安装MinIO
MinIO只是对象存储的一种方案,市面上还有很多方法,除了私人部署的方法,各大互联网厂商还提供了很多云服务,如阿里的对象存储OSS等。
下面展示的只是MinIO的Linux系统中的单机安装方式。它的安装方式还有docker,分布式安装,这里就不展示了。
MinIO的官网 (opens new window)地址如下:
- https://min.io/docs/minio/linux/operations/install-deploy-manage/deploy-minio-single-node-single-drive.html#create-the-systemd-service-file
# 安装软件
使用wget
下载最新版本的MinIO(替换为官网 (opens new window)的最新链接):
wget https://dl.min.io/server/minio/release/linux-amd64/minio
chmod +x minio
sudo mv minio /usr/local/bin/
2
3
在下载时,你可能会发现,他有以下几个版本,RPM (RHEL)、DEB (Debian/Ubuntu)、Binary,其中Binary是编译后的二进制文件,可以直接启动,那RPM和DEB呢,它们是安装文件,安装后,会在系统中创建对应服务。但它们两个该选哪个版本呢?
在下载软件安装包时,选择 RPM(适用于 Red Hat 系列发行版)或 DEB(适用于 Debian/Ubuntu 系列发行版)的格式,主要取决于你使用的 Linux 发行版。
那如何判断你系统是哪种格式呢?
# ERP和DEB
如果你使用的是 Red Hat、CentOS、Fedora 等基于 Red Hat 的发行版,系统默认支持 RPM 格式。验证方法如下:
rpm --version
如果输出版本号(例如 RPM version 4.14.3
),则说明系统支持 RPM。
如果你使用的是 Debian、Ubuntu、Linux Mint 等基于 Debian 的发行版,系统默认支持 DEB 格式。验证方法如下:
dpkg --version
如果输出版本号(例如 dpkg-deb 1.21.9
),则说明系统支持 DEB。
以下是一个帮助快速辨别的表格:
系统类型 | 推荐格式 | 验证命令 | 包管理器 |
---|---|---|---|
Red Hat/CentOS/Fedora | RPM | rpm --version | yum 或 dnf |
Debian/Ubuntu/Linux Mint | DEB | dpkg --version | apt 或 apt-get |
下载完成后,运行安装命令
sudo dnf install minio.rpm
// 或者 以下命令
sudo yum install minio.rpm
2
3
4
# 创建数据存储目录
MinIO的配置文件通常位于 /etc/default/minio
,你需要编辑或创建它:
# 管理员账号和密码(建议设置强密码)
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=minioadmin
# 数据存储目录(确保目录存在且有权限)
MINIO_VOLUMES="/data/minio"
# 监听地址和端口
MINIO_OPTS="--address :9000 --console-address :9001"
2
3
4
5
6
7
8
9
MINIO_VOLUMES
:指定MinIO存储数据的目录(例如/data/minio
),需要提前创建并设置权限。--address :9000
:MinIO服务的API端口。--console-address :9001
:MinIO控制台的Web界面端口。
# 创建服务
创建或编辑MinIO的Systemd服务文件:
在官网的描述中,服务配置文件位于,/usr/lib/systemd/system/minio.service
路径。
[Unit]
Description=MinIO
Documentation=https://min.io/docs/minio/linux/index.html
Wants=network-online.target
After=network-online.target
AssertFileIsExecutable=/usr/local/bin/minio
[Service]
WorkingDirectory=/usr/local
User=minio-user
Group=minio-user
ProtectProc=invisible
EnvironmentFile=-/etc/default/minio
ExecStartPre=/bin/bash -c "if [ -z \"${MINIO_VOLUMES}\" ]; then echo \"Variable MINIO_VOLUMES not set in /etc/default/minio\"; exit 1; fi"
ExecStart=/usr/local/bin/minio server $MINIO_OPTS $MINIO_VOLUMES
# MinIO RELEASE.2023-05-04T21-44-30Z adds support for Type=notify (https://www.freedesktop.org/software/systemd/man/systemd.service.html#Type=)
# This may improve systemctl setups where other services use `After=minio.server`
# Uncomment the line to enable the functionality
# Type=notify
# Let systemd restart this service always
Restart=always
# Specifies the maximum file descriptor number that can be opened by this process
LimitNOFILE=65536
# Specifies the maximum number of threads this process can create
TasksMax=infinity
# Disable timeout logic and wait until process is stopped
TimeoutStopSec=infinity
SendSIGKILL=no
[Install]
WantedBy=multi-user.target
# Built for ${project.name}-${project.version} (${project.name})
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
虽然服务文件已自动生成在 /usr/lib/systemd/system/minio.service
路径中了,但建议将其换到/etc/systemd/system/
下。
因为它,优先级更高,systemd
在加载服务时,优先使用 /etc/systemd/system/
下的配置,会覆盖 /usr/lib/
中的默认配置。
sudo cp /usr/lib/systemd/system/minio.service /etc/systemd/system/
sudo systemctl daemon-reload
2
# 重新加载配置
sudo systemctl daemon-reload
# 启动服务
# 启动服务
sudo systemctl start minio
# 检查服务状态
sudo systemctl status minio
2
3
4
如果看到如下输出,恭喜你成功了
● minio.service - MinIO
Loaded: loaded (/etc/systemd/system/minio.service; enabled; vendor preset: disabled)
Active: active (running) since ...
2
3
# SpringBoot集成MinIO
MinIO的官方网站是没有集成SpringBoot的示例的,但有对应的Java版本,大家阅读后,应该就能很轻易的整合到SpringBoot中,下面就展示一个整合的例子。
# 引入依赖
在Spring Boot中集成MinIO,先引入maven坐标,使用稳定版本的依赖:
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.7</version> <!-- 当前稳定版本 -->
</dependency>
2
3
4
5
# 编写配置类
MinIO的server端,运行着,你去访问,肯定要知道地址对吧,正常情况下服务端肯定不能让所有人都能访问对吧?所以我们需要配置秘钥,
为了实现以上需求,我们需要对minio进行一定配置,在SpringBoot中,就是编写一个配置类,然后创建一个Minio的Client,用于跟Server进行交互.
import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MinioConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.accessKey}")
private String accessKey;
@Value("${minio.secretKey}")
private String secretKey;
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 编写配置
minio:
endpoint: http://localhost:9000 # MinIO服务器地址
accessKey: your-access-key # 访问密钥
secretKey: your-secret-key # 私有密钥
bucket: your-bucket-name # 默认存储桶名称
2
3
4
5
其中的bucket可以理解成,数据库中的database。是一个用于划分数据的虚拟概念。
# 实现文件下载服务
import io.minio.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.util.UUID;
@Component
public class MinioUtils {
@Autowired
private MinioClient minioClient;
@Value("${minio.bucketName}")
private String bucketName;
@Value("${minio.endpoint}")
private String endpoint;
/**
* 生成新的文件名:UUID + 原始文件名
*/
private String generateNewObjectName(String originalFilename) {
String uniqueId = UUID.randomUUID().toString();
int dotIndex = originalFilename.lastIndexOf('.');
String prefix = (dotIndex != -1) ? originalFilename.substring(0, dotIndex) : originalFilename;
String suffix = (dotIndex != -1) ? originalFilename.substring(dotIndex) : "";
return uniqueId + "-" + prefix + suffix;
}
/**
* 上传文件
* @param file
* @param objectName
* @throws Exception
*/
public String uploadFile(MultipartFile file, String objectName) throws Exception {
boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!exists) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
// 2. 生成新的文件名
String newObjectName = generateNewObjectName(file.getOriginalFilename());
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(newObjectName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build());
// 生成文件的公开访问 URL
//TODO 如果 上传同一个图片时,怎么保证 不会重复上传,
// 因为对图片的名称做了处理,所以MinIO server不会进行判重,即使同一张图片 被上传多次,仍会进行多次保存
// 注意:确保 MinIO 配置允许匿名读取该对象或使用预签名 URL
return new StringBuilder(endpoint).append("/").append(bucketName).append("/").append(newObjectName).toString();
}
/**
* 下载文件
* @param objectName
* @return
* @throws Exception
*/
public GetObjectResponse downloadFile(String objectName) throws Exception {
return minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
}
/**
* 删除文件
* @param objectName 文件名
* @throws Exception
*/
public void deleteFile(String objectName) throws Exception {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build()
);
}
}
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
# 几个问题
下面问题的答案或者原因,仅供参考,因为可能不止这些。
问题:在MinIO文件上传中,为什么要对文件名进行重命名?
同名覆盖风险:当多个用户上传相同文件名(如
report.pdf
)时,后上传的文件会覆盖之前的文件解决方案:使用唯一标识符(如UUID)重命名确保每个文件都有唯一标识
路径遍历攻击:原始文件名可能包含恶意路径字符(
../
、/
等),可能导致文件被存储到意外位置特殊字符问题:文件名中的空格、中文、特殊符号可能导致URL解析错误
系统标准化:统一使用特定命名规则(如时间戳+UUID)便于管理
优化存储结构:可按日期生成目录结构
问题:上一个问题的原因中,有说到相同文件名的文件会覆盖之前的问题?那现在上传时,对文件名进行了处理,每次上传时,文件虽然相同,但都是新的文件名,岂不是同一个文件会重复存储很多次?造成很大的浪费?这种问题该怎么解决呢?
回答:
如果多个用户上传了 内容相同的文件,即使文件名不同(如通过 UUID 生成),MinIO 会存储多个副本,导致存储空间浪费。为了避免这种情况,通常需要引入 文件内容去重(Content Deduplication)机制。
文件名唯一性 ≠ 内容唯一性
- 你的代码中通过
UUID
生成文件名,确保了每个文件在 MinIO 中有唯一的路径(Key),这是正确的做法。 - 但 文件内容重复 的问题没有解决。例如,用户多次上传相同图片(文件名不同),MinIO 会存储多个副本。
MinIO 的默认行为
- MinIO 是对象存储系统,不主动去重。即使两个对象内容完全相同,只要 Key 不同,MinIO 会分别存储。
- 因此,如果希望避免重复存储相同内容的文件,必须在应用层手动实现去重逻辑。
MinIO 的 Key 是对象的完整路径标识,系统不会因内容相同而自动合并存储。这种设计提供了最大的灵活性,但需要开发者在应用层根据需求实现去重逻辑。
- 格式为:
<bucket-name>/<object-path>
- 示例:
user-uploads/profile-pictures/user123/avatar.jpg
- Key 是对象在存储桶内的唯一标识符
# 文件去重
去重问题,有很多地方需要去考虑,因为这里着重说下:
# 方案一:基于文件内容哈希去重
在上传文件时,计算文件的 哈希值(如 MD5、SHA-1),并检查 MinIO 中是否已存在相同哈希值的文件。
- 计算文件哈希值
- 使用工具(如 Java 的
MessageDigest
)计算文件的 MD5 或 SHA-1。 - 例如:
MD5(fileBytes)
生成唯一标识符。
- 使用工具(如 Java 的
- 检查哈希值是否已存在
- 在数据库中维护一个映射表:
hash -> objectName
。 - 上传前先查询数据库,如果哈希值已存在,直接返回已存在的文件 URL。
- 如果不存在,上传新文件并保存哈希值到数据库。
- 在数据库中维护一个映射表:
以下是一份参考代码:
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.security.MessageDigest;
import java.util.Optional;
@Service
public class FileDeduplicationService {
@Autowired
private FileStorageService fileStorageService;
@Autowired
private StringRedisTemplate redisTemplate; // 使用 Redis 存储哈希值与 objectName 映射
public String uploadWithDeduplication(MultipartFile file) throws Exception {
// 1. 计算文件的 MD5
String fileHash = calculateMD5(file);
// 2. 检查哈希值是否已存在
String existingObjectName = redisTemplate.opsForValue().get("file_hash:" + fileHash);
if (existingObjectName != null) {
// 文件已存在,直接返回 URL
return buildPermanentUrl(existingObjectName);
}
// 3. 上传新文件
String newObjectName = fileStorageService.generateNewObjectName(file.getOriginalFilename());
fileStorageService.uploadFileWithPublicAccess(file);
// 4. 保存哈希值与 objectName 的映射
redisTemplate.opsForValue().set("file_hash:" + fileHash, newObjectName);
// 5. 返回 URL
return buildPermanentUrl(newObjectName);
}
private String calculateMD5(MultipartFile file) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] hashBytes = md.digest(file.getBytes());
StringBuilder hexString = new StringBuilder();
for (byte b : hashBytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
}
private String buildPermanentUrl(String objectName) {
return "http://minio-endpoint/" + bucketName + "/" + objectName;
}
}
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
优点
- 避免重复存储相同内容的文件。
- 节省存储空间,尤其适用于大规模文件(如图片、视频)。
缺点
- 需要额外计算哈希值,略微增加 CPU 开销。
- 需要维护数据库或缓存(如 Redis)存储哈希值与 objectName 的映射。
上述例子中的MD5算法是否有,内容不同,但得到的值相同的情况?
MD5(Message Digest Algorithm 5)是一种被广泛使用的哈希函数,能够产生128位(16字节)的哈希值。然而,尽管MD5在设计之初是为了提供安全性和唯一性,它并不是完全没有碰撞的——即不同的输入内容可能产生相同的哈希输出。这种情况被称为哈希碰撞。
不同内容产生相同MD5哈希值的概率极低,但考虑到MD5存在的安全隐患,不建议在对安全性有要求的应用中使用MD5。如果你的应用主要是为了验证文件完整性而非安全性,比如简单的文件去重功能,那么MD5仍然是可用的,但作为开发者你应该意识到其潜在的风险。
# 方案二:基于文件名和 内容双重去重
如果用户上传的文件名和内容都相同,直接返回已存在的文件 URL。
实现逻辑
- 上传时,先检查文件名是否已存在(可选)。
- 如果文件名存在,检查内容是否相同(通过哈希值)。
- 如果文件名和内容都相同,直接返回已存在的 URL。
适用场景
- 用户可能重复上传相同文件名和内容的文件(如用户误操作)。
- 使用 Redis 缓存哈希值
- 将哈希值与 objectName 的映射存储在 Redis 中,提高查询效率。
- 设置合理的过期时间(TTL),避免缓存无限增长。
- 分片计算大文件哈希
- 对于大文件(如视频、大型 PDF),不要一次性读取全部内容。
- 使用流式计算哈希值(如
InputStream
逐块读取)。
因为:文件越大,哈希值计算时间确实越长,但通过分块处理、算法优化和缓存机制,可以显著降低性能影响。
- 定期清理冗余文件
- 如果某些文件的哈希值已从数据库中删除,但 MinIO 中仍有副本,可以通过后台任务清理。
以下是参考代码:
@RestController
@RequestMapping("/api/files")
public class FileController {
private final FileDeduplicationService deduplicationService;
public FileController(FileDeduplicationService deduplicationService) {
this.deduplicationService = deduplicationService;
}
@PostMapping("/upload-dedup")
public ResponseEntity<String> uploadWithDeduplication(@RequestParam("file") MultipartFile file) {
try {
String url = deduplicationService.uploadWithDeduplication(file);
return ResponseEntity.ok("File uploaded successfully. URL: " + url);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Upload failed: " + e.getMessage());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
问题:上述代码中,生成的访问链接,是任何人都可以访问吗?其他人知道了该地址,是否能修改和删除该文件?如果想生成临时的,有时效的访问链接如何实现?如果想要生成永久的访问链接,又该怎么实现?
- 上述代码生成的链接是永久的
- 文件的操作权限在服务端,为了安全跟方便,建议将部分文件,或者说某个
bucket
设置为 匿名用户只有只读权限
。 - 以下是生成临时的、有时效的访问链接的对应代码
/**
* 生成预签名 URL(GET 方法)
*/
private String generatePresignedUrl(String objectName, int expiresInSeconds) throws Exception {
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(objectName)
.expiry(expiresInSeconds, ChronoUnit.SECONDS)
.build()
);
}
// 4. 生成预签名 URL(有效期 7 天)
return generatePresignedUrl(newObjectName, 7 * 24 * 3600);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
永久链接的格式为:
http[s]://<endpoint>/<bucket-name>/<object-name>
其中:
endpoint
是你的 MinIO 服务地址(如localhost:9000
)bucket-name
是存储桶名称object-name
是文件的存储路径和名称
问题:客户端是否能设置文件的权限?