105 lines
3.0 KiB
Go
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)
|
|
}
|