@zenfs/core 0.0.1

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 (55) hide show
  1. package/README.md +293 -0
  2. package/dist/ApiError.d.ts +86 -0
  3. package/dist/ApiError.js +135 -0
  4. package/dist/backends/AsyncMirror.d.ts +102 -0
  5. package/dist/backends/AsyncMirror.js +252 -0
  6. package/dist/backends/AsyncStore.d.ts +166 -0
  7. package/dist/backends/AsyncStore.js +620 -0
  8. package/dist/backends/FolderAdapter.d.ts +52 -0
  9. package/dist/backends/FolderAdapter.js +184 -0
  10. package/dist/backends/InMemory.d.ts +25 -0
  11. package/dist/backends/InMemory.js +46 -0
  12. package/dist/backends/Locked.d.ts +64 -0
  13. package/dist/backends/Locked.js +302 -0
  14. package/dist/backends/OverlayFS.d.ts +120 -0
  15. package/dist/backends/OverlayFS.js +749 -0
  16. package/dist/backends/SyncStore.d.ts +223 -0
  17. package/dist/backends/SyncStore.js +479 -0
  18. package/dist/backends/backend.d.ts +73 -0
  19. package/dist/backends/backend.js +14 -0
  20. package/dist/backends/index.d.ts +11 -0
  21. package/dist/backends/index.js +15 -0
  22. package/dist/browser.min.js +12 -0
  23. package/dist/browser.min.js.map +7 -0
  24. package/dist/cred.d.ts +14 -0
  25. package/dist/cred.js +15 -0
  26. package/dist/emulation/callbacks.d.ts +382 -0
  27. package/dist/emulation/callbacks.js +422 -0
  28. package/dist/emulation/constants.d.ts +101 -0
  29. package/dist/emulation/constants.js +110 -0
  30. package/dist/emulation/fs.d.ts +7 -0
  31. package/dist/emulation/fs.js +5 -0
  32. package/dist/emulation/index.d.ts +5 -0
  33. package/dist/emulation/index.js +7 -0
  34. package/dist/emulation/promises.d.ts +309 -0
  35. package/dist/emulation/promises.js +521 -0
  36. package/dist/emulation/shared.d.ts +62 -0
  37. package/dist/emulation/shared.js +192 -0
  38. package/dist/emulation/sync.d.ts +278 -0
  39. package/dist/emulation/sync.js +392 -0
  40. package/dist/file.d.ts +449 -0
  41. package/dist/file.js +576 -0
  42. package/dist/filesystem.d.ts +367 -0
  43. package/dist/filesystem.js +542 -0
  44. package/dist/index.d.ts +78 -0
  45. package/dist/index.js +113 -0
  46. package/dist/inode.d.ts +51 -0
  47. package/dist/inode.js +112 -0
  48. package/dist/mutex.d.ts +12 -0
  49. package/dist/mutex.js +48 -0
  50. package/dist/stats.d.ts +98 -0
  51. package/dist/stats.js +226 -0
  52. package/dist/utils.d.ts +52 -0
  53. package/dist/utils.js +261 -0
  54. package/license.md +122 -0
  55. package/package.json +61 -0
