An undeclared file is deleted only when it holds what the host wrote; otherwise moved aside (hq issue 241)
This commit is contained in:
@@ -2897,3 +2897,39 @@ func managerAway(ctx context.Context, sys system.System, scope, user string, run
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// removeFile undoes a file the host wrote whole, and destroys nothing it did not make (novox/hq
|
||||
// issue 241).
|
||||
//
|
||||
// **Deleted only when it holds exactly what this host last wrote** — then nothing is lost, because
|
||||
// the mesh can write it again. Anything else is moved aside to `<path>.removed-<time>` and reported
|
||||
// kept: a file created once and filled since by its program, one edited on the machine, one this
|
||||
// host has no record of writing. The rule a directory already follows — the host removes what it
|
||||
// made and keeps what it did not — applied to the one shape that was still deleted outright.
|
||||
func removeFile(a store.Applied, now time.Time) (string, string, error) {
|
||||
info, err := os.Lstat(a.Target)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return "forgotten", "no longer there", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if info.Mode().IsRegular() && a.Wrote != "" {
|
||||
content, err := os.ReadFile(a.Target)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if digestOf(string(content)) == a.Wrote {
|
||||
if err := os.Remove(a.Target); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return "removed", "no longer declared, and held only what the mesh wrote", nil
|
||||
}
|
||||
}
|
||||
aside := a.Target + ".removed-" + now.Format("20060102-150405")
|
||||
if err := os.Rename(a.Target, aside); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return "kept", fmt.Sprintf("no longer declared, and it held more than the mesh wrote — moved aside to %s; "+
|
||||
"remove it by hand once you know what it is", aside), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package apply
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
)
|
||||
|
||||
// A file the host wrote whole is deleted only when it holds exactly what the host wrote; anything
|
||||
// else is moved aside, never destroyed (novox/hq issue 241).
|
||||
func TestAnUndeclaredFileIsDeletedOnlyWhenItIsWhatTheMeshWrote(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
now := time.Date(2026, 10, 5, 9, 30, 0, 0, time.UTC)
|
||||
|
||||
pure := filepath.Join(dir, "config.conf")
|
||||
if err := os.WriteFile(pure, []byte("what the mesh wrote\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
action, _, err := removeFile(store.Applied{Type: "file", Target: pure, Wrote: digestOf("what the mesh wrote\n")}, now)
|
||||
if err != nil || action != "removed" {
|
||||
t.Fatalf("a file holding only the mesh's content was %q (%v)", action, err)
|
||||
}
|
||||
if _, err := os.Stat(pure); !os.IsNotExist(err) {
|
||||
t.Fatal("the mesh's own file is still there")
|
||||
}
|
||||
|
||||
for name, a := range map[string]store.Applied{
|
||||
"filled since": {Type: "file", Wrote: digestOf("seed\n")},
|
||||
"no record of it": {Type: "file"},
|
||||
} {
|
||||
path := filepath.Join(dir, strings.ReplaceAll(name, " ", "-")+".db")
|
||||
if err := os.WriteFile(path, []byte("seed\nand a program's data\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.Target = path
|
||||
action, detail, err := removeFile(a, now)
|
||||
if err != nil || action != "kept" {
|
||||
t.Fatalf("%s: was %q (%v)", name, action, err)
|
||||
}
|
||||
aside := path + ".removed-20261005-093000"
|
||||
got, err := os.ReadFile(aside)
|
||||
if err != nil || string(got) != "seed\nand a program's data\n" {
|
||||
t.Fatalf("%s: not moved aside intact to %s: %v", name, aside, err)
|
||||
}
|
||||
if !strings.Contains(detail, aside) {
|
||||
t.Errorf("%s: the report does not say where it went: %s", name, detail)
|
||||
}
|
||||
}
|
||||
|
||||
if action, _, _ := removeFile(store.Applied{Type: "file", Target: filepath.Join(dir, "gone")}, now); action != "forgotten" {
|
||||
t.Errorf("a file already gone was %q", action)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
)
|
||||
@@ -22,7 +23,8 @@ import (
|
||||
// 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.
|
||||
// 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
|
||||
@@ -36,13 +38,9 @@ import (
|
||||
// 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
|
||||
// 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)
|
||||
|
||||
Reference in New Issue
Block a user