ThinkPHP 8 集成 MinIO 实现文件上传/删除/下载完整教程

ThinkPHP 8 集成 MinIO 实现文件上传/删除/下载完整教程

一、环境准备:安装依赖

MinIO 兼容 S3 API,可通过 AWS S3 SDK 操作。使用 Composer 安装依赖:

1
composer require aws/aws-sdk-php "^3.0"

Composer 安装依赖成功截图
图:Composer 安装 aws-sdk-php 依赖成功

二、配置文件:创建 MinIO 连接配置

在 ThinkPHP 8 的 config 目录下创建 minio.php,填写 MinIO 连接参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
// config/minio.php
return [
// MinIO服务地址(含端口,本地默认9000)
'endpoint' => env('MINIO_ENDPOINT', 'http://192.168.20.147:9000'),
// 是否使用路径风格(MinIO推荐开启,自托管场景必备)
'use_path_style_endpoint' => true,
// 认证信息(MinIO控制台创建的Access Key和Secret Key)
'credentials' => [
'key' => env('MINIO_ACCESS_KEY', '5QEARuEh9VzSusd9qCAO'),
'secret' => env('MINIO_SECRET_KEY', 'TO6TdWpTpkldCyvaov69XBf6EcOvuQGoVyDkXYOW'),
],
// 区域(MinIO可随意填写,非必填但不能为空,示例:us-east-1)
'region' => env('MINIO_REGION', 'us-east-1'),
// 是否启用SSL(本地测试默认false,生产环境根据配置调整)
'ssl.certificate_authority' => env('MINIO_SSL', false),
// 默认存储桶(需提前在MinIO控制台创建)
'bucket' => env('MINIO_BUCKET', 'articleinnerimg'),
];

⚠️ 注意:

  1. 存储桶需提前在 MinIO 控制台手动创建
  2. Access Key/Secret Key 需从 MinIO 控制台获取
  3. 本地测试时 ssl 建议设为 false,生产环境若配置 HTTPS 需设为 true

三、核心功能:文件上传逻辑

3.1 初始化 MinIO 连接

通过 S3 SDK 初始化连接,参数直接读取配置文件:

1
2
3
4
5
6
7
8
9
10
// 读取MinIO配置
$config = config('minio');

// 初始化S3客户端(MinIO兼容S3协议)
$s3Client = new S3Client([
'endpoint' => $config['endpoint'],
'use_path_style_endpoint' => $config['use_path_style_endpoint'],
'credentials' => $config['credentials'],
'region' => $config['region'],
]);

关键参数说明:

