@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.
Files changed (135) hide show
  1. package/dist/backends/backend.d.ts +19 -15
  2. package/dist/backends/backend.js +36 -19
  3. package/dist/backends/cow.d.ts +20 -30
  4. package/dist/backends/cow.js +83 -192
  5. package/dist/backends/fetch.d.ts +1 -0
  6. package/dist/backends/fetch.js +30 -30
  7. package/dist/backends/index.d.ts +1 -1
  8. package/dist/backends/index.js +1 -1
  9. package/dist/backends/memory.d.ts +5 -7
  10. package/dist/backends/memory.js +2 -3
  11. package/dist/backends/passthrough.d.ts +19 -23
  12. package/dist/backends/passthrough.js +98 -288
  13. package/dist/backends/port.d.ts +220 -0
  14. package/dist/backends/port.js +328 -0
  15. package/dist/backends/single_buffer.d.ts +59 -47
  16. package/dist/backends/single_buffer.js +468 -219
  17. package/dist/backends/store/fs.d.ts +25 -35
  18. package/dist/backends/store/fs.js +276 -315
  19. package/dist/backends/store/store.d.ts +10 -15
  20. package/dist/backends/store/store.js +11 -10
  21. package/dist/config.d.ts +3 -12
  22. package/dist/config.js +17 -19
  23. package/dist/context.d.ts +8 -21
  24. package/dist/context.js +33 -10
  25. package/dist/index.d.ts +2 -1
  26. package/dist/index.js +2 -1
  27. package/dist/internal/contexts.d.ts +63 -0
  28. package/dist/internal/contexts.js +15 -0
  29. package/dist/internal/credentials.d.ts +2 -11
  30. package/dist/internal/credentials.js +0 -19
  31. package/dist/internal/devices.d.ts +18 -80
  32. package/dist/internal/devices.js +103 -316
  33. package/dist/internal/error.d.ts +9 -204
  34. package/dist/internal/error.js +19 -288
  35. package/dist/internal/file_index.d.ts +1 -1
  36. package/dist/internal/file_index.js +11 -11
  37. package/dist/internal/filesystem.d.ts +51 -94
  38. package/dist/internal/filesystem.js +21 -20
  39. package/dist/internal/index.d.ts +1 -2
  40. package/dist/internal/index.js +1 -2
  41. package/dist/internal/index_fs.d.ts +12 -30
  42. package/dist/internal/index_fs.js +37 -69
  43. package/dist/internal/inode.d.ts +140 -24
  44. package/dist/internal/inode.js +515 -66
  45. package/dist/mixins/async.js +52 -112
  46. package/dist/mixins/mutexed.d.ts +19 -18
  47. package/dist/mixins/mutexed.js +62 -64
  48. package/dist/mixins/readonly.d.ts +7 -6
  49. package/dist/mixins/readonly.js +24 -18
  50. package/dist/mixins/sync.js +8 -8
  51. package/dist/{vfs/path.d.ts → path.d.ts} +3 -4
  52. package/dist/{vfs/path.js → path.js} +6 -9
  53. package/dist/polyfills.js +1 -1
  54. package/dist/readline.d.ts +134 -0
  55. package/dist/readline.js +623 -0
  56. package/dist/utils.d.ts +9 -37
  57. package/dist/utils.js +17 -85
  58. package/dist/vfs/acl.d.ts +42 -0
  59. package/dist/vfs/acl.js +268 -0
  60. package/dist/vfs/async.d.ts +9 -23
  61. package/dist/vfs/async.js +25 -27
  62. package/dist/vfs/config.d.ts +6 -18
  63. package/dist/vfs/config.js +8 -18
  64. package/dist/vfs/dir.d.ts +3 -3
  65. package/dist/vfs/dir.js +12 -12
  66. package/dist/vfs/file.d.ts +106 -0
  67. package/dist/vfs/file.js +244 -0
  68. package/dist/vfs/flags.d.ts +19 -0
  69. package/dist/vfs/flags.js +62 -0
  70. package/dist/vfs/index.d.ts +4 -10
  71. package/dist/vfs/index.js +4 -13
  72. package/dist/vfs/ioctl.d.ts +88 -0
  73. package/dist/vfs/ioctl.js +409 -0
  74. package/dist/vfs/promises.d.ts +81 -19
  75. package/dist/vfs/promises.js +404 -288
  76. package/dist/vfs/shared.d.ts +7 -37
  77. package/dist/vfs/shared.js +29 -85
  78. package/dist/{stats.d.ts → vfs/stats.d.ts} +14 -28
  79. package/dist/{stats.js → vfs/stats.js} +11 -66
  80. package/dist/vfs/streams.d.ts +1 -0
  81. package/dist/vfs/streams.js +32 -27
  82. package/dist/vfs/sync.d.ts +3 -3
  83. package/dist/vfs/sync.js +263 -260
  84. package/dist/vfs/watchers.d.ts +2 -2
  85. package/dist/vfs/watchers.js +12 -12
  86. package/dist/vfs/xattr.d.ts +116 -0
  87. package/dist/vfs/xattr.js +201 -0
  88. package/package.json +5 -3
  89. package/readme.md +1 -1
  90. package/scripts/test.js +2 -2
  91. package/tests/assignment.ts +1 -1
  92. package/tests/backend/config.worker.js +4 -1
  93. package/tests/backend/fetch.test.ts +3 -0
  94. package/tests/backend/port.test.ts +19 -33
  95. package/tests/backend/remote.worker.js +4 -1
  96. package/tests/backend/single-buffer.test.ts +53 -0
  97. package/tests/backend/single-buffer.worker.js +30 -0
  98. package/tests/common/context.test.ts +3 -3
  99. package/tests/common/handle.test.ts +17 -12
  100. package/tests/common/mutex.test.ts +9 -9
  101. package/tests/common/path.test.ts +1 -1
  102. package/tests/common/readline.test.ts +104 -0
  103. package/tests/common.ts +4 -19
  104. package/tests/fetch/fetch.ts +2 -2
  105. package/tests/fs/append.test.ts +4 -4
  106. package/tests/fs/directory.test.ts +25 -25
  107. package/tests/fs/errors.test.ts +15 -19
  108. package/tests/fs/links.test.ts +4 -3
  109. package/tests/fs/open.test.ts +4 -21
  110. package/tests/fs/permissions.test.ts +14 -18
  111. package/tests/fs/read.test.ts +10 -9
  112. package/tests/fs/readFile.test.ts +10 -26
  113. package/tests/fs/rename.test.ts +4 -9
  114. package/tests/fs/stat.test.ts +8 -8
  115. package/tests/fs/streams.test.ts +2 -11
  116. package/tests/fs/times.test.ts +7 -7
  117. package/tests/fs/truncate.test.ts +8 -36
  118. package/tests/fs/watch.test.ts +10 -10
  119. package/tests/fs/write.test.ts +77 -13
  120. package/tests/fs/xattr.test.ts +85 -0
  121. package/tests/logs.js +22 -0
  122. package/tests/setup/context.ts +1 -1
  123. package/tests/setup/index.ts +3 -3
  124. package/tests/setup/port.ts +7 -1
  125. package/dist/backends/port/fs.d.ts +0 -84
  126. package/dist/backends/port/fs.js +0 -151
  127. package/dist/backends/port/rpc.d.ts +0 -77
  128. package/dist/backends/port/rpc.js +0 -100
  129. package/dist/backends/store/simple.d.ts +0 -20
  130. package/dist/backends/store/simple.js +0 -13
  131. package/dist/internal/file.d.ts +0 -359
  132. package/dist/internal/file.js +0 -751
  133. package/dist/internal/log.d.ts +0 -133
  134. package/dist/internal/log.js +0 -218
  135. package/tests/fs/writeFile.test.ts +0 -70
