@zenfs/core 1.0.11 → 1.1.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/dist/backends/file_index.js +3 -3
- package/dist/backends/overlay.js +3 -3
- package/dist/backends/store/fs.d.ts +5 -5
- package/dist/backends/store/fs.js +5 -5
- package/dist/config.d.ts +8 -0
- package/dist/config.js +10 -0
- package/dist/devices.d.ts +158 -0
- package/dist/devices.js +423 -0
- package/dist/emulation/constants.d.ts +5 -0
- package/dist/emulation/constants.js +5 -0
- package/dist/emulation/promises.d.ts +5 -0
- package/dist/emulation/promises.js +31 -9
- package/dist/emulation/shared.js +3 -2
- package/dist/emulation/sync.d.ts +5 -0
- package/dist/emulation/sync.js +30 -8
- package/dist/file.js +1 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/inode.d.ts +0 -5
- package/dist/inode.js +0 -5
- package/dist/stats.js +1 -2
- package/dist/utils.d.ts +14 -4
- package/dist/utils.js +28 -6
- package/package.json +5 -1
- package/readme.md +58 -2
- package/src/backends/file_index.ts +3 -3
- package/src/backends/overlay.ts +3 -3
- package/src/backends/store/fs.ts +10 -9
- package/src/config.ts +22 -0
- package/src/devices.ts +469 -0
- package/src/emulation/constants.ts +6 -0
- package/src/emulation/promises.ts +41 -10
- package/src/emulation/shared.ts +3 -2
- package/src/emulation/sync.ts +33 -9
- package/src/file.ts +1 -2
- package/src/index.ts +2 -0
- package/src/inode.ts +0 -6
- package/src/stats.ts +1 -2
- package/src/utils.ts +33 -6
package/dist/utils.d.ts
CHANGED
|
@@ -17,20 +17,30 @@ export declare function mkdirpSync(path: string, mode: number, fs: FileSystem):
|
|
|
17
17
|
* @hidden
|
|
18
18
|
*/
|
|
19
19
|
export declare function levenshtein(a: string, b: string): number;
|
|
20
|
+
/** @hidden */
|
|
21
|
+
export declare const setImmediate: (callback: () => unknown) => void;
|
|
20
22
|
/**
|
|
21
|
-
*
|
|
23
|
+
* Encodes a string into a buffer
|
|
24
|
+
* @internal
|
|
22
25
|
*/
|
|
23
|
-
export declare
|
|
26
|
+
export declare function encodeRaw(input: string): Uint8Array;
|
|
27
|
+
/**
|
|
28
|
+
* Decodes a string from a buffer
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
export declare function decodeRaw(input?: Uint8Array): string;
|
|
24
32
|
/**
|
|
25
33
|
* Encodes a string into a buffer
|
|
26
34
|
* @internal
|
|
27
35
|
*/
|
|
28
|
-
export declare function
|
|
36
|
+
export declare function encodeUTF8(input: string): Uint8Array;
|
|
37
|
+
export { /** @deprecated @hidden */ encodeUTF8 as encode };
|
|
29
38
|
/**
|
|
30
39
|
* Decodes a string from a buffer
|
|
31
40
|
* @internal
|
|
32
41
|
*/
|
|
33
|
-
export declare function
|
|
42
|
+
export declare function decodeUTF8(input?: Uint8Array): string;
|
|
43
|
+
export { /** @deprecated @hidden */ decodeUTF8 as decode };
|
|
34
44
|
/**
|
|
35
45
|
* Decodes a directory listing
|
|
36
46
|
* @hidden
|
package/dist/utils.js
CHANGED
|
@@ -83,45 +83,67 @@ export function levenshtein(a, b) {
|
|
|
83
83
|
}
|
|
84
84
|
return dd;
|
|
85
85
|
}
|
|
86
|
+
/** @hidden */
|
|
87
|
+
export const setImmediate = typeof globalThis.setImmediate == 'function' ? globalThis.setImmediate : (cb) => setTimeout(cb, 0);
|
|
86
88
|
/**
|
|
87
|
-
*
|
|
89
|
+
* Encodes a string into a buffer
|
|
90
|
+
* @internal
|
|
88
91
|
*/
|
|
89
|
-
export
|
|
92
|
+
export function encodeRaw(input) {
|
|
93
|
+
if (typeof input != 'string') {
|
|
94
|
+
throw new ErrnoError(Errno.EINVAL, 'Can not encode a non-string');
|
|
95
|
+
}
|
|
96
|
+
return new Uint8Array(Array.from(input).map(char => char.charCodeAt(0)));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Decodes a string from a buffer
|
|
100
|
+
* @internal
|
|
101
|
+
*/
|
|
102
|
+
export function decodeRaw(input) {
|
|
103
|
+
if (!(input instanceof Uint8Array)) {
|
|
104
|
+
throw new ErrnoError(Errno.EINVAL, 'Can not decode a non-Uint8Array');
|
|
105
|
+
}
|
|
106
|
+
return Array.from(input)
|
|
107
|
+
.map(char => String.fromCharCode(char))
|
|
108
|
+
.join('');
|
|
109
|
+
}
|
|
90
110
|
const encoder = new TextEncoder();
|
|
91
111
|
/**
|
|
92
112
|
* Encodes a string into a buffer
|
|
93
113
|
* @internal
|
|
94
114
|
*/
|
|
95
|
-
export function
|
|
115
|
+
export function encodeUTF8(input) {
|
|
96
116
|
if (typeof input != 'string') {
|
|
97
117
|
throw new ErrnoError(Errno.EINVAL, 'Can not encode a non-string');
|
|
98
118
|
}
|
|
99
119
|
return encoder.encode(input);
|
|
100
120
|
}
|
|
121
|
+
export { /** @deprecated @hidden */ encodeUTF8 as encode };
|
|
101
122
|
const decoder = new TextDecoder();
|
|
102
123
|
/**
|
|
103
124
|
* Decodes a string from a buffer
|
|
104
125
|
* @internal
|
|
105
126
|
*/
|
|
106
|
-
export function
|
|
127
|
+
export function decodeUTF8(input) {
|
|
107
128
|
if (!(input instanceof Uint8Array)) {
|
|
108
129
|
throw new ErrnoError(Errno.EINVAL, 'Can not decode a non-Uint8Array');
|
|
109
130
|
}
|
|
110
131
|
return decoder.decode(input);
|
|
111
132
|
}
|
|
133
|
+
export { /** @deprecated @hidden */ decodeUTF8 as decode };
|
|
112
134
|
/**
|
|
113
135
|
* Decodes a directory listing
|
|
114
136
|
* @hidden
|
|
115
137
|
*/
|
|
116
138
|
export function decodeDirListing(data) {
|
|
117
|
-
return JSON.parse(
|
|
139
|
+
return JSON.parse(decodeUTF8(data), (k, v) => (k == '' ? v : BigInt(v)));
|
|
118
140
|
}
|
|
119
141
|
/**
|
|
120
142
|
* Encodes a directory listing
|
|
121
143
|
* @hidden
|
|
122
144
|
*/
|
|
123
145
|
export function encodeDirListing(data) {
|
|
124
|
-
return
|
|
146
|
+
return encodeUTF8(JSON.stringify(data, (k, v) => (k == '' ? v : v.toString())));
|
|
125
147
|
}
|
|
126
148
|
/**
|
|
127
149
|
* converts Date or number to a integer UNIX timestamp
|
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenfs/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "A filesystem, anywhere",
|
|
5
|
+
"funding": {
|
|
6
|
+
"type": "individual",
|
|
7
|
+
"url": "https://github.com/sponsors/james-pre"
|
|
8
|
+
},
|
|
5
9
|
"main": "dist/index.js",
|
|
6
10
|
"types": "dist/index.d.ts",
|
|
7
11
|
"keywords": [
|
package/readme.md
CHANGED
|
@@ -15,9 +15,11 @@ ZenFS is modular and extensible. The core includes some built-in backends:
|
|
|
15
15
|
|
|
16
16
|
ZenFS supports a number of other backends. Many are provided as separate packages under `@zenfs`. More backends can be defined by separate libraries by extending the `FileSystem` class and providing a `Backend` object.
|
|
17
17
|
|
|
18
|
+
You can find all of the packages available over at [zenfs.dev](https://zenfs.dev).
|
|
19
|
+
|
|
18
20
|
As an added bonus, all ZenFS backends support synchronous operations. All of the backends included with the core are cross-platform.
|
|
19
21
|
|
|
20
|
-
For more information, see the [docs](https://
|
|
22
|
+
For more information, see the [docs](https://zenfs.dev/core).
|
|
21
23
|
|
|
22
24
|
## Installing
|
|
23
25
|
|
|
@@ -143,8 +145,62 @@ fs.mount('/mnt/zip', zipfs);
|
|
|
143
145
|
fs.umount('/mnt/zip'); // finished using the zip
|
|
144
146
|
```
|
|
145
147
|
|
|
148
|
+
> [!CAUTION]
|
|
149
|
+
> Instances of backends follow the _internal_ API. You should never use a backend's methods unless you are extending a backend.
|
|
150
|
+
|
|
151
|
+
#### Devices and device files
|
|
152
|
+
|
|
146
153
|
> [!WARNING]
|
|
147
|
-
>
|
|
154
|
+
> This is an **experimental** feature. Breaking changes may occur during non-major releases. Using this feature is the fastest way to make it stable.
|
|
155
|
+
|
|
156
|
+
ZenFS includes experimental support for device files. These are designed to follow Linux's device file behavior, for consistency and ease of use. You can automatically add some normal devices with the `addDevices` configuration option:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
await configure({
|
|
160
|
+
mounts: {
|
|
161
|
+
/* ... */
|
|
162
|
+
},
|
|
163
|
+
addDevices: true,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
fs.writeFileSync('/dev/null', 'Some data to be discarded');
|
|
167
|
+
|
|
168
|
+
const randomData = new Unit8Array(100);
|
|
169
|
+
|
|
170
|
+
const random = fs.openSync('/dev/random', 'r');
|
|
171
|
+
fs.readSync(random, randomData);
|
|
172
|
+
fs.closeSync(random);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
You can create your own devices by implementing a `DeviceDriver`. For example, the null device looks similar to this:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const customNullDevice = {
|
|
179
|
+
name: 'custom_null',
|
|
180
|
+
isBuffered: false,
|
|
181
|
+
read() {
|
|
182
|
+
return 0;
|
|
183
|
+
},
|
|
184
|
+
write() {},
|
|
185
|
+
};
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Note the actual implementation's write is slightly more complicated since it adds to the file position. You can find more information on the docs.
|
|
189
|
+
|
|
190
|
+
Finally, if you'd like to use your custom device with the file system, you can use so through the aptly named `DeviceFS`.
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
const devfs = fs.mounts.get('/dev') as DeviceFS;
|
|
194
|
+
devfs.createDevice('/custom', customNullDevice);
|
|
195
|
+
|
|
196
|
+
fs.writeFileSync('/dev/custom', 'This gets discarded.');
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
In the above example, `createDevice` works relative to the `DeviceFS` mount point.
|
|
200
|
+
|
|
201
|
+
Additionally, a type assertion (` as ...`) is used since `fs.mounts` does not keep track of which file system type is mapped to which mount point. Doing so would create significant maintenance costs due to the complexity of implementing it.
|
|
202
|
+
|
|
203
|
+
If you would like to see a more intuitive way adding custom devices (e.g. `fs.mknod`), please feel free to open an issue for a feature request.
|
|
148
204
|
|
|
149
205
|
## Using with bundlers
|
|
150
206
|
|
|
@@ -8,7 +8,7 @@ import { FileSystem } from '../filesystem.js';
|
|
|
8
8
|
import { Readonly } from '../mixins/readonly.js';
|
|
9
9
|
import type { StatsLike } from '../stats.js';
|
|
10
10
|
import { Stats } from '../stats.js';
|
|
11
|
-
import {
|
|
11
|
+
import { decodeUTF8, encodeUTF8 } from '../utils.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* An Index in JSON form
|
|
@@ -83,7 +83,7 @@ export class Index extends Map<string, Stats> {
|
|
|
83
83
|
for (const [path, data] of Object.entries(json.entries)) {
|
|
84
84
|
const stats = new Stats(data);
|
|
85
85
|
if (stats.isDirectory()) {
|
|
86
|
-
stats.fileData =
|
|
86
|
+
stats.fileData = encodeUTF8(JSON.stringify(this.dirEntries(path)));
|
|
87
87
|
}
|
|
88
88
|
this.set(path, stats);
|
|
89
89
|
}
|
|
@@ -195,7 +195,7 @@ export abstract class IndexFS extends Readonly(FileSystem) {
|
|
|
195
195
|
throw ErrnoError.With('ENOTDIR', path, 'readdir');
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
const content: unknown = JSON.parse(
|
|
198
|
+
const content: unknown = JSON.parse(decodeUTF8(stats.fileData));
|
|
199
199
|
if (!Array.isArray(content)) {
|
|
200
200
|
throw ErrnoError.With('ENODATA', path, 'readdir');
|
|
201
201
|
}
|
package/src/backends/overlay.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { FileSystemMetadata } from '../filesystem.js';
|
|
|
6
6
|
import { FileSystem } from '../filesystem.js';
|
|
7
7
|
import { Mutexed } from '../mixins/mutexed.js';
|
|
8
8
|
import { Stats } from '../stats.js';
|
|
9
|
-
import {
|
|
9
|
+
import { decodeUTF8, encodeUTF8 } from '../utils.js';
|
|
10
10
|
import type { Backend } from './backend.js';
|
|
11
11
|
/** @internal */
|
|
12
12
|
const deletionLogPath = '/.deleted';
|
|
@@ -99,7 +99,7 @@ export class UnmutexedOverlayFS extends FileSystem {
|
|
|
99
99
|
const file = await this.writable.openFile(deletionLogPath, parseFlag('r'));
|
|
100
100
|
const { size } = await file.stat();
|
|
101
101
|
const { buffer } = await file.read(new Uint8Array(size));
|
|
102
|
-
this._deleteLog =
|
|
102
|
+
this._deleteLog = decodeUTF8(buffer);
|
|
103
103
|
} catch (err) {
|
|
104
104
|
if ((err as ErrnoError).errno !== Errno.ENOENT) {
|
|
105
105
|
throw err;
|
|
@@ -386,7 +386,7 @@ export class UnmutexedOverlayFS extends FileSystem {
|
|
|
386
386
|
this._deleteLogUpdatePending = true;
|
|
387
387
|
const log = await this.writable.openFile(deletionLogPath, parseFlag('w'));
|
|
388
388
|
try {
|
|
389
|
-
await log.write(
|
|
389
|
+
await log.write(encodeUTF8(this._deleteLog));
|
|
390
390
|
if (this._deleteLogUpdateNeeded) {
|
|
391
391
|
this._deleteLogUpdateNeeded = false;
|
|
392
392
|
await this.updateLog('');
|
package/src/backends/store/fs.ts
CHANGED
|
@@ -6,8 +6,9 @@ import { PreloadFile } from '../../file.js';
|
|
|
6
6
|
import { FileSystem, type FileSystemMetadata } from '../../filesystem.js';
|
|
7
7
|
import { type Ino, Inode, randomIno, rootIno } from '../../inode.js';
|
|
8
8
|
import type { FileType, Stats } from '../../stats.js';
|
|
9
|
-
import { decodeDirListing,
|
|
9
|
+
import { decodeDirListing, encodeUTF8, encodeDirListing } from '../../utils.js';
|
|
10
10
|
import type { Store, Transaction } from './store.js';
|
|
11
|
+
import type { File } from '../../file.js';
|
|
11
12
|
|
|
12
13
|
const maxInodeAllocTries = 5;
|
|
13
14
|
|
|
@@ -172,17 +173,17 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
|
|
|
172
173
|
return this.findINodeSync(tx, path).toStats();
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
public async createFile(path: string, flag: string, mode: number): Promise<
|
|
176
|
+
public async createFile(path: string, flag: string, mode: number): Promise<File> {
|
|
176
177
|
const node = await this.commitNew(path, S_IFREG, mode, new Uint8Array(0));
|
|
177
178
|
return new PreloadFile(this, path, flag, node.toStats(), new Uint8Array(0));
|
|
178
179
|
}
|
|
179
180
|
|
|
180
|
-
public createFileSync(path: string, flag: string, mode: number):
|
|
181
|
+
public createFileSync(path: string, flag: string, mode: number): File {
|
|
181
182
|
this.commitNewSync(path, S_IFREG, mode);
|
|
182
183
|
return this.openFileSync(path, flag);
|
|
183
184
|
}
|
|
184
185
|
|
|
185
|
-
public async openFile(path: string, flag: string): Promise<
|
|
186
|
+
public async openFile(path: string, flag: string): Promise<File> {
|
|
186
187
|
await using tx = this.store.transaction();
|
|
187
188
|
const node = await this.findINode(tx, path),
|
|
188
189
|
data = await tx.get(node.ino);
|
|
@@ -192,7 +193,7 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
|
|
|
192
193
|
return new PreloadFile(this, path, flag, node.toStats(), data);
|
|
193
194
|
}
|
|
194
195
|
|
|
195
|
-
public openFileSync(path: string, flag: string):
|
|
196
|
+
public openFileSync(path: string, flag: string): File {
|
|
196
197
|
using tx = this.store.transaction();
|
|
197
198
|
const node = this.findINodeSync(tx, path),
|
|
198
199
|
data = tx.getSync(node.ino);
|
|
@@ -225,11 +226,11 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
|
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
public async mkdir(path: string, mode: number): Promise<void> {
|
|
228
|
-
await this.commitNew(path, S_IFDIR, mode,
|
|
229
|
+
await this.commitNew(path, S_IFDIR, mode, encodeUTF8('{}'));
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
public mkdirSync(path: string, mode: number): void {
|
|
232
|
-
this.commitNewSync(path, S_IFDIR, mode,
|
|
233
|
+
this.commitNewSync(path, S_IFDIR, mode, encodeUTF8('{}'));
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
public async readdir(path: string): Promise<string[]> {
|
|
@@ -334,7 +335,7 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
|
|
|
334
335
|
const inode = new Inode();
|
|
335
336
|
inode.mode = 0o777 | S_IFDIR;
|
|
336
337
|
// If the root doesn't exist, the first random ID shouldn't exist either.
|
|
337
|
-
await tx.set(inode.ino,
|
|
338
|
+
await tx.set(inode.ino, encodeUTF8('{}'));
|
|
338
339
|
await tx.set(rootIno, inode.data);
|
|
339
340
|
await tx.commit();
|
|
340
341
|
}
|
|
@@ -351,7 +352,7 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
|
|
|
351
352
|
const inode = new Inode();
|
|
352
353
|
inode.mode = 0o777 | S_IFDIR;
|
|
353
354
|
// If the root doesn't exist, the first random ID shouldn't exist either.
|
|
354
|
-
tx.setSync(inode.ino,
|
|
355
|
+
tx.setSync(inode.ino, encodeUTF8('{}'));
|
|
355
356
|
tx.setSync(rootIno, inode.data);
|
|
356
357
|
tx.commitSync();
|
|
357
358
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Backend, BackendConfiguration, FilesystemOf, SharedConfig } from './backends/backend.js';
|
|
2
2
|
import { checkOptions, isBackend, isBackendConfig } from './backends/backend.js';
|
|
3
3
|
import { credentials } from './credentials.js';
|
|
4
|
+
import { DeviceFS, fullDevice, nullDevice, randomDevice, zeroDevice } from './devices.js';
|
|
4
5
|
import * as fs from './emulation/index.js';
|
|
5
6
|
import type { AbsolutePath } from './emulation/path.js';
|
|
6
7
|
import type { MountObject } from './emulation/shared.js';
|
|
@@ -77,14 +78,25 @@ export interface Configuration<T extends ConfigMounts> extends SharedConfig {
|
|
|
77
78
|
* An object mapping mount points to mount configuration
|
|
78
79
|
*/
|
|
79
80
|
mounts: { [K in keyof T & AbsolutePath]: MountConfiguration<T[K]> };
|
|
81
|
+
|
|
80
82
|
/**
|
|
81
83
|
* The uid to use
|
|
84
|
+
* @default 0
|
|
82
85
|
*/
|
|
83
86
|
uid: number;
|
|
87
|
+
|
|
84
88
|
/**
|
|
85
89
|
* The gid to use
|
|
90
|
+
* @default 0
|
|
86
91
|
*/
|
|
87
92
|
gid: number;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Whether to automatically add normal Linux devices
|
|
96
|
+
* @default false
|
|
97
|
+
* @experimental
|
|
98
|
+
*/
|
|
99
|
+
addDevices: boolean;
|
|
88
100
|
}
|
|
89
101
|
|
|
90
102
|
/**
|
|
@@ -110,6 +122,16 @@ export async function configure<T extends ConfigMounts>(configuration: Partial<C
|
|
|
110
122
|
|
|
111
123
|
Object.assign(credentials, { uid, gid, suid: uid, sgid: gid, euid: uid, egid: gid });
|
|
112
124
|
|
|
125
|
+
if (configuration.addDevices) {
|
|
126
|
+
const devfs = new DeviceFS();
|
|
127
|
+
devfs.createDevice('/null', nullDevice);
|
|
128
|
+
devfs.createDevice('/zero', zeroDevice);
|
|
129
|
+
devfs.createDevice('/full', fullDevice);
|
|
130
|
+
devfs.createDevice('/random', randomDevice);
|
|
131
|
+
await devfs.ready();
|
|
132
|
+
fs.mount('/dev', devfs);
|
|
133
|
+
}
|
|
134
|
+
|
|
113
135
|
if (!configuration.mounts) {
|
|
114
136
|
return;
|
|
115
137
|
}
|