@wizzlethorpe/vaults 0.13.2 → 0.13.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -131,13 +131,19 @@ async function syncImages(host, vault, manifestFiles) {
131
131
  }
132
132
  const lastImageManifest = host.getVaultState(vault.id).lastImageManifest;
133
133
  const last = new Map(Object.entries(lastImageManifest || {}));
134
+ const baseDir = vaultCacheDir(vault.id);
135
+ const present = await listCached(baseDir, last.keys());
136
+ const vanished = [...last.keys()].filter((p) => !present.has(p));
137
+ for (const path of vanished) last.delete(path);
138
+ if (vanished.length > 0) {
139
+ console.info(`Vaults | ${vanished.length} cached file(s) are gone from ${baseDir}; re-fetching.`);
140
+ }
134
141
  const toDownload = [];
135
142
  for (const [path, hash] of remoteImages) {
136
143
  if (last.get(path) !== hash) toDownload.push(path);
137
144
  }
138
- const toDelete = [...last.keys()].filter((p) => !remoteImages.has(p));
145
+ let toDelete = [...last.keys()].filter((p) => !remoteImages.has(p));
139
146
  if (toDownload.length === 0 && toDelete.length === 0) return { downloaded: 0, removed: 0, errors: 0 };
140
- const baseDir = vaultCacheDir(vault.id);
141
147
  const worldRoot = `worlds/${game.world.id}`;
142
148
  const dirsNeeded = /* @__PURE__ */ new Set();
143
149
  const addChain = (fullPath) => {
@@ -155,6 +161,7 @@ async function syncImages(host, vault, manifestFiles) {
155
161
  if (segs.length > 0) addChain(`${baseDir}/${segs.join("/")}`);
156
162
  }
157
163
  await ensureDirs([...dirsNeeded]);
164
+ await writeCacheMarker(baseDir, vault);
158
165
  const sizeOf = /* @__PURE__ */ new Map();
159
166
  for (const f of manifestFiles) sizeOf.set(f.path, f.size ?? 0);
160
167
  const chunks = [];
@@ -203,6 +210,10 @@ async function syncImages(host, vault, manifestFiles) {
203
210
  console.warn(`Vaults | ${errors.length} image(s) failed to download:`, errors);
204
211
  }
205
212
  let removed = 0;
213
+ if (toDelete.length > 0 && !canDelete()) {
214
+ console.info(`Vaults | ${toDelete.length} cached image(s) are no longer in the vault. Foundry provides no API to delete files, so they stay in ${baseDir} until removed by hand.`);
215
+ toDelete = [];
216
+ }
206
217
  for (const path of toDelete) {
207
218
  try {
208
219
  await deleteFromWorld(baseDir, path);
@@ -302,6 +313,47 @@ async function ensureDirs(paths) {
302
313
  }
303
314
  }
304
315
  }
316
+ async function writeCacheMarker(baseDir, vault) {
317
+ const body = JSON.stringify({
318
+ label: vault.label ?? null,
319
+ url: vault.url ?? null,
320
+ vaultId: vault.id,
321
+ note: "Cache for one vault. Safe to delete entirely; the next sync re-downloads it."
322
+ }, null, 2);
323
+ try {
324
+ await uploadToWorld(baseDir, "vault-info.json", new Blob([body], { type: "application/json" }));
325
+ } catch (err) {
326
+ console.debug(`Vaults | could not write cache marker in ${baseDir}:`, err?.message || err);
327
+ }
328
+ }
329
+ async function listCached(baseDir, paths) {
330
+ const byDir = /* @__PURE__ */ new Map();
331
+ for (const p of paths) {
332
+ const segs = p.split("/");
333
+ segs.pop();
334
+ const dir = segs.join("/");
335
+ if (!byDir.has(dir)) byDir.set(dir, []);
336
+ byDir.get(dir).push(p);
337
+ }
338
+ const present = /* @__PURE__ */ new Set();
339
+ for (const dir of byDir.keys()) {
340
+ const full = dir ? `${baseDir}/${dir}` : baseDir;
341
+ let listing;
342
+ try {
343
+ listing = await fp().browse("data", full);
344
+ } catch {
345
+ continue;
346
+ }
347
+ for (const file of listing?.files ?? []) {
348
+ const name = decodeURIComponent(String(file).split("/").pop());
349
+ present.add(dir ? `${dir}/${name}` : name);
350
+ }
351
+ }
352
+ return present;
353
+ }
354
+ function canDelete() {
355
+ return typeof fp().deleteFile === "function";
356
+ }
305
357
  async function deleteFromWorld(baseDir, path) {
306
358
  const full = `${baseDir}/${path}`;
307
359
  const impl = fp();
@@ -364,7 +416,9 @@ function buildPathIndex(manifestFiles) {
364
416
  docTargets.set(mdPath, docName);
365
417
  }
366
418
  }
367
- return { paths, idOverrides, docTargets };
419
+ const hashes = /* @__PURE__ */ new Map();
420
+ for (const f of manifestFiles) if (f.hash) hashes.set(f.path, f.hash);
421
+ return { paths, idOverrides, docTargets, hashes };
368
422
  }
