feat(backup): autosave backend, continuous snapshots, and Settings modal
Background autosave toggle plus whole-book snapshots (default <book>/Folio-Backups, configurable location, retention, interval) with a one-minute change check in the main process. New Settings modal (sidebar, Ctrl+, , File menu) exposes autosave, snapshots, folder picker, and Snapshot now.
This commit is contained in:
parent
341b89cd8a
commit
1413e73015
11 changed files with 682 additions and 14 deletions
99
tests/run-snapshots-test.mjs
Normal file
99
tests/run-snapshots-test.mjs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// Standalone test for continuous snapshot logic (src/main/backup.ts).
|
||||
// Run with: node tests/run-snapshots-test.mjs
|
||||
import fs from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { bundleForTest } from "./helpers/bundle.mjs";
|
||||
|
||||
const { mod: project, cleanupBundle: cleanupProj } = await bundleForTest({
|
||||
name: "folio-snap-project",
|
||||
entryPoints: ["src/main/project.ts"],
|
||||
});
|
||||
const { mod: chapters, cleanupBundle: cleanupChap } = await bundleForTest({
|
||||
name: "folio-snap-chapters",
|
||||
entryPoints: ["src/main/chapters.ts"],
|
||||
});
|
||||
const { mod: backup, cleanupBundle: cleanupBackup } = await bundleForTest({
|
||||
name: "folio-snap-backup",
|
||||
entryPoints: ["src/main/backup.ts"],
|
||||
});
|
||||
|
||||
const { createBook } = project;
|
||||
const { createChapter, setChapterContent } = chapters;
|
||||
const {
|
||||
bindBackupSettings,
|
||||
getSnapshotRoot,
|
||||
listSnapshots,
|
||||
createSnapshot,
|
||||
pruneSnapshots,
|
||||
bookChangedSince,
|
||||
SNAPSHOT_DIR_NAME,
|
||||
} = backup;
|
||||
|
||||
let failures = 0;
|
||||
function ok(cond, msg) {
|
||||
if (cond) console.log(" ok -", msg);
|
||||
else { failures++; console.error("FAIL -", msg); }
|
||||
}
|
||||
|
||||
// In-memory settings store (backup.ts stays Electron-free).
|
||||
const settings = new Map();
|
||||
bindBackupSettings({
|
||||
get: (k, fb) => (settings.has(k) ? settings.get(k) : fb),
|
||||
set: (k, v) => { settings.set(k, v); },
|
||||
});
|
||||
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "folio-snap-"));
|
||||
createBook(dir, "Snap Book");
|
||||
const c = createChapter(dir, "One");
|
||||
setChapterContent(dir, c.id, "hello");
|
||||
|
||||
// default root = <book>/Folio-Backups
|
||||
ok(getSnapshotRoot(dir) === path.join(dir, SNAPSHOT_DIR_NAME), "default snapshot root is per-book folder");
|
||||
|
||||
// snapshot creates a timestamped copy with content
|
||||
const s1 = createSnapshot(dir, 20);
|
||||
ok(typeof s1 === "string" && fs.existsSync(s1), "createSnapshot returns existing dir: " + s1);
|
||||
ok(fs.existsSync(path.join(s1, "folio.json")), "snapshot contains folio.json");
|
||||
ok(listSnapshots(dir).length === 1, "one snapshot listed");
|
||||
|
||||
// snapshots never nest: second snapshot must not contain the backup dir
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
const s2 = createSnapshot(dir, 20);
|
||||
ok(listSnapshots(dir).length === 2, "two snapshots listed");
|
||||
ok(!fs.existsSync(path.join(s2, SNAPSHOT_DIR_NAME)), "snapshot does not contain nested backup dir");
|
||||
|
||||
// bookChangedSince: quiet -> false, touched -> true
|
||||
ok(bookChangedSince(dir, Date.now()) === false, "no change since now");
|
||||
setChapterContent(dir, c.id, "hello edited");
|
||||
ok(bookChangedSince(dir, Date.now() - 1000) === true, "detects content change");
|
||||
|
||||
// pruning keeps newest N
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
createSnapshot(dir, 20);
|
||||
ok(listSnapshots(dir).length === 3, "three snapshots before prune");
|
||||
pruneSnapshots(dir, 2);
|
||||
const kept = listSnapshots(dir);
|
||||
ok(kept.length === 2, "prune keeps newest 2");
|
||||
ok(!fs.existsSync(s1), "oldest snapshot pruned");
|
||||
|
||||
// custom root honored
|
||||
const custom = fs.mkdtempSync(path.join(os.tmpdir(), "folio-snaproot-"));
|
||||
settings.set("snapshotLocation", custom);
|
||||
ok(getSnapshotRoot(dir) === custom, "custom snapshot root honored");
|
||||
const s3 = createSnapshot(dir, 20);
|
||||
ok(s3.startsWith(custom), "snapshot lands in custom root");
|
||||
settings.delete("snapshotLocation");
|
||||
|
||||
// missing dir -> null, no throw
|
||||
ok(createSnapshot(path.join(dir, "nope"), 5) === null, "missing book returns null");
|
||||
|
||||
// cleanup
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
fs.rmSync(custom, { recursive: true, force: true });
|
||||
cleanupProj();
|
||||
cleanupChap();
|
||||
cleanupBackup();
|
||||
|
||||
if (failures) { console.error(`${failures} snapshot check(s) FAILED`); process.exit(1); }
|
||||
console.log("All snapshot checks passed.");
|
||||
Loading…
Add table
Add a link
Reference in a new issue