mirror of
https://github.com/grub-basket/SP.git
synced 2026-07-22 07:46:27 +00:00
0.99.14: fix encrypted notes disappearing on app restart
This commit is contained in:
parent
3060869b79
commit
4add22ecfc
6 changed files with 108 additions and 52 deletions
64
main.js
64
main.js
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "stashpad",
|
||||
"name": "Stashpad",
|
||||
"version": "0.99.13",
|
||||
"version": "0.99.14",
|
||||
"minAppVersion": "1.7.0",
|
||||
"description": "Chat-style nested-notes view: rapid capture, outliner navigation, in-place editing.",
|
||||
"author": "Human",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "stashpad-obsidian",
|
||||
"version": "0.99.13",
|
||||
"version": "0.99.14",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
|
|
|
|||
13
release-notes/0.99.14.md
Normal file
13
release-notes/0.99.14.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# 0.99.14 — Encrypted notes no longer disappear on restart
|
||||
|
||||
Fixes a bug where a locked (encrypted) note's 🔒 placeholder could vanish after
|
||||
restarting the app. The placeholders were rebuilt by scanning the vault's file
|
||||
index for the encrypted blobs, but on a fresh start the view renders before the
|
||||
vault finishes indexing those files — so the placeholders came up empty and the
|
||||
registry that anchors them could be cleared.
|
||||
|
||||
Now the placeholders render from the saved registry (available immediately at
|
||||
startup) and the registry is reconciled against the disk directly (and only
|
||||
after the vault is fully loaded), so locked notes stay put across restarts. A
|
||||
locked note whose registry entry had been lost is recovered from its sidecar on
|
||||
the next launch.
|
||||
|
|
@ -14,6 +14,14 @@
|
|||
}
|
||||
// wipe every note in F EXCEPT the Home note
|
||||
for (const f of app.vault.getMarkdownFiles().filter((f) => (f.parent?.path || "") === F && f.basename !== `Home-${F}`)) await app.vault.delete(f);
|
||||
// clear encryption artifacts (.stashenc blobs + .stashmeta sidecars) + registry entries
|
||||
try { const li = await ad.list(F); for (const path of li.files) if (/\.(stashenc|stashmeta)$/.test(path)) await ad.remove(path); } catch (e) {}
|
||||
const p = app.plugins.plugins["stashpad"];
|
||||
if (p?.settings?.lockedSubtrees) {
|
||||
const before = p.settings.lockedSubtrees.length;
|
||||
p.settings.lockedSubtrees = p.settings.lockedSubtrees.filter((e) => (e.folder || "").replace(/\/+$/, "") !== F);
|
||||
if (p.settings.lockedSubtrees.length !== before) await p.saveSettings();
|
||||
}
|
||||
if (!(await ad.exists(F))) await ad.mkdir(F);
|
||||
if (!app.vault.getAbstractFileByPath(`${F}/Home-${F}.md`)) {
|
||||
await app.vault.create(`${F}/Home-${F}.md`, `---\nid: __root__\nparent: __root__\ncreated: 2026-06-08T12:00:00\nattachments: []\n---\n# ${F} Home\n`);
|
||||
|
|
|
|||
71
src/main.ts
71
src/main.ts
|
|
@ -672,8 +672,10 @@ export default class StashpadPlugin extends Plugin {
|
|||
// If a vault password was remembered in this device's keychain, unlock now.
|
||||
void this.encryption.tryAutoUnlock();
|
||||
// Reconcile the locked-subtree registry from on-disk `.stashmeta` sidecars
|
||||
// (recovers placeholder placement after a settings desync or cross-device sync).
|
||||
void this.reconcileLockedRegistry();
|
||||
// (recovers placeholder placement after a settings desync or cross-device
|
||||
// sync). Deferred to onLayoutReady (below) so it runs AFTER the vault has
|
||||
// finished indexing the `.stashenc` blobs — running it during onload once
|
||||
// wiped the registry against an empty file index.
|
||||
this.settingTab = new StashpadSettingTab(this.app, this);
|
||||
this.addSettingTab(this.settingTab);
|
||||
|
||||
|
|
@ -705,6 +707,9 @@ export default class StashpadPlugin extends Plugin {
|
|||
// are accurate (avoids creating a duplicate before the cache lists
|
||||
// the existing one). New folders are seeded at creation time instead.
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
// Vault is fully indexed now — safe to reconcile locked placeholders
|
||||
// (drop entries whose blob is truly gone, add cross-device blobs).
|
||||
void this.reconcileLockedRegistry();
|
||||
window.setTimeout(() => { void this.seedLocalAuthorStubsEverywhere(); }, 4000);
|
||||
// 0.79.12: register each Stashpad folder's _archive in Obsidian's
|
||||
// "Excluded files" so native search / quick switcher / graph / link
|
||||
|
|
@ -2880,20 +2885,38 @@ export default class StashpadPlugin extends Plugin {
|
|||
* blob was synced in from another device with no local registry entry. Adds an
|
||||
* entry for any `.stashenc` missing one; drops entries whose blob is gone. */
|
||||
async reconcileLockedRegistry(): Promise<void> {
|
||||
const blobs = this.app.vault.getFiles().filter((f) => f.extension === "stashenc");
|
||||
const blobPaths = new Set(blobs.map((f) => f.path));
|
||||
let reg = (this.settings.lockedSubtrees ?? []).filter((e) => blobPaths.has(e.blob));
|
||||
const orig = this.settings.lockedSubtrees ?? [];
|
||||
// DROP entries via the ADAPTER (disk truth), NOT vault.getFiles() — the vault
|
||||
// index lags on startup, and filtering against it once wiped the whole
|
||||
// registry when this ran before indexing finished (encrypted notes vanished
|
||||
// on restart). adapter.exists reads disk directly, so it's accurate even
|
||||
// mid-startup. (Unlock already removes its own entry, so a surviving entry
|
||||
// whose blob is genuinely gone is the rare external-deletion case.)
|
||||
let reg: typeof orig = [];
|
||||
for (const e of orig) {
|
||||
const ef = (e.folder ?? "").replace(/\/+$/, "");
|
||||
if (ef === "_deleted" || ef.startsWith("_deleted/")) continue; // encrypted-trash blobs aren't locked placeholders
|
||||
try { if (await this.app.vault.adapter.exists(e.blob)) reg.push(e); }
|
||||
catch { reg.push(e); } // unknown → keep (never wipe on an I/O hiccup)
|
||||
}
|
||||
let changed = reg.length !== orig.length;
|
||||
const have = new Set(reg.map((e) => e.blob));
|
||||
let changed = reg.length !== (this.settings.lockedSubtrees ?? []).length;
|
||||
for (const f of blobs) {
|
||||
if (have.has(f.path)) continue;
|
||||
// ADD entries for `.stashenc` blobs with no registry entry (synced from
|
||||
// another device). Skip the `_deleted/` encrypted-trash store.
|
||||
for (const f of this.app.vault.getFiles()) {
|
||||
if (f.extension !== "stashenc" || have.has(f.path)) continue;
|
||||
const folder = f.parent?.path?.replace(/\/+$/, "") ?? "";
|
||||
if (folder === "_deleted" || folder.startsWith("_deleted/")) continue;
|
||||
const m = await readLockedMeta(this.app, f.path);
|
||||
if (!m) continue; // no sidecar → the scan still shows it at root (never stranded)
|
||||
const folder = f.parent?.path?.replace(/\/+$/, "") ?? "";
|
||||
reg = [...reg, { folder, blob: f.path, parentId: m.parentId, title: m.title, count: m.count, created: m.created, rootId: m.rootId, prevSibling: m.prevSibling }];
|
||||
reg.push({ folder, blob: f.path, parentId: m.parentId, title: m.title, count: m.count, created: m.created, rootId: m.rootId, prevSibling: m.prevSibling });
|
||||
changed = true;
|
||||
}
|
||||
if (changed) { this.settings.lockedSubtrees = reg; await this.saveSettings(); }
|
||||
if (changed) {
|
||||
this.settings.lockedSubtrees = reg;
|
||||
await this.saveSettings();
|
||||
this.refreshAllStashpadViews?.(); // repaint placeholders that were added/dropped
|
||||
}
|
||||
}
|
||||
|
||||
/** Locked-subtree placeholders attached under `parentId` in `folder` (for the
|
||||
|
|
@ -2904,16 +2927,28 @@ export default class StashpadPlugin extends Plugin {
|
|||
* with its filename as the title, so it's never stranded/unreachable. */
|
||||
lockedSubtreesFor(folder: string, parentId: StashpadId): Array<{ blob: string; title: string; count: number; created: string; rootId?: StashpadId; parentId?: StashpadId | null; prevSibling?: StashpadId | null }> {
|
||||
const cleaned = folder.replace(/\/+$/, "");
|
||||
const byBlob = new Map((this.settings.lockedSubtrees ?? []).map((e) => [e.blob, e]));
|
||||
const out: Array<{ blob: string; title: string; count: number; created: string; rootId?: StashpadId; parentId?: StashpadId | null; prevSibling?: StashpadId | null }> = [];
|
||||
const seen = new Set<string>();
|
||||
// REGISTRY FIRST — the registry (settings) loads synchronously at startup, so
|
||||
// locked placeholders render immediately on app restart, BEFORE the vault
|
||||
// finishes indexing the `.stashenc` blobs (vault.getFiles() lags there, which
|
||||
// is what made encrypted notes "disappear" on restart until 0.99.14).
|
||||
for (const e of this.settings.lockedSubtrees ?? []) {
|
||||
if ((e.folder ?? "").replace(/\/+$/, "") !== cleaned) continue;
|
||||
if ((e.parentId ?? ROOT_ID) !== parentId) continue;
|
||||
out.push({ blob: e.blob, title: e.title ?? "", count: e.count ?? 0, created: e.created ?? "", rootId: e.rootId, parentId: e.parentId ?? ROOT_ID, prevSibling: e.prevSibling ?? null });
|
||||
seen.add(e.blob);
|
||||
}
|
||||
// Then any `.stashenc` blob on disk with NO registry entry (e.g. synced in
|
||||
// from another device) — shown at ROOT with the filename as title so it's
|
||||
// never stranded. Skips the `_deleted/` encrypted-trash store.
|
||||
for (const f of this.app.vault.getFiles()) {
|
||||
if (f.extension !== "stashenc") continue;
|
||||
if ((f.parent?.path?.replace(/\/+$/, "") ?? "") !== cleaned) continue;
|
||||
const e = byBlob.get(f.path);
|
||||
if ((e?.parentId ?? ROOT_ID) !== parentId) continue;
|
||||
// Carry the positional fields so the placeholder + unlock restore the
|
||||
// reordered slot WITHOUT depending on the in-memory registry being intact.
|
||||
out.push({ blob: f.path, title: e?.title ?? f.basename, count: e?.count ?? 0, created: e?.created ?? "", rootId: e?.rootId, parentId: e?.parentId ?? ROOT_ID, prevSibling: e?.prevSibling ?? null });
|
||||
if (seen.has(f.path)) continue;
|
||||
const fdir = f.parent?.path?.replace(/\/+$/, "") ?? "";
|
||||
if (fdir !== cleaned || fdir === "_deleted" || fdir.startsWith("_deleted/")) continue;
|
||||
if (parentId !== ROOT_ID) continue; // unregistered → attach at root only
|
||||
out.push({ blob: f.path, title: f.basename, count: 0, created: "", rootId: undefined, parentId: ROOT_ID, prevSibling: null });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue