Rename host library to master throughout

The two libraries are now "master" and "slave" everywhere: UI text, API
JSON fields (master_root, rel_master, master_size, ...), Go identifiers,
database columns, quality verdicts, quarantine subfolders and docs.

Existing databases are upgraded by a new migration that renames the
columns in place and rewrites stored "host" values, so pairs, history,
cached verdicts and restorable quarantine entries carry over. Network
"host" usages (SplitHostPort, the Origin/Host check) are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cdnutterandClaude Opus 5.5 committed 2026-09-22 16:50:55 -07:00
1 parent d06b6d546d
commit 5b088bb4a6
19 files changed
+339 -326

No files matched your search

+79 -79
View File
@@ -19,15 +19,15 @@ import (
)
type LinkOptions struct {
// AllowDowngrade confirms linking when the host copy scored lower.
// AllowDowngrade confirms linking when the master copy scored lower.
AllowDowngrade bool
Trigger string
}
// snapshot holds the verified state of both files right before an operation.
type snapshot struct {
hostAbs, slaveAbs string
hi, si fsops.Info
masterAbs, slaveAbs string
mi, si fsops.Info
}
// preflight re-validates a match against the filesystem: both files must be
@@ -36,28 +36,28 @@ type snapshot struct {
func preflight(l *libs, m store.Match) (snapshot, error) {
var s snapshot
var err error
if s.hostAbs, err = l.host.Resolve(m.RelHost); err != nil {
if s.masterAbs, err = l.master.Resolve(m.RelMaster); err != nil {
return s, err
}
if s.slaveAbs, err = l.slave.Resolve(m.RelSlave); err != nil {
return s, err
}
if s.hi, err = fsops.RegularFile(s.hostAbs); err != nil {
return s, fmt.Errorf("host file: %w", err)
if s.mi, err = fsops.RegularFile(s.masterAbs); err != nil {
return s, fmt.Errorf("master file: %w", err)
}
if s.si, err = fsops.RegularFile(s.slaveAbs); err != nil {
return s, fmt.Errorf("slave file: %w", err)
}
if s.hi.Ino != m.HostIno || !s.hi.Unchanged(m.HostSize, m.HostMtime) {
return s, fmt.Errorf("host %w", fsops.ErrChanged)
if s.mi.Ino != m.MasterIno || !s.mi.Unchanged(m.MasterSize, m.MasterMtime) {
return s, fmt.Errorf("master %w", fsops.ErrChanged)
}
if s.si.Ino != m.SlaveIno || !s.si.Unchanged(m.SlaveSize, m.SlaveMtime) {
return s, fmt.Errorf("slave %w", fsops.ErrChanged)
}
if s.hi.Dev != s.si.Dev || s.hi.Dev != l.dev {
if s.mi.Dev != s.si.Dev || s.mi.Dev != l.dev {
return s, fsops.ErrCrossDevice
}
if s.hi.SameInode(s.si) {
if s.mi.SameInode(s.si) {
return s, errors.New("already linked")
}
return s, nil
@@ -128,7 +128,7 @@ func (e *Engine) finish(ctx context.Context, o *store.Operation, err error) erro
e.log.Warn("operation failed", "op", o.ID, "action", o.Action, "slave", o.RelSlave, "err", err)
} else {
o.Status = store.OpDone
e.log.Info("operation done", "op", o.ID, "action", o.Action, "slave", o.RelSlave, "host", o.RelHost)
e.log.Info("operation done", "op", o.ID, "action", o.Action, "slave", o.RelSlave, "master", o.RelMaster)
}
if ferr := e.st.FinishOp(ctx, o.ID, o.Status, o.Error); ferr != nil && err == nil {
return ferr
@@ -142,10 +142,10 @@ func verdictOf(m store.Match) probe.Verdict {
return v
}
// Link replaces the slave file with a hardlink of the host file.
// Link replaces the slave file with a hardlink of the master file.
//
// Identical files are re-compared byte for byte right before linking and no
// quarantine copy is kept (the data exists in the host). Non-identical files
// quarantine copy is kept (the data exists in the master). Non-identical files
// are quarantined first so the operation can be undone.
func (e *Engine) Link(ctx context.Context, matchID int64, opt LinkOptions) (store.Operation, error) {
e.mu.Lock()
@@ -176,7 +176,7 @@ func (e *Engine) Link(ctx context.Context, matchID int64, opt LinkOptions) (stor
return store.Operation{}, err
}
if identical {
same, err := fsops.SameContent(s.hostAbs, s.slaveAbs)
same, err := fsops.SameContent(s.masterAbs, s.slaveAbs)
if err != nil {
return store.Operation{}, err
}
@@ -186,7 +186,7 @@ func (e *Engine) Link(ctx context.Context, matchID int64, opt LinkOptions) (stor
}
}
newRel := filepath.Join(filepath.Dir(m.RelSlave), filepath.Base(m.RelHost))
newRel := filepath.Join(filepath.Dir(m.RelSlave), filepath.Base(m.RelMaster))
newAbs, err := l.slave.Resolve(newRel)
if err != nil {
return store.Operation{}, err
@@ -198,7 +198,7 @@ func (e *Engine) Link(ctx context.Context, matchID int64, opt LinkOptions) (stor
}
}
o := store.Operation{PairID: l.pair.ID, Action: store.ActLink, RelHost: m.RelHost, RelSlave: m.RelSlave,
o := store.Operation{PairID: l.pair.ID, Action: store.ActLink, RelMaster: m.RelMaster, RelSlave: m.RelSlave,
RelNew: newRel, Trigger: opt.Trigger}
if identical {
o.Bytes = s.si.Size // space freed
@@ -226,7 +226,7 @@ func (e *Engine) Link(ctx context.Context, matchID int64, opt LinkOptions) (stor
}
}
if !rename {
if err := fsops.ReplaceWithLink(s.hostAbs, s.hi, s.slaveAbs); err != nil {
if err := fsops.ReplaceWithLink(s.masterAbs, s.mi, s.slaveAbs); err != nil {
if q != "" {
if cur, e2 := fsops.Lstat(s.slaveAbs); e2 == nil && cur.SameInode(s.si) {
dropQuarantine(l, q, s.si) // original untouched
@@ -237,7 +237,7 @@ func (e *Engine) Link(ctx context.Context, matchID int64, opt LinkOptions) (stor
}
return nil
}
if err := fsops.LinkNew(s.hostAbs, s.hi, newAbs); err != nil {
if err := fsops.LinkNew(s.masterAbs, s.mi, newAbs); err != nil {
if q != "" {
dropQuarantine(l, q, s.si)
e.clearQuarantine(ctx, o.ID)
@@ -259,7 +259,7 @@ func (e *Engine) clearQuarantine(ctx context.Context, opID int64) {
}
// Promote is the override path: the slave copy is better, so it replaces the
// host file (whose original is quarantined) and both libraries share it.
// master file (whose original is quarantined) and both libraries share it.
func (e *Engine) Promote(ctx context.Context, matchID int64, confirmed bool) (store.Operation, error) {
if !confirmed {
return store.Operation{}, ErrConfirm
@@ -282,20 +282,20 @@ func (e *Engine) Promote(ctx context.Context, matchID int64, confirmed bool) (st
if err != nil {
return store.Operation{}, err
}
newHostRel := filepath.Join(filepath.Dir(m.RelHost), filepath.Base(m.RelSlave))
newHostAbs, err := l.host.Resolve(newHostRel)
newMasterRel := filepath.Join(filepath.Dir(m.RelMaster), filepath.Base(m.RelSlave))
newMasterAbs, err := l.master.Resolve(newMasterRel)
if err != nil {
return store.Operation{}, err
}
rename := newHostRel != m.RelHost
rename := newMasterRel != m.RelMaster
if rename {
if _, err := os.Lstat(newHostAbs); err == nil {
return store.Operation{}, fmt.Errorf("host already has %s: %w", newHostRel, fsops.ErrExists)
if _, err := os.Lstat(newMasterAbs); err == nil {
return store.Operation{}, fmt.Errorf("master already has %s: %w", newMasterRel, fsops.ErrExists)
}
}
o := store.Operation{PairID: l.pair.ID, Action: store.ActPromote, RelHost: m.RelHost, RelSlave: m.RelSlave,
RelNew: newHostRel, QuarantineSide: "host", Trigger: "manual"}
o := store.Operation{PairID: l.pair.ID, Action: store.ActPromote, RelMaster: m.RelMaster, RelSlave: m.RelSlave,
RelNew: newMasterRel, QuarantineSide: "master", Trigger: "manual"}
if err := e.begin(ctx, &o); err != nil {
return o, err
}
@@ -303,31 +303,31 @@ func (e *Engine) Promote(ctx context.Context, matchID int64, confirmed bool) (st
return o, nil
}
err = func() error {
q := quarantinePath(l, o.ID, "host", m.RelHost)
if err := quarantine(s.hostAbs, s.hi, q); err != nil {
q := quarantinePath(l, o.ID, "master", m.RelMaster)
if err := quarantine(s.masterAbs, s.mi, q); err != nil {
return fmt.Errorf("quarantine: %w", err)
}
o.QuarantinePath = q
if _, err := e.st.DB.ExecContext(ctx, `UPDATE operations SET quarantine_path = ? WHERE id = ?`, q, o.ID); err != nil {
dropQuarantine(l, q, s.hi)
dropQuarantine(l, q, s.mi)
return err
}
if !rename {
if err := fsops.ReplaceWithLink(s.slaveAbs, s.si, s.hostAbs); err != nil {
if cur, e2 := fsops.Lstat(s.hostAbs); e2 == nil && cur.SameInode(s.hi) {
dropQuarantine(l, q, s.hi)
if err := fsops.ReplaceWithLink(s.slaveAbs, s.si, s.masterAbs); err != nil {
if cur, e2 := fsops.Lstat(s.masterAbs); e2 == nil && cur.SameInode(s.mi) {
dropQuarantine(l, q, s.mi)
e.clearQuarantine(ctx, o.ID)
}
return err
}
return nil
}
if err := fsops.LinkNew(s.slaveAbs, s.si, newHostAbs); err != nil {
dropQuarantine(l, q, s.hi)
if err := fsops.LinkNew(s.slaveAbs, s.si, newMasterAbs); err != nil {
dropQuarantine(l, q, s.mi)
e.clearQuarantine(ctx, o.ID)
return err
}
return removeOriginal(s.hostAbs, s.hi)
return removeOriginal(s.masterAbs, s.mi)
}()
err = e.finish(ctx, &o, err)
e.refresh(ctx, l, m.RelSlave)
@@ -368,15 +368,15 @@ func expand(ctx context.Context, l *libs, root fsops.Root, rels []string) ([]str
return sortedKeys(set), nil
}
// StartBuild hardlinks the chosen host files/folders into the slave library,
// mirroring the host path. Existing slave files are never overwritten.
// StartBuild hardlinks the chosen master files/folders into the slave library,
// mirroring the master path. Existing slave files are never overwritten.
func (e *Engine) StartBuild(pairID int64, rels []string) (*Job, error) {
return e.startJob(pairID, "build", func(ctx context.Context, j *Job) error {
l, err := e.loadPair(ctx, pairID)
if err != nil {
return err
}
files, err := expand(ctx, l, l.host, rels)
files, err := expand(ctx, l, l.master, rels)
if err != nil {
return err
}
@@ -395,7 +395,7 @@ func (e *Engine) StartBuild(pairID int64, rels []string) (*Job, error) {
func (e *Engine) buildOne(ctx context.Context, j *Job, l *libs, rel string) {
e.mu.Lock()
defer e.mu.Unlock()
hostAbs, err := l.host.Resolve(rel)
masterAbs, err := l.master.Resolve(rel)
if err != nil {
j.skip(rel + ": " + err.Error())
return
@@ -405,20 +405,20 @@ func (e *Engine) buildOne(ctx context.Context, j *Job, l *libs, rel string) {
j.skip(rel + ": " + err.Error())
return
}
hi, err := fsops.RegularFile(hostAbs)
mi, err := fsops.RegularFile(masterAbs)
if err != nil {
j.skip(rel + ": " + err.Error())
return
}
if si, err := fsops.Lstat(slaveAbs); err == nil {
if si.SameInode(hi) {
if si.SameInode(mi) {
j.count("already linked")
} else {
j.skip(rel + ": a different file already exists in the slave library (scan, then use Review)")
}
return
}
o := store.Operation{PairID: l.pair.ID, Action: store.ActBuild, RelHost: rel, RelSlave: rel, RelNew: rel, Trigger: "build"}
o := store.Operation{PairID: l.pair.ID, Action: store.ActBuild, RelMaster: rel, RelSlave: rel, RelNew: rel, Trigger: "build"}
if err := e.begin(ctx, &o); err != nil {
j.skip(rel + ": " + err.Error())
return
@@ -429,7 +429,7 @@ func (e *Engine) buildOne(ctx context.Context, j *Job, l *libs, rel string) {
}
err = fsops.MkdirAllLike(filepath.Dir(slaveAbs))
if err == nil {
err = fsops.LinkNew(hostAbs, hi, slaveAbs)
err = fsops.LinkNew(masterAbs, mi, slaveAbs)
}
if err = e.finish(ctx, &o, err); err != nil {
j.skip(rel + ": " + err.Error())
@@ -439,7 +439,7 @@ func (e *Engine) buildOne(ctx context.Context, j *Job, l *libs, rel string) {
e.refresh(ctx, l, rel)
}
// StartRemove deletes slave files that are hardlinks of the host file at the
// StartRemove deletes slave files that are hardlinks of the master file at the
// same path. Anything that is not such a link is left alone.
func (e *Engine) StartRemove(pairID int64, rels []string) (*Job, error) {
return e.startJob(pairID, "remove", func(ctx context.Context, j *Job) error {
@@ -466,15 +466,15 @@ func (e *Engine) StartRemove(pairID int64, rels []string) (*Job, error) {
func (e *Engine) removeOne(ctx context.Context, j *Job, l *libs, rel string) {
e.mu.Lock()
defer e.mu.Unlock()
hostAbs, err1 := l.host.Resolve(rel)
masterAbs, err1 := l.master.Resolve(rel)
slaveAbs, err2 := l.slave.Resolve(rel)
if err1 != nil || err2 != nil {
j.skip(rel + ": invalid path")
return
}
hi, err := fsops.RegularFile(hostAbs)
mi, err := fsops.RegularFile(masterAbs)
if err != nil {
j.skip(rel + ": no host file at the same path, so it is not removed")
j.skip(rel + ": no master file at the same path, so it is not removed")
return
}
si, err := fsops.RegularFile(slaveAbs)
@@ -482,11 +482,11 @@ func (e *Engine) removeOne(ctx context.Context, j *Job, l *libs, rel string) {
j.skip(rel + ": " + err.Error())
return
}
if !si.SameInode(hi) {
j.skip(rel + ": not a hardlink of the host file, so it is not removed")
if !si.SameInode(mi) {
j.skip(rel + ": not a hardlink of the master file, so it is not removed")
return
}
o := store.Operation{PairID: l.pair.ID, Action: store.ActRemove, RelHost: rel, RelSlave: rel, Trigger: "build"}
o := store.Operation{PairID: l.pair.ID, Action: store.ActRemove, RelMaster: rel, RelSlave: rel, Trigger: "build"}
if err := e.begin(ctx, &o); err != nil {
j.skip(rel + ": " + err.Error())
return
@@ -495,7 +495,7 @@ func (e *Engine) removeOne(ctx context.Context, j *Job, l *libs, rel string) {
j.count("planned")
return
}
err = fsops.RemoveLink(slaveAbs, hi)
err = fsops.RemoveLink(slaveAbs, mi)
if err = e.finish(ctx, &o, err); err != nil {
j.skip(rel + ": " + err.Error())
return
@@ -533,7 +533,7 @@ func (e *Engine) Undo(ctx context.Context, opID int64) error {
}
return p
}
hostAbs := res(l.host, o.RelHost)
masterAbs := res(l.master, o.RelMaster)
slaveAbs := res(l.slave, o.RelSlave)
if err != nil {
return err
@@ -541,24 +541,24 @@ func (e *Engine) Undo(ctx context.Context, opID int64) error {
switch o.Action {
case store.ActBuild:
hi, err := fsops.RegularFile(hostAbs)
mi, err := fsops.RegularFile(masterAbs)
if err != nil {
return err
}
if err := fsops.RemoveLink(slaveAbs, hi); err != nil {
if err := fsops.RemoveLink(slaveAbs, mi); err != nil {
return err
}
pruneEmpty(filepath.Dir(slaveAbs), l.slave.Path)
case store.ActRemove:
hi, err := fsops.RegularFile(hostAbs)
mi, err := fsops.RegularFile(masterAbs)
if err != nil {
return err
}
if err := fsops.MkdirAllLike(filepath.Dir(slaveAbs)); err != nil {
return err
}
if err := fsops.LinkNew(hostAbs, hi, slaveAbs); err != nil {
if err := fsops.LinkNew(masterAbs, mi, slaveAbs); err != nil {
return err
}
@@ -567,7 +567,7 @@ func (e *Engine) Undo(ctx context.Context, opID int64) error {
if err != nil {
return err
}
hi, err := fsops.RegularFile(hostAbs)
mi, err := fsops.RegularFile(masterAbs)
if err != nil {
return err
}
@@ -575,8 +575,8 @@ func (e *Engine) Undo(ctx context.Context, opID int64) error {
if err != nil {
return err
}
if !cur.SameInode(hi) {
return errors.New("the slave file is no longer linked to the host file; refusing to undo")
if !cur.SameInode(mi) {
return errors.New("the slave file is no longer linked to the master file; refusing to undo")
}
renamed := o.RelNew != o.RelSlave
if renamed {
@@ -586,7 +586,7 @@ func (e *Engine) Undo(ctx context.Context, opID int64) error {
}
if o.QuarantinePath == "" {
// Identical link: give the slave its own copy of the bytes again.
if err := fsops.CopyReplace(hostAbs, slaveAbs, hi.Mode); err != nil {
if err := fsops.CopyReplace(masterAbs, slaveAbs, mi.Mode); err != nil {
return err
}
} else {
@@ -605,14 +605,14 @@ func (e *Engine) Undo(ctx context.Context, opID int64) error {
dropQuarantine(l, o.QuarantinePath, qi)
}
if renamed {
if err := fsops.RemoveLink(newAbs, hi); err != nil {
if err := fsops.RemoveLink(newAbs, mi); err != nil {
return err
}
}
e.refresh(ctx, l, o.RelNew)
case store.ActPromote:
newHostAbs := res(l.host, o.RelNew)
newMasterAbs := res(l.master, o.RelNew)
if err != nil {
return err
}
@@ -620,25 +620,25 @@ func (e *Engine) Undo(ctx context.Context, opID int64) error {
if err != nil {
return err
}
cur, err := fsops.RegularFile(newHostAbs)
cur, err := fsops.RegularFile(newMasterAbs)
if err != nil {
return err
}
if !cur.SameInode(si) {
return errors.New("the host file changed after promotion; refusing to undo")
return errors.New("the master file changed after promotion; refusing to undo")
}
qi, err := e.quarantined(l, o.QuarantinePath)
if err != nil {
return err
}
if o.RelNew == o.RelHost {
err = fsops.ReplaceWithLink(o.QuarantinePath, qi, hostAbs)
if o.RelNew == o.RelMaster {
err = fsops.ReplaceWithLink(o.QuarantinePath, qi, masterAbs)
} else {
if _, e2 := os.Lstat(hostAbs); e2 == nil {
return fmt.Errorf("%s: %w", o.RelHost, fsops.ErrExists)
if _, e2 := os.Lstat(masterAbs); e2 == nil {
return fmt.Errorf("%s: %w", o.RelMaster, fsops.ErrExists)
}
if err = fsops.LinkNew(o.QuarantinePath, qi, hostAbs); err == nil {
err = fsops.RemoveLink(newHostAbs, si)
if err = fsops.LinkNew(o.QuarantinePath, qi, masterAbs); err == nil {
err = fsops.RemoveLink(newMasterAbs, si)
}
}
if err != nil {
@@ -732,8 +732,8 @@ func (e *Engine) Recover(ctx context.Context) {
}
for _, o := range ops {
if p, err := e.st.Pair(ctx, o.PairID); err == nil {
for _, root := range []string{p.SlaveRoot, p.HostRoot} {
for _, rel := range []string{o.RelSlave, o.RelNew, o.RelHost} {
for _, root := range []string{p.SlaveRoot, p.MasterRoot} {
for _, rel := range []string{o.RelSlave, o.RelNew, o.RelMaster} {
if rel == "" {
continue
}
@@ -763,15 +763,15 @@ type TreeEntry struct {
Linked int `json:"linked,omitempty"`
}
// Tree lists a host directory and shows how much of it is in the slave.
// Tree lists a master directory and shows how much of it is in the slave.
func (e *Engine) Tree(ctx context.Context, pairID int64, rel string) ([]TreeEntry, error) {
l, err := e.loadPair(ctx, pairID)
if err != nil {
return nil, err
}
dir := l.host.Path
dir := l.master.Path
if rel != "" {
if dir, err = l.host.Resolve(rel); err != nil {
if dir, err = l.master.Resolve(rel); err != nil {
return nil, err
}
}
@@ -779,7 +779,7 @@ func (e *Engine) Tree(ctx context.Context, pairID int64, rel string) ([]TreeEntr
if err != nil {
return nil, err
}
linked, _ := e.st.LinkedHostPaths(ctx, pairID, rel)
linked, _ := e.st.LinkedMasterPaths(ctx, pairID, rel)
perChild := map[string]int{}
for _, p := range linked {
sub := p
@@ -807,16 +807,16 @@ func (e *Engine) Tree(ctx context.Context, pairID int64, rel string) ([]TreeEntr
if !l.hasExt(name) {
continue
}
hi, err := fsops.Lstat(filepath.Join(dir, name))
mi, err := fsops.Lstat(filepath.Join(dir, name))
if err != nil {
continue
}
te.Size = hi.Size
te.Size = mi.Size
te.State = "missing"
if sp, err := l.slave.Resolve(child); err == nil {
if si, err := fsops.Lstat(sp); err == nil {
te.State = "conflict"
if si.SameInode(hi) {
if si.SameInode(mi) {
te.State = "linked"
}
}