package store import ( "encoding/json" "errors" "fmt" "io/fs" "os" "path/filepath" "sync" "time" ) // LibrariesName is the file, beside the node's state, of every library this machine is home to (novox/hq ADR 0328 // §6, §7, §9): which filesystem its root was kept on, whether and how it is kept, and the last walk under it. Its own // file for the reason the hand-overs have theirs: the take act runs beside an apply, whose save of the state would // lose a record written into it meanwhile. const LibrariesName = "libraries.json" // LibrariesPath is that file beside the state at statePath. func LibrariesPath(statePath string) string { return filepath.Join(filepath.Dir(statePath), LibrariesName) } // How a library came to be kept. const ( // KeptByWarrant is a root that held data, taken on the operator's word (ADR 0328 §7). KeptByWarrant = "warrant" // KeptEmpty is a root that held nothing yet when it was first kept. KeptEmpty = "empty" // KeptInShape is a root found already in the library's group with group write and setgid. KeptInShape = "in-shape" ) // LibraryRecord is one library as the engine keeps it on its home. Counts, names and mount points only beside its // root: never a path under it. type LibraryRecord struct { Library string `json:"library"` Root string `json:"root"` // GID is the library's group as it was last kept: what a walk stopped at its bound goes on with. GID int `json:"gid,omitempty"` // Mount is the mount point that held the root when the engine first saw it; Placed and Fs name that filesystem // (the engine's PlacedID, and what it was made from: UUID, filesystem id or source). Mount string `json:"mount,omitempty"` Placed string `json:"placed,omitempty"` Fs string `json:"fs,omitempty"` // Kept is that the mesh keeps it, on the filesystem KeptOn names (a Placed): another filesystem at the root is // judged afresh. How is why; Warrant and By the warrant and who asked, for one taken on a warrant. Kept bool `json:"kept,omitempty"` KeptOn string `json:"kept_on,omitempty"` How string `json:"how,omitempty"` Warrant string `json:"warrant,omitempty"` By string `json:"by,omitempty"` At time.Time `json:"at,omitzero"` // Act is the last walk under the root; Cursor where a walk that met its bound stopped (the names of each level // below the root, never said), for the next to go on from. Act *KeptAct `json:"act,omitempty"` // Paths are the sub-paths kept so far on the filesystem KeptOn names: one asked for later that holds data not // in the group waits for the operator's word as a root would, unless the library was taken whole. Paths []string `json:"paths,omitempty"` Cursor []string `json:"cursor,omitempty"` } // KeptAct is one walk under a library's root, as the engine records it. Counts only: never a path. type KeptAct struct { 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"` // On names the filesystem it walked (a Placed). On string `json:"on,omitempty"` // 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; Shared the files with another name root owns, left as they are. 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"` Shared int `json:"shared,omitempty"` // Linked is the files with another name an account owns, given the group; Stripped the files whose setuid or // setgid bit the group change took, and which stays taken. Linked int `json:"linked,omitempty"` Stripped int `json:"stripped,omitempty"` // Why is what stopped or failed it, in words with no path. Why string `json:"why,omitempty"` } // LibraryFile is the libraries' file, one writer at a time in this process. type LibraryFile struct { path string mu sync.Mutex } // NewLibraryFile is the libraries' file beside the state at statePath. func NewLibraryFile(statePath string) *LibraryFile { return &LibraryFile{path: LibrariesPath(statePath)} } // All is every library's record, by name. None is an empty map; a file that cannot be read is an error. func (k *LibraryFile) All() (map[string]LibraryRecord, error) { k.mu.Lock() defer k.mu.Unlock() return k.load() } // Update changes one library's record 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 *LibraryFile) Update(name string, change func(r *LibraryRecord, had bool) bool) error { k.mu.Lock() defer k.mu.Unlock() all, err := k.load() if err != nil { return err } r, had := all[name] r.Library = name if change(&r, had) { all[name] = r } else { delete(all, name) } return k.save(all) } func (k *LibraryFile) load() (map[string]LibraryRecord, error) { raw, err := os.ReadFile(k.path) if errors.Is(err, fs.ErrNotExist) { return map[string]LibraryRecord{}, nil } if err != nil { return nil, fmt.Errorf("reading the libraries: %w", err) } out := map[string]LibraryRecord{} if err := json.Unmarshal(raw, &out); err != nil { return nil, fmt.Errorf("the libraries (%s) are unreadable: %w", k.path, err) } return out, nil } func (k *LibraryFile) save(all map[string]LibraryRecord) error { raw, err := json.MarshalIndent(all, "", " ") if err != nil { return err } tmp, err := os.CreateTemp(filepath.Dir(k.path), ".libraries-*.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) }