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