@hoardodile/host 0.1.1 → 0.1.3
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.
- package/dist/chunks/worker-entry.mjs +6 -1
- package/dist/hoard/index.d.ts +44 -42
- package/dist/hoard/index.js +73 -47
- package/dist/hoard/index.js.map +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.js +126 -23
- package/dist/index.js.map +1 -1
- package/dist/pack-6IscrwYN.d.ts +35 -0
- package/package.json +2 -2
- package/src/archive/archive-ops.test.ts +49 -1
- package/src/archive/extract.ts +15 -0
- package/src/archive/index.ts +1 -1
- package/src/archive/pack.ts +50 -0
- package/src/hoard/index.ts +4 -1
- package/src/hoard/paths.test.ts +1 -0
- package/src/hoard/paths.ts +46 -11
- package/src/hoard/versioned-folder-ops.ts +29 -6
- package/src/hooks.test.ts +70 -0
- package/src/hooks.ts +26 -0
- package/src/index.ts +1 -0
- package/src/install-api.ts +68 -0
- package/src/sandbox/host.ts +30 -1
- package/src/sandbox/worker-entry.mjs +6 -1
|
@@ -43,6 +43,7 @@ const HOOK_NAMES = [
|
|
|
43
43
|
"coverLocal",
|
|
44
44
|
"listFiles",
|
|
45
45
|
"imageHashes",
|
|
46
|
+
"onInstall",
|
|
46
47
|
]
|
|
47
48
|
|
|
48
49
|
const API_METHOD_NAMES = [
|
|
@@ -283,7 +284,11 @@ function send(message) {
|
|
|
283
284
|
function approxByteSize(value) {
|
|
284
285
|
if (value instanceof Uint8Array) return value.byteLength
|
|
285
286
|
try {
|
|
286
|
-
return
|
|
287
|
+
// `undefined` (hooks that legitimately return nothing — e.g.
|
|
288
|
+
// `onInstall`) stringifies to `undefined`, not a string: an empty
|
|
289
|
+
// payload must size 0, never throw into the Infinity branch.
|
|
290
|
+
const text = JSON.stringify(value)
|
|
291
|
+
return (typeof text === "string" ? text : "").length * 2
|
|
287
292
|
} catch {
|
|
288
293
|
return Infinity
|
|
289
294
|
}
|
package/dist/hoard/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
export { Z as ZipStreamEntry, s as streamStoredZip } from '../pack-6IscrwYN.js';
|
|
2
|
+
import 'node:stream';
|
|
3
|
+
|
|
4
|
+
type ContainerFormat = "zip" | "tar" | "7z" | "rar" | "xz" | "gzip";
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* Whole-archive extraction. Every supported format (zip, tar, 7z, rar,
|
|
@@ -33,6 +36,14 @@ type ExtractArchiveOptions = {
|
|
|
33
36
|
/** Optional entry-count budget (enforced on the listing). */
|
|
34
37
|
readonly maxEntries?: number;
|
|
35
38
|
readonly onProgress?: ZipExtractReporter;
|
|
39
|
+
/**
|
|
40
|
+
* Optional format allow-list (e.g. `["zip"]` for plugin installs).
|
|
41
|
+
* An archive sniffed outside the list is rejected up front with
|
|
42
|
+
* `resource.archive_format_not_allowed` — project-internal channels
|
|
43
|
+
* that only ever produce one format stop admitting the others
|
|
44
|
+
* instead of opening every codec to untrusted input.
|
|
45
|
+
*/
|
|
46
|
+
readonly formats?: readonly ContainerFormat[];
|
|
36
47
|
};
|
|
37
48
|
/**
|
|
38
49
|
* Stream an archive into `destDir`, format-agnostic: every supported
|
|
@@ -92,28 +103,6 @@ declare function listArchiveEntries(archivePath: string): Promise<readonly Archi
|
|
|
92
103
|
*/
|
|
93
104
|
declare function validateArchiveBudget(archivePath: string, maxBytes: number): Promise<void>;
|
|
94
105
|
|
|
95
|
-
/**
|
|
96
|
-
* On-the-fly zip exports. Resource sources are stored as bare files on
|
|
97
|
-
* disk — nothing is packed at commit time; this module streams STORED
|
|
98
|
-
* zip bytes straight to the consumer without a staging file.
|
|
99
|
-
*/
|
|
100
|
-
/**
|
|
101
|
-
* A logical zip entry whose bytes are produced on demand. Streams are
|
|
102
|
-
* read once, in order, when the output is consumed.
|
|
103
|
-
*/
|
|
104
|
-
type ZipStreamEntry = {
|
|
105
|
-
readonly name: string;
|
|
106
|
-
readonly size: number;
|
|
107
|
-
readonly openStream: () => Readable;
|
|
108
|
-
};
|
|
109
|
-
/**
|
|
110
|
-
* Stream a STORED zip from an ordered list of logical entries. Used by
|
|
111
|
-
* the HTTP layer for exports (resource source packs, bulk downloads)
|
|
112
|
-
* without a staging file on disk. Zero-length entries are packed as
|
|
113
|
-
* empty buffers.
|
|
114
|
-
*/
|
|
115
|
-
declare function streamStoredZip(entries: readonly ZipStreamEntry[]): NodeJS.ReadableStream;
|
|
116
|
-
|
|
117
106
|
/**
|
|
118
107
|
* Shared directory-size walk: the one implementation of "recursive byte
|
|
119
108
|
* size of a directory tree" for host storage. Consumers opt into
|
|
@@ -199,7 +188,8 @@ declare const RESOURCE_DATA_DIR_NAME = "data";
|
|
|
199
188
|
* per-version database snapshot), `db-backups/` (manual backups,
|
|
200
189
|
* only kept for the current version), `snapshots/` (automatic daily
|
|
201
190
|
* snapshots, only kept for the current version), `resources/<id>/`,
|
|
202
|
-
* `characters/<id>/`, `
|
|
191
|
+
* `characters/<id>/`, `tags/<id>/` (tag art — see {@link VersionPaths.tag}),
|
|
192
|
+
* `plugins/<id>/` (installed content plugins
|
|
203
193
|
* frozen with that version; the builtin `file` plugin is not stored
|
|
204
194
|
* here). Old versions are FROZEN: no writes ever land in
|
|
205
195
|
* `versions/<v>` once a `versions/<v+1>` exists.
|
|
@@ -256,10 +246,18 @@ type VersionPaths = {
|
|
|
256
246
|
resources(): string;
|
|
257
247
|
/** Root folder of all characters in this version: `<root>/versions/<v>/characters`. */
|
|
258
248
|
characters(): string;
|
|
249
|
+
/** Root folder of all tags in this version: `<root>/versions/<v>/tags`. */
|
|
250
|
+
tags(): string;
|
|
259
251
|
/** Root folder of all documents in this version: `<root>/versions/<v>/documents`. */
|
|
260
252
|
documents(): string;
|
|
261
253
|
/** Root folder of a character: `<root>/versions/<v>/characters/<id>`. */
|
|
262
254
|
character(id: string): string;
|
|
255
|
+
/**
|
|
256
|
+
* Root folder of a tag: `<root>/versions/<v>/tags/<id>`. Holds the
|
|
257
|
+
* tag's single image slot (`image.<ext>`), the same convention as
|
|
258
|
+
* character avatar/fullbody slots.
|
|
259
|
+
*/
|
|
260
|
+
tag(id: string): string;
|
|
263
261
|
/** Root of manual backups: `<root>/versions/<v>/db-backups`. */
|
|
264
262
|
dbBackups(): string;
|
|
265
263
|
/** Path to one manual backup: `<root>/versions/<v>/db-backups/<name>`. */
|
|
@@ -274,7 +272,7 @@ type VersionPaths = {
|
|
|
274
272
|
* delete cannot remove a folder whose files live under frozen past
|
|
275
273
|
* archives.
|
|
276
274
|
*/
|
|
277
|
-
deletedMarker(kind: "resources" | "characters", id: string): string;
|
|
275
|
+
deletedMarker(kind: "resources" | "characters" | "tags", id: string): string;
|
|
278
276
|
/** Root folder of a document: `<root>/versions/<v>/documents/<id>`. */
|
|
279
277
|
document(id: string): string;
|
|
280
278
|
/**
|
|
@@ -301,11 +299,11 @@ type LocalPaths = {
|
|
|
301
299
|
logs(): string;
|
|
302
300
|
/**
|
|
303
301
|
* Path to a local derived cover/thumb variant:
|
|
304
|
-
* `<localRoot>/cache/<resources|characters>/<id>/<variant>.<format>`.
|
|
302
|
+
* `<localRoot>/cache/<resources|characters|tags>/<id>/<variant>.<format>`.
|
|
305
303
|
* Holds synthesized covers (resource covers, character avatars and
|
|
306
|
-
* fullbody images); re-rendered when cleared.
|
|
304
|
+
* fullbody images, tag art); re-rendered when cleared.
|
|
307
305
|
*/
|
|
308
|
-
localCover(subjectKind: "resource" | "character", id: string, variant: string, format?: string): string;
|
|
306
|
+
localCover(subjectKind: "resource" | "character" | "tag", id: string, variant: string, format?: string): string;
|
|
309
307
|
/**
|
|
310
308
|
* Per-file derived image variant:
|
|
311
309
|
* `<localRoot>/cache/resources/<id>/file-preview/<sourceCacheId>__<variantKey>.<format>`.
|
|
@@ -331,6 +329,13 @@ type LocalPaths = {
|
|
|
331
329
|
* (b) thumbnail variants (`avatar.webp`, `fullbody.webp`).
|
|
332
330
|
*/
|
|
333
331
|
character(id: string): string;
|
|
332
|
+
/**
|
|
333
|
+
* Root of the local per-tag directory:
|
|
334
|
+
* `<localRoot>/cache/tags/<id>`.
|
|
335
|
+
* Holds (a) versioned copies of replaced tag art and (b) the tag
|
|
336
|
+
* thumbnail variant (`image.avif`).
|
|
337
|
+
*/
|
|
338
|
+
tag(id: string): string;
|
|
334
339
|
/** Root of the trash: `<localRoot>/trash`. */
|
|
335
340
|
trash(): string;
|
|
336
341
|
/** Path to a single trashed item: `<localRoot>/trash/<id>`. */
|
|
@@ -351,6 +356,14 @@ type LocalPaths = {
|
|
|
351
356
|
* `local/` (never synced) so each host has its own seal key.
|
|
352
357
|
*/
|
|
353
358
|
sessionKey(): string;
|
|
359
|
+
/**
|
|
360
|
+
* Path to the seed-removal marker file: `<root>/local/seed-removals.json`.
|
|
361
|
+
* Holds the plugin ids (UUIDs) whose bundled seed was deliberately
|
|
362
|
+
* uninstalled by this host, so boot-time seeding skips them until the
|
|
363
|
+
* user restores them. Host-only state — never synced, survives app
|
|
364
|
+
* updates, and stays out of the wipe-on-clear `cache/` tree.
|
|
365
|
+
*/
|
|
366
|
+
seedRemovals(): string;
|
|
354
367
|
/**
|
|
355
368
|
* Root of the host-only temporary directory tree:
|
|
356
369
|
* `<localRoot>/.tmp`. Holds the global staging pool
|
|
@@ -680,6 +693,7 @@ declare function versionedDbFile(root: string, v: number): string;
|
|
|
680
693
|
*/
|
|
681
694
|
declare function versionedPath(root: string, v: number): string;
|
|
682
695
|
|
|
696
|
+
type VersionedFolderSubjectKind = "resource" | "character" | "tag";
|
|
683
697
|
type VersionedFolderOps = {
|
|
684
698
|
/** Ensure the current-version entity folder exists. */
|
|
685
699
|
ensureFolder(id: string): Promise<void>;
|
|
@@ -699,21 +713,9 @@ type VersionedFolderOps = {
|
|
|
699
713
|
*/
|
|
700
714
|
moveFolderToTrash(id: string): Promise<string>;
|
|
701
715
|
};
|
|
702
|
-
/**
|
|
703
|
-
* The four lifecycle operations shared by the resource and character
|
|
704
|
-
* file-system layers. They differ only in which versioned folder they
|
|
705
|
-
* target and how the trash / placeholder names are derived.
|
|
706
|
-
*
|
|
707
|
-
* `moveFolderToTrash` treats `EPERM`/`EBUSY`/`UNKNOWN` as transient Windows
|
|
708
|
-
* locks (a file inside the folder is still open) and leaves the source in
|
|
709
|
-
* place; a boot-time orphan sweep reclaims it later.
|
|
710
|
-
*
|
|
711
|
-
* `readOnly` is a live `{ current: boolean }` ref (the server's runtime
|
|
712
|
-
* read-only flag), so a version switch mid-request re-reads it.
|
|
713
|
-
*/
|
|
714
716
|
declare function buildVersionedFolderOps(paths: StoragePaths, readOnly: {
|
|
715
717
|
readonly current: boolean;
|
|
716
|
-
}, kind:
|
|
718
|
+
}, kind: VersionedFolderSubjectKind): VersionedFolderOps;
|
|
717
719
|
/**
|
|
718
720
|
* Move every file in `sourceFolder` whose name matches `match` into
|
|
719
721
|
* `destFolder` under a timestamped archive name (`<prefix><stamp><ext>`, or
|
|
@@ -751,4 +753,4 @@ type VersionedWriteCommand<T> = (paths: StoragePaths["latest"]) => T | Promise<T
|
|
|
751
753
|
*/
|
|
752
754
|
declare function writeVersioned<T>(paths: StoragePaths, readOnly: boolean, cmd: VersionedWriteCommand<T>): Promise<T>;
|
|
753
755
|
|
|
754
|
-
export { type CreateNextVersionResult, type CreateStoragePathsOptions, type DirSizeOptions, type LocalPaths, ORDER_MANIFEST_NAME, type OccupiedNames, PluginVaultPathError, RESOURCE_DATA_DIR_NAME, type StoragePaths, type VaultCommitResult, type VersionPaths, type VersionedFolderOps, type
|
|
756
|
+
export { type CreateNextVersionResult, type CreateStoragePathsOptions, type DirSizeOptions, type LocalPaths, ORDER_MANIFEST_NAME, type OccupiedNames, PluginVaultPathError, RESOURCE_DATA_DIR_NAME, type StoragePaths, type VaultCommitResult, type VersionPaths, type VersionedFolderOps, type VersionedFolderSubjectKind, type VersionedWriteCommand, archiveStaleFiles, assertInside, assertSafeSegment, buildVersionedFolderOps, commitVaultFile, createNextVersion, createOccupiedNames, createStoragePaths, currentVersion, discardVaultTempFile, ensureBootstrapVersion, extractArchiveInto, findStagedArchiveFile, findStagedPoolFile, imageVariantKey, listArchiveEntries, listVersions, naturalSort, occupyEntryName, orderEntries, orderManifestPath, parseOrderManifest, parsePluginVaultDest, readActiveVersion, readOrderManifest, removeStagedPoolFile, resolveStagedPoolFiles, sanitizeEntryName, sumDirSizes, uniqueEntryName, validateArchiveBudget, vaultFileSha256, vaultReadFile, vaultRemoveFile, vaultStatFile, vaultTempFile, vaultTotalSize, versionedDbFile, versionedPath, writeActiveVersion, writeOrderManifest, writeStagedArchiveFile, writeStagedPoolFile, writeVersioned };
|
package/dist/hoard/index.js
CHANGED
|
@@ -573,16 +573,16 @@ function parseSltListing(stdout, legacyNames) {
|
|
|
573
573
|
}
|
|
574
574
|
continue;
|
|
575
575
|
}
|
|
576
|
-
const
|
|
577
|
-
if (
|
|
578
|
-
const key = line.slice(0,
|
|
576
|
+
const sep4 = line.indexOf(" = ");
|
|
577
|
+
if (sep4 < 0) continue;
|
|
578
|
+
const key = line.slice(0, sep4);
|
|
579
579
|
if (key === "Path") {
|
|
580
|
-
const raw = Buffer.from(line.slice(
|
|
580
|
+
const raw = Buffer.from(line.slice(sep4 + 3), "latin1");
|
|
581
581
|
const decoded = legacyNames ? decodeLegacyZipName(raw) : raw.toString("utf8");
|
|
582
582
|
name = decoded.replace(/\\/g, "/");
|
|
583
583
|
continue;
|
|
584
584
|
}
|
|
585
|
-
const value = line.slice(
|
|
585
|
+
const value = line.slice(sep4 + 3);
|
|
586
586
|
switch (key) {
|
|
587
587
|
case "Size":
|
|
588
588
|
sizeBytes = Number.parseInt(value, 10) || 0;
|
|
@@ -694,6 +694,13 @@ async function extractArchiveInto(source, destDir, opts) {
|
|
|
694
694
|
{}
|
|
695
695
|
);
|
|
696
696
|
}
|
|
697
|
+
if (opts.formats !== void 0 && !opts.formats.includes(format)) {
|
|
698
|
+
throw invalid(
|
|
699
|
+
"resource.archive_format_not_allowed",
|
|
700
|
+
`archive format ${format} is not allowed here (allowed: ${opts.formats.join(", ")})`,
|
|
701
|
+
{ allowed: opts.formats }
|
|
702
|
+
);
|
|
703
|
+
}
|
|
697
704
|
if (format === "zip" && resolveSevenZipPath() === void 0) {
|
|
698
705
|
return extractZipBuffer(buffer2, destDir, opts);
|
|
699
706
|
}
|
|
@@ -1359,65 +1366,69 @@ function createStoragePaths(opts) {
|
|
|
1359
1366
|
}
|
|
1360
1367
|
function versionAt(version) {
|
|
1361
1368
|
const vSeg = assertSafeSegment(String(version));
|
|
1362
|
-
const vRoot =
|
|
1369
|
+
const vRoot = join6(versionsRootPath, vSeg);
|
|
1363
1370
|
return {
|
|
1364
1371
|
root: vRoot,
|
|
1365
1372
|
version,
|
|
1366
|
-
versionSnapshotDb: () =>
|
|
1367
|
-
resource: (id) =>
|
|
1368
|
-
resourceData: (id) =>
|
|
1369
|
-
resources: () =>
|
|
1370
|
-
characters: () =>
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1373
|
+
versionSnapshotDb: () => join6(vRoot, "app.sqlite"),
|
|
1374
|
+
resource: (id) => join6(vRoot, "resources", assertSafeSegment(id)),
|
|
1375
|
+
resourceData: (id) => join6(vRoot, "resources", assertSafeSegment(id), RESOURCE_DATA_DIR_NAME),
|
|
1376
|
+
resources: () => join6(vRoot, "resources"),
|
|
1377
|
+
characters: () => join6(vRoot, "characters"),
|
|
1378
|
+
tags: () => join6(vRoot, "tags"),
|
|
1379
|
+
documents: () => join6(vRoot, "documents"),
|
|
1380
|
+
character: (id) => join6(vRoot, "characters", assertSafeSegment(id)),
|
|
1381
|
+
tag: (id) => join6(vRoot, "tags", assertSafeSegment(id)),
|
|
1382
|
+
dbBackups: () => join6(vRoot, "db-backups"),
|
|
1383
|
+
dbBackup: (name) => join6(vRoot, "db-backups", assertSafeSegment(name)),
|
|
1384
|
+
snapshots: () => join6(vRoot, "snapshots"),
|
|
1385
|
+
snapshot: (name) => join6(vRoot, "snapshots", assertSafeSegment(name)),
|
|
1386
|
+
deletedMarker: (kind, id) => join6(vRoot, kind, assertSafeSegment(id), ".deleted"),
|
|
1387
|
+
document: (id) => join6(vRoot, "documents", assertSafeSegment(id)),
|
|
1388
|
+
plugins: () => join6(vRoot, "plugins"),
|
|
1389
|
+
pluginVaultDir: (id) => join6(vRoot, "plugins", assertSafeSegment(id), "vault")
|
|
1381
1390
|
};
|
|
1382
1391
|
}
|
|
1383
1392
|
const active = versionAt(activeVersion);
|
|
1384
1393
|
const latest = versionAt(latestVersion);
|
|
1385
|
-
const uploadStagingRootPath =
|
|
1386
|
-
const cacheRoot =
|
|
1394
|
+
const uploadStagingRootPath = join6(localRoot, ".tmp");
|
|
1395
|
+
const cacheRoot = join6(localRoot, "cache");
|
|
1387
1396
|
const local = {
|
|
1388
1397
|
root: localRoot,
|
|
1389
1398
|
cache: () => cacheRoot,
|
|
1390
|
-
logs: () =>
|
|
1391
|
-
localCover: (subjectKind, id, variant, format) =>
|
|
1399
|
+
logs: () => join6(localRoot, "logs"),
|
|
1400
|
+
localCover: (subjectKind, id, variant, format) => join6(
|
|
1392
1401
|
cacheRoot,
|
|
1393
1402
|
localCoverSubjectDir(subjectKind),
|
|
1394
1403
|
assertSafeSegment(id),
|
|
1395
1404
|
`${assertSafeSegment(variant)}.${format ?? "avif"}`
|
|
1396
1405
|
),
|
|
1397
|
-
resFileVariant: (id, filename, variantKey, format) =>
|
|
1406
|
+
resFileVariant: (id, filename, variantKey, format) => join6(
|
|
1398
1407
|
cacheRoot,
|
|
1399
1408
|
"resources",
|
|
1400
1409
|
assertSafeSegment(id),
|
|
1401
1410
|
"file-preview",
|
|
1402
1411
|
`${assertSafeSegment(sourceCacheId(filename))}__${assertSafeSegment(variantKey)}.${format ?? "avif"}`
|
|
1403
1412
|
),
|
|
1404
|
-
resFilePreviewDir: (id) =>
|
|
1405
|
-
resFilesCache: (id) =>
|
|
1406
|
-
resource: (id) =>
|
|
1407
|
-
character: (id) =>
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
+
resFilePreviewDir: (id) => join6(cacheRoot, "resources", assertSafeSegment(id), "file-preview"),
|
|
1414
|
+
resFilesCache: (id) => join6(cacheRoot, "resources", assertSafeSegment(id), "files-cache.json"),
|
|
1415
|
+
resource: (id) => join6(cacheRoot, "resources", assertSafeSegment(id)),
|
|
1416
|
+
character: (id) => join6(cacheRoot, "characters", assertSafeSegment(id)),
|
|
1417
|
+
tag: (id) => join6(cacheRoot, "tags", assertSafeSegment(id)),
|
|
1418
|
+
trash: () => join6(localRoot, "trash"),
|
|
1419
|
+
trashItem: (id) => join6(localRoot, "trash", assertSafeSegment(id)),
|
|
1420
|
+
tmp: () => join6(cacheRoot, "tmp"),
|
|
1421
|
+
tmpFile: (name) => join6(cacheRoot, "tmp", assertSafeSegment(name)),
|
|
1422
|
+
sessionKey: () => join6(localRoot, ".session-key"),
|
|
1423
|
+
seedRemovals: () => join6(localRoot, "seed-removals.json"),
|
|
1413
1424
|
uploadStagingRoot: () => uploadStagingRootPath,
|
|
1414
|
-
stagingPoolRoot: () =>
|
|
1415
|
-
stagingPoolFile: (fileId, ext) =>
|
|
1425
|
+
stagingPoolRoot: () => join6(uploadStagingRootPath, "staging"),
|
|
1426
|
+
stagingPoolFile: (fileId, ext) => join6(
|
|
1416
1427
|
uploadStagingRootPath,
|
|
1417
1428
|
"staging",
|
|
1418
1429
|
`${assertSafeSegment(fileId)}${ext}`
|
|
1419
1430
|
),
|
|
1420
|
-
resVideoFrame: (id, filename, timeMs) =>
|
|
1431
|
+
resVideoFrame: (id, filename, timeMs) => join6(
|
|
1421
1432
|
cacheRoot,
|
|
1422
1433
|
"resources",
|
|
1423
1434
|
assertSafeSegment(id),
|
|
@@ -1425,14 +1436,14 @@ function createStoragePaths(opts) {
|
|
|
1425
1436
|
assertSafeSegment(sourceCacheId(filename)),
|
|
1426
1437
|
`${timeMs}.avif`
|
|
1427
1438
|
),
|
|
1428
|
-
resExtractedDir: (id, fileVersion) =>
|
|
1439
|
+
resExtractedDir: (id, fileVersion) => join6(
|
|
1429
1440
|
cacheRoot,
|
|
1430
1441
|
"resources",
|
|
1431
1442
|
assertSafeSegment(id),
|
|
1432
1443
|
"extracted",
|
|
1433
1444
|
`v${fileVersion}`
|
|
1434
1445
|
),
|
|
1435
|
-
resExtractedEntry: (id, fileVersion, entryName) =>
|
|
1446
|
+
resExtractedEntry: (id, fileVersion, entryName) => join6(
|
|
1436
1447
|
cacheRoot,
|
|
1437
1448
|
"resources",
|
|
1438
1449
|
assertSafeSegment(id),
|
|
@@ -1440,7 +1451,7 @@ function createStoragePaths(opts) {
|
|
|
1440
1451
|
`v${fileVersion}`,
|
|
1441
1452
|
assertSafeSegment(sourceCacheId(entryName))
|
|
1442
1453
|
),
|
|
1443
|
-
resExtractedArchivesDir: (id, fileVersion) =>
|
|
1454
|
+
resExtractedArchivesDir: (id, fileVersion) => join6(
|
|
1444
1455
|
cacheRoot,
|
|
1445
1456
|
"resources",
|
|
1446
1457
|
assertSafeSegment(id),
|
|
@@ -1457,11 +1468,11 @@ function createStoragePaths(opts) {
|
|
|
1457
1468
|
latest,
|
|
1458
1469
|
local,
|
|
1459
1470
|
atVersion: (v) => versionAt(v),
|
|
1460
|
-
runtimeDb: () =>
|
|
1471
|
+
runtimeDb: () => join6(root, "app.sqlite")
|
|
1461
1472
|
};
|
|
1462
1473
|
}
|
|
1463
1474
|
function localCoverSubjectDir(subjectKind) {
|
|
1464
|
-
return subjectKind === "resource" ? "resources" : "characters";
|
|
1475
|
+
return subjectKind === "resource" ? "resources" : subjectKind === "character" ? "characters" : "tags";
|
|
1465
1476
|
}
|
|
1466
1477
|
function toCacheBasename(filename) {
|
|
1467
1478
|
const dot = filename.lastIndexOf(".");
|
|
@@ -1501,7 +1512,7 @@ function assertSafeSegment(segment) {
|
|
|
1501
1512
|
}
|
|
1502
1513
|
return segment;
|
|
1503
1514
|
}
|
|
1504
|
-
function
|
|
1515
|
+
function join6(...segments) {
|
|
1505
1516
|
return resolve(...segments);
|
|
1506
1517
|
}
|
|
1507
1518
|
function assertInside(ancestor, candidate) {
|
|
@@ -1850,10 +1861,25 @@ async function writeVersioned(paths, readOnly, cmd) {
|
|
|
1850
1861
|
}
|
|
1851
1862
|
|
|
1852
1863
|
// src/hoard/versioned-folder-ops.ts
|
|
1864
|
+
var KIND_LAYOUT = {
|
|
1865
|
+
resource: { trashPrefix: "resources-", deletedKind: "resources" },
|
|
1866
|
+
character: { trashPrefix: "characters-", deletedKind: "characters" },
|
|
1867
|
+
tag: { trashPrefix: "tags-", deletedKind: "tags" }
|
|
1868
|
+
};
|
|
1853
1869
|
function buildVersionedFolderOps(paths, readOnly, kind) {
|
|
1854
|
-
const folderOf = (id) => (current) =>
|
|
1855
|
-
|
|
1856
|
-
|
|
1870
|
+
const folderOf = (id) => (current) => {
|
|
1871
|
+
switch (kind) {
|
|
1872
|
+
case "resource":
|
|
1873
|
+
return current.resource(id);
|
|
1874
|
+
case "character":
|
|
1875
|
+
return current.character(id);
|
|
1876
|
+
case "tag":
|
|
1877
|
+
return current.tag(id);
|
|
1878
|
+
}
|
|
1879
|
+
};
|
|
1880
|
+
const layout = KIND_LAYOUT[kind];
|
|
1881
|
+
const trashPrefix = layout.trashPrefix;
|
|
1882
|
+
const deletedKind = layout.deletedKind;
|
|
1857
1883
|
async function ensureFolder(id) {
|
|
1858
1884
|
await writeVersioned(
|
|
1859
1885
|
paths,
|