package api import ( "encoding/base64" "fmt" "net/http" "os" "path/filepath" "strings" "sync/atomic" "time" "github.com/muyue/muyue/internal/config" ) var imageDir string func init() { dir, err := config.ConfigDir() if err != nil { dir = "/tmp/muyue" } imageDir = filepath.Join(dir, "images") os.MkdirAll(imageDir, 0755) } var imageCounter uint64 func saveImage(dataURI, filename, mimeType string) (string, error) { parts := strings.SplitN(dataURI, ",", 2) if len(parts) != 2 { return "", fmt.Errorf("invalid data URI") } encoded := parts[1] decoded, err := base64.StdEncoding.DecodeString(encoded) if err != nil { return "", fmt.Errorf("base64 decode: %w", err) } if len(decoded) > 10*1024*1024 { return "", fmt.Errorf("image too large (max 10MB)") } id := fmt.Sprintf("%d-%d", time.Now().UnixMilli(), atomic.AddUint64(&imageCounter, 1)) ext := ".png" switch mimeType { case "image/jpeg": ext = ".jpg" case "image/webp": ext = ".webp" } filePath := filepath.Join(imageDir, id+ext) if err := os.WriteFile(filePath, decoded, 0600); err != nil { return "", fmt.Errorf("write image: %w", err) } return id + ext, nil } func imagePath(id string) string { return filepath.Join(imageDir, filepath.Base(id)) } func cleanupImages(ids []string) { for _, id := range ids { p := imagePath(id) if err := os.Remove(p); err != nil && !os.IsNotExist(err) { _ = err } } } func (s *Server) handleServeImage(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { writeError(w, "GET only", http.StatusMethodNotAllowed) return } id := strings.TrimPrefix(r.URL.Path, "/api/images/") if id == "" { writeError(w, "image id required", http.StatusBadRequest) return } filePath := imagePath(id) if _, err := os.Stat(filePath); err != nil { writeError(w, "image not found", http.StatusNotFound) return } ext := strings.ToLower(filepath.Ext(id)) switch ext { case ".jpg", ".jpeg": w.Header().Set("Content-Type", "image/jpeg") case ".png": w.Header().Set("Content-Type", "image/png") case ".webp": w.Header().Set("Content-Type", "image/webp") default: w.Header().Set("Content-Type", "application/octet-stream") } w.Header().Set("Cache-Control", "public, max-age=86400") http.ServeFile(w, r, filePath) }