314 lines
10 KiB
Markdown
314 lines
10 KiB
Markdown
# 7.2 上传配置详解
|
||
|
||
:::tip 概述
|
||
文件上传配置定义在 `config/file.php` 和 `.env` 中,控制上传目录、访问域名、大小限制、允许的文件类型等。所有值通过 `env()` 函数从 `.env` 读取,支持多环境切换;`config/file.php` 中的注释明确标注了每个配置的**读取方**。
|
||
:::
|
||
|
||
## 配置文件
|
||
|
||
```php
|
||
// config/file.php — 文件上传配置(来自项目实际代码,节选)
|
||
return [
|
||
// 文件上传根目录(相对 public),所有上传文件均存放于此
|
||
// 读取方:UploadController::uploadFile()、save_file()、save_remote_file()
|
||
'upload_dir' => env('FILE_UPLOAD_DIR', 'uploads'),
|
||
|
||
// 文件访问域名,用于拼接完整 URL;为空时返回相对路径
|
||
// 读取方:get_file_url()、save_content()、processContentFieldsOnRead()
|
||
'domain_url' => env('FILE_DOMAIN_URL', ''),
|
||
|
||
// 临时文件目录名(相对 upload_dir)
|
||
// 读取方:UploadController::uploadFile()(上传落盘目录)、save_file()(识别 temp/ 开头的临时文件后迁移到正式目录)
|
||
'temp_dir' => 'temp',
|
||
|
||
// 允许上传的图片后缀(逗号分隔)
|
||
// 安全说明:svg 已移除,因其可内嵌 JavaScript 导致 XSS 风险
|
||
'image_ext' => env('FILE_IMAGE_EXT', 'gif,jpg,jpeg,png,bmp,webp'),
|
||
|
||
// 允许上传的文件后缀(逗号分隔)
|
||
'file_ext' => env('FILE_EXT', 'gif,jpg,jpeg,png,bmp,webp,doc,docx,xls,xlsx,ppt,pptx,pdf,zip,rar,txt,csv'),
|
||
|
||
// 单文件最大尺寸(MB),转换为字节存储
|
||
'max_size' => (int) env('FILE_MAX_SIZE', 10) * 1024 * 1024,
|
||
];
|
||
```
|
||
|
||
## 配置项详解
|
||
|
||
| 配置项 | .env 变量 | 默认值 | 说明 |
|
||
|--------|----------|--------|------|
|
||
| `upload_dir` | `FILE_UPLOAD_DIR` | `uploads` | 上传根目录(**相对 `public/`**),所有文件存储于此 |
|
||
| `domain_url` | `FILE_DOMAIN_URL` | (空) | 文件访问域名,用于拼接完整 URL |
|
||
| `temp_dir` | — | `temp` | 临时目录名(相对 upload_dir),上传接口的落盘目录,`save_file()` 据此识别待迁移文件 |
|
||
| `image_ext` | `FILE_IMAGE_EXT` | `gif,jpg,jpeg,png,bmp,webp` | 图片允许后缀(**无 svg**) |
|
||
| `file_ext` | `FILE_EXT` | 图片+文档+压缩包等 | 文件允许后缀 |
|
||
| `max_size` | `FILE_MAX_SIZE` | `10` | 单文件最大大小(MB,配置内自动转字节) |
|
||
|
||
### upload_dir — 上传根目录
|
||
|
||
```ini
|
||
# .env
|
||
FILE_UPLOAD_DIR=uploads
|
||
```
|
||
|
||
**要求:**
|
||
|
||
- 本项目为**相对 `public/` 的目录**,Web 可直接访问其中的静态文件
|
||
- 目录必须存在且有写入权限(Docker 镜像已创建并授权 `public/uploads`)
|
||
- 上传文件随项目目录持久化(Docker 下即宿主机目录,不依赖命名卷)
|
||
|
||
**各环境示例:**
|
||
|
||
| 环境 | 值 | 实际位置 |
|
||
|------|------|---------|
|
||
| 本地开发 | `uploads` | `项目/public/uploads/` |
|
||
| Linux 生产 | `uploads`(或绝对路径) | Nginx root 下可直接访问 |
|
||
| Docker | `uploads` | 挂载的项目目录内 |
|
||
|
||
### domain_url — 文件访问域名
|
||
|
||
```ini
|
||
# .env
|
||
FILE_DOMAIN_URL=https://cdn.example.com
|
||
```
|
||
|
||
**要求:**
|
||
|
||
- 不要以 `/` 结尾(读取方会 `rtrim` 处理,但配置时保持规范)
|
||
- 必须是前端可访问的域名
|
||
- 本地开发可留空(接口返回相对路径,由同源/Vite 代理直接访问)
|
||
|
||
**各环境示例:**
|
||
|
||
| 环境 | 域名示例 |
|
||
|------|---------|
|
||
| 本地开发 | (留空,使用相对路径) |
|
||
| 虚拟域名开发 | `http://file.laravel13.elevue` |
|
||
| 测试环境 | `https://test-cdn.example.com` |
|
||
| 生产环境 | `https://cdn.example.com` 或对象存储域名 |
|
||
|
||
### temp_dir — 临时目录
|
||
|
||
```php
|
||
// config/file.php
|
||
'temp_dir' => 'temp',
|
||
```
|
||
|
||
**说明:**
|
||
|
||
- 上传接口 `uploadFile` 把文件写入 `{upload_dir}/temp/Ymd/`,不直接落正式目录
|
||
- `save_file()` / `save_content()` 依据 `temp/` 前缀把文件迁移到 Service 的 `fileSaveDir` 子目录
|
||
- 正式目录未被表单引用的临时文件可按周期清理(见下方「问题 4」)
|
||
|
||
### max_size — 文件大小限制
|
||
|
||
```ini
|
||
# .env(单位 MB,config 内 ×1024×1024 转字节)
|
||
FILE_MAX_SIZE=10
|
||
```
|
||
|
||
**建议值:**
|
||
|
||
| 场景 | 建议值 | 说明 |
|
||
|------|--------|------|
|
||
| 头像上传 | 2 MB | 图片通常不会太大 |
|
||
| 文章封面 | 5 MB | 压缩后的图片 |
|
||
| 通用文件上传 | 10 MB | 默认值 |
|
||
| 文档上传 | 20 MB | PDF/Word 等 |
|
||
|
||
:::warning 双层限制
|
||
业务层限制生效前,PHP 层必须放行:`php.ini` 的 `upload_max_filesize` / `post_max_size` 需 **大于等于** `FILE_MAX_SIZE`(建议 50M)。
|
||
:::
|
||
|
||
### image_ext / file_ext — 后缀白名单
|
||
|
||
```ini
|
||
# .env
|
||
FILE_IMAGE_EXT=gif,jpg,jpeg,png,bmp,webp # uploadFile?type=image 校验
|
||
FILE_EXT=gif,jpg,jpeg,png,bmp,webp,doc,docx,xls,xlsx,ppt,pptx,pdf,zip,rar,txt,csv # uploadFile(默认 type=file)校验
|
||
```
|
||
|
||
**说明:**
|
||
|
||
- `image_ext` 用于 `type=image`,只允许图片格式;**svg 已移除**(内嵌 JS 的 XSS 风险)
|
||
- `file_ext` 用于 `type=file`,允许图片、文档、压缩包等常见格式
|
||
- 白名单为空数组时跳过校验(防御性兜底),生产环境务必保留非空配置
|
||
|
||
## .env 配置示例
|
||
|
||
```ini
|
||
# 开发环境
|
||
FILE_UPLOAD_DIR=uploads
|
||
FILE_DOMAIN_URL=
|
||
FILE_MAX_SIZE=10
|
||
|
||
# 生产环境(示例)
|
||
FILE_UPLOAD_DIR=uploads
|
||
FILE_DOMAIN_URL=https://cdn.example.com
|
||
FILE_MAX_SIZE=10
|
||
FILE_IMAGE_EXT=gif,jpg,jpeg,png,bmp,webp
|
||
FILE_EXT=gif,jpg,jpeg,png,bmp,webp,doc,docx,xls,xlsx,pdf,zip,rar
|
||
```
|
||
|
||
## 目录结构
|
||
|
||
```text
|
||
public/
|
||
└── {upload_dir}/ # upload_dir(默认 uploads)
|
||
├── temp/ # 临时目录(UploadController 上传落盘)
|
||
│ └── 20261002/
|
||
│ └── 123456_a1b2c3d4e5f6.jpg
|
||
├── article/ # 正式目录(由 Service 的 fileSaveDir 配置)
|
||
│ └── 20261002/
|
||
│ └── 153052_9f8e7d6c5b4a.pdf
|
||
├── example/
|
||
├── user/
|
||
│ └── avatar/
|
||
└── file_template/
|
||
```
|
||
|
||
### 目录命名规则
|
||
|
||
| 目录 | 说明 | 来源 |
|
||
|------|------|------|
|
||
| `temp/` | 临时文件目录(上传接口落盘) | `config/file.php → temp_dir` |
|
||
| `{module}/` | 按模块的正式目录 | Service 的 `fileSaveDir`(未配置按模型名推导) |
|
||
| `{Ymd}/` | 日期子目录 | 自动创建 |
|
||
| `{His}_{random}.{ext}` | 时间+随机文件名 | 防冲突与可预测性 |
|
||
|
||
## 文件生命周期
|
||
|
||
```text
|
||
1. 前端上传 → POST /api/upload/uploadFile
|
||
保存到: public/uploads/temp/20261002/123456_a1b2c3d4e5f6.jpg
|
||
返回: filePath + fileUrl
|
||
|
||
2. 前端提交表单 → 携带 filePath
|
||
POST /api/example/add { name: "测试", avatar: "temp/20261002/..." }
|
||
|
||
3. ExampleService::add() → processFileFieldsOnSave → save_file()
|
||
temp/ → 迁移到 fileSaveDir(example/)后,正式目录相对路径入库
|
||
(其它分支:去掉域名直接用;外部 URL → 下载保存)
|
||
|
||
4. ExampleService::detail() → processFileFieldsOnRead → get_file_url()
|
||
相对路径 → 拼接 FILE_DOMAIN_URL
|
||
example/20261002/...
|
||
→ https://cdn.example.com/example/20261002/...
|
||
```
|
||
|
||
## 与 Service 层的配合
|
||
|
||
```php
|
||
class ArticleService extends BaseTenantService
|
||
{
|
||
use HasFileProcessing, /* 其他 Trait */;
|
||
|
||
protected array $fileFields = ['cover'];
|
||
protected string $fileSaveDir = 'article';
|
||
}
|
||
```
|
||
|
||
**实际路径映射:**
|
||
|
||
```text
|
||
配置:
|
||
FILE_UPLOAD_DIR = uploads
|
||
FILE_DOMAIN_URL = https://cdn.example.com
|
||
fileSaveDir = article
|
||
|
||
结果:
|
||
上传文件: public/uploads/temp/20261002/xxx.jpg
|
||
Service 归一化后入库: article/20261002/xxx.jpg
|
||
前端显示: https://cdn.example.com/article/20261002/xxx.jpg
|
||
```
|
||
|
||
## Nginx 配置
|
||
|
||
### 静态文件直接由 Nginx 返回
|
||
|
||
```nginx
|
||
# 项目方案:public/uploads 位于站点根内,location / 由 try_files 直接返回静态文件
|
||
location / {
|
||
try_files $uri $uri/ /index.php?$query_string;
|
||
}
|
||
```
|
||
|
||
### 禁止上传目录执行 PHP
|
||
|
||
```nginx
|
||
# docker/nginx.conf 已内置:仅入口 index.php 可执行,其余 .php 一律 404
|
||
location ~ \.php$ {
|
||
return 404;
|
||
}
|
||
```
|
||
|
||
### 禁止访问隐藏文件
|
||
|
||
```nginx
|
||
location ~ /\. {
|
||
deny all;
|
||
}
|
||
```
|
||
|
||
## 常见问题
|
||
|
||
### 问题 1:上传提示"目录不可写"
|
||
|
||
**原因:** `public/uploads` 没有写入权限。
|
||
|
||
**解决:**
|
||
|
||
```bash
|
||
# Linux
|
||
chmod -R 755 public/uploads
|
||
chown -R www-data:www-data public/uploads
|
||
|
||
# Docker:镜像内已 chown(storage / bootstrap/cache / public/uploads)
|
||
```
|
||
|
||
### 问题 2:上传后图片不显示
|
||
|
||
**原因:** `FILE_DOMAIN_URL` 配置错误或与前端不同源。
|
||
|
||
**解决:**
|
||
|
||
1. 检查 `.env` 中 `FILE_DOMAIN_URL` 是否为前端可访问的地址(改后 `php artisan config:clear`)
|
||
2. 留空时用相对路径——确认前端与后端同源或经代理
|
||
3. 浏览器直接打开图片 URL 验证
|
||
|
||
### 问题 3:文件大小限制不生效
|
||
|
||
**原因:** PHP 层限制小于业务层。
|
||
|
||
**解决:**
|
||
|
||
```ini
|
||
; php.ini(修改后重启服务)
|
||
upload_max_filesize = 50M
|
||
post_max_size = 50M
|
||
max_execution_time = 300
|
||
```
|
||
|
||
### 问题 4:临时文件未清理
|
||
|
||
**原因:** 上传后未提交表单(或表单未引用)的文件停留在 `temp/`,不会被迁移。
|
||
|
||
**解决:** 定期清理 `temp/` 中超过 1 天的文件:
|
||
|
||
```bash
|
||
# 定时任务:每天凌晨清理 1 天前的临时文件
|
||
find public/uploads/temp/ -type f -mtime +1 -delete
|
||
```
|
||
|
||
## 安全建议
|
||
|
||
| 建议 | 说明 |
|
||
|------|------|
|
||
| 扩展名白名单 | 只允许已知安全的文件类型(`FILE_IMAGE_EXT`/`FILE_EXT`) |
|
||
| 移除高危后缀 | svg 已移除;生产白名单去掉 `sql/js/css` 等可执行类 |
|
||
| MIME 类型检查 | 导入等场景配合 `finfo` 检测真实 MIME(见 [7.4 数据导入导出](/zh/tools/excel.html)) |
|
||
| 随机文件名 | 服务端生成(时间+随机串),防止路径遍历和文件名注入 |
|
||
| 文件大小限制 | `FILE_MAX_SIZE` 防止大文件 DoS |
|
||
| Nginx 禁止执行 PHP | 仅入口 `index.php` 可执行(Docker 配置已内置) |
|
||
| 定期清理临时文件 | `temp/` 内未被引用的文件按周期清理,避免磁盘空间被占满 |
|