@@ -0,0 +1,223 @@
1
+ /// <reference types="node" />
2
+ import { Cred } from '../cred';
3
+ import { File, FileFlag, PreloadFile } from '../file';
4
+ import { SynchronousFileSystem } from '../filesystem';
5
+ import { Stats } from '../stats';
6
+ /**
7
+ * Represents a *synchronous* key-value store.
8
+ */
9
+ export interface SyncKeyValueStore {
10
+ /**
11
+ * The name of the key-value store.
12
+ */
13
+ name(): string;
14
+ /**
15
+ * Empties the key-value store completely.
16
+ */
17
+ clear(): void;
18
+ /**
19
+ * Begins a new read-only transaction.
20
+ */
21
+ beginTransaction(type: 'readonly'): SyncKeyValueROTransaction;
22
+ /**
23
+ * Begins a new read-write transaction.
24
+ */
25
+ beginTransaction(type: 'readwrite'): SyncKeyValueRWTransaction;
26
+ beginTransaction(type: string): SyncKeyValueROTransaction;
27
+ }
28
+ /**
29
+ * A read-only transaction for a synchronous key value store.
30
+ */
31
+ export interface SyncKeyValueROTransaction {
32
+ /**
33
+ * Retrieves the data at the given key. Throws an ApiError if an error occurs
34
+ * or if the key does not exist.
35
+ * @param key The key to look under for data.
36
+ * @return The data stored under the key, or undefined if not present.
37
+ */
38
+ get(key: string): Buffer | undefined;
39
+ }
40
+ /**
41
+ * A read-write transaction for a synchronous key value store.
42
+ */
43
+ export interface SyncKeyValueRWTransaction extends SyncKeyValueROTransaction {
44
+ /**
45
+ * Adds the data to the store under the given key.
46
+ * @param key The key to add the data under.
47
+ * @param data The data to add to the store.
48
+ * @param overwrite If 'true', overwrite any existing data. If 'false',
49
+ * avoids storing the data if the key exists.
50
+ * @return True if storage succeeded, false otherwise.
51
+ */
52
+ put(key: string, data: Buffer, overwrite: boolean): boolean;
53
+ /**
54
+ * Deletes the data at the given key.
55
+ * @param key The key to delete from the store.
56
+ */
57
+ del(key: string): void;
58
+ /**
59
+ * Commits the transaction.
60
+ */
61
+ commit(): void;
62
+ /**
63
+ * Aborts and rolls back the transaction.
64
+ */
65
+ abort(): void;
66
+ }
67
+ /**
68
+ * An interface for simple synchronous key-value stores that don't have special
69
+ * support for transactions and such.
70
+ */
71
+ export interface SimpleSyncStore {
72
+ get(key: string): Buffer | undefined;
73
+ put(key: string, data: Buffer, overwrite: boolean): boolean;
74
+ del(key: string): void;
75
+ }
76
+ /**
77
+ * A simple RW transaction for simple synchronous key-value stores.
78
+ */
79
+ export declare class SimpleSyncRWTransaction implements SyncKeyValueRWTransaction {
80
+ private store;
81
+ /**
82
+ * Stores data in the keys we modify prior to modifying them.
83
+ * Allows us to roll back commits.
84
+ */
85
+ private originalData;
86
+ /**
87
+ * List of keys modified in this transaction, if any.
88
+ */
89
+ private modifiedKeys;
90
+ constructor(store: SimpleSyncStore);
91
+ get(key: string): Buffer | undefined;
92
+ put(key: string, data: Buffer, overwrite: boolean): boolean;
93
+ del(key: string): void;
94
+ commit(): void;
95
+ abort(): void;
96
+ private _has;
97
+ /**
98
+ * Stashes given key value pair into `originalData` if it doesn't already
99
+ * exist. Allows us to stash values the program is requesting anyway to
100
+ * prevent needless `get` requests if the program modifies the data later
101
+ * on during the transaction.
102
+ */
103
+ private stashOldValue;
104
+ /**
105
+ * Marks the given key as modified, and stashes its value if it has not been
106
+ * stashed already.
107
+ */
108
+ private markModified;
109
+ }
110
+ export interface SyncKeyValueFileSystemOptions {
111
+ /**
112
+ * The actual key-value store to read from/write to.
113
+ */
114
+ store: SyncKeyValueStore;
115
+ /**
116
+ * Should the file system support properties (mtime/atime/ctime/chmod/etc)?
117
+ * Enabling this slightly increases the storage space per file, and adds
118
+ * atime updates every time a file is accessed, mtime updates every time
119
+ * a file is modified, and permission checks on every operation.
120
+ *
121
+ * Defaults to *false*.
122
+ */
123
+ supportProps?: boolean;
124
+ /**
125
+ * Should the file system support links?
126
+ */
127
+ supportLinks?: boolean;
128
+ }
129
+ export declare class SyncKeyValueFile extends PreloadFile<SyncKeyValueFileSystem> implements File {
130
+ constructor(_fs: SyncKeyValueFileSystem, _path: string, _flag: FileFlag, _stat: Stats, contents?: Buffer);
131
+ syncSync(): void;
132
+ closeSync(): void;
133
+ }
134
+ /**
135
+ * A "Synchronous key-value file system". Stores data to/retrieves data from an
136
+ * underlying key-value store.
137
+ *
138
+ * We use a unique ID for each node in the file system. The root node has a
139
+ * fixed ID.
140
+ * @todo Introduce Node ID caching.
141
+ * @todo Check modes.
142
+ */
143
+ export declare class SyncKeyValueFileSystem extends SynchronousFileSystem {
144
+ static isAvailable(): boolean;
145
+ private store;
146
+ constructor(options: SyncKeyValueFileSystemOptions);
147
+ getName(): string;
148
+ isReadOnly(): boolean;
149
+ supportsSymlinks(): boolean;
150
+ supportsProps(): boolean;
151
+ supportsSynch(): boolean;
152
+ /**
153
+ * Delete all contents stored in the file system.
154
+ */
155
+ empty(): void;
156
+ accessSync(p: string, mode: number, cred: Cred): void;
157
+ renameSync(oldPath: string, newPath: string, cred: Cred): void;
158
+ statSync(p: string, cred: Cred): Stats;
159
+ createFileSync(p: string, flag: FileFlag, mode: number, cred: Cred): File;
160
+ openFileSync(p: string, flag: FileFlag, cred: Cred): File;
161
+ unlinkSync(p: string, cred: Cred): void;
162
+ rmdirSync(p: string, cred: Cred): void;
163
+ mkdirSync(p: string, mode: number, cred: Cred): void;
164
+ readdirSync(p: string, cred: Cred): string[];
165
+ chmodSync(p: string, mode: number, cred: Cred): void;
166
+ chownSync(p: string, new_uid: number, new_gid: number, cred: Cred): void;
167
+ _syncSync(p: string, data: Buffer, stats: Stats): void;
168
+ /**
169
+ * Checks if the root directory exists. Creates it if it doesn't.
170
+ */
171
+ private makeRootDirectory;
172
+ /**
173
+ * Helper function for findINode.
174
+ * @param parent The parent directory of the file we are attempting to find.
175
+ * @param filename The filename of the inode we are attempting to find, minus
176
+ * the parent.
177
+ * @return string The ID of the file's inode in the file system.
178
+ */
179
+ private _findINode;
180
+ /**
181
+ * Finds the Inode of the given path.
182
+ * @param p The path to look up.
183
+ * @return The Inode of the path p.
184
+ * @todo memoize/cache
185
+ */
186
+ private findINode;
187
+ /**
188
+ * Given the ID of a node, retrieves the corresponding Inode.
189
+ * @param tx The transaction to use.
190
+ * @param p The corresponding path to the file (used for error messages).
191
+ * @param id The ID to look up.
192
+ */
193
+ private getINode;
194
+ /**
195
+ * Given the Inode of a directory, retrieves the corresponding directory
196
+ * listing.
197
+ */
198
+ private getDirListing;
199
+ /**
200
+ * Creates a new node under a random ID. Retries 5 times before giving up in
201
+ * the exceedingly unlikely chance that we try to reuse a random GUID.
202
+ * @return The GUID that the data was stored under.
203
+ */
204
+ private addNewNode;
205
+ /**
206
+ * Commits a new file (well, a FILE or a DIRECTORY) to the file system with
207
+ * the given mode.
208
+ * Note: This will commit the transaction.
209
+ * @param p The path to the new file.
210
+ * @param type The type of the new file.
211
+ * @param mode The mode to create the new file with.
212
+ * @param data The data to store at the file's data node.
213
+ * @return The Inode for the new file.
214
+ */
215
+ private commitNewFile;
216
+ /**
217
+ * Remove all traces of the given path from the file system.
218
+ * @param p The path to remove from the file system.
219
+ * @param isDir Does the path belong to a directory, or a file?
220
+ * @todo Update mtime.
221
+ */
222
+ private removeEntry;
223
+ }
@@ -0,0 +1,479 @@
1
+ import * as path from 'path';
2
+ import { ApiError, ErrorCode } from '../ApiError';
3
+ import { W_OK, R_OK } from '../emulation/constants';
4
+ import { FileFlag, PreloadFile } from '../file';
5
+ import { SynchronousFileSystem } from '../filesystem';
6
+ import Inode from '../inode';
7
+ import { FileType } from '../stats';
8
+ import { randomUUID, getEmptyDirNode, ROOT_NODE_ID } from '../utils';
9
+ /**
10
+ * A simple RW transaction for simple synchronous key-value stores.
11
+ */
12
+ export class SimpleSyncRWTransaction {
13
+ constructor(store) {
14
+ this.store = store;
15
+ /**
16
+ * Stores data in the keys we modify prior to modifying them.
17
+ * Allows us to roll back commits.
18
+ */
19
+ this.originalData = {};
20
+ /**
21
+ * List of keys modified in this transaction, if any.
22
+ */
23
+ this.modifiedKeys = [];
24
+ }
25
+ get(key) {
26
+ const val = this.store.get(key);
27
+ this.stashOldValue(key, val);
28
+ return val;
29
+ }
30
+ put(key, data, overwrite) {
31
+ this.markModified(key);
32
+ return this.store.put(key, data, overwrite);
33
+ }
34
+ del(key) {
35
+ this.markModified(key);
36
+ this.store.del(key);
37
+ }
38
+ commit() {
39
+ /* NOP */
40
+ }
41
+ abort() {
42
+ // Rollback old values.
43
+ for (const key of this.modifiedKeys) {
44
+ const value = this.originalData[key];
45
+ if (!value) {
46
+ // Key didn't exist.
47
+ this.store.del(key);
48
+ }
49
+ else {
50
+ // Key existed. Store old value.
51
+ this.store.put(key, value, true);
52
+ }
53
+ }
54
+ }
55
+ _has(key) {
56
+ return Object.prototype.hasOwnProperty.call(this.originalData, key);
57
+ }
58
+ /**
59
+ * Stashes given key value pair into `originalData` if it doesn't already
60
+ * exist. Allows us to stash values the program is requesting anyway to
61
+ * prevent needless `get` requests if the program modifies the data later
62
+ * on during the transaction.
63
+ */
64
+ stashOldValue(key, value) {
65
+ // Keep only the earliest value in the transaction.
66
+ if (!this._has(key)) {
67
+ this.originalData[key] = value;
68
+ }
69
+ }
70
+ /**
71
+ * Marks the given key as modified, and stashes its value if it has not been
72
+ * stashed already.
73
+ */
74
+ markModified(key) {
75
+ if (this.modifiedKeys.indexOf(key) === -1) {
76
+ this.modifiedKeys.push(key);
77
+ if (!this._has(key)) {
78
+ this.originalData[key] = this.store.get(key);
79
+ }
80
+ }
81
+ }
82
+ }
83
+ export class SyncKeyValueFile extends PreloadFile {
84
+ constructor(_fs, _path, _flag, _stat, contents) {
85
+ super(_fs, _path, _flag, _stat, contents);
86
+ }
87
+ syncSync() {
88
+ if (this.isDirty()) {
89
+ this._fs._syncSync(this.getPath(), this.getBuffer(), this.getStats());
90
+ this.resetDirty();
91
+ }
92
+ }
93
+ closeSync() {
94
+ this.syncSync();
95
+ }
96
+ }
97
+ /**
98
+ * A "Synchronous key-value file system". Stores data to/retrieves data from an
99
+ * underlying key-value store.
100
+ *
101
+ * We use a unique ID for each node in the file system. The root node has a
102
+ * fixed ID.
103
+ * @todo Introduce Node ID caching.
104
+ * @todo Check modes.
105
+ */
106
+ export class SyncKeyValueFileSystem extends SynchronousFileSystem {
107
+ static isAvailable() {
108
+ return true;
109
+ }
110
+ constructor(options) {
111
+ super();
112
+ this.store = options.store;
113
+ // INVARIANT: Ensure that the root exists.
114
+ this.makeRootDirectory();
115
+ }
116
+ getName() {
117
+ return this.store.name();
118
+ }
119
+ isReadOnly() {
120
+ return false;
121
+ }
122
+ supportsSymlinks() {
123
+ return false;
124
+ }
125
+ supportsProps() {
126
+ return true;
127
+ }
128
+ supportsSynch() {
129
+ return true;
130
+ }
131
+ /**
132
+ * Delete all contents stored in the file system.
133
+ */
134
+ empty() {
135
+ this.store.clear();
136
+ // INVARIANT: Root always exists.
137
+ this.makeRootDirectory();
138
+ }
139
+ accessSync(p, mode, cred) {
140
+ const tx = this.store.beginTransaction('readonly'), node = this.findINode(tx, p);
141
+ if (!node.toStats().hasAccess(mode, cred)) {
142
+ throw ApiError.EACCES(p);
143
+ }
144
+ }
145
+ renameSync(oldPath, newPath, cred) {
146
+ const tx = this.store.beginTransaction('readwrite'), oldParent = path.dirname(oldPath), oldName = path.basename(oldPath), newParent = path.dirname(newPath), newName = path.basename(newPath),
147
+ // Remove oldPath from parent's directory listing.
148
+ oldDirNode = this.findINode(tx, oldParent), oldDirList = this.getDirListing(tx, oldParent, oldDirNode);
149
+ if (!oldDirNode.toStats().hasAccess(W_OK, cred)) {
150
+ throw ApiError.EACCES(oldPath);
151
+ }
152
+ if (!oldDirList[oldName]) {
153
+ throw ApiError.ENOENT(oldPath);
154
+ }
155
+ const nodeId = oldDirList[oldName];
156
+ delete oldDirList[oldName];
157
+ // Invariant: Can't move a folder inside itself.
158
+ // This funny little hack ensures that the check passes only if oldPath
159
+ // is a subpath of newParent. We append '/' to avoid matching folders that
160
+ // are a substring of the bottom-most folder in the path.
161
+ if ((newParent + '/').indexOf(oldPath + '/') === 0) {
162
+ throw new ApiError(ErrorCode.EBUSY, oldParent);
163
+ }
164
+ // Add newPath to parent's directory listing.
165
+ let newDirNode, newDirList;
166
+ if (newParent === oldParent) {
167
+ // Prevent us from re-grabbing the same directory listing, which still
168
+ // contains oldName.
169
+ newDirNode = oldDirNode;
170
+ newDirList = oldDirList;
171
+ }
172
+ else {
173
+ newDirNode = this.findINode(tx, newParent);
174
+ newDirList = this.getDirListing(tx, newParent, newDirNode);
175
+ }
176
+ if (newDirList[newName]) {
177
+ // If it's a file, delete it.
178
+ const newNameNode = this.getINode(tx, newPath, newDirList[newName]);
179
+ if (newNameNode.isFile()) {
180
+ try {
181
+ tx.del(newNameNode.id);
182
+ tx.del(newDirList[newName]);
183
+ }
184
+ catch (e) {
185
+ tx.abort();
186
+ throw e;
187
+ }
188
+ }
189
+ else {
190
+ // If it's a directory, throw a permissions error.
191
+ throw ApiError.EPERM(newPath);
192
+ }
193
+ }
194
+ newDirList[newName] = nodeId;
195
+ // Commit the two changed directory listings.
196
+ try {
197
+ tx.put(oldDirNode.id, Buffer.from(JSON.stringify(oldDirList)), true);
198
+ tx.put(newDirNode.id, Buffer.from(JSON.stringify(newDirList)), true);
199
+ }
200
+ catch (e) {
201
+ tx.abort();
202
+ throw e;
203
+ }
204
+ tx.commit();
205
+ }
206
+ statSync(p, cred) {
207
+ // Get the inode to the item, convert it into a Stats object.
208
+ const stats = this.findINode(this.store.beginTransaction('readonly'), p).toStats();
209
+ if (!stats.hasAccess(R_OK, cred)) {
210
+ throw ApiError.EACCES(p);
211
+ }
212
+ return stats;
213
+ }
214
+ createFileSync(p, flag, mode, cred) {
215
+ const tx = this.store.beginTransaction('readwrite'), data = Buffer.alloc(0), newFile = this.commitNewFile(tx, p, FileType.FILE, mode, cred, data);
216
+ // Open the file.
217
+ return new SyncKeyValueFile(this, p, flag, newFile.toStats(), data);
218
+ }
219
+ openFileSync(p, flag, cred) {
220
+ const tx = this.store.beginTransaction('readonly'), node = this.findINode(tx, p), data = tx.get(node.id);
221
+ if (!node.toStats().hasAccess(flag.getMode(), cred)) {
222
+ throw ApiError.EACCES(p);
223
+ }
224
+ if (data === undefined) {
225
+ throw ApiError.ENOENT(p);
226
+ }
227
+ return new SyncKeyValueFile(this, p, flag, node.toStats(), data);
228
+ }
229
+ unlinkSync(p, cred) {
230
+ this.removeEntry(p, false, cred);
231
+ }
232
+ rmdirSync(p, cred) {
233
+ // Check first if directory is empty.
234
+ if (this.readdirSync(p, cred).length > 0) {
235
+ throw ApiError.ENOTEMPTY(p);
236
+ }
237
+ else {
238
+ this.removeEntry(p, true, cred);
239
+ }
240
+ }
241
+ mkdirSync(p, mode, cred) {
242
+ const tx = this.store.beginTransaction('readwrite'), data = Buffer.from('{}');
243
+ this.commitNewFile(tx, p, FileType.DIRECTORY, mode, cred, data);
244
+ }
245
+ readdirSync(p, cred) {
246
+ const tx = this.store.beginTransaction('readonly');
247
+ const node = this.findINode(tx, p);
248
+ if (!node.toStats().hasAccess(R_OK, cred)) {
249
+ throw ApiError.EACCES(p);
250
+ }
251
+ return Object.keys(this.getDirListing(tx, p, node));
252
+ }
253
+ chmodSync(p, mode, cred) {
254
+ const fd = this.openFileSync(p, FileFlag.getFileFlag('r+'), cred);
255
+ fd.chmodSync(mode);
256
+ }
257
+ chownSync(p, new_uid, new_gid, cred) {
258
+ const fd = this.openFileSync(p, FileFlag.getFileFlag('r+'), cred);
259
+ fd.chownSync(new_uid, new_gid);
260
+ }
261
+ _syncSync(p, data, stats) {
262
+ // @todo Ensure mtime updates properly, and use that to determine if a data
263
+ // update is required.
264
+ const tx = this.store.beginTransaction('readwrite'),
265
+ // We use the _findInode helper because we actually need the INode id.
266
+ fileInodeId = this._findINode(tx, path.dirname(p), path.basename(p)), fileInode = this.getINode(tx, p, fileInodeId), inodeChanged = fileInode.update(stats);
267
+ try {
268
+ // Sync data.
269
+ tx.put(fileInode.id, data, true);
270
+ // Sync metadata.
271
+ if (inodeChanged) {
272
+ tx.put(fileInodeId, fileInode.toBuffer(), true);
273
+ }
274
+ }
275
+ catch (e) {
276
+ tx.abort();
277
+ throw e;
278
+ }
279
+ tx.commit();
280
+ }
281
+ /**
282
+ * Checks if the root directory exists. Creates it if it doesn't.
283
+ */
284
+ makeRootDirectory() {
285
+ const tx = this.store.beginTransaction('readwrite');
286
+ if (tx.get(ROOT_NODE_ID) === undefined) {
287
+ // Create new inode.
288
+ const currTime = new Date().getTime(),
289
+ // Mode 0666, owned by root:root
290
+ dirInode = new Inode(randomUUID(), 4096, 511 | FileType.DIRECTORY, currTime, currTime, currTime, 0, 0);
291
+ // If the root doesn't exist, the first random ID shouldn't exist,
292
+ // either.
293
+ tx.put(dirInode.id, getEmptyDirNode(), false);
294
+ tx.put(ROOT_NODE_ID, dirInode.toBuffer(), false);
295
+ tx.commit();
296
+ }
297
+ }
298
+ /**
299
+ * Helper function for findINode.
300
+ * @param parent The parent directory of the file we are attempting to find.
301
+ * @param filename The filename of the inode we are attempting to find, minus
302
+ * the parent.
303
+ * @return string The ID of the file's inode in the file system.
304
+ */
305
+ _findINode(tx, parent, filename, visited = new Set()) {
306
+ const currentPath = path.posix.join(parent, filename);
307
+ if (visited.has(currentPath)) {
308
+ throw new ApiError(ErrorCode.EIO, 'Infinite loop detected while finding inode', currentPath);
309
+ }
310
+ visited.add(currentPath);
311
+ const readDirectory = (inode) => {
312
+ // Get the root's directory listing.
313
+ const dirList = this.getDirListing(tx, parent, inode);
314
+ // Get the file's ID.
315
+ if (dirList[filename]) {
316
+ return dirList[filename];
317
+ }
318
+ else {
319
+ throw ApiError.ENOENT(path.resolve(parent, filename));
320
+ }
321
+ };
322
+ if (parent === '.') {
323
+ parent = process.cwd();
324
+ }
325
+ if (parent === '/') {
326
+ if (filename === '') {
327
+ // BASE CASE #1: Return the root's ID.
328
+ return ROOT_NODE_ID;
329
+ }
330
+ else {
331
+ // BASE CASE #2: Find the item in the root node.
332
+ return readDirectory(this.getINode(tx, parent, ROOT_NODE_ID));
333
+ }
334
+ }
335
+ else {
336
+ return readDirectory(this.getINode(tx, parent + path.sep + filename, this._findINode(tx, path.dirname(parent), path.basename(parent), visited)));
337
+ }
338
+ }
339
+ /**
340
+ * Finds the Inode of the given path.
341
+ * @param p The path to look up.
342
+ * @return The Inode of the path p.
343
+ * @todo memoize/cache
344
+ */
345
+ findINode(tx, p) {
346
+ return this.getINode(tx, p, this._findINode(tx, path.dirname(p), path.basename(p)));
347
+ }
348
+ /**
349
+ * Given the ID of a node, retrieves the corresponding Inode.
350
+ * @param tx The transaction to use.
351
+ * @param p The corresponding path to the file (used for error messages).
352
+ * @param id The ID to look up.
353
+ */
354
+ getINode(tx, p, id) {
355
+ const inode = tx.get(id);
356
+ if (inode === undefined) {
357
+ throw ApiError.ENOENT(p);
358
+ }
359
+ return Inode.fromBuffer(inode);
360
+ }
361
+ /**
362
+ * Given the Inode of a directory, retrieves the corresponding directory
363
+ * listing.
364
+ */
365
+ getDirListing(tx, p, inode) {
366
+ if (!inode.isDirectory()) {
367
+ throw ApiError.ENOTDIR(p);
368
+ }
369
+ const data = tx.get(inode.id);
370
+ if (data === undefined) {
371
+ throw ApiError.ENOENT(p);
372
+ }
373
+ return JSON.parse(data.toString());
374
+ }
375
+ /**
376
+ * Creates a new node under a random ID. Retries 5 times before giving up in
377
+ * the exceedingly unlikely chance that we try to reuse a random GUID.
378
+ * @return The GUID that the data was stored under.
379
+ */
380
+ addNewNode(tx, data) {
381
+ const retries = 0;
382
+ let currId;
383
+ while (retries < 5) {
384
+ try {
385
+ currId = randomUUID();
386
+ tx.put(currId, data, false);
387
+ return currId;
388
+ }
389
+ catch (e) {
390
+ // Ignore and reroll.
391
+ }
392
+ }
393
+ throw new ApiError(ErrorCode.EIO, 'Unable to commit data to key-value store.');
394
+ }
395
+ /**
396
+ * Commits a new file (well, a FILE or a DIRECTORY) to the file system with
397
+ * the given mode.
398
+ * Note: This will commit the transaction.
399
+ * @param p The path to the new file.
400
+ * @param type The type of the new file.
401
+ * @param mode The mode to create the new file with.
402
+ * @param data The data to store at the file's data node.
403
+ * @return The Inode for the new file.
404
+ */
405
+ commitNewFile(tx, p, type, mode, cred, data) {
406
+ const parentDir = path.dirname(p), fname = path.basename(p), parentNode = this.findINode(tx, parentDir), dirListing = this.getDirListing(tx, parentDir, parentNode), currTime = new Date().getTime();
407
+ //Check that the creater has correct access
408
+ if (!parentNode.toStats().hasAccess(0b0100 /* Write */, cred)) {
409
+ throw ApiError.EACCES(p);
410
+ }
411
+ // Invariant: The root always exists.
412
+ // If we don't check this prior to taking steps below, we will create a
413
+ // file with name '' in root should p == '/'.
414
+ if (p === '/') {
415
+ throw ApiError.EEXIST(p);
416
+ }
417
+ // Check if file already exists.
418
+ if (dirListing[fname]) {
419
+ throw ApiError.EEXIST(p);
420
+ }
421
+ let fileNode;
422
+ try {
423
+ // Commit data.
424
+ const dataId = this.addNewNode(tx, data);
425
+ fileNode = new Inode(dataId, data.length, mode | type, currTime, currTime, currTime, cred.uid, cred.gid);
426
+ // Commit file node.
427
+ const fileNodeId = this.addNewNode(tx, fileNode.toBuffer());
428
+ // Update and commit parent directory listing.
429
+ dirListing[fname] = fileNodeId;
430
+ tx.put(parentNode.id, Buffer.from(JSON.stringify(dirListing)), true);
431
+ }
432
+ catch (e) {
433
+ tx.abort();
434
+ throw e;
435
+ }
436
+ tx.commit();
437
+ return fileNode;
438
+ }
439
+ /**
440
+ * Remove all traces of the given path from the file system.
441
+ * @param p The path to remove from the file system.
442
+ * @param isDir Does the path belong to a directory, or a file?
443
+ * @todo Update mtime.
444
+ */
445
+ removeEntry(p, isDir, cred) {
446
+ const tx = this.store.beginTransaction('readwrite'), parent = path.dirname(p), parentNode = this.findINode(tx, parent), parentListing = this.getDirListing(tx, parent, parentNode), fileName = path.basename(p);
447
+ if (!parentListing[fileName]) {
448
+ throw ApiError.ENOENT(p);
449
+ }
450
+ const fileNodeId = parentListing[fileName];
451
+ // Get file inode.
452
+ const fileNode = this.getINode(tx, p, fileNodeId);
453
+ if (!fileNode.toStats().hasAccess(W_OK, cred)) {
454
+ throw ApiError.EACCES(p);
455
+ }
456
+ // Remove from directory listing of parent.
457
+ delete parentListing[fileName];
458
+ if (!isDir && fileNode.isDirectory()) {
459
+ throw ApiError.EISDIR(p);
460
+ }
461
+ else if (isDir && !fileNode.isDirectory()) {
462
+ throw ApiError.ENOTDIR(p);
463
+ }
464
+ try {
465
+ // Delete data.
466
+ tx.del(fileNode.id);
467
+ // Delete node.
468
+ tx.del(fileNodeId);
469
+ // Update directory listing.
470
+ tx.put(parentNode.id, Buffer.from(JSON.stringify(parentListing)), true);
471
+ }
472
+ catch (e) {
473
+ tx.abort();
474
+ throw e;
475
+ }
476
+ // Success.
477
+ tx.commit();
478
+ }
479
+ }