@zenfs/core 1.11.4 → 2.1.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/dist/backends/backend.d.ts +19 -15
- package/dist/backends/backend.js +36 -19
- package/dist/backends/cow.d.ts +20 -30
- package/dist/backends/cow.js +83 -192
- package/dist/backends/fetch.d.ts +1 -0
- package/dist/backends/fetch.js +30 -30
- package/dist/backends/index.d.ts +1 -1
- package/dist/backends/index.js +1 -1
- package/dist/backends/memory.d.ts +5 -7
- package/dist/backends/memory.js +2 -3
- package/dist/backends/passthrough.d.ts +19 -23
- package/dist/backends/passthrough.js +98 -288
- package/dist/backends/port.d.ts +220 -0
- package/dist/backends/port.js +328 -0
- package/dist/backends/single_buffer.d.ts +59 -47
- package/dist/backends/single_buffer.js +468 -219
- package/dist/backends/store/fs.d.ts +25 -35
- package/dist/backends/store/fs.js +276 -315
- package/dist/backends/store/store.d.ts +10 -15
- package/dist/backends/store/store.js +11 -10
- package/dist/config.d.ts +3 -12
- package/dist/config.js +17 -19
- package/dist/context.d.ts +8 -21
- package/dist/context.js +33 -10
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/internal/contexts.d.ts +63 -0
- package/dist/internal/contexts.js +15 -0
- package/dist/internal/credentials.d.ts +2 -11
- package/dist/internal/credentials.js +0 -19
- package/dist/internal/devices.d.ts +18 -80
- package/dist/internal/devices.js +103 -316
- package/dist/internal/error.d.ts +9 -204
- package/dist/internal/error.js +19 -288
- package/dist/internal/file_index.d.ts +1 -1
- package/dist/internal/file_index.js +11 -11
- package/dist/internal/filesystem.d.ts +51 -94
- package/dist/internal/filesystem.js +21 -20
- package/dist/internal/index.d.ts +1 -2
- package/dist/internal/index.js +1 -2
- package/dist/internal/index_fs.d.ts +12 -30
- package/dist/internal/index_fs.js +37 -69
- package/dist/internal/inode.d.ts +140 -24
- package/dist/internal/inode.js +515 -66
- package/dist/mixins/async.js +52 -112
- package/dist/mixins/mutexed.d.ts +19 -18
- package/dist/mixins/mutexed.js +62 -64
- package/dist/mixins/readonly.d.ts +7 -6
- package/dist/mixins/readonly.js +24 -18
- package/dist/mixins/sync.js +8 -8
- package/dist/{vfs/path.d.ts → path.d.ts} +3 -4
- package/dist/{vfs/path.js → path.js} +6 -9
- package/dist/polyfills.js +1 -1
- package/dist/readline.d.ts +134 -0
- package/dist/readline.js +623 -0
- package/dist/utils.d.ts +9 -37
- package/dist/utils.js +17 -85
- package/dist/vfs/acl.d.ts +42 -0
- package/dist/vfs/acl.js +268 -0
- package/dist/vfs/async.d.ts +9 -23
- package/dist/vfs/async.js +25 -27
- package/dist/vfs/config.d.ts +6 -18
- package/dist/vfs/config.js +8 -18
- package/dist/vfs/dir.d.ts +3 -3
- package/dist/vfs/dir.js +12 -12
- package/dist/vfs/file.d.ts +106 -0
- package/dist/vfs/file.js +244 -0
- package/dist/vfs/flags.d.ts +19 -0
- package/dist/vfs/flags.js +62 -0
- package/dist/vfs/index.d.ts +4 -10
- package/dist/vfs/index.js +4 -13
- package/dist/vfs/ioctl.d.ts +88 -0
- package/dist/vfs/ioctl.js +409 -0
- package/dist/vfs/promises.d.ts +81 -19
- package/dist/vfs/promises.js +404 -288
- package/dist/vfs/shared.d.ts +7 -37
- package/dist/vfs/shared.js +29 -85
- package/dist/{stats.d.ts → vfs/stats.d.ts} +14 -28
- package/dist/{stats.js → vfs/stats.js} +11 -66
- package/dist/vfs/streams.d.ts +1 -0
- package/dist/vfs/streams.js +32 -27
- package/dist/vfs/sync.d.ts +3 -3
- package/dist/vfs/sync.js +263 -260
- package/dist/vfs/watchers.d.ts +2 -2
- package/dist/vfs/watchers.js +12 -12
- package/dist/vfs/xattr.d.ts +116 -0
- package/dist/vfs/xattr.js +201 -0
- package/package.json +5 -3
- package/readme.md +1 -1
- package/scripts/test.js +2 -2
- package/tests/assignment.ts +1 -1
- package/tests/backend/config.worker.js +4 -1
- package/tests/backend/fetch.test.ts +3 -0
- package/tests/backend/port.test.ts +19 -33
- package/tests/backend/remote.worker.js +4 -1
- package/tests/backend/single-buffer.test.ts +53 -0
- package/tests/backend/single-buffer.worker.js +30 -0
- package/tests/common/context.test.ts +3 -3
- package/tests/common/handle.test.ts +17 -12
- package/tests/common/mutex.test.ts +9 -9
- package/tests/common/path.test.ts +1 -1
- package/tests/common/readline.test.ts +104 -0
- package/tests/common.ts +4 -19
- package/tests/fetch/fetch.ts +2 -2
- package/tests/fs/append.test.ts +4 -4
- package/tests/fs/directory.test.ts +25 -25
- package/tests/fs/errors.test.ts +15 -19
- package/tests/fs/links.test.ts +4 -3
- package/tests/fs/open.test.ts +4 -21
- package/tests/fs/permissions.test.ts +14 -18
- package/tests/fs/read.test.ts +10 -9
- package/tests/fs/readFile.test.ts +10 -26
- package/tests/fs/rename.test.ts +4 -9
- package/tests/fs/stat.test.ts +8 -8
- package/tests/fs/streams.test.ts +2 -11
- package/tests/fs/times.test.ts +7 -7
- package/tests/fs/truncate.test.ts +8 -36
- package/tests/fs/watch.test.ts +10 -10
- package/tests/fs/write.test.ts +77 -13
- package/tests/fs/xattr.test.ts +85 -0
- package/tests/logs.js +22 -0
- package/tests/setup/context.ts +1 -1
- package/tests/setup/index.ts +3 -3
- package/tests/setup/port.ts +7 -1
- package/dist/backends/port/fs.d.ts +0 -84
- package/dist/backends/port/fs.js +0 -151
- package/dist/backends/port/rpc.d.ts +0 -77
- package/dist/backends/port/rpc.js +0 -100
- package/dist/backends/store/simple.d.ts +0 -20
- package/dist/backends/store/simple.js +0 -13
- package/dist/internal/file.d.ts +0 -359
- package/dist/internal/file.js +0 -751
- package/dist/internal/log.d.ts +0 -133
- package/dist/internal/log.js +0 -218
- package/tests/fs/writeFile.test.ts +0 -70
package/dist/vfs/watchers.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { EventEmitter as NodeEventEmitter } from 'node:events';
|
|
|
2
2
|
import type * as fs from 'node:fs';
|
|
3
3
|
import type { V_Context } from '../context.js';
|
|
4
4
|
import { EventEmitter } from 'eventemitter3';
|
|
5
|
-
import { type Stats } from '
|
|
5
|
+
import { type Stats } from './stats.js';
|
|
6
6
|
/**
|
|
7
7
|
* Base class for file system watchers.
|
|
8
8
|
* Provides event handling capabilities for watching file system changes.
|
|
@@ -75,5 +75,5 @@ export declare function removeWatcher(path: string, watcher: FSWatcher): void;
|
|
|
75
75
|
/**
|
|
76
76
|
* @internal @hidden
|
|
77
77
|
*/
|
|
78
|
-
export declare function emitChange(
|
|
78
|
+
export declare function emitChange($: V_Context, eventType: fs.WatchEventType, filename: string): void;
|
|
79
79
|
export {};
|
package/dist/vfs/watchers.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { EventEmitter } from 'eventemitter3';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { UV } from 'kerium';
|
|
3
|
+
import { basename, dirname, join, relative } from '../path.js';
|
|
4
4
|
import { normalizePath } from '../utils.js';
|
|
5
|
-
import {
|
|
5
|
+
import { isStatsEqual } from './stats.js';
|
|
6
6
|
import { statSync } from './sync.js';
|
|
7
7
|
/**
|
|
8
8
|
* Base class for file system watchers.
|
|
@@ -29,19 +29,19 @@ class Watcher extends EventEmitter {
|
|
|
29
29
|
this.path = path;
|
|
30
30
|
}
|
|
31
31
|
setMaxListeners() {
|
|
32
|
-
throw
|
|
32
|
+
throw UV('ENOSYS', 'Watcher.setMaxListeners');
|
|
33
33
|
}
|
|
34
34
|
getMaxListeners() {
|
|
35
|
-
throw
|
|
35
|
+
throw UV('ENOSYS', 'Watcher.getMaxListeners');
|
|
36
36
|
}
|
|
37
37
|
prependListener() {
|
|
38
|
-
throw
|
|
38
|
+
throw UV('ENOSYS', 'Watcher.prependListener');
|
|
39
39
|
}
|
|
40
40
|
prependOnceListener() {
|
|
41
|
-
throw
|
|
41
|
+
throw UV('ENOSYS', 'Watcher.prependOnceListener');
|
|
42
42
|
}
|
|
43
43
|
rawListeners() {
|
|
44
|
-
throw
|
|
44
|
+
throw UV('ENOSYS', 'Watcher.rawListeners');
|
|
45
45
|
}
|
|
46
46
|
ref() {
|
|
47
47
|
return this;
|
|
@@ -138,10 +138,10 @@ export function removeWatcher(path, watcher) {
|
|
|
138
138
|
/**
|
|
139
139
|
* @internal @hidden
|
|
140
140
|
*/
|
|
141
|
-
export function emitChange(
|
|
141
|
+
export function emitChange($, eventType, filename) {
|
|
142
142
|
var _a;
|
|
143
|
-
if (
|
|
144
|
-
filename = join((_a =
|
|
143
|
+
if ($)
|
|
144
|
+
filename = join((_a = $.root) !== null && _a !== void 0 ? _a : '/', filename);
|
|
145
145
|
filename = normalizePath(filename);
|
|
146
146
|
// Notify watchers, including ones on parent directories if they are watching recursively
|
|
147
147
|
for (let path = filename; path != '/'; path = dirname(path)) {
|
|
@@ -149,7 +149,7 @@ export function emitChange(context, eventType, filename) {
|
|
|
149
149
|
if (!watchersForPath)
|
|
150
150
|
continue;
|
|
151
151
|
for (const watcher of watchersForPath) {
|
|
152
|
-
watcher.emit('change', eventType, relative(path, filename) || basename(filename));
|
|
152
|
+
watcher.emit('change', eventType, relative.call(watcher._context, path, filename) || basename(filename));
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { BufferEncodingOption, ObjectEncodingOptions } from 'node:fs';
|
|
2
|
+
import type { V_Context } from '../context.js';
|
|
3
|
+
/**
|
|
4
|
+
* Extended attribute name with namespace prefix.
|
|
5
|
+
* Format is namespace.attributename where namespace is one of:
|
|
6
|
+
* - user: User attributes
|
|
7
|
+
* - trusted: Trusted attributes (privileged)
|
|
8
|
+
* - system: System attributes
|
|
9
|
+
* - security: Security attributes
|
|
10
|
+
*
|
|
11
|
+
* Note: Currently only the 'user' namespace is supported.
|
|
12
|
+
*/
|
|
13
|
+
export type Name = `${'user' | 'trusted' | 'system' | 'security'}.${string}`;
|
|
14
|
+
/**
|
|
15
|
+
* Options for xattr operations.
|
|
16
|
+
*/
|
|
17
|
+
export interface Options {
|
|
18
|
+
/**
|
|
19
|
+
* If true, don't follow symlinks.
|
|
20
|
+
* @default false
|
|
21
|
+
*/
|
|
22
|
+
noFollow?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Encoding for attribute values.
|
|
25
|
+
* If 'buffer' or undefined, the value is returned as a Buffer.
|
|
26
|
+
* Otherwise, the value is returned as a string using the specified encoding.
|
|
27
|
+
* @default undefined
|
|
28
|
+
*/
|
|
29
|
+
encoding?: BufferEncoding | 'buffer';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Options for setting extended attributes.
|
|
33
|
+
* Extends the base Options with additional flags for create/replace behavior.
|
|
34
|
+
*/
|
|
35
|
+
export interface SetOptions extends Options {
|
|
36
|
+
/**
|
|
37
|
+
* If true, fail if the attribute already exists.
|
|
38
|
+
* @default false
|
|
39
|
+
*/
|
|
40
|
+
create?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* If true, fail if the attribute does not exist.
|
|
43
|
+
* @default false
|
|
44
|
+
*/
|
|
45
|
+
replace?: boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Gets the value of an extended attribute.
|
|
49
|
+
*
|
|
50
|
+
* @param path Path to the file
|
|
51
|
+
* @param name Name of the attribute to get
|
|
52
|
+
* @param opt Options for the operation
|
|
53
|
+
* @returns A buffer containing the attribute value when encoding is 'buffer' or undefined, or a string when a string encoding is specified
|
|
54
|
+
*/
|
|
55
|
+
export declare function get(this: V_Context, path: string, name: Name, opt?: Options & (BufferEncodingOption | {
|
|
56
|
+
encoding?: null;
|
|
57
|
+
})): Promise<Uint8Array>;
|
|
58
|
+
export declare function get(this: V_Context, path: string, name: Name, opt: Options & ObjectEncodingOptions): Promise<string>;
|
|
59
|
+
/**
|
|
60
|
+
* Synchronously gets the value of an extended attribute.
|
|
61
|
+
*
|
|
62
|
+
* @param path Path to the file
|
|
63
|
+
* @param name Name of the attribute to get
|
|
64
|
+
* @param opt Options for the operation
|
|
65
|
+
* @returns A buffer containing the attribute value when encoding is 'buffer' or undefined, or a string when a string encoding is specified
|
|
66
|
+
*/
|
|
67
|
+
export declare function getSync(this: V_Context, path: string, name: Name, opt?: Options & (BufferEncodingOption | {
|
|
68
|
+
encoding?: null;
|
|
69
|
+
})): Uint8Array;
|
|
70
|
+
export declare function getSync(this: V_Context, path: string, name: Name, opt: Options & ObjectEncodingOptions): string;
|
|
71
|
+
/**
|
|
72
|
+
* Sets the value of an extended attribute.
|
|
73
|
+
*
|
|
74
|
+
* @param path Path to the file
|
|
75
|
+
* @param name Name of the attribute to set
|
|
76
|
+
* @param value Value to set
|
|
77
|
+
* @param opt Options for the operation
|
|
78
|
+
*/
|
|
79
|
+
export declare function set(this: V_Context, path: string, name: Name, value: string | Uint8Array, opt?: SetOptions): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* Synchronously sets the value of an extended attribute.
|
|
82
|
+
*
|
|
83
|
+
* @param path Path to the file
|
|
84
|
+
* @param name Name of the attribute to set
|
|
85
|
+
* @param value Value to set
|
|
86
|
+
* @param opt Options for the operation
|
|
87
|
+
*/
|
|
88
|
+
export declare function setSync(this: V_Context, path: string, name: Name, value: string | Uint8Array, opt?: SetOptions): void;
|
|
89
|
+
/**
|
|
90
|
+
* Removes an extended attribute from a file.
|
|
91
|
+
*
|
|
92
|
+
* @param path Path to the file
|
|
93
|
+
* @param name Name of the attribute to remove
|
|
94
|
+
*/
|
|
95
|
+
export declare function remove(this: V_Context, path: string, name: Name): Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Synchronously removes an extended attribute from a file.
|
|
98
|
+
*
|
|
99
|
+
* @param path Path to the file
|
|
100
|
+
* @param name Name of the attribute to remove
|
|
101
|
+
*/
|
|
102
|
+
export declare function removeSync(this: V_Context, path: string, name: Name): void;
|
|
103
|
+
/**
|
|
104
|
+
* Lists all extended attributes of a file.
|
|
105
|
+
*
|
|
106
|
+
* @param path Path to the file
|
|
107
|
+
* @returns Array of attribute names
|
|
108
|
+
*/
|
|
109
|
+
export declare function list(this: V_Context, path: string): Promise<Name[]>;
|
|
110
|
+
/**
|
|
111
|
+
* Synchronously lists all extended attributes of a file.
|
|
112
|
+
*
|
|
113
|
+
* @param path Path to the file
|
|
114
|
+
* @returns Array of attribute names
|
|
115
|
+
*/
|
|
116
|
+
export declare function listSync(this: V_Context, path: string): Name[];
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
import { rethrow, setUVMessage, UV } from 'kerium';
|
|
3
|
+
import { Attributes, hasAccess } from '../internal/inode.js';
|
|
4
|
+
import { normalizePath } from '../utils.js';
|
|
5
|
+
import { checkAccess } from './config.js';
|
|
6
|
+
import { R_OK, W_OK } from './constants.js';
|
|
7
|
+
import { resolveMount } from './shared.js';
|
|
8
|
+
const _allowedRestrictedNames = [];
|
|
9
|
+
/**
|
|
10
|
+
* Check permission for the attribute name.
|
|
11
|
+
* For now, only attributes in the 'user' namespace are supported.
|
|
12
|
+
* @throws ENOTSUP for attributes in namespaces other than 'user'
|
|
13
|
+
*/
|
|
14
|
+
function checkName($, name, path, syscall) {
|
|
15
|
+
if (!name.startsWith('user.') && !_allowedRestrictedNames.includes(name))
|
|
16
|
+
throw UV('ENOTSUP', syscall, path);
|
|
17
|
+
}
|
|
18
|
+
export async function get(path, name, opt = {}) {
|
|
19
|
+
var _a;
|
|
20
|
+
path = normalizePath(path);
|
|
21
|
+
const { fs, path: resolved } = resolveMount(path, this);
|
|
22
|
+
checkName(this, name, path, 'xattr.get');
|
|
23
|
+
const inode = await fs.stat(resolved).catch(rethrow('xattr.get', path));
|
|
24
|
+
if (checkAccess && !hasAccess(this, inode, R_OK))
|
|
25
|
+
throw UV('EACCES', 'xattr.get', path);
|
|
26
|
+
(_a = inode.attributes) !== null && _a !== void 0 ? _a : (inode.attributes = new Attributes());
|
|
27
|
+
const value = inode.attributes.get(name);
|
|
28
|
+
if (!value)
|
|
29
|
+
throw UV('ENODATA', 'xattr.get', path);
|
|
30
|
+
const buffer = Buffer.from(value);
|
|
31
|
+
return opt.encoding == 'buffer' || !opt.encoding ? buffer : buffer.toString(opt.encoding);
|
|
32
|
+
}
|
|
33
|
+
export function getSync(path, name, opt = {}) {
|
|
34
|
+
var _a;
|
|
35
|
+
path = normalizePath(path);
|
|
36
|
+
checkName(this, name, path, 'xattr.get');
|
|
37
|
+
const { fs, path: resolved } = resolveMount(path, this);
|
|
38
|
+
let inode;
|
|
39
|
+
try {
|
|
40
|
+
inode = fs.statSync(resolved);
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
throw setUVMessage(Object.assign(e, { path }));
|
|
44
|
+
}
|
|
45
|
+
if (checkAccess && !hasAccess(this, inode, R_OK))
|
|
46
|
+
throw UV('EACCES', 'xattr.get', path);
|
|
47
|
+
(_a = inode.attributes) !== null && _a !== void 0 ? _a : (inode.attributes = new Attributes());
|
|
48
|
+
const value = inode.attributes.get(name);
|
|
49
|
+
if (!value)
|
|
50
|
+
throw UV('ENODATA', 'xattr.get', path);
|
|
51
|
+
const buffer = Buffer.from(value);
|
|
52
|
+
return opt.encoding == 'buffer' || !opt.encoding ? buffer : buffer.toString(opt.encoding);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Sets the value of an extended attribute.
|
|
56
|
+
*
|
|
57
|
+
* @param path Path to the file
|
|
58
|
+
* @param name Name of the attribute to set
|
|
59
|
+
* @param value Value to set
|
|
60
|
+
* @param opt Options for the operation
|
|
61
|
+
*/
|
|
62
|
+
export async function set(path, name, value, opt = {}) {
|
|
63
|
+
var _a;
|
|
64
|
+
path = normalizePath(path);
|
|
65
|
+
const { fs, path: resolved } = resolveMount(path, this);
|
|
66
|
+
checkName(this, name, path, 'xattr.set');
|
|
67
|
+
const inode = await fs.stat(resolved).catch(rethrow('xattr.set', path));
|
|
68
|
+
if (checkAccess && !hasAccess(this, inode, W_OK))
|
|
69
|
+
throw UV('EACCES', 'xattr.set', path);
|
|
70
|
+
(_a = inode.attributes) !== null && _a !== void 0 ? _a : (inode.attributes = new Attributes());
|
|
71
|
+
const attr = inode.attributes.get(name);
|
|
72
|
+
if (opt.create && attr)
|
|
73
|
+
throw UV('EEXIST', 'xattr.set', path);
|
|
74
|
+
if (opt.replace && !attr)
|
|
75
|
+
throw UV('ENODATA', 'xattr.set', path);
|
|
76
|
+
inode.attributes.set(name, Buffer.from(value));
|
|
77
|
+
await fs.touch(resolved, inode).catch(rethrow('xattr.set', path));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Synchronously sets the value of an extended attribute.
|
|
81
|
+
*
|
|
82
|
+
* @param path Path to the file
|
|
83
|
+
* @param name Name of the attribute to set
|
|
84
|
+
* @param value Value to set
|
|
85
|
+
* @param opt Options for the operation
|
|
86
|
+
*/
|
|
87
|
+
export function setSync(path, name, value, opt = {}) {
|
|
88
|
+
var _a;
|
|
89
|
+
path = normalizePath(path);
|
|
90
|
+
const { fs, path: resolved } = resolveMount(path, this);
|
|
91
|
+
checkName(this, name, path, 'xattr.set');
|
|
92
|
+
let inode;
|
|
93
|
+
try {
|
|
94
|
+
inode = fs.statSync(resolved);
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
throw setUVMessage(Object.assign(e, { path }));
|
|
98
|
+
}
|
|
99
|
+
if (checkAccess && !hasAccess(this, inode, W_OK))
|
|
100
|
+
throw UV('EACCES', 'xattr.set', path);
|
|
101
|
+
(_a = inode.attributes) !== null && _a !== void 0 ? _a : (inode.attributes = new Attributes());
|
|
102
|
+
const attr = inode.attributes.get(name);
|
|
103
|
+
if (opt.create && attr)
|
|
104
|
+
throw UV('EEXIST', 'xattr.set', path);
|
|
105
|
+
if (opt.replace && !attr)
|
|
106
|
+
throw UV('ENODATA', 'xattr.set', path);
|
|
107
|
+
inode.attributes.set(name, Buffer.from(value));
|
|
108
|
+
try {
|
|
109
|
+
fs.touchSync(resolved, inode);
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
throw setUVMessage(Object.assign(e, { path }));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Removes an extended attribute from a file.
|
|
117
|
+
*
|
|
118
|
+
* @param path Path to the file
|
|
119
|
+
* @param name Name of the attribute to remove
|
|
120
|
+
*/
|
|
121
|
+
export async function remove(path, name) {
|
|
122
|
+
var _a;
|
|
123
|
+
path = normalizePath(path);
|
|
124
|
+
const { fs, path: resolved } = resolveMount(path, this);
|
|
125
|
+
checkName(this, name, path, 'xattr.remove');
|
|
126
|
+
const inode = await fs.stat(resolved).catch(rethrow('xattr.remove', path));
|
|
127
|
+
if (checkAccess && !hasAccess(this, inode, W_OK))
|
|
128
|
+
throw UV('EACCES', 'xattr.remove', path);
|
|
129
|
+
(_a = inode.attributes) !== null && _a !== void 0 ? _a : (inode.attributes = new Attributes());
|
|
130
|
+
const attr = inode.attributes.get(name);
|
|
131
|
+
if (!attr)
|
|
132
|
+
throw UV('ENODATA', 'xattr.remove', path);
|
|
133
|
+
inode.attributes.remove(name);
|
|
134
|
+
await fs.touch(resolved, inode);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Synchronously removes an extended attribute from a file.
|
|
138
|
+
*
|
|
139
|
+
* @param path Path to the file
|
|
140
|
+
* @param name Name of the attribute to remove
|
|
141
|
+
*/
|
|
142
|
+
export function removeSync(path, name) {
|
|
143
|
+
var _a;
|
|
144
|
+
path = normalizePath(path);
|
|
145
|
+
const { fs, path: resolved } = resolveMount(path, this);
|
|
146
|
+
checkName(this, name, path, 'xattr.remove');
|
|
147
|
+
let inode;
|
|
148
|
+
try {
|
|
149
|
+
inode = fs.statSync(resolved);
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
throw setUVMessage(Object.assign(e, { path }));
|
|
153
|
+
}
|
|
154
|
+
if (checkAccess && !hasAccess(this, inode, W_OK))
|
|
155
|
+
throw UV('EACCES', 'xattr.remove', path);
|
|
156
|
+
(_a = inode.attributes) !== null && _a !== void 0 ? _a : (inode.attributes = new Attributes());
|
|
157
|
+
const attr = inode.attributes.get(name);
|
|
158
|
+
if (!attr)
|
|
159
|
+
throw UV('ENODATA', 'xattr.remove', path);
|
|
160
|
+
inode.attributes.remove(name);
|
|
161
|
+
try {
|
|
162
|
+
fs.touchSync(resolved, inode);
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
throw setUVMessage(Object.assign(e, { path }));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Lists all extended attributes of a file.
|
|
170
|
+
*
|
|
171
|
+
* @param path Path to the file
|
|
172
|
+
* @returns Array of attribute names
|
|
173
|
+
*/
|
|
174
|
+
export async function list(path) {
|
|
175
|
+
path = normalizePath(path);
|
|
176
|
+
const { fs, path: resolved } = resolveMount(path, this);
|
|
177
|
+
const inode = await fs.stat(resolved).catch(rethrow('xattr.list', path));
|
|
178
|
+
if (!inode.attributes)
|
|
179
|
+
return [];
|
|
180
|
+
return inode.attributes.keys().toArray();
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Synchronously lists all extended attributes of a file.
|
|
184
|
+
*
|
|
185
|
+
* @param path Path to the file
|
|
186
|
+
* @returns Array of attribute names
|
|
187
|
+
*/
|
|
188
|
+
export function listSync(path) {
|
|
189
|
+
path = normalizePath(path);
|
|
190
|
+
const { fs, path: resolved } = resolveMount(path, this);
|
|
191
|
+
let inode;
|
|
192
|
+
try {
|
|
193
|
+
inode = fs.statSync(resolved);
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
throw setUVMessage(Object.assign(e, { path }));
|
|
197
|
+
}
|
|
198
|
+
if (!inode.attributes)
|
|
199
|
+
return [];
|
|
200
|
+
return inode.attributes.keys().toArray();
|
|
201
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenfs/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "A filesystem, anywhere",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -67,11 +67,13 @@
|
|
|
67
67
|
"prepublishOnly": "npm run build"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@types/node": "^22.10.1",
|
|
70
|
+
"@types/node": "^22.10.1 <22.13.7",
|
|
71
71
|
"buffer": "^6.0.3",
|
|
72
72
|
"eventemitter3": "^5.0.1",
|
|
73
|
+
"kerium": "^1.3.4",
|
|
74
|
+
"memium": "^0.2.0",
|
|
73
75
|
"readable-stream": "^4.5.2",
|
|
74
|
-
"utilium": "^
|
|
76
|
+
"utilium": "^2.3.3"
|
|
75
77
|
},
|
|
76
78
|
"devDependencies": {
|
|
77
79
|
"@eslint/js": "^9.8.0",
|
package/readme.md
CHANGED
|
@@ -7,7 +7,7 @@ ZenFS is a cross-platform library that emulates the [NodeJS filesystem API](http
|
|
|
7
7
|
ZenFS is modular and extensible. The core includes some built-in backends:
|
|
8
8
|
|
|
9
9
|
- `InMemory`: Stores files in-memory. This is cleared when the runtime ends (e.g. a user navigating away from a web page or a Node process exiting)
|
|
10
|
-
- `
|
|
10
|
+
- `CopyOnWrite`: Use readable and writable file systems with ([copy-on-write](https://en.wikipedia.org/wiki/Copy-on-write)).
|
|
11
11
|
- `Fetch`: Downloads files over HTTP with the `fetch` API
|
|
12
12
|
- `Port`: Interacts with a remote over a `MessagePort`-like interface (e.g. a worker)
|
|
13
13
|
- `Passthrough`: Use an existing `node:fs` interface with ZenFS
|
package/scripts/test.js
CHANGED
|
@@ -9,7 +9,7 @@ const { values: options, positionals } = parseArgs({
|
|
|
9
9
|
options: {
|
|
10
10
|
// Output
|
|
11
11
|
help: { short: 'h', type: 'boolean', default: false },
|
|
12
|
-
verbose: { short: '
|
|
12
|
+
verbose: { short: 'v', type: 'boolean', default: false },
|
|
13
13
|
quiet: { short: 'q', type: 'boolean', default: false },
|
|
14
14
|
log: { short: 'l', type: 'string', default: '' },
|
|
15
15
|
'file-names': { short: 'N', type: 'boolean', default: false },
|
|
@@ -49,7 +49,7 @@ Behavior:
|
|
|
49
49
|
|
|
50
50
|
Output:
|
|
51
51
|
-h, --help Outputs this help message
|
|
52
|
-
-
|
|
52
|
+
-v, --verbose Output verbose messages
|
|
53
53
|
-q, --quiet Don't output normal messages
|
|
54
54
|
-l, --logs <level> Change the default log level for test output. Level can be a number or string
|
|
55
55
|
-N, --file-names Use full file paths for tests from setup files instead of the base name
|
package/tests/assignment.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
- ReadStream and WriteStream are excluded since they are polyfilled from another module
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { fs as zen } from '../src/index.js';
|
|
14
13
|
import type * as node from 'node:fs';
|
|
14
|
+
import { fs as zen } from '../src/index.js';
|
|
15
15
|
|
|
16
16
|
type Mock = {
|
|
17
17
|
[K in Exclude<keyof typeof node, 'ReadStream' | 'WriteStream'>]: Omit<(typeof node)[K], '__promisify__' | 'native'>;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { parentPort } from 'node:worker_threads';
|
|
2
|
-
import { resolveRemoteMount } from '../../dist/backends/port
|
|
2
|
+
import { resolveRemoteMount } from '../../dist/backends/port.js';
|
|
3
3
|
import { InMemory } from '../../dist/backends/memory.js';
|
|
4
|
+
import { setupLogs } from '../logs.js';
|
|
5
|
+
|
|
6
|
+
setupLogs('<config>');
|
|
4
7
|
|
|
5
8
|
await resolveRemoteMount(parentPort, { backend: InMemory });
|
|
@@ -4,6 +4,9 @@ import { suite, test } from 'node:test';
|
|
|
4
4
|
import { Worker } from 'node:worker_threads';
|
|
5
5
|
import { Fetch, configureSingle, fs, mounts, type FetchFS } from '../../dist/index.js';
|
|
6
6
|
import { baseUrl, defaultEntries, indexPath, whenServerReady } from '../fetch/config.js';
|
|
7
|
+
import { setupLogs } from '../logs.js';
|
|
8
|
+
|
|
9
|
+
setupLogs();
|
|
7
10
|
|
|
8
11
|
const server = new Worker(join(import.meta.dirname, '../fetch/server.js'));
|
|
9
12
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { suite, test } from 'node:test';
|
|
3
3
|
import { MessageChannel, Worker } from 'node:worker_threads';
|
|
4
|
-
import { Port, attachFS } from '../../dist/backends/port
|
|
5
|
-
import { waitOnline } from '../../dist/backends/port/rpc.js';
|
|
4
|
+
import { Port, attachFS, waitOnline } from '../../dist/backends/port.js';
|
|
6
5
|
import type { InMemoryStore, StoreFS } from '../../dist/index.js';
|
|
7
|
-
import {
|
|
6
|
+
import { InMemory, configure, configureSingle, fs, resolveMountConfig } from '../../dist/index.js';
|
|
7
|
+
import { setupLogs } from '../logs.js';
|
|
8
|
+
setupLogs();
|
|
8
9
|
|
|
9
10
|
// Tests a mis-configured `Port` using a MessageChannel
|
|
10
11
|
|
|
@@ -13,35 +14,20 @@ timeoutChannel.port2.unref();
|
|
|
13
14
|
|
|
14
15
|
await suite('Timeout', { timeout: 1000 }, () => {
|
|
15
16
|
test('Misconfiguration', async () => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} catch (e) {
|
|
25
|
-
assert(e instanceof ErrnoError);
|
|
26
|
-
error = e;
|
|
27
|
-
}
|
|
28
|
-
assert(error! instanceof ErrnoError);
|
|
29
|
-
assert.equal(error.code, 'EIO');
|
|
30
|
-
assert(error.message.includes('RPC Failed'));
|
|
17
|
+
const configured = configure({
|
|
18
|
+
mounts: {
|
|
19
|
+
'/tmp-timeout': { backend: InMemory, label: 'tmp' },
|
|
20
|
+
'/port': { backend: Port, port: timeoutChannel.port1, timeout: 100 },
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await assert.rejects(configured, { code: 'EIO', message: /RPC Failed/ });
|
|
31
25
|
});
|
|
32
26
|
|
|
33
27
|
test('Remote not attached', async () => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
await fs.promises.writeFile('/test', 'anything');
|
|
38
|
-
} catch (e) {
|
|
39
|
-
assert(e instanceof ErrnoError);
|
|
40
|
-
error = e;
|
|
41
|
-
}
|
|
42
|
-
assert(error! instanceof ErrnoError);
|
|
43
|
-
assert.equal(error.code, 'EIO');
|
|
44
|
-
assert(error.message.includes('RPC Failed'));
|
|
28
|
+
const configured = configureSingle({ backend: Port, port: timeoutChannel.port1, timeout: 100 });
|
|
29
|
+
|
|
30
|
+
await assert.rejects(configured, { code: 'EIO', message: /RPC Failed/ });
|
|
45
31
|
});
|
|
46
32
|
});
|
|
47
33
|
|
|
@@ -56,7 +42,7 @@ await suite('Remote FS with resolveRemoteMount', () => {
|
|
|
56
42
|
const content = 'FS is in a port';
|
|
57
43
|
|
|
58
44
|
test('Configuration', async () => {
|
|
59
|
-
await configureSingle({ backend: Port, port: configPort, timeout:
|
|
45
|
+
await configureSingle({ backend: Port, port: configPort, timeout: 500 });
|
|
60
46
|
});
|
|
61
47
|
|
|
62
48
|
test('Write', async () => {
|
|
@@ -79,9 +65,9 @@ let tmpfs: StoreFS<InMemoryStore>;
|
|
|
79
65
|
|
|
80
66
|
await suite('FS with MessageChannel', () => {
|
|
81
67
|
test('configuration', async () => {
|
|
82
|
-
tmpfs = await resolveMountConfig({ backend: InMemory,
|
|
68
|
+
tmpfs = await resolveMountConfig({ backend: InMemory, label: 'tmp' });
|
|
83
69
|
attachFS(channel.port2, tmpfs);
|
|
84
|
-
await configureSingle({ backend: Port, port: channel.port1, disableAsyncCache: true, timeout:
|
|
70
|
+
await configureSingle({ backend: Port, port: channel.port1, disableAsyncCache: true, timeout: 500 });
|
|
85
71
|
});
|
|
86
72
|
|
|
87
73
|
test('write', async () => {
|
|
@@ -116,7 +102,7 @@ await suite('Remote FS', () => {
|
|
|
116
102
|
const content = 'FS is in a port';
|
|
117
103
|
|
|
118
104
|
test('Configuration', async () => {
|
|
119
|
-
await configureSingle({ backend: Port, port: remotePort, timeout:
|
|
105
|
+
await configureSingle({ backend: Port, port: remotePort, timeout: 500 });
|
|
120
106
|
});
|
|
121
107
|
|
|
122
108
|
test('Write', async () => {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { parentPort } from 'node:worker_threads';
|
|
2
|
-
import { attachFS } from '../../dist/backends/port
|
|
2
|
+
import { attachFS } from '../../dist/backends/port.js';
|
|
3
3
|
import { mounts } from '../../dist/index.js';
|
|
4
|
+
import { setupLogs } from '../logs.js';
|
|
5
|
+
|
|
6
|
+
setupLogs('<remote>');
|
|
4
7
|
|
|
5
8
|
attachFS(parentPort, mounts.get('/'));
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { suite, test } from 'node:test';
|
|
3
|
+
import { Worker } from 'worker_threads';
|
|
4
|
+
import { fs, mount, resolveMountConfig, SingleBuffer } from '../../dist/index.js';
|
|
5
|
+
import { setupLogs } from '../logs.js';
|
|
6
|
+
|
|
7
|
+
setupLogs();
|
|
8
|
+
|
|
9
|
+
await suite('SingleBuffer', () => {
|
|
10
|
+
test('filesystem restoration from original buffer (with same metadata)', async () => {
|
|
11
|
+
const buffer = new ArrayBuffer(0x100000);
|
|
12
|
+
|
|
13
|
+
const writable = await resolveMountConfig({ backend: SingleBuffer, buffer });
|
|
14
|
+
mount('/mnt', writable);
|
|
15
|
+
|
|
16
|
+
fs.writeFileSync('/mnt/example.ts', 'console.log("hello world")', 'utf-8');
|
|
17
|
+
const stats = fs.statSync('/mnt/example.ts');
|
|
18
|
+
|
|
19
|
+
const snapshot = await resolveMountConfig({ backend: SingleBuffer, buffer });
|
|
20
|
+
mount('/snapshot', snapshot);
|
|
21
|
+
|
|
22
|
+
const snapshotStats = fs.statSync('/snapshot/example.ts');
|
|
23
|
+
|
|
24
|
+
assert.deepEqual(snapshotStats, stats);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('cross-thread SharedArrayBuffer', async () => {
|
|
28
|
+
const sharedBuffer = new SharedArrayBuffer(0x100000);
|
|
29
|
+
|
|
30
|
+
const writable = await resolveMountConfig({ backend: SingleBuffer, buffer: sharedBuffer });
|
|
31
|
+
fs.mkdirSync('/shared');
|
|
32
|
+
mount('/shared', writable);
|
|
33
|
+
|
|
34
|
+
const worker = new Worker(import.meta.dirname + '/single-buffer.worker.js', { workerData: sharedBuffer });
|
|
35
|
+
|
|
36
|
+
// Pause while we wait for the worker to emit the 'continue' message, which
|
|
37
|
+
// means it has mounted the filesystem and created /worker-file.ts
|
|
38
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
39
|
+
|
|
40
|
+
setTimeout(reject, 1000);
|
|
41
|
+
worker.on('message', message => {
|
|
42
|
+
if (message === 'continue') resolve();
|
|
43
|
+
else reject(message ?? new Error('Failed'));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await promise;
|
|
47
|
+
|
|
48
|
+
await worker.terminate();
|
|
49
|
+
worker.unref();
|
|
50
|
+
|
|
51
|
+
assert(fs.existsSync('/shared/worker-file.ts'));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { writeFileSync as _write } from 'node:fs';
|
|
3
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
4
|
+
import { configureSingle, fs, SingleBuffer } from '../../dist/index.js';
|
|
5
|
+
import { setupLogs } from '../logs.js';
|
|
6
|
+
|
|
7
|
+
setupLogs('<worker>');
|
|
8
|
+
|
|
9
|
+
const content = 'console.log("this file was created by the worker")';
|
|
10
|
+
|
|
11
|
+
const view = new Uint8Array(workerData);
|
|
12
|
+
|
|
13
|
+
if (process.env.DEBUG) _write('tmp/shared.bin', view);
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
await configureSingle({
|
|
17
|
+
backend: SingleBuffer,
|
|
18
|
+
buffer: workerData,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
fs.writeFileSync('/worker-file.ts', content, 'utf-8');
|
|
22
|
+
|
|
23
|
+
assert.equal(fs.readFileSync('/worker-file.ts', 'utf-8'), content);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
if (process.env.DEBUG) _write('tmp/shared.bin', view);
|
|
26
|
+
console.error(e);
|
|
27
|
+
parentPort.postMessage(e);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
parentPort.postMessage('continue');
|