@zenfs/core 1.1.6 → 1.2.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 (58) hide show
  1. package/dist/backends/file_index.js +0 -3
  2. package/dist/backends/overlay.js +0 -8
  3. package/dist/backends/store/fs.js +4 -17
  4. package/dist/config.d.ts +14 -0
  5. package/dist/config.js +4 -0
  6. package/dist/devices.js +0 -12
  7. package/dist/emulation/cache.d.ts +21 -0
  8. package/dist/emulation/cache.js +36 -0
  9. package/dist/emulation/promises.d.ts +9 -14
  10. package/dist/emulation/promises.js +71 -48
  11. package/dist/emulation/shared.d.ts +22 -0
  12. package/dist/emulation/shared.js +6 -0
  13. package/dist/emulation/sync.d.ts +11 -20
  14. package/dist/emulation/sync.js +44 -23
  15. package/package.json +4 -2
  16. package/scripts/test.js +14 -1
  17. package/src/backends/backend.ts +160 -0
  18. package/src/backends/fetch.ts +180 -0
  19. package/src/backends/file_index.ts +206 -0
  20. package/src/backends/memory.ts +50 -0
  21. package/src/backends/overlay.ts +560 -0
  22. package/src/backends/port/fs.ts +335 -0
  23. package/src/backends/port/readme.md +54 -0
  24. package/src/backends/port/rpc.ts +167 -0
  25. package/src/backends/readme.md +3 -0
  26. package/src/backends/store/fs.ts +700 -0
  27. package/src/backends/store/readme.md +9 -0
  28. package/src/backends/store/simple.ts +146 -0
  29. package/src/backends/store/store.ts +173 -0
  30. package/src/config.ts +173 -0
  31. package/src/credentials.ts +31 -0
  32. package/src/devices.ts +459 -0
  33. package/src/emulation/async.ts +834 -0
  34. package/src/emulation/cache.ts +44 -0
  35. package/src/emulation/constants.ts +182 -0
  36. package/src/emulation/dir.ts +138 -0
  37. package/src/emulation/index.ts +8 -0
  38. package/src/emulation/path.ts +440 -0
  39. package/src/emulation/promises.ts +1133 -0
  40. package/src/emulation/shared.ts +160 -0
  41. package/src/emulation/streams.ts +34 -0
  42. package/src/emulation/sync.ts +867 -0
  43. package/src/emulation/watchers.ts +193 -0
  44. package/src/error.ts +307 -0
  45. package/src/file.ts +661 -0
  46. package/src/filesystem.ts +174 -0
  47. package/src/index.ts +25 -0
  48. package/src/inode.ts +132 -0
  49. package/src/mixins/async.ts +208 -0
  50. package/src/mixins/index.ts +5 -0
  51. package/src/mixins/mutexed.ts +257 -0
  52. package/src/mixins/readonly.ts +96 -0
  53. package/src/mixins/shared.ts +25 -0
  54. package/src/mixins/sync.ts +58 -0
  55. package/src/polyfills.ts +21 -0
  56. package/src/stats.ts +363 -0
  57. package/src/utils.ts +288 -0
  58. package/tests/fs/readdir.test.ts +3 -3
