签入版本
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
# 7.1 文件上传全流程
|
||||
|
||||
:::tip 概述
|
||||
上传采用两段式: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 输出 |
|
||||
Reference in New Issue
Block a user