package main import ( "bytes" "fmt" "net/http" "os" "os/exec" "path/filepath" "sort" "strings" "time" ) func prunePreviewCacheDir(previewDir string, maxFrames int, maxAge time.Duration) { entries, err := os.ReadDir(previewDir) if err != nil { return } type frame struct { path string mt time.Time } now := time.Now() var frames []frame for _, e := range entries { name := e.Name() path := filepath.Join(previewDir, name) // .part Dateien immer weg if strings.HasSuffix(name, ".part") { _ = os.Remove(path) continue } // optional: preview.jpg neu erzeugen lassen, wenn uralt if name == "preview.jpg" { if info, err := e.Info(); err == nil { if maxAge > 0 && now.Sub(info.ModTime()) > maxAge { _ = os.Remove(path) } } continue } // Nur t_*.jpg verwalten if strings.HasPrefix(name, "t_") && strings.HasSuffix(name, ".jpg") { info, err := e.Info() if err != nil { continue } // alte Frames löschen if maxAge > 0 && now.Sub(info.ModTime()) > maxAge { _ = os.Remove(path) continue } frames = append(frames, frame{path: path, mt: info.ModTime()}) } } // Anzahl begrenzen: älteste zuerst löschen if maxFrames > 0 && len(frames) > maxFrames { sort.Slice(frames, func(i, j int) bool { return frames[i].mt.Before(frames[j].mt) }) toDelete := len(frames) - maxFrames for i := 0; i < toDelete; i++ { _ = os.Remove(frames[i].path) } } } func servePreviewJPEGBytes(w http.ResponseWriter, img []byte) { w.Header().Set("Content-Type", "image/jpeg") w.Header().Set("Cache-Control", "public, max-age=31536000") w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(http.StatusOK) _, _ = w.Write(img) } func servePreviewJPEGBytesNoStore(w http.ResponseWriter, img []byte) { w.Header().Set("Content-Type", "image/jpeg") w.Header().Set("Cache-Control", "no-store, max-age=0") w.Header().Set("Pragma", "no-cache") w.Header().Set("Expires", "0") w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(http.StatusOK) _, _ = w.Write(img) } func serveLivePreviewJPEGBytes(w http.ResponseWriter, img []byte) { w.Header().Set("Content-Type", "image/jpeg") w.Header().Set("Cache-Control", "no-store, max-age=0, must-revalidate") w.Header().Set("Pragma", "no-cache") w.Header().Set("Expires", "0") w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(http.StatusOK) _, _ = w.Write(img) } func servePreviewJPEGFile(w http.ResponseWriter, r *http.Request, path string) { w.Header().Set("Content-Type", "image/jpeg") w.Header().Set("Cache-Control", "public, max-age=31536000") w.Header().Set("X-Content-Type-Options", "nosniff") http.ServeFile(w, r, path) } func extractFirstFrameJPEG(path string) ([]byte, error) { cmd := exec.Command( ffmpegPath, "-hide_banner", "-loglevel", "error", "-i", path, "-frames:v", "1", "-vf", "scale=720:-2", "-q:v", "10", "-f", "image2pipe", "-vcodec", "mjpeg", "pipe:1", ) var out bytes.Buffer var stderr bytes.Buffer cmd.Stdout = &out cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return nil, fmt.Errorf("ffmpeg first-frame: %w (%s)", err, strings.TrimSpace(stderr.String())) } return out.Bytes(), nil }