//go:build linux package main import ( "os" "path/filepath" "sync" ) // systemInstallDataDir keeps the portable "data beside the binary" layout where // it works, and falls back to the XDG data directory where it cannot. // // Both halves are needed on Linux, and only on Linux. A tarball or AppImage // unpacked into the home directory behaves exactly like the Windows build — // the folder travels with the program, which is the whole point of the design. // But the ordinary way software arrives here is a package that installs into // /usr/bin or /opt, where no user may write, and telling an operator to "move // the program somewhere writable" is telling them their distribution installed // it wrong. So when the folder beside the binary is read-only, OpsLog keeps its // data in ~/.local/share/OpsLog instead and says so in the log. // // Detection is by TRYING, not by matching path prefixes: /opt, /usr/local and a // NFS-mounted home are all writable on some stations and not on others, and the // only honest test is whether the write succeeds. func systemInstallDataDir(besideExe string) (string, bool) { xdgOnce.Do(func() { xdgDir, xdgUsed = resolveDataDir(besideExe) }) return xdgDir, xdgUsed } var ( xdgOnce sync.Once xdgDir string xdgUsed bool ) func resolveDataDir(besideExe string) (string, bool) { if writable(besideExe) { return "", false } base := os.Getenv("XDG_DATA_HOME") if base == "" { home, err := os.UserHomeDir() if err != nil || home == "" { return "", false // nowhere better to go; let the caller report the failure } base = filepath.Join(home, ".local", "share") } alt := filepath.Join(base, "OpsLog", "data") if !writable(alt) { return "", false } bootLog("data dir: %s is not writable — keeping the data in %s instead", besideExe, alt) return alt, true } // writable reports whether dir can be created and written to. The probe file is // removed again; a leftover in the data folder would be one more thing to // explain. func writable(dir string) bool { if err := os.MkdirAll(dir, 0o755); err != nil { return false } probe := filepath.Join(dir, ".writetest") if err := os.WriteFile(probe, []byte("ok"), 0o644); err != nil { return false } _ = os.Remove(probe) return true } // dataDirAdvice is what the operator is told when neither location works — a // full disk, or a home directory that is not writable either. const dataDirAdvice = "\n\nOpsLog keeps its data next to the program, or in ~/.local/share/OpsLog when that folder belongs to the system. Neither could be written to: check the disk is not full and that your home directory is writable."