Files
go_blog/handlers/upload_validator.go
kevinandClaude Fable 5 e3367e6e12 feat: add platform configuration tables and admin settings
Add four DB-backed configuration tables for site-wide settings:
site_settings (logo, top-left title, header/footer text with zh/en
variants), upload_configs (master switch, default size, storage dir),
upload_file_types (per-extension whitelist with per-type size limits),
and download_baseurls (multiple download sources with priority/default).

- Models, AutoMigrate, and first-run seed (18 common file types)
- Process-level config cache warmed at startup, refreshed on admin save
- SetUserContext injects cached site settings into templates
- base.html renders logo (local upload or external URL), title, header
  banner, and footer with i18n fallback
- Upload validator drives avatar uploads (form + AJAX) through the
  configured switch/type/size policy
- Admin pages for site/upload/download settings with i18n keys

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-21 20:32:28 +08:00

73 lines
1.8 KiB
Go

package handlers
import (
"errors"
"fmt"
"mime/multipart"
"path/filepath"
"strings"
"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])
}