The host keeps the original of a file before writing over it (ADR 0102), but removing the file's record deleted the file and never put the original back, although ADR 0118 and the comment on meshMadeUnits say it does. A module writing /etc/pacman.conf, logrotate.conf, locale.conf or vconsole.conf whole would, once unassigned, leave the machine without the file. removeWhole now decides, in order: no kept original (the mesh made it) is removed as before; a file gone since is not brought back; a file changed since the mesh last wrote it is left as it stands, as a block or JSON write-into stays the machine's; an unreadable kept copy leaves the mesh's file in place. Otherwise the original goes back atomically with the mode and owner it was found with, now recorded beside Kept, and the outcome is "restored". None of it is fatal. The plan says "restore" for such a file. A kept original is carried only for the path it was kept from, and a file whose path moved keeps the original at its new path first, so a moved file is never given another path's original.
114 lines
4.9 KiB
Go
114 lines
4.9 KiB
Go
package apply
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
|
|
"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, as before.
|
|
// - **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 == "" {
|
|
if err := os.RemoveAll(a.Target); err != nil {
|
|
return "", "", err
|
|
}
|
|
if _, err := os.Stat(a.Target); !errors.Is(err, os.ErrNotExist) {
|
|
return "", "", fmt.Errorf("%s is still there after removing it", a.Target)
|
|
}
|
|
return "removed", "no longer declared", nil
|
|
}
|
|
|
|
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
|
|
}
|