Files
mesh-host/internal/store/libraries.go
T
jschoubben 2a2be687f3 Keep libraries under groups of their own, and give modules users in them (hq ADR 0328, issue 567)
ADR 0328 supersedes ADR 0325: shared data is a library with one home, kept usable by
its group rather than by ACL entries. The engine now makes each library's group and
each module's user in its libraries' groups; keeps a library's root and requested
sub-paths in its group with group write and setgid on its home, making missing
sub-paths as the root is; waits for the operator's word before touching a root that
holds data, and on the take act walks it by descriptor (no link, same filesystem,
bounded and resumed, files with other names left); holds the containers of a library
whose disk or mount is not there; and puts the tool runner's account in the group.
The ACL keeping, mask repair and watch are gone; an access is again as before ADR 0325.
2026-10-12 22:11:09 +02:00

160 lines
5.5 KiB
Go

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"`
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 elsewhere (more than one link), 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"`
// 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)
}