Documentation
¶
Overview ¶
Package gate 上传/下载准入闸门。
服务端强制执行 upload.open_upload / upload.require_login / download.require_login (此前三者只透传给前端做 UI 裁剪,后端不校验,形同虚设)。 判定规则:
- open_upload=false:拒绝全部匿名上传(登录用户不受影响)
- upload.require_login=true:拒绝匿名上传(含 chunk/presign 等纯匿名通道)
- download.require_login=true:拒绝匿名取件/下载
pickup.go 取件闸门:过期 → 管控 → 就绪 → 密码/防爆破锁定 的统一判定。
历史:同一条「取件前置校验」在四处各写一份且规格互不一致—— share/select、share/download、anonymous.Retrieve 的锁定在 handler 层 (scope 各为 pickup/download/anon,计数互不相通,同一口令在四个入口 各有独立爆破预算),preview 的锁定在域层且用 FormatLockKey(不做取件码 大小写折叠,存在变体绕过锁定漏洞)。2026-10-10 收口于此:
- 单一 scope "pickup":四入口共享 (IP, code) 失败计数;
- 统一 LookupLockKey(折叠开启时大小写变体同键);
- 判定顺序、锁定检查、失败记账、成功清零只此一份。
域/handler 侧职责不变:把 model.FileCode 映射为 PickupState(pkg 层不得 import repo,架构守卫规则 1),把 PickupVerdict 映射为各自的错误类型与 HTTP 响应形态(select 空密码提示不计数、preview 空密码计数等入口差异经 MissingCountsFailure 表达)。
quota.go 匿名上传 per-IP 日配额(治理 2026-10-03)。
此前配额只对登录用户生效(share.CreateShare → CheckQuota),匿名分片/直传 完全绕过——刷爆存储的主通道。本文件以"自然日"为窗口对匿名来源 IP 计数 (次数 + 字节双维度,任一超限即拒):
- Redis 可用:INCR + 当日剩余秒数 EXPIRE(多实例共享计数)
- 无 Redis:进程内 map + 日期轮转(单实例语义,重启清零)
Index ¶
- Constants
- Variables
- func CheckAnonymousQuota(ctx context.Context, ip string, addBytes int64) error
- func CheckDownloadLogin(userID *uint) error
- func CheckPresignPolicy(userID *uint) error
- func CheckUploadAllowed(userID *uint) error
- func CheckUploadLogin(userID *uint) error
- func CheckUploadSizeCap(userID *uint, totalSize, capSize int64) error
- func ErrUploadSizeCap(capBytes int64) error
- func PickupLockKey(clientIP, code string) string
- func PickupLockedRemain(ctx context.Context, clientIP, code string) (int, bool)
- func QuotaLimits() (count int64, bytes int64)
- func RecordPickupFailure(ctx context.Context, clientIP, code string)
- func SetQuotaRedis(rdb *redis.Client)
- type GateError
- type PickupCheck
- type PickupDecision
- type PickupState
- type PickupVerdict
Constants ¶
const PickupScope = "pickup"
PickupScope 统一锁定 scope:取件/下载/预览/匿名取件四入口共享失败计数。
Variables ¶
var ErrLoginRequired = newGateErr(errcode.CodeUnauthorized, "该操作需要登录后使用")
ErrLoginRequired 需要登录才能执行该操作。
var ErrPresignDisabled = newGateErr(errcode.CodePresignDisabled, "匿名直传未开放,文件将改用普通通道上传")
ErrPresignDisabled 匿名直传已被管理员关闭(download.presign_policy=disabled/authenticated)。
var ErrUploadDisabled = newGateErr(errcode.CodeUploadDisabled, "管理员已关闭匿名上传功能")
ErrUploadDisabled 上传已被管理员关闭(匿名通道)。
Functions ¶
func CheckAnonymousQuota ¶
CheckAnonymousQuota 匿名上传配额检查并计数(仅对匿名请求调用)。 超限返回 *GateError(业务码 10014);不限或登录请求恒通过。
func CheckDownloadLogin ¶
CheckDownloadLogin 取件/下载登录要求(download.require_login)。
func CheckPresignPolicy ¶
CheckPresignPolicy 直传策略闸门(download.presign_policy 三档):
- everyone:所有人可直传
- authenticated:仅登录用户(匿名 Init 返回 10015,前端回退分片中转)
- disabled:完全关闭直传(登录用户也拒绝——密钥轮换/通道故障时的总闸)
func CheckUploadAllowed ¶
CheckUploadAllowed 匿名上传总开关(upload.open_upload)。 登录用户(userID != nil)不受该开关限制。
func CheckUploadLogin ¶
CheckUploadLogin 上传登录要求(upload.require_login)。
func CheckUploadSizeCap ¶ added in v0.26.0
CheckUploadSizeCap 登录用户单次上传总大小预检(capSize<=0 = 不限;userID nil 跳过)。 分片 init / 多文件绑定按申报大小提前调用,把超限拦截在传输与合并落盘之前。
func ErrUploadSizeCap ¶ added in v0.26.0
ErrUploadSizeCap 单用户单次上传总大小超过上限(用户级 max_upload_size 优先, 回退系统默认 user.user_upload_size;匿名用户不受此限,走匿名日配额)。 必须携带 CodeTooLarge 业务码:2026-10-19 事故中 share 域以普通 error 返回, NewTypedError 兜底 CodeInternal,前端显示「服务器内部错误」掩盖真实原因, 用户在 450MB×2 整轮分片传输全部白费后才见到报错。
func PickupLockKey ¶ added in v0.15.1
PickupLockKey 统一锁定键(scope 固定 "pickup";取件码折叠开启时大小写 变体同键,与 LookupLockKey 语义一致)。handler 预检与枚举记账用同一键。
func PickupLockedRemain ¶ added in v0.15.1
PickupLockedRemain 查询统一键的剩余锁定秒数(handler 快速 429 预检用; 未锁定返回 (0, false))。
func RecordPickupFailure ¶ added in v0.15.1
RecordPickupFailure 在统一键上记一次失败(供「码不存在」等到达不了 CheckPickup 的路径做枚举防护记账,语义与旧 handler 层记账一致)。
func SetQuotaRedis ¶
SetQuotaRedis 注入 Redis(bootstrap 调用;nil = 内存模式)。 nil 归一化:typed-nil 接口会骗过内存模式守卫(!= nil 判真)。
Types ¶
type GateError ¶
GateError 携带业务码的闸门拒绝错误,handler 侧按 ErrCode 透传给前端。
type PickupCheck ¶ added in v0.15.1
type PickupCheck struct {
ClientIP string // 可信代理解析后的取件方 IP(锁定键维度)
Code string // 取件入口使用的码(分享码或取件码,锁定键维度)
Password string
AuthedByToken bool // 持有效下载令牌视为已认证,跳过密码段(share/download 通道)
// MissingCountsFailure 空密码是否按失败记账:preview/anonymous/download
// 为 true(对齐各自历史行为),select 为 false(空密码是"需要密码"提示态)。
MissingCountsFailure bool
}
PickupCheck 判定入参(HTTP 关注点由适配层提取后传入)。
type PickupDecision ¶ added in v0.15.1
type PickupDecision struct {
Verdict PickupVerdict
BlockedStatus string // Verdict==PickupBlocked 时有效
LockRemain int // Verdict==PickupLocked 时有效(秒)
}
PickupDecision CheckPickup 的完整判定。
func CheckPickup ¶ added in v0.15.1
func CheckPickup(ctx context.Context, st PickupState, req PickupCheck) PickupDecision
CheckPickup 统一取件闸门。判定顺序固定:过期 → 管控 → 就绪 → 锁定 → 密码;RequireAuth=false 或 AuthedByToken=true 时密码段整体跳过。 密码正确即在统一键清零失败计数;密码错误(含脏数据空哈希)记账一次。
type PickupState ¶ added in v0.15.1
type PickupState struct {
// Expired **只表示时间过期**(次数耗尽在交付面单独判定)。
// ⚠️ 2026-10-26 审计订正:旧注释写「时间或次数已耗尽」⇒ 会让人照着把
// 次数语义**合流回这个闸门** ⇒ 刚修好的 20004「超过取件次数」/
// 20007「取件码已用完」会被压回 404「不存在/已过期」(2026-10-24 那轮
// 踩过的坑)。⇒ 次数耗尽由 model.IsCountExhausted() 判定,并在**交付面**
// 返回独立错误码,**不在本闸门判**。
Expired bool // 时间过期(不含次数耗尽)
Blocked bool // 管控拒绝态(禁用/待审核)
BlockedStatus string // 原始状态串(blocked/pending_review),Blocked=true 时有效
NotReady bool // 登记占位未回填物理文件(匿名取件通道语义)
RequireAuth bool // 密码保护
PasswordHash string // bcrypt 哈希(RequireAuth=true 而哈希空 = 历史脏数据,按拒绝处理)
}
PickupState 取件目标分享的状态快照(域侧从 model.FileCode 映射; Blocked 由 IsBlockedShare() 判定,BlockedStatus 仅用于错误映射透传)。
type PickupVerdict ¶ added in v0.15.1
type PickupVerdict int
PickupVerdict 闸门判定结果。
const ( PickupAllowed PickupVerdict = iota // 放行 PickupBlocked // 管控拒绝态(携带 BlockedStatus) PickupNotReady // 未完成上传登记 PickupLocked // 防爆破锁定态(携带 LockRemain 秒) PickupPasswordMissing // 需要密码但未提供(未记账,入口自行决定提示/计数) PickupPasswordWrong // 密码错误(已在统一键记账,达阈值触发锁定) )