@zenfs/core 1.2.7 → 1.2.9
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/backend.d.ts +2 -1
- package/dist/backends/fetch.d.ts +10 -3
- package/dist/backends/fetch.js +9 -15
- package/dist/backends/memory.d.ts +0 -1
- package/dist/backends/memory.js +1 -5
- package/dist/backends/port/fs.d.ts +0 -2
- package/dist/backends/port/fs.js +0 -2
- package/dist/backends/store/fs.js +10 -10
- package/dist/config.js +32 -9
- package/dist/credentials.d.ts +3 -0
- package/dist/credentials.js +3 -0
- package/dist/devices.d.ts +50 -13
- package/dist/devices.js +32 -6
- package/dist/emulation/promises.js +3 -13
- package/dist/emulation/shared.d.ts +4 -1
- package/dist/emulation/shared.js +5 -3
- package/dist/emulation/sync.js +3 -14
- package/dist/mixins/async.js +2 -2
- package/package.json +2 -1
- package/readme.md +4 -0
- package/src/backends/backend.ts +2 -1
- package/src/backends/fetch.ts +18 -18
- package/src/backends/memory.ts +1 -5
- package/src/backends/port/fs.ts +0 -2
- package/src/backends/store/fs.ts +18 -18
- package/src/config.ts +33 -10
- package/src/credentials.ts +3 -0
- package/src/devices.ts +75 -12
- package/src/emulation/promises.ts +3 -14
- package/src/emulation/shared.ts +5 -3
- package/src/emulation/sync.ts +3 -15
- package/src/mixins/async.ts +2 -2
- package/tests/fs/directory.test.ts +83 -1
- package/tests/fs/permissions.test.ts +45 -2
- package/tests/mounts.test.ts +18 -0
- package/tests/fs/chmod.test.ts +0 -44
- package/tests/fs/readdir.test.ts +0 -87
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenfs/core",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.9",
|
|
4
4
|
"description": "A filesystem, anywhere",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"lint": "eslint src tests",
|
|
59
59
|
"test:common": "tsx --test --experimental-test-coverage 'tests/**/!(fs)/*.test.ts' 'tests/*.test.ts'",
|
|
60
60
|
"test": "tsx --test --experimental-test-coverage",
|
|
61
|
+
"pretest": "npm run build",
|
|
61
62
|
"build": "tsc -p tsconfig.json",
|
|
62
63
|
"build:docs": "typedoc",
|
|
63
64
|
"dev": "npm run build -- --watch",
|
package/readme.md
CHANGED
|
@@ -201,6 +201,10 @@ If you would like to see a more intuitive way adding custom devices (e.g. `fs.mk
|
|
|
201
201
|
|
|
202
202
|
ZenFS exports a drop-in for Node's `fs` module (up to the version of `@types/node` in package.json), so you can use it for your bundler of preference using the default export.
|
|
203
203
|
|
|
204
|
+
## Sponsors
|
|
205
|
+
|
|
206
|
+
A huge thank you to [ Deco.cx](https://github.com/deco-cx) for sponsoring ZenFS and helping to make this possible.
|
|
207
|
+
|
|
204
208
|
## Building
|
|
205
209
|
|
|
206
210
|
- Make sure you have Node and NPM installed. You must have Node v18 or newer.
|
package/src/backends/backend.ts
CHANGED
|
@@ -17,8 +17,9 @@ export type OptionsConfig<T> = {
|
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Description of the option. Used in error messages and documentation.
|
|
20
|
+
* @deprecated
|
|
20
21
|
*/
|
|
21
|
-
description
|
|
22
|
+
description?: string;
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Whether or not the option is required (optional can be set to null or undefined). Defaults to false.
|
package/src/backends/fetch.ts
CHANGED
|
@@ -12,11 +12,11 @@ import type { IndexData } from './file_index.js';
|
|
|
12
12
|
* constants.
|
|
13
13
|
* @hidden
|
|
14
14
|
*/
|
|
15
|
-
async function fetchFile(path: string, type: 'buffer'): Promise<Uint8Array>;
|
|
16
|
-
async function fetchFile<T extends object>(path: string, type: 'json'): Promise<T>;
|
|
17
|
-
async function fetchFile<T extends object>(path: string, type: 'buffer' | 'json'): Promise<T | Uint8Array>;
|
|
18
|
-
async function fetchFile<T extends object>(path: string, type: string): Promise<T | Uint8Array> {
|
|
19
|
-
const response = await fetch(path).catch((e: Error) => {
|
|
15
|
+
async function fetchFile(path: string, type: 'buffer', init?: RequestInit): Promise<Uint8Array>;
|
|
16
|
+
async function fetchFile<T extends object>(path: string, type: 'json', init?: RequestInit): Promise<T>;
|
|
17
|
+
async function fetchFile<T extends object>(path: string, type: 'buffer' | 'json', init?: RequestInit): Promise<T | Uint8Array>;
|
|
18
|
+
async function fetchFile<T extends object>(path: string, type: string, init?: RequestInit): Promise<T | Uint8Array> {
|
|
19
|
+
const response = await fetch(path, init).catch((e: Error) => {
|
|
20
20
|
throw new ErrnoError(Errno.EIO, e.message, path);
|
|
21
21
|
});
|
|
22
22
|
if (!response.ok) {
|
|
@@ -42,6 +42,11 @@ async function fetchFile<T extends object>(path: string, type: string): Promise<
|
|
|
42
42
|
* Configuration options for FetchFS.
|
|
43
43
|
*/
|
|
44
44
|
export interface FetchOptions {
|
|
45
|
+
/**
|
|
46
|
+
* Options to pass through to fetch calls
|
|
47
|
+
*/
|
|
48
|
+
requestInit?: RequestInit;
|
|
49
|
+
|
|
45
50
|
/**
|
|
46
51
|
* URL to a file index as a JSON file or the file index object itself.
|
|
47
52
|
* Defaults to `index.json`.
|
|
@@ -75,6 +80,7 @@ export interface FetchOptions {
|
|
|
75
80
|
*/
|
|
76
81
|
export class FetchFS extends IndexFS {
|
|
77
82
|
public readonly baseUrl: string;
|
|
83
|
+
public readonly requestInit?: RequestInit;
|
|
78
84
|
|
|
79
85
|
public async ready(): Promise<void> {
|
|
80
86
|
if (this._isInitialized) {
|
|
@@ -94,15 +100,16 @@ export class FetchFS extends IndexFS {
|
|
|
94
100
|
}
|
|
95
101
|
}
|
|
96
102
|
|
|
97
|
-
public constructor({ index = 'index.json', baseUrl = '' }: FetchOptions) {
|
|
103
|
+
public constructor({ index = 'index.json', baseUrl = '', requestInit }: FetchOptions) {
|
|
98
104
|
// prefix url must end in a directory separator.
|
|
99
105
|
if (baseUrl.at(-1) != '/') {
|
|
100
106
|
baseUrl += '/';
|
|
101
107
|
}
|
|
102
108
|
|
|
103
|
-
super(typeof index != 'string' ? index : fetchFile<IndexData>(baseUrl + index, 'json'));
|
|
109
|
+
super(typeof index != 'string' ? index : fetchFile<IndexData>(baseUrl + index, 'json', requestInit));
|
|
104
110
|
|
|
105
111
|
this.baseUrl = baseUrl;
|
|
112
|
+
this.requestInit = requestInit;
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
public metadata(): FileSystemMetadata {
|
|
@@ -136,7 +143,7 @@ export class FetchFS extends IndexFS {
|
|
|
136
143
|
return stats.fileData;
|
|
137
144
|
}
|
|
138
145
|
|
|
139
|
-
const data = await fetchFile(this.baseUrl + (path.startsWith('/') ? path.slice(1) : path), 'buffer');
|
|
146
|
+
const data = await fetchFile(this.baseUrl + (path.startsWith('/') ? path.slice(1) : path), 'buffer', this.requestInit);
|
|
140
147
|
stats.fileData = data;
|
|
141
148
|
return data;
|
|
142
149
|
}
|
|
@@ -154,16 +161,9 @@ const _Fetch = {
|
|
|
154
161
|
name: 'Fetch',
|
|
155
162
|
|
|
156
163
|
options: {
|
|
157
|
-
index: {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
description: 'URL to a file index as a JSON file or the file index object itself, generated with the make-index script. Defaults to `index.json`.',
|
|
161
|
-
},
|
|
162
|
-
baseUrl: {
|
|
163
|
-
type: 'string',
|
|
164
|
-
required: false,
|
|
165
|
-
description: 'Used as the URL prefix for fetched files. Default: Fetch files relative to the index.',
|
|
166
|
-
},
|
|
164
|
+
index: { type: ['string', 'object'], required: false },
|
|
165
|
+
baseUrl: { type: 'string', required: false },
|
|
166
|
+
requestInit: { type: 'object', required: false },
|
|
167
167
|
},
|
|
168
168
|
|
|
169
169
|
isAvailable(): boolean {
|
package/src/backends/memory.ts
CHANGED
|
@@ -32,11 +32,7 @@ const _InMemory = {
|
|
|
32
32
|
return true;
|
|
33
33
|
},
|
|
34
34
|
options: {
|
|
35
|
-
name: {
|
|
36
|
-
type: 'string',
|
|
37
|
-
required: false,
|
|
38
|
-
description: 'The name of the store',
|
|
39
|
-
},
|
|
35
|
+
name: { type: 'string', required: false },
|
|
40
36
|
},
|
|
41
37
|
create({ name }: { name?: string }) {
|
|
42
38
|
const fs = new StoreFS(new InMemoryStore(name));
|
package/src/backends/port/fs.ts
CHANGED
|
@@ -298,7 +298,6 @@ const _Port = {
|
|
|
298
298
|
port: {
|
|
299
299
|
type: 'object',
|
|
300
300
|
required: true,
|
|
301
|
-
description: 'The target port that you want to connect to',
|
|
302
301
|
validator(port: RPC.Port) {
|
|
303
302
|
// Check for a `postMessage` function.
|
|
304
303
|
if (typeof port?.postMessage != 'function') {
|
|
@@ -309,7 +308,6 @@ const _Port = {
|
|
|
309
308
|
timeout: {
|
|
310
309
|
type: 'number',
|
|
311
310
|
required: false,
|
|
312
|
-
description: 'How long to wait before the request times out',
|
|
313
311
|
},
|
|
314
312
|
},
|
|
315
313
|
|
package/src/backends/store/fs.ts
CHANGED
|
@@ -534,22 +534,22 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
|
|
|
534
534
|
* @param data The data to store at the file's data node.
|
|
535
535
|
*/
|
|
536
536
|
private async commitNew(path: string, type: FileType, mode: number, data: Uint8Array): Promise<Inode> {
|
|
537
|
-
await using tx = this.store.transaction();
|
|
538
|
-
const parentPath = dirname(path),
|
|
539
|
-
parent = await this.findINode(tx, parentPath);
|
|
540
|
-
|
|
541
|
-
const fname = basename(path),
|
|
542
|
-
listing = await this.getDirListing(tx, parent, parentPath);
|
|
543
|
-
|
|
544
537
|
/*
|
|
545
538
|
The root always exists.
|
|
546
539
|
If we don't check this prior to taking steps below,
|
|
547
|
-
we will create a file with name '' in root
|
|
540
|
+
we will create a file with name '' in root if path is '/'.
|
|
548
541
|
*/
|
|
549
|
-
if (path
|
|
542
|
+
if (path == '/') {
|
|
550
543
|
throw ErrnoError.With('EEXIST', path, 'commitNew');
|
|
551
544
|
}
|
|
552
545
|
|
|
546
|
+
await using tx = this.store.transaction();
|
|
547
|
+
const parentPath = dirname(path),
|
|
548
|
+
parent = await this.findINode(tx, parentPath);
|
|
549
|
+
|
|
550
|
+
const fname = basename(path),
|
|
551
|
+
listing = await this.getDirListing(tx, parent, parentPath);
|
|
552
|
+
|
|
553
553
|
// Check if file already exists.
|
|
554
554
|
if (listing[fname]) {
|
|
555
555
|
await tx.abort();
|
|
@@ -581,22 +581,22 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
|
|
|
581
581
|
* @return The Inode for the new file.
|
|
582
582
|
*/
|
|
583
583
|
protected commitNewSync(path: string, type: FileType, mode: number, data: Uint8Array = new Uint8Array()): Inode {
|
|
584
|
-
using tx = this.store.transaction();
|
|
585
|
-
const parentPath = dirname(path),
|
|
586
|
-
parent = this.findINodeSync(tx, parentPath);
|
|
587
|
-
|
|
588
|
-
const fname = basename(path),
|
|
589
|
-
listing = this.getDirListingSync(tx, parent, parentPath);
|
|
590
|
-
|
|
591
584
|
/*
|
|
592
585
|
The root always exists.
|
|
593
586
|
If we don't check this prior to taking steps below,
|
|
594
|
-
we will create a file with name '' in root
|
|
587
|
+
we will create a file with name '' in root if path is '/'.
|
|
595
588
|
*/
|
|
596
|
-
if (path
|
|
589
|
+
if (path == '/') {
|
|
597
590
|
throw ErrnoError.With('EEXIST', path, 'commitNew');
|
|
598
591
|
}
|
|
599
592
|
|
|
593
|
+
using tx = this.store.transaction();
|
|
594
|
+
const parentPath = dirname(path),
|
|
595
|
+
parent = this.findINodeSync(tx, parentPath);
|
|
596
|
+
|
|
597
|
+
const fname = basename(path),
|
|
598
|
+
listing = this.getDirListingSync(tx, parent, parentPath);
|
|
599
|
+
|
|
600
600
|
// Check if file already exists.
|
|
601
601
|
if (listing[fname]) {
|
|
602
602
|
throw ErrnoError.With('EEXIST', path, 'commitNew');
|
package/src/config.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
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
|
|
4
|
+
import { DeviceFS } from './devices.js';
|
|
5
5
|
import * as cache from './emulation/cache.js';
|
|
6
|
+
import { config } from './emulation/config.js';
|
|
6
7
|
import * as fs from './emulation/index.js';
|
|
7
8
|
import type { AbsolutePath } from './emulation/path.js';
|
|
8
|
-
import { type MountObject } from './emulation/shared.js';
|
|
9
|
-
import { config } from './emulation/config.js';
|
|
10
9
|
import { Errno, ErrnoError } from './error.js';
|
|
11
10
|
import { FileSystem } from './filesystem.js';
|
|
12
11
|
|
|
@@ -151,6 +150,27 @@ export async function configureSingle<T extends Backend>(configuration: MountCon
|
|
|
151
150
|
fs.mount('/', resolved);
|
|
152
151
|
}
|
|
153
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Like `fs.mount`, but it also creates missing directories.
|
|
155
|
+
* @privateRemarks
|
|
156
|
+
* This is implemented as a separate function to avoid a circular dependency between emulation/shared.ts and other emulation layer files.
|
|
157
|
+
* @internal
|
|
158
|
+
*/
|
|
159
|
+
async function mount(path: string, mount: FileSystem): Promise<void> {
|
|
160
|
+
if (path == '/') {
|
|
161
|
+
fs.mount(path, mount);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const stats = await fs.promises.stat(path).catch(() => null);
|
|
166
|
+
if (!stats) {
|
|
167
|
+
await fs.promises.mkdir(path, { recursive: true });
|
|
168
|
+
} else if (!stats.isDirectory()) {
|
|
169
|
+
throw ErrnoError.With('ENOTDIR', path, 'configure');
|
|
170
|
+
}
|
|
171
|
+
fs.mount(path, mount);
|
|
172
|
+
}
|
|
173
|
+
|
|
154
174
|
/**
|
|
155
175
|
* Configures ZenFS with `configuration`
|
|
156
176
|
* @see Configuration
|
|
@@ -168,18 +188,18 @@ export async function configure<T extends ConfigMounts>(configuration: Partial<C
|
|
|
168
188
|
|
|
169
189
|
if (configuration.addDevices) {
|
|
170
190
|
const devfs = new DeviceFS();
|
|
171
|
-
devfs.
|
|
172
|
-
devfs.createDevice('/zero', zeroDevice);
|
|
173
|
-
devfs.createDevice('/full', fullDevice);
|
|
174
|
-
devfs.createDevice('/random', randomDevice);
|
|
191
|
+
devfs.addDefaults();
|
|
175
192
|
await devfs.ready();
|
|
176
|
-
|
|
193
|
+
await mount('/dev', devfs);
|
|
177
194
|
}
|
|
178
195
|
|
|
179
196
|
if (!configuration.mounts) {
|
|
180
197
|
return;
|
|
181
198
|
}
|
|
182
199
|
|
|
200
|
+
const toMount: [string, FileSystem][] = [];
|
|
201
|
+
let unmountRoot = false;
|
|
202
|
+
|
|
183
203
|
for (const [point, mountConfig] of Object.entries(configuration.mounts)) {
|
|
184
204
|
if (!point.startsWith('/')) {
|
|
185
205
|
throw new ErrnoError(Errno.EINVAL, 'Mount points must have absolute paths');
|
|
@@ -189,8 +209,11 @@ export async function configure<T extends ConfigMounts>(configuration: Partial<C
|
|
|
189
209
|
mountConfig.disableAsyncCache ??= configuration.disableAsyncCache || false;
|
|
190
210
|
}
|
|
191
211
|
|
|
192
|
-
|
|
212
|
+
if (point == '/') unmountRoot = true;
|
|
213
|
+
toMount.push([point, await resolveMountConfig(mountConfig)]);
|
|
193
214
|
}
|
|
194
215
|
|
|
195
|
-
fs.
|
|
216
|
+
if (unmountRoot) fs.umount('/');
|
|
217
|
+
|
|
218
|
+
await Promise.all(toMount.map(([point, fs]) => mount(point, fs)));
|
|
196
219
|
}
|
package/src/credentials.ts
CHANGED
package/src/devices.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This is a great resource: https://www.kernel.org/doc/html/latest/admin-guide/devices.html
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import type { FileReadResult } from 'node:fs/promises';
|
|
2
6
|
import { InMemoryStore } from './backends/memory.js';
|
|
3
7
|
import { StoreFS } from './backends/store/fs.js';
|
|
@@ -12,9 +16,10 @@ import type { Ino } from './inode.js';
|
|
|
12
16
|
/**
|
|
13
17
|
* A device
|
|
14
18
|
* @todo Maybe add major/minor number or some other device information, like a UUID?
|
|
15
|
-
* @
|
|
19
|
+
* @privateRemarks
|
|
20
|
+
* UUIDs were considered, however they don't make sense without an easy mechanism for persistance
|
|
16
21
|
*/
|
|
17
|
-
export interface Device {
|
|
22
|
+
export interface Device<TData = any> {
|
|
18
23
|
/**
|
|
19
24
|
* The device's driver
|
|
20
25
|
*/
|
|
@@ -24,13 +29,31 @@ export interface Device {
|
|
|
24
29
|
* Which inode the device is assigned
|
|
25
30
|
*/
|
|
26
31
|
ino: Ino;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Data associated with a device.
|
|
35
|
+
* This is meant to be used by device drivers.
|
|
36
|
+
* @experimental
|
|
37
|
+
*/
|
|
38
|
+
data: TData;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Major device number
|
|
42
|
+
* @experimental
|
|
43
|
+
*/
|
|
44
|
+
major: number;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Minor device number
|
|
48
|
+
* @experimental
|
|
49
|
+
*/
|
|
50
|
+
minor: number;
|
|
27
51
|
}
|
|
28
52
|
|
|
29
53
|
/**
|
|
30
54
|
* A device driver
|
|
31
|
-
* @experimental
|
|
32
55
|
*/
|
|
33
|
-
export interface DeviceDriver {
|
|
56
|
+
export interface DeviceDriver<TData = any> {
|
|
34
57
|
/**
|
|
35
58
|
* The name of the device driver
|
|
36
59
|
*/
|
|
@@ -38,26 +61,42 @@ export interface DeviceDriver {
|
|
|
38
61
|
|
|
39
62
|
/**
|
|
40
63
|
* Whether the device is buffered (a "block" device) or unbuffered (a "character" device)
|
|
64
|
+
* @default false
|
|
41
65
|
*/
|
|
42
|
-
isBuffered
|
|
66
|
+
isBuffered?: boolean;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Initializes a new device.
|
|
70
|
+
* @returns `Device.data`
|
|
71
|
+
* @experimental
|
|
72
|
+
*/
|
|
73
|
+
init?(ino: Ino): {
|
|
74
|
+
data?: TData;
|
|
75
|
+
minor?: number;
|
|
76
|
+
major?: number;
|
|
77
|
+
};
|
|
43
78
|
|
|
44
79
|
/**
|
|
45
80
|
* Synchronously read from the device
|
|
81
|
+
* @group File operations
|
|
46
82
|
*/
|
|
47
83
|
read(file: DeviceFile, buffer: ArrayBufferView, offset?: number, length?: number, position?: number): number;
|
|
48
84
|
|
|
49
85
|
/**
|
|
50
86
|
* Synchronously write to the device
|
|
87
|
+
* @group File operations
|
|
51
88
|
*/
|
|
52
89
|
write(file: DeviceFile, buffer: Uint8Array, offset: number, length: number, position?: number): number;
|
|
53
90
|
|
|
54
91
|
/**
|
|
55
92
|
* Sync the device
|
|
93
|
+
* @group File operations
|
|
56
94
|
*/
|
|
57
95
|
sync?(file: DeviceFile): void;
|
|
58
96
|
|
|
59
97
|
/**
|
|
60
98
|
* Close the device
|
|
99
|
+
* @group File operations
|
|
61
100
|
*/
|
|
62
101
|
close?(file: DeviceFile): void;
|
|
63
102
|
}
|
|
@@ -67,7 +106,6 @@ export interface DeviceDriver {
|
|
|
67
106
|
* This class only does some simple things:
|
|
68
107
|
* It implements `truncate` using `write` and it has non-device methods throw.
|
|
69
108
|
* It is up to device drivers to implement the rest of the functionality.
|
|
70
|
-
* @experimental
|
|
71
109
|
*/
|
|
72
110
|
export class DeviceFile extends File {
|
|
73
111
|
public position = 0;
|
|
@@ -182,12 +220,15 @@ export class DeviceFile extends File {
|
|
|
182
220
|
}
|
|
183
221
|
|
|
184
222
|
/**
|
|
185
|
-
*
|
|
223
|
+
* A temporary file system that manages and interfaces with devices
|
|
186
224
|
*/
|
|
187
225
|
export class DeviceFS extends StoreFS<InMemoryStore> {
|
|
188
226
|
protected readonly devices = new Map<string, Device>();
|
|
189
227
|
|
|
190
|
-
|
|
228
|
+
/**
|
|
229
|
+
* Creates a new device at `path` relative to the `DeviceFS` root.
|
|
230
|
+
*/
|
|
231
|
+
public createDevice<TData = any>(path: string, driver: DeviceDriver<TData>): Device<TData | Record<string, never>> {
|
|
191
232
|
if (this.existsSync(path)) {
|
|
192
233
|
throw ErrnoError.With('EEXIST', path, 'mknod');
|
|
193
234
|
}
|
|
@@ -196,11 +237,25 @@ export class DeviceFS extends StoreFS<InMemoryStore> {
|
|
|
196
237
|
const dev = {
|
|
197
238
|
driver,
|
|
198
239
|
ino,
|
|
240
|
+
data: {},
|
|
241
|
+
minor: 0,
|
|
242
|
+
major: 0,
|
|
243
|
+
...driver.init?.(ino),
|
|
199
244
|
};
|
|
200
245
|
this.devices.set(path, dev);
|
|
201
246
|
return dev;
|
|
202
247
|
}
|
|
203
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Adds default devices
|
|
251
|
+
*/
|
|
252
|
+
public addDefaults(): void {
|
|
253
|
+
this.createDevice('/null', nullDevice);
|
|
254
|
+
this.createDevice('/zero', zeroDevice);
|
|
255
|
+
this.createDevice('/full', fullDevice);
|
|
256
|
+
this.createDevice('/random', randomDevice);
|
|
257
|
+
}
|
|
258
|
+
|
|
204
259
|
public constructor() {
|
|
205
260
|
super(new InMemoryStore('devfs'));
|
|
206
261
|
}
|
|
@@ -373,7 +428,9 @@ function defaultWrite(file: DeviceFile, buffer: Uint8Array, offset: number, leng
|
|
|
373
428
|
*/
|
|
374
429
|
export const nullDevice: DeviceDriver = {
|
|
375
430
|
name: 'null',
|
|
376
|
-
|
|
431
|
+
init() {
|
|
432
|
+
return { major: 1, minor: 3 };
|
|
433
|
+
},
|
|
377
434
|
read(): number {
|
|
378
435
|
return 0;
|
|
379
436
|
},
|
|
@@ -392,7 +449,9 @@ export const nullDevice: DeviceDriver = {
|
|
|
392
449
|
*/
|
|
393
450
|
export const zeroDevice: DeviceDriver = {
|
|
394
451
|
name: 'zero',
|
|
395
|
-
|
|
452
|
+
init() {
|
|
453
|
+
return { major: 1, minor: 5 };
|
|
454
|
+
},
|
|
396
455
|
read(file: DeviceFile, buffer: ArrayBufferView, offset = 0, length = buffer.byteLength): number {
|
|
397
456
|
const data = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
398
457
|
for (let i = offset; i < offset + length; i++) {
|
|
@@ -412,7 +471,9 @@ export const zeroDevice: DeviceDriver = {
|
|
|
412
471
|
*/
|
|
413
472
|
export const fullDevice: DeviceDriver = {
|
|
414
473
|
name: 'full',
|
|
415
|
-
|
|
474
|
+
init() {
|
|
475
|
+
return { major: 1, minor: 7 };
|
|
476
|
+
},
|
|
416
477
|
read(file: DeviceFile, buffer: ArrayBufferView, offset = 0, length = buffer.byteLength): number {
|
|
417
478
|
const data = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
418
479
|
for (let i = offset; i < offset + length; i++) {
|
|
@@ -435,7 +496,9 @@ export const fullDevice: DeviceDriver = {
|
|
|
435
496
|
*/
|
|
436
497
|
export const randomDevice: DeviceDriver = {
|
|
437
498
|
name: 'random',
|
|
438
|
-
|
|
499
|
+
init() {
|
|
500
|
+
return { major: 1, minor: 8 };
|
|
501
|
+
},
|
|
439
502
|
read(file: DeviceFile, buffer: ArrayBufferView, offset = 0, length = buffer.byteLength): number {
|
|
440
503
|
const data = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
441
504
|
for (let i = offset; i < offset + length; i++) {
|
|
@@ -13,11 +13,11 @@ import '../polyfills.js';
|
|
|
13
13
|
import { BigIntStats, type Stats } from '../stats.js';
|
|
14
14
|
import { decodeUTF8, normalizeMode, normalizeOptions, normalizePath, normalizeTime } from '../utils.js';
|
|
15
15
|
import * as cache from './cache.js';
|
|
16
|
+
import { config } from './config.js';
|
|
16
17
|
import * as constants from './constants.js';
|
|
17
18
|
import { Dir, Dirent } from './dir.js';
|
|
18
19
|
import { dirname, join, parse } from './path.js';
|
|
19
|
-
import { _statfs, fd2file, fdMap, file2fd, fixError,
|
|
20
|
-
import { config } from './config.js';
|
|
20
|
+
import { _statfs, fd2file, fdMap, file2fd, fixError, resolveMount, type InternalOptions, type ReaddirOptions } from './shared.js';
|
|
21
21
|
import { ReadStream, WriteStream } from './streams.js';
|
|
22
22
|
import { FSWatcher, emitChange } from './watchers.js';
|
|
23
23
|
export * as constants from './constants.js';
|
|
@@ -625,7 +625,7 @@ export async function rmdir(path: fs.PathLike): Promise<void> {
|
|
|
625
625
|
try {
|
|
626
626
|
const stats = await (cache.getStats(path) || fs.stat(resolved));
|
|
627
627
|
if (!stats) {
|
|
628
|
-
throw ErrnoError.With('ENOENT', path, '
|
|
628
|
+
throw ErrnoError.With('ENOENT', path, 'rmdir');
|
|
629
629
|
}
|
|
630
630
|
if (!stats.isDirectory()) {
|
|
631
631
|
throw ErrnoError.With('ENOTDIR', resolved, 'rmdir');
|
|
@@ -736,17 +736,6 @@ export async function readdir(
|
|
|
736
736
|
|
|
737
737
|
const entries = await fs.readdir(resolved).catch(handleError);
|
|
738
738
|
|
|
739
|
-
for (const point of mounts.keys()) {
|
|
740
|
-
if (point.startsWith(path)) {
|
|
741
|
-
const entry = point.slice(path.length);
|
|
742
|
-
if (entry.includes('/') || entry.length == 0) {
|
|
743
|
-
// ignore FSs mounted in subdirectories and any FS mounted to `path`.
|
|
744
|
-
continue;
|
|
745
|
-
}
|
|
746
|
-
entries.push(entry);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
|
|
750
739
|
const values: (string | Dirent | Buffer)[] = [];
|
|
751
740
|
const addEntry = async (entry: string) => {
|
|
752
741
|
let entryStats: Stats | undefined;
|
package/src/emulation/shared.ts
CHANGED
|
@@ -54,7 +54,7 @@ export function mount(mountPoint: string, fs: FileSystem): void {
|
|
|
54
54
|
*/
|
|
55
55
|
export function umount(mountPoint: string): void {
|
|
56
56
|
if (mountPoint[0] !== '/') {
|
|
57
|
-
mountPoint =
|
|
57
|
+
mountPoint = '/' + mountPoint;
|
|
58
58
|
}
|
|
59
59
|
mountPoint = resolve(mountPoint);
|
|
60
60
|
if (!mounts.has(mountPoint)) {
|
|
@@ -64,11 +64,10 @@ export function umount(mountPoint: string): void {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
|
-
* Gets the internal FileSystem for the path, then returns it along with the path relative to the FS' root
|
|
67
|
+
* Gets the internal `FileSystem` for the path, then returns it along with the path relative to the FS' root
|
|
68
68
|
*/
|
|
69
69
|
export function resolveMount(path: string): { fs: FileSystem; path: string; mountPoint: string } {
|
|
70
70
|
path = normalizePath(path);
|
|
71
|
-
// Maybe do something for devices here
|
|
72
71
|
const sortedMounts = [...mounts].sort((a, b) => (a[0].length > b[0].length ? -1 : 1)); // descending order of the string length
|
|
73
72
|
for (const [mountPoint, fs] of sortedMounts) {
|
|
74
73
|
// We know path is normalized, so it would be a substring of the mount point.
|
|
@@ -107,6 +106,9 @@ export function fixError<E extends ErrnoError>(e: E, paths: Record<string, strin
|
|
|
107
106
|
return e;
|
|
108
107
|
}
|
|
109
108
|
|
|
109
|
+
/**
|
|
110
|
+
* @deprecated
|
|
111
|
+
*/
|
|
110
112
|
export function mountObject(mounts: MountObject): void {
|
|
111
113
|
if ('/' in mounts) {
|
|
112
114
|
umount('/');
|
package/src/emulation/sync.ts
CHANGED
|
@@ -6,13 +6,13 @@ import { flagToMode, isAppendable, isExclusive, isReadable, isTruncating, isWrit
|
|
|
6
6
|
import type { FileContents } from '../filesystem.js';
|
|
7
7
|
import { BigIntStats, type Stats } from '../stats.js';
|
|
8
8
|
import { decodeUTF8, normalizeMode, normalizeOptions, normalizePath, normalizeTime } from '../utils.js';
|
|
9
|
+
import * as cache from './cache.js';
|
|
10
|
+
import { config } from './config.js';
|
|
9
11
|
import * as constants from './constants.js';
|
|
10
12
|
import { Dir, Dirent } from './dir.js';
|
|
11
13
|
import { dirname, join, parse } from './path.js';
|
|
12
|
-
import { _statfs, fd2file, fdMap, file2fd, fixError,
|
|
13
|
-
import { config } from './config.js';
|
|
14
|
+
import { _statfs, fd2file, fdMap, file2fd, fixError, resolveMount, type InternalOptions, type ReaddirOptions } from './shared.js';
|
|
14
15
|
import { emitChange } from './watchers.js';
|
|
15
|
-
import * as cache from './cache.js';
|
|
16
16
|
|
|
17
17
|
export function renameSync(oldPath: fs.PathLike, newPath: fs.PathLike): void {
|
|
18
18
|
oldPath = normalizePath(oldPath);
|
|
@@ -472,18 +472,6 @@ export function readdirSync(
|
|
|
472
472
|
throw fixError(e as ErrnoError, { [resolved]: path });
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
-
for (const mount of mounts.keys()) {
|
|
476
|
-
if (!mount.startsWith(path)) {
|
|
477
|
-
continue;
|
|
478
|
-
}
|
|
479
|
-
const entry = mount.slice(path.length);
|
|
480
|
-
if (entry.includes('/') || entry.length == 0) {
|
|
481
|
-
// ignore FSs mounted in subdirectories and any FS mounted to `path`.
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
entries.push(entry);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
475
|
// Iterate over entries and handle recursive case if needed
|
|
488
476
|
const values: (string | Dirent | Buffer)[] = [];
|
|
489
477
|
for (const entry of entries) {
|
package/src/mixins/async.ts
CHANGED
|
@@ -80,8 +80,8 @@ export function Async<T extends typeof FileSystem>(
|
|
|
80
80
|
const async = (this as StoreFS<Store>)['store'].transaction();
|
|
81
81
|
|
|
82
82
|
const promises = [];
|
|
83
|
-
for (const key of
|
|
84
|
-
promises.push(async.
|
|
83
|
+
for (const key of await async.keys()) {
|
|
84
|
+
promises.push(async.get(key).then(data => sync.setSync(key, data)));
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
await Promise.all(promises);
|