// backend\record_helpers_paths.go package main import ( "fmt" "net/http" "os" "path/filepath" "strings" ) func resolvePathRelativeToApp(p string) (string, error) { p = strings.TrimSpace(p) if p == "" { return "", nil } p = filepath.Clean(filepath.FromSlash(p)) if filepath.IsAbs(p) { return p, nil } exe, err := os.Executable() if err == nil { exeDir := filepath.Dir(exe) low := strings.ToLower(exeDir) // Heuristik: go run / tests -> exe liegt in Temp/go-build isTemp := strings.Contains(low, `\appdata\local\temp`) || strings.Contains(low, `\temp\`) || strings.Contains(low, `\tmp\`) || strings.Contains(low, `\go-build`) || strings.Contains(low, `/tmp/`) || strings.Contains(low, `/go-build`) if !isTemp { return filepath.Join(exeDir, p), nil } } // Fallback: Working Directory (Dev) wd, err := os.Getwd() if err != nil { return "", err } return filepath.Join(wd, p), nil } func getRecordingsDir() string { s := getSettings() abs, err := resolvePathRelativeToApp(s.RecordDir) if err == nil && strings.TrimSpace(abs) != "" { return abs } // Fallback (falls resolve fehlschlägt) return strings.TrimSpace(s.RecordDir) } func getKeepDir() string { s := getSettings() doneAbs, err := resolvePathRelativeToApp(s.DoneDir) if err != nil || strings.TrimSpace(doneAbs) == "" { doneAbs = strings.TrimSpace(s.DoneDir) } if strings.TrimSpace(doneAbs) == "" { return "" } return filepath.Join(doneAbs, "keep") } func getDoneDir() string { s := getSettings() doneAbs, err := resolvePathRelativeToApp(s.DoneDir) if err == nil && strings.TrimSpace(doneAbs) != "" { return doneAbs } return strings.TrimSpace(s.DoneDir) } func findVideoPath(file string) (string, error) { base := filepath.Base(file) // verhindert path traversal // TODO: passe diese Root-Dirs an deine echten Pfade an: roots := []string{ getRecordingsDir(), // z.B. downloads/output root getDoneDir(), // ✅ NEU: fertige Dateien liegen typischerweise hier getKeepDir(), // keep root } // 1) direkt in den Roots for _, root := range roots { root = strings.TrimSpace(root) if root == "" { continue } p := filepath.Join(root, base) if st, err := os.Stat(p); err == nil && !st.IsDir() { return p, nil } } // 2) 1 Ebene Unterordner: root/*/file for _, root := range roots { root = strings.TrimSpace(root) if root == "" { continue } matches, _ := filepath.Glob(filepath.Join(root, "*", base)) for _, p := range matches { if st, err := os.Stat(p); err == nil && !st.IsDir() { return p, nil } } } return "", os.ErrNotExist } func setNoStoreHeaders(w http.ResponseWriter) { // verhindert Browser/Proxy Caching (wichtig für Logs/Status) w.Header().Set("Cache-Control", "no-store, max-age=0") w.Header().Set("Pragma", "no-cache") w.Header().Set("Expires", "0") } func findFileInDirOrOneLevelSubdirs(root string, file string, skipDirName string) (string, os.FileInfo, bool) { // direct p := filepath.Join(root, file) if fi, err := os.Stat(p); err == nil && !fi.IsDir() && fi.Size() > 0 { return p, fi, true } entries, err := os.ReadDir(root) if err != nil { return "", nil, false } for _, e := range entries { if !e.IsDir() { continue } if skipDirName != "" && e.Name() == skipDirName { continue } pp := filepath.Join(root, e.Name(), file) if fi, err := os.Stat(pp); err == nil && !fi.IsDir() && fi.Size() > 0 { return pp, fi, true } } return "", nil, false } func resolveDoneFileByName(doneAbs string, file string) (full string, from string, fi os.FileInfo, err error) { // 1) done (root + /done//) — "keep" wird übersprungen if p, fi, ok := findFileInDirOrOneLevelSubdirs(doneAbs, file, "keep"); ok { return p, "done", fi, nil } // 2) keep (root + /done/keep//) keepDir := filepath.Join(doneAbs, "keep") if p, fi, ok := findFileInDirOrOneLevelSubdirs(keepDir, file, ""); ok { return p, "keep", fi, nil } return "", "", nil, fmt.Errorf("not found") } func isTrashPath(p string) bool { p = strings.ReplaceAll(p, "\\", "/") return strings.Contains(p, "/.trash/") || strings.HasSuffix(p, "/.trash") } func durationFromMetaIfFresh(videoPath, assetDir string, fi os.FileInfo) (float64, bool) { metaPath := filepath.Join(assetDir, "meta.json") return readVideoMetaDuration(metaPath, fi) } func durationSecondsCacheOnly(path string, fi os.FileInfo) float64 { durCache.mu.Lock() e, ok := durCache.m[path] durCache.mu.Unlock() if ok && e.size == fi.Size() && e.mod.Equal(fi.ModTime()) && e.sec > 0 { return e.sec } return 0 }