NFS and FUSE report a zero f_fsid, so another export or another FUSE mount at the same point was named alike and kept without a warrant; xfs's f_fsid follows the device number. A filesystem is now named by its UUID (its device matched in /dev/disk/by-uuid), else a nonzero f_fsid, else mountinfo's source; one that cannot be named is never kept or taken. Entries left on a filesystem not back within 30 days are forgotten, said once, and the naming read is shared per mount.
213 lines
8.7 KiB
Go
213 lines
8.7 KiB
Go
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"`
|
|
// Placed names the filesystem the folder was kept on (the engine's PlacedID: its mount point, source and type),
|
|
// so another filesystem mounted at the same point is a move, as another mount point is.
|
|
Placed string `json:"placed,omitempty"`
|
|
// Fs is what Placed was made from beside the mount point: "uuid:<the filesystem's UUID>", "fsid:<statfs's id>"
|
|
// or "source:<type>:<mountinfo's source>". Kept on this machine only, never said. Absent on a record from an
|
|
// earlier build, whose Placed is then not compared.
|
|
Fs string `json:"fs,omitempty"`
|
|
// TakenOn names, as Placed does, the filesystem 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. One that names no filesystem this way (a mount point, from an unmerged build) is not taken.
|
|
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"`
|
|
// Placed names that filesystem as KeptFolder.Placed does: entries left on the filesystem the folder is on
|
|
// now are the folder's own, never walked away.
|
|
Placed string `json:"placed,omitempty"`
|
|
Fs string `json:"fs,omitempty"`
|
|
Users []KeptUser `json:"users"`
|
|
// Since is when they were left: a filesystem not back at its mount point within LeftFor is gone, and what was
|
|
// left on it is forgotten (said once).
|
|
Since time.Time `json:"since,omitzero"`
|
|
}
|
|
|
|
// 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)
|
|
}
|