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.
160 lines
5.5 KiB
Go
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)
|
|
}
|