Add database backups and two-digit disc numbers
Disc numbers: the organizer writes two-digit disc numbers by default
(Disc 01, 01-04 Title, {disc}), with an option for the plain style
(Disc 2, 2-04). Migration 8 adds the per-pair setting.
Backups (Settings -> Backups), similar to Sonarr:
- A backup is a zip with a consistent snapshot of the database (VACUUM
INTO, safe while running) and backup.json. Configurable folder,
schedule (off/1/3/7/14 days) and retention for scheduled backups;
manual backups are kept until deleted. List, download, delete.
- Restore from a listed backup or an uploaded zip: validated first
(integrity check, playlink tables, not a newer schema), staged next to
the live database, then the server restarts; at startup the staged
database is swapped in and the previous one kept as
playlink.db.pre-restore-<time>. Refused while a job runs. Backup names
are validated, so downloads/deletes cannot reach other files.
- Optional backup before every action in live mode: once per action the
user starts (link, promote, bulk, build/remove, tag edit, folder
renames, undo, purge) and before scans auto-link or auto-organize,
never per file and never in dry run. If the backup fails the action
does not run. The newest 10 action backups are kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
96ea856dfd
commit
49fc32096a
18 files changed
+1235
-47
No files matched your search
@@ -528,3 +528,40 @@ func TestPromoteRenamedMasterStaysLinked(t *testing.T) {
|
||||
t.Fatalf("undo should restore the original match, got %s -> %q", got.Status, got.RelMaster)
|
||||
}
|
||||
}
|
||||
|
||||
// The pre-action backup runs once per action (not per file) and never in
|
||||
// dry run.
|
||||
func TestBeforeActionOncePerScan(t *testing.T) {
|
||||
ev := setup(t, false)
|
||||
calls := 0
|
||||
ev.e.SetBeforeAction(func(context.Context, string) error { calls++; return nil })
|
||||
ev.pair.AutoLinkIdentical = true
|
||||
ev.pair.SettleMinutes = 0
|
||||
ev.st.SavePair(ctx, &ev.pair)
|
||||
for i := 0; i < 5; i++ {
|
||||
rel := filepath.Join("A/B", string(rune('a'+i))+".flac")
|
||||
ev.write(ev.master, rel, "same")
|
||||
ev.write(ev.slave, rel, "same")
|
||||
}
|
||||
ev.scan()
|
||||
if calls != 1 {
|
||||
t.Fatalf("auto-linking 5 files should back up once, got %d", calls)
|
||||
}
|
||||
ev.st.SetSetting(ctx, "dry_run", "1")
|
||||
if err := ev.e.BeforeAction(ctx, "link"); err != nil || calls != 1 {
|
||||
t.Fatal("no backup in dry run")
|
||||
}
|
||||
ev.st.SetSetting(ctx, "dry_run", "0")
|
||||
ev.e.SetBeforeAction(func(context.Context, string) error { return errors.New("disk full") })
|
||||
ev.write(ev.master, "A/B/z.flac", "same")
|
||||
ev.write(ev.slave, "A/B/z.flac", "same")
|
||||
if _, err := ev.e.StartScan(ev.pair.ID, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for ev.e.Job(ev.pair.ID).State == "running" {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
if sameInode(t, filepath.Join(ev.master, "A/B/z.flac"), filepath.Join(ev.slave, "A/B/z.flac")) {
|
||||
t.Fatal("a failed backup must stop the action")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user