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

+22 -22
View File
@@ -26,8 +26,8 @@ const QuarantineDirName = ".playlink-trash"
var (
ErrBusy = errors.New("another job is already running for this library pair")
ErrDowngrade = errors.New("the host copy is lower quality than the slave copy; confirm the downgrade to continue")
ErrConfirm = errors.New("this action modifies the host library and must be confirmed")
ErrDowngrade = errors.New("the master copy is lower quality than the slave copy; confirm the downgrade to continue")
ErrConfirm = errors.New("this action modifies the master library and must be confirmed")
)
type Engine struct {
@@ -163,7 +163,7 @@ func (e *Engine) startJob(pairID int64, kind string, run func(ctx context.Contex
type libs struct {
pair store.Pair
host fsops.Root
master fsops.Root
slave fsops.Root
quarantine string
dev uint64
@@ -187,23 +187,23 @@ func (e *Engine) withinAllowed(p string) bool {
// called before every scan and operation so configuration drift (a remount,
// a moved folder) is caught before anything is touched.
func (e *Engine) open(p store.Pair) (*libs, error) {
host, err := fsops.NewRoot(p.HostRoot)
master, err := fsops.NewRoot(p.MasterRoot)
if err != nil {
return nil, fmt.Errorf("host library: %w", err)
return nil, fmt.Errorf("master library: %w", err)
}
slave, err := fsops.NewRoot(p.SlaveRoot)
if err != nil {
return nil, fmt.Errorf("slave library: %w", err)
}
for _, r := range []string{host.Path, slave.Path} {
for _, r := range []string{master.Path, slave.Path} {
if !e.withinAllowed(r) {
return nil, fmt.Errorf("%s is outside the allowed roots %v", r, e.allowedRoots)
}
}
if fsops.Within(host.Path, slave.Path) || fsops.Within(slave.Path, host.Path) {
return nil, errors.New("host and slave libraries must not overlap or contain each other")
if fsops.Within(master.Path, slave.Path) || fsops.Within(slave.Path, master.Path) {
return nil, errors.New("master and slave libraries must not overlap or contain each other")
}
hi, err := fsops.Lstat(host.Path)
mi, err := fsops.Lstat(master.Path)
if err != nil {
return nil, err
}
@@ -211,17 +211,17 @@ func (e *Engine) open(p store.Pair) (*libs, error) {
if err != nil {
return nil, err
}
if hi.Dev != si.Dev {
return nil, fmt.Errorf("host and slave are on different filesystems (device %d vs %d): %w. "+
"In Docker, mount a single common parent folder instead of two separate volumes", hi.Dev, si.Dev, fsops.ErrCrossDevice)
if mi.Dev != si.Dev {
return nil, fmt.Errorf("master and slave are on different filesystems (device %d vs %d): %w. "+
"In Docker, mount a single common parent folder instead of two separate volumes", mi.Dev, si.Dev, fsops.ErrCrossDevice)
}
q := filepath.Clean(p.QuarantineDir)
if !filepath.IsAbs(q) {
return nil, errors.New("quarantine directory must be absolute")
}
if fsops.Within(host.Path, q) {
return nil, errors.New("quarantine directory must not be inside the host library")
if fsops.Within(master.Path, q) {
return nil, errors.New("quarantine directory must not be inside the master library")
}
if q == slave.Path || !e.withinAllowed(q) {
return nil, errors.New("quarantine directory must be inside the allowed roots and not the slave root itself")
@@ -236,7 +236,7 @@ func (e *Engine) open(p store.Pair) (*libs, error) {
if err != nil {
return nil, err
}
if qi.Dev != hi.Dev {
if qi.Dev != mi.Dev {
return nil, errors.New("quarantine directory must be on the same filesystem as the libraries")
}
@@ -250,7 +250,7 @@ func (e *Engine) open(p store.Pair) (*libs, error) {
if len(exts) == 0 {
return nil, errors.New("no file extensions configured")
}
return &libs{pair: p, host: host, slave: slave, quarantine: q, dev: hi.Dev, exts: exts}, nil
return &libs{pair: p, master: master, slave: slave, quarantine: q, dev: mi.Dev, exts: exts}, nil
}
// NormalizePair fills defaults and validates a pair. It returns non-fatal
@@ -260,7 +260,7 @@ func (e *Engine) NormalizePair(p *store.Pair) ([]string, error) {
if p.Name == "" {
return nil, errors.New("name is required")
}
p.HostRoot = filepath.Clean(strings.TrimSpace(p.HostRoot))
p.MasterRoot = filepath.Clean(strings.TrimSpace(p.MasterRoot))
p.SlaveRoot = filepath.Clean(strings.TrimSpace(p.SlaveRoot))
if strings.TrimSpace(p.QuarantineDir) == "" {
p.QuarantineDir = filepath.Join(p.SlaveRoot, QuarantineDirName)
@@ -288,7 +288,7 @@ func (e *Engine) NormalizePair(p *store.Pair) ([]string, error) {
return nil, err
}
// Store resolved paths so later containment checks compare like with like.
p.HostRoot, p.SlaveRoot, p.QuarantineDir = l.host.Path, l.slave.Path, l.quarantine
p.MasterRoot, p.SlaveRoot, p.QuarantineDir = l.master.Path, l.slave.Path, l.quarantine
return layoutWarnings(l), nil
}
@@ -312,20 +312,20 @@ func layoutWarnings(l *libs) []string {
}
return
}
hd, _ := list(l.host.Path)
md, _ := list(l.master.Path)
sd, sf := list(l.slave.Path)
if len(sd) == 0 && sf == 0 {
warns = append(warns, "The slave library is empty. Use Build to pick songs from the host library to hardlink into it.")
warns = append(warns, "The slave library is empty. Use Build to pick songs from the master library to hardlink into it.")
return warns
}
shared := 0
for d := range sd {
if hd[d] {
if md[d] {
shared++
}
}
if len(sd) > 0 && shared*2 < len(sd) {
warns = append(warns, fmt.Sprintf("Only %d of %d top-level folders in the slave library also exist in the host library. "+
warns = append(warns, fmt.Sprintf("Only %d of %d top-level folders in the slave library also exist in the master library. "+
"Both libraries should use the same folder layout, or most files will not match.", shared, len(sd)))
}
return warns