import{_ as i,o as a,c as n,a2 as t}from"./chunks/framework.B-QBKwc1.js";const c=JSON.parse('{"title":"上传配置详解","description":"文件上传配置定义在 config/file.php 和 .env 中,控制上传目录、访问域名、大小限制、允许的文件类型等。","frontmatter":{},"headers":[],"relativePath":"zh/tools/upload-config.md","filePath":"zh/tools/upload-config.md"}'),e={name:"zh/tools/upload-config.md"};function l(p,s,h,d,k,r){return a(),n("div",null,[...s[0]||(s[0]=[t(`
概述
文件上传配置定义在 config/file.php 和 .env 中,控制上传目录、访问域名、大小限制、允许的文件类型等。所有值通过 env() 函数从 .env 读取,支持多环境切换;config/file.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,配置内自动转字节) |
# .env
FILE_UPLOAD_DIR=uploads要求:
public/ 的目录,Web 可直接访问其中的静态文件public/uploads)各环境示例:
| 环境 | 值 | 实际位置 |
|---|---|---|
| 本地开发 | uploads | 项目/public/uploads/ |
| Linux 生产 | uploads(或绝对路径) | Nginx root 下可直接访问 |
| Docker | uploads | 挂载的项目目录内 |
# .env
FILE_DOMAIN_URL=https://cdn.example.com要求:
/ 结尾(读取方会 rtrim 处理,但配置时保持规范)各环境示例:
| 环境 | 域名示例 |
|---|---|
| 本地开发 | (留空,使用相对路径) |
| 虚拟域名开发 | http://file.laravel13.elevue |
| 测试环境 | https://test-cdn.example.com |
| 生产环境 | https://cdn.example.com 或对象存储域名 |
// config/file.php
'temp_dir' => 'temp',说明:
uploadFile 把文件写入 {upload_dir}/temp/Ymd/,不直接落正式目录save_file() / save_content() 依据 temp/ 前缀把文件迁移到 Service 的 fileSaveDir 子目录# .env(单位 MB,config 内 ×1024×1024 转字节)
FILE_MAX_SIZE=10建议值:
| 场景 | 建议值 | 说明 |
|---|---|---|
| 头像上传 | 2 MB | 图片通常不会太大 |
| 文章封面 | 5 MB | 压缩后的图片 |
| 通用文件上传 | 10 MB | 默认值 |
| 文档上传 | 20 MB | PDF/Word 等 |
双层限制
业务层限制生效前,PHP 层必须放行:php.ini 的 upload_max_filesize / post_max_size 需 大于等于 FILE_MAX_SIZE(建议 50M)。
# .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,允许图片、文档、压缩包等常见格式# 开发环境
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,rarpublic/
└── {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} | 时间+随机文件名 | 防冲突与可预测性 |
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/...class ArticleService extends BaseTenantService
{
use HasFileProcessing, /* 其他 Trait */;
protected array $fileFields = ['cover'];
protected string $fileSaveDir = 'article';
}实际路径映射:
配置:
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# 项目方案:public/uploads 位于站点根内,location / 由 try_files 直接返回静态文件
location / {
try_files $uri $uri/ /index.php?$query_string;
}# docker/nginx.conf 已内置:仅入口 index.php 可执行,其余 .php 一律 404
location ~ \\.php$ {
return 404;
}location ~ /\\. {
deny all;
}原因: public/uploads 没有写入权限。
解决:
# Linux
chmod -R 755 public/uploads
chown -R www-data:www-data public/uploads
# Docker:镜像内已 chown(storage / bootstrap/cache / public/uploads)原因: FILE_DOMAIN_URL 配置错误或与前端不同源。
解决:
.env 中 FILE_DOMAIN_URL 是否为前端可访问的地址(改后 php artisan config:clear)原因: PHP 层限制小于业务层。
解决:
; php.ini(修改后重启服务)
upload_max_filesize = 50M
post_max_size = 50M
max_execution_time = 300原因: 上传后未提交表单(或表单未引用)的文件停留在 temp/,不会被迁移。
解决: 定期清理 temp/ 中超过 1 天的文件:
# 定时任务:每天凌晨清理 1 天前的临时文件
find public/uploads/temp/ -type f -mtime +1 -delete| 建议 | 说明 |
|---|---|
| 扩展名白名单 | 只允许已知安全的文件类型(FILE_IMAGE_EXT/FILE_EXT) |
| 移除高危后缀 | svg 已移除;生产白名单去掉 sql/js/css 等可执行类 |
| MIME 类型检查 | 导入等场景配合 finfo 检测真实 MIME(见 7.4 数据导入导出) |
| 随机文件名 | 服务端生成(时间+随机串),防止路径遍历和文件名注入 |
| 文件大小限制 | FILE_MAX_SIZE 防止大文件 DoS |
| Nginx 禁止执行 PHP | 仅入口 index.php 可执行(Docker 配置已内置) |
| 定期清理临时文件 | temp/ 内未被引用的文件按周期清理,避免磁盘空间被占满 |