@thi.ng/block-fs 0.2.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Change Log
2
2
 
3
- - **Last updated**: 2025-04-02T10:24:13Z
3
+ - **Last updated**: 2025-04-06T13:54:45Z
4
4
  - **Generator**: [thi.ng/monopub](https://thi.ng/monopub)
5
5
 
6
6
  All notable changes to this project will be documented in this file.
@@ -11,6 +11,44 @@ See [Conventional Commits](https://conventionalcommits.org/) for commit guidelin
11
11
  **Note:** Unlisted _patch_ versions only involve non-code or otherwise excluded changes
12
12
  and/or version bumps of transitive dependencies.
13
13
 
14
+ ## [0.4.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/block-fs@0.4.0) (2025-04-06)
15
+
16
+ #### 🚀 Features
17
+
18
+ - add support for wrapping `ArrayBuffer` ([e23f008](https://github.com/thi-ng/umbrella/commit/e23f008))
19
+ - update `MemoryBlockStorageOpts.buffer` to allow array buffers
20
+ - update `MemoryBlockStorage` ctor
21
+ - auto-infer MIME type in `.readAsObjectURL()` ([8fbcebd](https://github.com/thi-ng/umbrella/commit/8fbcebd))
22
+ - use `preferredTypeForPath()` as MIME type fallback
23
+ - update deps
24
+ - update CLI, add include/exclude regexp, logging ([ef04e09](https://github.com/thi-ng/umbrella/commit/ef04e09))
25
+ - add support for multiple include/exclude regexps in `convert` command
26
+ - add `--quiet` flag to disable logging
27
+
28
+ ## [0.3.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/block-fs@0.3.0) (2025-04-02)
29
+
30
+ #### 🚀 Features
31
+
32
+ - add `BlockFS.readAsObjectURL()` ([551327c](https://github.com/thi-ng/umbrella/commit/551327c))
33
+ - add mem storage opts, add logging ([27016d6](https://github.com/thi-ng/umbrella/commit/27016d6))
34
+ - add `MemoryBlockStorage` support for pre-loaded buffers
35
+ - add logging
36
+ - add docs
37
+ - add CLI app wrapper ([68abe74](https://github.com/thi-ng/umbrella/commit/68abe74))
38
+ - add CLI app wrapper with these commands:
39
+ - `convert`: convert file tree into single BlockFS blob
40
+ - `list`: list file tree of a BlockFS blob
41
+ - update deps
42
+ - improve tree display (`list` cmd) ([a23866c](https://github.com/thi-ng/umbrella/commit/a23866c))
43
+
44
+ #### 🩹 Bug fixes
45
+
46
+ - fix parent dir linkage ([a121e76](https://github.com/thi-ng/umbrella/commit/a121e76))
47
+
48
+ #### ♻️ Refactoring
49
+
50
+ - update sentinel block ID ([51a1e44](https://github.com/thi-ng/umbrella/commit/51a1e44))
51
+
14
52
  ## [0.2.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/block-fs@0.2.0) (2025-04-02)
15
53
 
16
54
  #### 🚀 Features
package/README.md CHANGED
@@ -21,10 +21,15 @@
21
21
  - [Root directory](#root-directory)
22
22
  - [Directory entries](#directory-entries)
23
23
  - [File blocks](#file-blocks)
24
+ - [Command line app](#command-line-app)
25
+ - [Convert file tree into single BlockFS blob](#convert-file-tree-into-single-blockfs-blob)
26
+ - [List file tree of a BlockFS blob](#list-file-tree-of-a-blockfs-blob)
24
27
  - [Status](#status)
25
28
  - [Installation](#installation)
26
29
  - [Dependencies](#dependencies)
27
30
  - [API](#api)
31
+ - [Basic usage](#basic-usage)
32
+ - [Working with a converted file system blob](#working-with-a-converted-file-system-blob)
28
33
  - [Authors](#authors)
29
34
  - [License](#license)
30
35
 
@@ -102,6 +107,138 @@ the max. number of blocks in the storage backend.
102
107
 
103
108
  TODO diagram
104
109
 
110
+ ### Command line app
111
+
112
+ The package includes a mult-command CLI app with the following operations:
113
+
114
+ #### Convert file tree into single BlockFS blob
115
+
116
+ The `convert` command is used to bundle an entire file tree from the host system
117
+ into a single binary blob based on `BlockFS` with configured block size. The
118
+ file tree MUST fit into the RAM available to `bun` (or `node`).
119
+
120
+ Once bundled, the binary blob can then be used together with
121
+ [`MemoryBlockStorage`](https://docs.thi.ng/umbrella/block-fs/classes/MemoryBlockStorage.html)
122
+ and [`BlockFS`](https://docs.thi.ng/umbrella/block-fs/classes/BlockFS.html) for
123
+ other purposes (e.g. distributed with your web app to provide a virtual
124
+ filesystem). Also see [API example further
125
+ below](#working-with-a-converted-file-system-blob).
126
+
127
+ Example usage to bundle the source directory of this package:
128
+
129
+ ```bash
130
+ npx @thi.ng/block-fs convert -o dummy.dat packages/block-fs/src/
131
+
132
+ # [INFO] blockfs: number of files: 11
133
+ # [INFO] blockfs: number of directories: 2
134
+ # [INFO] blockfs: total file size: 40341
135
+ # [INFO] blockfs: number of blocks: 56
136
+ # [INFO] blockfs: writing file: dummy.dat
137
+ ```
138
+
139
+ General usage:
140
+
141
+ ```text
142
+ npx @thi.ng/block-fs convert --help
143
+
144
+ █ █ █ │
145
+ ██ █ │
146
+ █ █ █ █ █ █ █ █ │ @thi.ng/block-fs 0.4.0
147
+ █ █ █ █ █ █ █ █ █ │ Block-based storage & file system layer
148
+ █ │
149
+ █ █ │
150
+
151
+ Usage: blockfs <cmd> [opts] input [...]
152
+ blockfs <cmd> --help
153
+
154
+ Available commands:
155
+
156
+ convert : Convert file tree into single BlockFS blob
157
+ list : List file tree of a BlockFS blob
158
+
159
+ Flags:
160
+
161
+ -q, --quiet Disable logging
162
+ -v, --verbose Display extra logging information
163
+
164
+ Main:
165
+
166
+ -bs BYTES, --block-size BYTES Block size (default: 1024)
167
+ -i EXT, --exclude EXT [multiple] File exclusion regexp
168
+ -i EXT, --include EXT [multiple] File inclusion regexp
169
+ -n INT, --num-blocks INT Number of blocks (multiple of 8)
170
+ -o STR, --out STR [required] Output file path
171
+ ```
172
+
173
+ #### List file tree of a BlockFS blob
174
+
175
+ The `list` command is used to list the files & directories stored in a binary blob created via the `convert` command. Several output options (e.g. `tree`-like output) are supported.
176
+
177
+ ```bash
178
+ npx @thi.ng/block-fs list dummy.dat
179
+ # /api.ts
180
+ # /cli.ts
181
+ # /directory.ts
182
+ # /entry.ts
183
+ # /fs.ts
184
+ # /index.ts
185
+ # /lock.ts
186
+ # /storage
187
+ # /storage/astorage.ts
188
+ # /storage/file.ts
189
+ # /storage/memory.ts
190
+ # /utils.ts
191
+
192
+ npx @thi.ng/block-fs list --tree dummy.dat
193
+ # ├── api.ts
194
+ # ├── cli.ts
195
+ # ├── directory.ts
196
+ # ├── entry.ts
197
+ # ├── fs.ts
198
+ # ├── index.ts
199
+ # ├── lock.ts
200
+ # ├── storage
201
+ # │ ├── astorage.ts
202
+ # │ ├── file.ts
203
+ # │ └── memory.ts
204
+ # └── utils.ts
205
+
206
+ # display file sizes & modification times
207
+ npx @thi.ng/block-fs list --tree --all dummy.dat
208
+ # ├── api.ts 2204 2025-04-02T10:22:55.573Z
209
+ # ├── cli.ts 6799 2025-04-02T18:07:58.895Z
210
+ # ├── directory.ts 3994 2025-04-02T13:47:00.108Z
211
+ # ├── entry.ts 4130 2025-04-02T10:22:55.574Z
212
+ # ├── fs.ts 16377 2025-04-02T13:46:36.608Z
213
+ # ├── index.ts 317 2025-04-01T21:38:08.232Z
214
+ # ├── lock.ts 1501 2025-04-01T21:38:08.232Z
215
+ # ├── storage 2025-04-02T18:33:47.389Z
216
+ # │ ├── astorage.ts 1205 2025-04-02T10:22:55.574Z
217
+ # │ ├── file.ts 1780 2025-04-02T14:25:12.461Z
218
+ # │ └── memory.ts 1802 2025-04-02T14:26:02.163Z
219
+ # └── utils.ts 418 2025-04-02T10:22:55.574Z
220
+ ```
221
+
222
+ General usage:
223
+
224
+ ```text
225
+ npx @thi.ng/block-fs list --help
226
+
227
+ Usage: blockfs <cmd> [opts] input
228
+
229
+ Flags:
230
+
231
+ -a, --all Display all attribs
232
+ -t, --tree List files as tree
233
+ -v, --verbose Display extra process information
234
+ -m, --with-mtime Display modified times
235
+ -s, --with-size Display file sizes
236
+
237
+ Main:
238
+
239
+ -bs BYTES, --block-size BYTES Block size (default: 1024)
240
+ ```
241
+
105
242
  ## Status
106
243
 
107
244
  **ALPHA** - bleeding edge / work-in-progress
@@ -134,17 +271,19 @@ For Node.js REPL:
134
271
  const bf = await import("@thi.ng/block-fs");
135
272
  ```
136
273
 
137
- Package sizes (brotli'd, pre-treeshake): ESM: 4.32 KB
274
+ Package sizes (brotli'd, pre-treeshake): ESM: 4.50 KB
138
275
 
139
276
  ## Dependencies
140
277
 
141
278
  - [@thi.ng/api](https://github.com/thi-ng/umbrella/tree/develop/packages/api)
279
+ - [@thi.ng/args](https://github.com/thi-ng/umbrella/tree/develop/packages/args)
142
280
  - [@thi.ng/binary](https://github.com/thi-ng/umbrella/tree/develop/packages/binary)
143
281
  - [@thi.ng/bitfield](https://github.com/thi-ng/umbrella/tree/develop/packages/bitfield)
144
282
  - [@thi.ng/checks](https://github.com/thi-ng/umbrella/tree/develop/packages/checks)
145
283
  - [@thi.ng/errors](https://github.com/thi-ng/umbrella/tree/develop/packages/errors)
146
284
  - [@thi.ng/file-io](https://github.com/thi-ng/umbrella/tree/develop/packages/file-io)
147
285
  - [@thi.ng/logger](https://github.com/thi-ng/umbrella/tree/develop/packages/logger)
286
+ - [@thi.ng/mime](https://github.com/thi-ng/umbrella/tree/develop/packages/mime)
148
287
  - [@thi.ng/random](https://github.com/thi-ng/umbrella/tree/develop/packages/random)
149
288
  - [@thi.ng/strings](https://github.com/thi-ng/umbrella/tree/develop/packages/strings)
150
289
 
@@ -154,7 +293,9 @@ Note: @thi.ng/api is in _most_ cases a type-only import (not used at runtime)
154
293
 
155
294
  [Generated API docs](https://docs.thi.ng/umbrella/block-fs/)
156
295
 
157
- ```ts tangle:export/readme.ts
296
+ ### Basic usage
297
+
298
+ ```ts tangle:export/readme-1.ts
158
299
  import { BlockFS, MemoryBlockStorage } from "@thi.ng/block-fs";
159
300
 
160
301
  // create in-memory storage (64KB)
@@ -216,6 +357,45 @@ for await (let entry of fs.root.tree()) {
216
357
  // /deeply/nested/paths/are-ok 4n 2025-04-01T20:18:55.919Z
217
358
  ```
218
359
 
360
+ ### Working with a converted file system blob
361
+
362
+ This example shows how to use a binary blob created via the [CLI `blockfs
363
+ convert` command](#convert-file-tree-into-single-blockfs-blob) as a virtual file
364
+ system...
365
+
366
+ ```ts tangle:export/readme-2.ts
367
+ import { BlockFS, MemoryBlockStorage } from "@thi.ng/block-fs";
368
+
369
+ // load binary blob
370
+ const response = await fetch("./blocks.dat");
371
+ const buffer = await response.arrayBuffer();
372
+
373
+ // wrap as block storage
374
+ const storage = new MemoryBlockStorage({
375
+ buffer,
376
+ blockSize: 1024,
377
+ numBlocks: buffer.byteLength / 1024
378
+ });
379
+
380
+ // wrap as file system
381
+ const fs = new BlockFS(storage);
382
+
383
+ // list all entries (recursive)
384
+ for await(let f of fs.root.tree()) {
385
+ console.log(f.path);
386
+ }
387
+
388
+ // list all entries in a directory
389
+ const dir = (await fs.entryForPath("/path/to/dir")).directory;
390
+ for await (let f of dir) {
391
+ console.log(f.path);
392
+ }
393
+
394
+ // load an image as blob URL (MIME type is inferred automatically)
395
+ const img = new Image();
396
+ img.src = await fs.readAsObjectURL("/assets/test.jpg");
397
+ ```
398
+
219
399
  ## Authors
220
400
 
221
401
  - [Karsten Schmidt](https://thi.ng)
package/bin/blockfs ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # https://stackoverflow.com/a/246128/294515
4
+ SOURCE="${BASH_SOURCE[0]}"
5
+ while [ -L "$SOURCE" ]; do
6
+ DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )"
7
+ SOURCE="$(readlink "$SOURCE")"
8
+ [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
9
+ done
10
+ DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )"
11
+ DIR="$(dirname $DIR)"
12
+
13
+ # prefer using bun
14
+ if [ -x "$(command -v bun)" ]; then
15
+ CMD=bun
16
+ else
17
+ CMD=node
18
+ fi
19
+
20
+ /usr/bin/env $CMD "$DIR/cli.js" "$DIR" "$@"
package/cli.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { type Command, type CommandCtx } from "@thi.ng/args";
2
+ interface CLIOpts {
3
+ verbose: boolean;
4
+ quiet: boolean;
5
+ }
6
+ interface ConvertOpts extends CLIOpts {
7
+ numBlocks?: number;
8
+ blockSize: number;
9
+ exclude?: string[];
10
+ include?: string[];
11
+ out: string;
12
+ }
13
+ interface ListOpts extends CLIOpts {
14
+ blockSize: number;
15
+ all: boolean;
16
+ tree: boolean;
17
+ withMtime: boolean;
18
+ withSize: boolean;
19
+ }
20
+ export interface AppCtx<T extends CLIOpts> extends CommandCtx<T, CLIOpts> {
21
+ }
22
+ export declare const CONVERT: Command<ConvertOpts, CLIOpts, AppCtx<ConvertOpts>>;
23
+ export declare const LIST: Command<ListOpts, CLIOpts, AppCtx<ListOpts>>;
24
+ export {};
25
+ //# sourceMappingURL=cli.d.ts.map
package/cli.js ADDED
@@ -0,0 +1,229 @@
1
+ import {
2
+ cliApp,
3
+ flag,
4
+ int,
5
+ string,
6
+ strings
7
+ } from "@thi.ng/args";
8
+ import { align, isPow2 } from "@thi.ng/binary";
9
+ import { illegalArgs } from "@thi.ng/errors";
10
+ import { files, readBinary, readJSON, writeFile } from "@thi.ng/file-io";
11
+ import { LogLevel } from "@thi.ng/logger";
12
+ import { statSync } from "node:fs";
13
+ import { dirname, join, relative, resolve } from "node:path";
14
+ import { Entry } from "./entry.js";
15
+ import { BlockFS } from "./fs.js";
16
+ import { MemoryBlockStorage } from "./storage/memory.js";
17
+ const PKG = readJSON(join(process.argv[2], "package.json"));
18
+ const ARG_BLOCKSIZE = {
19
+ blockSize: int({
20
+ alias: "bs",
21
+ desc: "Block size",
22
+ hint: "BYTES",
23
+ default: 1024,
24
+ coerce: (x) => {
25
+ const size = +x;
26
+ if (!isPow2(size)) illegalArgs("block size must be a power of 2");
27
+ return size;
28
+ }
29
+ })
30
+ };
31
+ const collectFiles = ({
32
+ opts: { include, exclude },
33
+ inputs
34
+ }) => {
35
+ const root = resolve(inputs[0]);
36
+ const filtered = [];
37
+ const dirs = /* @__PURE__ */ new Set();
38
+ const $include = include?.map((x) => new RegExp(x));
39
+ const $exclude = exclude?.map((x) => new RegExp(x));
40
+ let total = 0;
41
+ for (let f of files(root)) {
42
+ const stats = statSync(f);
43
+ if ($exclude && $exclude.some((x) => x.test(f))) continue;
44
+ if ($include && !$include.some((x) => x.test(f))) continue;
45
+ const dest = relative(root, f);
46
+ filtered.push({
47
+ src: f,
48
+ dest,
49
+ size: stats.size,
50
+ ctime: stats.ctimeMs,
51
+ mtime: stats.mtimeMs
52
+ });
53
+ dirs.add(dirname(dest));
54
+ total += stats.size;
55
+ }
56
+ return { files: filtered, dirs: [...dirs], size: total };
57
+ };
58
+ const computeBlockCount = (collected, blockSize, numBlocks) => {
59
+ let blocks = collected.dirs.length;
60
+ const blockIDBytes = numBlocks ? align(Math.ceil(Math.log2(numBlocks)), 8) >> 3 : align(Math.ceil(Math.log2(collected.size / blockSize + blocks)), 8) >> 3;
61
+ const blockDataSizeBytes = align(Math.ceil(Math.log2(blockSize)), 8) >> 3;
62
+ const blockDataSize = blockSize - blockIDBytes - blockDataSizeBytes;
63
+ blocks += Math.ceil(
64
+ (collected.files.length + collected.dirs.length) * Entry.SIZE / blockDataSize
65
+ );
66
+ for (let f of collected.files) {
67
+ blocks += Math.ceil(f.size / blockDataSize);
68
+ }
69
+ const blockIDBytes2 = align(Math.ceil(Math.log2(blocks)), 8) >> 3;
70
+ return blockIDBytes2 > blockIDBytes ? computeBlockCount(collected, blockSize, blocks) : blocks;
71
+ };
72
+ const CONVERT = {
73
+ opts: {
74
+ ...ARG_BLOCKSIZE,
75
+ numBlocks: int({
76
+ alias: "n",
77
+ desc: "Number of blocks (multiple of 8)",
78
+ optional: true
79
+ }),
80
+ out: string({
81
+ alias: "o",
82
+ desc: "Output file path",
83
+ optional: false
84
+ }),
85
+ exclude: strings({
86
+ alias: "e",
87
+ desc: "File exclusion regexp",
88
+ hint: "EXT"
89
+ }),
90
+ include: strings({
91
+ alias: "i",
92
+ desc: "File inclusion regexp",
93
+ hint: "EXT"
94
+ })
95
+ },
96
+ desc: "Convert file tree into single BlockFS blob",
97
+ fn: async (ctx) => {
98
+ const collected = collectFiles(ctx);
99
+ const numBlocks = align(
100
+ ctx.opts.numBlocks ?? computeBlockCount(collected, ctx.opts.blockSize),
101
+ 8
102
+ );
103
+ ctx.logger.info("number of files:", collected.files.length);
104
+ ctx.logger.info("number of directories:", collected.dirs.length);
105
+ ctx.logger.info("total file size:", collected.size);
106
+ ctx.logger.info("number of blocks:", numBlocks);
107
+ const storage = new MemoryBlockStorage({
108
+ numBlocks,
109
+ blockSize: ctx.opts.blockSize,
110
+ logger: ctx.logger
111
+ });
112
+ const bfs = new BlockFS(storage, { logger: ctx.logger });
113
+ await bfs.init();
114
+ for (let f of collected.files) {
115
+ ctx.logger.info("writing file:", f.dest);
116
+ await bfs.writeFile(f.dest, readBinary(f.src));
117
+ const entry = await bfs.entryForPath(f.dest);
118
+ entry.ctime = f.ctime;
119
+ entry.mtime = f.mtime;
120
+ await entry.save();
121
+ }
122
+ writeFile(ctx.opts.out, storage.buffer, void 0, ctx.logger);
123
+ }
124
+ };
125
+ const LIST = {
126
+ opts: {
127
+ ...ARG_BLOCKSIZE,
128
+ all: flag({
129
+ alias: "a",
130
+ desc: "Display all attribs"
131
+ }),
132
+ tree: flag({
133
+ alias: "t",
134
+ desc: "List files as tree"
135
+ }),
136
+ withMtime: flag({
137
+ alias: "m",
138
+ desc: "Display modified times"
139
+ }),
140
+ withSize: flag({
141
+ alias: "s",
142
+ desc: "Display file sizes"
143
+ })
144
+ },
145
+ desc: "List file tree of a BlockFS blob",
146
+ fn: async (ctx) => {
147
+ if (ctx.opts.all) {
148
+ ctx.opts.withMtime = ctx.opts.withSize = true;
149
+ }
150
+ const buffer = readBinary(ctx.inputs[0]);
151
+ const storage = new MemoryBlockStorage({
152
+ numBlocks: buffer.length / ctx.opts.blockSize >>> 0,
153
+ blockSize: ctx.opts.blockSize,
154
+ logger: ctx.logger,
155
+ buffer
156
+ });
157
+ const bfs = new BlockFS(storage, { logger: ctx.logger });
158
+ await bfs.init();
159
+ const tree = [];
160
+ for await (let entry of bfs.root.tree()) {
161
+ const path = entry.path;
162
+ const depth = path.split("/").length - 2;
163
+ tree.push([path, depth, entry]);
164
+ }
165
+ tree.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
166
+ const rows = [];
167
+ const maxWidths = [0, 0, 0];
168
+ for (let i = 0, num = tree.length - 1; i <= num; i++) {
169
+ const f = tree[i];
170
+ const isLast = i === num || f[1] > tree[i + 1][1];
171
+ const row = ctx.opts.tree ? ["\u2502 ".repeat(f[1]) + (isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ") + f[2].name] : [f[0]];
172
+ if (ctx.opts.withSize) {
173
+ row.push(f[2].isDirectory() ? "" : String(Number(f[2].size)));
174
+ }
175
+ if (ctx.opts.withMtime) {
176
+ row.push(new Date(f[2].mtime).toISOString());
177
+ }
178
+ rows.push(row);
179
+ for (let i2 = 0; i2 < row.length; i2++) {
180
+ maxWidths[i2] = Math.max(maxWidths[i2], row[i2].length);
181
+ }
182
+ }
183
+ for (let row of rows) {
184
+ console.log(row.map((x, i) => x.padEnd(maxWidths[i])).join(" "));
185
+ }
186
+ }
187
+ };
188
+ cliApp({
189
+ opts: {
190
+ verbose: flag({
191
+ alias: "v",
192
+ desc: "Display extra logging information"
193
+ }),
194
+ quiet: flag({
195
+ alias: "q",
196
+ desc: "Disable logging"
197
+ })
198
+ },
199
+ commands: {
200
+ convert: CONVERT,
201
+ list: LIST
202
+ },
203
+ name: "blockfs",
204
+ ctx: async (ctx) => {
205
+ if (ctx.opts.quiet) ctx.logger.level = LogLevel.NONE;
206
+ else if (ctx.opts.verbose) ctx.logger.level = LogLevel.DEBUG;
207
+ return ctx;
208
+ },
209
+ start: 3,
210
+ usage: {
211
+ prefix: `
212
+ \u2588 \u2588 \u2588 \u2502
213
+ \u2588\u2588 \u2588 \u2502
214
+ \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2502 ${PKG.name} ${PKG.version}
215
+ \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2502 Block-based storage & file system layer
216
+ \u2588 \u2502
217
+ \u2588 \u2588 \u2502
218
+
219
+ Usage: blockfs <cmd> [opts] input [...]
220
+ blockfs <cmd> --help
221
+ `,
222
+ showGroupNames: true,
223
+ paramWidth: 32
224
+ }
225
+ });
226
+ export {
227
+ CONVERT,
228
+ LIST
229
+ };
package/directory.js CHANGED
@@ -65,7 +65,7 @@ class Directory {
65
65
  const block = (await fs.allocateBlocks(1))[0];
66
66
  const data = await fs.storage.loadBlock(block);
67
67
  fs.setBlockMeta(data, fs.sentinelID, 0);
68
- fs.setBlockLink(data, this.entry.start, fs.dataStartBlockID);
68
+ fs.setBlockLink(data, this.entry.start, fs.blockDataOffset);
69
69
  await fs.storage.saveBlock(block, data);
70
70
  return this.addEntry(
71
71
  {
package/fs.d.ts CHANGED
@@ -155,6 +155,23 @@ export declare class BlockFS {
155
155
  * @param path
156
156
  */
157
157
  readJSON<T>(path: string | number): Promise<T>;
158
+ /**
159
+ * Fully reads given file into a single byte buffer and returns it as blob
160
+ * object URL, optionally typed with given MIME type.
161
+ *
162
+ * @remarks
163
+ * If `type` is omitted, it will be attempted to be inferred automatically
164
+ * via [thi.ng/mime](https://thi.ng/mime).
165
+ *
166
+ * @remarks
167
+ * Reference:
168
+ *
169
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Blob#creating_a_url_representing_the_contents_of_a_typed_array
170
+ *
171
+ * @param path
172
+ * @param type
173
+ */
174
+ readAsObjectURL(path: string | number, type?: string): Promise<string>;
158
175
  /**
159
176
  * Takes an array of block IDs (or `null`) and a `data` byte array. Writes
160
177
  * chunks of data into given blocks and connecting each block as linked
package/fs.js CHANGED
@@ -5,6 +5,7 @@ import { assert } from "@thi.ng/errors/assert";
5
5
  import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
6
6
  import { illegalState } from "@thi.ng/errors/illegal-state";
7
7
  import { NULL_LOGGER } from "@thi.ng/logger/null";
8
+ import { preferredTypeForPath } from "@thi.ng/mime";
8
9
  import {
9
10
  EntryType
10
11
  } from "./api.js";
@@ -41,7 +42,7 @@ class BlockFS {
41
42
  this.rootDirBlockID = align(this.blockIndex.data.length, storage.blockSize) / storage.blockSize;
42
43
  this.dataStartBlockID = this.rootDirBlockID + 1;
43
44
  this.dirDataOffset = this.blockDataOffset + this.blockIDBytes;
44
- this.sentinelID = storage.numBlocks - 1;
45
+ this.sentinelID = 2 ** (this.blockIDBytes * 8) - 1;
45
46
  this.tmp = new Uint8Array(storage.blockSize);
46
47
  }
47
48
  blockIndex;
@@ -284,6 +285,29 @@ class BlockFS {
284
285
  async readJSON(path) {
285
286
  return JSON.parse(await this.readText(path));
286
287
  }
288
+ /**
289
+ * Fully reads given file into a single byte buffer and returns it as blob
290
+ * object URL, optionally typed with given MIME type.
291
+ *
292
+ * @remarks
293
+ * If `type` is omitted, it will be attempted to be inferred automatically
294
+ * via [thi.ng/mime](https://thi.ng/mime).
295
+ *
296
+ * @remarks
297
+ * Reference:
298
+ *
299
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Blob#creating_a_url_representing_the_contents_of_a_typed_array
300
+ *
301
+ * @param path
302
+ * @param type
303
+ */
304
+ async readAsObjectURL(path, type) {
305
+ return URL.createObjectURL(
306
+ new Blob([await this.readFile(path)], {
307
+ type: type ?? (isString(path) ? preferredTypeForPath(path) : void 0)
308
+ })
309
+ );
310
+ }
287
311
  /**
288
312
  * Takes an array of block IDs (or `null`) and a `data` byte array. Writes
289
313
  * chunks of data into given blocks and connecting each block as linked
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@thi.ng/block-fs",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Customizable block-based storage, adapters & file system layer",
5
5
  "type": "module",
6
6
  "module": "./index.js",
7
7
  "typings": "./index.d.ts",
8
+ "bin": {
9
+ "blockfs": "bin/blockfs"
10
+ },
8
11
  "sideEffects": false,
9
12
  "repository": {
10
13
  "type": "git",
@@ -40,12 +43,14 @@
40
43
  },
41
44
  "dependencies": {
42
45
  "@thi.ng/api": "^8.11.25",
46
+ "@thi.ng/args": "^2.3.66",
43
47
  "@thi.ng/binary": "^3.4.48",
44
48
  "@thi.ng/bitfield": "^2.4.0",
45
49
  "@thi.ng/checks": "^3.7.5",
46
50
  "@thi.ng/errors": "^2.5.31",
47
51
  "@thi.ng/file-io": "^2.1.34",
48
52
  "@thi.ng/logger": "^3.1.6",
53
+ "@thi.ng/mime": "^2.7.7",
49
54
  "@thi.ng/random": "^4.1.16",
50
55
  "@thi.ng/strings": "^3.9.10"
51
56
  },
@@ -56,12 +61,17 @@
56
61
  "typescript": "^5.8.2"
57
62
  },
58
63
  "keywords": [
64
+ "async",
59
65
  "binary",
60
66
  "block",
67
+ "cli",
68
+ "conversion",
61
69
  "file",
62
- "file-system",
70
+ "filesystem",
63
71
  "memory",
64
72
  "memory-mapped",
73
+ "mime",
74
+ "nodejs",
65
75
  "path",
66
76
  "storage",
67
77
  "typedarray",
@@ -80,6 +90,7 @@
80
90
  "files": [
81
91
  "./*.js",
82
92
  "./*.d.ts",
93
+ "bin",
83
94
  "storage"
84
95
  ],
85
96
  "exports": {
@@ -89,6 +100,9 @@
89
100
  "./api": {
90
101
  "default": "./api.js"
91
102
  },
103
+ "./cli": {
104
+ "default": "./cli.js"
105
+ },
92
106
  "./directory": {
93
107
  "default": "./directory.js"
94
108
  },
@@ -118,5 +132,5 @@
118
132
  "status": "alpha",
119
133
  "year": 2024
120
134
  },
121
- "gitHead": "de166de7bb358998c797090a49bf55bcb7c325ba\n"
135
+ "gitHead": "c88a589f33207b02a43172313b38ea09571265f1\n"
122
136
  }
package/storage/file.d.ts CHANGED
@@ -8,7 +8,13 @@ export declare class FileBlock implements IBlock {
8
8
  save(data: Uint8Array): Promise<void>;
9
9
  delete(): Promise<void>;
10
10
  }
11
+ /**
12
+ * Configuration options for {@link FileBlockStorage}.
13
+ */
11
14
  export interface FileBlockStorageOpts extends BlockStorageOpts {
15
+ /**
16
+ * Path to host filesystem base directory used for storing blocks.
17
+ */
12
18
  baseDir: string;
13
19
  }
14
20
  export declare class FileBlockStorage extends ABlockStorage<FileBlock> {
package/storage/file.js CHANGED
@@ -11,18 +11,15 @@ class FileBlock {
11
11
  this.id = id;
12
12
  }
13
13
  async load() {
14
- const path = this.storage.getPath(this.id);
15
- return existsSync(path) ? readBinary(path, this.storage.logger) : new Uint8Array(this.storage.blockSize);
14
+ const { storage } = this;
15
+ const path = storage.getPath(this.id);
16
+ return existsSync(path) ? readBinary(path, storage.logger) : new Uint8Array(storage.blockSize);
16
17
  }
17
18
  async save(data) {
18
- if (data.length !== this.storage.blockSize)
19
+ const { storage } = this;
20
+ if (data.length !== storage.blockSize)
19
21
  illegalArgs(`wrong block size: ${data.length}`);
20
- writeFile(
21
- this.storage.getPath(this.id),
22
- data,
23
- null,
24
- this.storage.logger
25
- );
22
+ writeFile(storage.getPath(this.id), data, null, storage.logger);
26
23
  }
27
24
  async delete() {
28
25
  deleteFile(this.storage.getPath(this.id), this.storage.logger);
@@ -8,9 +8,19 @@ export declare class MemoryBlock implements IBlock {
8
8
  save(data: Uint8Array): Promise<void>;
9
9
  delete(): Promise<void>;
10
10
  }
11
+ /**
12
+ * Configuration options for {@link MemoryBlockStorage}.
13
+ */
14
+ export interface MemoryBlockStorageOpts extends BlockStorageOpts {
15
+ /**
16
+ * Optional, pre-defined/loaded byte buffer. Must have at least `numBlocks *
17
+ * blockSize` capacity.
18
+ */
19
+ buffer?: Uint8Array | ArrayBufferLike;
20
+ }
11
21
  export declare class MemoryBlockStorage extends ABlockStorage<MemoryBlock> {
12
22
  buffer: Uint8Array;
13
- constructor(opts: BlockStorageOpts);
23
+ constructor(opts: MemoryBlockStorageOpts);
14
24
  hasBlock(id: number): Promise<boolean>;
15
25
  ensureBlock(id: number): MemoryBlock;
16
26
  }
package/storage/memory.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { isArrayBufferLike } from "@thi.ng/checks/is-arraybufferlike";
1
2
  import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
2
3
  import { ABlockStorage } from "./astorage.js";
3
4
  class MemoryBlock {
@@ -6,16 +7,16 @@ class MemoryBlock {
6
7
  this.id = id;
7
8
  }
8
9
  async load() {
9
- const size = this.storage.blockSize;
10
- return this.storage.buffer.subarray(
11
- this.id * size,
12
- (this.id + 1) * size
13
- );
10
+ const { storage } = this;
11
+ storage.logger.debug("load block", this.id);
12
+ const size = storage.blockSize;
13
+ return storage.buffer.subarray(this.id * size, (this.id + 1) * size);
14
14
  }
15
15
  async save(data) {
16
- if (data.length !== this.storage.blockSize)
17
- illegalArgs(`wrong block size`);
18
- this.storage.buffer.set(data, this.id * this.storage.blockSize);
16
+ const { storage } = this;
17
+ if (data.length !== storage.blockSize) illegalArgs(`wrong block size`);
18
+ storage.logger.debug("save block", this.id);
19
+ storage.buffer.set(data, this.id * storage.blockSize);
19
20
  }
20
21
  async delete() {
21
22
  const size = this.storage.blockSize;
@@ -26,7 +27,14 @@ class MemoryBlockStorage extends ABlockStorage {
26
27
  buffer;
27
28
  constructor(opts) {
28
29
  super(opts);
29
- this.buffer = new Uint8Array(this.numBlocks * this.blockSize);
30
+ const size = this.numBlocks * this.blockSize;
31
+ const buffer = opts.buffer ? isArrayBufferLike(opts.buffer) ? new Uint8Array(opts.buffer) : opts.buffer : void 0;
32
+ if (buffer && buffer.length < size) {
33
+ illegalArgs(
34
+ `given buffer is too small, expected at least ${size} bytes`
35
+ );
36
+ }
37
+ this.buffer = buffer ?? new Uint8Array(size);
30
38
  }
31
39
  async hasBlock(id) {
32
40
  this.ensureValidID(id);