@thi.ng/block-fs 0.3.0 → 0.4.1

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-02T18:47:59Z
3
+ - **Last updated**: 2025-04-16T11:11:14Z
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,27 @@ 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.1](https://github.com/thi-ng/umbrella/tree/@thi.ng/block-fs@0.4.1) (2025-04-16)
15
+
16
+ #### ♻️ Refactoring
17
+
18
+ - update Entry memory layout ([b5416bd](https://github.com/thi-ng/umbrella/commit/b5416bd))
19
+ - move block start & end ID locations
20
+
21
+ ## [0.4.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/block-fs@0.4.0) (2025-04-06)
22
+
23
+ #### 🚀 Features
24
+
25
+ - add support for wrapping `ArrayBuffer` ([e23f008](https://github.com/thi-ng/umbrella/commit/e23f008))
26
+ - update `MemoryBlockStorageOpts.buffer` to allow array buffers
27
+ - update `MemoryBlockStorage` ctor
28
+ - auto-infer MIME type in `.readAsObjectURL()` ([8fbcebd](https://github.com/thi-ng/umbrella/commit/8fbcebd))
29
+ - use `preferredTypeForPath()` as MIME type fallback
30
+ - update deps
31
+ - update CLI, add include/exclude regexp, logging ([ef04e09](https://github.com/thi-ng/umbrella/commit/ef04e09))
32
+ - add support for multiple include/exclude regexps in `convert` command
33
+ - add `--quiet` flag to disable logging
34
+
14
35
  ## [0.3.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/block-fs@0.3.0) (2025-04-02)
15
36
 
16
37
  #### 🚀 Features
package/README.md CHANGED
@@ -28,6 +28,8 @@
28
28
  - [Installation](#installation)
29
29
  - [Dependencies](#dependencies)
30
30
  - [API](#api)
31
+ - [Basic usage](#basic-usage)
32
+ - [Working with a converted file system blob](#working-with-a-converted-file-system-blob)
31
33
  - [Authors](#authors)
32
34
  - [License](#license)
33
35
 
@@ -94,16 +96,33 @@ entries/sizes. The default [`Entry`
94
96
  implementation](https://docs.thi.ng/umbrella/block-fs/classes/Entry.html)
95
97
  requires 64 bytes.
96
98
 
97
- TODO diagram
99
+ ![Memory layout diagram for a single directory entry](https://raw.githubusercontent.com/thi-ng/umbrella/develop/assets/block-fs/direntry-01.png)
98
100
 
99
101
  #### File blocks
100
102
 
101
103
  Files are stored as linked lists of blocks, with the first few bytes of each
102
- block reserved for linkage and number of data bytes in the block. The number of
103
- bytes effectively available for data depends on the configured block size and
104
- the max. number of blocks in the storage backend.
104
+ block reserved for linkage and number of data bytes in the block.
105
105
 
106
- TODO diagram
106
+ ![Memory layout diagram for a single file block](https://raw.githubusercontent.com/thi-ng/umbrella/develop/assets/block-fs/block-layout-01.png)
107
+
108
+ The number of bytes effectively available for data depends on the configured
109
+ block size and the max. number of blocks in the storage backend. For example, a
110
+ max. block count of 65536 and a block size of 256 bytes only requires a two
111
+ bytes for linkage and a third byte for storing the number of data bytes used in
112
+ the block. Hence, in this configuration 253 bytes per block are available for
113
+ data.
114
+
115
+ The following diagram shows a block which links to block ID 0x1234 and uses the
116
+ full 253 (0xfd in hex) bytes of data available:
117
+
118
+ ![Memory layout diagram for a single file block](https://raw.githubusercontent.com/thi-ng/umbrella/develop/assets/block-fs/block-layout-02.png)
119
+
120
+ The last block of a file uses a special sentinel marker to indicate that no
121
+ other blocks follow. This sentinel value again depends on the configured max.
122
+ block count, and in this example is 0xffff. This example block only stores 64
123
+ (0x40 in hex) bytes of data, with the remainder zeroed out.
124
+
125
+ ![Memory layout diagram for a sentinel file block](https://raw.githubusercontent.com/thi-ng/umbrella/develop/assets/block-fs/block-layout-03.png)
107
126
 
108
127
  ### Command line app
109
128
 
@@ -119,7 +138,8 @@ Once bundled, the binary blob can then be used together with
119
138
  [`MemoryBlockStorage`](https://docs.thi.ng/umbrella/block-fs/classes/MemoryBlockStorage.html)
120
139
  and [`BlockFS`](https://docs.thi.ng/umbrella/block-fs/classes/BlockFS.html) for
121
140
  other purposes (e.g. distributed with your web app to provide a virtual
122
- filesystem).
141
+ filesystem). Also see [API example further
142
+ below](#working-with-a-converted-file-system-blob).
123
143
 
124
144
  Example usage to bundle the source directory of this package:
125
145
 
@@ -138,15 +158,31 @@ General usage:
138
158
  ```text
139
159
  npx @thi.ng/block-fs convert --help
140
160
 
141
- Usage: blockfs <cmd> [opts] input
161
+ █ █ │
162
+ ██ █ │
163
+ █ █ █ █ █ █ █ █ │ @thi.ng/block-fs 0.4.0
164
+ █ █ █ █ █ █ █ █ █ │ Block-based storage & file system layer
165
+ █ │
166
+ █ █ │
167
+
168
+ Usage: blockfs <cmd> [opts] input [...]
169
+ blockfs <cmd> --help
170
+
171
+ Available commands:
172
+
173
+ convert : Convert file tree into single BlockFS blob
174
+ list : List file tree of a BlockFS blob
142
175
 
143
176
  Flags:
144
177
 
145
- -v, --verbose Display extra process information
178
+ -q, --quiet Disable logging
179
+ -v, --verbose Display extra logging information
146
180
 
147
181
  Main:
148
182
 
149
183
  -bs BYTES, --block-size BYTES Block size (default: 1024)
184
+ -i EXT, --exclude EXT [multiple] File exclusion regexp
185
+ -i EXT, --include EXT [multiple] File inclusion regexp
150
186
  -n INT, --num-blocks INT Number of blocks (multiple of 8)
151
187
  -o STR, --out STR [required] Output file path
152
188
  ```
@@ -252,7 +288,7 @@ For Node.js REPL:
252
288
  const bf = await import("@thi.ng/block-fs");
253
289
  ```
254
290
 
255
- Package sizes (brotli'd, pre-treeshake): ESM: 4.43 KB
291
+ Package sizes (brotli'd, pre-treeshake): ESM: 4.50 KB
256
292
 
257
293
  ## Dependencies
258
294
 
@@ -264,6 +300,7 @@ Package sizes (brotli'd, pre-treeshake): ESM: 4.43 KB
264
300
  - [@thi.ng/errors](https://github.com/thi-ng/umbrella/tree/develop/packages/errors)
265
301
  - [@thi.ng/file-io](https://github.com/thi-ng/umbrella/tree/develop/packages/file-io)
266
302
  - [@thi.ng/logger](https://github.com/thi-ng/umbrella/tree/develop/packages/logger)
303
+ - [@thi.ng/mime](https://github.com/thi-ng/umbrella/tree/develop/packages/mime)
267
304
  - [@thi.ng/random](https://github.com/thi-ng/umbrella/tree/develop/packages/random)
268
305
  - [@thi.ng/strings](https://github.com/thi-ng/umbrella/tree/develop/packages/strings)
269
306
 
@@ -273,7 +310,9 @@ Note: @thi.ng/api is in _most_ cases a type-only import (not used at runtime)
273
310
 
274
311
  [Generated API docs](https://docs.thi.ng/umbrella/block-fs/)
275
312
 
276
- ```ts tangle:export/readme.ts
313
+ ### Basic usage
314
+
315
+ ```ts tangle:export/readme-1.ts
277
316
  import { BlockFS, MemoryBlockStorage } from "@thi.ng/block-fs";
278
317
 
279
318
  // create in-memory storage (64KB)
@@ -335,6 +374,45 @@ for await (let entry of fs.root.tree()) {
335
374
  // /deeply/nested/paths/are-ok 4n 2025-04-01T20:18:55.919Z
336
375
  ```
337
376
 
377
+ ### Working with a converted file system blob
378
+
379
+ This example shows how to use a binary blob created via the [CLI `blockfs
380
+ convert` command](#convert-file-tree-into-single-blockfs-blob) as a virtual file
381
+ system...
382
+
383
+ ```ts tangle:export/readme-2.ts
384
+ import { BlockFS, MemoryBlockStorage } from "@thi.ng/block-fs";
385
+
386
+ // load binary blob
387
+ const response = await fetch("./blocks.dat");
388
+ const buffer = await response.arrayBuffer();
389
+
390
+ // wrap as block storage
391
+ const storage = new MemoryBlockStorage({
392
+ buffer,
393
+ blockSize: 1024,
394
+ numBlocks: buffer.byteLength / 1024
395
+ });
396
+
397
+ // wrap as file system
398
+ const fs = new BlockFS(storage);
399
+
400
+ // list all entries (recursive)
401
+ for await(let f of fs.root.tree()) {
402
+ console.log(f.path);
403
+ }
404
+
405
+ // list all entries in a directory
406
+ const dir = (await fs.entryForPath("/path/to/dir")).directory;
407
+ for await (let f of dir) {
408
+ console.log(f.path);
409
+ }
410
+
411
+ // load an image as blob URL (MIME type is inferred automatically)
412
+ const img = new Image();
413
+ img.src = await fs.readAsObjectURL("/assets/test.jpg");
414
+ ```
415
+
338
416
  ## Authors
339
417
 
340
418
  - [Karsten Schmidt](https://thi.ng)
package/cli.d.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { type Command, type CommandCtx } from "@thi.ng/args";
2
2
  interface CLIOpts {
3
3
  verbose: boolean;
4
+ quiet: boolean;
4
5
  }
5
6
  interface ConvertOpts extends CLIOpts {
6
7
  numBlocks?: number;
7
8
  blockSize: number;
9
+ exclude?: string[];
8
10
  include?: string[];
9
11
  out: string;
10
12
  }
package/cli.js CHANGED
@@ -28,25 +28,30 @@ const ARG_BLOCKSIZE = {
28
28
  }
29
29
  })
30
30
  };
31
- const collectFiles = (ctx) => {
32
- const root = resolve(ctx.inputs[0]);
31
+ const collectFiles = ({
32
+ opts: { include, exclude },
33
+ inputs
34
+ }) => {
35
+ const root = resolve(inputs[0]);
33
36
  const filtered = [];
34
37
  const dirs = /* @__PURE__ */ new Set();
38
+ const $include = include?.map((x) => new RegExp(x));
39
+ const $exclude = exclude?.map((x) => new RegExp(x));
35
40
  let total = 0;
36
41
  for (let f of files(root)) {
37
42
  const stats = statSync(f);
38
- if (stats.isFile()) {
39
- const dest = relative(root, f);
40
- filtered.push({
41
- src: f,
42
- dest,
43
- size: stats.size,
44
- ctime: stats.ctimeMs,
45
- mtime: stats.mtimeMs
46
- });
47
- dirs.add(dirname(dest));
48
- total += stats.size;
49
- }
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;
50
55
  }
51
56
  return { files: filtered, dirs: [...dirs], size: total };
52
57
  };
@@ -77,9 +82,14 @@ const CONVERT = {
77
82
  desc: "Output file path",
78
83
  optional: false
79
84
  }),
85
+ exclude: strings({
86
+ alias: "e",
87
+ desc: "File exclusion regexp",
88
+ hint: "EXT"
89
+ }),
80
90
  include: strings({
81
91
  alias: "i",
82
- desc: "Only include file extensions",
92
+ desc: "File inclusion regexp",
83
93
  hint: "EXT"
84
94
  })
85
95
  },
@@ -102,7 +112,7 @@ const CONVERT = {
102
112
  const bfs = new BlockFS(storage, { logger: ctx.logger });
103
113
  await bfs.init();
104
114
  for (let f of collected.files) {
105
- ctx.logger.debug("writing file:", f.dest);
115
+ ctx.logger.info("writing file:", f.dest);
106
116
  await bfs.writeFile(f.dest, readBinary(f.src));
107
117
  const entry = await bfs.entryForPath(f.dest);
108
118
  entry.ctime = f.ctime;
@@ -179,7 +189,11 @@ cliApp({
179
189
  opts: {
180
190
  verbose: flag({
181
191
  alias: "v",
182
- desc: "Display extra process information"
192
+ desc: "Display extra logging information"
193
+ }),
194
+ quiet: flag({
195
+ alias: "q",
196
+ desc: "Disable logging"
183
197
  })
184
198
  },
185
199
  commands: {
@@ -188,7 +202,8 @@ cliApp({
188
202
  },
189
203
  name: "blockfs",
190
204
  ctx: async (ctx) => {
191
- if (ctx.opts.verbose) ctx.logger.level = LogLevel.DEBUG;
205
+ if (ctx.opts.quiet) ctx.logger.level = LogLevel.NONE;
206
+ else if (ctx.opts.verbose) ctx.logger.level = LogLevel.DEBUG;
192
207
  return ctx;
193
208
  },
194
209
  start: 3,
package/entry.d.ts CHANGED
@@ -20,16 +20,16 @@ export declare class Entry implements IEntry {
20
20
  set owner(owner: number);
21
21
  get name(): string;
22
22
  set name(name: string);
23
+ get start(): number;
24
+ set start(block: number);
25
+ get end(): number;
26
+ set end(block: number);
23
27
  get size(): bigint;
24
28
  set size(size: bigint);
25
29
  get ctime(): number;
26
30
  set ctime(epoch: number);
27
31
  get mtime(): number;
28
32
  set mtime(epoch: number);
29
- get start(): number;
30
- set start(block: number);
31
- get end(): number;
32
- set end(block: number);
33
33
  set(spec: EntrySpec): void;
34
34
  release(): void;
35
35
  save(): Promise<void>;
package/entry.js CHANGED
@@ -66,35 +66,35 @@ class Entry {
66
66
  this.data.subarray(offset, offset + Entry.NAME_MAX_LENGTH)
67
67
  );
68
68
  }
69
+ get start() {
70
+ return this.view.getUint32(32, true);
71
+ }
72
+ set start(block) {
73
+ this.view.setUint32(32, block, true);
74
+ }
75
+ get end() {
76
+ return this.view.getUint32(36, true);
77
+ }
78
+ set end(block) {
79
+ this.view.setUint32(36, block, true);
80
+ }
69
81
  get size() {
70
- return this.view.getBigUint64(32, true);
82
+ return this.view.getBigUint64(40, true);
71
83
  }
72
84
  set size(size) {
73
- this.view.setBigUint64(32, size, true);
85
+ this.view.setBigUint64(40, size, true);
74
86
  }
75
87
  get ctime() {
76
- return Number(this.view.getBigUint64(40, true));
88
+ return Number(this.view.getBigUint64(48, true));
77
89
  }
78
90
  set ctime(epoch) {
79
- this.view.setBigUint64(40, BigInt(epoch), true);
91
+ this.view.setBigUint64(48, BigInt(epoch), true);
80
92
  }
81
93
  get mtime() {
82
- return Number(this.view.getBigUint64(48, true));
94
+ return Number(this.view.getBigUint64(56, true));
83
95
  }
84
96
  set mtime(epoch) {
85
- this.view.setBigUint64(48, BigInt(epoch), true);
86
- }
87
- get start() {
88
- return this.view.getUint32(56, true);
89
- }
90
- set start(block) {
91
- this.view.setUint32(56, block, true);
92
- }
93
- get end() {
94
- return this.view.getUint32(60, true);
95
- }
96
- set end(block) {
97
- this.view.setUint32(60, block, true);
97
+ this.view.setBigUint64(56, BigInt(epoch), true);
98
98
  }
99
99
  set(spec) {
100
100
  this.type = spec.type;
package/fs.d.ts CHANGED
@@ -160,6 +160,10 @@ export declare class BlockFS {
160
160
  * object URL, optionally typed with given MIME type.
161
161
  *
162
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
163
167
  * Reference:
164
168
  *
165
169
  * - https://developer.mozilla.org/en-US/docs/Web/API/Blob#creating_a_url_representing_the_contents_of_a_typed_array
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";
@@ -289,6 +290,10 @@ class BlockFS {
289
290
  * object URL, optionally typed with given MIME type.
290
291
  *
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
292
297
  * Reference:
293
298
  *
294
299
  * - https://developer.mozilla.org/en-US/docs/Web/API/Blob#creating_a_url_representing_the_contents_of_a_typed_array
@@ -298,7 +303,9 @@ class BlockFS {
298
303
  */
299
304
  async readAsObjectURL(path, type) {
300
305
  return URL.createObjectURL(
301
- new Blob([await this.readFile(path)], { type })
306
+ new Blob([await this.readFile(path)], {
307
+ type: type ?? (isString(path) ? preferredTypeForPath(path) : void 0)
308
+ })
302
309
  );
303
310
  }
304
311
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thi.ng/block-fs",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Customizable block-based storage, adapters & file system layer",
5
5
  "type": "module",
6
6
  "module": "./index.js",
@@ -42,32 +42,35 @@
42
42
  "tool:tangle": "../../node_modules/.bin/tangle src/**/*.ts"
43
43
  },
44
44
  "dependencies": {
45
- "@thi.ng/api": "^8.11.25",
46
- "@thi.ng/args": "^2.3.66",
47
- "@thi.ng/binary": "^3.4.48",
48
- "@thi.ng/bitfield": "^2.4.0",
49
- "@thi.ng/checks": "^3.7.5",
50
- "@thi.ng/errors": "^2.5.31",
51
- "@thi.ng/file-io": "^2.1.34",
52
- "@thi.ng/logger": "^3.1.6",
53
- "@thi.ng/random": "^4.1.16",
54
- "@thi.ng/strings": "^3.9.10"
45
+ "@thi.ng/api": "^8.11.26",
46
+ "@thi.ng/args": "^2.3.67",
47
+ "@thi.ng/binary": "^3.4.49",
48
+ "@thi.ng/bitfield": "^2.4.1",
49
+ "@thi.ng/checks": "^3.7.6",
50
+ "@thi.ng/errors": "^2.5.32",
51
+ "@thi.ng/file-io": "^2.1.35",
52
+ "@thi.ng/logger": "^3.1.7",
53
+ "@thi.ng/mime": "^2.7.8",
54
+ "@thi.ng/random": "^4.1.17",
55
+ "@thi.ng/strings": "^3.9.11"
55
56
  },
56
57
  "devDependencies": {
57
- "@types/node": "^22.13.14",
58
+ "@types/node": "^22.14.1",
58
59
  "esbuild": "^0.25.2",
59
- "typedoc": "^0.28.1",
60
- "typescript": "^5.8.2"
60
+ "typedoc": "^0.28.2",
61
+ "typescript": "^5.8.3"
61
62
  },
62
63
  "keywords": [
64
+ "async",
63
65
  "binary",
64
66
  "block",
65
67
  "cli",
66
68
  "conversion",
67
69
  "file",
68
- "file-system",
70
+ "filesystem",
69
71
  "memory",
70
72
  "memory-mapped",
73
+ "mime",
71
74
  "nodejs",
72
75
  "path",
73
76
  "storage",
@@ -129,5 +132,5 @@
129
132
  "status": "alpha",
130
133
  "year": 2024
131
134
  },
132
- "gitHead": "549e798e6a82254ee68727f12229c5252892b8f1\n"
135
+ "gitHead": "c464b6948f92cba90c2ea75b59203dad894fb450\n"
133
136
  }
@@ -16,7 +16,7 @@ export interface MemoryBlockStorageOpts extends BlockStorageOpts {
16
16
  * Optional, pre-defined/loaded byte buffer. Must have at least `numBlocks *
17
17
  * blockSize` capacity.
18
18
  */
19
- buffer?: Uint8Array;
19
+ buffer?: Uint8Array | ArrayBufferLike;
20
20
  }
21
21
  export declare class MemoryBlockStorage extends ABlockStorage<MemoryBlock> {
22
22
  buffer: Uint8Array;
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 {
@@ -27,12 +28,13 @@ class MemoryBlockStorage extends ABlockStorage {
27
28
  constructor(opts) {
28
29
  super(opts);
29
30
  const size = this.numBlocks * this.blockSize;
30
- if (opts.buffer && opts.buffer.length < size) {
31
+ const buffer = opts.buffer ? isArrayBufferLike(opts.buffer) ? new Uint8Array(opts.buffer) : opts.buffer : void 0;
32
+ if (buffer && buffer.length < size) {
31
33
  illegalArgs(
32
34
  `given buffer is too small, expected at least ${size} bytes`
33
35
  );
34
36
  }
35
- this.buffer = opts.buffer ?? new Uint8Array(size);
37
+ this.buffer = buffer ?? new Uint8Array(size);
36
38
  }
37
39
  async hasBlock(id) {
38
40
  this.ensureValidID(id);