@zenfs/core 1.1.6 → 1.2.1

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 (83) 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 +24 -1
  5. package/dist/config.js +5 -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/config.d.ts +10 -0
  10. package/dist/emulation/config.js +10 -0
  11. package/dist/emulation/promises.d.ts +9 -14
  12. package/dist/emulation/promises.js +71 -47
  13. package/dist/emulation/shared.d.ts +16 -0
  14. package/dist/emulation/sync.d.ts +11 -20
  15. package/dist/emulation/sync.js +44 -22
  16. package/dist/file.d.ts +1 -1
  17. package/dist/file.js +6 -3
  18. package/package.json +4 -2
  19. package/readme.md +1 -1
  20. package/scripts/test.js +19 -1
  21. package/src/backends/backend.ts +160 -0
  22. package/src/backends/fetch.ts +180 -0
  23. package/src/backends/file_index.ts +206 -0
  24. package/src/backends/memory.ts +50 -0
  25. package/src/backends/overlay.ts +560 -0
  26. package/src/backends/port/fs.ts +335 -0
  27. package/src/backends/port/readme.md +54 -0
  28. package/src/backends/port/rpc.ts +167 -0
  29. package/src/backends/readme.md +3 -0
  30. package/src/backends/store/fs.ts +700 -0
  31. package/src/backends/store/readme.md +9 -0
  32. package/src/backends/store/simple.ts +146 -0
  33. package/src/backends/store/store.ts +173 -0
  34. package/src/config.ts +185 -0
  35. package/src/credentials.ts +31 -0
  36. package/src/devices.ts +459 -0
  37. package/src/emulation/async.ts +834 -0
  38. package/src/emulation/cache.ts +44 -0
  39. package/src/emulation/config.ts +11 -0
  40. package/src/emulation/constants.ts +182 -0
  41. package/src/emulation/dir.ts +138 -0
  42. package/src/emulation/index.ts +8 -0
  43. package/src/emulation/path.ts +440 -0
  44. package/src/emulation/promises.ts +1134 -0
  45. package/src/emulation/shared.ts +153 -0
  46. package/src/emulation/streams.ts +34 -0
  47. package/src/emulation/sync.ts +868 -0
  48. package/src/emulation/watchers.ts +193 -0
  49. package/src/error.ts +307 -0
  50. package/src/file.ts +662 -0
  51. package/src/filesystem.ts +174 -0
  52. package/src/index.ts +25 -0
  53. package/src/inode.ts +132 -0
  54. package/src/mixins/async.ts +208 -0
  55. package/src/mixins/index.ts +5 -0
  56. package/src/mixins/mutexed.ts +257 -0
  57. package/src/mixins/readonly.ts +96 -0
  58. package/src/mixins/shared.ts +25 -0
  59. package/src/mixins/sync.ts +58 -0
  60. package/src/polyfills.ts +21 -0
  61. package/src/stats.ts +363 -0
  62. package/src/utils.ts +288 -0
  63. package/tests/common.ts +1 -11
  64. package/tests/fs/directory.test.ts +1 -1
  65. package/tests/fs/errors.test.ts +1 -1
  66. package/tests/fs/links.test.ts +1 -1
  67. package/tests/fs/open.test.ts +1 -1
  68. package/tests/fs/permissions.test.ts +4 -4
  69. package/tests/fs/readdir.test.ts +3 -3
  70. package/tests/fs/rename.test.ts +1 -1
  71. package/tests/fs/stat.test.ts +1 -1
  72. package/tests/fs/times.test.ts +2 -2
  73. package/tests/fs/truncate.test.ts +1 -1
  74. package/tests/port/channel.test.ts +3 -3
  75. package/tests/port/config.test.ts +4 -5
  76. package/tests/port/config.worker.js +5 -0
  77. package/tests/port/remote.test.ts +4 -5
  78. package/tests/port/remote.worker.js +5 -0
  79. package/tests/port/timeout.test.ts +2 -2
  80. package/tests/setup/common.ts +1 -1
  81. package/tests/setup/cow+fetch.ts +1 -1
  82. package/tests/port/config.worker.ts +0 -5
  83. package/tests/port/remote.worker.ts +0 -5
