@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,700 @@
|
|
|
1
|
+
import { credentials } from '../../credentials.js';
|
|
2
|
+
import { S_IFDIR, S_IFREG } from '../../emulation/constants.js';
|
|
3
|
+
import { basename, dirname, join, resolve } from '../../emulation/path.js';
|
|
4
|
+
import { Errno, ErrnoError } from '../../error.js';
|
|
5
|
+
import { PreloadFile } from '../../file.js';
|
|
6
|
+
import { FileSystem, type FileSystemMetadata } from '../../filesystem.js';
|
|
7
|
+
import { type Ino, Inode, randomIno, rootIno } from '../../inode.js';
|
|
8
|
+
import type { FileType, Stats } from '../../stats.js';
|
|
9
|
+
import { decodeDirListing, encodeUTF8, encodeDirListing } from '../../utils.js';
|
|
10
|
+
import type { Store, Transaction } from './store.js';
|
|
11
|
+
import type { File } from '../../file.js';
|
|
12
|
+
|
|
13
|
+
const maxInodeAllocTries = 5;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A file system which uses a key-value store.
|
|
17
|
+
*
|
|
18
|
+
* We use a unique ID for each node in the file system. The root node has a fixed ID.
|
|
19
|
+
* @todo Introduce Node ID caching.
|
|
20
|
+
* @todo Check modes.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export class StoreFS<T extends Store = Store> extends FileSystem {
|
|
24
|
+
private _initialized: boolean = false;
|
|
25
|
+
|
|
26
|
+
public async ready(): Promise<void> {
|
|
27
|
+
if (this._initialized) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
await this.checkRoot();
|
|
31
|
+
this._initialized = true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public constructor(protected store: T) {
|
|
35
|
+
super();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public metadata(): FileSystemMetadata {
|
|
39
|
+
return {
|
|
40
|
+
...super.metadata(),
|
|
41
|
+
name: this.store.name,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Delete all contents stored in the file system.
|
|
47
|
+
* @deprecated
|
|
48
|
+
*/
|
|
49
|
+
public async empty(): Promise<void> {
|
|
50
|
+
await this.store.clear();
|
|
51
|
+
// Root always exists.
|
|
52
|
+
await this.checkRoot();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Delete all contents stored in the file system.
|
|
57
|
+
* @deprecated
|
|
58
|
+
*/
|
|
59
|
+
public emptySync(): void {
|
|
60
|
+
this.store.clearSync();
|
|
61
|
+
// Root always exists.
|
|
62
|
+
this.checkRootSync();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @todo Make rename compatible with the cache.
|
|
67
|
+
*/
|
|
68
|
+
public async rename(oldPath: string, newPath: string): Promise<void> {
|
|
69
|
+
await using tx = this.store.transaction();
|
|
70
|
+
const oldParent = dirname(oldPath),
|
|
71
|
+
oldName = basename(oldPath),
|
|
72
|
+
newParent = dirname(newPath),
|
|
73
|
+
newName = basename(newPath),
|
|
74
|
+
// Remove oldPath from parent's directory listing.
|
|
75
|
+
oldDirNode = await this.findINode(tx, oldParent),
|
|
76
|
+
oldDirList = await this.getDirListing(tx, oldDirNode, oldParent);
|
|
77
|
+
|
|
78
|
+
if (!oldDirList[oldName]) {
|
|
79
|
+
throw ErrnoError.With('ENOENT', oldPath, 'rename');
|
|
80
|
+
}
|
|
81
|
+
const nodeId: Ino = oldDirList[oldName];
|
|
82
|
+
delete oldDirList[oldName];
|
|
83
|
+
|
|
84
|
+
/*
|
|
85
|
+
Can't move a folder inside itself.
|
|
86
|
+
This ensures that the check passes only if `oldPath` is a subpath of `newParent`.
|
|
87
|
+
We append '/' to avoid matching folders that are a substring of the bottom-most folder in the path.
|
|
88
|
+
*/
|
|
89
|
+
if ((newParent + '/').indexOf(oldPath + '/') === 0) {
|
|
90
|
+
throw new ErrnoError(Errno.EBUSY, oldParent);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Add newPath to parent's directory listing.
|
|
94
|
+
|
|
95
|
+
const sameParent = newParent === oldParent;
|
|
96
|
+
|
|
97
|
+
// Prevent us from re-grabbing the same directory listing, which still contains `oldName.`
|
|
98
|
+
const newDirNode: Inode = sameParent ? oldDirNode : await this.findINode(tx, newParent);
|
|
99
|
+
const newDirList: typeof oldDirList = sameParent ? oldDirList : await this.getDirListing(tx, newDirNode, newParent);
|
|
100
|
+
|
|
101
|
+
if (newDirList[newName]) {
|
|
102
|
+
// If it's a file, delete it, if it's a directory, throw a permissions error.
|
|
103
|
+
const newNameNode = await this.getINode(tx, newDirList[newName], newPath);
|
|
104
|
+
if (!newNameNode.toStats().isFile()) {
|
|
105
|
+
throw ErrnoError.With('EPERM', newPath, 'rename');
|
|
106
|
+
}
|
|
107
|
+
await tx.remove(newNameNode.ino);
|
|
108
|
+
await tx.remove(newDirList[newName]);
|
|
109
|
+
}
|
|
110
|
+
newDirList[newName] = nodeId;
|
|
111
|
+
// Commit the two changed directory listings.
|
|
112
|
+
await tx.set(oldDirNode.ino, encodeDirListing(oldDirList));
|
|
113
|
+
await tx.set(newDirNode.ino, encodeDirListing(newDirList));
|
|
114
|
+
await tx.commit();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public renameSync(oldPath: string, newPath: string): void {
|
|
118
|
+
using tx = this.store.transaction();
|
|
119
|
+
const oldParent = dirname(oldPath),
|
|
120
|
+
oldName = basename(oldPath),
|
|
121
|
+
newParent = dirname(newPath),
|
|
122
|
+
newName = basename(newPath),
|
|
123
|
+
// Remove oldPath from parent's directory listing.
|
|
124
|
+
oldDirNode = this.findINodeSync(tx, oldParent),
|
|
125
|
+
oldDirList = this.getDirListingSync(tx, oldDirNode, oldParent);
|
|
126
|
+
|
|
127
|
+
if (!oldDirList[oldName]) {
|
|
128
|
+
throw ErrnoError.With('ENOENT', oldPath, 'rename');
|
|
129
|
+
}
|
|
130
|
+
const ino: Ino = oldDirList[oldName];
|
|
131
|
+
delete oldDirList[oldName];
|
|
132
|
+
|
|
133
|
+
/*
|
|
134
|
+
Can't move a folder inside itself.
|
|
135
|
+
This ensures that the check passes only if `oldPath` is a subpath of `newParent`.
|
|
136
|
+
We append '/' to avoid matching folders that are a substring of the bottom-most folder in the path.
|
|
137
|
+
*/
|
|
138
|
+
if ((newParent + '/').indexOf(oldPath + '/') == 0) {
|
|
139
|
+
throw new ErrnoError(Errno.EBUSY, oldParent);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Add newPath to parent's directory listing.
|
|
143
|
+
const sameParent = newParent === oldParent;
|
|
144
|
+
|
|
145
|
+
// Prevent us from re-grabbing the same directory listing, which still contains `oldName.`
|
|
146
|
+
const newDirNode: Inode = sameParent ? oldDirNode : this.findINodeSync(tx, newParent);
|
|
147
|
+
const newDirList: typeof oldDirList = sameParent ? oldDirList : this.getDirListingSync(tx, newDirNode, newParent);
|
|
148
|
+
|
|
149
|
+
if (newDirList[newName]) {
|
|
150
|
+
// If it's a file, delete it, if it's a directory, throw a permissions error.
|
|
151
|
+
const newNameNode = this.getINodeSync(tx, newDirList[newName], newPath);
|
|
152
|
+
if (!newNameNode.toStats().isFile()) {
|
|
153
|
+
throw ErrnoError.With('EPERM', newPath, 'rename');
|
|
154
|
+
}
|
|
155
|
+
tx.removeSync(newNameNode.ino);
|
|
156
|
+
tx.removeSync(newDirList[newName]);
|
|
157
|
+
}
|
|
158
|
+
newDirList[newName] = ino;
|
|
159
|
+
|
|
160
|
+
// Commit the two changed directory listings.
|
|
161
|
+
tx.setSync(oldDirNode.ino, encodeDirListing(oldDirList));
|
|
162
|
+
tx.setSync(newDirNode.ino, encodeDirListing(newDirList));
|
|
163
|
+
tx.commitSync();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
public async stat(path: string): Promise<Stats> {
|
|
167
|
+
await using tx = this.store.transaction();
|
|
168
|
+
return (await this.findINode(tx, path)).toStats();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public statSync(path: string): Stats {
|
|
172
|
+
using tx = this.store.transaction();
|
|
173
|
+
return this.findINodeSync(tx, path).toStats();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
public async createFile(path: string, flag: string, mode: number): Promise<File> {
|
|
177
|
+
const node = await this.commitNew(path, S_IFREG, mode, new Uint8Array(0));
|
|
178
|
+
return new PreloadFile(this, path, flag, node.toStats(), new Uint8Array(0));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public createFileSync(path: string, flag: string, mode: number): File {
|
|
182
|
+
this.commitNewSync(path, S_IFREG, mode);
|
|
183
|
+
return this.openFileSync(path, flag);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
public async openFile(path: string, flag: string): Promise<File> {
|
|
187
|
+
await using tx = this.store.transaction();
|
|
188
|
+
const node = await this.findINode(tx, path),
|
|
189
|
+
data = await tx.get(node.ino);
|
|
190
|
+
if (!data) {
|
|
191
|
+
throw ErrnoError.With('ENOENT', path, 'openFile');
|
|
192
|
+
}
|
|
193
|
+
return new PreloadFile(this, path, flag, node.toStats(), data);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
public openFileSync(path: string, flag: string): File {
|
|
197
|
+
using tx = this.store.transaction();
|
|
198
|
+
const node = this.findINodeSync(tx, path),
|
|
199
|
+
data = tx.getSync(node.ino);
|
|
200
|
+
if (!data) {
|
|
201
|
+
throw ErrnoError.With('ENOENT', path, 'openFile');
|
|
202
|
+
}
|
|
203
|
+
return new PreloadFile(this, path, flag, node.toStats(), data);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
public async unlink(path: string): Promise<void> {
|
|
207
|
+
return this.remove(path, false);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
public unlinkSync(path: string): void {
|
|
211
|
+
this.removeSync(path, false);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
public async rmdir(path: string): Promise<void> {
|
|
215
|
+
if ((await this.readdir(path)).length) {
|
|
216
|
+
throw ErrnoError.With('ENOTEMPTY', path, 'rmdir');
|
|
217
|
+
}
|
|
218
|
+
await this.remove(path, true);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
public rmdirSync(path: string): void {
|
|
222
|
+
if (this.readdirSync(path).length) {
|
|
223
|
+
throw ErrnoError.With('ENOTEMPTY', path, 'rmdir');
|
|
224
|
+
}
|
|
225
|
+
this.removeSync(path, true);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
public async mkdir(path: string, mode: number): Promise<void> {
|
|
229
|
+
await this.commitNew(path, S_IFDIR, mode, encodeUTF8('{}'));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
public mkdirSync(path: string, mode: number): void {
|
|
233
|
+
this.commitNewSync(path, S_IFDIR, mode, encodeUTF8('{}'));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
public async readdir(path: string): Promise<string[]> {
|
|
237
|
+
await using tx = this.store.transaction();
|
|
238
|
+
const node = await this.findINode(tx, path);
|
|
239
|
+
return Object.keys(await this.getDirListing(tx, node, path));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
public readdirSync(path: string): string[] {
|
|
243
|
+
using tx = this.store.transaction();
|
|
244
|
+
const node = this.findINodeSync(tx, path);
|
|
245
|
+
return Object.keys(this.getDirListingSync(tx, node, path));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Updated the inode and data node at `path`
|
|
250
|
+
* @todo Ensure mtime updates properly, and use that to determine if a data update is required.
|
|
251
|
+
*/
|
|
252
|
+
public async sync(path: string, data: Uint8Array, stats: Readonly<Stats>): Promise<void> {
|
|
253
|
+
await using tx = this.store.transaction();
|
|
254
|
+
// We use _findInode because we actually need the INode id.
|
|
255
|
+
const fileInodeId = await this._findINode(tx, dirname(path), basename(path)),
|
|
256
|
+
fileInode = await this.getINode(tx, fileInodeId, path),
|
|
257
|
+
inodeChanged = fileInode.update(stats);
|
|
258
|
+
|
|
259
|
+
// Sync data.
|
|
260
|
+
await tx.set(fileInode.ino, data);
|
|
261
|
+
// Sync metadata.
|
|
262
|
+
if (inodeChanged) {
|
|
263
|
+
await tx.set(fileInodeId, fileInode.data);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await tx.commit();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Updated the inode and data node at `path`
|
|
271
|
+
* @todo Ensure mtime updates properly, and use that to determine if a data update is required.
|
|
272
|
+
*/
|
|
273
|
+
public syncSync(path: string, data: Uint8Array, stats: Readonly<Stats>): void {
|
|
274
|
+
using tx = this.store.transaction();
|
|
275
|
+
// We use _findInode because we actually need the INode id.
|
|
276
|
+
const fileInodeId = this._findINodeSync(tx, dirname(path), basename(path)),
|
|
277
|
+
fileInode = this.getINodeSync(tx, fileInodeId, path),
|
|
278
|
+
inodeChanged = fileInode.update(stats);
|
|
279
|
+
|
|
280
|
+
// Sync data.
|
|
281
|
+
tx.setSync(fileInode.ino, data);
|
|
282
|
+
// Sync metadata.
|
|
283
|
+
if (inodeChanged) {
|
|
284
|
+
tx.setSync(fileInodeId, fileInode.data);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
tx.commitSync();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
public async link(target: string, link: string): Promise<void> {
|
|
291
|
+
await using tx = this.store.transaction();
|
|
292
|
+
|
|
293
|
+
const newDir: string = dirname(link),
|
|
294
|
+
newDirNode = await this.findINode(tx, newDir),
|
|
295
|
+
listing = await this.getDirListing(tx, newDirNode, newDir);
|
|
296
|
+
|
|
297
|
+
const ino = await this._findINode(tx, dirname(target), basename(target));
|
|
298
|
+
const node = await this.getINode(tx, ino, target);
|
|
299
|
+
|
|
300
|
+
node.nlink++;
|
|
301
|
+
listing[basename(link)] = ino;
|
|
302
|
+
|
|
303
|
+
tx.setSync(ino, node.data);
|
|
304
|
+
tx.setSync(newDirNode.ino, encodeDirListing(listing));
|
|
305
|
+
tx.commitSync();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
public linkSync(target: string, link: string): void {
|
|
309
|
+
using tx = this.store.transaction();
|
|
310
|
+
|
|
311
|
+
const newDir: string = dirname(link),
|
|
312
|
+
newDirNode = this.findINodeSync(tx, newDir),
|
|
313
|
+
listing = this.getDirListingSync(tx, newDirNode, newDir);
|
|
314
|
+
|
|
315
|
+
const ino = this._findINodeSync(tx, dirname(target), basename(target));
|
|
316
|
+
const node = this.getINodeSync(tx, ino, target);
|
|
317
|
+
|
|
318
|
+
node.nlink++;
|
|
319
|
+
listing[basename(link)] = ino;
|
|
320
|
+
|
|
321
|
+
tx.setSync(ino, node.data);
|
|
322
|
+
tx.setSync(newDirNode.ino, encodeDirListing(listing));
|
|
323
|
+
tx.commitSync();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Checks if the root directory exists. Creates it if it doesn't.
|
|
328
|
+
*/
|
|
329
|
+
public async checkRoot(): Promise<void> {
|
|
330
|
+
await using tx = this.store.transaction();
|
|
331
|
+
if (await tx.get(rootIno)) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// Create new inode. o777, owned by root:root
|
|
335
|
+
const inode = new Inode();
|
|
336
|
+
inode.mode = 0o777 | S_IFDIR;
|
|
337
|
+
// If the root doesn't exist, the first random ID shouldn't exist either.
|
|
338
|
+
await tx.set(inode.ino, encodeUTF8('{}'));
|
|
339
|
+
await tx.set(rootIno, inode.data);
|
|
340
|
+
await tx.commit();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Checks if the root directory exists. Creates it if it doesn't.
|
|
345
|
+
*/
|
|
346
|
+
public checkRootSync(): void {
|
|
347
|
+
using tx = this.store.transaction();
|
|
348
|
+
if (tx.getSync(rootIno)) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
// Create new inode, mode o777, owned by root:root
|
|
352
|
+
const inode = new Inode();
|
|
353
|
+
inode.mode = 0o777 | S_IFDIR;
|
|
354
|
+
// If the root doesn't exist, the first random ID shouldn't exist either.
|
|
355
|
+
tx.setSync(inode.ino, encodeUTF8('{}'));
|
|
356
|
+
tx.setSync(rootIno, inode.data);
|
|
357
|
+
tx.commitSync();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Helper function for findINode.
|
|
362
|
+
* @param parent The parent directory of the file we are attempting to find.
|
|
363
|
+
* @param filename The filename of the inode we are attempting to find, minus
|
|
364
|
+
* the parent.
|
|
365
|
+
*/
|
|
366
|
+
private async _findINode(tx: Transaction, parent: string, filename: string, visited: Set<string> = new Set()): Promise<Ino> {
|
|
367
|
+
const currentPath = join(parent, filename);
|
|
368
|
+
if (visited.has(currentPath)) {
|
|
369
|
+
throw new ErrnoError(Errno.EIO, 'Infinite loop detected while finding inode', currentPath);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
visited.add(currentPath);
|
|
373
|
+
|
|
374
|
+
if (parent == '/' && filename === '') {
|
|
375
|
+
return rootIno;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const inode = parent == '/' ? await this.getINode(tx, rootIno, parent) : await this.findINode(tx, parent, visited);
|
|
379
|
+
const dirList = await this.getDirListing(tx, inode, parent);
|
|
380
|
+
|
|
381
|
+
if (!(filename in dirList)) {
|
|
382
|
+
throw ErrnoError.With('ENOENT', resolve(parent, filename), '_findINode');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return dirList[filename];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Helper function for findINode.
|
|
390
|
+
* @param parent The parent directory of the file we are attempting to find.
|
|
391
|
+
* @param filename The filename of the inode we are attempting to find, minus
|
|
392
|
+
* the parent.
|
|
393
|
+
* @return string The ID of the file's inode in the file system.
|
|
394
|
+
*/
|
|
395
|
+
protected _findINodeSync(tx: Transaction, parent: string, filename: string, visited: Set<string> = new Set()): Ino {
|
|
396
|
+
const currentPath = join(parent, filename);
|
|
397
|
+
if (visited.has(currentPath)) {
|
|
398
|
+
throw new ErrnoError(Errno.EIO, 'Infinite loop detected while finding inode', currentPath);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
visited.add(currentPath);
|
|
402
|
+
|
|
403
|
+
if (parent == '/' && filename === '') {
|
|
404
|
+
return rootIno;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const inode = parent == '/' ? this.getINodeSync(tx, rootIno, parent) : this.findINodeSync(tx, parent, visited);
|
|
408
|
+
const dir = this.getDirListingSync(tx, inode, parent);
|
|
409
|
+
|
|
410
|
+
if (!(filename in dir)) {
|
|
411
|
+
throw ErrnoError.With('ENOENT', resolve(parent, filename), '_findINode');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return dir[filename];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Finds the Inode of `path`.
|
|
419
|
+
* @param path The path to look up.
|
|
420
|
+
* @todo memoize/cache
|
|
421
|
+
*/
|
|
422
|
+
private async findINode(tx: Transaction, path: string, visited: Set<string> = new Set()): Promise<Inode> {
|
|
423
|
+
const id = await this._findINode(tx, dirname(path), basename(path), visited);
|
|
424
|
+
return this.getINode(tx, id, path);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Finds the Inode of `path`.
|
|
429
|
+
* @param path The path to look up.
|
|
430
|
+
* @return The Inode of the path p.
|
|
431
|
+
* @todo memoize/cache
|
|
432
|
+
*/
|
|
433
|
+
protected findINodeSync(tx: Transaction, path: string, visited: Set<string> = new Set()): Inode {
|
|
434
|
+
const ino = this._findINodeSync(tx, dirname(path), basename(path), visited);
|
|
435
|
+
return this.getINodeSync(tx, ino, path);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Given the ID of a node, retrieves the corresponding Inode.
|
|
440
|
+
* @param tx The transaction to use.
|
|
441
|
+
* @param path The corresponding path to the file (used for error messages).
|
|
442
|
+
* @param id The ID to look up.
|
|
443
|
+
*/
|
|
444
|
+
private async getINode(tx: Transaction, id: Ino, path: string): Promise<Inode> {
|
|
445
|
+
const data = await tx.get(id);
|
|
446
|
+
if (!data) {
|
|
447
|
+
throw ErrnoError.With('ENOENT', path, 'getINode');
|
|
448
|
+
}
|
|
449
|
+
return new Inode(data.buffer);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Given the ID of a node, retrieves the corresponding Inode.
|
|
454
|
+
* @param tx The transaction to use.
|
|
455
|
+
* @param path The corresponding path to the file (used for error messages).
|
|
456
|
+
* @param id The ID to look up.
|
|
457
|
+
*/
|
|
458
|
+
protected getINodeSync(tx: Transaction, id: Ino, path: string): Inode {
|
|
459
|
+
const data = tx.getSync(id);
|
|
460
|
+
if (!data) {
|
|
461
|
+
throw ErrnoError.With('ENOENT', path, 'getINode');
|
|
462
|
+
}
|
|
463
|
+
const inode = new Inode(data.buffer);
|
|
464
|
+
return inode;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Given the Inode of a directory, retrieves the corresponding directory
|
|
469
|
+
* listing.
|
|
470
|
+
*/
|
|
471
|
+
private async getDirListing(tx: Transaction, inode: Inode, path: string): Promise<{ [fileName: string]: Ino }> {
|
|
472
|
+
const data = await tx.get(inode.ino);
|
|
473
|
+
/*
|
|
474
|
+
Occurs when data is undefined,or corresponds to something other than a directory listing.
|
|
475
|
+
The latter should never occur unless the file system is corrupted.
|
|
476
|
+
*/
|
|
477
|
+
if (!data) {
|
|
478
|
+
throw ErrnoError.With('ENOENT', path, 'getDirListing');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return decodeDirListing(data);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Given the Inode of a directory, retrieves the corresponding directory listing.
|
|
486
|
+
*/
|
|
487
|
+
protected getDirListingSync(tx: Transaction, inode: Inode, p?: string): { [fileName: string]: Ino } {
|
|
488
|
+
const data = tx.getSync(inode.ino);
|
|
489
|
+
if (!data) {
|
|
490
|
+
throw ErrnoError.With('ENOENT', p, 'getDirListing');
|
|
491
|
+
}
|
|
492
|
+
return decodeDirListing(data);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Adds a new node under a random ID. Retries before giving up in
|
|
497
|
+
* the exceedingly unlikely chance that we try to reuse a random ino.
|
|
498
|
+
*/
|
|
499
|
+
private async addNew(tx: Transaction, data: Uint8Array, path: string): Promise<Ino> {
|
|
500
|
+
for (let i = 0; i < maxInodeAllocTries; i++) {
|
|
501
|
+
const ino: Ino = randomIno();
|
|
502
|
+
if (await tx.get(ino)) {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
await tx.set(ino, data);
|
|
506
|
+
return ino;
|
|
507
|
+
}
|
|
508
|
+
throw new ErrnoError(Errno.ENOSPC, 'No inode IDs available', path, 'addNewNode');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Creates a new node under a random ID. Retries before giving up in
|
|
513
|
+
* the exceedingly unlikely chance that we try to reuse a random ino.
|
|
514
|
+
* @return The ino that the data was stored under.
|
|
515
|
+
*/
|
|
516
|
+
protected addNewSync(tx: Transaction, data: Uint8Array, path: string): Ino {
|
|
517
|
+
for (let i = 0; i < maxInodeAllocTries; i++) {
|
|
518
|
+
const ino: Ino = randomIno();
|
|
519
|
+
if (tx.getSync(ino)) {
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
tx.setSync(ino, data);
|
|
523
|
+
return ino;
|
|
524
|
+
}
|
|
525
|
+
throw new ErrnoError(Errno.ENOSPC, 'No inode IDs available', path, 'addNewNode');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Commits a new file (well, a FILE or a DIRECTORY) to the file system with `mode`.
|
|
530
|
+
* Note: This will commit the transaction.
|
|
531
|
+
* @param path The path to the new file.
|
|
532
|
+
* @param type The type of the new file.
|
|
533
|
+
* @param mode The mode to create the new file with.
|
|
534
|
+
* @param data The data to store at the file's data node.
|
|
535
|
+
*/
|
|
536
|
+
private async commitNew(path: string, type: FileType, mode: number, data: Uint8Array): Promise<Inode> {
|
|
537
|
+
await using tx = this.store.transaction();
|
|
538
|
+
const parentPath = dirname(path),
|
|
539
|
+
parent = await this.findINode(tx, parentPath);
|
|
540
|
+
|
|
541
|
+
const fname = basename(path),
|
|
542
|
+
listing = await this.getDirListing(tx, parent, parentPath);
|
|
543
|
+
|
|
544
|
+
/*
|
|
545
|
+
The root always exists.
|
|
546
|
+
If we don't check this prior to taking steps below,
|
|
547
|
+
we will create a file with name '' in root should path == '/'.
|
|
548
|
+
*/
|
|
549
|
+
if (path === '/') {
|
|
550
|
+
throw ErrnoError.With('EEXIST', path, 'commitNew');
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Check if file already exists.
|
|
554
|
+
if (listing[fname]) {
|
|
555
|
+
await tx.abort();
|
|
556
|
+
throw ErrnoError.With('EEXIST', path, 'commitNew');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Commit data.
|
|
560
|
+
const inode = new Inode();
|
|
561
|
+
inode.ino = await this.addNew(tx, data, path);
|
|
562
|
+
inode.mode = mode | type;
|
|
563
|
+
inode.uid = credentials.uid;
|
|
564
|
+
inode.gid = credentials.gid;
|
|
565
|
+
inode.size = data.length;
|
|
566
|
+
|
|
567
|
+
// Update and commit parent directory listing.
|
|
568
|
+
listing[fname] = await this.addNew(tx, inode.data, path);
|
|
569
|
+
await tx.set(parent.ino, encodeDirListing(listing));
|
|
570
|
+
await tx.commit();
|
|
571
|
+
return inode;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Commits a new file (well, a FILE or a DIRECTORY) to the file system with `mode`.
|
|
576
|
+
* Note: This will commit the transaction.
|
|
577
|
+
* @param path The path to the new file.
|
|
578
|
+
* @param type The type of the new file.
|
|
579
|
+
* @param mode The mode to create the new file with.
|
|
580
|
+
* @param data The data to store at the file's data node.
|
|
581
|
+
* @return The Inode for the new file.
|
|
582
|
+
*/
|
|
583
|
+
protected commitNewSync(path: string, type: FileType, mode: number, data: Uint8Array = new Uint8Array()): Inode {
|
|
584
|
+
using tx = this.store.transaction();
|
|
585
|
+
const parentPath = dirname(path),
|
|
586
|
+
parent = this.findINodeSync(tx, parentPath);
|
|
587
|
+
|
|
588
|
+
const fname = basename(path),
|
|
589
|
+
listing = this.getDirListingSync(tx, parent, parentPath);
|
|
590
|
+
|
|
591
|
+
/*
|
|
592
|
+
The root always exists.
|
|
593
|
+
If we don't check this prior to taking steps below,
|
|
594
|
+
we will create a file with name '' in root should p == '/'.
|
|
595
|
+
*/
|
|
596
|
+
if (path === '/') {
|
|
597
|
+
throw ErrnoError.With('EEXIST', path, 'commitNew');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Check if file already exists.
|
|
601
|
+
if (listing[fname]) {
|
|
602
|
+
throw ErrnoError.With('EEXIST', path, 'commitNew');
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Commit data.
|
|
606
|
+
const node = new Inode();
|
|
607
|
+
node.ino = this.addNewSync(tx, data, path);
|
|
608
|
+
node.size = data.length;
|
|
609
|
+
node.mode = mode | type;
|
|
610
|
+
node.uid = credentials.uid;
|
|
611
|
+
node.gid = credentials.gid;
|
|
612
|
+
// Update and commit parent directory listing.
|
|
613
|
+
listing[fname] = this.addNewSync(tx, node.data, path);
|
|
614
|
+
tx.setSync(parent.ino, encodeDirListing(listing));
|
|
615
|
+
tx.commitSync();
|
|
616
|
+
return node;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Remove all traces of `path` from the file system.
|
|
621
|
+
* @param path The path to remove from the file system.
|
|
622
|
+
* @param isDir Does the path belong to a directory, or a file?
|
|
623
|
+
* @todo Update mtime.
|
|
624
|
+
*/
|
|
625
|
+
private async remove(path: string, isDir: boolean): Promise<void> {
|
|
626
|
+
await using tx = this.store.transaction();
|
|
627
|
+
const parent: string = dirname(path),
|
|
628
|
+
parentNode = await this.findINode(tx, parent),
|
|
629
|
+
listing = await this.getDirListing(tx, parentNode, parent),
|
|
630
|
+
fileName: string = basename(path);
|
|
631
|
+
|
|
632
|
+
if (!listing[fileName]) {
|
|
633
|
+
throw ErrnoError.With('ENOENT', path, 'remove');
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const fileIno = listing[fileName];
|
|
637
|
+
|
|
638
|
+
// Get file inode.
|
|
639
|
+
const fileNode = await this.getINode(tx, fileIno, path);
|
|
640
|
+
|
|
641
|
+
// Remove from directory listing of parent.
|
|
642
|
+
delete listing[fileName];
|
|
643
|
+
|
|
644
|
+
if (!isDir && fileNode.toStats().isDirectory()) {
|
|
645
|
+
throw ErrnoError.With('EISDIR', path, 'remove');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
await tx.set(parentNode.ino, encodeDirListing(listing));
|
|
649
|
+
|
|
650
|
+
if (--fileNode.nlink < 1) {
|
|
651
|
+
// remove file
|
|
652
|
+
await tx.remove(fileNode.ino);
|
|
653
|
+
await tx.remove(fileIno);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Success.
|
|
657
|
+
await tx.commit();
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Remove all traces of `path` from the file system.
|
|
662
|
+
* @param path The path to remove from the file system.
|
|
663
|
+
* @param isDir Does the path belong to a directory, or a file?
|
|
664
|
+
* @todo Update mtime.
|
|
665
|
+
*/
|
|
666
|
+
protected removeSync(path: string, isDir: boolean): void {
|
|
667
|
+
using tx = this.store.transaction();
|
|
668
|
+
const parent: string = dirname(path),
|
|
669
|
+
parentNode = this.findINodeSync(tx, parent),
|
|
670
|
+
listing = this.getDirListingSync(tx, parentNode, parent),
|
|
671
|
+
fileName: string = basename(path),
|
|
672
|
+
fileIno: Ino = listing[fileName];
|
|
673
|
+
|
|
674
|
+
if (!fileIno) {
|
|
675
|
+
throw ErrnoError.With('ENOENT', path, 'remove');
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Get file inode.
|
|
679
|
+
const fileNode = this.getINodeSync(tx, fileIno, path);
|
|
680
|
+
|
|
681
|
+
// Remove from directory listing of parent.
|
|
682
|
+
delete listing[fileName];
|
|
683
|
+
|
|
684
|
+
if (!isDir && fileNode.toStats().isDirectory()) {
|
|
685
|
+
throw ErrnoError.With('EISDIR', path, 'remove');
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Update directory listing.
|
|
689
|
+
tx.setSync(parentNode.ino, encodeDirListing(listing));
|
|
690
|
+
|
|
691
|
+
if (--fileNode.nlink < 1) {
|
|
692
|
+
// remove file
|
|
693
|
+
tx.removeSync(fileNode.ino);
|
|
694
|
+
tx.removeSync(fileIno);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Success.
|
|
698
|
+
tx.commitSync();
|
|
699
|
+
}
|
|
700
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Store
|
|
2
|
+
|
|
3
|
+
While `StoreFS`, `Store`, etc. don't provide any backends directly, they are invaluable for creating new backends with a minimal amount of code.
|
|
4
|
+
|
|
5
|
+
`StoreFS` implements the all of `FileSystem` using a `Store`.
|
|
6
|
+
|
|
7
|
+
`Store` and `Transaction` are simple interfaces which are used by `StoreFS`.
|
|
8
|
+
|
|
9
|
+
In [simple.ts](./simple.ts) you can find `SimpleSyncStore`, `SimpleAsyncStore`, and `SimpleTransaction`. These classes provide an even more simple interface. This means backends like `InMemory` can be implemented with a very small amount of code.
|