@thi.ng/block-fs 0.1.0 → 0.3.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-01T21:42:04Z
3
+ - **Last updated**: 2025-04-02T18:47:59Z
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,42 @@ 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.3.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/block-fs@0.3.0) (2025-04-02)
15
+
16
+ #### 🚀 Features
17
+
18
+ - add `BlockFS.readAsObjectURL()` ([551327c](https://github.com/thi-ng/umbrella/commit/551327c))
19
+ - add mem storage opts, add logging ([27016d6](https://github.com/thi-ng/umbrella/commit/27016d6))
20
+ - add `MemoryBlockStorage` support for pre-loaded buffers
21
+ - add logging
22
+ - add docs
23
+ - add CLI app wrapper ([68abe74](https://github.com/thi-ng/umbrella/commit/68abe74))
24
+ - add CLI app wrapper with these commands:
25
+ - `convert`: convert file tree into single BlockFS blob
26
+ - `list`: list file tree of a BlockFS blob
27
+ - update deps
28
+ - improve tree display (`list` cmd) ([a23866c](https://github.com/thi-ng/umbrella/commit/a23866c))
29
+
30
+ #### 🩹 Bug fixes
31
+
32
+ - fix parent dir linkage ([a121e76](https://github.com/thi-ng/umbrella/commit/a121e76))
33
+
34
+ #### ♻️ Refactoring
35
+
36
+ - update sentinel block ID ([51a1e44](https://github.com/thi-ng/umbrella/commit/51a1e44))
37
+
38
+ ## [0.2.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/block-fs@0.2.0) (2025-04-02)
39
+
40
+ #### 🚀 Features
41
+
42
+ - add path separator option, various refactoring ([875465e](https://github.com/thi-ng/umbrella/commit/875465e))
43
+ - add `BlockFSOpts.separator`
44
+ - rename `.readFileRaw()` => `.readBlocks()`
45
+ - rename `.writeFileRaw()` => `.writeBlocks()`
46
+ - add additional internal safety checks
47
+ - internal refactoring (`this` destructuring)
48
+ - add docs
49
+
14
50
  ## [0.1.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/block-fs@0.1.0) (2025-04-01)
15
51
 
16
52
  #### 🚀 Features
package/README.md CHANGED
@@ -21,6 +21,9 @@
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)
@@ -53,6 +56,7 @@ The package also provides an hierarchical filesystem layer with pluggable
53
56
  storage providers and other customizable aspects. The default implementation
54
57
  supports:
55
58
 
59
+ - 8 - 32bit block IDs
56
60
  - arbitrarily nested directories
57
61
  - filenames of max. 31 bytes (UTF-8) per directory level
58
62
  - max. 32 owner IDs
@@ -73,6 +77,9 @@ The filesystem stores a [bitfield](https://thi.ng/bitfield) of block allocations
73
77
  in the first N blocks. The number of blocks used depends on configured block
74
78
  size and the max. number of blocks in the storage backend.
75
79
 
80
+ Blocks can be reserved for custom purposes by calling
81
+ [`.allocateBlocks()`](https://docs.thi.ng/umbrella/block-fs/classes/BlockFS.html#allocateblocks).
82
+
76
83
  #### Root directory
77
84
 
78
85
  The root directory starts in block N, directly after the block allocation table.
@@ -98,6 +105,121 @@ the max. number of blocks in the storage backend.
98
105
 
99
106
  TODO diagram
100
107
 
108
+ ### Command line app
109
+
110
+ The package includes a mult-command CLI app with the following operations:
111
+
112
+ #### Convert file tree into single BlockFS blob
113
+
114
+ The `convert` command is used to bundle an entire file tree from the host system
115
+ into a single binary blob based on `BlockFS` with configured block size. The
116
+ file tree MUST fit into the RAM available to `bun` (or `node`).
117
+
118
+ Once bundled, the binary blob can then be used together with
119
+ [`MemoryBlockStorage`](https://docs.thi.ng/umbrella/block-fs/classes/MemoryBlockStorage.html)
120
+ and [`BlockFS`](https://docs.thi.ng/umbrella/block-fs/classes/BlockFS.html) for
121
+ other purposes (e.g. distributed with your web app to provide a virtual
122
+ filesystem).
123
+
124
+ Example usage to bundle the source directory of this package:
125
+
126
+ ```bash
127
+ npx @thi.ng/block-fs convert -o dummy.dat packages/block-fs/src/
128
+
129
+ # [INFO] blockfs: number of files: 11
130
+ # [INFO] blockfs: number of directories: 2
131
+ # [INFO] blockfs: total file size: 40341
132
+ # [INFO] blockfs: number of blocks: 56
133
+ # [INFO] blockfs: writing file: dummy.dat
134
+ ```
135
+
136
+ General usage:
137
+
138
+ ```text
139
+ npx @thi.ng/block-fs convert --help
140
+
141
+ Usage: blockfs <cmd> [opts] input
142
+
143
+ Flags:
144
+
145
+ -v, --verbose Display extra process information
146
+
147
+ Main:
148
+
149
+ -bs BYTES, --block-size BYTES Block size (default: 1024)
150
+ -n INT, --num-blocks INT Number of blocks (multiple of 8)
151
+ -o STR, --out STR [required] Output file path
152
+ ```
153
+
154
+ #### List file tree of a BlockFS blob
155
+
156
+ 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.
157
+
158
+ ```bash
159
+ npx @thi.ng/block-fs list dummy.dat
160
+ # /api.ts
161
+ # /cli.ts
162
+ # /directory.ts
163
+ # /entry.ts
164
+ # /fs.ts
165
+ # /index.ts
166
+ # /lock.ts
167
+ # /storage
168
+ # /storage/astorage.ts
169
+ # /storage/file.ts
170
+ # /storage/memory.ts
171
+ # /utils.ts
172
+
173
+ npx @thi.ng/block-fs list --tree dummy.dat
174
+ # ├── api.ts
175
+ # ├── cli.ts
176
+ # ├── directory.ts
177
+ # ├── entry.ts
178
+ # ├── fs.ts
179
+ # ├── index.ts
180
+ # ├── lock.ts
181
+ # ├── storage
182
+ # │ ├── astorage.ts
183
+ # │ ├── file.ts
184
+ # │ └── memory.ts
185
+ # └── utils.ts
186
+
187
+ # display file sizes & modification times
188
+ npx @thi.ng/block-fs list --tree --all dummy.dat
189
+ # ├── api.ts 2204 2025-04-02T10:22:55.573Z
190
+ # ├── cli.ts 6799 2025-04-02T18:07:58.895Z
191
+ # ├── directory.ts 3994 2025-04-02T13:47:00.108Z
192
+ # ├── entry.ts 4130 2025-04-02T10:22:55.574Z
193
+ # ├── fs.ts 16377 2025-04-02T13:46:36.608Z
194
+ # ├── index.ts 317 2025-04-01T21:38:08.232Z
195
+ # ├── lock.ts 1501 2025-04-01T21:38:08.232Z
196
+ # ├── storage 2025-04-02T18:33:47.389Z
197
+ # │ ├── astorage.ts 1205 2025-04-02T10:22:55.574Z
198
+ # │ ├── file.ts 1780 2025-04-02T14:25:12.461Z
199
+ # │ └── memory.ts 1802 2025-04-02T14:26:02.163Z
200
+ # └── utils.ts 418 2025-04-02T10:22:55.574Z
201
+ ```
202
+
203
+ General usage:
204
+
205
+ ```text
206
+ npx @thi.ng/block-fs list --help
207
+
208
+ Usage: blockfs <cmd> [opts] input
209
+
210
+ Flags:
211
+
212
+ -a, --all Display all attribs
213
+ -t, --tree List files as tree
214
+ -v, --verbose Display extra process information
215
+ -m, --with-mtime Display modified times
216
+ -s, --with-size Display file sizes
217
+
218
+ Main:
219
+
220
+ -bs BYTES, --block-size BYTES Block size (default: 1024)
221
+ ```
222
+
101
223
  ## Status
102
224
 
103
225
  **ALPHA** - bleeding edge / work-in-progress
@@ -130,11 +252,12 @@ For Node.js REPL:
130
252
  const bf = await import("@thi.ng/block-fs");
131
253
  ```
132
254
 
133
- Package sizes (brotli'd, pre-treeshake): ESM: 4.11 KB
255
+ Package sizes (brotli'd, pre-treeshake): ESM: 4.43 KB
134
256
 
135
257
  ## Dependencies
136
258
 
137
259
  - [@thi.ng/api](https://github.com/thi-ng/umbrella/tree/develop/packages/api)
260
+ - [@thi.ng/args](https://github.com/thi-ng/umbrella/tree/develop/packages/args)
138
261
  - [@thi.ng/binary](https://github.com/thi-ng/umbrella/tree/develop/packages/binary)
139
262
  - [@thi.ng/bitfield](https://github.com/thi-ng/umbrella/tree/develop/packages/bitfield)
140
263
  - [@thi.ng/checks](https://github.com/thi-ng/umbrella/tree/develop/packages/checks)
@@ -198,7 +321,10 @@ console.log(await fs.readFile("/deeply/nested/paths/are-ok"));
198
321
 
199
322
  // iterate all files & directory entries in root dir
200
323
  for await (let entry of fs.root.tree()) {
201
- console.log(entry.type, entry.path, entry.size, new Date(entry.ctime));
324
+ // entry.path is absolute path
325
+ // entry.size is always a bigint
326
+ // entry.ctime/mtime is UNIX epoch
327
+ console.log(entry.path, entry.size, new Date(entry.ctime));
202
328
  }
203
329
 
204
330
  // /hello 0n 2025-04-01T20:18:55.916Z
package/api.d.ts CHANGED
@@ -18,18 +18,35 @@ export interface IBlockStorage {
18
18
  }
19
19
  export interface BlockStorageOpts {
20
20
  logger?: ILogger;
21
+ /**
22
+ * Max. number of blocks. Will be rounded up to a multiple of 8.
23
+ */
21
24
  numBlocks: number;
25
+ /**
26
+ * Block size in bytes. Must be a power of 2. For usage with
27
+ * {@link BlockFS}, requires a minimum size of 128 bytes (though in practice
28
+ * should be larger to minimize overhead per block).
29
+ */
22
30
  blockSize: Pow2;
23
31
  }
24
32
  export interface EntrySpec {
33
+ /** Entry type (file or directory) */
25
34
  type: EntryType;
35
+ /** TODO currently still unused */
26
36
  locked?: boolean;
37
+ /** Owner ID */
27
38
  owner?: number;
39
+ /** Entry name */
28
40
  name: string;
41
+ /** Only used for files. File size as bigint */
29
42
  size?: bigint;
43
+ /** Entry creation timestamp */
30
44
  ctime?: number;
45
+ /** Entry modification timestamp */
31
46
  mtime?: number;
47
+ /** Data start block ID */
32
48
  start: number;
49
+ /** Data end block ID */
33
50
  end?: number;
34
51
  }
35
52
  export declare enum EntryType {
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,23 @@
1
+ import { type Command, type CommandCtx } from "@thi.ng/args";
2
+ interface CLIOpts {
3
+ verbose: boolean;
4
+ }
5
+ interface ConvertOpts extends CLIOpts {
6
+ numBlocks?: number;
7
+ blockSize: number;
8
+ include?: string[];
9
+ out: string;
10
+ }
11
+ interface ListOpts extends CLIOpts {
12
+ blockSize: number;
13
+ all: boolean;
14
+ tree: boolean;
15
+ withMtime: boolean;
16
+ withSize: boolean;
17
+ }
18
+ export interface AppCtx<T extends CLIOpts> extends CommandCtx<T, CLIOpts> {
19
+ }
20
+ export declare const CONVERT: Command<ConvertOpts, CLIOpts, AppCtx<ConvertOpts>>;
21
+ export declare const LIST: Command<ListOpts, CLIOpts, AppCtx<ListOpts>>;
22
+ export {};
23
+ //# sourceMappingURL=cli.d.ts.map
package/cli.js ADDED
@@ -0,0 +1,214 @@
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 = (ctx) => {
32
+ const root = resolve(ctx.inputs[0]);
33
+ const filtered = [];
34
+ const dirs = /* @__PURE__ */ new Set();
35
+ let total = 0;
36
+ for (let f of files(root)) {
37
+ 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
+ }
50
+ }
51
+ return { files: filtered, dirs: [...dirs], size: total };
52
+ };
53
+ const computeBlockCount = (collected, blockSize, numBlocks) => {
54
+ let blocks = collected.dirs.length;
55
+ const blockIDBytes = numBlocks ? align(Math.ceil(Math.log2(numBlocks)), 8) >> 3 : align(Math.ceil(Math.log2(collected.size / blockSize + blocks)), 8) >> 3;
56
+ const blockDataSizeBytes = align(Math.ceil(Math.log2(blockSize)), 8) >> 3;
57
+ const blockDataSize = blockSize - blockIDBytes - blockDataSizeBytes;
58
+ blocks += Math.ceil(
59
+ (collected.files.length + collected.dirs.length) * Entry.SIZE / blockDataSize
60
+ );
61
+ for (let f of collected.files) {
62
+ blocks += Math.ceil(f.size / blockDataSize);
63
+ }
64
+ const blockIDBytes2 = align(Math.ceil(Math.log2(blocks)), 8) >> 3;
65
+ return blockIDBytes2 > blockIDBytes ? computeBlockCount(collected, blockSize, blocks) : blocks;
66
+ };
67
+ const CONVERT = {
68
+ opts: {
69
+ ...ARG_BLOCKSIZE,
70
+ numBlocks: int({
71
+ alias: "n",
72
+ desc: "Number of blocks (multiple of 8)",
73
+ optional: true
74
+ }),
75
+ out: string({
76
+ alias: "o",
77
+ desc: "Output file path",
78
+ optional: false
79
+ }),
80
+ include: strings({
81
+ alias: "i",
82
+ desc: "Only include file extensions",
83
+ hint: "EXT"
84
+ })
85
+ },
86
+ desc: "Convert file tree into single BlockFS blob",
87
+ fn: async (ctx) => {
88
+ const collected = collectFiles(ctx);
89
+ const numBlocks = align(
90
+ ctx.opts.numBlocks ?? computeBlockCount(collected, ctx.opts.blockSize),
91
+ 8
92
+ );
93
+ ctx.logger.info("number of files:", collected.files.length);
94
+ ctx.logger.info("number of directories:", collected.dirs.length);
95
+ ctx.logger.info("total file size:", collected.size);
96
+ ctx.logger.info("number of blocks:", numBlocks);
97
+ const storage = new MemoryBlockStorage({
98
+ numBlocks,
99
+ blockSize: ctx.opts.blockSize,
100
+ logger: ctx.logger
101
+ });
102
+ const bfs = new BlockFS(storage, { logger: ctx.logger });
103
+ await bfs.init();
104
+ for (let f of collected.files) {
105
+ ctx.logger.debug("writing file:", f.dest);
106
+ await bfs.writeFile(f.dest, readBinary(f.src));
107
+ const entry = await bfs.entryForPath(f.dest);
108
+ entry.ctime = f.ctime;
109
+ entry.mtime = f.mtime;
110
+ await entry.save();
111
+ }
112
+ writeFile(ctx.opts.out, storage.buffer, void 0, ctx.logger);
113
+ }
114
+ };
115
+ const LIST = {
116
+ opts: {
117
+ ...ARG_BLOCKSIZE,
118
+ all: flag({
119
+ alias: "a",
120
+ desc: "Display all attribs"
121
+ }),
122
+ tree: flag({
123
+ alias: "t",
124
+ desc: "List files as tree"
125
+ }),
126
+ withMtime: flag({
127
+ alias: "m",
128
+ desc: "Display modified times"
129
+ }),
130
+ withSize: flag({
131
+ alias: "s",
132
+ desc: "Display file sizes"
133
+ })
134
+ },
135
+ desc: "List file tree of a BlockFS blob",
136
+ fn: async (ctx) => {
137
+ if (ctx.opts.all) {
138
+ ctx.opts.withMtime = ctx.opts.withSize = true;
139
+ }
140
+ const buffer = readBinary(ctx.inputs[0]);
141
+ const storage = new MemoryBlockStorage({
142
+ numBlocks: buffer.length / ctx.opts.blockSize >>> 0,
143
+ blockSize: ctx.opts.blockSize,
144
+ logger: ctx.logger,
145
+ buffer
146
+ });
147
+ const bfs = new BlockFS(storage, { logger: ctx.logger });
148
+ await bfs.init();
149
+ const tree = [];
150
+ for await (let entry of bfs.root.tree()) {
151
+ const path = entry.path;
152
+ const depth = path.split("/").length - 2;
153
+ tree.push([path, depth, entry]);
154
+ }
155
+ tree.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
156
+ const rows = [];
157
+ const maxWidths = [0, 0, 0];
158
+ for (let i = 0, num = tree.length - 1; i <= num; i++) {
159
+ const f = tree[i];
160
+ const isLast = i === num || f[1] > tree[i + 1][1];
161
+ const row = ctx.opts.tree ? ["\u2502 ".repeat(f[1]) + (isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ") + f[2].name] : [f[0]];
162
+ if (ctx.opts.withSize) {
163
+ row.push(f[2].isDirectory() ? "" : String(Number(f[2].size)));
164
+ }
165
+ if (ctx.opts.withMtime) {
166
+ row.push(new Date(f[2].mtime).toISOString());
167
+ }
168
+ rows.push(row);
169
+ for (let i2 = 0; i2 < row.length; i2++) {
170
+ maxWidths[i2] = Math.max(maxWidths[i2], row[i2].length);
171
+ }
172
+ }
173
+ for (let row of rows) {
174
+ console.log(row.map((x, i) => x.padEnd(maxWidths[i])).join(" "));
175
+ }
176
+ }
177
+ };
178
+ cliApp({
179
+ opts: {
180
+ verbose: flag({
181
+ alias: "v",
182
+ desc: "Display extra process information"
183
+ })
184
+ },
185
+ commands: {
186
+ convert: CONVERT,
187
+ list: LIST
188
+ },
189
+ name: "blockfs",
190
+ ctx: async (ctx) => {
191
+ if (ctx.opts.verbose) ctx.logger.level = LogLevel.DEBUG;
192
+ return ctx;
193
+ },
194
+ start: 3,
195
+ usage: {
196
+ prefix: `
197
+ \u2588 \u2588 \u2588 \u2502
198
+ \u2588\u2588 \u2588 \u2502
199
+ \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2502 ${PKG.name} ${PKG.version}
200
+ \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2502 Block-based storage & file system layer
201
+ \u2588 \u2502
202
+ \u2588 \u2588 \u2502
203
+
204
+ Usage: blockfs <cmd> [opts] input [...]
205
+ blockfs <cmd> --help
206
+ `,
207
+ showGroupNames: true,
208
+ paramWidth: 32
209
+ }
210
+ });
211
+ export {
212
+ CONVERT,
213
+ LIST
214
+ };
package/directory.js CHANGED
@@ -30,18 +30,22 @@ class Directory {
30
30
  }
31
31
  }
32
32
  async traverse() {
33
+ const {
34
+ fs,
35
+ fs: { sentinelID, storage }
36
+ } = this;
33
37
  const blocks = [];
34
38
  const entries = [];
35
39
  let blockID = this.entry.start;
36
40
  while (true) {
37
41
  blocks.push(blockID);
38
- const block = await this.fs.storage.loadBlock(blockID);
39
- let { next, size } = this.fs.getBlockMeta(block);
40
- if (!next) next = this.fs.sentinelID;
42
+ const block = await storage.loadBlock(blockID);
43
+ let { next, size } = fs.getBlockMeta(block);
44
+ if (!next) next = sentinelID;
41
45
  for (let i = 0; i < size; i++) {
42
46
  entries.push(this.defEntry(i, blockID, block));
43
47
  }
44
- if (next === this.fs.sentinelID) break;
48
+ if (next === sentinelID) break;
45
49
  blockID = next;
46
50
  }
47
51
  return { blocks, entries };
@@ -52,16 +56,17 @@ class Directory {
52
56
  }
53
57
  }
54
58
  async mkdir(name) {
59
+ const fs = this.fs;
55
60
  const length = utf8Length(name);
56
- if (!length || length > this.fs.opts.entry.maxLength)
61
+ if (!length || length > fs.opts.entry.maxLength)
57
62
  illegalArgs(`invalid name: '${name}'`);
58
63
  const traversed = await this.traverse();
59
64
  this.ensureUniqueName(name, traversed.entries);
60
- const block = (await this.fs.allocateBlocks(1))[0];
61
- const data = await this.fs.storage.loadBlock(block);
62
- this.fs.setBlockMeta(data, this.fs.sentinelID, 0);
63
- this.fs.setBlockLink(data, this.entry.start, this.fs.dataStartBlockID);
64
- await this.fs.storage.saveBlock(block, data);
65
+ const block = (await fs.allocateBlocks(1))[0];
66
+ const data = await fs.storage.loadBlock(block);
67
+ fs.setBlockMeta(data, fs.sentinelID, 0);
68
+ fs.setBlockLink(data, this.entry.start, fs.blockDataOffset);
69
+ await fs.storage.saveBlock(block, data);
65
70
  return this.addEntry(
66
71
  {
67
72
  name,
package/entry.js CHANGED
@@ -124,7 +124,7 @@ class Entry {
124
124
  path.unshift(entry.name);
125
125
  entry = entry.parent?.entry;
126
126
  }
127
- return path.join("/");
127
+ return path.join(this.fs.opts.separator);
128
128
  }
129
129
  /**
130
130
  * Returns {@link IDirectory} wrapper for this entry (only if a directory,
package/fs.d.ts CHANGED
@@ -3,6 +3,15 @@ import type { ILogger } from "@thi.ng/logger";
3
3
  import { EntryType, type IBlockStorage, type IDirectory, type IEntry } from "./api.js";
4
4
  import { Lock } from "./lock.js";
5
5
  export interface BlockFSOpts {
6
+ /**
7
+ * Path separator.
8
+ *
9
+ * @defaultValue `/`
10
+ */
11
+ separator: string;
12
+ /**
13
+ * Logger instance to use.
14
+ */
6
15
  logger: ILogger;
7
16
  /**
8
17
  * Customizable {@link IDirectory} factory function. By default creates
@@ -66,16 +75,112 @@ export declare class BlockFS {
66
75
  root: IDirectory;
67
76
  constructor(storage: IBlockStorage, opts?: Partial<BlockFSOpts>);
68
77
  init(): Promise<this>;
78
+ /**
79
+ * Attempts to find the {@link IEntry} for given `path` and returns it if
80
+ * successful. Throws error if no such path exists.
81
+ *
82
+ * @remarks
83
+ * Also see {@link BlockFS.ensureEntryForPath}.
84
+ *
85
+ * @param path
86
+ */
69
87
  entryForPath(path: string): Promise<IEntry>;
88
+ /**
89
+ * Attempts to find or to create the {@link IEntry} for given `path` and
90
+ * entry type (file or directory) and then returns it if successful. Throws
91
+ * error if the path exists, but does not match the expected type or if the
92
+ * entry could not be created for other reasons.
93
+ *
94
+ * @remarks
95
+ * Also see {@link BlockFS.entryForPath}.
96
+ *
97
+ * @param path
98
+ * @param type
99
+ */
70
100
  ensureEntryForPath(path: string, type: EntryType): Promise<IEntry>;
101
+ /**
102
+ * Attempts to allocate a number of free blocks required for storing the
103
+ * given number of `bytes`. If successful, marks the blocks as used in the
104
+ * allocation table and then returns list of their IDs, otherwise throws an
105
+ * error if there're insufficient blocks available.
106
+ *
107
+ * @param bytes
108
+ */
71
109
  allocateBlocks(bytes: number): Promise<number[]>;
110
+ /**
111
+ * Marks the given block IDs as free/unused in the block allocation table
112
+ * and deletes/clears the blocks.
113
+ *
114
+ * @param ids
115
+ */
72
116
  freeBlocks(ids: number[]): Promise<void>;
117
+ /**
118
+ * Same as POSIX `mkdirp`. Attempts to create given directory, including any
119
+ * missing intermediate ones defined by `path`.
120
+ *
121
+ * @param path
122
+ */
73
123
  mkdir(path: string): Promise<IEntry>;
74
- readFileRaw(path: string | number): AsyncGenerator<Uint8Array<ArrayBufferLike>, void, unknown>;
124
+ /**
125
+ * Async iterator. Reads block list for given `path` (file path or start
126
+ * block ID) and yields data contents (byte arrays) of each block.
127
+ *
128
+ * @remarks
129
+ * Also see other read methods:
130
+ *
131
+ * - {@link BlockFS.readFile}
132
+ * - {@link BlockFS.readText}
133
+ * - {@link BlockFS.readJSON}
134
+ *
135
+ * @param path
136
+ */
137
+ readBlocks(path: string | number): AsyncGenerator<Uint8Array<ArrayBufferLike>, void, unknown>;
138
+ /**
139
+ * Fully reads given file into a single byte buffer and returns it.
140
+ *
141
+ * @param path
142
+ */
75
143
  readFile(path: string | number): Promise<Uint8Array<ArrayBuffer>>;
144
+ /**
145
+ * Fully reads given file into a single UTF-8 byte buffer, then decodes and
146
+ * returns it as string.
147
+ *
148
+ * @param path
149
+ */
76
150
  readText(path: string | number): Promise<string>;
77
- readJSON(path: string | number): Promise<any>;
78
- writeFileRaw(blocks: number[] | null, data: Uint8Array): Promise<{
151
+ /**
152
+ * Fully reads given file into a single UTF-8 byte buffer, then decodes it
153
+ * as JSON and returns result.
154
+ *
155
+ * @param path
156
+ */
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
+ * Reference:
164
+ *
165
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Blob#creating_a_url_representing_the_contents_of_a_typed_array
166
+ *
167
+ * @param path
168
+ * @param type
169
+ */
170
+ readAsObjectURL(path: string | number, type?: string): Promise<string>;
171
+ /**
172
+ * Takes an array of block IDs (or `null`) and a `data` byte array. Writes
173
+ * chunks of data into given blocks and connecting each block as linked
174
+ * list. Returns object of start/end block IDs and data size.
175
+ *
176
+ * @remarks
177
+ * If `blocks` is null, blocks are automatically allocated via
178
+ * {@link BlockFS.allocateBlocks}.
179
+ *
180
+ * @param blocks
181
+ * @param data
182
+ */
183
+ writeBlocks(blocks: number[] | null, data: Uint8Array): Promise<{
79
184
  start: number;
80
185
  end: number;
81
186
  size: number;
@@ -85,7 +190,7 @@ export declare class BlockFS {
85
190
  end: number;
86
191
  size: number;
87
192
  }>;
88
- appendFileRaw(blockID: number, data: Uint8Array): Promise<{
193
+ appendBlocks(blockID: number, data: Uint8Array): Promise<{
89
194
  start: number;
90
195
  end: number;
91
196
  size: number;
package/fs.js CHANGED
@@ -3,6 +3,7 @@ import { defBitField } from "@thi.ng/bitfield/bitfield";
3
3
  import { isString } from "@thi.ng/checks/is-string";
4
4
  import { assert } from "@thi.ng/errors/assert";
5
5
  import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
6
+ import { illegalState } from "@thi.ng/errors/illegal-state";
6
7
  import { NULL_LOGGER } from "@thi.ng/logger/null";
7
8
  import {
8
9
  EntryType
@@ -22,9 +23,10 @@ class BlockFS {
22
23
  constructor(storage, opts) {
23
24
  this.storage = storage;
24
25
  this.opts = {
25
- logger: NULL_LOGGER,
26
26
  directory: (fs, entry) => new Directory(fs, entry),
27
27
  entry: DEFAULT_ENTRY,
28
+ logger: NULL_LOGGER,
29
+ separator: "/",
28
30
  ...opts
29
31
  };
30
32
  assert(
@@ -39,7 +41,7 @@ class BlockFS {
39
41
  this.rootDirBlockID = align(this.blockIndex.data.length, storage.blockSize) / storage.blockSize;
40
42
  this.dataStartBlockID = this.rootDirBlockID + 1;
41
43
  this.dirDataOffset = this.blockDataOffset + this.blockIDBytes;
42
- this.sentinelID = storage.numBlocks - 1;
44
+ this.sentinelID = 2 ** (this.blockIDBytes * 8) - 1;
43
45
  this.tmp = new Uint8Array(storage.blockSize);
44
46
  }
45
47
  blockIndex;
@@ -67,60 +69,84 @@ class BlockFS {
67
69
  /** Root directory */
68
70
  root;
69
71
  async init() {
70
- const indexSize = this.blockIndex.data.length;
71
- const blockSize = this.storage.blockSize;
72
- for (let i = 0; i < this.rootDirBlockID; i++) {
73
- const data = await this.storage.loadBlock(i);
72
+ const { blockIndex, storage, opts, rootDirBlockID } = this;
73
+ const indexSize = blockIndex.data.length;
74
+ const blockSize = storage.blockSize;
75
+ for (let i = 0; i < rootDirBlockID; i++) {
76
+ const data = await storage.loadBlock(i);
74
77
  const offset = i * blockSize;
75
78
  if (offset + data.length > indexSize) {
76
- this.blockIndex.data.set(
79
+ blockIndex.data.set(
77
80
  data.subarray(0, indexSize - offset),
78
81
  offset
79
82
  );
80
83
  } else {
81
- this.blockIndex.data.set(data, offset);
84
+ blockIndex.data.set(data, offset);
82
85
  }
83
86
  }
84
- this.blockIndex.fill(1, 0, this.dataStartBlockID);
85
- const rootEntry = this.opts.entry.factory(
87
+ blockIndex.fill(1, 0, this.dataStartBlockID);
88
+ const rootEntry = opts.entry.factory(
86
89
  this,
87
90
  null,
88
- this.rootDirBlockID,
89
- new Uint8Array(this.opts.entry.size),
91
+ rootDirBlockID,
92
+ new Uint8Array(opts.entry.size),
90
93
  0
91
94
  );
92
95
  rootEntry.set({
93
96
  name: "",
94
97
  type: EntryType.DIR,
95
- start: this.rootDirBlockID,
98
+ start: rootDirBlockID,
96
99
  owner: 0
97
100
  });
98
- this.root = this.opts.directory(this, rootEntry);
101
+ this.root = opts.directory(this, rootEntry);
99
102
  return this;
100
103
  }
104
+ /**
105
+ * Attempts to find the {@link IEntry} for given `path` and returns it if
106
+ * successful. Throws error if no such path exists.
107
+ *
108
+ * @remarks
109
+ * Also see {@link BlockFS.ensureEntryForPath}.
110
+ *
111
+ * @param path
112
+ */
101
113
  async entryForPath(path) {
102
114
  let dir = this.root;
103
- if (path[0] === "/") path = path.substring(1);
115
+ const { directory, separator } = this.opts;
116
+ if (path[0] === separator) path = path.substring(1);
104
117
  if (path === "") return dir.entry;
105
- const $path = path.split("/");
106
- for (let i = 0; i < $path.length; i++) {
118
+ const $path = path.split(separator);
119
+ for (let i = 0, len = $path.length - 1; i <= len; i++) {
107
120
  let entry = await dir.findName($path[i]);
108
121
  if (!entry) break;
109
- if (i === $path.length - 1) return entry;
122
+ if (i === len) return entry;
110
123
  if (!entry.isDirectory()) illegalArgs(path);
111
- dir = this.opts.directory(this, entry);
124
+ dir = directory(this, entry);
112
125
  }
113
126
  illegalArgs(path);
114
127
  }
128
+ /**
129
+ * Attempts to find or to create the {@link IEntry} for given `path` and
130
+ * entry type (file or directory) and then returns it if successful. Throws
131
+ * error if the path exists, but does not match the expected type or if the
132
+ * entry could not be created for other reasons.
133
+ *
134
+ * @remarks
135
+ * Also see {@link BlockFS.entryForPath}.
136
+ *
137
+ * @param path
138
+ * @param type
139
+ */
115
140
  async ensureEntryForPath(path, type) {
116
141
  let dir = this.root;
117
- if (path[0] === "/") path = path.substring(1);
142
+ const { directory, separator } = this.opts;
143
+ if (path[0] === separator) path = path.substring(1);
118
144
  if (path === "") return dir.entry;
119
- const $path = path.split("/");
120
- for (let i = 0; i < $path.length; i++) {
145
+ const $path = path.split(separator);
146
+ for (let i = 0, len = $path.length - 1; i <= len; i++) {
121
147
  let entry = await dir.findName($path[i]);
122
148
  if (!entry) {
123
- if (i < $path.length - 1) {
149
+ if (i < len) {
124
150
  entry = await dir.mkdir($path[i]);
125
151
  } else {
126
152
  return await dir.addEntry({
@@ -130,7 +156,7 @@ class BlockFS {
130
156
  });
131
157
  }
132
158
  }
133
- if (i === $path.length - 1) {
159
+ if (i === len) {
134
160
  if (entry.type !== type)
135
161
  illegalArgs(
136
162
  `path exists, but is not a ${EntryType[type]}: ${path}`
@@ -138,32 +164,53 @@ class BlockFS {
138
164
  return entry;
139
165
  }
140
166
  if (!entry.isDirectory()) illegalArgs(path);
141
- dir = this.opts.directory(this, entry);
167
+ dir = directory(this, entry);
142
168
  }
143
169
  illegalArgs(path);
144
170
  }
171
+ /**
172
+ * Attempts to allocate a number of free blocks required for storing the
173
+ * given number of `bytes`. If successful, marks the blocks as used in the
174
+ * allocation table and then returns list of their IDs, otherwise throws an
175
+ * error if there're insufficient blocks available.
176
+ *
177
+ * @param bytes
178
+ */
145
179
  async allocateBlocks(bytes) {
146
- const lockID = await this.lock.acquire();
180
+ const {
181
+ blockDataSize,
182
+ blockIndex,
183
+ dataStartBlockID,
184
+ lock,
185
+ sentinelID
186
+ } = this;
187
+ const lockID = await lock.acquire();
147
188
  try {
148
189
  const ids = [];
149
- let last = this.dataStartBlockID;
190
+ let last = dataStartBlockID;
150
191
  while (bytes > 0) {
151
- const next = this.blockIndex.firstZero(last);
152
- if (next < 0 || next >= this.sentinelID) {
192
+ const next = blockIndex.firstZero(last);
193
+ if (next < 0 || next >= sentinelID) {
153
194
  throw new Error(
154
195
  `insufficient free blocks for storing ${bytes} bytes`
155
196
  );
156
197
  }
157
198
  ids.push(next);
158
199
  last = next + 1;
159
- bytes -= this.blockDataSize;
200
+ bytes -= blockDataSize;
160
201
  }
161
202
  await this.updateBlockIndex(ids, 1);
162
203
  return ids;
163
204
  } finally {
164
- await this.lock.release(lockID);
205
+ await lock.release(lockID);
165
206
  }
166
207
  }
208
+ /**
209
+ * Marks the given block IDs as free/unused in the block allocation table
210
+ * and deletes/clears the blocks.
211
+ *
212
+ * @param ids
213
+ */
167
214
  async freeBlocks(ids) {
168
215
  const lockID = await this.lock.acquire();
169
216
  try {
@@ -173,54 +220,116 @@ class BlockFS {
173
220
  await this.lock.release(lockID);
174
221
  }
175
222
  }
223
+ /**
224
+ * Same as POSIX `mkdirp`. Attempts to create given directory, including any
225
+ * missing intermediate ones defined by `path`.
226
+ *
227
+ * @param path
228
+ */
176
229
  async mkdir(path) {
177
230
  return this.ensureEntryForPath(path, EntryType.DIR);
178
231
  }
179
- async *readFileRaw(path) {
232
+ /**
233
+ * Async iterator. Reads block list for given `path` (file path or start
234
+ * block ID) and yields data contents (byte arrays) of each block.
235
+ *
236
+ * @remarks
237
+ * Also see other read methods:
238
+ *
239
+ * - {@link BlockFS.readFile}
240
+ * - {@link BlockFS.readText}
241
+ * - {@link BlockFS.readJSON}
242
+ *
243
+ * @param path
244
+ */
245
+ async *readBlocks(path) {
180
246
  let blockID = isString(path) ? (await this.entryForPath(path)).start : path;
247
+ const { blockDataOffset, blockIndex, sentinelID, storage } = this;
181
248
  while (true) {
182
- if (!this.blockIndex.at(blockID)) {
249
+ if (!blockIndex.at(blockID)) {
183
250
  throw new Error(`invalid block ref: ${blockID}`);
184
251
  }
185
- const bytes = await this.storage.loadBlock(blockID);
252
+ const bytes = await storage.loadBlock(blockID);
186
253
  const { next, size } = this.getBlockMeta(bytes);
187
- yield bytes.subarray(
188
- this.blockDataOffset,
189
- this.blockDataOffset + size
190
- );
191
- if (next === this.sentinelID) return;
254
+ yield bytes.subarray(blockDataOffset, blockDataOffset + size);
255
+ if (next === sentinelID) return;
192
256
  blockID = next;
193
257
  }
194
258
  }
259
+ /**
260
+ * Fully reads given file into a single byte buffer and returns it.
261
+ *
262
+ * @param path
263
+ */
195
264
  async readFile(path) {
196
265
  const buffer = [];
197
- for await (let block of this.readFileRaw(path)) buffer.push(...block);
266
+ for await (let block of this.readBlocks(path)) buffer.push(...block);
198
267
  return new Uint8Array(buffer);
199
268
  }
269
+ /**
270
+ * Fully reads given file into a single UTF-8 byte buffer, then decodes and
271
+ * returns it as string.
272
+ *
273
+ * @param path
274
+ */
200
275
  async readText(path) {
201
276
  return new TextDecoder().decode(await this.readFile(path));
202
277
  }
278
+ /**
279
+ * Fully reads given file into a single UTF-8 byte buffer, then decodes it
280
+ * as JSON and returns result.
281
+ *
282
+ * @param path
283
+ */
203
284
  async readJSON(path) {
204
285
  return JSON.parse(await this.readText(path));
205
286
  }
206
- async writeFileRaw(blocks, data) {
287
+ /**
288
+ * Fully reads given file into a single byte buffer and returns it as blob
289
+ * object URL, optionally typed with given MIME type.
290
+ *
291
+ * @remarks
292
+ * Reference:
293
+ *
294
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Blob#creating_a_url_representing_the_contents_of_a_typed_array
295
+ *
296
+ * @param path
297
+ * @param type
298
+ */
299
+ async readAsObjectURL(path, type) {
300
+ return URL.createObjectURL(
301
+ new Blob([await this.readFile(path)], { type })
302
+ );
303
+ }
304
+ /**
305
+ * Takes an array of block IDs (or `null`) and a `data` byte array. Writes
306
+ * chunks of data into given blocks and connecting each block as linked
307
+ * list. Returns object of start/end block IDs and data size.
308
+ *
309
+ * @remarks
310
+ * If `blocks` is null, blocks are automatically allocated via
311
+ * {@link BlockFS.allocateBlocks}.
312
+ *
313
+ * @param blocks
314
+ * @param data
315
+ */
316
+ async writeBlocks(blocks, data) {
317
+ const { blockDataOffset, blockDataSize, sentinelID, storage } = this;
207
318
  if (!blocks) blocks = await this.allocateBlocks(data.length);
208
319
  let offset = 0;
209
320
  for (let i = 0, numBlocks = blocks.length - 1; i <= numBlocks; i++) {
321
+ if (offset >= data.length)
322
+ illegalState(`too many blocks, EOF already reached`);
210
323
  const id = blocks[i];
211
- const block = await this.storage.loadBlock(id);
212
- i < numBlocks ? this.setBlockMeta(block, blocks[i + 1], this.blockDataSize) : this.setBlockMeta(
213
- block,
214
- this.sentinelID,
215
- data.length - offset
216
- );
217
- const chunk = data.subarray(offset, offset + this.blockDataSize);
218
- block.set(chunk, this.blockDataOffset);
219
- if (chunk.length < this.blockDataSize) {
220
- block.fill(0, this.blockDataOffset + chunk.length);
324
+ const block = await storage.loadBlock(id);
325
+ i < numBlocks ? this.setBlockMeta(block, blocks[i + 1], blockDataSize) : this.setBlockMeta(block, sentinelID, data.length - offset);
326
+ const chunk = data.subarray(offset, offset + blockDataSize);
327
+ block.set(chunk, blockDataOffset);
328
+ if (chunk.length < blockDataSize) {
329
+ block.fill(0, blockDataOffset + chunk.length);
221
330
  }
222
- await this.storage.saveBlock(id, block);
223
- offset += this.blockDataSize;
331
+ await storage.saveBlock(id, block);
332
+ offset += blockDataSize;
224
333
  }
225
334
  return {
226
335
  start: blocks[0],
@@ -230,7 +339,7 @@ class BlockFS {
230
339
  }
231
340
  async writeFile(path, data) {
232
341
  if (isString(data)) data = new TextEncoder().encode(data);
233
- if (!path) return this.writeFileRaw(null, data);
342
+ if (!path) return this.writeBlocks(null, data);
234
343
  const entry = await this.ensureEntryForPath(path, EntryType.FILE);
235
344
  let blocks = await this.blockList(entry.start);
236
345
  const overflow = data.length - blocks.length * this.blockDataSize;
@@ -241,37 +350,38 @@ class BlockFS {
241
350
  await this.freeBlocks(blocks.slice(needed));
242
351
  blocks = blocks.slice(0, needed);
243
352
  }
244
- blocks.sort();
353
+ blocks.sort((a, b) => a - b);
245
354
  entry.start = blocks[0];
246
355
  entry.end = blocks[blocks.length - 1];
247
356
  entry.size = BigInt(data.length);
248
357
  entry.mtime = Date.now();
249
358
  await entry.save();
250
- return this.writeFileRaw(blocks, data);
359
+ return this.writeBlocks(blocks, data);
251
360
  }
252
- async appendFileRaw(blockID, data) {
253
- if (blockID === this.sentinelID) return this.writeFileRaw(null, data);
361
+ async appendBlocks(blockID, data) {
362
+ if (blockID === this.sentinelID) return this.writeBlocks(null, data);
363
+ const { blockDataOffset, blockDataSize, storage } = this;
254
364
  const blocks = await this.blockList(blockID);
255
365
  const lastBlockID = blocks[blocks.length - 1];
256
- const lastBlock = await this.storage.loadBlock(lastBlockID);
366
+ const lastBlock = await storage.loadBlock(lastBlockID);
257
367
  const currLength = decodeBytes(
258
368
  lastBlock,
259
369
  this.blockIDBytes,
260
370
  this.blockDataSizeBytes
261
371
  );
262
- const remaining = this.blockDataSize - currLength;
263
- lastBlock.fill(0, 0, this.blockDataOffset);
372
+ const remaining = blockDataSize - currLength;
373
+ lastBlock.fill(0, 0, blockDataOffset);
264
374
  lastBlock.set(
265
375
  data.subarray(0, remaining),
266
- this.blockDataOffset + currLength
376
+ blockDataOffset + currLength
267
377
  );
268
378
  let newEndBlockID;
269
379
  if (data.length > remaining) {
270
- const { start, end } = await this.writeFileRaw(
380
+ const { start, end } = await this.writeBlocks(
271
381
  null,
272
382
  data.subarray(remaining)
273
383
  );
274
- this.setBlockMeta(lastBlock, start, this.blockDataSize);
384
+ this.setBlockMeta(lastBlock, start, blockDataSize);
275
385
  newEndBlockID = end;
276
386
  } else {
277
387
  this.setBlockMeta(
@@ -281,14 +391,14 @@ class BlockFS {
281
391
  );
282
392
  newEndBlockID = lastBlockID;
283
393
  }
284
- await this.storage.saveBlock(lastBlockID, lastBlock);
394
+ await storage.saveBlock(lastBlockID, lastBlock);
285
395
  return { start: blockID, end: newEndBlockID };
286
396
  }
287
397
  async appendFile(path, data) {
288
398
  if (isString(data)) data = new TextEncoder().encode(data);
289
- if (!isString(path)) return this.appendFileRaw(path, data);
399
+ if (!isString(path)) return this.appendBlocks(path, data);
290
400
  const entry = await this.ensureEntryForPath(path, EntryType.FILE);
291
- const { start, end } = await this.appendFileRaw(entry.end, data);
401
+ const { start, end } = await this.appendBlocks(entry.end, data);
292
402
  if (entry.start === this.sentinelID) entry.start = start;
293
403
  entry.end = end;
294
404
  entry.size += BigInt(data.length);
@@ -308,33 +418,35 @@ class BlockFS {
308
418
  }
309
419
  }
310
420
  async blockList(blockID) {
421
+ const { blockIDBytes, sentinelID, storage } = this;
311
422
  const blocks = [];
312
- if (blockID === this.sentinelID) return blocks;
423
+ if (blockID === sentinelID) return blocks;
313
424
  while (true) {
314
425
  blocks.push(blockID);
315
- const block = await this.storage.loadBlock(blockID);
316
- const nextID = decodeBytes(block, 0, this.blockIDBytes);
317
- if (nextID === this.sentinelID) break;
426
+ const block = await storage.loadBlock(blockID);
427
+ const nextID = decodeBytes(block, 0, blockIDBytes);
428
+ if (nextID === sentinelID) break;
318
429
  blockID = nextID;
319
430
  }
320
431
  return blocks;
321
432
  }
322
433
  async updateBlockIndex(ids, state) {
323
- const blockSize = this.storage.blockSize;
434
+ const { blockIndex, storage, tmp } = this;
435
+ const blockSize = storage.blockSize;
324
436
  const updatedBlocks = /* @__PURE__ */ new Set();
325
437
  for (let id of ids) {
326
- this.blockIndex.setAt(id, state);
438
+ blockIndex.setAt(id, state);
327
439
  updatedBlocks.add((id >>> 3) / blockSize | 0);
328
440
  }
329
441
  for (let id of updatedBlocks) {
330
442
  this.opts.logger.debug("update block index", id);
331
- const chunk = this.blockIndex.data.subarray(
443
+ const chunk = blockIndex.data.subarray(
332
444
  id * blockSize,
333
445
  (id + 1) * blockSize
334
446
  );
335
- this.tmp.set(chunk);
336
- if (chunk.length < blockSize) this.tmp.fill(0, chunk.length);
337
- await this.storage.saveBlock(id, this.tmp);
447
+ tmp.set(chunk);
448
+ if (chunk.length < blockSize) tmp.fill(0, chunk.length);
449
+ await storage.saveBlock(id, tmp);
338
450
  }
339
451
  }
340
452
  /** @internal */
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@thi.ng/block-fs",
3
- "version": "0.1.0",
3
+ "version": "0.3.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,6 +43,7 @@
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",
@@ -58,10 +62,13 @@
58
62
  "keywords": [
59
63
  "binary",
60
64
  "block",
65
+ "cli",
66
+ "conversion",
61
67
  "file",
62
68
  "file-system",
63
69
  "memory",
64
70
  "memory-mapped",
71
+ "nodejs",
65
72
  "path",
66
73
  "storage",
67
74
  "typedarray",
@@ -80,6 +87,7 @@
80
87
  "files": [
81
88
  "./*.js",
82
89
  "./*.d.ts",
90
+ "bin",
83
91
  "storage"
84
92
  ],
85
93
  "exports": {
@@ -89,6 +97,9 @@
89
97
  "./api": {
90
98
  "default": "./api.js"
91
99
  },
100
+ "./cli": {
101
+ "default": "./cli.js"
102
+ },
92
103
  "./directory": {
93
104
  "default": "./directory.js"
94
105
  },
@@ -118,5 +129,5 @@
118
129
  "status": "alpha",
119
130
  "year": 2024
120
131
  },
121
- "gitHead": "87aa2d0e64a357476c10fd57aabdfded13c79f7d\n"
132
+ "gitHead": "549e798e6a82254ee68727f12229c5252892b8f1\n"
122
133
  }
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;
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
@@ -6,16 +6,16 @@ class MemoryBlock {
6
6
  this.id = id;
7
7
  }
8
8
  async load() {
9
- const size = this.storage.blockSize;
10
- return this.storage.buffer.subarray(
11
- this.id * size,
12
- (this.id + 1) * size
13
- );
9
+ const { storage } = this;
10
+ storage.logger.debug("load block", this.id);
11
+ const size = storage.blockSize;
12
+ return storage.buffer.subarray(this.id * size, (this.id + 1) * size);
14
13
  }
15
14
  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);
15
+ const { storage } = this;
16
+ if (data.length !== storage.blockSize) illegalArgs(`wrong block size`);
17
+ storage.logger.debug("save block", this.id);
18
+ storage.buffer.set(data, this.id * storage.blockSize);
19
19
  }
20
20
  async delete() {
21
21
  const size = this.storage.blockSize;
@@ -26,7 +26,13 @@ class MemoryBlockStorage extends ABlockStorage {
26
26
  buffer;
27
27
  constructor(opts) {
28
28
  super(opts);
29
- this.buffer = new Uint8Array(this.numBlocks * this.blockSize);
29
+ const size = this.numBlocks * this.blockSize;
30
+ if (opts.buffer && opts.buffer.length < size) {
31
+ illegalArgs(
32
+ `given buffer is too small, expected at least ${size} bytes`
33
+ );
34
+ }
35
+ this.buffer = opts.buffer ?? new Uint8Array(size);
30
36
  }
31
37
  async hasBlock(id) {
32
38
  this.ensureValidID(id);