369
423
  function docNameFromBase(base) {
370
424
  if (typeof base !== "string" || !base) return null;
@@ -398,9 +452,9 @@ function decodeHtmlEntities(s) {
398
452
  ta.innerHTML = s;
399
453
  return ta.value;
400
454
  }
401
- async function transformHtmlForFoundry(vault, html, index) {
455
+ async function transformHtmlForFoundry(vault, html, index, mediaRefs) {
402
456
  html = await rewriteWikilinks(vault.id, html, index);
403
- html = rewriteMediaSrcs(vault.id, html);
457
+ html = rewriteMediaSrcs(vault.id, html, index?.hashes, mediaRefs);
404
458
  html = rewritePassthroughLinks(vault.id, html);
405
459
  html = await applyDomTransforms(html, vault, index);
406
460
  return html;
@@ -537,14 +591,20 @@ async function rewriteWikilinks(vaultId, html, index) {
537
591
  }
538
592
  return html;
539
593
  }
540
- function rewriteMediaSrcs(vaultId, html) {
594
+ function rewriteMediaSrcs(vaultId, html, hashes, mediaRefs) {
541
595
  return html.replace(MEDIA_SRC_RE, (full, tag, before, src, after) => {
542
596
  if (!src.startsWith("/")) return full;
543
597
  const path = decodeURIComponent(src.replace(/^\//, ""));
544
598
  if (!CACHED_EXT_RE.test(path)) return full;
545
- return `<${tag}${before}src="${escapeAttr(localFileUrl(vaultId, path))}"${after}>`;
599
+ mediaRefs?.add(path);
600
+ const url2 = localFileUrl(vaultId, path) + mediaVersion(hashes, path);
601
+ return `<${tag}${before}src="${escapeAttr(url2)}"${after}>`;
546
602
  });
547
603
  }
604
+ function mediaVersion(hashes, path) {
605
+ const hash = hashes?.get(path);
606
+ return hash ? `?v=${encodeURIComponent(String(hash).slice(0, 12))}` : "";
607
+ }
548
608
  function rewritePassthroughLinks(vaultId, html) {
549
609
  return html.replace(ANCHOR_RE, (full, attrs, inner) => {
550
610
  const cls = ATTR_CLASS_RE.exec(attrs)?.[1] || "";
@@ -629,8 +689,8 @@ function buildFolderInfo(mdPaths) {
629
689
  function isIndexFile(filename) {
630
690
  return filename.replace(/\.md$/i, "") === INDEX_BASENAME;
631
691
  }
632
- async function upsertFile(vault, path, body, index, meta, folderInfo) {
633
- let html = await transformHtmlForFoundry(vault, body, index);
692
+ async function upsertFile(vault, path, body, index, meta, folderInfo, mediaRefs) {
693
+ let html = await transformHtmlForFoundry(vault, body, index, mediaRefs);
634
694
  html = await appendInstanceDocLink(html, vault, path, meta);
635
695
  const segs = path.split("/");
636
696
  const filename = segs.pop();
@@ -1177,7 +1237,10 @@ async function sync(host, vault, { forceFull = false } = {}) {
1177
1237
  for (const f of files) {
1178
1238
  if (f.meta && f.path.endsWith(".body.html")) bodyMetaIndex.set(f.path, f.meta);
1179
1239
  }
1180
- const toUpsert = bodyPaths.filter((p) => remote.get(p) !== local.get(p));
1240
+ const prevImages = forceFull ? /* @__PURE__ */ new Map() : new Map(Object.entries(lastSync.lastImageManifest || {}));
1241
+ const lastMediaRefs = lastSync.lastMediaRefs || {};
1242
+ const mediaStale = (bodyPath) => (lastMediaRefs[bodyPath] || []).some((m) => remote.get(m) !== prevImages.get(m));
1243
+ const toUpsert = bodyPaths.filter((p) => remote.get(p) !== local.get(p) || mediaStale(p));
1181
1244
  const toDelete = [...local.keys()].filter((p) => p.endsWith(".body.html") && !remote.has(p));
1182
1245
  if (forceFull) await host.setVaultState(vault.id, { lastImageManifest: {} });
1183
1246
  let imageStats = { downloaded: 0, removed: 0, errors: 0 };
@@ -1215,6 +1278,8 @@ async function sync(host, vault, { forceFull = false } = {}) {
1215
1278
  return { ok: false, refreshHandlerAssets: false };
1216
1279
  }
1217
1280
  let added = 0, modified = 0, instances = 0;
1281
+ const mediaRefs = {};
1282
+ for (const p of bodyPaths) if (lastMediaRefs[p]) mediaRefs[p] = lastMediaRefs[p];
1218
1283
  for (const bodyPath of toUpsert) {
1219
1284
  const html = bodies.get(bodyPath);
1220
1285
  if (html == null) {
@@ -1227,7 +1292,9 @@ async function sync(host, vault, { forceFull = false } = {}) {
1227
1292
  if (pageMeta?.foundry?.journal === false) {
1228
1293
  await deleteFile(vault, logicalPath);
1229
1294
  } else {
1230
- const result = await upsertFile(vault, logicalPath, html, pathIndex, pageMeta, folderInfo);
1295
+ const refs = /* @__PURE__ */ new Set();
1296
+ const result = await upsertFile(vault, logicalPath, html, pathIndex, pageMeta, folderInfo, refs);
1297
+ mediaRefs[bodyPath] = [...refs];
1231
1298
  if (result === "added") added++;
1232
1299
  else modified++;
1233
1300
  }
@@ -1258,7 +1325,7 @@ async function sync(host, vault, { forceFull = false } = {}) {
1258
1325
  console.warn(`Vaults | delete instance failed for ${logicalPath}:`, err);
1259
1326
  }
1260
1327
  }
1261
- await host.setVaultState(vault.id, { lastManifest: Object.fromEntries(remote) });
1328
+ await host.setVaultState(vault.id, { lastManifest: Object.fromEntries(remote), lastMediaRefs: mediaRefs });
1262
1329
  await reconcileEntryPlacement(vault, folderInfo);
1263
1330
  await reconcileOwnership(vault, bodyMetaIndex);
1264
1331
  const seconds = ((Date.now() - start) / 1e3).toFixed(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wizzlethorpe/vaults",
3
- "version": "0.13.2",
3
+ "version": "0.13.5",
4
4
  "description": "Sync an Obsidian vault to a Cloudflare-hosted wiki with role-based access. Renders locally, deploys to Cloudflare Pages.",
5
5
  "keywords": [
6
6
  "obsidian",