@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.
- package/dist/backends/file_index.js +0 -3
- package/dist/backends/overlay.js +0 -8
- package/dist/backends/store/fs.js +4 -17
- package/dist/config.d.ts +14 -0
- package/dist/config.js +4 -0
- package/dist/devices.js +0 -12
- package/dist/emulation/cache.d.ts +21 -0
- package/dist/emulation/cache.js +36 -0
- package/dist/emulation/promises.d.ts +9 -14
- package/dist/emulation/promises.js +71 -48
- package/dist/emulation/shared.d.ts +22 -0
- package/dist/emulation/shared.js +6 -0
- package/dist/emulation/sync.d.ts +11 -20
- package/dist/emulation/sync.js +44 -23
- package/package.json +4 -2
- package/scripts/test.js +14 -1
- package/src/backends/backend.ts +160 -0
- package/src/backends/fetch.ts +180 -0
- package/src/backends/file_index.ts +206 -0
- package/src/backends/memory.ts +50 -0
- package/src/backends/overlay.ts +560 -0
- package/src/backends/port/fs.ts +335 -0
- package/src/backends/port/readme.md +54 -0
- package/src/backends/port/rpc.ts +167 -0
- package/src/backends/readme.md +3 -0
- package/src/backends/store/fs.ts +700 -0
- package/src/backends/store/readme.md +9 -0
- package/src/backends/store/simple.ts +146 -0
- package/src/backends/store/store.ts +173 -0
- package/src/config.ts +173 -0
- package/src/credentials.ts +31 -0
- package/src/devices.ts +459 -0
- package/src/emulation/async.ts +834 -0
- package/src/emulation/cache.ts +44 -0
- package/src/emulation/constants.ts +182 -0
- package/src/emulation/dir.ts +138 -0
- package/src/emulation/index.ts +8 -0
- package/src/emulation/path.ts +440 -0
- package/src/emulation/promises.ts +1133 -0
- package/src/emulation/shared.ts +160 -0
- package/src/emulation/streams.ts +34 -0
- package/src/emulation/sync.ts +867 -0
- package/src/emulation/watchers.ts +193 -0
- package/src/error.ts +307 -0
- package/src/file.ts +661 -0
- package/src/filesystem.ts +174 -0
- package/src/index.ts +25 -0
- package/src/inode.ts +132 -0
- package/src/mixins/async.ts +208 -0
- package/src/mixins/index.ts +5 -0
- package/src/mixins/mutexed.ts +257 -0
- package/src/mixins/readonly.ts +96 -0
- package/src/mixins/shared.ts +25 -0
- package/src/mixins/sync.ts +58 -0
- package/src/polyfills.ts +21 -0
- package/src/stats.ts +363 -0
- package/src/utils.ts +288 -0
- package/tests/fs/readdir.test.ts +3 -3
|
@@ -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;
|