7.1 文件上传全流程
概述
上传采用两段式:SaveTemp 先入临时目录(白名单+大小+图片嗅探校验),表单保存时 SaveFromTemp 移动到正式子目录。未走第二段的 temp 文件由每日 03:30 清理任务过期回收,不污染正式目录。
上传接口
text
POST /api/upload/uploadFile (sys:upload:file)multipart 字段名 file
POST /api/upload/uploadFiles (sys:upload:files)多文件
入参 type=image 时按图片规则校验,否则按普通文件multipart 字段约定:*ghttp.UploadFile + p:"file" 标签。
两段式流程图
text
阶段一(上传时,POST /api/upload/uploadFile):
浏览器 ──multipart──▶ SaveTemp
│ ① 大小 ≤ file.maxSize(MB)
│ ② 扩展名 ∈ file.fileExt / file.imageExt
│ ③ type=image:前512B 魔数嗅探须 image/*
▼
落盘 {uploadDir}/temp/{yyyymmdd}/{32位hex}.{ext}
▼
返回 {url:"{domainUrl}/temp/...", path:"temp/.../x.png"(入库形态)} → 前端预览
阶段二(表单保存时,spec.saveTransform / SaveFileField):
表单携带 temp URL ──▶ SaveFromTemp(url, saveDir="user")
│ 剥离 domainUrl → 相对路径
│ 以 temp/ 开头?──否──▶ 已正式/外站:归一化或原样返回
│ 是
▼
{uploadDir}/{saveDir}/{yyyymmdd}/{HHmmss}_{4字节hex}.{ext}(Rename,跨卷复制回退)
▼
库中写入相对路径 "user/20261017/153012_ab12.png"
▼
读侧 GetFileUrl:domainUrl + "/" + 相对路径 ──▶ 前端展示SaveTemp 校验链(第一段)
text
1) 大小 ≤ file.maxSize(MB,默认 10)
2) 扩展名白名单:file.fileExt(普通)/ file.imageExt(图片)
3) 读取前 512 字节:type=image 时做魔数嗅探(http.DetectContentType 须 image/*),并据文件头嗅探 MIME
落盘:{file.uploadDir}/{file.tempDir}/{yyyymmdd}/{32位随机hex}.{ext}
返回 Result{Url(可预览完整地址), Path(相对 uploadDir,入库形态), Name, Size, Ext, Mime}SaveFromTemp 处理规则(第二段)
| 入参形态 | 处理 |
|---|---|
| 空值 | 返回 "" |
| 本站 URL(匹配 file.domainUrl 前缀) | 剥离域名得相对路径 |
| 相对路径以 temp/ 开头 | 移动到 {saveDir}/{yyyymmdd}/{HHmmss}_{4字节hex}.{ext}(os.Rename,跨卷回退"复制后删源");返回新相对路径(入库形态) |
| 已是正式相对路径 | 归一化后原样返回(幂等) |
| 外站 http(s) URL | 原样返回(不处理外链) |
SaveFromTempMulti:按逗号拆分逐个转正(如 article.images 多图字段)。
路径契约
text
数据库统一存"相对 uploadDir 的路径"(如 tenant/20260923/x.png)
对外访问地址 = GetFileUrl = file.domainUrl + "/" + 相对路径
(domainUrl 为空时用 /upload/ 相对路径)
静态访问:router SetServerRoot("resource") 提供 /upload/**
Response 中间件对 .svg 强制 attachment + nosniff(防存储型 XSS)声明式用法(ModuleSpec)
go
FileFields: []string{"avatar"} + FileSaveDir: "user"
// 写侧 saveTransform 自动 temp→正式;读侧 readTransform 自动 GetFileUrl 补域名
// MultiFileFields 处理逗号多文件;ContentFields 走富文本流程模块已接入:user(avatar)、article(cover/images/content)、tenant(logo)、example(avatar) 等。
临时目录清理(运营闭环)
text
cmd.go 注册 gcron "0 30 3 * * *"(每日 03:30)→ logic.CleanTemp:
扫描 {uploadDir}/{tempDir} 下目录名为 yyyymmdd 且早于
now-file.tempExpireDays(默认 7 天,0=不清理)的日期目录整目录删除
返回删除数量并记日志常见问题
| 症状 | 排查 |
|---|---|
| 上传图片 404 | file.domainUrl 与静态服务根的相对关系;库中存相对路径,展示层必须经 GetFileUrl |
| 上传被拒 | 扩展名不在白名单 / 超 maxSize / 图片魔数嗅探不过(改后缀伪造) |
| temp 文件堆积 | CleanTemp 的 gcron 已注册?file.tempExpireDays > 0? |
| 跨卷移动失败 | os.Rename 失败自动回退"复制后删源";检查磁盘空间与权限 |
| 富文本图片不显示 | 读取侧走 Detail/List(基座自动 Expand),不要直接 Struct 输出 |