Files
mesh-host/internal/apply/whole.go
T

112 lines
4.9 KiB
Go

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
}