package apply import ( "errors" "fmt" "os" "strconv" "time" "github.com/novox/mesh-host/internal/store" ) // A file written whole, undeclared (novox/hq ADR 0118, ADR 0102). // // **What the mesh made goes; what it wrote over is given back.** Before the host writes a file over // one it has no record of making, it keeps the original first (ADR 0102: "whatever the host writes // over without a record of it, it keeps first"). Undeclaring gives a thing back the state it was // found in (ADR 0118), so a file with a kept original is not deleted when its record goes: the // original is put back, with the mode and owner it was found with. Deleting it was the failure — // a module that writes the package manager's configuration whole, unassigned, left the machine with // no configuration at all. // // The cases, decided once and in this order: // // - **No kept original** — the mesh made the file where there was none (or the record is from // before the host kept originals, which it cannot tell apart): removed while it holds exactly // what the mesh wrote, and moved aside otherwise (removeFile, novox/hq issue 241). // - **The file is gone** — somebody removed it: nothing is put back, since bringing back a file a // person deleted is not giving back the state the mesh found; the original stays kept. // - **The file was changed since the mesh last wrote it** — it is somebody's again, as a block or // a JSON file the mesh wrote into stays somebody's: left exactly as it stands, never clobbered, // and the outcome names where the original is so a person can choose. // - **The kept copy cannot be read** — the mesh's file is left in place rather than deleted, and // the outcome says the original is missing. // - Otherwise the original is written back atomically, and the outcome is "restored". // // **Never fatal.** Each case that leaves the file says so and lets the record go; none stops the // rest of an unassignment. The kept copy itself is never deleted (novox/hq ADR 0100). func removeWhole(a store.Applied) (string, string, error) { if a.Kept == "" { // The mesh made it: deleted only while it holds exactly what the mesh wrote, otherwise moved // aside — a file created once and filled since is data (novox/hq issue 241). return removeFile(a, time.Now()) } current, err := os.ReadFile(a.Target) if errors.Is(err, os.ErrNotExist) { return "forgotten", "no longer there; the original the mesh wrote over stays kept at " + a.Kept, nil } if err != nil { return "kept", fmt.Sprintf("no longer declared, and it cannot be read (%v), so it was left as it "+ "is; the original the mesh wrote over is kept at %s", err, a.Kept), nil } if a.Wrote == "" || digestOf(string(current)) != a.Wrote { return "kept", "no longer declared, and changed on the machine since the mesh last wrote it, so " + "it was left as it is; the original the mesh wrote over is kept at " + a.Kept, nil } original, err := os.ReadFile(a.Kept) if err != nil { return "kept", fmt.Sprintf("no longer declared, but the original it was written over cannot be "+ "read at %s (%v), so the mesh's file was left in place", a.Kept, err), nil } info, err := os.Stat(a.Target) if err != nil { return "kept", fmt.Sprintf("no longer declared, and it cannot be seen (%v), so it was left as it "+ "is; the original the mesh wrote over is kept at %s", err, a.Kept), nil } mode := info.Mode().Perm() if a.KeptMode != "" { if m, err := strconv.ParseUint(a.KeptMode, 8, 32); err == nil { mode = os.FileMode(m).Perm() } } if err := writeAtomically(a.Target, original, mode); err != nil { return "kept", fmt.Sprintf("no longer declared, and the original kept at %s could not be put "+ "back (%v), so the mesh's file was left in place", a.Kept, err), nil } detail := "no longer declared; the original the mesh wrote over was put back from " + a.Kept if err := giveOwnerBack(a.Target, a.KeptOwner, info); err != nil { detail += "; " + err.Error() } if back, err := os.ReadFile(a.Target); err != nil || string(back) != string(original) { return "kept", "no longer declared; putting back the original kept at " + a.Kept + " did not leave it there — check the file by hand", nil } return "restored", detail, nil } // giveOwnerBack gives a file put back the owner its original was found with — "uid:gid" as a hold // records it — or, on a record from before the host kept that, the owner of what it replaced. func giveOwnerBack(path, owner string, was os.FileInfo) error { if owner == "" { return keepOwner(path, was) } uid, gid, err := idsOf(owner) if err != nil { return fmt.Errorf("its owner %q could not be read: %w", owner, err) } now, err := os.Stat(path) if err != nil { return err } if u, g, ok := ownerOf(now); ok && u == uid && g == gid { return nil } if err := os.Chown(path, uid, gid); err != nil { return fmt.Errorf("its owner %s could not be given back: %w", owner, err) } return nil }