112 lines
4.9 KiB
Go
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
|
|
}
|