package api import ( "bytes" "image" "image/jpeg" _ "image/jpeg" "image/png" _ "image/png" "io" "log/slog" "os" "path/filepath" "strings" "github.com/disintegration/imaging" "golang.org/x/image/webp" ) const maxImageDimension = 1920 const jpegQuality = 80 // saveUpload writes an uploaded file to uploadDir, resizing images where applicable. // peeked contains the first bytes already read for MIME detection. // Returns the saved filename (including extension). func saveUpload(uploadDir, baseName, mime string, peeked []byte, rest io.Reader) (string, error) { full := io.MultiReader(bytes.NewReader(peeked), rest) switch mime { case "image/jpeg", "image/png": return saveResizedImage(uploadDir, baseName, mime, full) case "image/webp": return saveResizedWebP(uploadDir, baseName, full) default: ext := allowedMIME[mime] dest := filepath.Join(uploadDir, baseName+ext) return baseName + ext, saveRaw(dest, full) } } func saveResizedImage(uploadDir, baseName, mime string, r io.Reader) (string, error) { img, _, err := image.Decode(r) if err != nil { // Fallback: save raw if decode fails slog.Warn("image decode failed, saving raw", "mime", mime, "err", err) return "", err } b := img.Bounds() if b.Dx() > maxImageDimension || b.Dy() > maxImageDimension { img = imaging.Fit(img, maxImageDimension, maxImageDimension, imaging.Lanczos) } ext := allowedMIME[mime] filename := baseName + ext dest := filepath.Join(uploadDir, filename) out, err := os.Create(dest) if err != nil { return "", err } defer out.Close() switch mime { case "image/jpeg": err = jpeg.Encode(out, img, &jpeg.Options{Quality: jpegQuality}) case "image/png": err = png.Encode(out, img) } if err != nil { os.Remove(dest) return "", err } return filename, nil } func saveResizedWebP(uploadDir, baseName string, r io.Reader) (string, error) { img, err := webp.Decode(r) if err != nil { slog.Warn("webp decode failed, saving raw", "err", err) return "", err } b := img.Bounds() if b.Dx() > maxImageDimension || b.Dy() > maxImageDimension { img = imaging.Fit(img, maxImageDimension, maxImageDimension, imaging.Lanczos) } // Re-encode as JPEG (no pure-Go WebP encoder available) filename := baseName + ".jpg" dest := filepath.Join(uploadDir, filename) out, err := os.Create(dest) if err != nil { return "", err } defer out.Close() if err := jpeg.Encode(out, img, &jpeg.Options{Quality: jpegQuality}); err != nil { os.Remove(dest) return "", err } return filename, nil } func saveRaw(dest string, r io.Reader) error { out, err := os.Create(dest) if err != nil { return err } defer out.Close() _, err = io.Copy(out, r) return err } // mimeCategory returns "image", "video", "audio" or "other". func mimeCategory(mime string) string { switch { case strings.HasPrefix(mime, "image/"): return "image" case strings.HasPrefix(mime, "video/"): return "video" case strings.HasPrefix(mime, "audio/"): return "audio" default: return "other" } }