26.9:版本号统一 + CI 精简 + 前端产物重建
CI 测试 / go vet + go test (push) Successful in 49s

- 全项目版本号统一:v3.x 迭代号(26.9/26.9/26.9/26.9 及裸 v2/v3)→ 26.9,
  覆盖 Go 注释 / 文档 / openapi.yaml / README×4 / 前端源码(80+ 处)
- v31_test.go 更名 custom_code_test.go;TestV2AccessorDefaults → TestKVAccessorDefaults
- docs/api/00-overview.md 更新日志合并为单条 26.9 条目(修复错位拼接)
- .goreleaser.yaml 头部注释与实际一致(Pro 2.18.1 / GITEA_TOKEN / semver tag 要求)
- CI:release-image.yml → ci.yml,仅保留 vet+test 门禁;
  镜像发布移交 GoReleaser Pro(原 build-push 的 tag 校验与 26.9 版本方案冲突,历史 9 次失败)
- 前端重建:server/web/dist 与 web-embed 同步(docs 文案嵌入更新)
This commit is contained in:
2026-09-08 03:16:51 +08:00
parent 27432218c4
commit 84df9996cb
122 changed files with 247 additions and 12268 deletions
+15 -15
View File
@@ -78,7 +78,7 @@ func registerAdmin(r *gin.Engine, d *Deps) {
authed.PATCH("/settings/password", d.adminChangePassword)
authed.POST("/settings/password", d.adminChangePassword)
// v3 存储引擎:运行时热切换(健康检查通过才生效,失败保持原引擎)
// 26.9 存储引擎:运行时热切换(健康检查通过才生效,失败保持原引擎)
authed.POST("/storage/switch", d.adminStorageSwitch)
// 审计日志查询(需求 ③;logs 为 list 的别名)
@@ -404,7 +404,7 @@ func buildAdminFileItem(fc *model.FileCodes, now time.Time) gin.H {
"hasDownloadLimit": fc.ExpiredCount >= 0, "has_download_limit": fc.ExpiredCount >= 0,
"isPermanent": fc.ExpiredAt == nil && fc.ExpiredCount < 0, "is_permanent": fc.ExpiredAt == nil && fc.ExpiredCount < 0,
"remainingDownloads": remaining, "remaining_downloads": remaining,
"engine": fc.Engine, // v3:归属引擎(管理端展示/排查用)
"engine": fc.Engine, // 26.9:归属引擎(管理端展示/排查用)
}
if fc.FileHash != nil {
item["fileHash"] = *fc.FileHash
@@ -784,7 +784,7 @@ func (d *Deps) adminFilePreview(c *gin.Context) {
// ============ 配置 ============
// configKeys 管理端可见/可改的配置键(不含 jwt_secretadmin_token 屏蔽展示)。
// v2 新增键(需求 ①②③④⑩):背景图、页脚、通知开关、保存/存储策略、频率限制。
// 26.9 新增键(需求 ①②③④⑩):背景图、页脚、通知开关、保存/存储策略、频率限制。
var configKeys = []string{
"site_name", "name", "description", "page_explain", "keywords",
"notify_title", "notify_content", "notify_enabled", "logo_url", "favicon_url",
@@ -794,11 +794,11 @@ var configKeys = []string{
"code_generate_type", "enableChunk",
"uploadMinute", "uploadCount", "errorMinute", "errorCount",
"loginCount", "loginMinute",
"opacity", "background", "showAdminAddr", "robotsText", "site_domain", // v3.1:站点对外域名
"upload_rate", "download_rate", // v3.2:上下行带宽字节/秒(0=不限速)
"opacity", "background", "showAdminAddr", "robotsText", "site_domain", // 26.9:站点对外域名
"upload_rate", "download_rate", // 26.9:上下行带宽字节/秒(0=不限速)
"adminSessionExpire", "storage_path", "local_storage_path",
"file_storage",
// v3 存储引擎与引擎参数(热切换;凭据为敏感键,get 掩码/update 空跳过)
// 26.9 存储引擎与引擎参数(热切换;凭据为敏感键,get 掩码/update 空跳过)
"storage_engine",
"local_storage_path",
"webdav_url", "webdav_root_path", "webdav_username", "webdav_password",
@@ -807,7 +807,7 @@ var configKeys = []string{
}
// intConfigKeys 需按 schema 边界校验的整型键(adminConfigUpdate 归一化用)。
// v1 既有键保留原语义;v2 新增键(max_file_size/max_save_count/notify_enabled
// v1 既有键保留原语义;26.9 新增键(max_file_size/max_save_count/notify_enabled
// 的边界来自 settings.KVSchema(单一事实来源在 config/schema.go)。
var intConfigKeys = []string{
"openUpload", "enableChunk", "showAdminAddr", "storageLimit",
@@ -815,7 +815,7 @@ var intConfigKeys = []string{
"loginCount", "loginMinute", "max_save_seconds", "uploadSize",
"adminSessionExpire",
"max_save_count", "max_file_size", "notify_enabled",
"upload_rate", "download_rate", // v3.2
"upload_rate", "download_rate", // 26.9
}
// validateConfigValue 按 settings.KVSchema 校验单个配置值:
@@ -905,7 +905,7 @@ func toStrSlice(v any) []string {
}
// adminConfigGet 读取配置(对齐参考 get_configadmin_token 屏蔽、jwt_secret 不下发)。
// v3:引擎凭据类敏感键返回掩码占位(前端表单"留空=不修改");storage_engine 为当前热切换后的引擎。
// 26.9:引擎凭据类敏感键返回掩码占位(前端表单"留空=不修改");storage_engine 为当前热切换后的引擎。
func (d *Deps) adminConfigGet(c *gin.Context) {
cfg := d.Cfg
out := gin.H{}
@@ -925,7 +925,7 @@ func (d *Deps) adminConfigGet(c *gin.Context) {
}
// jwt_secret 永不下发
delete(out, "jwt_secret")
// 引擎运行时状态(v3:热切换即时生效,无需重启)
// 引擎运行时状态(26.9:热切换即时生效,无需重启)
out["_engine_hint"] = gin.H{
"storage_backend": d.Store.CurrentName(),
"engines": gin.H{"local": true, "s3": true, "webdav": true},
@@ -973,7 +973,7 @@ func (d *Deps) adminConfigUpdate(c *gin.Context) {
if !known {
continue
}
// 类型归一 + schema 边界校验(v2:数值/字符串长度/列表键统一走 KVSchema)
// 类型归一 + schema 边界校验(26.9:数值/字符串长度/列表键统一走 KVSchema)
isInt := false
for _, key := range intConfigKeys {
if key == k {
@@ -1067,7 +1067,7 @@ func (d *Deps) adminConfigUpdate(c *gin.Context) {
dbPatch["jwt_secret"] = settings.GenerateJWTSecret()
}
// v3.1site_domain 规范化(http(s)://host[:port];空=用当前地址)
// 26.9site_domain 规范化(http(s)://host[:port];空=用当前地址)
if raw, ok := dbPatch["site_domain"]; ok {
sv, _ := raw.(string)
normalized, err := normalizeSiteDomain(sv)
@@ -1078,7 +1078,7 @@ func (d *Deps) adminConfigUpdate(c *gin.Context) {
dbPatch["site_domain"] = normalized
}
// v3 引擎键处理:参数键与 storage_engine 分离。
// 26.9 引擎键处理:参数键与 storage_engine 分离。
// 1) storage_engine 只接受合法枚举;
// 2) 敏感凭据键空串/掩码=不修改(避免管理端表单回显把密钥抹掉);
// 3) 先持久化普通键+参数键 → Invalidate 对应引擎缓存 → 再尝试 Switch 新引擎;
@@ -1152,7 +1152,7 @@ func engineOfParamKeys(patch map[string]any) []string {
return out
}
// adminStorageSwitch v3 存储引擎热切换:{engine:"local"|"s3"|"webdav"}。
// adminStorageSwitch 26.9 存储引擎热切换:{engine:"local"|"s3"|"webdav"}。
// 成功:持久化 storage_engine KV 并返回当前引擎;失败:503 且原引擎不变。
func (d *Deps) adminStorageSwitch(c *gin.Context) {
var body struct {
@@ -1302,7 +1302,7 @@ func (d *Deps) fileByID(c *gin.Context, id int64) (*model.FileCodes, error) {
// deleteFileCode 删除分享记录与存储文件(文本分享无存储文件)。
func (d *Deps) deleteFileCode(c *gin.Context, fc *model.FileCodes) error {
if fc.Text == nil && fc.FilePath != nil && fc.UUIDFileName != nil {
// v3:删除走文件归属引擎(旧引擎里的文件也要能删掉)
// 26.9:删除走文件归属引擎(旧引擎里的文件也要能删掉)
if delStore, dErr := d.storeFor(fc.Engine); dErr == nil {
if err := delStore.DeleteFile(c.Request.Context(), fileSavePath(fc)); err != nil && !errors.Is(err, storage.ErrNotFound) {
return errInternal("存储文件删除失败: " + err.Error())
+9 -9
View File
@@ -99,7 +99,7 @@ func (d *Deps) chunkInit(c *gin.Context) {
// 服务端按分片数上限校验总大小(防分片声明绕过)
totalChunks := (req.FileSize + chunkSize - 1) / chunkSize
maxPossible := totalChunks * chunkSize
// v2 需求 ④⑩:动态策略校验(max_file_size0=回落 uploadSize
// 26.9 需求 ④⑩:动态策略校验(max_file_size0=回落 uploadSize
if err := d.CurrentUploadPolicy().CheckSize(maxPossible); err != nil {
auditUploadEntry(c, "", safeName, req.FileSize, 0)
auditRecordFailed(c, d.AuditSvc, "文件大小超过限制")
@@ -165,7 +165,7 @@ func (d *Deps) chunkInit(c *gin.Context) {
ChunkHash: req.FileHash,
FileName: safeName,
SavePath: savePath,
Engine: d.Store.CurrentName(), // v3:会话归属引擎(分片/合并全程走同一引擎)
Engine: d.Store.CurrentName(), // 26.9:会话归属引擎(分片/合并全程走同一引擎)
}
if err := d.DB.WithContext(ctx).Create(&session).Error; err != nil {
releaseStorage(ctx, d.DB, resToken)
@@ -389,7 +389,7 @@ func (d *Deps) saveOneChunk(c *gin.Context, ctx context.Context, session *model.
ChunkSize: session.ChunkSize,
FileName: session.FileName,
SavePath: session.SavePath,
Engine: session.Engine, // v3:继承会话引擎
Engine: session.Engine, // 26.9:继承会话引擎
}
if err := d.DB.WithContext(ctx).
Where("upload_id = ? AND chunk_index = ?", session.UploadID, idx).
@@ -444,7 +444,7 @@ func (d *Deps) chunkStatus(c *gin.Context) {
type chunkCompleteRequest struct {
ExpireValue int `json:"expire_value" form:"expire_value"`
ExpireStyle string `json:"expire_style" form:"expire_style"`
Code string `json:"code" form:"code"` // v3.1:自定义提取码(4-8 位字母数字,空=随机)
Code string `json:"code" form:"code"` // 26.9:自定义提取码(4-8 位字母数字,空=随机)
}
// chunkComplete 合并分片并创建分享(对齐参考 complete_upload)。
@@ -489,7 +489,7 @@ func (d *Deps) chunkComplete(c *gin.Context) {
respondError(c, err)
return
}
// v3.1:自定义提取码(合并前校验,失败快速返回)
// 26.9:自定义提取码(合并前校验,失败快速返回)
if err := validatePickupCode(req.Code); err != nil {
auditUploadEntry(c, uploadID, session.FileName, session.FileSize, 0)
auditRecordFailed(c, d.AuditSvc, "提取码非法")
@@ -539,7 +539,7 @@ func (d *Deps) chunkComplete(c *gin.Context) {
}
return rec.ChunkHash, nil
}
// v3:合并走会话归属引擎(会话创建时的引擎,即使中途热切换也不受影响)
// 26.9:合并走会话归属引擎(会话创建时的引擎,即使中途热切换也不受影响)
mergeStore, sErr := d.storeFor(session.Engine)
if sErr != nil {
auditUploadEntry(c, uploadID, session.FileName, session.FileSize, session.FileSize)
@@ -556,7 +556,7 @@ func (d *Deps) chunkComplete(c *gin.Context) {
return
}
// 创建分享记录(v3.1:支持自定义提取码)
// 创建分享记录(26.9:支持自定义提取码)
code, err := pickCustomCode(ctx, d.DB, d.Cfg, req.Code)
if err == nil {
fc := model.FileCodes{
@@ -568,7 +568,7 @@ func (d *Deps) chunkComplete(c *gin.Context) {
ExpiredAt: exp.ExpiredAt,
ExpiredCount: exp.ExpiredCount,
UsedCount: exp.UsedCount,
Engine: session.Engine, // v3:归属引擎戳
Engine: session.Engine, // 26.9:归属引擎戳
}
// 拆分路径与文件名(对齐参考:path=dirname(save_path), uuid=basename
dir, name := splitDirBase(session.SavePath)
@@ -578,7 +578,7 @@ func (d *Deps) chunkComplete(c *gin.Context) {
fc.Prefix = trimExt(name)
fc.Suffix = ext
err = d.DB.WithContext(ctx).Create(&fc).Error
err = mapCodeConflict(err) // v3.1
err = mapCodeConflict(err) // 26.9
}
if err == nil {
// 成功:清理分片与记录(走归属引擎)
@@ -1,6 +1,6 @@
package api
// v3.1:自定义提取码与站点域名单元测试。
// 26.9:自定义提取码与站点域名单元测试。
import (
"encoding/json"
@@ -13,7 +13,7 @@ import (
"github.com/gin-gonic/gin"
)
// postForm 以 urlencoded 表单调用 handlerv3.1 测试辅助)。
// postForm 以 urlencoded 表单调用 handler26.9 测试辅助)。
func postForm(d *Deps, path string, fields map[string]string) *httptest.ResponseRecorder {
form := url.Values{}
for k, v := range fields {
+8 -8
View File
@@ -125,7 +125,7 @@ func generateCode(style string) string {
return string(b)
}
// ============ 自定义提取码(v3.1,防撞库)============
// ============ 自定义提取码(26.9,防撞库)============
const (
// pickupCodeMinLen 最小长度:L3 由 4 提升至 5(4 位码空间仅 168 万,
@@ -172,7 +172,7 @@ func pickCustomCode(ctx context.Context, db *gorm.DB, cfg *config.Config, custom
return custom, nil
}
// mapCodeConflict v3.1:分享记录创建失败时,若是自定义码唯一索引冲突(并发兜底,
// mapCodeConflict 26.9:分享记录创建失败时,若是自定义码唯一索引冲突(并发兜底,
// pickCustomCode 的预查重未覆盖竞态),转为友好 400;其余错误原样返回。
func mapCodeConflict(err error) error {
if err == nil {
@@ -188,9 +188,9 @@ func mapCodeConflict(err error) error {
return err
}
// ============ 站点对外域名(v3.1============
// ============ 站点对外域名(26.9============
// SiteDomain 站点对外域名规范化(v3.1):空串合法(分享链接用当前访问地址)。
// SiteDomain 站点对外域名规范化(26.9):空串合法(分享链接用当前访问地址)。
// 接受 http(s)://host[:port] 或裸 host[:port](自动补 http://,内网场景)。
// 拒绝路径/查询/片段/用户信息/非 http(s) 协议/非法主机字符(防 javascript: 注入分享链接)。
var siteDomainHostRe = regexp.MustCompile(`^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$`)
@@ -275,7 +275,7 @@ type expireResult struct {
// resolveExpire 校验 expire_style 白名单并计算过期信息。
// 对齐参考:max_save_seconds>0 时为最长保存上限(超限 403),否则默认 7 天上限;
// v2 需求 ④:style=count 时 expire_value 不得超出 max_save_count0=不限制,超限 403)。
// 26.9 需求 ④:style=count 时 expire_value 不得超出 max_save_count0=不限制,超限 403)。
func resolveExpire(cfg *config.Config, expireValue int, expireStyle string) (*expireResult, error) {
allowed := cfg.ExpireStyle()
okStyle := false
@@ -358,7 +358,7 @@ func formatDurationCN(d time.Duration) string {
// ============ 存储路径 / 容量预留 ============
// storeFor v3:按归属引擎取存储实例(空戳/未知名回落当前引擎,兼容历史数据)。
// storeFor 26.9:按归属引擎取存储实例(空戳/未知名回落当前引擎,兼容历史数据)。
func (d *Deps) storeFor(engine string) (storage.Storage, error) {
if engine == "" || !storage.ValidEngine(engine) {
return d.Store, nil
@@ -656,7 +656,7 @@ func readMultipartHeader(f multipart.File, n int64) []byte {
// ============ 杂项 ============
// humanSize 把字节数转成人类可读描述(B/KB/MB/GB 自适应;
// v2 需求④:max_file_size 支持子 MB 上限,固定 MB 格式会显示 0.00 MB)。
// 26.9 需求④:max_file_size 支持子 MB 上限,固定 MB 格式会显示 0.00 MB)。
func humanSize(n int64) string {
const kb, mb, gb = int64(1024), int64(1024 * 1024), int64(1024 * 1024 * 1024)
switch {
@@ -728,7 +728,7 @@ func bindJSONOrForm(c *gin.Context, obj any) error {
}
return nil
}
// v3.1.1 兼容归一化:旧前端 bundlefetch 字符串 body 默认 text/plain)发的
// 26.9 兼容归一化:旧前端 bundlefetch 字符串 body 默认 text/plain)发的
// 是 text/plain + urlencoded 格式。此类请求改写 Content-Type 后走表单绑定,
// 否则 ShouldBind 对 text/plain 不解析,非空字段全部丢失。
base := ct
+1 -1
View File
@@ -1,4 +1,4 @@
// policy.go — v2 上传策略统一读取与校验(需求 ④⑩)。
// policy.go — 26.9 上传策略统一读取与校验(需求 ④⑩)。
//
// 管理端在后台设置页修改策略(settings KVt1 schema)后,上传链路
// share/file、chunk、presign)每次请求实时读取当前策略并动态校验:
+6 -6
View File
@@ -55,7 +55,7 @@ func newPolicyTestDeps(t *testing.T) *Deps {
if err != nil {
t.Fatalf("storage.NewLocalStorage: %v", err)
}
// v3:包装为 Managerbuild 直接返回 local 实例,测试无需真实多引擎)
// 26.9:包装为 Managerbuild 直接返回 local 实例,测试无需真实多引擎)
storeMgr := storage.NewManager("local", store, func(string) (storage.Storage, error) {
return storage.NewLocalStorage(filepath.Join(dir, "storage"))
})
@@ -141,13 +141,13 @@ func respBody(t *testing.T, w *httptest.ResponseRecorder) (code int, data map[st
// pngMagic 最小合法 PNG 头(magic 校验可识别)。
var pngMagic = []byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}
// ============ ① 公开 configv2 展示与策略字段下发 ============
// ============ ① 公开 config26.9 展示与策略字段下发 ============
// TestPublicConfigV2Fields 验证 /api/v1/config 下发背景/页脚/备案/通知与策略范围,
// 且响应不包含任何敏感键(admin_token/jwt_secret)。
func TestPublicConfigV2Fields(t *testing.T) {
d := newPolicyTestDeps(t)
// 管理端先设置 v2 展示字段
// 管理端先设置 26.9 展示字段
if w := patchConfig(d, map[string]any{
"background_url": "https://cdn.example.com/bg.png",
"footer_text": "自定义页脚内容",
@@ -204,9 +204,9 @@ func TestPublicConfigV2Fields(t *testing.T) {
}
}
// ============ ② 管理端 get/updatev2 键全链路 + 类型范围校验 ============
// ============ ② 管理端 get/update26.9 键全链路 + 类型范围校验 ============
// TestAdminConfigV2RoundTrip 验证 v2 新键 update → get → public 的往返,
// TestAdminConfigV2RoundTrip 验证 26.9 新键 update → get → public 的往返,
// 且 admin_token 屏蔽、jwt_secret 不下发。
func TestAdminConfigV2RoundTrip(t *testing.T) {
d := newPolicyTestDeps(t)
@@ -480,7 +480,7 @@ func TestPolicySnapshotMatchesConfig(t *testing.T) {
// 编译期保证 fmt 被使用(测试辅助函数中错误路径占位)。
var _ = fmt.Sprintf
// ============ v3 存储引擎热切换 ============
// ============ 26.9 存储引擎热切换 ============
// switchEngine 调用 POST /admin/storage/switch。
func switchEngine(d *Deps, engine string) *httptest.ResponseRecorder {
+14 -14
View File
@@ -51,7 +51,7 @@ type presignInitRequest struct {
FileSize int64 `json:"file_size" form:"file_size"`
ExpireValue int `json:"expire_value" form:"expire_value"`
ExpireStyle string `json:"expire_style" form:"expire_style"`
Code string `json:"code" form:"code"` // v3.1:自定义提取码(init 时校验,完成时落库)
Code string `json:"code" form:"code"` // 26.9:自定义提取码(init 时校验,完成时落库)
}
// presignInit 初始化预签名上传(对齐参考 presign_upload_init):
@@ -74,7 +74,7 @@ func (d *Deps) presignInit(c *gin.Context) {
response.Fail(c, http.StatusBadRequest, "文件名非法")
return
}
// v3.1:自定义提取码提前校验(init 时快速失败;完成请求须再次携带)
// 26.9:自定义提取码提前校验(init 时快速失败;完成请求须再次携带)
if err := validatePickupCode(req.Code); err != nil {
auditRecordFailed(c, d.AuditSvc, "提取码非法")
respondError(c, err)
@@ -86,7 +86,7 @@ func (d *Deps) presignInit(c *gin.Context) {
respondError(c, err)
return
}
// v2 需求 ④⑩:动态策略校验(max_file_size0=回落 uploadSize
// 26.9 需求 ④⑩:动态策略校验(max_file_size0=回落 uploadSize
if err := d.CurrentUploadPolicy().CheckSize(req.FileSize); err != nil {
auditUploadEntry(c, "", safeName, req.FileSize, 0)
auditRecordFailed(c, d.AuditSvc, "文件大小超过限制")
@@ -146,7 +146,7 @@ func (d *Deps) presignInit(c *gin.Context) {
FileSize: req.FileSize,
SavePath: savePath,
Mode: mode,
Engine: d.Store.CurrentName(), // v3:会话归属引擎
Engine: d.Store.CurrentName(), // 26.9:会话归属引擎
ExpireValue: req.ExpireValue,
ExpireStyle: req.ExpireStyle,
ExpiresAt: time.Now().Add(presignSessionExpires * time.Second),
@@ -195,7 +195,7 @@ func (d *Deps) presignProxy(c *gin.Context) {
respondError(c, err)
return
}
// v3.1:自定义提取码随代理上传表单携带(init 时已预校验)
// 26.9:自定义提取码随代理上传表单携带(init 时已预校验)
if err := validatePickupCode(c.PostForm("code")); err != nil {
auditUploadEntry(c, uploadID, session.FileName, session.FileSize, 0)
auditRecordFailed(c, d.AuditSvc, "提取码非法")
@@ -234,7 +234,7 @@ func (d *Deps) presignProxy(c *gin.Context) {
if err == nil {
defer func() { _ = f.Close() }()
if err = validateFileMagic(d.Cfg, session.FileName, fh.Header.Get("Content-Type"), readMultipartHeader(f, 64)); err == nil {
// v3:落盘走会话归属引擎
// 26.9:落盘走会话归属引擎
var ps storage.Storage
ps, sErr := d.storeFor(session.Engine)
if sErr != nil {
@@ -259,7 +259,7 @@ func (d *Deps) presignProxy(c *gin.Context) {
releaseStorage(ctx, d.DB, "presign:"+uploadID)
if err != nil {
if ps, sErr := d.storeFor(session.Engine); sErr == nil {
_ = ps.DeleteFile(ctx, session.SavePath) // v3:清理走归属引擎
_ = ps.DeleteFile(ctx, session.SavePath) // 26.9:清理走归属引擎
}
auditUploadEntry(c, uploadID, session.FileName, session.FileSize, 0)
auditRecordFailed(c, d.AuditSvc, "创建分享失败")
@@ -299,7 +299,7 @@ func (d *Deps) presignConfirm(c *gin.Context) {
return
}
// v3:直传文件存在性按会话归属引擎检查(直传可能落在旧引擎)
// 26.9:直传文件存在性按会话归属引擎检查(直传可能落在旧引擎)
psCheck, sErr := d.storeFor(session.Engine)
if sErr != nil {
auditUploadEntry(c, uploadID, session.FileName, session.FileSize, 0)
@@ -307,7 +307,7 @@ func (d *Deps) presignConfirm(c *gin.Context) {
respondError(c, mapStorageError(sErr))
return
}
// v3.1:自定义提取码随确认请求携带(query 或 JSON/form body,均可选)
// 26.9:自定义提取码随确认请求携带(query 或 JSON/form body,均可选)
customCode := c.Query("code")
if customCode == "" && c.Request.Body != nil && c.Request.ContentLength != 0 {
var fin struct {
@@ -378,7 +378,7 @@ func (d *Deps) presignConfirm(c *gin.Context) {
releaseStorage(ctx, d.DB, "presign:"+uploadID)
if err != nil {
if ps, sErr := d.storeFor(session.Engine); sErr == nil {
_ = ps.DeleteFile(ctx, session.SavePath) // v3:清理走归属引擎
_ = ps.DeleteFile(ctx, session.SavePath) // 26.9:清理走归属引擎
}
auditUploadEntry(c, uploadID, session.FileName, session.FileSize, 0)
auditRecordFailed(c, d.AuditSvc, "创建分享失败")
@@ -399,7 +399,7 @@ func (d *Deps) createRecordFromSession(c *gin.Context, session *model.PresignUpl
if err != nil {
return "", err
}
// v3.1:完成请求的自定义提取码兜底校验(init 已验,防只发完成请求绕过)
// 26.9:完成请求的自定义提取码兜底校验(init 已验,防只发完成请求绕过)
if err := validatePickupCode(customCode); err != nil {
return "", err
}
@@ -420,10 +420,10 @@ func (d *Deps) createRecordFromSession(c *gin.Context, session *model.PresignUpl
ExpiredAt: exp.ExpiredAt,
ExpiredCount: exp.ExpiredCount,
UsedCount: exp.UsedCount,
Engine: session.Engine, // v3:归属引擎戳
Engine: session.Engine, // 26.9:归属引擎戳
}
if err := d.DB.WithContext(ctx).Create(&fc).Error; err != nil {
return "", mapCodeConflict(err) // v3.1:并发占用自定义码 → 友好 400
return "", mapCodeConflict(err) // 26.9:并发占用自定义码 → 友好 400
}
return code, nil
}
@@ -474,7 +474,7 @@ func (d *Deps) presignCancel(c *gin.Context) {
return
}
if session.Mode == "direct" {
// v3:清理走会话归属引擎
// 26.9:清理走会话归属引擎
if ps, sErr := d.storeFor(session.Engine); sErr == nil {
if exists, eErr := ps.FileExists(ctx, session.SavePath); eErr == nil && exists {
_ = ps.DeleteFile(ctx, session.SavePath)
+4 -4
View File
@@ -21,7 +21,7 @@ type Deps struct {
Mgr *settings.Manager
AuditSvc *audit.Service
Limiter *middleware.RateLimiter
Store *storage.Manager // v3:可热切换引擎管理器(实现 Storage 接口)
Store *storage.Manager // 26.9:可热切换引擎管理器(实现 Storage 接口)
Version string
}
@@ -104,7 +104,7 @@ func (d *Deps) robotsText(c *gin.Context) {
c.Data(http.StatusOK, "text/plain; charset=utf-8", []byte(d.Cfg.GetString("robotsText")))
}
// publicConfig 公共配置(前端首页/上传页所需;v2 需求 ①②③④⑩ 扩展):
// publicConfig 公共配置(前端首页/上传页所需;26.9 需求 ①②③④⑩ 扩展):
// - 展示字段:站点信息、Logo/favicon、背景图、页脚文案/备案号、通知;
// - 策略范围(上传页动态渲染):大小上限、类型白名单、过期方式、保存
// 时间/次数上限、上传频率(仅范围,不含内部实现键)。
@@ -132,10 +132,10 @@ func (d *Deps) publicConfig(c *gin.Context) {
// 需求 ②:页脚自定义内容与备案号
"footer_text": cfg.FooterText(),
"footer_beian": cfg.FooterBeian(),
// v3:当前存储引擎名(仅名称,任何引擎参数/凭据不下发)
// 26.9:当前存储引擎名(仅名称,任何引擎参数/凭据不下发)
"storage_engine": d.Store.CurrentName(),
"site_domain": d.Cfg.SiteDomain(),
// v3.2:上下行带宽字节/秒(公开下发,0=不限速,方便管理端展示当前值)
// 26.9:上下行带宽字节/秒(公开下发,0=不限速,方便管理端展示当前值)
"upload_rate": d.Cfg.UploadRate(),
"download_rate": d.Cfg.DownloadRate(),
// 需求 ③:系统通知(开关 + 内容,前台右上角悬浮窗)
+11 -11
View File
@@ -90,7 +90,7 @@ func (d *Deps) shareText(c *gin.Context) {
if !requireUploadLimit(c, d.Limiter) {
return
}
// v3.1 修复:JSON/表单/ultipart 统一绑定(form+json 双标签——此前仅 PostForm 时,
// 26.9 修复:JSON/表单/ultipart 统一绑定(form+json 双标签——此前仅 PostForm 时,
// JSON 提交会静默存成空文本并 200,取件页空白)。
var body struct {
Text string `json:"text" form:"text"`
@@ -120,7 +120,7 @@ func (d *Deps) shareText(c *gin.Context) {
if expireStyle == "" {
expireStyle = "day"
}
// v3.1:自定义提取码格式校验(4-8 位字母数字,空=随机)
// 26.9:自定义提取码格式校验(4-8 位字母数字,空=随机)
if err := validatePickupCode(body.Code); err != nil {
respondError(c, err)
return
@@ -153,11 +153,11 @@ func (d *Deps) shareText(c *gin.Context) {
ExpiredAt: exp.ExpiredAt,
ExpiredCount: exp.ExpiredCount,
UsedCount: exp.UsedCount,
Engine: d.Store.CurrentName(), // v3:归属引擎戳(文本也记录,保持一致性)
Engine: d.Store.CurrentName(), // 26.9:归属引擎戳(文本也记录,保持一致性)
}
err = d.DB.WithContext(ctx).Create(&fc).Error
}
err = mapCodeConflict(err) // v3.1:自定义码唯一索引冲突 → 友好 400
err = mapCodeConflict(err) // 26.9:自定义码唯一索引冲突 → 友好 400
releaseStorage(ctx, d.DB, token)
if err != nil {
auditRecordFailed(c, d.AuditSvc, "文本分享创建失败")
@@ -188,7 +188,7 @@ func (d *Deps) shareFile(c *gin.Context) {
return
}
origName := fh.Filename
// v2 需求 ④⑩:动态策略校验(max_file_size0=回落 uploadSize
// 26.9 需求 ④⑩:动态策略校验(max_file_size0=回落 uploadSize
if err := d.CurrentUploadPolicy().CheckSize(fh.Size); err != nil {
auditUploadEntry(c, "", origName, fh.Size, 0)
auditRecordFailed(c, d.AuditSvc, "大小超过限制")
@@ -205,7 +205,7 @@ func (d *Deps) shareFile(c *gin.Context) {
return
}
// v3.1:自定义提取码(落盘前校验,失败快速返回不占容量预留)
// 26.9:自定义提取码(落盘前校验,失败快速返回不占容量预留)
if err := validatePickupCode(c.PostForm("code")); err != nil {
auditUploadEntry(c, "", origName, fh.Size, 0)
auditRecordFailed(c, d.AuditSvc, "提取码非法")
@@ -257,10 +257,10 @@ func (d *Deps) shareFile(c *gin.Context) {
ExpiredAt: exp.ExpiredAt,
ExpiredCount: exp.ExpiredCount,
UsedCount: exp.UsedCount,
Engine: d.Store.CurrentName(), // v3:归属引擎戳(下载按此取回)
Engine: d.Store.CurrentName(), // 26.9:归属引擎戳(下载按此取回)
}
if err = d.DB.WithContext(ctx).Create(&fc).Error; err != nil {
err = mapCodeConflict(err) // v3.1
err = mapCodeConflict(err) // 26.9
// 记录创建失败:清理已落盘文件
_ = d.Store.DeleteFile(ctx, savePath)
}
@@ -273,7 +273,7 @@ func (d *Deps) shareFile(c *gin.Context) {
auditUploadEntry(c, "", origName, fh.Size, 0)
auditRecordFailed(c, d.AuditSvc, "文件保存失败")
if be, ok := err.(*apiError); ok && be.Status == http.StatusBadRequest {
respondError(c, err) // v3.1:提取码冲突等业务 400 原样透出
respondError(c, err) // 26.9:提取码冲突等业务 400 原样透出
} else {
respondError(c, mapStorageError(err))
}
@@ -544,7 +544,7 @@ func (d *Deps) serveFile(c *gin.Context, fc *model.FileCodes) {
savePath := fileSavePath(fc)
name := fc.Prefix + fc.Suffix
// v3:按文件归属引擎取回(切换引擎后旧文件仍可下载);空戳=历史数据回落当前引擎
// 26.9:按文件归属引擎取回(切换引擎后旧文件仍可下载);空戳=历史数据回落当前引擎
store, err := d.storeFor(fc.Engine)
if err != nil {
auditUploadEntry(c, fc.Code, name, fc.Size, 0)
@@ -597,7 +597,7 @@ func (d *Deps) serveFile(c *gin.Context, fc *model.FileCodes) {
}
auditUploadEntry(c, fc.Code, name, total, 0)
c.Status(status)
// v3.2:下载带宽限速(storage.ReadCloser → 限速 reader → c.Writer
// 26.9:下载带宽限速(storage.ReadCloser → 限速 reader → c.Writer
dlReader := middleware.WrapReadCloser(dl, d.Cfg.DownloadRate())
n, _ := io.Copy(c.Writer, dlReader)
middleware.AuditSet(c, func(e *audit.Entry) {