参数 作用 注意事项
endpoint MinIO 服务地址(含端口) 本地示例:http://192.168.20.147:9000
use_path_style_endpoint 路径风格访问(推荐 true 自托管 MinIO 必须开启,虚拟主机风格需配置 DNS
credentials 认证信息 从 MinIO 控制台「Access Keys」中创建
region 区域标识 非必填,可随意填写(如 us-east-1

3.2 上传核心:putObject 方法

通过 putObject 上传文件,需指定存储桶、文件索引(Key)、文件内容(Body)和 MIME 类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 构建文件索引(Key):含目录结构+唯一文件名
$filename = "DemoFile/20251118/" . uniqid() . "_" . $originalName;

// 上传文件到MinIO
$result = $s3Client->putObject([
'Bucket' => $config['bucket'], // 存储桶名称
'Key' => $filename, // 文件索引(含目录结构,模拟文件夹)
'Body' => fopen($imgFile->getPathname(), 'r'), // 文件流
'ContentType' => $imgFile->getMime(), // 文件MIME类型
]);

// 构建可访问URL(MinIO地址+存储桶+文件索引)
$fileUrl = $config['endpoint'] . '/' . $config['bucket'] . '/' . $filename;

3.3 完整上传控制器代码

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
<?php
namespace app\controller;

use think\facade\Request;
use think\facade\Config;
use Aws\S3\S3Client;
use Aws\Exception\AwsException;

class DemoController
{
/**
* 显示上传页面
*/
public function upload()
{
return view("demo/upload");
}

/**
* 处理图片上传(支持目录分级存储)
* @param string $prefix 一级目录前缀(默认:DemoFile)
* @return \think\response\Json
*/
public function img(string $prefix = "DemoFile")
{
// 1. 获取上传文件
$imgFile = Request::file("image");
if (!$imgFile) {
return json(['code' => 1, 'msg' => '请选择文件']);
}
if (!$imgFile->isValid()) {
return json(['code' => 1, 'msg' => '文件无效:' . $imgFile->getError()]);
}

try {
// 2. 读取MinIO配置
$config = Config::get('minio');

// 3. 初始化S3客户端
$s3Client = new S3Client([
'endpoint' => $config['endpoint'],
'use_path_style_endpoint' => $config['use_path_style_endpoint'],
'credentials' => $config['credentials'],
'region' => $config['region'],
]);

// 4. 构建文件路径(分级存储:前缀/日期/唯一文件名)
$originalName = basename($imgFile->getOriginalName()); // 原始文件名(去除路径)
$dateDir = date('Ymd'); // 按日期创建二级目录
$dirPath = "{$prefix}/{$dateDir}/"; // 完整目录路径
$filename = $dirPath . uniqid() . '_' . $originalName; // 最终文件索引(Key)

// 5. 上传文件到MinIO
$s3Client->putObject([
'Bucket' => $config['bucket'],
'Key' => $filename,
'Body' => fopen($imgFile->getPathname(), 'r'),
'ContentType' => $imgFile->getMime(),
]);

// 6. 构建可访问URL
$fileUrl = $config['endpoint'] . '/' . $config['bucket'] . '/' . $filename;

// 7. 返回结果
return json([
'code' => 0,
'msg' => '上传成功',
'data' => [
'url' => $fileUrl,
'filename' => $filename // 存储文件索引(用于后续删除/下载)
]
]);

} catch (AwsException $e) {
// 捕获AWS SDK异常
return json(['code' => 1, 'msg' => '上传失败:' . $e->getMessage()]);
} catch (\Exception $e) {
// 捕获其他异常
return json(['code' => 1, 'msg' => '系统异常:' . $e->getMessage()]);
}
}
}

3.4 前端上传页面(含进度条)

创建 view/demo/upload.html 文件,支持图片选择、上传进度显示、结果反馈:

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
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MinIO文件上传演示</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.upload-container {
max-width: 600px;
margin: 50px auto;
padding: 25px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h2 {
color: #333;
margin-bottom: 20px;
text-align: center;
}
.upload-form {
display: flex;
flex-direction: column;
gap: 15px;
}
.file-input {
padding: 10px;
border: 2px dashed #ccc;
border-radius: 4px;
transition: border-color 0.3s;
}
.file-input:hover {
border-color: #4096ff;
}
.submit-btn {
padding: 12px;
background-color: #4096ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
.submit-btn:hover {
background-color: #1677ff;
}
.progress-container {
height: 8px;
background-color: #f0f0f0;
border-radius: 4px;
overflow: hidden;
display: none;
}
.progress-bar {
height: 100%;
background-color: #52c41a;
width: 0%;
transition: width 0.3s;
}
.message {
padding: 10px;
border-radius: 4px;
margin-top: 10px;
display: none;
}
.success {
background-color: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
.error {
background-color: #fff2f0;
color: #f5222d;
border: 1px solid #ffccc7;
}
.file-info {
margin-top: 10px;
font-size: 14px;
color: #666;
display: none;
}
.file-info a {
color: #4096ff;
text-decoration: none;
word-break: break-all;
}
</style>
</head>
<body>
<div class="upload-container">
<h2>MinIO文件上传演示</h2>
<form id="uploadForm" class="upload-form">
<input type="file" name="image" class="file-input" accept="image/*" required>
<div class="progress-container">
<div class="progress-bar" id="progressBar"></div>
</div>
<button type="submit" class="submit-btn">上传文件</button>
<div class="message" id="message"></div>
<div class="file-info" id="fileInfo">
<p>文件URL: <a id="fileUrl" target="_blank"></a></p>
<p>文件索引(Key): <span id="fileName"></span></p>
</div>
</form>
</div>

<script>
const form = document.getElementById('uploadForm');
const progressBar = document.getElementById('progressBar');
const progressContainer = progressBar.parentElement;
const message = document.getElementById('message');
const fileInfo = document.getElementById('fileInfo');
const fileUrl = document.getElementById('fileUrl');
const fileName = document.getElementById('fileName');

// 表单提交事件(异步上传)
form.addEventListener('submit', async (e) => {
e.preventDefault(); // 阻止默认表单提交

const fileInput = form.querySelector('input[type="file"]');
const file = fileInput.files[0];

// 基础校验
if (!file) {
showMessage('请选择要上传的图片', 'error');
return;
}
if (!file.type.startsWith('image/')) {
showMessage('请上传图片文件(jpg、png、gif等)', 'error');
return;
}

// 构建FormData
const formData = new FormData();
formData.append('image', file);

try {
// 显示进度条,隐藏之前的消息和文件信息
progressContainer.style.display = 'block';
progressBar.style.width = '0%';
hideMessage();
fileInfo.style.display = 'none';

// 发起上传请求(支持进度监听)
const response = await fetch('/DemoController/img', {
method: 'POST',
body: formData,
// 监听上传进度
onprogress: (e) => {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
progressBar.style.width = `${percent}%`;
}
}
});

// 解析响应结果
const result = await response.json();

if (result.code === 0) {
showMessage('上传成功', 'success');
fileInfo.style.display = 'block';
fileUrl.href = result.data.url;
fileUrl.textContent = result.data.url;
fileName.textContent = result.data.filename;
} else {
showMessage(`上传失败: ${result.msg}`, 'error');
}

} catch (error) {
showMessage(`网络错误: ${error.message}`, 'error');
} finally {
// 3秒后隐藏进度条
setTimeout(() => {
progressContainer.style.display = 'none';
}, 3000);
}
});

// 显示消息提示
function showMessage(text, type) {
message.textContent = text;
message.className = 'message ' + type;
message.style.display = 'block';
}

// 隐藏消息提示
function hideMessage() {
message.style.display = 'none';
}
</script>
</body>
</html>

3.5 上传测试效果

  1. 访问 http://你的域名/DemoController/upload 进入上传页面
  2. 选择图片并点击「上传文件」,可看到进度条动态加载
  3. 上传成功后显示文件 URL 和索引(Key)

MinIO上传页面演示
图:MinIO 上传页面演示

上传成功后,MinIO 控制台会显示分级目录结构的文件:

MinIO分级存储效果
图:分级存储效果(DemoFile/日期/文件名)

✨ 核心优化:通过 prefix/日期/文件名 的结构模拟目录,解决了根目录文件混乱的问题,方便后续管理。MinIO 本身没有真实目录,而是通过 Key 中的 / 实现分级。

四、扩展功能:文件删除

通过文件索引(Key)删除 MinIO 中的文件,需传递完整的 Key(含目录结构):

4.1 删除控制器方法

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
/**
* 删除MinIO中的图片
* @param string $key 文件索引(含目录结构,如:DemoFile/20251118/691c71a48050d_xxx.png)
* @return \think\response\Json
*/
public function deleteImg(string $key)
{
// 校验Key是否为空
if (empty($key)) {
return json(['code' => 1, 'msg' => '请传入要删除的文件Key']);
}

try {
$config = Config::get('minio');
$s3Client = new S3Client([
'endpoint' => $config['endpoint'],
'use_path_style_endpoint' => $config['use_path_style_endpoint'],
'credentials' => $config['credentials'],
'region' => $config['region'],
]);

// 执行删除操作
$result = $s3Client->deleteObject([
'Bucket' => $config['bucket'],
'Key' => $key // 必须传递完整的文件索引(含目录)
]);

// 校验删除结果(204表示删除成功)
if ($result->get('@metadata')['statusCode'] === 204) {
return json(['code' => 0, 'msg' => '图片删除成功']);
} else {
return json(['code' => 1, 'msg' => '图片删除失败,状态异常']);
}

} catch (AwsException $e) {
return json(['code' => 1, 'msg' => '删除失败:' . $e->getMessage()]);
}
}

4.2 调用方式

通过 GET/POST 请求传递 key 参数即可:

1
GET: http://你的域名/DemoController/deleteImg?key=DemoFile/20251118/691c71a48050d_xxx.png

删除成功后,MinIO 控制台对应的文件会被移除:

MinIO文件删除效果
图:文件删除成功反馈

五、扩展功能:文件下载

通过文件索引(Key)下载 MinIO 中的文件,支持自定义下载文件名:

5.1 下载控制器方法

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
/**
* 下载MinIO中的图片
* @param string $key 文件索引(含目录结构)
* @param string|null $downloadName 自定义下载文件名(默认使用原始文件名)
* @return \think\response\Json|\think\response\Response
*/
public function downloadImg(string $key, ?string $downloadName = null)
{
// 校验Key是否为空
if (empty($key)) {
return json(['code' => 1, 'msg' => '请传入要下载的文件Key']);
}

try {
$config = Config::get('minio');
$s3Client = new S3Client([
'endpoint' => $config['endpoint'],
'use_path_style_endpoint' => $config['use_path_style_endpoint'],
'credentials' => $config['credentials'],
'region' => $config['region'],
]);

// 获取文件对象
$result = $s3Client->getObject([
'Bucket' => $config['bucket'],
'Key' => $key
]);

// 处理下载文件名(默认使用Key中的原始文件名)
if (empty($downloadName)) {
$downloadName = basename($key);
}

// 获取文件MIME类型(用于响应头)
$mimeType = $result->get('ContentType') ?? 'application/octet-stream';
// 获取文件内容
$fileContent = (string)$result->get('Body');

// 构建响应(触发浏览器下载)
$response = response($fileContent)
->header('Content-Type', $mimeType)
->header('Content-Disposition', 'attachment; filename="' . rawurlencode($downloadName) . '"')
->header('Content-Length', strlen($fileContent));

return $response;

} catch (AwsException $e) {
return json(['code' => 1, 'msg' => '下载失败:' . $e->getMessage()]);
}
}

5.2 调用方式

  1. 默认文件名下载:

    1
    GET: http://你的域名/DemoController/downloadImg?key=DemoFile/20251118/691c71a48050d_xxx.png
  2. 自定义文件名下载:

    1
    GET: http://你的域名/DemoController/downloadImg?key=DemoFile/20251118/691c71a48050d_xxx.png&downloadName=自定义图片.png

下载效果:浏览器会自动触发文件下载,文件名符合预期:

MinIO文件下载效果
图:文件下载成功反馈

六、关键注意事项

  1. 存储桶权限:确保 MinIO 存储桶已开启「读/写」权限,否则会出现上传/删除失败
  2. 文件索引(Key):删除/下载时必须传递完整的 Key(含目录结构),否则会提示「文件不存在」
  3. 异常处理:代码中已包含 AWS SDK 异常和通用异常捕获,便于问题排查
  4. 性能优化:大文件上传建议使用分片上传(需扩展 SDK 功能),本示例适用于普通图片/小文件
  5. 安全建议
    • 生产环境建议启用 SSL(HTTPS)
    • Access Key/Secret Key 建议通过环境变量配置(.env 文件),避免硬编码
    • 对上传文件进行大小限制(如 $imgFile->getSize() <= 1024*1024*5 限制5MB)
    • 对文件类型进行严格校验(避免上传恶意文件)

七、常见问题排查

  1. 上传失败:无法连接 MinIO

    • 检查 MinIO 服务是否启动
    • 检查 endpoint 地址和端口是否正确
    • 检查服务器防火墙是否开放 MinIO 端口(默认9000)
  2. 上传失败:Access Denied

    • 检查 Access Key/Secret Key 是否正确
    • 检查存储桶是否存在
    • 检查存储桶权限是否为「读/写」
  3. 文件无法访问

    • 检查 MinIO 存储桶是否开启「公开访问」(或配置访问策略)
    • 检查文件 URL 是否正确(格式:endpoint/bucket/key
  4. 删除/下载失败:文件不存在

    • 检查传递的 key 是否完整(含目录结构)
    • 检查 key 中的大小写是否与 MinIO 中一致(MinIO Key 区分大小写)