usermod --gid regroups every file under an account's home, and accounts were made with / as home: they now have /nonexistent, and an older account's home is moved there (without its files) before its group changes. A sub-path asked for later that holds data not in the group waits for the operator as the root would. The take act gives the group to hardlinked files an account owns (the *arr apps' downloads) and leaves root's; a file's setuid or setgid bit the regroup takes is never put back, so no setgid file hands out the library's group; each is counted. An apply no longer waits for a running take walk.
167 lines
6.0 KiB
Go
167 lines
6.0 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"`
|
|
// 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)
|
|
}
|