fix: portable folder — stop storing absolute database paths

Moving the folder (C:\OpsLog → D:\OpsLog, or onto a stick) broke everything:
config.json and each profile's logbook path were absolute, so on the new
machine the pointer named a drive that no longer applied.

The logbook case was the dangerous one. db.Open CREATES what is missing, so if
the stale C: path happened to be creatable, the operator silently got a NEW
EMPTY logbook instead of an error — with their QSOs sitting untouched in the
folder they had just copied.

A path inside the application folder is now stored relative to it and resolved
against the current location at read time. A path OUTSIDE it (a synced folder,
a chosen drive) stays absolute and untouched — that is a deliberate choice; it
is only re-rooted when it has gone missing AND a file of the same name exists in
this install's data folder, i.e. the copied-folder case. With no such twin the
path is left alone so the failure is reported rather than papered over with an
unrelated database. Both rules are pinned by tests.
This commit is contained in:
2026-07-27 13:48:11 +02:00
parent 2ad72b19fb
commit 139b4675e3
3 changed files with 162 additions and 4 deletions
+71
View File
@@ -0,0 +1,71 @@
package main
import (
"os"
"path/filepath"
"testing"
)
// The portable-folder contract: a database that lives inside the application
// folder must be stored relative to it, so copying C:\OpsLog to D:\OpsLog (or to
// a USB stick) keeps working. A path the operator deliberately put elsewhere
// must be left exactly as it is.
func TestPortablePath(t *testing.T) {
base := appDir()
if base == "" {
t.Skip("no executable dir available")
}
inside := filepath.Join(base, "data", "logbook.db")
if got := portablePath(inside); got != "data/logbook.db" {
t.Errorf("inside the app folder: got %q, want %q", got, "data/logbook.db")
}
// Outside: an absolute path on another drive / a synced folder is a
// deliberate choice and must survive untouched.
outside := filepath.FromSlash("Z:/Sync/ham/logbook.db")
if got := portablePath(outside); got != outside {
t.Errorf("outside the app folder: got %q, want it unchanged", got)
}
if got := portablePath(""); got != "" {
t.Errorf("empty path: got %q", got)
}
}
func TestResolvePath(t *testing.T) {
base := appDir()
if base == "" {
t.Skip("no executable dir available")
}
// A relative path is anchored to the CURRENT install, whatever drive it is on.
want := filepath.Join(base, "data", "logbook.db")
if got := resolvePath("", "data/logbook.db"); got != want {
t.Errorf("relative: got %q, want %q", got, want)
}
// An absolute path that still exists is honoured as-is.
dir := t.TempDir()
real := filepath.Join(dir, "logbook.db")
if err := os.WriteFile(real, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if got := resolvePath(dir, real); got != real {
t.Errorf("existing absolute: got %q, want %q", got, real)
}
// The rescue: a path from ANOTHER machine, with the same file present in this
// install's data folder — the copied-folder case.
stale := filepath.FromSlash("C:/OldPC/OpsLog/data/logbook.db")
if got := resolvePath(dir, stale); got != real {
t.Errorf("stale absolute with a local twin: got %q, want %q", got, real)
}
// No local twin: leave it alone so the failure is REPORTED rather than
// silently replaced by an unrelated database.
missing := filepath.FromSlash("C:/OldPC/OpsLog/data/other.db")
if got := resolvePath(dir, missing); got != missing {
t.Errorf("stale absolute with no twin: got %q, want it unchanged", got)
}
}