@zenfs/core 0.9.7 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/backends/Index.d.ts +3 -3
  2. package/dist/backends/Index.js +23 -23
  3. package/dist/backends/backend.js +6 -5
  4. package/dist/backends/fetch.d.ts +84 -0
  5. package/dist/backends/fetch.js +170 -0
  6. package/dist/backends/{Locked.d.ts → locked.d.ts} +13 -13
  7. package/dist/backends/{Locked.js → locked.js} +54 -54
  8. package/dist/backends/{InMemory.d.ts → memory.d.ts} +7 -9
  9. package/dist/backends/memory.js +38 -0
  10. package/dist/backends/{Overlay.d.ts → overlay.d.ts} +12 -13
  11. package/dist/backends/{Overlay.js → overlay.js} +105 -110
  12. package/dist/backends/port/fs.d.ts +123 -0
  13. package/dist/backends/port/fs.js +239 -0
  14. package/dist/backends/port/rpc.d.ts +60 -0
  15. package/dist/backends/port/rpc.js +71 -0
  16. package/dist/backends/store/fs.d.ts +169 -0
  17. package/dist/backends/store/fs.js +743 -0
  18. package/dist/backends/store/simple.d.ts +64 -0
  19. package/dist/backends/store/simple.js +111 -0
  20. package/dist/backends/store/store.d.ts +111 -0
  21. package/dist/backends/store/store.js +62 -0
  22. package/dist/browser.min.js +4 -4
  23. package/dist/browser.min.js.map +4 -4
  24. package/dist/config.d.ts +8 -10
  25. package/dist/config.js +11 -11
  26. package/dist/emulation/async.js +6 -6
  27. package/dist/emulation/dir.js +2 -2
  28. package/dist/emulation/index.d.ts +1 -1
  29. package/dist/emulation/index.js +1 -1
  30. package/dist/emulation/path.d.ts +3 -2
  31. package/dist/emulation/path.js +19 -45
  32. package/dist/emulation/promises.d.ts +7 -12
  33. package/dist/emulation/promises.js +144 -146
  34. package/dist/emulation/shared.d.ts +5 -10
  35. package/dist/emulation/shared.js +9 -9
  36. package/dist/emulation/streams.js +3 -3
  37. package/dist/emulation/sync.js +25 -25
  38. package/dist/{ApiError.d.ts → error.d.ts} +13 -15
  39. package/dist/error.js +291 -0
  40. package/dist/file.d.ts +2 -0
  41. package/dist/file.js +11 -5
  42. package/dist/filesystem.d.ts +3 -3
  43. package/dist/filesystem.js +41 -44
  44. package/dist/index.d.ts +7 -6
  45. package/dist/index.js +7 -6
  46. package/dist/inode.d.ts +1 -1
  47. package/dist/mutex.js +2 -1
  48. package/dist/utils.d.ts +8 -7
  49. package/dist/utils.js +11 -12
  50. package/package.json +3 -3
  51. package/readme.md +17 -9
  52. package/src/backends/Index.ts +23 -23
  53. package/src/backends/backend.ts +8 -7
  54. package/src/backends/fetch.ts +229 -0
  55. package/src/backends/{Locked.ts → locked.ts} +55 -55
  56. package/src/backends/memory.ts +44 -0
  57. package/src/backends/{Overlay.ts → overlay.ts} +108 -114
  58. package/src/backends/port/fs.ts +306 -0
  59. package/src/backends/port/readme.md +59 -0
  60. package/src/backends/port/rpc.ts +144 -0
  61. package/src/backends/store/fs.ts +881 -0
  62. package/src/backends/store/readme.md +9 -0
  63. package/src/backends/store/simple.ts +144 -0
  64. package/src/backends/store/store.ts +164 -0
  65. package/src/config.ts +21 -25
  66. package/src/emulation/async.ts +6 -6
  67. package/src/emulation/dir.ts +2 -2
  68. package/src/emulation/index.ts +1 -1
  69. package/src/emulation/path.ts +25 -49
  70. package/src/emulation/promises.ts +150 -159
  71. package/src/emulation/shared.ts +13 -15
  72. package/src/emulation/streams.ts +3 -3
  73. package/src/emulation/sync.ts +28 -28
  74. package/src/{ApiError.ts → error.ts} +89 -90
  75. package/src/file.ts +13 -5
  76. package/src/filesystem.ts +44 -47
  77. package/src/index.ts +7 -6
  78. package/src/inode.ts +1 -1
  79. package/src/mutex.ts +3 -1
  80. package/src/utils.ts +16 -18
  81. package/tsconfig.json +2 -2
  82. package/dist/ApiError.js +0 -292
  83. package/dist/backends/AsyncStore.d.ts +0 -204
  84. package/dist/backends/AsyncStore.js +0 -509
  85. package/dist/backends/InMemory.js +0 -49
  86. package/dist/backends/SyncStore.d.ts +0 -213
  87. package/dist/backends/SyncStore.js +0 -445
  88. package/src/backends/AsyncStore.ts +0 -655
  89. package/src/backends/InMemory.ts +0 -56
  90. package/src/backends/SyncStore.ts +0 -589