@@ -0,0 +1,146 @@
1
+ import type { Ino } from '../../inode.js';
2
+ import { SyncTransaction, type Store } from './store.js';
3
+
4
+ /**
5
+ * An interface for simple synchronous stores that don't have special support for transactions and such.
6
+ */
7
+ export interface SimpleSyncStore extends Store {
8
+ get(ino: Ino): Uint8Array | undefined;
9
+ set(ino: Ino, data: Uint8Array): void;
10
+ delete(ino: Ino): void;
11
+ }
12
+
13
+ /**
14
+ * An interface for simple asynchronous stores that don't have special support for transactions and such.
15
+ * This class adds caching at the store level.
16
+ */
17
+ export abstract class SimpleAsyncStore implements SimpleSyncStore {
18
+ public abstract name: string;
19
+
20
+ protected cache: Map<Ino, Uint8Array> = new Map();
21
+
22
+ protected queue: Set<Promise<unknown>> = new Set();
23
+
24
+ protected abstract entries(): Promise<Iterable<[Ino, Uint8Array]>>;
25
+
26
+ public get(ino: Ino): Uint8Array | undefined {
27
+ return this.cache.get(ino);
28
+ }
29
+
30
+ public set(ino: Ino, data: Uint8Array): void {
31
+ this.cache.set(ino, data);
32
+ this.queue.add(this._set(ino, data));
33
+ }
34
+
35
+ protected abstract _set(ino: Ino, data: Uint8Array): Promise<void>;
36
+
37
+ public delete(ino: Ino): void {
38
+ this.cache.delete(ino);
39
+ this.queue.add(this._delete(ino));
40
+ }
41
+
42
+ protected abstract _delete(ino: Ino): Promise<void>;
43
+
44
+ public clearSync(): void {
45
+ this.cache.clear();
46
+ this.queue.add(this.clear());
47
+ }
48
+
49
+ public abstract clear(): Promise<void>;
50
+
51
+ public async sync(): Promise<void> {
52
+ for (const [ino, data] of await this.entries()) {
53
+ if (!this.cache.has(ino)) {
54
+ this.cache.set(ino, data);
55
+ }
56
+ }
57
+ for (const promise of this.queue) {
58
+ await promise;
59
+ }
60
+ }
61
+
62
+ public transaction(): SimpleTransaction {
63
+ return new SimpleTransaction(this);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Transaction for simple stores.
69
+ * @see SimpleSyncStore
70
+ * @see SimpleAsyncStore
71
+ */
72
+ export class SimpleTransaction extends SyncTransaction<SimpleSyncStore> {
73
+ /**
74
+ * Stores data in the keys we modify prior to modifying them.
75
+ * Allows us to roll back commits.
76
+ */
77
+ protected originalData: Map<Ino, Uint8Array | void> = new Map();
78
+ /**
79
+ * List of keys modified in this transaction, if any.
80
+ */
81
+ protected modifiedKeys: Set<Ino> = new Set();
82
+
83
+ protected declare store: SimpleSyncStore;
84
+
85
+ public getSync(ino: Ino): Uint8Array {
86
+ const val = this.store.get(ino);
87
+ this.stashOldValue(ino, val);
88
+ return val!;
89
+ }
90
+
91
+ public setSync(ino: Ino, data: Uint8Array): void {
92
+ this.markModified(ino);
93
+ return this.store.set(ino, data);
94
+ }
95
+
96
+ public removeSync(ino: Ino): void {
97
+ this.markModified(ino);
98
+ this.store.delete(ino);
99
+ }
100
+
101
+ public commitSync(): void {
102
+ this.done = true;
103
+ }
104
+
105
+ public abortSync(): void {
106
+ if (!this.done) {
107
+ return;
108
+ }
109
+ // Rollback old values.
110
+ for (const key of this.modifiedKeys) {
111
+ const value = this.originalData.get(key);
112
+ if (!value) {
113
+ // Key didn't exist.
114
+ this.store.delete(key);
115
+ } else {
116
+ // Key existed. Store old value.
117
+ this.store.set(key, value);
118
+ }
119
+ }
120
+ this.done = true;
121
+ }
122
+
123
+ /**
124
+ * Stashes given key value pair into `originalData` if it doesn't already
125
+ * exist. Allows us to stash values the program is requesting anyway to
126
+ * prevent needless `get` requests if the program modifies the data later
127
+ * on during the transaction.
128
+ */
129
+ protected stashOldValue(ino: Ino, value?: Uint8Array): void {
130
+ // Keep only the earliest value in the transaction.
131
+ if (!this.originalData.has(ino)) {
132
+ this.originalData.set(ino, value);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Marks `ino` as modified, and stashes its value if it has not been
138
+ * stashed already.
139
+ */
140
+ protected markModified(ino: Ino): void {
141
+ this.modifiedKeys.add(ino);
142
+ if (!this.originalData.has(ino)) {
143
+ this.originalData.set(ino, this.store.get(ino));
144
+ }
145
+ }
146
+ }
@@ -0,0 +1,173 @@
1
+ import { ErrnoError } from '../../error.js';
2
+ import type { Ino } from '../../inode.js';
3
+ import '../../polyfills.js';
4
+
5
+ /**
6
+ * Represents a key-value store.
7
+ */
8
+ export interface Store {
9
+ /**
10
+ * The name of the store.
11
+ */
12
+ readonly name: string;
13
+
14
+ /**
15
+ * Syncs the store
16
+ */
17
+ sync(): Promise<void>;
18
+
19
+ /**
20
+ * Empties the store completely.
21
+ */
22
+ clear(): Promise<void> | void;
23
+
24
+ /**
25
+ * Empties the store completely.
26
+ */
27
+ clearSync(): void;
28
+
29
+ /**
30
+ * Begins a new transaction.
31
+ */
32
+ transaction(): Transaction;
33
+ }
34
+
35
+ /**
36
+ * A transaction for a store.
37
+ */
38
+ export abstract class Transaction<T extends Store = Store> {
39
+ public constructor(protected store: T) {}
40
+
41
+ /**
42
+ * Whether the transaction was commited or aborted
43
+ */
44
+ protected done: boolean = false;
45
+
46
+ /**
47
+ * Retrieves the data at `ino`.
48
+ * @param ino The key to look under for data.
49
+ */
50
+ public abstract get(ino: Ino): Promise<Uint8Array>;
51
+
52
+ /**
53
+ * Retrieves the data at `ino`.
54
+ * Throws an error if an error occurs or if the key does not exist.
55
+ * @param ino The key to look under for data.
56
+ * @return The data stored under the key, or undefined if not present.
57
+ */
58
+ public abstract getSync(ino: Ino): Uint8Array;
59
+
60
+ /**
61
+ * Adds the data to the store under `ino`. Overwrites any existing data.
62
+ * @param ino The key to add the data under.
63
+ * @param data The data to add to the store.
64
+ */
65
+ public abstract set(ino: Ino, data: Uint8Array): Promise<void>;
66
+
67
+ /**
68
+ * Adds the data to the store under `ino`.
69
+ * @param ino The key to add the data under.
70
+ * @param data The data to add to the store.
71
+ */
72
+ public abstract setSync(ino: Ino, data: Uint8Array): void;
73
+
74
+ /**
75
+ * Deletes the data at `ino`.
76
+ * @param ino The key to delete from the store.
77
+ */
78
+ public abstract remove(ino: Ino): Promise<void>;
79
+
80
+ /**
81
+ * Deletes the data at `ino`.
82
+ * @param ino The key to delete from the store.
83
+ */
84
+ public abstract removeSync(ino: Ino): void;
85
+
86
+ /**
87
+ * Commits the transaction.
88
+ */
89
+ public abstract commit(): Promise<void>;
90
+
91
+ public async [Symbol.asyncDispose]() {
92
+ if (this.done) {
93
+ return;
94
+ }
95
+
96
+ await this.abort();
97
+ }
98
+
99
+ /**
100
+ * Commits the transaction.
101
+ */
102
+ public abstract commitSync(): void;
103
+
104
+ public [Symbol.dispose]() {
105
+ if (this.done) {
106
+ return;
107
+ }
108
+
109
+ this.abortSync();
110
+ }
111
+
112
+ /**
113
+ * Aborts and rolls back the transaction.
114
+ */
115
+ public abstract abort(): Promise<void>;
116
+
117
+ /**
118
+ * Aborts and rolls back the transaction.
119
+ */
120
+ public abstract abortSync(): void;
121
+ }
122
+
123
+ /**
124
+ * Transaction that implements asynchronous operations with synchronous ones
125
+ */
126
+ export abstract class SyncTransaction<T extends Store = Store> extends Transaction<T> {
127
+ /* eslint-disable @typescript-eslint/require-await */
128
+ public async get(ino: Ino): Promise<Uint8Array> {
129
+ return this.getSync(ino);
130
+ }
131
+
132
+ public async set(ino: bigint, data: Uint8Array): Promise<void> {
133
+ return this.setSync(ino, data);
134
+ }
135
+
136
+ public async remove(ino: Ino): Promise<void> {
137
+ return this.removeSync(ino);
138
+ }
139
+
140
+ public async commit(): Promise<void> {
141
+ return this.commitSync();
142
+ }
143
+
144
+ public async abort(): Promise<void> {
145
+ return this.abortSync();
146
+ }
147
+ /* eslint-enable @typescript-eslint/require-await */
148
+ }
149
+
150
+ /**
151
+ * Transaction that only supports asynchronous operations
152
+ */
153
+ export abstract class AsyncTransaction<T extends Store = Store> extends Transaction<T> {
154
+ public getSync(): Uint8Array {
155
+ throw ErrnoError.With('ENOSYS', undefined, 'AsyncTransaction.getSync');
156
+ }
157
+
158
+ public setSync(): void {
159
+ throw ErrnoError.With('ENOSYS', undefined, 'AsyncTransaction.setSync');
160
+ }
161
+
162
+ public removeSync(): void {
163
+ throw ErrnoError.With('ENOSYS', undefined, 'AsyncTransaction.removeSync');
164
+ }
165
+
166
+ public commitSync(): void {
167
+ throw ErrnoError.With('ENOSYS', undefined, 'AsyncTransaction.commitSync');
168
+ }
169
+
170
+ public abortSync(): void {
171
+ throw ErrnoError.With('ENOSYS', undefined, 'AsyncTransaction.abortSync');
172
+ }
173
+ }
package/src/config.ts ADDED
@@ -0,0 +1,173 @@
1
+ import type { Backend, BackendConfiguration, FilesystemOf, SharedConfig } from './backends/backend.js';
2
+ import { checkOptions, isBackend, isBackendConfig } from './backends/backend.js';
3
+ import { credentials } from './credentials.js';
4
+ import { DeviceFS, fullDevice, nullDevice, randomDevice, zeroDevice } from './devices.js';
5
+ import * as cache from './emulation/cache.js';
6
+ import * as fs from './emulation/index.js';
7
+ import type { AbsolutePath } from './emulation/path.js';
8
+ import { config, type MountObject } from './emulation/shared.js';
9
+ import { Errno, ErrnoError } from './error.js';
10
+ import { FileSystem } from './filesystem.js';
11
+
12
+ /**
13
+ * Configuration for a specific mount point
14
+ */
15
+ export type MountConfiguration<T extends Backend> = FilesystemOf<T> | BackendConfiguration<T> | T;
16
+
17
+ function isMountConfig<T extends Backend>(arg: unknown): arg is MountConfiguration<T> {
18
+ return isBackendConfig(arg) || isBackend(arg) || arg instanceof FileSystem;
19
+ }
20
+
21
+ /**
22
+ * Retrieve a file system with `configuration`.
23
+ * @see MountConfiguration
24
+ */
25
+ export async function resolveMountConfig<T extends Backend>(configuration: MountConfiguration<T>, _depth = 0): Promise<FilesystemOf<T>> {
26
+ if (typeof configuration !== 'object' || configuration == null) {
27
+ throw new ErrnoError(Errno.EINVAL, 'Invalid options on mount configuration');
28
+ }
29
+
30
+ if (!isMountConfig(configuration)) {
31
+ throw new ErrnoError(Errno.EINVAL, 'Invalid mount configuration');
32
+ }
33
+
34
+ if (configuration instanceof FileSystem) {
35
+ await configuration.ready();
36
+ return configuration;
37
+ }
38
+
39
+ if (isBackend(configuration)) {
40
+ configuration = { backend: configuration } as BackendConfiguration<T>;
41
+ }
42
+
43
+ for (const [key, value] of Object.entries(configuration)) {
44
+ if (key == 'backend') {
45
+ continue;
46
+ }
47
+
48
+ if (!isMountConfig(value)) {
49
+ continue;
50
+ }
51
+
52
+ if (_depth > 10) {
53
+ throw new ErrnoError(Errno.EINVAL, 'Invalid configuration, too deep and possibly infinite');
54
+ }
55
+
56
+ (configuration as Record<string, FileSystem>)[key] = await resolveMountConfig(value, ++_depth);
57
+ }
58
+
59
+ const { backend } = configuration;
60
+
61
+ if (!(await backend.isAvailable())) {
62
+ throw new ErrnoError(Errno.EPERM, 'Backend not available: ' + backend.name);
63
+ }
64
+ await checkOptions(backend, configuration);
65
+ const mount = (await backend.create(configuration)) as FilesystemOf<T>;
66
+ mount._disableSync = configuration.disableAsyncCache || false;
67
+ await mount.ready();
68
+ return mount;
69
+ }
70
+
71
+ export interface ConfigMounts {
72
+ [K: AbsolutePath]: Backend;
73
+ }
74
+
75
+ /**
76
+ * Configuration
77
+ */
78
+ export interface Configuration<T extends ConfigMounts> extends SharedConfig {
79
+ /**
80
+ * An object mapping mount points to mount configuration
81
+ */
82
+ mounts: { [K in keyof T & AbsolutePath]: MountConfiguration<T[K]> };
83
+
84
+ /**
85
+ * The uid to use
86
+ * @default 0
87
+ */
88
+ uid: number;
89
+
90
+ /**
91
+ * The gid to use
92
+ * @default 0
93
+ */
94
+ gid: number;
95
+
96
+ /**
97
+ * Whether to automatically add normal Linux devices
98
+ * @default false
99
+ * @experimental
100
+ */
101
+ addDevices: boolean;
102
+
103
+ /**
104
+ * If true, enables caching stats for certain operations.
105
+ * This should reduce the number of stat calls performed.
106
+ * @default false
107
+ * @experimental
108
+ */
109
+ cacheStats: boolean;
110
+
111
+ /**
112
+ * If true, disables *all* permissions checking.
113
+ * This can increase performance
114
+ * @default false
115
+ * @experimental
116
+ */
117
+ disableAccessChecks: boolean;
118
+ }
119
+
120
+ /**
121
+ * Configures ZenFS with single mount point /
122
+ */
123
+ export async function configureSingle<T extends Backend>(configuration: MountConfiguration<T>): Promise<void> {
124
+ if (!isBackendConfig(configuration)) {
125
+ throw new TypeError('Invalid single mount point configuration');
126
+ }
127
+
128
+ const resolved = await resolveMountConfig(configuration);
129
+ fs.umount('/');
130
+ fs.mount('/', resolved);
131
+ }
132
+
133
+ /**
134
+ * Configures ZenFS with `configuration`
135
+ * @see Configuration
136
+ */
137
+ export async function configure<T extends ConfigMounts>(configuration: Partial<Configuration<T>>): Promise<void> {
138
+ const uid = 'uid' in configuration ? configuration.uid || 0 : 0;
139
+ const gid = 'gid' in configuration ? configuration.gid || 0 : 0;
140
+
141
+ Object.assign(credentials, { uid, gid, suid: uid, sgid: gid, euid: uid, egid: gid });
142
+
143
+ cache.setEnabled(configuration.cacheStats ?? false);
144
+ config.checkAccess = !configuration.disableAccessChecks;
145
+
146
+ if (configuration.addDevices) {
147
+ const devfs = new DeviceFS();
148
+ devfs.createDevice('/null', nullDevice);
149
+ devfs.createDevice('/zero', zeroDevice);
150
+ devfs.createDevice('/full', fullDevice);
151
+ devfs.createDevice('/random', randomDevice);
152
+ await devfs.ready();
153
+ fs.mount('/dev', devfs);
154
+ }
155
+
156
+ if (!configuration.mounts) {
157
+ return;
158
+ }
159
+
160
+ for (const [point, mountConfig] of Object.entries(configuration.mounts)) {
161
+ if (!point.startsWith('/')) {
162
+ throw new ErrnoError(Errno.EINVAL, 'Mount points must have absolute paths');
163
+ }
164
+
165
+ if (isBackendConfig(mountConfig)) {
166
+ mountConfig.disableAsyncCache ??= configuration.disableAsyncCache || false;
167
+ }
168
+
169
+ configuration.mounts[point as keyof T & `/${string}`] = await resolveMountConfig(mountConfig);
170
+ }
171
+
172
+ fs.mountObject(configuration.mounts as MountObject);
173
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Credentials used for various operations.
3
+ * Similar to Linux's cred struct.
4
+ * @see https://github.com/torvalds/linux/blob/master/include/linux/cred.h
5
+ */
6
+ export interface Credentials {
7
+ uid: number;
8
+ gid: number;
9
+ suid: number;
10
+ sgid: number;
11
+ euid: number;
12
+ egid: number;
13
+ }
14
+
15
+ export const credentials: Credentials = {
16
+ uid: 0,
17
+ gid: 0,
18
+ suid: 0,
19
+ sgid: 0,
20
+ euid: 0,
21
+ egid: 0,
22
+ };
23
+
24
+ export const rootCredentials: Credentials = {
25
+ uid: 0,
26
+ gid: 0,
27
+ suid: 0,
28
+ sgid: 0,
29
+ euid: 0,
30
+ egid: 0,
31
+ };