@thi.ng/block-fs 0.3.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-02T18:47:59Z
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,20 @@ 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
+
14
28
  ## [0.3.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/block-fs@0.3.0) (2025-04-02)
15
29
 
16
30
  #### 🚀 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
 
@@ -119,7 +121,8 @@ Once bundled, the binary blob can then be used together with
119
121
  [`MemoryBlockStorage`](https://docs.thi.ng/umbrella/block-fs/classes/MemoryBlockStorage.html)
120
122
  and [`BlockFS`](https://docs.thi.ng/umbrella/block-fs/classes/BlockFS.html) for
121
123
  other purposes (e.g. distributed with your web app to provide a virtual
122
- filesystem).
124
+ filesystem). Also see [API example further
125
+ below](#working-with-a-converted-file-system-blob).
123
126
 
124
127
  Example usage to bundle the source directory of this package:
125
128
 
@@ -138,15 +141,31 @@ General usage:
138
141
  ```text
139
142
  npx @thi.ng/block-fs convert --help
140
143
 
141
- Usage: blockfs <cmd> [opts] input
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
142
158
 
143
159
  Flags:
144
160
 
145
- -v, --verbose Display extra process information
161
+ -q, --quiet Disable logging
162
+ -v, --verbose Display extra logging information
146
163
 
147
164
  Main:
148
165
 
149
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
150
169
  -n INT, --num-blocks INT Number of blocks (multiple of 8)
151
170
  -o STR, --out STR [required] Output file path
152
171
  ```
@@ -252,7 +271,7 @@ For Node.js REPL:
252
271
  const bf = await import("@thi.ng/block-fs");
253
272
  ```
254
273
 
255
- Package sizes (brotli'd, pre-treeshake): ESM: 4.43 KB
274
+ Package sizes (brotli'd, pre-treeshake): ESM: 4.50 KB
256
275
 
257
276
  ## Dependencies
258
277
 
@@ -264,6 +283,7 @@ Package sizes (brotli'd, pre-treeshake): ESM: 4.43 KB
264
283
  - [@thi.ng/errors](https://github.com/thi-ng/umbrella/tree/develop/packages/errors)
265
284
  - [@thi.ng/file-io](https://github.com/thi-ng/umbrella/tree/develop/packages/file-io)
266
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)
267
287
  - [@thi.ng/random](https://github.com/thi-ng/umbrella/tree/develop/packages/random)
268
288
  - [@thi.ng/strings](https://github.com/thi-ng/umbrella/tree/develop/packages/strings)
269
289
 
@@ -273,7 +293,9 @@ Note: @thi.ng/api is in _most_ cases a type-only import (not used at runtime)
273
293
 
274
294
  [Generated API docs](https://docs.thi.ng/umbrella/block-fs/)
275
295
 
276
- ```ts tangle:export/readme.ts
296
+ ### Basic usage
297
+
298
+ ```ts tangle:export/readme-1.ts
277
299
  import { BlockFS, MemoryBlockStorage } from "@thi.ng/block-fs";
278
300
 
279
301
  // create in-memory storage (64KB)
@@ -335,6 +357,45 @@ for await (let entry of fs.root.tree()) {
335
357
  // /deeply/nested/paths/are-ok 4n 2025-04-01T20:18:55.919Z
336
358
  ```
337
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
+
338
399
  ## Authors
339
400
 
340
401
  - [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/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.0",
4
4
  "description": "Customizable block-based storage, adapters & file system layer",
5
5
  "type": "module",
6
6
  "module": "./index.js",
@@ -50,6 +50,7 @@
50
50
  "@thi.ng/errors": "^2.5.31",
51
51
  "@thi.ng/file-io": "^2.1.34",
52
52
  "@thi.ng/logger": "^3.1.6",
53
+ "@thi.ng/mime": "^2.7.7",
53
54
  "@thi.ng/random": "^4.1.16",
54
55
  "@thi.ng/strings": "^3.9.10"
55
56
  },
@@ -60,14 +61,16 @@
60
61
  "typescript": "^5.8.2"
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": "c88a589f33207b02a43172313b38ea09571265f1\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);