Go 1.27.1 (Gin+GORM) + Vue 3 文件快传服务: - 安全审计全部修复(docs/security-audit-2026-09-05.md): bcrypt 密码哈希与自动升级、presign 直传服务端大小/内容校验、 全局请求体上限、依赖升级(govulncheck 0 命中)、janitor 后台清理、 管理端审计动作落库、/admin CORS 收紧、通知内容白名单净化、 会话默认 7 天、限流缓存故障降级、robots.txt 端点等 - 前端:取件链接复制修复(不再重复拼接提取码)、markdown 净化器加固 - Redis 支持库号(FCB_REDIS_DB / redis://…/db URL) - 文档:docs/api/* 与 openapi.yaml 同步最新行为(robots.txt、 提码 5 位起、chunk 32MiB 上限、admin 审计动作等) 验证:gofmt/go vet/go test 全绿;二进制端到端冒烟通过
104 lines
2.4 KiB
Go
104 lines
2.4 KiB
Go
package storage
|
|
|
|
import (
|
|
"path"
|
|
"strings"
|
|
)
|
|
|
|
// ChunkDir 实现默认分片目录约定:<父目录>/chunks/<uploadID>。
|
|
// local/s3/webdav 三引擎共用,保持分片路径一致。
|
|
func ChunkDir(savePath, uploadID string) string {
|
|
dir := path.Dir(savePath)
|
|
name := path.Base(savePath)
|
|
// 防御:savePath 非法时仍返回明确结构,具体引擎再做安全校验
|
|
if name == "." || name == "/" {
|
|
name = "file"
|
|
}
|
|
return path.Join(dir, "chunks", uploadID) + "/" + name
|
|
}
|
|
|
|
// ChunkPartPath 分片对象完整路径(相对存储根)。
|
|
func ChunkPartPath(savePath, uploadID string, index int) string {
|
|
dir := path.Dir(savePath)
|
|
return path.Join(dir, "chunks", uploadID, itoa(index)+".part")
|
|
}
|
|
|
|
// SanitizePath 清理相对路径:统一斜杠、去首尾斜杠、拒绝 .. 穿越。
|
|
// 返回清理后的相对路径与是否合法。
|
|
func SanitizePath(p string) (string, bool) {
|
|
raw := strings.ReplaceAll(strings.TrimSpace(p), "\\", "/")
|
|
raw = strings.TrimPrefix(raw, "/")
|
|
if raw == "" {
|
|
return "", false
|
|
}
|
|
cleaned := path.Clean(raw)
|
|
if cleaned == ".." || strings.HasPrefix(cleaned, "../") || path.IsAbs(cleaned) {
|
|
return "", false
|
|
}
|
|
// 拒绝任何单独的 .. 段
|
|
for _, seg := range strings.Split(cleaned, "/") {
|
|
if seg == ".." {
|
|
return "", false
|
|
}
|
|
}
|
|
return cleaned, true
|
|
}
|
|
|
|
// SanitizeFileName 清理文件名:剥离路径、替换非法字符、限制长度。
|
|
// 对齐参考 core/utils.py 的 sanitize_filename。
|
|
func SanitizeFileName(name string) string {
|
|
// 剥离路径
|
|
if idx := strings.LastIndexAny(name, "/\\"); idx >= 0 {
|
|
name = name[idx+1:]
|
|
}
|
|
var b strings.Builder
|
|
for _, r := range name {
|
|
switch {
|
|
case r < 0x20 || r == 0x7f:
|
|
b.WriteByte('_')
|
|
case strings.ContainsRune(`\*?:"<>|`, r):
|
|
b.WriteByte('_')
|
|
case r == ' ':
|
|
b.WriteByte('_')
|
|
default:
|
|
b.WriteRune(r)
|
|
}
|
|
}
|
|
cleaned := b.String()
|
|
// 压缩连续下划线
|
|
for strings.Contains(cleaned, "__") {
|
|
cleaned = strings.ReplaceAll(cleaned, "__", "_")
|
|
}
|
|
cleaned = strings.Trim(cleaned, "._")
|
|
if cleaned == "" {
|
|
return "unnamed_file"
|
|
}
|
|
if len(cleaned) > 255 {
|
|
cleaned = cleaned[:255]
|
|
}
|
|
return cleaned
|
|
}
|
|
|
|
// itoa 小整数转字符串。
|
|
func itoa(n int) string {
|
|
if n == 0 {
|
|
return "0"
|
|
}
|
|
neg := n < 0
|
|
if neg {
|
|
n = -n
|
|
}
|
|
var buf [21]byte
|
|
i := len(buf)
|
|
for n > 0 {
|
|
i--
|
|
buf[i] = byte('0' + n%10)
|
|
n /= 10
|
|
}
|
|
if neg {
|
|
i--
|
|
buf[i] = '-'
|
|
}
|
|
return string(buf[i:])
|
|
}
|