@storyshelf/storage-local 0.1.0

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/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @storyshelf/storage-local
2
+
3
+ The default storage adapter for self-hosted StoryShelf: reads and writes blobs to the local filesystem under a data directory. One `docker run` to self-host.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ nub add @storyshelf/storage-local
9
+ ```
10
+
11
+ or
12
+
13
+ ```sh
14
+ npm install @storyshelf/storage-local
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```ts
20
+ import { createLocalStorage } from "@storyshelf/storage-local";
21
+ import { createShelfRouter } from "@storyshelf/core";
22
+ import { createSqliteDatabase } from "@storyshelf/db-sqlite";
23
+
24
+ const storage = createLocalStorage("./data");
25
+ const database = createSqliteDatabase("./data/shelf.db");
26
+
27
+ const app = createShelfRouter({ database, storage });
28
+ ```
29
+
30
+ ## API
31
+
32
+ ### `createLocalStorage(dataDir: string): StorageAdapter`
33
+
34
+ Stores all blobs under the resolved `dataDir` (creating directories as needed). Paths are confined to the data directory — any path escaping it throws an error.
35
+
36
+ The returned adapter implements every method of the `StorageAdapter` interface (`read`, `write`, `delete`, `exists`, `list(prefix)`).
37
+
38
+ ## How it fits in
39
+
40
+ `storage-local` is the default `storage` option for `createShelfRouter` in single-node deployments, storing screenshots, diff overlays, and storybook archives on disk. It implements the same `StorageAdapter` interface as `@storyshelf/storage-s3`, so moving to object storage later is a drop-in swap.
41
+
42
+ See `docs/architecture.md` and ADR 0006.
@@ -0,0 +1,14 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { StorageAdapter } from "@storyshelf/core/adapter/storage";
4
+ //#region src/index.d.ts
5
+ /**
6
+ * Create a local filesystem-backed StorageAdapter rooted at the given directory.
7
+ *
8
+ * @param dataDir - Root directory in which all stored files live.
9
+ * @returns A StorageAdapter that reads and writes files under `dataDir`.
10
+ */
11
+ declare function createLocalStorage(dataDir: string): StorageAdapter;
12
+ //#endregion
13
+ export { createLocalStorage };
14
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,68 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
5
+ import { dirname, join, relative, resolve, sep } from "node:path";
6
+ //#region src/index.ts
7
+ async function pathExists(target) {
8
+ try {
9
+ await access(target);
10
+ return true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+ /**
16
+ * Create a local filesystem-backed StorageAdapter rooted at the given directory.
17
+ *
18
+ * @param dataDir - Root directory in which all stored files live.
19
+ * @returns A StorageAdapter that reads and writes files under `dataDir`.
20
+ */
21
+ function createLocalStorage(dataDir) {
22
+ const root = resolve(dataDir);
23
+ function toAbsolute(path) {
24
+ const target = resolve(root, path);
25
+ const rel = relative(root, target);
26
+ if (rel === ".." || rel.startsWith(`..${sep}`)) throw new Error(`Path escapes storage directory: ${path}`);
27
+ return target;
28
+ }
29
+ async function walk(dir) {
30
+ const entries = await readdir(dir, { withFileTypes: true });
31
+ return (await Promise.all(entries.map(async (entry) => {
32
+ const full = join(dir, entry.name);
33
+ if (entry.isDirectory()) return await walk(full);
34
+ return [relative(root, full)];
35
+ }))).flat();
36
+ }
37
+ return {
38
+ metadata: {
39
+ name: "Local Storage",
40
+ version: "0.1.0",
41
+ description: "Local filesystem storage adapter",
42
+ kind: "local"
43
+ },
44
+ async read(path) {
45
+ return await readFile(toAbsolute(path));
46
+ },
47
+ async write(path, data) {
48
+ const target = toAbsolute(path);
49
+ await mkdir(dirname(target), { recursive: true });
50
+ await writeFile(target, data);
51
+ },
52
+ async delete(path) {
53
+ await rm(toAbsolute(path), { force: true });
54
+ },
55
+ async exists(path) {
56
+ return await pathExists(toAbsolute(path));
57
+ },
58
+ async list(prefix) {
59
+ const dir = toAbsolute(prefix);
60
+ if (!await pathExists(dir)) return [];
61
+ return walk(dir);
62
+ }
63
+ };
64
+ }
65
+ //#endregion
66
+ export { createLocalStorage };
67
+
68
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { access, mkdir, readdir, readFile, rm, writeFile } from \"node:fs/promises\";\nimport { dirname, join, relative, resolve, sep } from \"node:path\";\n\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\n\ndeclare const __PKG_VERSION__: string;\n\nasync function pathExists(target: string): Promise<boolean> {\n try {\n await access(target);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Create a local filesystem-backed StorageAdapter rooted at the given directory.\n *\n * @param dataDir - Root directory in which all stored files live.\n * @returns A StorageAdapter that reads and writes files under `dataDir`.\n */\nexport function createLocalStorage(dataDir: string): StorageAdapter {\n const root = resolve(dataDir);\n\n function toAbsolute(path: string): string {\n const target = resolve(root, path);\n const rel = relative(root, target);\n if (rel === \"..\" || rel.startsWith(`..${sep}`)) {\n throw new Error(`Path escapes storage directory: ${path}`);\n }\n return target;\n }\n\n async function walk(dir: string): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n const nested = await Promise.all(\n entries.map(async (entry) => {\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n return await walk(full);\n }\n return [relative(root, full)];\n }),\n );\n return nested.flat();\n }\n\n return {\n metadata: {\n name: \"Local Storage\",\n version: typeof __PKG_VERSION__ === \"undefined\" ? \"0.0.0\" : __PKG_VERSION__, // oxlint-disable-line unicorn/no-typeof-undefined\n description: \"Local filesystem storage adapter\",\n kind: \"local\",\n },\n async read(path) {\n return await readFile(toAbsolute(path));\n },\n async write(path, data) {\n const target = toAbsolute(path);\n await mkdir(dirname(target), { recursive: true });\n await writeFile(target, data);\n },\n async delete(path) {\n await rm(toAbsolute(path), { force: true });\n },\n async exists(path) {\n return await pathExists(toAbsolute(path));\n },\n async list(prefix) {\n const dir = toAbsolute(prefix);\n if (!(await pathExists(dir))) {\n return [];\n }\n return walk(dir);\n },\n };\n}\n"],"mappings":";;;;;;AAOA,eAAe,WAAW,QAAkC;CAC1D,IAAI;EACF,MAAM,OAAO,MAAM;EACnB,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;AAQA,SAAgB,mBAAmB,SAAiC;CAClE,MAAM,OAAO,QAAQ,OAAO;CAE5B,SAAS,WAAW,MAAsB;EACxC,MAAM,SAAS,QAAQ,MAAM,IAAI;EACjC,MAAM,MAAM,SAAS,MAAM,MAAM;EACjC,IAAI,QAAQ,QAAQ,IAAI,WAAW,KAAK,KAAK,GAC3C,MAAM,IAAI,MAAM,mCAAmC,MAAM;EAE3D,OAAO;CACT;CAEA,eAAe,KAAK,KAAgC;EAClD,MAAM,UAAU,MAAM,QAAQ,KAAK,EAAE,eAAe,KAAK,CAAC;EAU1D,QAAO,MATc,QAAQ,IAC3B,QAAQ,IAAI,OAAO,UAAU;GAC3B,MAAM,OAAO,KAAK,KAAK,MAAM,IAAI;GACjC,IAAI,MAAM,YAAY,GACpB,OAAO,MAAM,KAAK,IAAI;GAExB,OAAO,CAAC,SAAS,MAAM,IAAI,CAAC;EAC9B,CAAC,CACH,EAAA,CACc,KAAK;CACrB;CAEA,OAAO;EACL,UAAU;GACR,MAAM;GACN,SAAA;GACA,aAAa;GACb,MAAM;EACR;EACA,MAAM,KAAK,MAAM;GACf,OAAO,MAAM,SAAS,WAAW,IAAI,CAAC;EACxC;EACA,MAAM,MAAM,MAAM,MAAM;GACtB,MAAM,SAAS,WAAW,IAAI;GAC9B,MAAM,MAAM,QAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;GAChD,MAAM,UAAU,QAAQ,IAAI;EAC9B;EACA,MAAM,OAAO,MAAM;GACjB,MAAM,GAAG,WAAW,IAAI,GAAG,EAAE,OAAO,KAAK,CAAC;EAC5C;EACA,MAAM,OAAO,MAAM;GACjB,OAAO,MAAM,WAAW,WAAW,IAAI,CAAC;EAC1C;EACA,MAAM,KAAK,QAAQ;GACjB,MAAM,MAAM,WAAW,MAAM;GAC7B,IAAI,CAAE,MAAM,WAAW,GAAG,GACxB,OAAO,CAAC;GAEV,OAAO,KAAK,GAAG;EACjB;CACF;AACF"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@storyshelf/storage-local",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Local filesystem storage adapter for StoryShelf.",
6
+ "author": {
7
+ "name": "Siddhant Gupta",
8
+ "url": "https://guptasiddhant.com"
9
+ },
10
+ "license": "MIT",
11
+ "sideEffects": false,
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/GuptaSiddhant/storyshelf.git",
15
+ "directory": "packages/storage-local"
16
+ },
17
+ "homepage": "https://github.com/GuptaSiddhant/storyshelf#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/GuptaSiddhant/storyshelf/issues"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "exports": {
24
+ ".": "./dist/index.mjs",
25
+ "./package.json": "./package.json"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "scripts": {
32
+ "build": "tsdown",
33
+ "dev": "tsdown -w",
34
+ "fmt": "oxfmt -c ../../.oxfmtrc.json ./src",
35
+ "lint": "oxlint --type-aware --type-check ./src",
36
+ "test": "vitest run",
37
+ "prepublishOnly": "nub run build"
38
+ },
39
+ "dependencies": {
40
+ "@storyshelf/core": "workspace:*"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "catalog:",
44
+ "@vitest/coverage-v8": "catalog:",
45
+ "oxfmt": "catalog:",
46
+ "oxlint": "catalog:",
47
+ "oxlint-tsgolint": "catalog:",
48
+ "tsdown": "catalog:",
49
+ "typescript": "catalog:",
50
+ "vitest": "catalog:"
51
+ },
52
+ "types": "./dist/index.d.mts",
53
+ "exports": {
54
+ ".": {
55
+ "source": "./src/index.ts",
56
+ "default": "./dist/index.mjs"
57
+ },
58
+ "./package.json": "./package.json"
59
+ }
60
+ }