@zenfs/core 0.0.12 → 0.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.
Files changed (54) hide show
  1. package/dist/ApiError.d.ts +52 -15
  2. package/dist/ApiError.js +77 -50
  3. package/dist/FileIndex.d.ts +32 -35
  4. package/dist/FileIndex.js +93 -109
  5. package/dist/backends/AsyncMirror.d.ts +42 -43
  6. package/dist/backends/AsyncMirror.js +154 -147
  7. package/dist/backends/AsyncStore.d.ts +29 -28
  8. package/dist/backends/AsyncStore.js +375 -482
  9. package/dist/backends/FolderAdapter.js +8 -19
  10. package/dist/backends/InMemory.d.ts +16 -13
  11. package/dist/backends/InMemory.js +29 -14
  12. package/dist/backends/Locked.d.ts +8 -28
  13. package/dist/backends/Locked.js +74 -224
  14. package/dist/backends/OverlayFS.d.ts +26 -34
  15. package/dist/backends/OverlayFS.js +303 -511
  16. package/dist/backends/SyncStore.d.ts +54 -72
  17. package/dist/backends/SyncStore.js +159 -161
  18. package/dist/backends/backend.d.ts +45 -29
  19. package/dist/backends/backend.js +83 -13
  20. package/dist/backends/index.d.ts +6 -7
  21. package/dist/backends/index.js +5 -6
  22. package/dist/browser.min.js +21 -6
  23. package/dist/browser.min.js.map +4 -4
  24. package/dist/emulation/callbacks.d.ts +119 -113
  25. package/dist/emulation/callbacks.js +129 -92
  26. package/dist/emulation/constants.js +1 -1
  27. package/dist/emulation/dir.d.ts +55 -0
  28. package/dist/emulation/dir.js +104 -0
  29. package/dist/emulation/fs.d.ts +1 -2
  30. package/dist/emulation/fs.js +0 -1
  31. package/dist/emulation/index.d.ts +3 -0
  32. package/dist/emulation/index.js +3 -0
  33. package/dist/emulation/promises.d.ts +265 -145
  34. package/dist/emulation/promises.js +526 -383
  35. package/dist/emulation/shared.d.ts +20 -6
  36. package/dist/emulation/shared.js +22 -23
  37. package/dist/emulation/streams.d.ts +102 -0
  38. package/dist/emulation/streams.js +55 -0
  39. package/dist/emulation/sync.d.ts +98 -69
  40. package/dist/emulation/sync.js +280 -133
  41. package/dist/file.d.ts +175 -173
  42. package/dist/file.js +257 -273
  43. package/dist/filesystem.d.ts +71 -244
  44. package/dist/filesystem.js +67 -472
  45. package/dist/index.d.ts +7 -44
  46. package/dist/index.js +22 -75
  47. package/dist/inode.d.ts +37 -28
  48. package/dist/inode.js +123 -65
  49. package/dist/stats.d.ts +91 -36
  50. package/dist/stats.js +138 -110
  51. package/dist/utils.d.ts +26 -13
  52. package/dist/utils.js +79 -107
  53. package/package.json +7 -4
  54. package/readme.md +2 -40
@@ -1,142 +1,80 @@
1
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
- return new (P || (P = Promise))(function (resolve, reject) {
4
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
- step((generator = generator.apply(thisArg, _arguments || [])).next());
8
- });
9
- };
10
1
  import { dirname, basename, join, resolve } from '../emulation/path.js';
11
2
  import { ApiError, ErrorCode } from '../ApiError.js';
12
3
  import { W_OK, R_OK } from '../emulation/constants.js';
13
- import { PreloadFile, FileFlag } from '../file.js';
14
- import { BaseFileSystem } from '../filesystem.js';
15
- import Inode from '../inode.js';
4
+ import { PreloadFile } from '../file.js';
5
+ import { AsyncFileSystem } from '../filesystem.js';
6
+ import { randomIno, Inode } from '../inode.js';
16
7
  import { FileType } from '../stats.js';
