package store import ( "encoding/json" "errors" "fmt" "io/fs" "os" "path/filepath" "sync" "time" ) // KeptName is the file, beside the node's state, of every shared folder the engine keeps (novox/hq ADR 0325 §5): // whose entries it added, on which filesystem it was placed, whether it was taken and under which warrant, and how // far its sweep got. Its own file for the reason the hand-overs have theirs: the take act, the watch and the sweep // run beside an apply, whose save of the state would lose a record written into it meanwhile. const KeptName = "kept-folders.json" // KeptPath is that file beside the state at statePath. func KeptPath(statePath string) string { return filepath.Join(filepath.Dir(statePath), KeptName) } // KeptUser is one identity the mesh keeps an entry for on a folder: its name, its number, its use and where it // runs (a container, or the tool runner standing for every module whose tools use the folder, ADR 0325 §4). type KeptUser struct { Identity string `json:"identity"` UID uint32 `json:"uid"` Use string `json:"use"` From string `json:"from"` } // How a folder came to be kept. const ( // KeptByWarrant is a folder that held data, taken on the operator's word (ADR 0325 §8). KeptByWarrant = "warrant" // KeptEmpty is a folder that held nothing yet when it was first kept (§5). KeptEmpty = "empty" // KeptMade is a folder the engine made under a parent on its placed filesystem (§9). KeptMade = "made" ) // KeptFolder is one shared folder the engine keeps, or has seen and waits to keep. Counts and mount points only // beside its own path: never a path under it. type KeptFolder struct { Path string `json:"path"` // Mount is the mount point of the filesystem the folder was placed on when the engine first saw it there: // later, a nearest existing parent on another one is the disk not there (ADR 0325 §9). Mount string `json:"mount,omitempty"` // Kept is that the mesh keeps it; How why (KeptByWarrant, KeptEmpty, KeptMade); Warrant and By the warrant and // who asked, for a folder taken on one; At since when. Kept bool `json:"kept,omitempty"` How string `json:"how,omitempty"` Warrant string `json:"warrant,omitempty"` By string `json:"by,omitempty"` At time.Time `json:"at,omitzero"` // Users are the identities whose entries are on everything under the folder; Want what the last apply // declared. Where they differ a walk adds or removes exactly the difference. Users []KeptUser `json:"users,omitempty"` Want []KeptUser `json:"want,omitempty"` // IDs are the accesses that last named the folder: what a leftover is said under once none does. IDs []string `json:"ids,omitempty"` // TakenOn is the mount point the operator's warrant was carried out on (ADR 0325 §8): the controller's word // that the folder was taken holds for that filesystem alone, and a folder moved to another asks again. TakenOn string `json:"taken_on,omitempty"` // Left is the entries the mesh added on a filesystem no longer at the folder (a disk mounted over it, or // one gone): taken out by a drop walk once that filesystem holds the folder again, said until then. Left []KeptLeft `json:"left,omitempty"` // Act is the last walk under the folder: a take, an add or a drop. Act *KeptAct `json:"act,omitempty"` // Sweep is how far the sweep got (ADR 0325 §10). Sweep KeptSweep `json:"sweep"` // Watches is how many folders under it are watched; OutOfWatches that the machine's watch limit was met, and // the sweep alone finds what is made where no watch is (§6). Watches int `json:"watches,omitempty"` OutOfWatches bool `json:"out_of_watches,omitempty"` } // KeptLeft is the mesh's entries left on one filesystem: its mount point and whose entries they are. type KeptLeft struct { Mount string `json:"mount"` Users []KeptUser `json:"users"` } // KeptAct is one walk under a kept folder, as the engine records it. Counts only: never a path. type KeptAct struct { // Act is take, add or drop. Act string `json:"act"` Warrant string `json:"warrant,omitempty"` By string `json:"by,omitempty"` State string `json:"state"` Started time.Time `json:"started"` Ended time.Time `json:"ended,omitzero"` // Folders and Files are the entries changed; Kept the ones that already had what they need; Links the links // passed by unfollowed; Other the entries neither a file nor a folder; Mounts the folders on another // filesystem not entered. Folders int `json:"folders,omitempty"` Files int `json:"files,omitempty"` Kept int `json:"kept,omitempty"` Links int `json:"links,omitempty"` Other int `json:"other,omitempty"` Mounts int `json:"mounts,omitempty"` // Why is what stopped or failed it, in words with no path. Why string `json:"why,omitempty"` } // KeptSweep is the sweep's place in a kept folder: where the last look stopped, below the folder, by the names // of each level (never said, only resumed from); when it last covered the folder whole; and what this pass found. type KeptSweep struct { Cursor []string `json:"cursor,omitempty"` Covered time.Time `json:"covered,omitzero"` // Read is how many entries this pass read so far; Narrowed how many gave a user less than its right and were // put back; Unrepaired how many could not be (an owner's bits, which the mesh never changes, or a write that // failed). Read int `json:"read,omitempty"` Narrowed int `json:"narrowed,omitempty"` Unrepaired int `json:"unrepaired,omitempty"` // LastUnrepaired is what the last whole pass could not put back: said until the next pass is through. LastUnrepaired int `json:"last_unrepaired,omitempty"` // Bound is that the last look ran out of its bound before the folder was covered. Bound bool `json:"bound,omitempty"` } // KeptFile is the kept folders' file, one writer at a time in this process. type KeptFile struct { path string mu sync.Mutex } // NewKeptFile is the kept folders' file beside the state at statePath. func NewKeptFile(statePath string) *KeptFile { return &KeptFile{path: KeptPath(statePath)} } // All is every kept or seen folder, by path. None is an empty map; a file that cannot be read is an error. func (k *KeptFile) All() (map[string]KeptFolder, error) { k.mu.Lock() defer k.mu.Unlock() return k.load() } // Update changes the record of one folder under the file's lock: change is given the record (empty when there is // none) and whether there was one, and says whether to keep it; a record not kept is removed. func (k *KeptFile) Update(path string, change func(f *KeptFolder, had bool) bool) error { k.mu.Lock() defer k.mu.Unlock() all, err := k.load() if err != nil { return err } f, had := all[path] f.Path = path if change(&f, had) { all[path] = f } else { delete(all, path) } return k.save(all) } func (k *KeptFile) load() (map[string]KeptFolder, error) { raw, err := os.ReadFile(k.path) if errors.Is(err, fs.ErrNotExist) { return map[string]KeptFolder{}, nil } if err != nil { return nil, fmt.Errorf("reading the kept folders: %w", err) } out := map[string]KeptFolder{} if err := json.Unmarshal(raw, &out); err != nil { return nil, fmt.Errorf("the kept folders (%s) are unreadable: %w", k.path, err) } return out, nil } func (k *KeptFile) save(all map[string]KeptFolder) error { raw, err := json.MarshalIndent(all, "", " ") if err != nil { return err } tmp, err := os.CreateTemp(filepath.Dir(k.path), ".kept-folders-*.json") if err != nil { return err } defer os.Remove(tmp.Name()) if _, err := tmp.Write(append(raw, '\n')); err != nil { tmp.Close() return err } if err := tmp.Sync(); err != nil { tmp.Close() return err } if err := tmp.Close(); err != nil { return err } return os.Rename(tmp.Name(), k.path) }