@@ -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 '../stats.js';
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(context: V_Context, eventType: fs.WatchEventType, filename: string): void;
78
+ export declare function emitChange($: V_Context, eventType: fs.WatchEventType, filename: string): void;
79
79
  export {};
@@ -1,8 +1,8 @@
1
1
  import { EventEmitter } from 'eventemitter3';
2
- import { ErrnoError } from '../internal/error.js';
3
- import { isStatsEqual } from '../stats.js';
2
+ import { UV } from 'kerium';
3
+ import { basename, dirname, join, relative } from '../path.js';
4
4
  import { normalizePath } from '../utils.js';
5
- import { basename, dirname, join, relative } from './path.js';
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 ErrnoError.With('ENOSYS', this.path, 'Watcher.setMaxListeners');
32
+ throw UV('ENOSYS', 'Watcher.setMaxListeners');
33
33
  }
34
34
  getMaxListeners() {
35
- throw ErrnoError.With('ENOSYS', this.path, 'Watcher.getMaxListeners');
35
+ throw UV('ENOSYS', 'Watcher.getMaxListeners');
36
36
  }
37
37
  prependListener() {
38
- throw ErrnoError.With('ENOSYS', this.path, 'Watcher.prependListener');
38
+ throw UV('ENOSYS', 'Watcher.prependListener');
39
39
  }
40
40
  prependOnceListener() {
41
- throw ErrnoError.With('ENOSYS', this.path, 'Watcher.prependOnceListener');
41
+ throw UV('ENOSYS', 'Watcher.prependOnceListener');
42
42
  }
43
43
  rawListeners() {
44
- throw ErrnoError.With('ENOSYS', this.path, 'Watcher.rawListeners');
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(context, eventType, filename) {
141
+ export function emitChange($, eventType, filename) {
142
142
  var _a;
143
- if (context)
144
- filename = join((_a = context.root) !== null && _a !== void 0 ? _a : '/', filename);
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.11.4",
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": "^1.3.3"
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
- - `Overlay`: Use a read-only file system as read-write by overlaying a writable file system on top of it. ([copy-on-write](https://en.wikipedia.org/wiki/Copy-on-write))
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: 'w', type: 'boolean', default: false },
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
- -w, --verbose Output verbose messages
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
@@ -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/fs.js';
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/fs.js';
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 { ErrnoError, InMemory, configure, configureSingle, fs, resolveMountConfig } from '../../dist/index.js';
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
- let error: ErrnoError;
17
- try {
18
- await configure({
19
- mounts: {
20
- '/tmp-timeout': { backend: InMemory, name: 'tmp' },
21
- '/port': { backend: Port, port: timeoutChannel.port1, timeout: 100 },
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
- let error: ErrnoError;
35
- try {
36
- await configureSingle({ backend: Port, port: timeoutChannel.port1, timeout: 100 });
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: 100 });
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, name: 'tmp' });
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: 100 });
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: 100 });
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/fs.js';
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');