- 48 个 Go 文件所有注释(行注释/块注释/行尾注释,含 _test.go)翻译为中文 - 保留技术标识符:SECURITY_TODO(n)、unsafe-inline、sqlite/mysql、路由参数等 - 代码、字符串字面量、日志消息保持英文原文,零逻辑改动 - go build/vet 通过,go test -count=1 ./... 全绿
103 lines
2.9 KiB
Go
103 lines
2.9 KiB
Go
package handlers
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"mime/multipart"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"github.com/gabriel-vasile/mimetype"
|
||
|
||
"go_blog/models"
|
||
)
|
||
|
||
// ErrUploadsDisabled 在全局上传开关关闭时返回。
|
||
var ErrUploadsDisabled = errors.New("uploads are disabled")
|
||
|
||
// FileValidationError 描述上传文件被拒绝的原因。
|
||
type FileValidationError struct {
|
||
Reason string
|
||
}
|
||
|
||
func (e *FileValidationError) Error() string { return e.Reason }
|
||
|
||
// FileCheck 是验证上传文件头的结果。
|
||
type FileCheck struct {
|
||
OK bool
|
||
Type *models.UploadFileType // 匹配的类型,未匹配时为 nil
|
||
MaxSize int64 // 生效的字节限制
|
||
}
|
||
|
||
// ValidateUpload 依据缓存的平台上传策略校验文件头:
|
||
// 总开关、扩展名白名单以及按类型的单文件大小限制。
|
||
// 报告的 MaxSize 是生效限制(按类型覆盖,否则使用默认值)。
|
||
func ValidateUpload(header *multipart.FileHeader) FileCheck {
|
||
cfg := models.GetUploadConfig()
|
||
|
||
if !cfg.Enabled {
|
||
return FileCheck{OK: false}
|
||
}
|
||
|
||
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||
def := cfg.DefaultMaxSize
|
||
for i := range models.GetUploadFileTypes() {
|
||
t := &models.GetUploadFileTypes()[i]
|
||
if !t.Enabled {
|
||
continue
|
||
}
|
||
if strings.EqualFold(t.Extension, ext) {
|
||
max := t.EffectiveMaxSize(def)
|
||
if header.Size > max {
|
||
return FileCheck{OK: false, Type: t, MaxSize: max}
|
||
}
|
||
return FileCheck{OK: true, Type: t, MaxSize: max}
|
||
}
|
||
}
|
||
|
||
// 扩展名不在白名单内。
|
||
return FileCheck{OK: false, MaxSize: def}
|
||
}
|
||
|
||
// formatSize 将字节数渲染为人类可读的字符串。
|
||
func formatSize(b int64) string {
|
||
const unit = 1024
|
||
if b < unit {
|
||
return fmt.Sprintf("%d B", b)
|
||
}
|
||
div, exp := int64(unit), 0
|
||
for n := b / unit; n >= unit; n /= unit {
|
||
div *= unit
|
||
exp++
|
||
}
|
||
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
||
}
|
||
|
||
// contentMatchesType 将上传字节的魔数与匹配扩展名配置的 MIME 类型比对
|
||
// (SECURITY_TODO #14)。它有意保持宽松:空/未知的 MIME 策略和无法识别的
|
||
// 内容均可通过(扩展名白名单仍是主要关卡);声称是 .txt 却携带 PNG 字节
|
||
// 的文件会被拒绝。
|
||
func contentMatchesType(t *models.UploadFileType, content []byte) bool {
|
||
if t == nil || len(content) == 0 {
|
||
return true
|
||
}
|
||
expected := strings.ToLower(strings.TrimSpace(t.MimeType))
|
||
if expected == "" || expected == "application/octet-stream" {
|
||
// 无具体策略,或管理员明确允许任意二进制内容。
|
||
return true
|
||
}
|
||
// 去除管理员可能复制过来的 charset 参数。
|
||
if i := strings.Index(expected, ";"); i >= 0 {
|
||
expected = strings.TrimSpace(expected[:i])
|
||
}
|
||
if expected == "" {
|
||
return true
|
||
}
|
||
det := mimetype.Detect(content)
|
||
if det == nil || det.String() == "" {
|
||
// 内容无法识别(如特殊的 Unicode 文本);仅由头部策略把关。
|
||
return true
|
||
}
|
||
return det.Is(expected)
|
||
}
|