package dispatch import ( "context" "fmt" "path" "sort" "strings" "time" "github.com/dbledeez/panel/pkg/regionmedic" panelv1 "github.com/dbledeez/panel/proto/panel/v1" "google.golang.org/protobuf/types/known/timestamppb" ) // Region Medic agent handlers — scan a 7DTD instance's active-world Region // directory for .7rg corruption, and heal one region from the newest clean // panel backup. Volume files are reached through the runtime's container-path // ops (the agent runs unprivileged, so it cannot touch /var/lib/docker/volumes // directly). The controller orchestrates stop→heal→start; handleRegionHeal // itself only performs the file swap and refuses to run on a live container. // activeRegionDir resolves the container-absolute Region directory of the // instance's active world by reading GameWorld/GameName from the live // serverconfig.xml. Works on a stopped container (CopyFileFromContainer does). func (d *Dispatcher) activeRegionDir(ctx context.Context, container string) (string, error) { raw, err := d.runtime.CopyFileFromContainer(ctx, container, "/game-saves/serverconfig.xml") if err != nil { return "", fmt.Errorf("read serverconfig.xml: %w", err) } world := xmlProp(string(raw), "GameWorld") game := xmlProp(string(raw), "GameName") if world == "" || game == "" { return "", fmt.Errorf("serverconfig.xml missing GameWorld/GameName (got %q/%q)", world, game) } // HOME is pinned to /game-saves by the 7dtd entrypoint. return path.Join("/game-saves/.local/share/7DaysToDie/Saves", world, game, "Region"), nil } // xmlProp pulls the value from a 7DTD serverconfig line: // // // // Tolerant of attribute order and single/double quotes; skips commented lines. func xmlProp(content, name string) string { for _, raw := range strings.Split(content, "\n") { line := strings.TrimSpace(raw) if line == "" || strings.HasPrefix(line, "