@@ -0,0 +1,881 @@
1
+ import type { Cred } from '../../cred.js';
2
+ import { W_OK, R_OK } from '../../emulation/constants.js';
3
+ import { dirname, basename, join, resolve } from '../../emulation/path.js';
4
+ import { ErrnoError, Errno } from '../../error.js';
5
+ import { PreloadFile, flagToMode } from '../../file.js';
6
+ import { FileSystem, type FileSystemMetadata } from '../../filesystem.js';
7
+ import { type Ino, Inode, rootIno, randomIno } from '../../inode.js';
8
+ import { type Stats, FileType } from '../../stats.js';
9
+ import { encodeDirListing, encode, decodeDirListing } from '../../utils.js';
10
+ import type { Store, Transaction } from './store.js';
11
+
12
+ const maxInodeAllocTries = 5;
13
+
14
+ /**
15
+ * A synchronous key-value file system. Uses a SyncStore to store the data.
16
+ *
17
+ * We use a unique ID for each node in the file system. The root node has a fixed ID.
18
+ * @todo Introduce Node ID caching.
19
+ * @todo Check modes.
20
+ * @internal
21
+ */
22
+ export class StoreFS extends FileSystem {
23
+ protected get store(): Store {
24
+ if (!this._store) {
25
+ throw new ErrnoError(Errno.ENODATA, 'No store attached');
26
+ }
27
+ return this._store;
28
+ }
29
+
30
+ protected _store?: Store;
31
+
32
+ private _initialized: boolean = false;
33
+
34
+ public async ready(): Promise<void> {
35
+ await super.ready();
36
+ if (this._initialized) {
37
+ return;
38
+ }
39
+ this._initialized = true;
40
+ this._store = await this.$store;
41
+ }
42
+
43
+ constructor(private $store: Store | Promise<Store>) {
44
+ super();
45
+
46
+ if (!($store instanceof Promise)) {
47
+ this._store = $store;
48
+ this._initialized = true;
49
+ this.makeRootDirectorySync();
50
+ }
51
+ }
52
+
53
+ public metadata(): FileSystemMetadata {
54
+ return {
55
+ ...super.metadata(),
56
+ name: this.store.name,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Delete all contents stored in the file system.
62
+ */
63
+ public async empty(): Promise<void> {
64
+ await this.store.clear();
65
+ // Root always exists.
66
+ await this.makeRootDirectory();
67
+ }
68
+
69
+ /**
70
+ * Delete all contents stored in the file system.
71
+ */
72
+ public emptySync(): void {
73
+ this.store.clearSync();
74
+ // Root always exists.
75
+ this.makeRootDirectorySync();
76
+ }
77
+
78
+ /**
79
+ * @todo Make rename compatible with the cache.
80
+ */
81
+ public async rename(oldPath: string, newPath: string, cred: Cred): Promise<void> {
82
+ const tx = this.store.transaction(),
83
+ oldParent = dirname(oldPath),
84
+ oldName = basename(oldPath),
85
+ newParent = dirname(newPath),
86
+ newName = basename(newPath),
87
+ // Remove oldPath from parent's directory listing.
88
+ oldDirNode = await this.findINode(tx, oldParent),
89
+ oldDirList = await this.getDirListing(tx, oldDirNode, oldParent);
90
+
91
+ if (!oldDirNode.toStats().hasAccess(W_OK, cred)) {
92
+ throw ErrnoError.With('EACCES', oldPath, 'rename');
93
+ }
94
+
95
+ if (!oldDirList[oldName]) {
96
+ throw ErrnoError.With('ENOENT', oldPath, 'rename');
97
+ }
98
+ const nodeId: Ino = oldDirList[oldName];
99
+ delete oldDirList[oldName];
100
+
101
+ // Invariant: Can't move a folder inside itself.
102
+ // This funny little hack ensures that the check passes only if oldPath
103
+ // is a subpath of newParent. We append '/' to avoid matching folders that
104
+ // are a substring of the bottom-most folder in the path.
105
+ if ((newParent + '/').indexOf(oldPath + '/') === 0) {
106
+ throw new ErrnoError(Errno.EBUSY, oldParent);
107
+ }
108
+
109
+ // Add newPath to parent's directory listing.
110
+ let newDirNode: Inode, newDirList: typeof oldDirList;
111
+ if (newParent === oldParent) {
112
+ // Prevent us from re-grabbing the same directory listing, which still
113
+ // contains oldName.
114
+ newDirNode = oldDirNode;
115
+ newDirList = oldDirList;
116
+ } else {
117
+ newDirNode = await this.findINode(tx, newParent);
118
+ newDirList = await this.getDirListing(tx, newDirNode, newParent);
119
+ }
120
+
121
+ if (newDirList[newName]) {
122
+ // If it's a file, delete it.
123
+ const newNameNode = await this.getINode(tx, newDirList[newName], newPath);
124
+ if (newNameNode.toStats().isFile()) {
125
+ try {
126
+ await tx.remove(newNameNode.ino);
127
+ await tx.remove(newDirList[newName]);
128
+ } catch (e) {
129
+ await tx.abort();
130
+ throw e;
131
+ }
132
+ } else {
133
+ // If it's a directory, throw a permissions error.
134
+ throw ErrnoError.With('EPERM', newPath, 'rename');
135
+ }
136
+ }
137
+ newDirList[newName] = nodeId;
138
+ // Commit the two changed directory listings.
139
+ try {
140
+ await tx.set(oldDirNode.ino, encodeDirListing(oldDirList));
141
+ await tx.set(newDirNode.ino, encodeDirListing(newDirList));
142
+ } catch (e) {
143
+ await tx.abort();
144
+ throw e;
145
+ }
146
+
147
+ await tx.commit();
148
+ }
149
+
150
+ public renameSync(oldPath: string, newPath: string, cred: Cred): void {
151
+ const tx = this.store.transaction(),
152
+ oldParent = dirname(oldPath),
153
+ oldName = basename(oldPath),
154
+ newParent = dirname(newPath),
155
+ newName = basename(newPath),
156
+ // Remove oldPath from parent's directory listing.
157
+ oldDirNode = this.findINodeSync(tx, oldParent),
158
+ oldDirList = this.getDirListingSync(tx, oldDirNode, oldParent);
159
+
160
+ if (!oldDirNode.toStats().hasAccess(W_OK, cred)) {
161
+ throw ErrnoError.With('EACCES', oldPath, 'rename');
162
+ }
163
+
164
+ if (!oldDirList[oldName]) {
165
+ throw ErrnoError.With('ENOENT', oldPath, 'rename');
166
+ }
167
+ const ino: Ino = oldDirList[oldName];
168
+ delete oldDirList[oldName];
169
+
170
+ // Invariant: Can't move a folder inside itself.
171
+ // This funny little hack ensures that the check passes only if oldPath
172
+ // is a subpath of newParent. We append '/' to avoid matching folders that
173
+ // are a substring of the bottom-most folder in the path.
174
+ if ((newParent + '/').indexOf(oldPath + '/') == 0) {
175
+ throw new ErrnoError(Errno.EBUSY, oldParent);
176
+ }
177
+
178
+ // Add newPath to parent's directory listing.
179
+ let newDirNode: Inode, newDirList: typeof oldDirList;
180
+ if (newParent === oldParent) {
181
+ // Prevent us from re-grabbing the same directory listing, which still
182
+ // contains oldName.
183
+ newDirNode = oldDirNode;
184
+ newDirList = oldDirList;
185
+ } else {
186
+ newDirNode = this.findINodeSync(tx, newParent);
187
+ newDirList = this.getDirListingSync(tx, newDirNode, newParent);
188
+ }
189
+
190
+ if (newDirList[newName]) {
191
+ // If it's a file, delete it.
192
+ const newNameNode = this.getINodeSync(tx, newDirList[newName], newPath);
193
+ if (newNameNode.toStats().isFile()) {
194
+ try {
195
+ tx.removeSync(newNameNode.ino);
196
+ tx.removeSync(newDirList[newName]);
197
+ } catch (e) {
198
+ tx.abortSync();
199
+ throw e;
200
+ }
201
+ } else {
202
+ // If it's a directory, throw a permissions error.
203
+ throw ErrnoError.With('EPERM', newPath, 'rename');
204
+ }
205
+ }
206
+ newDirList[newName] = ino;
207
+
208
+ // Commit the two changed directory listings.
209
+ try {
210
+ tx.setSync(oldDirNode.ino, encodeDirListing(oldDirList));
211
+ tx.setSync(newDirNode.ino, encodeDirListing(newDirList));
212
+ } catch (e) {
213
+ tx.abortSync();
214
+ throw e;
215
+ }
216
+
217
+ tx.commitSync();
218
+ }
219
+
220
+ public async stat(path: string, cred: Cred): Promise<Stats> {
221
+ const tx = this.store.transaction();
222
+ const inode = await this.findINode(tx, path);
223
+ if (!inode) {
224
+ throw ErrnoError.With('ENOENT', path, 'stat');
225
+ }
226
+ const stats = inode.toStats();
227
+ if (!stats.hasAccess(R_OK, cred)) {
228
+ throw ErrnoError.With('EACCES', path, 'stat');
229
+ }
230
+ return stats;
231
+ }
232
+
233
+ public statSync(path: string, cred: Cred): Stats {
234
+ // Get the inode to the item, convert it into a Stats object.
235
+ const stats = this.findINodeSync(this.store.transaction(), path).toStats();
236
+ if (!stats.hasAccess(R_OK, cred)) {
237
+ throw ErrnoError.With('EACCES', path, 'stat');
238
+ }
239
+ return stats;
240
+ }
241
+
242
+ public async createFile(path: string, flag: string, mode: number, cred: Cred): Promise<PreloadFile<this>> {
243
+ const data = new Uint8Array(0);
244
+ const file = await this.commitNew(this.store.transaction(), path, FileType.FILE, mode, cred, data);
245
+ return new PreloadFile(this, path, flag, file.toStats(), data);
246
+ }
247
+
248
+ public createFileSync(path: string, flag: string, mode: number, cred: Cred): PreloadFile<this> {
249
+ this.commitNewSync(path, FileType.FILE, mode, cred);
250
+ return this.openFileSync(path, flag, cred);
251
+ }
252
+
253
+ public async openFile(path: string, flag: string, cred: Cred): Promise<PreloadFile<this>> {
254
+ const tx = this.store.transaction(),
255
+ node = await this.findINode(tx, path),
256
+ data = await tx.get(node.ino);
257
+ if (!node.toStats().hasAccess(flagToMode(flag), cred)) {
258
+ throw ErrnoError.With('EACCES', path, 'openFile');
259
+ }
260
+ if (!data) {
261
+ throw ErrnoError.With('ENOENT', path, 'openFile');
262
+ }
263
+ return new PreloadFile(this, path, flag, node.toStats(), data);
264
+ }
265
+
266
+ public openFileSync(path: string, flag: string, cred: Cred): PreloadFile<this> {
267
+ const tx = this.store.transaction(),
268
+ node = this.findINodeSync(tx, path),
269
+ data = tx.getSync(node.ino);
270
+ if (!node.toStats().hasAccess(flagToMode(flag), cred)) {
271
+ throw ErrnoError.With('EACCES', path, 'openFile');
272
+ }
273
+ if (!data) {
274
+ throw ErrnoError.With('ENOENT', path, 'openFile');
275
+ }
276
+ return new PreloadFile(this, path, flag, node.toStats(), data);
277
+ }
278
+
279
+ public async unlink(path: string, cred: Cred): Promise<void> {
280
+ return this.remove(path, false, cred);
281
+ }
282
+
283
+ public unlinkSync(path: string, cred: Cred): void {
284
+ this.removeSync(path, false, cred);
285
+ }
286
+
287
+ public async rmdir(path: string, cred: Cred): Promise<void> {
288
+ // Check first if directory is empty.
289
+ const list = await this.readdir(path, cred);
290
+ if (list.length > 0) {
291
+ throw ErrnoError.With('ENOTEMPTY', path, 'rmdir');
292
+ }
293
+ await this.remove(path, true, cred);
294
+ }
295
+
296
+ public rmdirSync(path: string, cred: Cred): void {
297
+ // Check first if directory is empty.
298
+ if (this.readdirSync(path, cred).length > 0) {
299
+ throw ErrnoError.With('ENOTEMPTY', path, 'rmdir');
300
+ } else {
301
+ this.removeSync(path, true, cred);
302
+ }
303
+ }
304
+
305
+ public async mkdir(path: string, mode: number, cred: Cred): Promise<void> {
306
+ const tx = this.store.transaction(),
307
+ data = encode('{}');
308
+ await this.commitNew(tx, path, FileType.DIRECTORY, mode, cred, data);
309
+ }
310
+
311
+ public mkdirSync(path: string, mode: number, cred: Cred): void {
312
+ this.commitNewSync(path, FileType.DIRECTORY, mode, cred, encode('{}'));
313
+ }
314
+
315
+ public async readdir(path: string, cred: Cred): Promise<string[]> {
316
+ const tx = this.store.transaction();
317
+ const node = await this.findINode(tx, path);
318
+ if (!node.toStats().hasAccess(R_OK, cred)) {
319
+ throw ErrnoError.With('EACCES', path, 'readdur');
320
+ }
321
+ return Object.keys(await this.getDirListing(tx, node, path));
322
+ }
323
+
324
+ public readdirSync(path: string, cred: Cred): string[] {
325
+ const tx = this.store.transaction();
326
+ const node = this.findINodeSync(tx, path);
327
+ if (!node.toStats().hasAccess(R_OK, cred)) {
328
+ throw ErrnoError.With('EACCES', path, 'readdir');
329
+ }
330
+ return Object.keys(this.getDirListingSync(tx, node, path));
331
+ }
332
+
333
+ /**
334
+ * Updated the inode and data node at the given path
335
+ * @todo Ensure mtime updates properly, and use that to determine if a data update is required.
336
+ */
337
+ public async sync(path: string, data: Uint8Array, stats: Readonly<Stats>): Promise<void> {
338
+ const tx = this.store.transaction(),
339
+ // We use _findInode because we actually need the INode id.
340
+ fileInodeId = await this._findINode(tx, dirname(path), basename(path)),
341
+ fileInode = await this.getINode(tx, fileInodeId, path),
342
+ inodeChanged = fileInode.update(stats);
343
+
344
+ try {
345
+ // Sync data.
346
+ await tx.set(fileInode.ino, data);
347
+ // Sync metadata.
348
+ if (inodeChanged) {
349
+ await tx.set(fileInodeId, fileInode.data);
350
+ }
351
+ } catch (e) {
352
+ await tx.abort();
353
+ throw e;
354
+ }
355
+ await tx.commit();
356
+ }
357
+
358
+ /**
359
+ * Updated the inode and data node at the given path
360
+ * @todo Ensure mtime updates properly, and use that to determine if a data update is required.
361
+ */
362
+ public syncSync(path: string, data: Uint8Array, stats: Readonly<Stats>): void {
363
+ const tx = this.store.transaction(),
364
+ // We use _findInode because we actually need the INode id.
365
+ fileInodeId = this._findINodeSync(tx, dirname(path), basename(path)),
366
+ fileInode = this.getINodeSync(tx, fileInodeId, path),
367
+ inodeChanged = fileInode.update(stats);
368
+
369
+ try {
370
+ // Sync data.
371
+ tx.setSync(fileInode.ino, data);
372
+ // Sync metadata.
373
+ if (inodeChanged) {
374
+ tx.setSync(fileInodeId, fileInode.data);
375
+ }
376
+ } catch (e) {
377
+ tx.abortSync();
378
+ throw e;
379
+ }
380
+ tx.commitSync();
381
+ }
382
+
383
+ public async link(existing: string, newpath: string, cred: Cred): Promise<void> {
384
+ const tx = this.store.transaction(),
385
+ existingDir: string = dirname(existing),
386
+ existingDirNode = await this.findINode(tx, existingDir);
387
+
388
+ if (!existingDirNode.toStats().hasAccess(R_OK, cred)) {
389
+ throw ErrnoError.With('EACCES', existingDir, 'link');
390
+ }
391
+
392
+ const newDir: string = dirname(newpath),
393
+ newDirNode = await this.findINode(tx, newDir),
394
+ newListing = await this.getDirListing(tx, newDirNode, newDir);
395
+
396
+ if (!newDirNode.toStats().hasAccess(W_OK, cred)) {
397
+ throw ErrnoError.With('EACCES', newDir, 'link');
398
+ }
399
+
400
+ const ino = await this._findINode(tx, existingDir, basename(existing));
401
+ const node = await this.getINode(tx, ino, existing);
402
+
403
+ if (!node.toStats().hasAccess(W_OK, cred)) {
404
+ throw ErrnoError.With('EACCES', newpath, 'link');
405
+ }
406
+
407
+ node.nlink++;
408
+ newListing[basename(newpath)] = ino;
409
+ try {
410
+ tx.setSync(ino, node.data);
411
+ tx.setSync(newDirNode.ino, encodeDirListing(newListing));
412
+ } catch (e) {
413
+ tx.abortSync();
414
+ throw e;
415
+ }
416
+ tx.commitSync();
417
+ }
418
+
419
+ public linkSync(existing: string, newpath: string, cred: Cred): void {
420
+ const tx = this.store.transaction(),
421
+ existingDir: string = dirname(existing),
422
+ existingDirNode = this.findINodeSync(tx, existingDir);
423
+
424
+ if (!existingDirNode.toStats().hasAccess(R_OK, cred)) {
425
+ throw ErrnoError.With('EACCES', existingDir, 'link');
426
+ }
427
+
428
+ const newDir: string = dirname(newpath),
429
+ newDirNode = this.findINodeSync(tx, newDir),
430
+ newListing = this.getDirListingSync(tx, newDirNode, newDir);
431
+
432
+ if (!newDirNode.toStats().hasAccess(W_OK, cred)) {
433
+ throw ErrnoError.With('EACCES', newDir, 'link');
434
+ }
435
+
436
+ const ino = this._findINodeSync(tx, existingDir, basename(existing));
437
+ const node = this.getINodeSync(tx, ino, existing);
438
+
439
+ if (!node.toStats().hasAccess(W_OK, cred)) {
440
+ throw ErrnoError.With('EACCES', newpath, 'link');
441
+ }
442
+ node.nlink++;
443
+ newListing[basename(newpath)] = ino;
444
+ try {
445
+ tx.setSync(ino, node.data);
446
+ tx.setSync(newDirNode.ino, encodeDirListing(newListing));
447
+ } catch (e) {
448
+ tx.abortSync();
449
+ throw e;
450
+ }
451
+ tx.commitSync();
452
+ }
453
+
454
+ /**
455
+ * Checks if the root directory exists. Creates it if it doesn't.
456
+ */
457
+ private async makeRootDirectory(): Promise<void> {
458
+ const tx = this.store.transaction();
459
+ if (await tx.get(rootIno)) {
460
+ return;
461
+ }
462
+ // Create new inode. o777, owned by root:root
463
+ const inode = new Inode();
464
+ inode.mode = 0o777 | FileType.DIRECTORY;
465
+ // If the root doesn't exist, the first random ID shouldn't exist either.
466
+ await tx.set(inode.ino, encode('{}'));
467
+ await tx.set(rootIno, inode.data);
468
+ await tx.commit();
469
+ }
470
+
471
+ /**
472
+ * Checks if the root directory exists. Creates it if it doesn't.
473
+ */
474
+ protected makeRootDirectorySync(): void {
475
+ const tx = this.store.transaction();
476
+ if (tx.getSync(rootIno)) {
477
+ return;
478
+ }
479
+ // Create new inode, mode o777, owned by root:root
480
+ const inode = new Inode();
481
+ inode.mode = 0o777 | FileType.DIRECTORY;
482
+ // If the root doesn't exist, the first random ID shouldn't exist either.
483
+ tx.setSync(inode.ino, encode('{}'));
484
+ tx.setSync(rootIno, inode.data);
485
+ tx.commitSync();
486
+ }
487
+
488
+ /**
489
+ * Helper function for findINode.
490
+ * @param parent The parent directory of the file we are attempting to find.
491
+ * @param filename The filename of the inode we are attempting to find, minus
492
+ * the parent.
493
+ */
494
+ private async _findINode(tx: Transaction, parent: string, filename: string, visited: Set<string> = new Set()): Promise<Ino> {
495
+ const currentPath = join(parent, filename);
496
+ if (visited.has(currentPath)) {
497
+ throw new ErrnoError(Errno.EIO, 'Infinite loop detected while finding inode', currentPath);
498
+ }
499
+
500
+ visited.add(currentPath);
501
+
502
+ if (parent == '/' && filename === '') {
503
+ return rootIno;
504
+ }
505
+
506
+ const inode = parent == '/' ? await this.getINode(tx, rootIno, parent) : await this.findINode(tx, parent, visited);
507
+ const dirList = await this.getDirListing(tx, inode, parent);
508
+
509
+ if (!(filename in dirList)) {
510
+ throw ErrnoError.With('ENOENT', resolve(parent, filename), '_findINode');
511
+ }
512
+
513
+ return dirList[filename];
514
+ }
515
+
516
+ /**
517
+ * Helper function for findINode.
518
+ * @param parent The parent directory of the file we are attempting to find.
519
+ * @param filename The filename of the inode we are attempting to find, minus
520
+ * the parent.
521
+ * @return string The ID of the file's inode in the file system.
522
+ */
523
+ protected _findINodeSync(tx: Transaction, parent: string, filename: string, visited: Set<string> = new Set()): Ino {
524
+ const currentPath = join(parent, filename);
525
+ if (visited.has(currentPath)) {
526
+ throw new ErrnoError(Errno.EIO, 'Infinite loop detected while finding inode', currentPath);
527
+ }
528
+
529
+ visited.add(currentPath);
530
+
531
+ if (parent == '/' && filename === '') {
532
+ return rootIno;
533
+ }
534
+
535
+ const inode = parent == '/' ? this.getINodeSync(tx, rootIno, parent) : this.findINodeSync(tx, parent, visited);
536
+ const dir = this.getDirListingSync(tx, inode, parent);
537
+
538
+ if (!(filename in dir)) {
539
+ throw ErrnoError.With('ENOENT', resolve(parent, filename), '_findINode');
540
+ }
541
+
542
+ return dir[filename];
543
+ }
544
+
545
+ /**
546
+ * Finds the Inode of the given path.
547
+ * @param path The path to look up.
548
+ * @todo memoize/cache
549
+ */
550
+ private async findINode(tx: Transaction, path: string, visited: Set<string> = new Set()): Promise<Inode> {
551
+ const id = await this._findINode(tx, dirname(path), basename(path), visited);
552
+ return this.getINode(tx, id!, path);
553
+ }
554
+
555
+ /**
556
+ * Finds the Inode of the given path.
557
+ * @param path The path to look up.
558
+ * @return The Inode of the path p.
559
+ * @todo memoize/cache
560
+ */
561
+ protected findINodeSync(tx: Transaction, path: string, visited: Set<string> = new Set()): Inode {
562
+ const ino = this._findINodeSync(tx, dirname(path), basename(path), visited);
563
+ return this.getINodeSync(tx, ino, path);
564
+ }
565
+
566
+ /**
567
+ * Given the ID of a node, retrieves the corresponding Inode.
568
+ * @param tx The transaction to use.
569
+ * @param path The corresponding path to the file (used for error messages).
570
+ * @param id The ID to look up.
571
+ */
572
+ private async getINode(tx: Transaction, id: Ino, path: string): Promise<Inode> {
573
+ const data = await tx.get(id);
574
+ if (!data) {
575
+ throw ErrnoError.With('ENOENT', path, 'getINode');
576
+ }
577
+ return new Inode(data.buffer);
578
+ }
579
+
580
+ /**
581
+ * Given the ID of a node, retrieves the corresponding Inode.
582
+ * @param tx The transaction to use.
583
+ * @param path The corresponding path to the file (used for error messages).
584
+ * @param id The ID to look up.
585
+ */
586
+ protected getINodeSync(tx: Transaction, id: Ino, path: string): Inode {
587
+ const data = tx.getSync(id);
588
+ if (!data) {
589
+ throw ErrnoError.With('ENOENT', path, 'getINode');
590
+ }
591
+ const inode = new Inode(data.buffer);
592
+ return inode;
593
+ }
594
+
595
+ /**
596
+ * Given the Inode of a directory, retrieves the corresponding directory
597
+ * listing.
598
+ */
599
+ private async getDirListing(tx: Transaction, inode: Inode, path: string): Promise<{ [fileName: string]: Ino }> {
600
+ if (!inode.toStats().isDirectory()) {
601
+ throw ErrnoError.With('ENOTDIR', path, 'getDirListing');
602
+ }
603
+ const data = await tx.get(inode.ino);
604
+ if (!data) {
605
+ /*
606
+ Occurs when data is undefined, or corresponds to something other
607
+ than a directory listing. The latter should never occur unless
608
+ the file system is corrupted.
609
+ */
610
+ throw ErrnoError.With('ENOENT', path, 'getDirListing');
611
+ }
612
+
613
+ return decodeDirListing(data);
614
+ }
615
+
616
+ /**
617
+ * Given the Inode of a directory, retrieves the corresponding directory listing.
618
+ */
619
+ protected getDirListingSync(tx: Transaction, inode: Inode, p?: string): { [fileName: string]: Ino } {
620
+ if (!inode.toStats().isDirectory()) {
621
+ throw ErrnoError.With('ENOTDIR', p, 'getDirListing');
622
+ }
623
+ const data = tx.getSync(inode.ino);
624
+ if (!data) {
625
+ throw ErrnoError.With('ENOENT', p, 'getDirListing');
626
+ }
627
+ return decodeDirListing(data);
628
+ }
629
+
630
+ /**
631
+ * Adds a new node under a random ID. Retries before giving up in
632
+ * the exceedingly unlikely chance that we try to reuse a random ino.
633
+ */
634
+ private async addNew(tx: Transaction, data: Uint8Array, path: string): Promise<Ino> {
635
+ for (let i = 0; i < maxInodeAllocTries; i++) {
636
+ const ino: Ino = randomIno();
637
+ if (await tx.get(ino)) {
638
+ continue;
639
+ }
640
+ await tx.set(ino, data);
641
+ return ino;
642
+ }
643
+ throw new ErrnoError(Errno.ENOSPC, 'No inode IDs available', path, 'addNewNode');
644
+ }
645
+
646
+ /**
647
+ * Creates a new node under a random ID. Retries before giving up in
648
+ * the exceedingly unlikely chance that we try to reuse a random ino.
649
+ * @return The ino that the data was stored under.
650
+ */
651
+ protected addNewSync(tx: Transaction, data: Uint8Array, path: string): Ino {
652
+ for (let i = 0; i < maxInodeAllocTries; i++) {
653
+ const ino: Ino = randomIno();
654
+ if (tx.getSync(ino)) {
655
+ continue;
656
+ }
657
+ tx.setSync(ino, data);
658
+ return ino;
659
+ }
660
+ throw new ErrnoError(Errno.ENOSPC, 'No inode IDs available', path, 'addNewNode');
661
+ }
662
+
663
+ /**
664
+ * Commits a new file (well, a FILE or a DIRECTORY) to the file system with
665
+ * the given mode.
666
+ * Note: This will commit the transaction.
667
+ * @param path The path to the new file.
668
+ * @param type The type of the new file.
669
+ * @param mode The mode to create the new file with.
670
+ * @param cred The UID/GID to create the file with
671
+ * @param data The data to store at the file's data node.
672
+ */
673
+ private async commitNew(tx: Transaction, path: string, type: FileType, mode: number, cred: Cred, data: Uint8Array): Promise<Inode> {
674
+ const parentPath = dirname(path),
675
+ parent = await this.findINode(tx, parentPath);
676
+
677
+ //Check that the creater has correct access
678
+ if (!parent.toStats().hasAccess(W_OK, cred)) {
679
+ throw ErrnoError.With('EACCES', path, 'commitNewFile');
680
+ }
681
+
682
+ const fname = basename(path),
683
+ listing = await this.getDirListing(tx, parent, parentPath);
684
+
685
+ /*
686
+ The root always exists.
687
+ If we don't check this prior to taking steps below,
688
+ we will create a file with name '' in root should path == '/'.
689
+ */
690
+ if (path === '/') {
691
+ throw ErrnoError.With('EEXIST', path, 'commitNewFile');
692
+ }
693
+
694
+ // Check if file already exists.
695
+ if (listing[fname]) {
696
+ await tx.abort();
697
+ throw ErrnoError.With('EEXIST', path, 'commitNewFile');
698
+ }
699
+ try {
700
+ // Commit data.
701
+
702
+ const inode = new Inode();
703
+ inode.ino = await this.addNew(tx, data, path);
704
+ inode.mode = mode | type;
705
+ inode.uid = cred.uid;
706
+ inode.gid = cred.gid;
707
+ inode.size = data.length;
708
+
709
+ // Update and commit parent directory listing.
710
+ listing[fname] = await this.addNew(tx, inode.data, path);
711
+ await tx.set(parent.ino, encodeDirListing(listing));
712
+ await tx.commit();
713
+ return inode;
714
+ } catch (e) {
715
+ tx.abort();
716
+ throw e;
717
+ }
718
+ }
719
+
720
+ /**
721
+ * Commits a new file (well, a FILE or a DIRECTORY) to the file system with the given mode.
722
+ * Note: This will commit the transaction.
723
+ * @param path The path to the new file.
724
+ * @param type The type of the new file.
725
+ * @param mode The mode to create the new file with.
726
+ * @param data The data to store at the file's data node.
727
+ * @return The Inode for the new file.
728
+ */
729
+ protected commitNewSync(path: string, type: FileType, mode: number, cred: Cred, data: Uint8Array = new Uint8Array()): Inode {
730
+ const tx = this.store.transaction(),
731
+ parentPath = dirname(path),
732
+ parent = this.findINodeSync(tx, parentPath);
733
+
734
+ //Check that the creater has correct access
735
+ if (!parent.toStats().hasAccess(W_OK, cred)) {
736
+ throw ErrnoError.With('EACCES', path, 'commitNewFile');
737
+ }
738
+
739
+ const fname = basename(path),
740
+ listing = this.getDirListingSync(tx, parent, parentPath);
741
+
742
+ /*
743
+ The root always exists.
744
+ If we don't check this prior to taking steps below,
745
+ we will create a file with name '' in root should p == '/'.
746
+ */
747
+ if (path === '/') {
748
+ throw ErrnoError.With('EEXIST', path, 'commitNewFile');
749
+ }
750
+
751
+ // Check if file already exists.
752
+ if (listing[fname]) {
753
+ throw ErrnoError.With('EEXIST', path, 'commitNewFile');
754
+ }
755
+
756
+ const node = new Inode();
757
+ try {
758
+ // Commit data.
759
+ node.ino = this.addNewSync(tx, data, path);
760
+ node.size = data.length;
761
+ node.mode = mode | type;
762
+ node.uid = cred.uid;
763
+ node.gid = cred.gid;
764
+ // Update and commit parent directory listing.
765
+ listing[fname] = this.addNewSync(tx, node.data, path);
766
+ tx.setSync(parent.ino, encodeDirListing(listing));
767
+ } catch (e) {
768
+ tx.abortSync();
769
+ throw e;
770
+ }
771
+ tx.commitSync();
772
+ return node;
773
+ }
774
+
775
+ /**
776
+ * Remove all traces of the given path from the file system.
777
+ * @param path The path to remove from the file system.
778
+ * @param isDir Does the path belong to a directory, or a file?
779
+ * @todo Update mtime.
780
+ */
781
+ private async remove(path: string, isDir: boolean, cred: Cred): Promise<void> {
782
+ const tx = this.store.transaction(),
783
+ parent: string = dirname(path),
784
+ parentNode = await this.findINode(tx, parent),
785
+ listing = await this.getDirListing(tx, parentNode, parent),
786
+ fileName: string = basename(path);
787
+
788
+ if (!listing[fileName]) {
789
+ throw ErrnoError.With('ENOENT', path, 'removeEntry');
790
+ }
791
+
792
+ const fileIno = listing[fileName];
793
+
794
+ // Get file inode.
795
+ const fileNode = await this.getINode(tx, fileIno, path);
796
+
797
+ if (!fileNode.toStats().hasAccess(W_OK, cred)) {
798
+ throw ErrnoError.With('EACCES', path, 'removeEntry');
799
+ }
800
+
801
+ // Remove from directory listing of parent.
802
+ delete listing[fileName];
803
+
804
+ if (!isDir && fileNode.toStats().isDirectory()) {
805
+ throw ErrnoError.With('EISDIR', path, 'removeEntry');
806
+ }
807
+
808
+ if (isDir && !fileNode.toStats().isDirectory()) {
809
+ throw ErrnoError.With('ENOTDIR', path, 'removeEntry');
810
+ }
811
+
812
+ try {
813
+ await tx.set(parentNode.ino, encodeDirListing(listing));
814
+
815
+ if (--fileNode.nlink < 1) {
816
+ // remove file
817
+ await tx.remove(fileNode.ino);
818
+ await tx.remove(fileIno);
819
+ }
820
+ } catch (e) {
821
+ await tx.abort();
822
+ throw e;
823
+ }
824
+
825
+ // Success.
826
+ await tx.commit();
827
+ }
828
+
829
+ /**
830
+ * Remove all traces of the given path from the file system.
831
+ * @param path The path to remove from the file system.
832
+ * @param isDir Does the path belong to a directory, or a file?
833
+ * @todo Update mtime.
834
+ */
835
+ protected removeSync(path: string, isDir: boolean, cred: Cred): void {
836
+ const tx = this.store.transaction(),
837
+ parent: string = dirname(path),
838
+ parentNode = this.findINodeSync(tx, parent),
839
+ listing = this.getDirListingSync(tx, parentNode, parent),
840
+ fileName: string = basename(path),
841
+ fileIno: Ino = listing[fileName];
842
+
843
+ if (!fileIno) {
844
+ throw ErrnoError.With('ENOENT', path, 'removeEntry');
845
+ }
846
+
847
+ // Get file inode.
848
+ const fileNode = this.getINodeSync(tx, fileIno, path);
849
+
850
+ if (!fileNode.toStats().hasAccess(W_OK, cred)) {
851
+ throw ErrnoError.With('EACCES', path, 'removeEntry');
852
+ }
853
+
854
+ // Remove from directory listing of parent.
855
+ delete listing[fileName];
856
+
857
+ if (!isDir && fileNode.toStats().isDirectory()) {
858
+ throw ErrnoError.With('EISDIR', path, 'removeEntry');
859
+ }
860
+
861
+ if (isDir && !fileNode.toStats().isDirectory()) {
862
+ throw ErrnoError.With('ENOTDIR', path, 'removeEntry');
863
+ }
864
+
865
+ try {
866
+ // Update directory listing.
867
+ tx.setSync(parentNode.ino, encodeDirListing(listing));
868
+
869
+ if (--fileNode.nlink < 1) {
870
+ // remove file
871
+ tx.removeSync(fileNode.ino);
872
+ tx.removeSync(fileIno);
873
+ }
874
+ } catch (e) {
875
+ tx.abortSync();
876
+ throw e;
877
+ }
878
+ // Success.
879
+ tx.commitSync();
880
+ }
881
+ }