17
- import { ROOT_NODE_ID, randomUUID, encode } from '../utils.js';
18
- class LRUNode {
19
- constructor(key, value) {
20
- this.key = key;
21
- this.value = value;
22
- this.prev = null;
23
- this.next = null;
24
- }
25
- }
26
- // Adapted from https://chrisrng.svbtle.com/lru-cache-in-javascript
8
+ import { encode, decodeDirListing, encodeDirListing } from '../utils.js';
9
+ import { rootIno } from '../inode.js';
10
+ /**
11
+ * Last Recently Used cache
12
+ */
27
13
  class LRUCache {
28
14
  constructor(limit) {
29
15
  this.limit = limit;
30
- this.size = 0;
31
- this.map = {};
32
- this.head = null;
33
- this.tail = null;
16
+ this.cache = [];
34
17
  }
35
- /**
36
- * Change or add a new value in the cache
37
- * We overwrite the entry if it already exists
38
- */
39
18
  set(key, value) {
40
- const node = new LRUNode(key, value);
41
- if (this.map[key]) {
42
- this.map[key].value = node.value;
43
- this.remove(node.key);
19
+ const existingIndex = this.cache.findIndex(node => node.key === key);
20
+ if (existingIndex != -1) {
21
+ this.cache.splice(existingIndex, 1);
44
22
  }
45
- else {
46
- if (this.size >= this.limit) {
47
- delete this.map[this.tail.key];
48
- this.size--;
49
- this.tail = this.tail.prev;
50
- this.tail.next = null;
51
- }
23
+ else if (this.cache.length >= this.limit) {
24
+ this.cache.shift();
52
25
  }
53
- this.setHead(node);
26
+ this.cache.push({ key, value });
54
27
  }
55
- /* Retrieve a single entry from the cache */
56
28
  get(key) {
57
- if (this.map[key]) {
58
- const value = this.map[key].value;
59
- const node = new LRUNode(key, value);
60
- this.remove(key);
61
- this.setHead(node);
62
- return value;
63
- }
64
- else {
65
- return null;
66
- }
67
- }
68
- /* Remove a single entry from the cache */
69
- remove(key) {
70
- const node = this.map[key];
29
+ const node = this.cache.find(n => n.key === key);
71
30
  if (!node) {
72
31
  return;
73
32
  }
74
- if (node.prev !== null) {
75
- node.prev.next = node.next;
76
- }
77
- else {
78
- this.head = node.next;
79
- }
80
- if (node.next !== null) {
81
- node.next.prev = node.prev;
82
- }
83
- else {
84
- this.tail = node.prev;
85
- }
86
- delete this.map[key];
87
- this.size--;
33
+ // Move the accessed item to the end of the cache (most recently used)
34
+ this.set(key, node.value);
35
+ return node.value;
88
36
  }
89
- /* Resets the entire cache - Argument limit is optional to be reset */
90
- removeAll() {
91
- this.size = 0;
92
- this.map = {};
93
- this.head = null;
94
- this.tail = null;
95
- }
96
- setHead(node) {
97
- node.next = this.head;
98
- node.prev = null;
99
- if (this.head !== null) {
100
- this.head.prev = node;
101
- }
102
- this.head = node;
103
- if (this.tail === null) {
104
- this.tail = node;
37
+ remove(key) {
38
+ const index = this.cache.findIndex(node => node.key === key);
39
+ if (index !== -1) {
40
+ this.cache.splice(index, 1);
105
41
  }
106
- this.size++;
107
- this.map[node.key] = node;
42
+ }
43
+ reset() {
44
+ this.cache = [];
108
45
  }
109
46
  }
110
- export class AsyncKeyValueFile extends PreloadFile {
47
+ export class AsyncFile extends PreloadFile {
111
48
  constructor(_fs, _path, _flag, _stat, contents) {
112
49
  super(_fs, _path, _flag, _stat, contents);
113
50
  }
114
- sync() {
115
- return __awaiter(this, void 0, void 0, function* () {
116
- if (!this.isDirty()) {
117
- return;
118
- }
119
- yield this._fs._sync(this.getPath(), this.getBuffer(), this.getStats());
120
- this.resetDirty();
121
- });
51
+ async sync() {
52
+ if (!this.isDirty()) {
53
+ return;
54
+ }
55
+ await this.fs.sync(this.path, this._buffer, this.stats);
56
+ this.resetDirty();
57
+ }
58
+ syncSync() {
59
+ throw new ApiError(ErrorCode.ENOTSUP);
60
+ }
61
+ async close() {
62
+ this.sync();
122
63
  }
123
- close() {
124
- return __awaiter(this, void 0, void 0, function* () {
125
- this.sync();
126
- });
64
+ closeSync() {
65
+ throw new ApiError(ErrorCode.ENOTSUP);
127
66
  }
128
67
  }
129
68
  /**
130
69
  * An "Asynchronous key-value file system". Stores data to/retrieves data from
131
70
  * an underlying asynchronous key-value store.
132
71
  */
133
- export class AsyncKeyValueFileSystem extends BaseFileSystem {
134
- static isAvailable() {
135
- return true;
72
+ export class AsyncStoreFileSystem extends AsyncFileSystem {
73
+ ready() {
74
+ return this._ready;
136
75
  }
137
76
  constructor(cacheSize) {
138
77
  super();
139
- this._cache = null;
140
78
  if (cacheSize > 0) {
141
79
  this._cache = new LRUCache(cacheSize);
142
80
  }
@@ -145,245 +83,212 @@ export class AsyncKeyValueFileSystem extends BaseFileSystem {
145
83
  * Initializes the file system. Typically called by subclasses' async
146
84
  * constructors.
147
85
  */
148
- init(store) {
149
- return __awaiter(this, void 0, void 0, function* () {
150
- this.store = store;
151
- // INVARIANT: Ensure that the root exists.
152
- yield this.makeRootDirectory();
153
- });
154
- }
155
- getName() {
156
- return this.store.name();
157
- }
158
- isReadOnly() {
159
- return false;
160
- }
161
- supportsSymlinks() {
162
- return false;
163
- }
164
- supportsProps() {
165
- return true;
166
- }
167
- supportsSynch() {
168
- return false;
86
+ async init(store) {
87
+ this.store = store;
88
+ // INVARIANT: Ensure that the root exists.
89
+ await this.makeRootDirectory();
169
90
  }
170
91
  /**
171
92
  * Delete all contents stored in the file system.
172
93
  */
173
- empty() {
174
- return __awaiter(this, void 0, void 0, function* () {
175
- if (this._cache) {
176
- this._cache.removeAll();
177
- }
178
- yield this.store.clear();
179
- // INVARIANT: Root always exists.
180
- yield this.makeRootDirectory();
181
- });
182
- }
183
- access(p, mode, cred) {
184
- return __awaiter(this, void 0, void 0, function* () {
185
- const tx = this.store.beginTransaction('readonly');
186
- const inode = yield this.findINode(tx, p);
187
- if (!inode) {
188
- throw ApiError.ENOENT(p);
189
- }
190
- if (!inode.toStats().hasAccess(mode, cred)) {
191
- throw ApiError.EACCES(p);
192
- }
193
- });
94
+ async empty() {
95
+ if (this._cache) {
96
+ this._cache.reset();
97
+ }
98
+ await this.store.clear();
99
+ // INVARIANT: Root always exists.
100
+ await this.makeRootDirectory();
194
101
  }
195
102
  /**
196
103
  * @todo Make rename compatible with the cache.
197
104
  */
198
- rename(oldPath, newPath, cred) {
199
- return __awaiter(this, void 0, void 0, function* () {
200
- const c = this._cache;
201
- if (this._cache) {
202
- // Clear and disable cache during renaming process.
203
- this._cache = null;
204
- c.removeAll();
105
+ async rename(oldPath, newPath, cred) {
106
+ const c = this._cache;
107
+ if (this._cache) {
108
+ // Clear and disable cache during renaming process.
109
+ this._cache = null;
110
+ c.reset();
111
+ }
112
+ try {
113
+ const tx = this.store.beginTransaction('readwrite'), oldParent = dirname(oldPath), oldName = basename(oldPath), newParent = dirname(newPath), newName = basename(newPath),
114
+ // Remove oldPath from parent's directory listing.
115
+ oldDirNode = await this.findINode(tx, oldParent), oldDirList = await this.getDirListing(tx, oldDirNode, oldParent);
116
+ if (!oldDirNode.toStats().hasAccess(W_OK, cred)) {
117
+ throw ApiError.EACCES(oldPath);
118
+ }
119
+ if (!oldDirList[oldName]) {
120
+ throw ApiError.ENOENT(oldPath);
121
+ }
122
+ const nodeId = oldDirList[oldName];
123
+ delete oldDirList[oldName];
124
+ // Invariant: Can't move a folder inside itself.
125
+ // This funny little hack ensures that the check passes only if oldPath
126
+ // is a subpath of newParent. We append '/' to avoid matching folders that
127
+ // are a substring of the bottom-most folder in the path.
128
+ if ((newParent + '/').indexOf(oldPath + '/') === 0) {
129
+ throw new ApiError(ErrorCode.EBUSY, oldParent);
130
+ }
131
+ // Add newPath to parent's directory listing.
132
+ let newDirNode, newDirList;
133
+ if (newParent === oldParent) {
134
+ // Prevent us from re-grabbing the same directory listing, which still
135
+ // contains oldName.
136
+ newDirNode = oldDirNode;
137
+ newDirList = oldDirList;
205
138
  }
206
- try {
207
- const tx = this.store.beginTransaction('readwrite'), oldParent = dirname(oldPath), oldName = basename(oldPath), newParent = dirname(newPath), newName = basename(newPath),
208
- // Remove oldPath from parent's directory listing.
209
- oldDirNode = yield this.findINode(tx, oldParent), oldDirList = yield this.getDirListing(tx, oldParent, oldDirNode);
210
- if (!oldDirNode.toStats().hasAccess(W_OK, cred)) {
211
- throw ApiError.EACCES(oldPath);
212
- }
213
- if (!oldDirList[oldName]) {
214
- throw ApiError.ENOENT(oldPath);
215
- }
216
- const nodeId = oldDirList[oldName];
217
- delete oldDirList[oldName];
218
- // Invariant: Can't move a folder inside itself.
219
- // This funny little hack ensures that the check passes only if oldPath
220
- // is a subpath of newParent. We append '/' to avoid matching folders that
221
- // are a substring of the bottom-most folder in the path.
222
- if ((newParent + '/').indexOf(oldPath + '/') === 0) {
223
- throw new ApiError(ErrorCode.EBUSY, oldParent);
224
- }
225
- // Add newPath to parent's directory listing.
226
- let newDirNode, newDirList;
227
- if (newParent === oldParent) {
228
- // Prevent us from re-grabbing the same directory listing, which still
229
- // contains oldName.
230
- newDirNode = oldDirNode;
231
- newDirList = oldDirList;
232
- }
233
- else {
234
- newDirNode = yield this.findINode(tx, newParent);
235
- newDirList = yield this.getDirListing(tx, newParent, newDirNode);
236
- }
237
- if (newDirList[newName]) {
238
- // If it's a file, delete it.
239
- const newNameNode = yield this.getINode(tx, newPath, newDirList[newName]);
240
- if (newNameNode.isFile()) {
241
- try {
242
- yield tx.del(newNameNode.id);
243
- yield tx.del(newDirList[newName]);
244
- }
245
- catch (e) {
246
- yield tx.abort();
247
- throw e;
248
- }
139
+ else {
140
+ newDirNode = await this.findINode(tx, newParent);
141
+ newDirList = await this.getDirListing(tx, newDirNode, newParent);
142
+ }
143
+ if (newDirList[newName]) {
144
+ // If it's a file, delete it.
145
+ const newNameNode = await this.getINode(tx, newDirList[newName], newPath);
146
+ if (newNameNode.toStats().isFile()) {
147
+ try {
148
+ await tx.remove(newNameNode.ino);
149
+ await tx.remove(newDirList[newName]);
249
150
  }
250
- else {
251
- // If it's a directory, throw a permissions error.
252
- throw ApiError.EPERM(newPath);
151
+ catch (e) {
152
+ await tx.abort();
153
+ throw e;
253
154
  }
254
155
  }
255
- newDirList[newName] = nodeId;
256
- // Commit the two changed directory listings.
257
- try {
258
- yield tx.put(oldDirNode.id, encode(JSON.stringify(oldDirList)), true);
259
- yield tx.put(newDirNode.id, encode(JSON.stringify(newDirList)), true);
260
- }
261
- catch (e) {
262
- yield tx.abort();
263
- throw e;
264
- }
265
- yield tx.commit();
266
- }
267
- finally {
268
- if (c) {
269
- this._cache = c;
156
+ else {
157
+ // If it's a directory, throw a permissions error.
158
+ throw ApiError.EPERM(newPath);
270
159
  }
271
160
  }
272
- });
273
- }
274
- stat(p, cred) {
275
- return __awaiter(this, void 0, void 0, function* () {
276
- const tx = this.store.beginTransaction('readonly');
277
- const inode = yield this.findINode(tx, p);
278
- const stats = inode.toStats();
279
- if (!stats.hasAccess(R_OK, cred)) {
280
- throw ApiError.EACCES(p);
281
- }
282
- return stats;
283
- });
284
- }
285
- createFile(p, flag, mode, cred) {
286
- return __awaiter(this, void 0, void 0, function* () {
287
- const tx = this.store.beginTransaction('readwrite'), data = new Uint8Array(0), newFile = yield this.commitNewFile(tx, p, FileType.FILE, mode, cred, data);
288
- // Open the file.
289
- return new AsyncKeyValueFile(this, p, flag, newFile.toStats(), data);
290
- });
291
- }
292
- openFile(p, flag, cred) {
293
- return __awaiter(this, void 0, void 0, function* () {
294
- const tx = this.store.beginTransaction('readonly'), node = yield this.findINode(tx, p), data = yield tx.get(node.id);
295
- if (!node.toStats().hasAccess(flag.getMode(), cred)) {
296
- throw ApiError.EACCES(p);
297
- }
298
- if (data === undefined) {
299
- throw ApiError.ENOENT(p);
300
- }
301
- return new AsyncKeyValueFile(this, p, flag, node.toStats(), data);
302
- });
303
- }
304
- unlink(p, cred) {
305
- return __awaiter(this, void 0, void 0, function* () {
306
- return this.removeEntry(p, false, cred);
307
- });
308
- }
309
- rmdir(p, cred) {
310
- return __awaiter(this, void 0, void 0, function* () {
311
- // Check first if directory is empty.
312
- const list = yield this.readdir(p, cred);
313
- if (list.length > 0) {
314
- throw ApiError.ENOTEMPTY(p);
315
- }
316
- yield this.removeEntry(p, true, cred);
317
- });
318
- }
319
- mkdir(p, mode, cred) {
320
- return __awaiter(this, void 0, void 0, function* () {
321
- const tx = this.store.beginTransaction('readwrite'), data = encode('{}');
322
- yield this.commitNewFile(tx, p, FileType.DIRECTORY, mode, cred, data);
323
- });
324
- }
325
- readdir(p, cred) {
326
- return __awaiter(this, void 0, void 0, function* () {
327
- const tx = this.store.beginTransaction('readonly');
328
- const node = yield this.findINode(tx, p);
329
- if (!node.toStats().hasAccess(R_OK, cred)) {
330
- throw ApiError.EACCES(p);
331
- }
332
- return Object.keys(yield this.getDirListing(tx, p, node));
333
- });
334
- }
335
- chmod(p, mode, cred) {
336
- return __awaiter(this, void 0, void 0, function* () {
337
- const fd = yield this.openFile(p, FileFlag.getFileFlag('r+'), cred);
338
- yield fd.chmod(mode);
339
- });
340
- }
341
- chown(p, new_uid, new_gid, cred) {
342
- return __awaiter(this, void 0, void 0, function* () {
343
- const fd = yield this.openFile(p, FileFlag.getFileFlag('r+'), cred);
344
- yield fd.chown(new_uid, new_gid);
345
- });
346
- }
347
- _sync(p, data, stats) {
348
- return __awaiter(this, void 0, void 0, function* () {
349
- // @todo Ensure mtime updates properly, and use that to determine if a data
350
- // update is required.
351
- const tx = this.store.beginTransaction('readwrite'),
352
- // We use the _findInode helper because we actually need the INode id.
353
- fileInodeId = yield this._findINode(tx, dirname(p), basename(p)), fileInode = yield this.getINode(tx, p, fileInodeId), inodeChanged = fileInode.update(stats);
161
+ newDirList[newName] = nodeId;
162
+ // Commit the two changed directory listings.
354
163
  try {
355
- // Sync data.
356
- yield tx.put(fileInode.id, data, true);
357
- // Sync metadata.
358
- if (inodeChanged) {
359
- yield tx.put(fileInodeId, fileInode.serialize(), true);
360
- }
164
+ await tx.put(oldDirNode.ino, encodeDirListing(oldDirList), true);
165
+ await tx.put(newDirNode.ino, encodeDirListing(newDirList), true);
361
166
  }
362
167
  catch (e) {
363
- yield tx.abort();
168
+ await tx.abort();
364
169
  throw e;
365
170
  }
366
- yield tx.commit();
367
- });
171
+ await tx.commit();
172
+ }
173
+ finally {
174
+ if (c) {
175
+ this._cache = c;
176
+ }
177
+ }
178
+ }
179
+ async stat(p, cred) {
180
+ const tx = this.store.beginTransaction('readonly');
181
+ const inode = await this.findINode(tx, p);
182
+ if (!inode) {
183
+ throw ApiError.ENOENT(p);
184
+ }
185
+ const stats = inode.toStats();
186
+ if (!stats.hasAccess(R_OK, cred)) {
187
+ throw ApiError.EACCES(p);
188
+ }
189
+ return stats;
190
+ }
191
+ async createFile(p, flag, mode, cred) {
192
+ const tx = this.store.beginTransaction('readwrite'), data = new Uint8Array(0), newFile = await this.commitNewFile(tx, p, FileType.FILE, mode, cred, data);
193
+ // Open the file.
194
+ return new AsyncFile(this, p, flag, newFile.toStats(), data);
195
+ }
196
+ async openFile(p, flag, cred) {
197
+ const tx = this.store.beginTransaction('readonly'), node = await this.findINode(tx, p), data = await tx.get(node.ino);
198
+ if (!node.toStats().hasAccess(flag.mode, cred)) {
199
+ throw ApiError.EACCES(p);
200
+ }
201
+ if (!data) {
202
+ throw ApiError.ENOENT(p);
203
+ }
204
+ return new AsyncFile(this, p, flag, node.toStats(), data);
205
+ }
206
+ async unlink(p, cred) {
207
+ return this.removeEntry(p, false, cred);
208
+ }
209
+ async rmdir(p, cred) {
210
+ // Check first if directory is empty.
211
+ const list = await this.readdir(p, cred);
212
+ if (list.length > 0) {
213
+ throw ApiError.ENOTEMPTY(p);
214
+ }
215
+ await this.removeEntry(p, true, cred);
216
+ }
217
+ async mkdir(p, mode, cred) {
218
+ const tx = this.store.beginTransaction('readwrite'), data = encode('{}');
219
+ await this.commitNewFile(tx, p, FileType.DIRECTORY, mode, cred, data);
220
+ }
221
+ async readdir(p, cred) {
222
+ const tx = this.store.beginTransaction('readonly');
223
+ const node = await this.findINode(tx, p);
224
+ if (!node.toStats().hasAccess(R_OK, cred)) {
225
+ throw ApiError.EACCES(p);
226
+ }
227
+ return Object.keys(await this.getDirListing(tx, node, p));
368
228
  }
369
229
  /**
370
- * Checks if the root directory exists. Creates it if it doesn't.
230
+ * Updated the inode and data node at the given path
231
+ * @todo Ensure mtime updates properly, and use that to determine if a data update is required.
371
232
  */
372
- makeRootDirectory() {
373
- return __awaiter(this, void 0, void 0, function* () {
374
- const tx = this.store.beginTransaction('readwrite');
375
- if ((yield tx.get(ROOT_NODE_ID)) === undefined) {
376
- // Create new inode.
377
- const currTime = new Date().getTime(),
378
- // Mode 0666, owned by root:root
379
- dirInode = new Inode(randomUUID(), 4096, 511 | FileType.DIRECTORY, currTime, currTime, currTime, 0, 0);
380
- // If the root doesn't exist, the first random ID shouldn't exist,
381
- // either.
382
- yield tx.put(dirInode.id, encode('{}'), false);
383
- yield tx.put(ROOT_NODE_ID, dirInode.serialize(), false);
384
- yield tx.commit();
233
+ async sync(p, data, stats) {
234
+ const tx = this.store.beginTransaction('readwrite'),
235
+ // We use the _findInode helper because we actually need the INode id.
236
+ fileInodeId = await this._findINode(tx, dirname(p), basename(p)), fileInode = await this.getINode(tx, fileInodeId, p), inodeChanged = fileInode.update(stats);
237
+ try {
238
+ // Sync data.
239
+ await tx.put(fileInode.ino, data, true);
240
+ // Sync metadata.
241
+ if (inodeChanged) {
242
+ await tx.put(fileInodeId, fileInode.data, true);
385
243
  }
386
- });
244
+ }
245
+ catch (e) {
246
+ await tx.abort();
247
+ throw e;
248
+ }
249
+ await tx.commit();
250
+ }
251
+ async link(existing, newpath, cred) {
252
+ const tx = this.store.beginTransaction('readwrite'), existingDir = dirname(existing), existingDirNode = await this.findINode(tx, existingDir);
253
+ if (!existingDirNode.toStats().hasAccess(R_OK, cred)) {
254
+ throw ApiError.EACCES(existingDir);
255
+ }
256
+ const newDir = dirname(newpath), newDirNode = await this.findINode(tx, newDir), newListing = await this.getDirListing(tx, newDirNode, newDir);
257
+ if (!newDirNode.toStats().hasAccess(W_OK, cred)) {
258
+ throw ApiError.EACCES(newDir);
259
+ }
260
+ const ino = await this._findINode(tx, existingDir, basename(existing));
261
+ const node = await this.getINode(tx, ino, existing);
262
+ if (!node.toStats().hasAccess(W_OK, cred)) {
263
+ throw ApiError.EACCES(newpath);
264
+ }
265
+ node.nlink++;
266
+ newListing[basename(newpath)] = ino;
267
+ try {
268
+ tx.put(ino, node.data, true);
269
+ tx.put(newDirNode.ino, encodeDirListing(newListing), true);
270
+ }
271
+ catch (e) {
272
+ tx.abort();
273
+ throw e;
274
+ }
275
+ tx.commit();
276
+ }
277
+ /**
278
+ * Checks if the root directory exists. Creates it if it doesn't.
279
+ */
280
+ async makeRootDirectory() {
281
+ const tx = this.store.beginTransaction('readwrite');
282
+ if ((await tx.get(rootIno)) === undefined) {
283
+ // Create new inode. o777, owned by root:root
284
+ const dirInode = new Inode();
285
+ dirInode.mode = 0o777 | FileType.DIRECTORY;
286
+ // If the root doesn't exist, the first random ID shouldn't exist,
287
+ // either.
288
+ await tx.put(dirInode.ino, encode('{}'), false);
289
+ await tx.put(rootIno, dirInode.data, false);
290
+ await tx.commit();
291
+ }
387
292
  }
388
293
  /**
389
294
  * Helper function for findINode.
@@ -391,48 +296,30 @@ export class AsyncKeyValueFileSystem extends BaseFileSystem {
391
296
  * @param filename The filename of the inode we are attempting to find, minus
392
297
  * the parent.
393
298
  */
394
- _findINode(tx, parent, filename, visited = new Set()) {
395
- return __awaiter(this, void 0, void 0, function* () {
396
- const currentPath = join(parent, filename);
397
- if (visited.has(currentPath)) {
398
- throw new ApiError(ErrorCode.EIO, 'Infinite loop detected while finding inode', currentPath);
399
- }
400
- visited.add(currentPath);
401
- if (this._cache) {
402
- const id = this._cache.get(currentPath);
403
- if (id) {
404
- return id;
405
- }
299
+ async _findINode(tx, parent, filename, visited = new Set()) {
300
+ const currentPath = join(parent, filename);
301
+ if (visited.has(currentPath)) {
302
+ throw new ApiError(ErrorCode.EIO, 'Infinite loop detected while finding inode', currentPath);
303
+ }
304
+ visited.add(currentPath);
305
+ if (this._cache) {
306
+ const id = this._cache.get(currentPath);
307
+ if (id) {
308
+ return id;
406
309
  }
407
- if (parent === '/') {
408
- if (filename === '') {
409
- // BASE CASE #1: Return the root's ID.
410
- if (this._cache) {
411
- this._cache.set(currentPath, ROOT_NODE_ID);
412
- }
413
- return ROOT_NODE_ID;
414
- }
415
- else {
416
- // BASE CASE #2: Find the item in the root node.
417
- const inode = yield this.getINode(tx, parent, ROOT_NODE_ID);
418
- const dirList = yield this.getDirListing(tx, parent, inode);
419
- if (dirList[filename]) {
420
- const id = dirList[filename];
421
- if (this._cache) {
422
- this._cache.set(currentPath, id);
423
- }
424
- return id;
425
- }
426
- else {
427
- throw ApiError.ENOENT(resolve(parent, filename));
428
- }
310
+ }
311
+ if (parent === '/') {
312
+ if (filename === '') {
313
+ // BASE CASE #1: Return the root's ID.
314
+ if (this._cache) {
315
+ this._cache.set(currentPath, rootIno);
429
316
  }
317
+ return rootIno;
430
318
  }
431
319
  else {
432
- // Get the parent directory's INode, and find the file in its directory
433
- // listing.
434
- const inode = yield this.findINode(tx, parent, visited);
435
- const dirList = yield this.getDirListing(tx, parent, inode);
320
+ // BASE CASE #2: Find the item in the root node.
321
+ const inode = await this.getINode(tx, rootIno, parent);
322
+ const dirList = await this.getDirListing(tx, inode, parent);
436
323
  if (dirList[filename]) {
437
324
  const id = dirList[filename];
438
325
  if (this._cache) {
@@ -444,18 +331,32 @@ export class AsyncKeyValueFileSystem extends BaseFileSystem {
444
331
  throw ApiError.ENOENT(resolve(parent, filename));
445
332
  }
446
333
  }
447
- });
334
+ }
335
+ else {
336
+ // Get the parent directory's INode, and find the file in its directory
337
+ // listing.
338
+ const inode = await this.findINode(tx, parent, visited);
339
+ const dirList = await this.getDirListing(tx, inode, parent);
340
+ if (dirList[filename]) {
341
+ const id = dirList[filename];
342
+ if (this._cache) {
343
+ this._cache.set(currentPath, id);
344
+ }
345
+ return id;
346
+ }
347
+ else {
348
+ throw ApiError.ENOENT(resolve(parent, filename));
349
+ }
350
+ }
448
351
  }
449
352
  /**
450
353
  * Finds the Inode of the given path.
451
354
  * @param p The path to look up.
452
355
  * @todo memoize/cache
453
356
  */
454
- findINode(tx, p, visited = new Set()) {
455
- return __awaiter(this, void 0, void 0, function* () {
456
- const id = yield this._findINode(tx, dirname(p), basename(p), visited);
457
- return this.getINode(tx, p, id);
458
- });
357
+ async findINode(tx, p, visited = new Set()) {
358
+ const id = await this._findINode(tx, dirname(p), basename(p), visited);
359
+ return this.getINode(tx, id, p);
459
360
  }
460
361
  /**
461
362
  * Given the ID of a node, retrieves the corresponding Inode.
@@ -463,62 +364,56 @@ export class AsyncKeyValueFileSystem extends BaseFileSystem {
463
364
  * @param p The corresponding path to the file (used for error messages).
464
365
  * @param id The ID to look up.
465
366
  */
466
- getINode(tx, p, id) {
467
- return __awaiter(this, void 0, void 0, function* () {
468
- const data = yield tx.get(id);
469
- if (!data) {
470
- throw ApiError.ENOENT(p);
471
- }
472
- return Inode.Deserialize(data);
473
- });
367
+ async getINode(tx, id, p) {
368
+ const data = await tx.get(id);
369
+ if (!data) {
370
+ throw ApiError.ENOENT(p);
371
+ }
372
+ return new Inode(data.buffer);
474
373
  }
475
374
  /**
476
375
  * Given the Inode of a directory, retrieves the corresponding directory
477
376
  * listing.
478
377
  */
479
- getDirListing(tx, p, inode) {
480
- return __awaiter(this, void 0, void 0, function* () {
481
- if (!inode.isDirectory()) {
482
- throw ApiError.ENOTDIR(p);
483
- }
484
- const data = yield tx.get(inode.id);
485
- try {
486
- return JSON.parse(data.toString());
487
- }
488
- catch (e) {
489
- // Occurs when data is undefined, or corresponds to something other
490
- // than a directory listing. The latter should never occur unless
491
- // the file system is corrupted.
492
- throw ApiError.ENOENT(p);
493
- }
494
- });
378
+ async getDirListing(tx, inode, p) {
379
+ if (!inode.toStats().isDirectory()) {
380
+ throw ApiError.ENOTDIR(p);
381
+ }
382
+ const data = await tx.get(inode.ino);
383
+ if (!data) {
384
+ /*
385
+ Occurs when data is undefined, or corresponds to something other
386
+ than a directory listing. The latter should never occur unless
387
+ the file system is corrupted.
388
+ */
389
+ throw ApiError.ENOENT(p);
390
+ }
391
+ return decodeDirListing(data);
495
392
  }
496
393
  /**
497
394
  * Adds a new node under a random ID. Retries 5 times before giving up in
498
- * the exceedingly unlikely chance that we try to reuse a random GUID.
395
+ * the exceedingly unlikely chance that we try to reuse a random ino.
499
396
  */
500
- addNewNode(tx, data) {
501
- return __awaiter(this, void 0, void 0, function* () {
502
- let retries = 0;
503
- const reroll = () => __awaiter(this, void 0, void 0, function* () {
504
- if (++retries === 5) {
505
- // Max retries hit. Return with an error.
506
- throw new ApiError(ErrorCode.EIO, 'Unable to commit data to key-value store.');
397
+ async addNewNode(tx, data) {
398
+ let retries = 0;
399
+ const reroll = async () => {
400
+ if (++retries === 5) {
401
+ // Max retries hit. Return with an error.
402
+ throw new ApiError(ErrorCode.EIO, 'Unable to commit data to key-value store.');
403
+ }
404
+ else {
405
+ // Try again.
406
+ const ino = randomIno();
407
+ const committed = await tx.put(ino, data, false);
408
+ if (!committed) {
409
+ return reroll();
507
410
  }
508
411
  else {
509
- // Try again.
510
- const currId = randomUUID();
511
- const committed = yield tx.put(currId, data, false);
512
- if (!committed) {
513
- return reroll();
514
- }
515
- else {
516
- return currId;
517
- }
412
+ return ino;
518
413
  }
519
- });
520
- return reroll();
521
- });
414
+ }
415
+ };
416
+ return reroll();
522
417
  }
523
418
  /**
524
419
  * Commits a new file (well, a FILE or a DIRECTORY) to the file system with
@@ -530,41 +425,41 @@ export class AsyncKeyValueFileSystem extends BaseFileSystem {
530
425
  * @param cred The UID/GID to create the file with
531
426
  * @param data The data to store at the file's data node.
532
427
  */
533
- commitNewFile(tx, p, type, mode, cred, data) {
534
- return __awaiter(this, void 0, void 0, function* () {
535
- const parentDir = dirname(p), fname = basename(p), parentNode = yield this.findINode(tx, parentDir), dirListing = yield this.getDirListing(tx, parentDir, parentNode), currTime = new Date().getTime();
536
- //Check that the creater has correct access
537
- if (!parentNode.toStats().hasAccess(W_OK, cred)) {
538
- throw ApiError.EACCES(p);
539
- }
540
- // Invariant: The root always exists.
541
- // If we don't check this prior to taking steps below, we will create a
542
- // file with name '' in root should p == '/'.
543
- if (p === '/') {
544
- throw ApiError.EEXIST(p);
545
- }
546
- // Check if file already exists.
547
- if (dirListing[fname]) {
548
- yield tx.abort();
549
- throw ApiError.EEXIST(p);
550
- }
551
- try {
552
- // Commit data.
553
- const dataId = yield this.addNewNode(tx, data);
554
- const fileNode = new Inode(dataId, data.length, mode | type, currTime, currTime, currTime, cred.uid, cred.gid);
555
- // Commit file node.
556
- const fileNodeId = yield this.addNewNode(tx, fileNode.serialize());
557
- // Update and commit parent directory listing.
558
- dirListing[fname] = fileNodeId;
559
- yield tx.put(parentNode.id, encode(JSON.stringify(dirListing)), true);
560
- yield tx.commit();
561
- return fileNode;
562
- }
563
- catch (e) {
564
- tx.abort();
565
- throw e;
566
- }
567
- });
428
+ async commitNewFile(tx, p, type, mode, cred, data) {
429
+ const parentDir = dirname(p), fname = basename(p), parentNode = await this.findINode(tx, parentDir), dirListing = await this.getDirListing(tx, parentNode, parentDir);
430
+ //Check that the creater has correct access
431
+ if (!parentNode.toStats().hasAccess(W_OK, cred)) {
432
+ throw ApiError.EACCES(p);
433
+ }
434
+ // Invariant: The root always exists.
435
+ // If we don't check this prior to taking steps below, we will create a
436
+ // file with name '' in root should p == '/'.
437
+ if (p === '/') {
438
+ throw ApiError.EEXIST(p);
439
+ }
440
+ // Check if file already exists.
441
+ if (dirListing[fname]) {
442
+ await tx.abort();
443
+ throw ApiError.EEXIST(p);
444
+ }
445
+ try {
446
+ // Commit data.
447
+ const inode = new Inode();
448
+ inode.ino = await this.addNewNode(tx, data);
449
+ inode.mode = mode | type;
450
+ inode.uid = cred.uid;
451
+ inode.gid = cred.gid;
452
+ inode.size = data.length;
453
+ // Update and commit parent directory listing.
454
+ dirListing[fname] = await this.addNewNode(tx, inode.data);
455
+ await tx.put(parentNode.ino, encodeDirListing(dirListing), true);
456
+ await tx.commit();
457
+ return inode;
458
+ }
459
+ catch (e) {
460
+ tx.abort();
461
+ throw e;
462
+ }
568
463
  }
569
464
  /**
570
465
  * Remove all traces of the given path from the file system.
@@ -578,43 +473,41 @@ export class AsyncKeyValueFileSystem extends BaseFileSystem {
578
473
  * @param isDir Does the path belong to a directory, or a file?
579
474
  * @todo Update mtime.
580
475
  */
581
- removeEntry(p, isDir, cred) {
582
- return __awaiter(this, void 0, void 0, function* () {
583
- if (this._cache) {
584
- this._cache.remove(p);
585
- }
586
- const tx = this.store.beginTransaction('readwrite'), parent = dirname(p), parentNode = yield this.findINode(tx, parent), parentListing = yield this.getDirListing(tx, parent, parentNode), fileName = basename(p);
587
- if (!parentListing[fileName]) {
588
- throw ApiError.ENOENT(p);
589
- }
590
- const fileNodeId = parentListing[fileName];
591
- // Get file inode.
592
- const fileNode = yield this.getINode(tx, p, fileNodeId);
593
- if (!fileNode.toStats().hasAccess(W_OK, cred)) {
594
- throw ApiError.EACCES(p);
595
- }
596
- // Remove from directory listing of parent.
597
- delete parentListing[fileName];
598
- if (!isDir && fileNode.isDirectory()) {
599
- throw ApiError.EISDIR(p);
600
- }
601
- else if (isDir && !fileNode.isDirectory()) {
602
- throw ApiError.ENOTDIR(p);
603
- }
604
- try {
605
- // Delete data.
606
- yield tx.del(fileNode.id);
607
- // Delete node.
608
- yield tx.del(fileNodeId);
609
- // Update directory listing.
610
- yield tx.put(parentNode.id, encode(JSON.stringify(parentListing)), true);
611
- }
612
- catch (e) {
613
- yield tx.abort();
614
- throw e;
476
+ async removeEntry(p, isDir, cred) {
477
+ if (this._cache) {
478
+ this._cache.remove(p);
479
+ }
480
+ const tx = this.store.beginTransaction('readwrite'), parent = dirname(p), parentNode = await this.findINode(tx, parent), parentListing = await this.getDirListing(tx, parentNode, parent), fileName = basename(p);
481
+ if (!parentListing[fileName]) {
482
+ throw ApiError.ENOENT(p);
483
+ }
484
+ const fileIno = parentListing[fileName];
485
+ // Get file inode.
486
+ const fileNode = await this.getINode(tx, fileIno, p);
487
+ if (!fileNode.toStats().hasAccess(W_OK, cred)) {
488
+ throw ApiError.EACCES(p);
489
+ }
490
+ // Remove from directory listing of parent.
491
+ delete parentListing[fileName];
492
+ if (!isDir && fileNode.toStats().isDirectory()) {
493
+ throw ApiError.EISDIR(p);
494
+ }
495
+ if (isDir && !fileNode.toStats().isDirectory()) {
496
+ throw ApiError.ENOTDIR(p);
497
+ }
498
+ try {
499
+ await tx.put(parentNode.ino, encodeDirListing(parentListing), true);
500
+ if (--fileNode.nlink < 1) {
501
+ // remove file
502
+ await tx.remove(fileNode.ino);
503
+ await tx.remove(fileIno);
615
504
  }
616
- // Success.
617
- yield tx.commit();
618
- });
505
+ }
506
+ catch (e) {
507
+ await tx.abort();
508
+ throw e;
509
+ }
510
+ // Success.
511
+ await tx.commit();
619
512
  }
620
513
  }