169 lines
3.3 KiB
Go
169 lines
3.3 KiB
Go
// backend\generated_gc.go
|
||
|
||
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"io/fs"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync/atomic"
|
||
"time"
|
||
)
|
||
|
||
var generatedGCRunning int32
|
||
|
||
// Startet den GC im Hintergrund, aber nur wenn nicht schon einer läuft.
|
||
func triggerGeneratedGarbageCollectorAsync() {
|
||
if !atomic.CompareAndSwapInt32(&generatedGCRunning, 0, 1) {
|
||
return
|
||
}
|
||
|
||
go func() {
|
||
defer atomic.StoreInt32(&generatedGCRunning, 0)
|
||
runGeneratedGarbageCollector() // ohne Sleep
|
||
}()
|
||
}
|
||
|
||
// Läuft 1× nach Serverstart (mit Delay), löscht /generated/* Ordner, für die es kein Video in /done mehr gibt.
|
||
func startGeneratedGarbageCollector() {
|
||
time.Sleep(3 * time.Second)
|
||
runGeneratedGarbageCollector()
|
||
}
|
||
|
||
// Core-Logik ohne Delay (für manuelle Trigger, z.B. nach Cleanup)
|
||
func runGeneratedGarbageCollector() {
|
||
s := getSettings()
|
||
|
||
doneAbs, err := resolvePathRelativeToApp(s.DoneDir)
|
||
if err != nil {
|
||
fmt.Println("🧹 [gc] resolve doneDir failed:", err)
|
||
return
|
||
}
|
||
doneAbs = strings.TrimSpace(doneAbs)
|
||
if doneAbs == "" {
|
||
return
|
||
}
|
||
|
||
// 1) Live-IDs sammeln: alle mp4/ts unter /done (rekursiv), .trash ignorieren
|
||
live := make(map[string]struct{}, 4096)
|
||
|
||
_ = filepath.WalkDir(doneAbs, func(p string, d fs.DirEntry, err error) error {
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
|
||
name := d.Name()
|
||
|
||
if d.IsDir() {
|
||
if strings.EqualFold(name, ".trash") {
|
||
return fs.SkipDir
|
||
}
|
||
return nil
|
||
}
|
||
|
||
ext := strings.ToLower(filepath.Ext(name))
|
||
if ext != ".mp4" && ext != ".ts" {
|
||
return nil
|
||
}
|
||
|
||
info, err := d.Info()
|
||
if err != nil || info.IsDir() || info.Size() <= 0 {
|
||
return nil
|
||
}
|
||
|
||
base := strings.TrimSuffix(name, ext)
|
||
id, err := sanitizeID(stripHotPrefix(base))
|
||
if err != nil || id == "" {
|
||
return nil
|
||
}
|
||
|
||
live[id] = struct{}{}
|
||
return nil
|
||
})
|
||
|
||
// 2) /generated/meta/<id> prüfen
|
||
metaRoot, err := generatedMetaRoot()
|
||
if err == nil {
|
||
metaRoot = strings.TrimSpace(metaRoot)
|
||
}
|
||
if err != nil || metaRoot == "" {
|
||
return
|
||
}
|
||
|
||
removedMeta := 0
|
||
checkedMeta := 0
|
||
|
||
if entries, err := os.ReadDir(metaRoot); err == nil {
|
||
for _, e := range entries {
|
||
if !e.IsDir() {
|
||
continue
|
||
}
|
||
id := strings.TrimSpace(e.Name())
|
||
if id == "" || strings.HasPrefix(id, ".") {
|
||
continue
|
||
}
|
||
|
||
checkedMeta++
|
||
if _, ok := live[id]; ok {
|
||
continue
|
||
}
|
||
|
||
removeGeneratedForID(id)
|
||
removedMeta++
|
||
}
|
||
}
|
||
|
||
fmt.Printf("🧹 [gc] generated/meta checked=%d removed_orphans=%d\n", checkedMeta, removedMeta)
|
||
|
||
// 3) Optional: legacy /generated/<id>
|
||
genRoot, err := generatedRoot()
|
||
if err == nil {
|
||
genRoot = strings.TrimSpace(genRoot)
|
||
}
|
||
if err != nil || genRoot == "" {
|
||
return
|
||
}
|
||
|
||
reserved := map[string]struct{}{
|
||
"meta": {},
|
||
"covers": {},
|
||
"cover": {},
|
||
"temp": {},
|
||
"tmp": {},
|
||
".trash": {},
|
||
}
|
||
|
||
removedLegacy := 0
|
||
checkedLegacy := 0
|
||
|
||
if entries, err := os.ReadDir(genRoot); err == nil {
|
||
for _, e := range entries {
|
||
if !e.IsDir() {
|
||
continue
|
||
}
|
||
|
||
name := strings.TrimSpace(e.Name())
|
||
if name == "" || strings.HasPrefix(name, ".") {
|
||
continue
|
||
}
|
||
if _, ok := reserved[strings.ToLower(name)]; ok {
|
||
continue
|
||
}
|
||
|
||
checkedLegacy++
|
||
if _, ok := live[name]; ok {
|
||
continue
|
||
}
|
||
|
||
removeGeneratedForID(name)
|
||
removedLegacy++
|
||
}
|
||
}
|
||
|
||
if checkedLegacy > 0 || removedLegacy > 0 {
|
||
fmt.Printf("🧹 [gc] generated legacy checked=%d removed_orphans=%d\n", checkedLegacy, removedLegacy)
|
||
}
|
||
}
|