@@ -0,0 +1,180 @@
1
+ import { Errno, ErrnoError } from '../error.js';
2
+ import type { FileSystemMetadata } from '../filesystem.js';
3
+ import type { Stats } from '../stats.js';
4
+ import type { Backend } from './backend.js';
5
+ import { IndexFS } from './file_index.js';
6
+ import type { IndexData } from './file_index.js';
7
+
8
+ /**
9
+ * Asynchronously download a file as a buffer or a JSON object.
10
+ * Note that the third function signature with a non-specialized type is
11
+ * invalid, but TypeScript requires it when you specialize string arguments to
12
+ * constants.
13
+ * @hidden
14
+ */
15
+ async function fetchFile(path: string, type: 'buffer'): Promise<Uint8Array>;
16
+ async function fetchFile<T extends object>(path: string, type: 'json'): Promise<T>;
17
+ async function fetchFile<T extends object>(path: string, type: 'buffer' | 'json'): Promise<T | Uint8Array>;
18
+ async function fetchFile<T extends object>(path: string, type: string): Promise<T | Uint8Array> {
19
+ const response = await fetch(path).catch((e: Error) => {
20
+ throw new ErrnoError(Errno.EIO, e.message, path);
21
+ });
22
+ if (!response.ok) {
23
+ throw new ErrnoError(Errno.EIO, 'fetch failed: response returned code ' + response.status, path);
24
+ }
25
+ switch (type) {
26
+ case 'buffer': {
27
+ const arrayBuffer = await response.arrayBuffer().catch((e: Error) => {
28
+ throw new ErrnoError(Errno.EIO, e.message, path);
29
+ });
30
+ return new Uint8Array(arrayBuffer);
31
+ }
32
+ case 'json':
33
+ return response.json().catch((e: Error) => {
34
+ throw new ErrnoError(Errno.EIO, e.message, path);
35
+ }) as Promise<T>;
36
+ default:
37
+ throw new ErrnoError(Errno.EINVAL, 'Invalid download type: ' + type);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Configuration options for FetchFS.
43
+ */
44
+ export interface FetchOptions {
45
+ /**
46
+ * URL to a file index as a JSON file or the file index object itself.
47
+ * Defaults to `index.json`.
48
+ */
49
+ index?: string | IndexData;
50
+
51
+ /** Used as the URL prefix for fetched files.
52
+ * Default: Fetch files relative to the index.
53
+ */
54
+ baseUrl?: string;
55
+ }
56
+
57
+ /**
58
+ * A simple filesystem backed by HTTP using the `fetch` API.
59
+ *
60
+ *
61
+ * Index objects look like the following:
62
+ *
63
+ * ```json
64
+ * {
65
+ * "version": 1,
66
+ * "entries": {
67
+ * "/home": { ... },
68
+ * "/home/jvilk": { ... },
69
+ * "/home/james": { ... }
70
+ * }
71
+ * }
72
+ * ```
73
+ *
74
+ * Each entry contains the stats associated with the file.
75
+ */
76
+ export class FetchFS extends IndexFS {
77
+ public readonly baseUrl: string;
78
+
79
+ public async ready(): Promise<void> {
80
+ if (this._isInitialized) {
81
+ return;
82
+ }
83
+ await super.ready();
84
+
85
+ if (this._disableSync) {
86
+ return;
87
+ }
88
+
89
+ /**
90
+ * Iterate over all of the files and cache their contents
91
+ */
92
+ for (const [path, stats] of this.index.files()) {
93
+ await this.getData(path, stats);
94
+ }
95
+ }
96
+
97
+ public constructor({ index = 'index.json', baseUrl = '' }: FetchOptions) {
98
+ // prefix url must end in a directory separator.
99
+ if (baseUrl.at(-1) != '/') {
100
+ baseUrl += '/';
101
+ }
102
+
103
+ super(typeof index != 'string' ? index : fetchFile<IndexData>(baseUrl + index, 'json'));
104
+
105
+ this.baseUrl = baseUrl;
106
+ }
107
+
108
+ public metadata(): FileSystemMetadata {
109
+ return {
110
+ ...super.metadata(),
111
+ name: FetchFS.name,
112
+ readonly: true,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Preload the `path` into the index.
118
+ */
119
+ public preload(path: string, buffer: Uint8Array): void {
120
+ const stats = this.index.get(path);
121
+ if (!stats) {
122
+ throw ErrnoError.With('ENOENT', path, 'preload');
123
+ }
124
+ if (!stats.isFile()) {
125
+ throw ErrnoError.With('EISDIR', path, 'preload');
126
+ }
127
+ stats.size = buffer.length;
128
+ stats.fileData = buffer;
129
+ }
130
+
131
+ /**
132
+ * @todo Be lazier about actually requesting the data?
133
+ */
134
+ protected async getData(path: string, stats: Stats): Promise<Uint8Array> {
135
+ if (stats.fileData) {
136
+ return stats.fileData;
137
+ }
138
+
139
+ const data = await fetchFile(this.baseUrl + (path.startsWith('/') ? path.slice(1) : path), 'buffer');
140
+ stats.fileData = data;
141
+ return data;
142
+ }
143
+
144
+ protected getDataSync(path: string, stats: Stats): Uint8Array {
145
+ if (stats.fileData) {
146
+ return stats.fileData;
147
+ }
148
+
149
+ throw new ErrnoError(Errno.ENODATA, '', path, 'getData');
150
+ }
151
+ }
152
+
153
+ const _Fetch = {
154
+ name: 'Fetch',
155
+
156
+ options: {
157
+ index: {
158
+ type: ['string', 'object'],
159
+ required: false,
160
+ description: 'URL to a file index as a JSON file or the file index object itself, generated with the make-index script. Defaults to `index.json`.',
161
+ },
162
+ baseUrl: {
163
+ type: 'string',
164
+ required: false,
165
+ description: 'Used as the URL prefix for fetched files. Default: Fetch files relative to the index.',
166
+ },
167
+ },
168
+
169
+ isAvailable(): boolean {
170
+ return typeof globalThis.fetch == 'function';
171
+ },
172
+
173
+ create(options: FetchOptions) {
174
+ return new FetchFS(options);
175
+ },
176
+ } as const satisfies Backend<FetchFS, FetchOptions>;
177
+ type _Fetch = typeof _Fetch;
178
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
179
+ export interface Fetch extends _Fetch {}
180
+ export const Fetch: Fetch = _Fetch;
@@ -0,0 +1,206 @@
1
+ /* Note: this file is named file_index.ts because Typescript has special behavior regarding index.ts which can't be disabled. */
2
+
3
+ import { isJSON } from 'utilium';
4
+ import { basename, dirname } from '../emulation/path.js';
5
+ import { Errno, ErrnoError } from '../error.js';
6
+ import { NoSyncFile, isWriteable } from '../file.js';
7
+ import { FileSystem } from '../filesystem.js';
8
+ import { Readonly } from '../mixins/readonly.js';
9
+ import type { StatsLike } from '../stats.js';
10
+ import { Stats } from '../stats.js';
11
+ import { decodeUTF8, encodeUTF8 } from '../utils.js';
12
+
13
+ /**
14
+ * An Index in JSON form
15
+ * @internal
16
+ */
17
+ export interface IndexData {
18
+ version: 1;
19
+ entries: Record<string, StatsLike<number>>;
20
+ }
21
+
22
+ export const version = 1;
23
+
24
+ /**
25
+ * An index of files
26
+ * @internal
27
+ */
28
+ export class Index extends Map<string, Stats> {
29
+ /**
30
+ * Convenience method
31
+ */
32
+ public files(): Map<string, Stats> {
33
+ const files = new Map<string, Stats>();
34
+ for (const [path, stats] of this) {
35
+ if (stats.isFile()) {
36
+ files.set(path, stats);
37
+ }
38
+ }
39
+ return files;
40
+ }
41
+
42
+ /**
43
+ * Converts the index to JSON
44
+ */
45
+ public toJSON(): IndexData {
46
+ return {
47
+ version,
48
+ entries: Object.fromEntries(this),
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Converts the index to a string
54
+ */
55
+ public toString(): string {
56
+ return JSON.stringify(this.toJSON());
57
+ }
58
+
59
+ /**
60
+ * Returns the files in the directory `dir`.
61
+ * This is expensive so it is only called once per directory.
62
+ */
63
+ protected dirEntries(dir: string): string[] {
64
+ const entries = [];
65
+ for (const entry of this.keys()) {
66
+ if (dirname(entry) == dir) {
67
+ entries.push(basename(entry));
68
+ }
69
+ }
70
+ return entries;
71
+ }
72
+
73
+ /**
74
+ * Loads the index from JSON data
75
+ */
76
+ public fromJSON(json: IndexData): void {
77
+ if (json.version != version) {
78
+ throw new ErrnoError(Errno.EINVAL, 'Index version mismatch');
79
+ }
80
+
81
+ this.clear();
82
+
83
+ for (const [path, data] of Object.entries(json.entries)) {
84
+ const stats = new Stats(data);
85
+ if (stats.isDirectory()) {
86
+ stats.fileData = encodeUTF8(JSON.stringify(this.dirEntries(path)));
87
+ }
88
+ this.set(path, stats);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Parses an index from a string
94
+ */
95
+ public static parse(data: string): Index {
96
+ if (!isJSON(data)) {
97
+ throw new ErrnoError(Errno.EINVAL, 'Invalid JSON');
98
+ }
99
+
100
+ const json = JSON.parse(data) as IndexData;
101
+ const index = new Index();
102
+ index.fromJSON(json);
103
+ return index;
104
+ }
105
+ }
106
+
107
+ export abstract class IndexFS extends Readonly(FileSystem) {
108
+ protected index: Index = new Index();
109
+
110
+ protected _isInitialized: boolean = false;
111
+
112
+ public async ready(): Promise<void> {
113
+ await super.ready();
114
+ if (this._isInitialized) {
115
+ return;
116
+ }
117
+ this.index.fromJSON(await this.indexData);
118
+ this._isInitialized = true;
119
+ }
120
+
121
+ public constructor(private indexData: IndexData | Promise<IndexData>) {
122
+ super();
123
+ }
124
+
125
+ public async reloadFiles(): Promise<void> {
126
+ for (const [path, stats] of this.index.files()) {
127
+ delete stats.fileData;
128
+ stats.fileData = await this.getData(path, stats);
129
+ }
130
+ }
131
+
132
+ public reloadFilesSync(): void {
133
+ for (const [path, stats] of this.index.files()) {
134
+ delete stats.fileData;
135
+ stats.fileData = this.getDataSync(path, stats);
136
+ }
137
+ }
138
+
139
+ public stat(path: string): Promise<Stats> {
140
+ return Promise.resolve(this.statSync(path));
141
+ }
142
+
143
+ public statSync(path: string): Stats {
144
+ if (!this.index.has(path)) {
145
+ throw ErrnoError.With('ENOENT', path, 'stat');
146
+ }
147
+
148
+ return this.index.get(path)!;
149
+ }
150
+
151
+ public async openFile(path: string, flag: string): Promise<NoSyncFile<this>> {
152
+ if (isWriteable(flag)) {
153
+ // You can't write to files on this file system.
154
+ throw new ErrnoError(Errno.EPERM, path);
155
+ }
156
+
157
+ // Check if the path exists, and is a file.
158
+ const stats = this.index.get(path);
159
+
160
+ if (!stats) {
161
+ throw ErrnoError.With('ENOENT', path, 'openFile');
162
+ }
163
+
164
+ return new NoSyncFile(this, path, flag, stats, stats.isDirectory() ? stats.fileData : await this.getData(path, stats));
165
+ }
166
+
167
+ public openFileSync(path: string, flag: string): NoSyncFile<this> {
168
+ if (isWriteable(flag)) {
169
+ // You can't write to files on this file system.
170
+ throw new ErrnoError(Errno.EPERM, path);
171
+ }
172
+
173
+ // Check if the path exists, and is a file.
174
+ const stats = this.index.get(path);
175
+
176
+ if (!stats) {
177
+ throw ErrnoError.With('ENOENT', path, 'openFile');
178
+ }
179
+
180
+ return new NoSyncFile(this, path, flag, stats, stats.isDirectory() ? stats.fileData : this.getDataSync(path, stats));
181
+ }
182
+
183
+ public readdir(path: string): Promise<string[]> {
184
+ return Promise.resolve(this.readdirSync(path));
185
+ }
186
+
187
+ public readdirSync(path: string): string[] {
188
+ // Check if it exists.
189
+ const stats = this.index.get(path);
190
+ if (!stats) {
191
+ throw ErrnoError.With('ENOENT', path, 'readdir');
192
+ }
193
+
194
+ const content: unknown = JSON.parse(decodeUTF8(stats.fileData));
195
+ if (!Array.isArray(content)) {
196
+ throw ErrnoError.With('ENODATA', path, 'readdir');
197
+ }
198
+ if (!content.every(item => typeof item == 'string')) {
199
+ throw ErrnoError.With('ENODATA', path, 'readdir');
200
+ }
201
+ return content;
202
+ }
203
+
204
+ protected abstract getData(path: string, stats: Stats): Promise<Uint8Array>;
205
+ protected abstract getDataSync(path: string, stats: Stats): Uint8Array;
206
+ }
@@ -0,0 +1,50 @@
1
+ import type { Ino } from '../inode.js';
2
+ import type { Backend } from './backend.js';
3
+ import { StoreFS } from './store/fs.js';
4
+ import { SimpleTransaction, type SimpleSyncStore } from './store/simple.js';
5
+
6
+ /**
7
+ * A simple in-memory store
8
+ */
9
+ export class InMemoryStore extends Map<Ino, Uint8Array> implements SimpleSyncStore {
10
+ public constructor(public name: string = 'tmp') {
11
+ super();
12
+ }
13
+
14
+ public async sync(): Promise<void> {}
15
+
16
+ public clearSync(): void {
17
+ this.clear();
18
+ }
19
+
20
+ public transaction(): SimpleTransaction {
21
+ return new SimpleTransaction(this);
22
+ }
23
+ }
24
+
25
+ /**
26
+ * A simple in-memory file system backed by an InMemoryStore.
27
+ * Files are not persisted across page loads.
28
+ */
29
+ const _InMemory = {
30
+ name: 'InMemory',
31
+ isAvailable(): boolean {
32
+ return true;
33
+ },
34
+ options: {
35
+ name: {
36
+ type: 'string',
37
+ required: false,
38
+ description: 'The name of the store',
39
+ },
40
+ },
41
+ create({ name }: { name?: string }) {
42
+ const fs = new StoreFS(new InMemoryStore(name));
43
+ fs.checkRootSync();
44
+ return fs;
45
+ },
46
+ } as const satisfies Backend<StoreFS<InMemoryStore>, { name?: string }>;
47
+ type _InMemory = typeof _InMemory;
48
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
49
+ export interface InMemory extends _InMemory {}
50
+ export const InMemory: InMemory = _InMemory;