Files
go_blog/handlers/upload_validator.go
T

105 lines
3.0 KiB
Go

package handlers
import (
"errors"
"fmt"
"mime/multipart"
"path/filepath"
"strings"
"github.com/gabriel-vasile/mimetype"
"go_blog/models"
)
// ErrUploadsDisabled is returned when the global upload switch is off.
var ErrUploadsDisabled = errors.New("uploads are disabled")
// FileValidationError describes why an uploaded file was rejected.
type FileValidationError struct {
Reason string
}
func (e *FileValidationError) Error() string { return e.Reason }
// FileCheck is the outcome of validating an uploaded file header.
type FileCheck struct {
OK bool
Type *models.UploadFileType // matched type, nil if not found
MaxSize int64 // effective byte limit applied
}
// ValidateUpload checks a file header against the cached platform upload
// policy: master switch, extension whitelist, and per-type size limit. The
// reported MaxSize is the effective limit (per-type override, else default).
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}
}
}
// Extension not in the whitelist.
return FileCheck{OK: false, MaxSize: def}
}
// formatSize renders a byte count as a human-readable string.
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 checks the uploaded bytes' magic bytes against the MIME
// type configured for the matched extension (SECURITY_TODO #14). It is
// deliberately lenient: empty/unknown MIME policies and unrecognizable
// content pass (the extension whitelist remains the primary gate); a file
// that claims .txt while carrying PNG bytes is rejected.
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" {
// No concrete policy, or the admin explicitly allows any binary.
return true
}
// Normalize away a charset parameter the admin may have copied.
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() == "" {
// Content undetectable (e.g. exotic Unicode text); header policy
// alone remains the gate.
return true
}
return det.Is(expected)
}