@zenfs/core 2.7.1 → 2.7.3-newstreams.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/README.md +11 -108
- package/dist/backends/fetch.d.ts +2 -0
- package/dist/backends/fetch.js +9 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +2 -0
- package/dist/context.js +4 -4
- package/dist/internal/contexts.js +2 -2
- package/dist/internal/devices.d.ts +18 -1
- package/dist/internal/devices.js +14 -0
- package/dist/internal/filesystem.d.ts +1 -0
- package/dist/internal/filesystem.js +16 -10
- package/dist/internal/index_fs.d.ts +6 -0
- package/dist/internal/index_fs.js +20 -2
- package/dist/mixins/async.d.ts +5 -6
- package/dist/mixins/async.js +72 -9
- package/dist/node/streams.d.ts +2 -2
- package/dist/node/streams.js +9 -3
- package/dist/node/sync.d.ts +7 -6
- package/dist/node/sync.js +19 -2
- package/dist/vfs/file.js +16 -10
- package/dist/vfs/shared.js +1 -1
- package/dist/vfs/vnode.js +3 -15
- package/package.json +6 -7
- package/tests/common/context.test.ts +11 -1
- package/tests/fs/streams.test.ts +60 -0
- package/tests/setup.ts +1 -1
- package/types/README.md +0 -1
- package/types/readable-stream.d.ts +0 -18
package/README.md
CHANGED
|
@@ -43,6 +43,9 @@ Your financial support would go a long way toward improving ZenFS and its commun
|
|
|
43
43
|
|
|
44
44
|
## Usage
|
|
45
45
|
|
|
46
|
+
> [!IMPORTANT]
|
|
47
|
+
> **[Check out the ZenFS docs!](https://zenfs.dev/start/usage/)**
|
|
48
|
+
|
|
46
49
|
```js
|
|
47
50
|
import { fs } from '@zenfs/core'; // You can also use the default export
|
|
48
51
|
|
|
@@ -52,15 +55,9 @@ const contents = fs.readFileSync('/test.txt', 'utf-8');
|
|
|
52
55
|
console.log(contents);
|
|
53
56
|
```
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
You can configure ZenFS to use a different backend and mount multiple backends. It is strongly recommended to do so using the `configure` function.
|
|
60
|
-
|
|
61
|
-
You can use multiple backends by passing an object to `configure` which maps paths to file systems.
|
|
62
|
-
|
|
63
|
-
The following example mounts a zip file to `/zip`, in-memory storage to `/tmp`, and IndexedDB to `/home`. Note that `/` has the default in-memory backend.
|
|
58
|
+
A single `InMemory` backend is created by default, mounted on `/`. To use different backends, and to
|
|
59
|
+
mount more than one, configure ZenFS with `configure`. This mounts a zip file to `/mnt/zip`,
|
|
60
|
+
in-memory storage to `/tmp`, and IndexedDB to `/home`:
|
|
64
61
|
|
|
65
62
|
```js
|
|
66
63
|
import { configure, InMemory } from '@zenfs/core';
|
|
@@ -78,110 +75,16 @@ await configure({
|
|
|
78
75
|
});
|
|
79
76
|
```
|
|
80
77
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
> [!TIP]
|
|
84
|
-
> When configuring a mount point, you can pass in
|
|
85
|
-
>
|
|
86
|
-
> 1. A `Backend` object, if the backend has no required options
|
|
87
|
-
> 2. An object that has the options accepted by the backend and a `backend` property which is a `Backend` object
|
|
88
|
-
> 3. A `FileSystem` instance
|
|
89
|
-
|
|
90
|
-
Here is an example that mounts the `WebStorage` backend from `@zenfs/dom` on `/`:
|
|
91
|
-
|
|
92
|
-
```js
|
|
93
|
-
import { configureSingle, fs } from '@zenfs/core';
|
|
94
|
-
import { WebStorage } from '@zenfs/dom';
|
|
95
|
-
|
|
96
|
-
await configureSingle({ backend: WebStorage });
|
|
97
|
-
|
|
98
|
-
if (!fs.existsSync('/test.txt')) {
|
|
99
|
-
fs.writeFileSync('/test.txt', 'This will persist across reloads!');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const contents = fs.readFileSync('/test.txt', 'utf-8');
|
|
103
|
-
console.log(contents);
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
#### FS Promises
|
|
107
|
-
|
|
108
|
-
The FS promises API is exposed as `promises`.
|
|
109
|
-
|
|
110
|
-
```js
|
|
111
|
-
import { configureSingle } from '@zenfs/core';
|
|
112
|
-
import { exists, writeFile } from '@zenfs/core/promises';
|
|
113
|
-
import { IndexedDB } from '@zenfs/dom';
|
|
114
|
-
|
|
115
|
-
await configureSingle({ backend: IndexedDB });
|
|
116
|
-
|
|
117
|
-
const exists = await exists('/myfile.txt');
|
|
118
|
-
if (!exists) {
|
|
119
|
-
await writeFile('/myfile.txt', 'Lots of persistent data');
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
> [!NOTE]
|
|
124
|
-
> You can import the promises API using:
|
|
125
|
-
>
|
|
126
|
-
> 1. Exports from `@zenfs/core/promises`
|
|
127
|
-
> 2. The `promises` export from `@zenfs/core`
|
|
128
|
-
> 3. `fs.promises` on the exported `fs` from `@zenfs/core`.
|
|
78
|
+
The `fs/promises` API is available from `@zenfs/core/promises`, as the `promises` export, or as
|
|
79
|
+
`fs.promises`.
|
|
129
80
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
If you would like to create backends without configure (e.g. to do something dynamic at runtime), you may do so by importing the backend and calling `resolveMountConfig` with it.
|
|
133
|
-
|
|
134
|
-
You can then mount and unmount the backend instance by using `mount` and `umount`.
|
|
135
|
-
|
|
136
|
-
```js
|
|
137
|
-
import { configure, resolveMountConfig, InMemory } from '@zenfs/core';
|
|
138
|
-
import { IndexedDB } from '@zenfs/dom';
|
|
139
|
-
import { Zip } from '@zenfs/archives';
|
|
140
|
-
|
|
141
|
-
await configure({
|
|
142
|
-
mounts: {
|
|
143
|
-
'/tmp': InMemory,
|
|
144
|
-
'/home': IndexedDB,
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
fs.mkdirSync('/mnt/zip', { recursive: true });
|
|
149
|
-
|
|
150
|
-
const res = await fetch('mydata.zip');
|
|
151
|
-
const zipfs = await resolveMountConfig({ backend: Zip, data: await res.arrayBuffer() });
|
|
152
|
-
fs.mount('/mnt/zip', zipfs);
|
|
153
|
-
|
|
154
|
-
// do stuff with the mounted zip
|
|
155
|
-
|
|
156
|
-
fs.umount('/mnt/zip'); // finished using the zip
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
> [!CAUTION]
|
|
160
|
-
> Instances of backends follow the _internal_ API. You should never use a backend's methods unless you are extending a backend.
|
|
161
|
-
|
|
162
|
-
### Devices and device files
|
|
163
|
-
|
|
164
|
-
ZenFS includes support for device files. These are designed to follow Linux's device file behavior, for consistency and ease of use. Check out the [Devices and Device Drivers](https://zenfs.dev/core/documents/Devices_and_Device_Drivers) documentation for more information.
|
|
165
|
-
|
|
166
|
-
## `node:*` emulation
|
|
167
|
-
|
|
168
|
-
ZenFS also includes emulation of some other `node:` modules for various reasons, importable from `@zenfs/core/<name>`:
|
|
169
|
-
|
|
170
|
-
- `node:path`
|
|
171
|
-
- `node:readline`
|
|
172
|
-
|
|
173
|
-
For example:
|
|
174
|
-
|
|
175
|
-
```ts
|
|
176
|
-
import * as path from '@zenfs/core/path';
|
|
177
|
-
```
|
|
81
|
+
For the full usage guide, see **[the documentation](https://zenfs.dev/start/usage/)**.
|
|
82
|
+
This includes mounting at runtime, contexts and permissions, devices, and the `node:*`module emulation.
|
|
178
83
|
|
|
179
84
|
## Bundling
|
|
180
85
|
|
|
181
86
|
ZenFS exports a drop-in for Node's `fs` module, so you can use it for your bundler of preference using the default export.
|
|
182
|
-
|
|
183
|
-
> [!IMPORTANT]
|
|
184
|
-
> See [COPYING.md](./COPYING.md)
|
|
87
|
+
See [COPYING.md](./COPYING.md) for more info.
|
|
185
88
|
|
|
186
89
|
## Sponsors
|
|
187
90
|
|
package/dist/backends/fetch.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export declare class FetchFS extends IndexFS {
|
|
|
47
47
|
protected _async(p: Promise<unknown>): void;
|
|
48
48
|
constructor(index: Index, baseUrl: string, requestInit?: RequestInit, remoteWrite?: boolean | undefined);
|
|
49
49
|
protected resize(path: string, inode: Inode, oldSize: number): Promise<void> | void;
|
|
50
|
+
protected _create(path: string, inode: Inode): Promise<void>;
|
|
51
|
+
protected _createSync(path: string, inode: Inode): void;
|
|
50
52
|
touch(path: string, metadata: InodeLike): Promise<void>;
|
|
51
53
|
touchSync(path: string, metadata: InodeLike): void;
|
|
52
54
|
protected remove(path: string): Promise<void>;
|
package/dist/backends/fetch.js
CHANGED
|
@@ -63,6 +63,14 @@ export class FetchFS extends IndexFS {
|
|
|
63
63
|
if (this.remoteWrite)
|
|
64
64
|
return requests.set(url, zeroes, { offset: oldSize, warn }, this.requestInit).catch(parseError);
|
|
65
65
|
}
|
|
66
|
+
async _create(path, inode) {
|
|
67
|
+
await requests
|
|
68
|
+
.set(this.baseUrl + path, new Uint8Array(inode.size), { warn, cacheOnly: !this.remoteWrite, method: 'PUT' }, this.requestInit)
|
|
69
|
+
.catch(parseError);
|
|
70
|
+
}
|
|
71
|
+
_createSync(path, inode) {
|
|
72
|
+
this._async(this._create(path, inode));
|
|
73
|
+
}
|
|
66
74
|
async touch(path, metadata) {
|
|
67
75
|
const inode = this.index.get(path) ?? _throw(withErrno('ENOENT'));
|
|
68
76
|
const oldSize = inode.size;
|
|
@@ -81,7 +89,7 @@ export class FetchFS extends IndexFS {
|
|
|
81
89
|
await requests.remove(this.baseUrl + path, { warn, cacheOnly: !this.remoteWrite }, this.requestInit);
|
|
82
90
|
}
|
|
83
91
|
removeSync(path) {
|
|
84
|
-
this._async(
|
|
92
|
+
this._async(this.remove(path));
|
|
85
93
|
}
|
|
86
94
|
async read(path, buffer, offset = 0, end) {
|
|
87
95
|
const inode = this.index.get(path);
|
package/dist/config.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ export interface Configuration<T extends ConfigMounts> extends SharedConfig {
|
|
|
56
56
|
/**
|
|
57
57
|
* Whether to automatically add normal Linux devices
|
|
58
58
|
* @default false
|
|
59
|
+
* @deprecated Use `@zenfs/linux`!
|
|
60
|
+
* @todo [breaking] remove
|
|
59
61
|
*/
|
|
60
62
|
addDevices: boolean;
|
|
61
63
|
/**
|
|
@@ -97,6 +99,8 @@ export declare function configureSingle<T extends Backend>(configuration: MountC
|
|
|
97
99
|
export declare function configureSingleSync<T extends Backend>(configuration: MountConfiguration<T>): void;
|
|
98
100
|
/**
|
|
99
101
|
* @category Backends and Configuration
|
|
102
|
+
* @deprecated Use `@zenfs/linux`!
|
|
103
|
+
* @todo [breaking] remove
|
|
100
104
|
*/
|
|
101
105
|
export declare function addDevice(driver: DeviceDriver, options?: object): Device;
|
|
102
106
|
/**
|
package/dist/config.js
CHANGED
|
@@ -189,6 +189,8 @@ function mountWithMkdirSync(path, fs) {
|
|
|
189
189
|
}
|
|
190
190
|
/**
|
|
191
191
|
* @category Backends and Configuration
|
|
192
|
+
* @deprecated Use `@zenfs/linux`!
|
|
193
|
+
* @todo [breaking] remove
|
|
192
194
|
*/
|
|
193
195
|
export function addDevice(driver, options) {
|
|
194
196
|
const devfs = defaultContext.mounts.get('/dev');
|
package/dist/context.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
2
|
import { UV } from 'kerium';
|
|
3
3
|
import { bindFunctions } from 'utilium';
|
|
4
|
-
import { contextOf, createChildContext } from './internal/contexts.js';
|
|
4
|
+
import { contextOf, createChildContext, defaultContext } from './internal/contexts.js';
|
|
5
5
|
import * as fs from './node/index.js';
|
|
6
6
|
import * as path from './path.js';
|
|
7
7
|
import * as xattr from './vfs/xattr.js';
|
|
@@ -17,9 +17,9 @@ export const boundContexts = new Map();
|
|
|
17
17
|
* @category Contexts
|
|
18
18
|
*/
|
|
19
19
|
export function bindContext(init = {}) {
|
|
20
|
-
const $ = contextOf(this);
|
|
21
|
-
if (!fs.statSync.call(
|
|
22
|
-
throw UV('ENOTDIR', { syscall: 'chroot', path:
|
|
20
|
+
const $ = contextOf(this), parent = $.parent || defaultContext;
|
|
21
|
+
if (!fs.statSync.call(parent, parent.root).isDirectory())
|
|
22
|
+
throw UV('ENOTDIR', { syscall: 'chroot', path: parent.root });
|
|
23
23
|
const ctx = createChildContext($, init);
|
|
24
24
|
const bound = Object.assign(ctx, {
|
|
25
25
|
fs: {
|
|
@@ -46,7 +46,7 @@ let _nextId = 2;
|
|
|
46
46
|
*/
|
|
47
47
|
export function createChildContext(parent, init = {}) {
|
|
48
48
|
assertContext(parent);
|
|
49
|
-
const { root = parent.root, pwd = parent.pwd, credentials = structuredClone(parent.credentials), mounts
|
|
49
|
+
const { root = parent.root, pwd = parent.pwd, credentials = structuredClone(parent.credentials), mounts } = init;
|
|
50
50
|
const ctx = {
|
|
51
51
|
[kIsContext]: true,
|
|
52
52
|
id: _nextId++,
|
|
@@ -56,7 +56,7 @@ export function createChildContext(parent, init = {}) {
|
|
|
56
56
|
descriptors: new Map(),
|
|
57
57
|
parent: parent,
|
|
58
58
|
children: [],
|
|
59
|
-
mounts: new Map(Object.entries(mounts)),
|
|
59
|
+
mounts: mounts ? new Map(Object.entries(mounts)) : parent.mounts,
|
|
60
60
|
};
|
|
61
61
|
Object.defineProperties(ctx, {
|
|
62
62
|
id: { configurable: false, writable: false },
|
|
@@ -4,10 +4,11 @@ import type { CreationOptions } from './filesystem.js';
|
|
|
4
4
|
import { Inode, type InodeLike } from './inode.js';
|
|
5
5
|
/**
|
|
6
6
|
* A device
|
|
7
|
-
* @todo Maybe add some other device information, like a UUID?
|
|
8
7
|
* @category Internals
|
|
9
8
|
* @privateRemarks
|
|
10
9
|
* UUIDs were considered, however they don't make sense without an easy mechanism for persistence
|
|
10
|
+
* @deprecated Use `@zenfs/linux`!
|
|
11
|
+
* @todo [breaking] remove
|
|
11
12
|
*/
|
|
12
13
|
export interface Device<TData = any> {
|
|
13
14
|
/**
|
|
@@ -34,6 +35,8 @@ export interface Device<TData = any> {
|
|
|
34
35
|
}
|
|
35
36
|
/**
|
|
36
37
|
* @category Internals
|
|
38
|
+
* @deprecated Use `@zenfs/linux`!
|
|
39
|
+
* @todo [breaking] remove
|
|
37
40
|
*/
|
|
38
41
|
export interface DeviceInit<TData = any> {
|
|
39
42
|
data?: TData;
|
|
@@ -45,6 +48,8 @@ export interface DeviceInit<TData = any> {
|
|
|
45
48
|
/**
|
|
46
49
|
* A device driver
|
|
47
50
|
* @category Internals
|
|
51
|
+
* @deprecated Use `@zenfs/linux`!
|
|
52
|
+
* @todo [breaking] remove
|
|
48
53
|
*/
|
|
49
54
|
export interface DeviceDriver<TData = any> {
|
|
50
55
|
/**
|
|
@@ -88,6 +93,8 @@ export interface DeviceDriver<TData = any> {
|
|
|
88
93
|
/**
|
|
89
94
|
* A temporary file system that manages and interfaces with devices
|
|
90
95
|
* @category Internals
|
|
96
|
+
* @deprecated Use `@zenfs/linux`!
|
|
97
|
+
* @todo [breaking] remove
|
|
91
98
|
*/
|
|
92
99
|
export declare class DeviceFS extends StoreFS<InMemoryStore> {
|
|
93
100
|
protected readonly devices: Map<string, Device<any>>;
|
|
@@ -132,6 +139,8 @@ export declare class DeviceFS extends StoreFS<InMemoryStore> {
|
|
|
132
139
|
* - Writes discard data, advancing the file position.
|
|
133
140
|
* @category Internals
|
|
134
141
|
* @internal
|
|
142
|
+
* @deprecated Use `@zenfs/linux`!
|
|
143
|
+
* @todo [breaking] remove
|
|
135
144
|
*/
|
|
136
145
|
export declare const nullDevice: DeviceDriver;
|
|
137
146
|
/**
|
|
@@ -144,6 +153,8 @@ export declare const nullDevice: DeviceDriver;
|
|
|
144
153
|
* - Provides basic file metadata, treating it as a character device.
|
|
145
154
|
* @category Internals
|
|
146
155
|
* @internal
|
|
156
|
+
* @deprecated Use `@zenfs/linux`!
|
|
157
|
+
* @todo [breaking] remove
|
|
147
158
|
*/
|
|
148
159
|
export declare const zeroDevice: DeviceDriver;
|
|
149
160
|
/**
|
|
@@ -152,6 +163,8 @@ export declare const zeroDevice: DeviceDriver;
|
|
|
152
163
|
* - Writes always fail with ENOSPC (no space left on device).
|
|
153
164
|
* @category Internals
|
|
154
165
|
* @internal
|
|
166
|
+
* @deprecated Use `@zenfs/linux`!
|
|
167
|
+
* @todo [breaking] remove
|
|
155
168
|
*/
|
|
156
169
|
export declare const fullDevice: DeviceDriver;
|
|
157
170
|
/**
|
|
@@ -160,12 +173,16 @@ export declare const fullDevice: DeviceDriver;
|
|
|
160
173
|
* - Writes discard data, advancing the file position.
|
|
161
174
|
* @category Internals
|
|
162
175
|
* @internal
|
|
176
|
+
* @deprecated Use `@zenfs/linux`!
|
|
177
|
+
* @todo [breaking] remove
|
|
163
178
|
*/
|
|
164
179
|
export declare const randomDevice: DeviceDriver;
|
|
165
180
|
/**
|
|
166
181
|
* Shortcuts for importing.
|
|
167
182
|
* @category Internals
|
|
168
183
|
* @internal
|
|
184
|
+
* @deprecated Use `@zenfs/linux`!
|
|
185
|
+
* @todo [breaking] remove
|
|
169
186
|
*/
|
|
170
187
|
export declare const devices: {
|
|
171
188
|
null: DeviceDriver<any>;
|
package/dist/internal/devices.js
CHANGED
|
@@ -13,6 +13,8 @@ import { Inode } from './inode.js';
|
|
|
13
13
|
/**
|
|
14
14
|
* A temporary file system that manages and interfaces with devices
|
|
15
15
|
* @category Internals
|
|
16
|
+
* @deprecated Use `@zenfs/linux`!
|
|
17
|
+
* @todo [breaking] remove
|
|
16
18
|
*/
|
|
17
19
|
export class DeviceFS extends StoreFS {
|
|
18
20
|
devices = new Map();
|
|
@@ -231,6 +233,8 @@ const emptyBuffer = new Uint8Array();
|
|
|
231
233
|
* - Writes discard data, advancing the file position.
|
|
232
234
|
* @category Internals
|
|
233
235
|
* @internal
|
|
236
|
+
* @deprecated Use `@zenfs/linux`!
|
|
237
|
+
* @todo [breaking] remove
|
|
234
238
|
*/
|
|
235
239
|
export const nullDevice = {
|
|
236
240
|
name: 'null',
|
|
@@ -255,6 +259,8 @@ export const nullDevice = {
|
|
|
255
259
|
* - Provides basic file metadata, treating it as a character device.
|
|
256
260
|
* @category Internals
|
|
257
261
|
* @internal
|
|
262
|
+
* @deprecated Use `@zenfs/linux`!
|
|
263
|
+
* @todo [breaking] remove
|
|
258
264
|
*/
|
|
259
265
|
export const zeroDevice = {
|
|
260
266
|
name: 'zero',
|
|
@@ -275,6 +281,8 @@ export const zeroDevice = {
|
|
|
275
281
|
* - Writes always fail with ENOSPC (no space left on device).
|
|
276
282
|
* @category Internals
|
|
277
283
|
* @internal
|
|
284
|
+
* @deprecated Use `@zenfs/linux`!
|
|
285
|
+
* @todo [breaking] remove
|
|
278
286
|
*/
|
|
279
287
|
export const fullDevice = {
|
|
280
288
|
name: 'full',
|
|
@@ -295,6 +303,8 @@ export const fullDevice = {
|
|
|
295
303
|
* - Writes discard data, advancing the file position.
|
|
296
304
|
* @category Internals
|
|
297
305
|
* @internal
|
|
306
|
+
* @deprecated Use `@zenfs/linux`!
|
|
307
|
+
* @todo [breaking] remove
|
|
298
308
|
*/
|
|
299
309
|
export const randomDevice = {
|
|
300
310
|
name: 'random',
|
|
@@ -315,6 +325,8 @@ export const randomDevice = {
|
|
|
315
325
|
* Simulates the `/dev/console` device.
|
|
316
326
|
* @category Internals
|
|
317
327
|
* @experimental @internal
|
|
328
|
+
* @deprecated Use `@zenfs/linux`!
|
|
329
|
+
* @todo [breaking] remove
|
|
318
330
|
*/
|
|
319
331
|
const consoleDevice = {
|
|
320
332
|
name: 'console',
|
|
@@ -334,6 +346,8 @@ const consoleDevice = {
|
|
|
334
346
|
* Shortcuts for importing.
|
|
335
347
|
* @category Internals
|
|
336
348
|
* @internal
|
|
349
|
+
* @deprecated Use `@zenfs/linux`!
|
|
350
|
+
* @todo [breaking] remove
|
|
337
351
|
*/
|
|
338
352
|
export const devices = {
|
|
339
353
|
null: nullDevice,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
2
|
import { withErrno } from 'kerium';
|
|
3
3
|
import { ioctl_default_ops_async, ioctl_default_ops_sync } from './ioctl.js';
|
|
4
|
-
|
|
4
|
+
/** Default bytes per chunk for streamed reads, matching Node's default stream `highWaterMark` */
|
|
5
|
+
const defaultChunkSize = 0x10000;
|
|
5
6
|
/**
|
|
6
7
|
* Provides a consistent and easy to use internal API.
|
|
7
8
|
* Default implementations for `exists` and `existsSync` are included.
|
|
@@ -97,17 +98,22 @@ export class FileSystem {
|
|
|
97
98
|
* @privateRemarks The default implementation of `streamRead` uses "chunked" `read`s
|
|
98
99
|
*/
|
|
99
100
|
streamRead(path, options) {
|
|
101
|
+
const { chunkSize = defaultChunkSize } = options;
|
|
102
|
+
let offset = options.start ?? 0;
|
|
103
|
+
let end = options.end;
|
|
100
104
|
return new ReadableStream({
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const buffer = new Uint8Array(bytesRead);
|
|
107
|
-
await this.read(path, buffer, offset, offset + bytesRead).catch(controller.error.bind(controller));
|
|
108
|
-
controller.enqueue(buffer);
|
|
105
|
+
pull: async (controller) => {
|
|
106
|
+
end ??= (await this.stat(path)).size;
|
|
107
|
+
if (offset >= end) {
|
|
108
|
+
controller.close();
|
|
109
|
+
return;
|
|
109
110
|
}
|
|
110
|
-
|
|
111
|
+
const buffer = new Uint8Array(Math.min(chunkSize, end - offset));
|
|
112
|
+
await this.read(path, buffer, offset, offset + buffer.byteLength);
|
|
113
|
+
offset += buffer.byteLength;
|
|
114
|
+
controller.enqueue(buffer);
|
|
115
|
+
if (offset >= end)
|
|
116
|
+
controller.close();
|
|
111
117
|
},
|
|
112
118
|
type: 'bytes',
|
|
113
119
|
});
|
|
@@ -5,6 +5,8 @@ import { Inode, type InodeLike } from './inode.js';
|
|
|
5
5
|
* A file system that uses an `Index` for metadata.
|
|
6
6
|
* @category Internals
|
|
7
7
|
* @internal
|
|
8
|
+
*
|
|
9
|
+
* @todo [breaking] replace the three method `create` system with something more intuitive, e.g. requiring subclasses to implement `createFile`, with this just exposing `create` for convenience.
|
|
8
10
|
*/
|
|
9
11
|
export declare abstract class IndexFS extends FileSystem {
|
|
10
12
|
readonly index: Index;
|
|
@@ -27,6 +29,10 @@ export declare abstract class IndexFS extends FileSystem {
|
|
|
27
29
|
unlinkSync(path: string): void;
|
|
28
30
|
rmdir(path: string): Promise<void>;
|
|
29
31
|
rmdirSync(path: string): void;
|
|
32
|
+
/** Create a new file in the underlying backend */
|
|
33
|
+
protected _create?(path: string, inode: Inode): Promise<void>;
|
|
34
|
+
/** Create a new file in the underlying backend */
|
|
35
|
+
protected _createSync?(path: string, inode: Inode): void;
|
|
30
36
|
protected create(path: string, options: CreationOptions): Inode;
|
|
31
37
|
createFile(path: string, options: CreationOptions): Promise<Inode>;
|
|
32
38
|
createFileSync(path: string, options: CreationOptions): Inode;
|
|
@@ -11,6 +11,8 @@ import { Inode } from './inode.js';
|
|
|
11
11
|
* A file system that uses an `Index` for metadata.
|
|
12
12
|
* @category Internals
|
|
13
13
|
* @internal
|
|
14
|
+
*
|
|
15
|
+
* @todo [breaking] replace the three method `create` system with something more intuitive, e.g. requiring subclasses to implement `createFile`, with this just exposing `create` for convenience.
|
|
14
16
|
*/
|
|
15
17
|
export class IndexFS extends FileSystem {
|
|
16
18
|
index;
|
|
@@ -165,10 +167,26 @@ export class IndexFS extends FileSystem {
|
|
|
165
167
|
return inode;
|
|
166
168
|
}
|
|
167
169
|
async createFile(path, options) {
|
|
168
|
-
|
|
170
|
+
const inode = this.create(path, { ...options, mode: options.mode | S_IFREG });
|
|
171
|
+
try {
|
|
172
|
+
await this._create?.(path, inode);
|
|
173
|
+
return inode;
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
this.index.delete(path);
|
|
177
|
+
throw e;
|
|
178
|
+
}
|
|
169
179
|
}
|
|
170
180
|
createFileSync(path, options) {
|
|
171
|
-
|
|
181
|
+
const inode = this.create(path, { ...options, mode: options.mode | S_IFREG });
|
|
182
|
+
try {
|
|
183
|
+
this._createSync?.(path, inode);
|
|
184
|
+
return inode;
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
this.index.delete(path);
|
|
188
|
+
throw e;
|
|
189
|
+
}
|
|
172
190
|
}
|
|
173
191
|
async mkdir(path, options) {
|
|
174
192
|
options = { ...options, mode: options.mode | S_IFDIR };
|
package/dist/mixins/async.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Semaphore } from 'kerium/locks';
|
|
1
2
|
import type { FileSystem } from '../internal/filesystem.js';
|
|
2
3
|
import { type _SyncFSKeys, type AsyncFSMethods, type Mixin } from './shared.js';
|
|
3
4
|
/**
|
|
@@ -12,13 +13,11 @@ export type AsyncOperation = {
|
|
|
12
13
|
* @category Internals
|
|
13
14
|
*/
|
|
14
15
|
export interface AsyncMixin extends Pick<FileSystem, Exclude<_SyncFSKeys, 'existsSync'>> {
|
|
15
|
-
/**
|
|
16
|
-
* @internal @protected
|
|
17
|
-
*/
|
|
16
|
+
/** @internal @protected */
|
|
18
17
|
_sync?: FileSystem;
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
/** Restrict how many files can be copied across at once @internal @protected */
|
|
19
|
+
_crossCopySemaphore?: Semaphore;
|
|
20
|
+
/** @deprecated Use {@link sync | `sync`} instead */
|
|
22
21
|
queueDone(): Promise<void>;
|
|
23
22
|
ready(): Promise<void>;
|
|
24
23
|
sync(): Promise<void>;
|
package/dist/mixins/async.js
CHANGED
|
@@ -1,3 +1,55 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
1
53
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
54
|
import { withErrno } from 'kerium';
|
|
3
55
|
import { crit, debug, err } from 'kerium/log';
|
|
@@ -164,19 +216,30 @@ export function Async(FS) {
|
|
|
164
216
|
},
|
|
165
217
|
});
|
|
166
218
|
}
|
|
167
|
-
/**
|
|
168
|
-
|
|
169
|
-
|
|
219
|
+
/** @internal @protected */
|
|
220
|
+
_crossCopySemaphore;
|
|
221
|
+
/** @internal */
|
|
170
222
|
async crossCopy(path) {
|
|
171
223
|
this.checkSync();
|
|
172
224
|
const stats = await this.stat(path);
|
|
173
225
|
if (!isDirectory(stats)) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
226
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
227
|
+
try {
|
|
228
|
+
const _ = __addDisposableResource(env_1, (await this._crossCopySemaphore?.get()) || { [Symbol.dispose]() { } }, false);
|
|
229
|
+
this._sync.createFileSync(path, stats);
|
|
230
|
+
const buffer = new Uint8Array(stats.size);
|
|
231
|
+
await this.read(path, buffer, 0, stats.size);
|
|
232
|
+
this._sync.writeSync(path, buffer, 0);
|
|
233
|
+
this._sync.touchSync(path, stats);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
catch (e_1) {
|
|
237
|
+
env_1.error = e_1;
|
|
238
|
+
env_1.hasError = true;
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
__disposeResources(env_1);
|
|
242
|
+
}
|
|
180
243
|
}
|
|
181
244
|
if (path !== '/') {
|
|
182
245
|
this._sync.mkdirSync(path, stats);
|
package/dist/node/streams.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/// <reference path="../../types/readable-stream.d.ts" preserve="true" />
|
|
2
1
|
import type { Abortable } from 'node:events';
|
|
3
2
|
import type * as fs from 'node:fs';
|
|
4
3
|
import type { CreateReadStreamOptions, CreateWriteStreamOptions } from 'node:fs/promises';
|
|
5
4
|
import type { Callback } from '../utils.js';
|
|
6
5
|
import type { FileHandle } from './promises.js';
|
|
7
|
-
import { Readable
|
|
6
|
+
import { Readable } from '@zenfs/streams/readable';
|
|
7
|
+
import { Writable } from '@zenfs/streams/writable';
|
|
8
8
|
interface FSImplementation {
|
|
9
9
|
open?: (...args: unknown[]) => unknown;
|
|
10
10
|
close?: (...args: unknown[]) => unknown;
|
package/dist/node/streams.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { eos } from '@zenfs/streams/end-of-stream';
|
|
2
|
+
import { Readable } from '@zenfs/streams/readable';
|
|
3
|
+
import { Writable } from '@zenfs/streams/writable';
|
|
1
4
|
import { Errno, Exception, UV } from 'kerium';
|
|
2
5
|
import { warn } from 'kerium/log';
|
|
3
|
-
import { finished, Readable, Writable } from 'readable-stream';
|
|
4
6
|
/**
|
|
5
7
|
* A ReadStream implementation that wraps an underlying global ReadableStream.
|
|
6
8
|
*/
|
|
@@ -16,7 +18,11 @@ export class ReadStream extends Readable {
|
|
|
16
18
|
.then(handle => {
|
|
17
19
|
this._path = handle['vfs'].path;
|
|
18
20
|
// `end` is inclusive for streams, but the internal range is not
|
|
19
|
-
const internal = handle.readableWebStream({
|
|
21
|
+
const internal = handle.readableWebStream({
|
|
22
|
+
start: opts.start,
|
|
23
|
+
end: opts.end === undefined ? undefined : opts.end + 1,
|
|
24
|
+
chunkSize: opts.highWaterMark,
|
|
25
|
+
});
|
|
20
26
|
this.reader = internal.getReader();
|
|
21
27
|
this.pending = false;
|
|
22
28
|
})
|
|
@@ -46,7 +52,7 @@ export class ReadStream extends Readable {
|
|
|
46
52
|
* Closing before the stream has finished reports `ERR_STREAM_PREMATURE_CLOSE`.
|
|
47
53
|
*/
|
|
48
54
|
close(callback = () => null) {
|
|
49
|
-
|
|
55
|
+
eos(this, error => callback(error ?? null));
|
|
50
56
|
this.destroy();
|
|
51
57
|
}
|
|
52
58
|
get path() {
|
package/dist/node/sync.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BufferView } from 'node:buffer';
|
|
1
2
|
import type * as fs from 'node:fs';
|
|
2
3
|
import type { V_Context } from '../context.js';
|
|
3
4
|
import type { FileContents } from '../vfs/shared.js';
|
|
@@ -72,14 +73,14 @@ export declare function lopenSync(this: V_Context, path: fs.PathLike, flag: stri
|
|
|
72
73
|
* Synchronously reads the entire contents of a file.
|
|
73
74
|
* @option encoding The string encoding for the file contents. Defaults to `null`.
|
|
74
75
|
* @option flag Defaults to `'r'`.
|
|
76
|
+
* @option buffer A view (or a factory taking the file size) the contents are read into,
|
|
77
|
+
* instead of allocating a new buffer. It must be large enough to hold the entire file.
|
|
75
78
|
* @returns file contents
|
|
76
79
|
*/
|
|
77
|
-
export declare function readFileSync(this: V_Context, path: fs.PathOrFileDescriptor, options
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
export declare function readFileSync(this: V_Context, path: fs.PathOrFileDescriptor, options
|
|
81
|
-
flag?: string;
|
|
82
|
-
}) | BufferEncoding | null): string;
|
|
80
|
+
export declare function readFileSync<T extends NodeJS.ArrayBufferView>(this: V_Context, path: fs.PathOrFileDescriptor, options: fs.ReadFileSyncOptionsWithBuffer<T>): BufferView<T>;
|
|
81
|
+
export declare function readFileSync(this: V_Context, path: fs.PathOrFileDescriptor, options?: fs.ReadFileSyncOptionsWithBufferEncoding | null): NonSharedBuffer;
|
|
82
|
+
export declare function readFileSync(this: V_Context, path: fs.PathOrFileDescriptor, options: fs.ReadFileSyncOptionsWithStringEncoding | BufferEncoding): string;
|
|
83
|
+
export declare function readFileSync(this: V_Context, path: fs.PathOrFileDescriptor, options: fs.ReadFileSyncOptions): string | NonSharedBuffer;
|
|
83
84
|
/**
|
|
84
85
|
* Synchronously writes data to a file, replacing the file if it already exists.
|
|
85
86
|
*
|
package/dist/node/sync.js
CHANGED
|
@@ -175,6 +175,22 @@ openSync;
|
|
|
175
175
|
export function lopenSync(path, flag, mode) {
|
|
176
176
|
return toFD(_sync.open(this, path, { flag, mode, preserveSymlinks: true }));
|
|
177
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Resolves the `buffer` option of `readFileSync` into a buffer covering the first `size` bytes of the view.
|
|
180
|
+
* @hidden
|
|
181
|
+
*/
|
|
182
|
+
function intoBuffer(into, size) {
|
|
183
|
+
const isFn = typeof into == 'function';
|
|
184
|
+
const view = isFn ? into(size) : into;
|
|
185
|
+
const name = isFn ? 'options.buffer()' : 'options.buffer';
|
|
186
|
+
if (!ArrayBuffer.isView(view))
|
|
187
|
+
throw Object.assign(new TypeError(`The "options.buffer" property must be of type function or an instance of Buffer, TypedArray, or DataView. Received ${typeof view}`), { code: 'ERR_INVALID_ARG_TYPE' });
|
|
188
|
+
if (view.byteLength < size)
|
|
189
|
+
throw Object.assign(new TypeError(`The property '${name}.byteLength' is smaller than the file size of ${size} bytes. Received ${view.byteLength}`), {
|
|
190
|
+
code: 'ERR_INVALID_ARG_VALUE',
|
|
191
|
+
});
|
|
192
|
+
return Buffer.from(view.buffer, view.byteOffset, size);
|
|
193
|
+
}
|
|
178
194
|
export function readFileSync(path, _options = {}) {
|
|
179
195
|
const env_3 = { stack: [], error: void 0, hasError: false };
|
|
180
196
|
try {
|
|
@@ -186,7 +202,8 @@ export function readFileSync(path, _options = {}) {
|
|
|
186
202
|
? fromFD(this, path)
|
|
187
203
|
: _sync.open(this, path.toString(), { flag: options.flag, mode: 0o644, preserveSymlinks: false }), false);
|
|
188
204
|
const { size } = file.stat();
|
|
189
|
-
const
|
|
205
|
+
const into = typeof _options == 'object' && _options !== null ? _options.buffer : undefined;
|
|
206
|
+
const data = into === undefined ? Buffer.alloc(size) : intoBuffer(into, size);
|
|
190
207
|
file.readSync(data, 0, size, 0);
|
|
191
208
|
return options.encoding ? data.toString(options.encoding) : data;
|
|
192
209
|
}
|
|
@@ -584,7 +601,7 @@ export function copyFileSync(source, destination, flags) {
|
|
|
584
601
|
destination = normalizePath.call(this, destination);
|
|
585
602
|
if (flags && flags & constants.COPYFILE_EXCL && existsSync.call(this, destination))
|
|
586
603
|
throw UV('EEXIST', 'copyFile', destination);
|
|
587
|
-
writeFileSync.call(this, destination, readFileSync.call(this, source));
|
|
604
|
+
writeFileSync.call(this, destination, readFileSync.call(this, source, {}));
|
|
588
605
|
emitChange(this, 'rename', destination.toString());
|
|
589
606
|
}
|
|
590
607
|
copyFileSync;
|
package/dist/vfs/file.js
CHANGED
|
@@ -93,8 +93,8 @@ import { _chown, InodeFlags, isBlockDevice, isCharacterDevice } from '../interna
|
|
|
93
93
|
import '../polyfills.js';
|
|
94
94
|
import { validateFD } from '../utils.js';
|
|
95
95
|
import { cacheOf } from './vcache.js';
|
|
96
|
-
/**
|
|
97
|
-
const streamChunkSize =
|
|
96
|
+
/** Default bytes per chunk for streamed reads, matching Node's default stream `highWaterMark` */
|
|
97
|
+
const streamChunkSize = 0x10000;
|
|
98
98
|
/**
|
|
99
99
|
* @internal
|
|
100
100
|
*/
|
|
@@ -608,19 +608,25 @@ let Handle = (() => {
|
|
|
608
608
|
if (this.closed)
|
|
609
609
|
throw UV('EBADF', 'streamRead', this.path);
|
|
610
610
|
const { vnode } = this;
|
|
611
|
+
const { chunkSize = streamChunkSize } = options;
|
|
612
|
+
let offset = options.start ?? 0;
|
|
613
|
+
let end = options.end;
|
|
611
614
|
return new ReadableStream({
|
|
612
|
-
async
|
|
615
|
+
async pull(controller) {
|
|
613
616
|
const env_15 = { stack: [], error: void 0, hasError: false };
|
|
614
617
|
try {
|
|
615
618
|
const _ = __addDisposableResource(env_15, await vnode.lock('ro'), false);
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
await vnode.read(buffer, offset, offset + bytesRead).catch(controller.error.bind(controller));
|
|
621
|
-
controller.enqueue(buffer);
|
|
619
|
+
end ??= vnode.inode.size;
|
|
620
|
+
if (offset >= end) {
|
|
621
|
+
controller.close();
|
|
622
|
+
return;
|
|
622
623
|
}
|
|
623
|
-
|
|
624
|
+
const buffer = new Uint8Array(Math.min(chunkSize, end - offset));
|
|
625
|
+
await vnode.read(buffer, offset, offset + buffer.byteLength);
|
|
626
|
+
offset += buffer.byteLength;
|
|
627
|
+
controller.enqueue(buffer);
|
|
628
|
+
if (offset >= end)
|
|
629
|
+
controller.close();
|
|
624
630
|
}
|
|
625
631
|
catch (e_15) {
|
|
626
632
|
env_15.error = e_15;
|
package/dist/vfs/shared.js
CHANGED
|
@@ -128,7 +128,7 @@ export function chroot(path) {
|
|
|
128
128
|
throw UV('EBUSY', 'chroot', handle.path);
|
|
129
129
|
handle.path = handle.path.slice($.root.length);
|
|
130
130
|
}
|
|
131
|
-
if (newRoot.length
|
|
131
|
+
if (newRoot.length < $.root.length)
|
|
132
132
|
throw withErrno('EPERM', 'Can not chroot() outside of current root');
|
|
133
133
|
$.root = newRoot;
|
|
134
134
|
}
|
package/dist/vfs/vnode.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
2
|
import { RwLockable } from 'kerium/locks';
|
|
3
|
-
import { Resource } from 'utilium/cache';
|
|
3
|
+
import { addRange, Resource } from 'utilium/cache';
|
|
4
4
|
import { InodeFlags, isBlockDevice, isCharacterDevice } from '../internal/inode.js';
|
|
5
5
|
/**
|
|
6
6
|
* A VFS-level node, roughly equivalent to Linux's in-memory `struct inode`.
|
|
@@ -57,17 +57,7 @@ export class VNode extends RwLockable {
|
|
|
57
57
|
}
|
|
58
58
|
/** Add a range to `dirtyRanges`, merging overlapping and adjacent ranges */
|
|
59
59
|
markDirty(start, end) {
|
|
60
|
-
this.dirtyRanges
|
|
61
|
-
this.dirtyRanges.sort((a, b) => a.start - b.start);
|
|
62
|
-
const merged = [];
|
|
63
|
-
for (const range of this.dirtyRanges) {
|
|
64
|
-
const last = merged.at(-1);
|
|
65
|
-
if (last && range.start <= last.end)
|
|
66
|
-
last.end = Math.max(last.end, range.end);
|
|
67
|
-
else
|
|
68
|
-
merged.push(range);
|
|
69
|
-
}
|
|
70
|
-
this.dirtyRanges = merged;
|
|
60
|
+
addRange(this.dirtyRanges, start, end);
|
|
71
61
|
}
|
|
72
62
|
/**
|
|
73
63
|
* Copy `[start, end)` from the cache into `buffer`.
|
|
@@ -75,12 +65,10 @@ export class VNode extends RwLockable {
|
|
|
75
65
|
*/
|
|
76
66
|
copyFromCache(buffer, start, end) {
|
|
77
67
|
let pos = start;
|
|
78
|
-
for (const region of this.data.
|
|
68
|
+
for (const region of this.data.regionsIn(start, end)) {
|
|
79
69
|
const regionEnd = region.offset + region.data.byteLength;
|
|
80
70
|
if (regionEnd <= pos)
|
|
81
71
|
continue;
|
|
82
|
-
if (region.offset >= end)
|
|
83
|
-
break;
|
|
84
72
|
if (region.offset > pos) {
|
|
85
73
|
buffer.fill(0, pos - start, region.offset - start);
|
|
86
74
|
pos = region.offset;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenfs/core",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.3-newstreams.0",
|
|
4
4
|
"description": "A filesystem, anywhere",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -69,19 +69,18 @@
|
|
|
69
69
|
"lint": "eslint src tests",
|
|
70
70
|
"test": "npx -s zenfs-test --clean; npx -s zenfs-test -abcpn; tests/fetch/run.sh; npx -s zenfs-test --report-only",
|
|
71
71
|
"test:native": "npx -s zenfs-test tests/native-setup.ts --verbose",
|
|
72
|
-
"build": "tsc
|
|
73
|
-
"
|
|
74
|
-
"dev": "tsc -p tsconfig.json --watch",
|
|
72
|
+
"build": "tsc --build",
|
|
73
|
+
"dev": "tsc --build --watch",
|
|
75
74
|
"prepublishOnly": "npm run build"
|
|
76
75
|
},
|
|
77
76
|
"dependencies": {
|
|
78
77
|
"@types/node": "^26.0.0",
|
|
78
|
+
"@zenfs/streams": "^0.0.1",
|
|
79
79
|
"buffer": "^6.0.3",
|
|
80
80
|
"eventemitter3": "^5.0.1",
|
|
81
|
-
"kerium": "^1.
|
|
81
|
+
"kerium": "^1.5.0",
|
|
82
82
|
"memium": "^1.0.0",
|
|
83
|
-
"
|
|
84
|
-
"utilium": "^3.0.0"
|
|
83
|
+
"utilium": "^3.7.0"
|
|
85
84
|
},
|
|
86
85
|
"devDependencies": {
|
|
87
86
|
"@eslint/js": "^10.0.1",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
-
import { bindContext, configure, fs, InMemory } from '@zenfs/core';
|
|
2
|
+
import { bindContext, configure, fs, InMemory, type BoundContext } from '@zenfs/core';
|
|
3
3
|
import assert from 'node:assert/strict';
|
|
4
4
|
import { suite, test } from 'node:test';
|
|
5
5
|
import { canary } from 'utilium';
|
|
@@ -124,4 +124,14 @@ suite('Context', () => {
|
|
|
124
124
|
assert.equal(ctx.readFileSync('/copy.txt', 'utf-8'), 'not in real root!');
|
|
125
125
|
assert(fs.readdirSync('/ctx').includes('copy.txt'));
|
|
126
126
|
});
|
|
127
|
+
|
|
128
|
+
test('bindContext should resolve root relative to the parent context', () => {
|
|
129
|
+
fs.mkdirSync('/secure');
|
|
130
|
+
const ctx = bindContext({ root: '/secure', credentials: { uid: 333, gid: 333 } });
|
|
131
|
+
|
|
132
|
+
const plain = bindContext({ credentials: { uid: 1, gid: 1 } });
|
|
133
|
+
console.log('child of / context: ok, root =', plain.root);
|
|
134
|
+
|
|
135
|
+
assert.doesNotThrow(() => ctx.bind({ credentials: { uid: 500, gid: 500 } }));
|
|
136
|
+
});
|
|
127
137
|
});
|
package/tests/fs/streams.test.ts
CHANGED
|
@@ -141,4 +141,64 @@ suite('Streams', config('streams'), () => {
|
|
|
141
141
|
// Closing invalidates the descriptor, so it fails range validation
|
|
142
142
|
assert.throws(() => fileHandle.createWriteStream(), { code: 'ERR_OUT_OF_RANGE' });
|
|
143
143
|
});
|
|
144
|
+
|
|
145
|
+
// A file large enough to span several chunks, so the read path has to stitch
|
|
146
|
+
// them back together and the vnode cache has to grow to hold them.
|
|
147
|
+
const large = Buffer.alloc(200 * 1024);
|
|
148
|
+
for (let i = 0; i < large.length; i++) large[i] = (i * 31 + 7) & 0xff;
|
|
149
|
+
|
|
150
|
+
test('createReadStream reads a multi-chunk file byte for byte', async () => {
|
|
151
|
+
await fs.promises.writeFile('large.bin', large);
|
|
152
|
+
|
|
153
|
+
const chunks: Uint8Array[] = [];
|
|
154
|
+
for await (const chunk of fs.createReadStream('large.bin')) chunks.push(chunk as Uint8Array);
|
|
155
|
+
|
|
156
|
+
assert.deepEqual(Buffer.concat(chunks), large);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('createReadStream honors highWaterMark as the chunk size', async () => {
|
|
160
|
+
await fs.promises.writeFile('large.bin', large);
|
|
161
|
+
|
|
162
|
+
const highWaterMark = 64 * 1024;
|
|
163
|
+
const sizes: number[] = [];
|
|
164
|
+
for await (const chunk of fs.createReadStream('large.bin', { highWaterMark })) sizes.push((chunk as Uint8Array).byteLength);
|
|
165
|
+
|
|
166
|
+
// Every chunk but the last is a full highWaterMark, and they sum to the file
|
|
167
|
+
assert.deepEqual(sizes.slice(0, -1), Array(sizes.length - 1).fill(highWaterMark));
|
|
168
|
+
assert.equal(
|
|
169
|
+
sizes.reduce((a, b) => a + b, 0),
|
|
170
|
+
large.length
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('createReadStream with start and end spanning chunks', async () => {
|
|
175
|
+
await fs.promises.writeFile('large.bin', large);
|
|
176
|
+
|
|
177
|
+
// `end` is inclusive, and this range deliberately starts and stops mid-chunk
|
|
178
|
+
const start = 1000;
|
|
179
|
+
const end = 150_000;
|
|
180
|
+
const chunks: Uint8Array[] = [];
|
|
181
|
+
for await (const chunk of fs.createReadStream('large.bin', { start, end, highWaterMark: 8192 })) chunks.push(chunk as Uint8Array);
|
|
182
|
+
|
|
183
|
+
assert.deepEqual(Buffer.concat(chunks), large.subarray(start, end + 1));
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('a read stream does not read past the end of the file', async () => {
|
|
187
|
+
await fs.promises.writeFile('small.bin', large.subarray(0, 10));
|
|
188
|
+
|
|
189
|
+
const chunks: Uint8Array[] = [];
|
|
190
|
+
for await (const chunk of fs.createReadStream('small.bin', { highWaterMark: 64 * 1024 })) chunks.push(chunk as Uint8Array);
|
|
191
|
+
|
|
192
|
+
assert.equal(chunks.length, 1);
|
|
193
|
+
assert.deepEqual(chunks[0], large.subarray(0, 10));
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('a read stream over an empty file ends without a chunk', async () => {
|
|
197
|
+
await fs.promises.writeFile('empty.bin', '');
|
|
198
|
+
|
|
199
|
+
const chunks: unknown[] = [];
|
|
200
|
+
for await (const chunk of fs.createReadStream('empty.bin')) chunks.push(chunk);
|
|
201
|
+
|
|
202
|
+
assert.deepEqual(chunks, []);
|
|
203
|
+
});
|
|
144
204
|
});
|
package/tests/setup.ts
CHANGED
|
@@ -8,7 +8,7 @@ export const data = join(import.meta.dirname, 'data');
|
|
|
8
8
|
|
|
9
9
|
export const tmp = join(import.meta.dirname, 'tmp');
|
|
10
10
|
|
|
11
|
-
if (!existsSync(tmp)) mkdirSync(tmp);
|
|
11
|
+
if (!existsSync(tmp)) mkdirSync(tmp, { recursive: true });
|
|
12
12
|
|
|
13
13
|
export async function copyAsync(_path: string, fs: NodeFS = _fs): Promise<void> {
|
|
14
14
|
const path = relative(data, _path) || '/';
|
package/types/README.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
This directory contains declaration files. Due to numerous TypeScript shenanigans, placing them into `src` is a really bad idea. Instead, we put them here.
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
@types/readable-stream has *many* problems:
|
|
3
|
-
https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/71923
|
|
4
|
-
https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/71307
|
|
5
|
-
https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/71083
|
|
6
|
-
https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/66049
|
|
7
|
-
... and many more
|
|
8
|
-
|
|
9
|
-
This allows us to bypass those problems.
|
|
10
|
-
|
|
11
|
-
Warning: Do not install @types/readable-stream alongside this package!
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/// <reference types="node" preserve="true" />
|
|
15
|
-
declare module 'readable-stream' {
|
|
16
|
-
import { finished, Readable, Writable } from 'node:stream';
|
|
17
|
-
export { finished, Readable, Writable };
|
|
18
|
-
}
|