UNPKG

@codesandbox/sandpack-client

Version:

<img style="width:100%" src="https://user-images.githubusercontent.com/4838076/143581035-ebee5ba2-9cb1-4fe8-a05b-2f44bd69bb4b.gif" alt="Component toolkit for live running code editing experiences" />

1,209 lines 48.9 kB
"use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.AsyncKeyValueFileSystem = exports.AsyncKeyValueFile = exports.SyncKeyValueFileSystem = exports.SyncKeyValueFile = exports.SimpleSyncRWTransaction = void 0; var file_system_1 = require("../core/file_system"); var api_error_1 = require("../core/api_error"); var node_fs_stats_1 = require("../core/node_fs_stats"); var path = require("path"); var inode_1 = require("../generic/inode"); var preload_file_1 = require("../generic/preload_file"); var util_1 = require("../core/util"); /** * @hidden */ var ROOT_NODE_ID = "/"; /** * @hidden */ var emptyDirNode = null; /** * Returns an empty directory node. * @hidden */ function getEmptyDirNode() { if (emptyDirNode) { return emptyDirNode; } return emptyDirNode = Buffer.from("{}"); } /** * Generates a random ID. * @hidden */ function GenerateRandomID() { // From http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0; var v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } /** * Helper function. Checks if 'e' is defined. If so, it triggers the callback * with 'e' and returns false. Otherwise, returns true. * @hidden */ function noError(e, cb) { if (e) { cb(e); return false; } return true; } /** * Helper function. Checks if 'e' is defined. If so, it aborts the transaction, * triggers the callback with 'e', and returns false. Otherwise, returns true. * @hidden */ function noErrorTx(e, tx, cb) { if (e) { tx.abort(function () { cb(e); }); return false; } return true; } var LRUNode = /** @class */ (function () { function LRUNode(key, value) { this.key = key; this.value = value; this.prev = null; this.next = null; } return LRUNode; }()); // Adapted from https://chrisrng.svbtle.com/lru-cache-in-javascript var LRUCache = /** @class */ (function () { function LRUCache(limit) { this.limit = limit; this.size = 0; this.map = {}; this.head = null; this.tail = null; } /** * Change or add a new value in the cache * We overwrite the entry if it already exists */ LRUCache.prototype.set = function (key, value) { var node = new LRUNode(key, value); if (this.map[key]) { this.map[key].value = node.value; this.remove(node.key); } else { if (this.size >= this.limit) { delete this.map[this.tail.key]; this.size--; this.tail = this.tail.prev; this.tail.next = null; } } this.setHead(node); }; /* Retrieve a single entry from the cache */ LRUCache.prototype.get = function (key) { if (this.map[key]) { var value = this.map[key].value; var node = new LRUNode(key, value); this.remove(key); this.setHead(node); return value; } else { return null; } }; /* Remove a single entry from the cache */ LRUCache.prototype.remove = function (key) { var node = this.map[key]; if (!node) { return; } if (node.prev !== null) { node.prev.next = node.next; } else { this.head = node.next; } if (node.next !== null) { node.next.prev = node.prev; } else { this.tail = node.prev; } delete this.map[key]; this.size--; }; /* Resets the entire cache - Argument limit is optional to be reset */ LRUCache.prototype.removeAll = function () { this.size = 0; this.map = {}; this.head = null; this.tail = null; }; LRUCache.prototype.setHead = function (node) { node.next = this.head; node.prev = null; if (this.head !== null) { this.head.prev = node; } this.head = node; if (this.tail === null) { this.tail = node; } this.size++; this.map[node.key] = node; }; return LRUCache; }()); /** * A simple RW transaction for simple synchronous key-value stores. */ var SimpleSyncRWTransaction = /** @class */ (function () { function SimpleSyncRWTransaction(store) { this.store = store; /** * Stores data in the keys we modify prior to modifying them. * Allows us to roll back commits. */ this.originalData = {}; /** * List of keys modified in this transaction, if any. */ this.modifiedKeys = []; } SimpleSyncRWTransaction.prototype.get = function (key) { var val = this.store.get(key); this.stashOldValue(key, val); return val; }; SimpleSyncRWTransaction.prototype.put = function (key, data, overwrite) { this.markModified(key); return this.store.put(key, data, overwrite); }; SimpleSyncRWTransaction.prototype.del = function (key) { this.markModified(key); this.store.del(key); }; SimpleSyncRWTransaction.prototype.commit = function () { }; SimpleSyncRWTransaction.prototype.abort = function () { // Rollback old values. for (var _i = 0, _a = this.modifiedKeys; _i < _a.length; _i++) { var key = _a[_i]; var value = this.originalData[key]; if (!value) { // Key didn't exist. this.store.del(key); } else { // Key existed. Store old value. this.store.put(key, value, true); } } }; /** * Stashes given key value pair into `originalData` if it doesn't already * exist. Allows us to stash values the program is requesting anyway to * prevent needless `get` requests if the program modifies the data later * on during the transaction. */ SimpleSyncRWTransaction.prototype.stashOldValue = function (key, value) { // Keep only the earliest value in the transaction. if (!this.originalData.hasOwnProperty(key)) { this.originalData[key] = value; } }; /** * Marks the given key as modified, and stashes its value if it has not been * stashed already. */ SimpleSyncRWTransaction.prototype.markModified = function (key) { if (this.modifiedKeys.indexOf(key) === -1) { this.modifiedKeys.push(key); if (!this.originalData.hasOwnProperty(key)) { this.originalData[key] = this.store.get(key); } } }; return SimpleSyncRWTransaction; }()); exports.SimpleSyncRWTransaction = SimpleSyncRWTransaction; var SyncKeyValueFile = /** @class */ (function (_super) { __extends(SyncKeyValueFile, _super); function SyncKeyValueFile(_fs, _path, _flag, _stat, contents) { return _super.call(this, _fs, _path, _flag, _stat, contents) || this; } SyncKeyValueFile.prototype.syncSync = function () { if (this.isDirty()) { this._fs._syncSync(this.getPath(), this.getBuffer(), this.getStats()); this.resetDirty(); } }; SyncKeyValueFile.prototype.closeSync = function () { this.syncSync(); }; return SyncKeyValueFile; }(preload_file_1.default)); exports.SyncKeyValueFile = SyncKeyValueFile; /** * A "Synchronous key-value file system". Stores data to/retrieves data from an * underlying key-value store. * * We use a unique ID for each node in the file system. The root node has a * fixed ID. * @todo Introduce Node ID caching. * @todo Check modes. */ var SyncKeyValueFileSystem = /** @class */ (function (_super) { __extends(SyncKeyValueFileSystem, _super); function SyncKeyValueFileSystem(options) { var _this = _super.call(this) || this; _this.store = options.store; // INVARIANT: Ensure that the root exists. _this.makeRootDirectory(); return _this; } SyncKeyValueFileSystem.isAvailable = function () { return true; }; SyncKeyValueFileSystem.prototype.getName = function () { return this.store.name(); }; SyncKeyValueFileSystem.prototype.isReadOnly = function () { return false; }; SyncKeyValueFileSystem.prototype.supportsSymlinks = function () { return false; }; SyncKeyValueFileSystem.prototype.supportsProps = function () { return false; }; SyncKeyValueFileSystem.prototype.supportsSynch = function () { return true; }; /** * Delete all contents stored in the file system. */ SyncKeyValueFileSystem.prototype.empty = function () { this.store.clear(); // INVARIANT: Root always exists. this.makeRootDirectory(); }; SyncKeyValueFileSystem.prototype.renameSync = function (oldPath, newPath) { var tx = this.store.beginTransaction('readwrite'), oldParent = path.dirname(oldPath), oldName = path.basename(oldPath), newParent = path.dirname(newPath), newName = path.basename(newPath), // Remove oldPath from parent's directory listing. oldDirNode = this.findINode(tx, oldParent), oldDirList = this.getDirListing(tx, oldParent, oldDirNode); if (!oldDirList[oldName]) { throw api_error_1.ApiError.ENOENT(oldPath); } var nodeId = oldDirList[oldName]; delete oldDirList[oldName]; // Invariant: Can't move a folder inside itself. // This funny little hack ensures that the check passes only if oldPath // is a subpath of newParent. We append '/' to avoid matching folders that // are a substring of the bottom-most folder in the path. if ((newParent + '/').indexOf(oldPath + '/') === 0) { throw new api_error_1.ApiError(api_error_1.ErrorCode.EBUSY, oldParent); } // Add newPath to parent's directory listing. var newDirNode, newDirList; if (newParent === oldParent) { // Prevent us from re-grabbing the same directory listing, which still // contains oldName. newDirNode = oldDirNode; newDirList = oldDirList; } else { newDirNode = this.findINode(tx, newParent); newDirList = this.getDirListing(tx, newParent, newDirNode); } if (newDirList[newName]) { // If it's a file, delete it. var newNameNode = this.getINode(tx, newPath, newDirList[newName]); if (newNameNode.isFile()) { try { tx.del(newNameNode.id); tx.del(newDirList[newName]); } catch (e) { tx.abort(); throw e; } } else { // If it's a directory, throw a permissions error. throw api_error_1.ApiError.EPERM(newPath); } } newDirList[newName] = nodeId; // Commit the two changed directory listings. try { tx.put(oldDirNode.id, Buffer.from(JSON.stringify(oldDirList)), true); tx.put(newDirNode.id, Buffer.from(JSON.stringify(newDirList)), true); } catch (e) { tx.abort(); throw e; } tx.commit(); }; SyncKeyValueFileSystem.prototype.statSync = function (p, isLstat) { // Get the inode to the item, convert it into a Stats object. return this.findINode(this.store.beginTransaction('readonly'), p).toStats(); }; SyncKeyValueFileSystem.prototype.createFileSync = function (p, flag, mode) { var tx = this.store.beginTransaction('readwrite'), data = (0, util_1.emptyBuffer)(), newFile = this.commitNewFile(tx, p, node_fs_stats_1.FileType.FILE, mode, data); // Open the file. return new SyncKeyValueFile(this, p, flag, newFile.toStats(), data); }; SyncKeyValueFileSystem.prototype.openFileSync = function (p, flag) { var tx = this.store.beginTransaction('readonly'), node = this.findINode(tx, p), data = tx.get(node.id); if (data === undefined) { throw api_error_1.ApiError.ENOENT(p); } return new SyncKeyValueFile(this, p, flag, node.toStats(), data); }; SyncKeyValueFileSystem.prototype.unlinkSync = function (p) { this.removeEntry(p, false); }; SyncKeyValueFileSystem.prototype.rmdirSync = function (p) { // Check first if directory is empty. if (this.readdirSync(p).length > 0) { throw api_error_1.ApiError.ENOTEMPTY(p); } else { this.removeEntry(p, true); } }; SyncKeyValueFileSystem.prototype.mkdirSync = function (p, mode) { var tx = this.store.beginTransaction('readwrite'), data = Buffer.from('{}'); this.commitNewFile(tx, p, node_fs_stats_1.FileType.DIRECTORY, mode, data); }; SyncKeyValueFileSystem.prototype.readdirSync = function (p) { var tx = this.store.beginTransaction('readonly'); return Object.keys(this.getDirListing(tx, p, this.findINode(tx, p))); }; SyncKeyValueFileSystem.prototype._syncSync = function (p, data, stats) { // @todo Ensure mtime updates properly, and use that to determine if a data // update is required. var tx = this.store.beginTransaction('readwrite'), // We use the _findInode helper because we actually need the INode id. fileInodeId = this._findINode(tx, path.dirname(p), path.basename(p)), fileInode = this.getINode(tx, p, fileInodeId), inodeChanged = fileInode.update(stats); try { // Sync data. tx.put(fileInode.id, data, true); // Sync metadata. if (inodeChanged) { tx.put(fileInodeId, fileInode.toBuffer(), true); } } catch (e) { tx.abort(); throw e; } tx.commit(); }; /** * Checks if the root directory exists. Creates it if it doesn't. */ SyncKeyValueFileSystem.prototype.makeRootDirectory = function () { var tx = this.store.beginTransaction('readwrite'); if (tx.get(ROOT_NODE_ID) === undefined) { // Create new inode. var currTime = (new Date()).getTime(), // Mode 0666 dirInode = new inode_1.default(GenerateRandomID(), 4096, 511 | node_fs_stats_1.FileType.DIRECTORY, currTime, currTime, currTime); // If the root doesn't exist, the first random ID shouldn't exist, // either. tx.put(dirInode.id, getEmptyDirNode(), false); tx.put(ROOT_NODE_ID, dirInode.toBuffer(), false); tx.commit(); } }; /** * Helper function for findINode. * @param parent The parent directory of the file we are attempting to find. * @param filename The filename of the inode we are attempting to find, minus * the parent. * @return string The ID of the file's inode in the file system. */ SyncKeyValueFileSystem.prototype._findINode = function (tx, parent, filename) { var _this = this; var readDirectory = function (inode) { // Get the root's directory listing. var dirList = _this.getDirListing(tx, parent, inode); // Get the file's ID. if (dirList[filename]) { return dirList[filename]; } else { throw api_error_1.ApiError.ENOENT(path.resolve(parent, filename)); } }; if (parent === '/') { if (filename === '') { // BASE CASE #1: Return the root's ID. return ROOT_NODE_ID; } else { // BASE CASE #2: Find the item in the root ndoe. return readDirectory(this.getINode(tx, parent, ROOT_NODE_ID)); } } else { return readDirectory(this.getINode(tx, parent + path.sep + filename, this._findINode(tx, path.dirname(parent), path.basename(parent)))); } }; /** * Finds the Inode of the given path. * @param p The path to look up. * @return The Inode of the path p. * @todo memoize/cache */ SyncKeyValueFileSystem.prototype.findINode = function (tx, p) { return this.getINode(tx, p, this._findINode(tx, path.dirname(p), path.basename(p))); }; /** * Given the ID of a node, retrieves the corresponding Inode. * @param tx The transaction to use. * @param p The corresponding path to the file (used for error messages). * @param id The ID to look up. */ SyncKeyValueFileSystem.prototype.getINode = function (tx, p, id) { var inode = tx.get(id); if (inode === undefined) { throw api_error_1.ApiError.ENOENT(p); } return inode_1.default.fromBuffer(inode); }; /** * Given the Inode of a directory, retrieves the corresponding directory * listing. */ SyncKeyValueFileSystem.prototype.getDirListing = function (tx, p, inode) { if (!inode.isDirectory()) { throw api_error_1.ApiError.ENOTDIR(p); } var data = tx.get(inode.id); if (data === undefined) { throw api_error_1.ApiError.ENOENT(p); } return JSON.parse(data.toString()); }; /** * Creates a new node under a random ID. Retries 5 times before giving up in * the exceedingly unlikely chance that we try to reuse a random GUID. * @return The GUID that the data was stored under. */ SyncKeyValueFileSystem.prototype.addNewNode = function (tx, data) { var retries = 0; var currId; while (retries < 5) { try { currId = GenerateRandomID(); tx.put(currId, data, false); return currId; } catch (e) { // Ignore and reroll. } } throw new api_error_1.ApiError(api_error_1.ErrorCode.EIO, 'Unable to commit data to key-value store.'); }; /** * Commits a new file (well, a FILE or a DIRECTORY) to the file system with * the given mode. * Note: This will commit the transaction. * @param p The path to the new file. * @param type The type of the new file. * @param mode The mode to create the new file with. * @param data The data to store at the file's data node. * @return The Inode for the new file. */ SyncKeyValueFileSystem.prototype.commitNewFile = function (tx, p, type, mode, data) { var parentDir = path.dirname(p), fname = path.basename(p), parentNode = this.findINode(tx, parentDir), dirListing = this.getDirListing(tx, parentDir, parentNode), currTime = (new Date()).getTime(); // Invariant: The root always exists. // If we don't check this prior to taking steps below, we will create a // file with name '' in root should p == '/'. if (p === '/') { throw api_error_1.ApiError.EEXIST(p); } // Check if file already exists. if (dirListing[fname]) { throw api_error_1.ApiError.EEXIST(p); } var fileNode; try { // Commit data. var dataId = this.addNewNode(tx, data); fileNode = new inode_1.default(dataId, data.length, mode | type, currTime, currTime, currTime); // Commit file node. var fileNodeId = this.addNewNode(tx, fileNode.toBuffer()); // Update and commit parent directory listing. dirListing[fname] = fileNodeId; tx.put(parentNode.id, Buffer.from(JSON.stringify(dirListing)), true); } catch (e) { tx.abort(); throw e; } tx.commit(); return fileNode; }; /** * Remove all traces of the given path from the file system. * @param p The path to remove from the file system. * @param isDir Does the path belong to a directory, or a file? * @todo Update mtime. */ SyncKeyValueFileSystem.prototype.removeEntry = function (p, isDir) { var tx = this.store.beginTransaction('readwrite'), parent = path.dirname(p), parentNode = this.findINode(tx, parent), parentListing = this.getDirListing(tx, parent, parentNode), fileName = path.basename(p); if (!parentListing[fileName]) { throw api_error_1.ApiError.ENOENT(p); } // Remove from directory listing of parent. var fileNodeId = parentListing[fileName]; delete parentListing[fileName]; // Get file inode. var fileNode = this.getINode(tx, p, fileNodeId); if (!isDir && fileNode.isDirectory()) { throw api_error_1.ApiError.EISDIR(p); } else if (isDir && !fileNode.isDirectory()) { throw api_error_1.ApiError.ENOTDIR(p); } try { // Delete data. tx.del(fileNode.id); // Delete node. tx.del(fileNodeId); // Update directory listing. tx.put(parentNode.id, Buffer.from(JSON.stringify(parentListing)), true); } catch (e) { tx.abort(); throw e; } // Success. tx.commit(); }; return SyncKeyValueFileSystem; }(file_system_1.SynchronousFileSystem)); exports.SyncKeyValueFileSystem = SyncKeyValueFileSystem; var AsyncKeyValueFile = /** @class */ (function (_super) { __extends(AsyncKeyValueFile, _super); function AsyncKeyValueFile(_fs, _path, _flag, _stat, contents) { return _super.call(this, _fs, _path, _flag, _stat, contents) || this; } AsyncKeyValueFile.prototype.sync = function (cb) { var _this = this; if (this.isDirty()) { this._fs._sync(this.getPath(), this.getBuffer(), this.getStats(), function (e) { if (!e) { _this.resetDirty(); } cb(e); }); } else { cb(); } }; AsyncKeyValueFile.prototype.close = function (cb) { this.sync(cb); }; return AsyncKeyValueFile; }(preload_file_1.default)); exports.AsyncKeyValueFile = AsyncKeyValueFile; /** * An "Asynchronous key-value file system". Stores data to/retrieves data from * an underlying asynchronous key-value store. */ var AsyncKeyValueFileSystem = /** @class */ (function (_super) { __extends(AsyncKeyValueFileSystem, _super); function AsyncKeyValueFileSystem(cacheSize) { var _this = _super.call(this) || this; _this._cache = null; if (cacheSize > 0) { _this._cache = new LRUCache(cacheSize); } return _this; } AsyncKeyValueFileSystem.isAvailable = function () { return true; }; /** * Initializes the file system. Typically called by subclasses' async * constructors. */ AsyncKeyValueFileSystem.prototype.init = function (store, cb) { this.store = store; // INVARIANT: Ensure that the root exists. this.makeRootDirectory(cb); }; AsyncKeyValueFileSystem.prototype.getName = function () { return this.store.name(); }; AsyncKeyValueFileSystem.prototype.isReadOnly = function () { return false; }; AsyncKeyValueFileSystem.prototype.supportsSymlinks = function () { return false; }; AsyncKeyValueFileSystem.prototype.supportsProps = function () { return false; }; AsyncKeyValueFileSystem.prototype.supportsSynch = function () { return false; }; /** * Delete all contents stored in the file system. */ AsyncKeyValueFileSystem.prototype.empty = function (cb) { var _this = this; if (this._cache) { this._cache.removeAll(); } this.store.clear(function (e) { if (noError(e, cb)) { // INVARIANT: Root always exists. _this.makeRootDirectory(cb); } }); }; AsyncKeyValueFileSystem.prototype.rename = function (oldPath, newPath, cb) { var _this = this; // TODO: Make rename compatible with the cache. if (this._cache) { // Clear and disable cache during renaming process. var c_1 = this._cache; this._cache = null; c_1.removeAll(); var oldCb_1 = cb; cb = function (e) { // Restore empty cache. _this._cache = c_1; oldCb_1(e); }; } var tx = this.store.beginTransaction('readwrite'); var oldParent = path.dirname(oldPath), oldName = path.basename(oldPath); var newParent = path.dirname(newPath), newName = path.basename(newPath); var inodes = {}; var lists = {}; var errorOccurred = false; // Invariant: Can't move a folder inside itself. // This funny little hack ensures that the check passes only if oldPath // is a subpath of newParent. We append '/' to avoid matching folders that // are a substring of the bottom-most folder in the path. if ((newParent + '/').indexOf(oldPath + '/') === 0) { return cb(new api_error_1.ApiError(api_error_1.ErrorCode.EBUSY, oldParent)); } /** * Responsible for Phase 2 of the rename operation: Modifying and * committing the directory listings. Called once we have successfully * retrieved both the old and new parent's inodes and listings. */ var theOleSwitcharoo = function () { // Sanity check: Ensure both paths are present, and no error has occurred. if (errorOccurred || !lists.hasOwnProperty(oldParent) || !lists.hasOwnProperty(newParent)) { return; } var oldParentList = lists[oldParent], oldParentINode = inodes[oldParent], newParentList = lists[newParent], newParentINode = inodes[newParent]; // Delete file from old parent. if (!oldParentList[oldName]) { cb(api_error_1.ApiError.ENOENT(oldPath)); } else { var fileId_1 = oldParentList[oldName]; delete oldParentList[oldName]; // Finishes off the renaming process by adding the file to the new // parent. var completeRename_1 = function () { newParentList[newName] = fileId_1; // Commit old parent's list. tx.put(oldParentINode.id, Buffer.from(JSON.stringify(oldParentList)), true, function (e) { if (noErrorTx(e, tx, cb)) { if (oldParent === newParent) { // DONE! tx.commit(cb); } else { // Commit new parent's list. tx.put(newParentINode.id, Buffer.from(JSON.stringify(newParentList)), true, function (e) { if (noErrorTx(e, tx, cb)) { tx.commit(cb); } }); } } }); }; if (newParentList[newName]) { // 'newPath' already exists. Check if it's a file or a directory, and // act accordingly. _this.getINode(tx, newPath, newParentList[newName], function (e, inode) { if (noErrorTx(e, tx, cb)) { if (inode.isFile()) { // Delete the file and continue. tx.del(inode.id, function (e) { if (noErrorTx(e, tx, cb)) { tx.del(newParentList[newName], function (e) { if (noErrorTx(e, tx, cb)) { completeRename_1(); } }); } }); } else { // Can't overwrite a directory using rename. tx.abort(function (e) { cb(api_error_1.ApiError.EPERM(newPath)); }); } } }); } else { completeRename_1(); } } }; /** * Grabs a path's inode and directory listing, and shoves it into the * inodes and lists hashes. */ var processInodeAndListings = function (p) { _this.findINodeAndDirListing(tx, p, function (e, node, dirList) { if (e) { if (!errorOccurred) { errorOccurred = true; tx.abort(function () { cb(e); }); } // If error has occurred already, just stop here. } else { inodes[p] = node; lists[p] = dirList; theOleSwitcharoo(); } }); }; processInodeAndListings(oldParent); if (oldParent !== newParent) { processInodeAndListings(newParent); } }; AsyncKeyValueFileSystem.prototype.stat = function (p, isLstat, cb) { var tx = this.store.beginTransaction('readonly'); this.findINode(tx, p, function (e, inode) { if (noError(e, cb)) { cb(null, inode.toStats()); } }); }; AsyncKeyValueFileSystem.prototype.createFile = function (p, flag, mode, cb) { var _this = this; var tx = this.store.beginTransaction('readwrite'), data = (0, util_1.emptyBuffer)(); this.commitNewFile(tx, p, node_fs_stats_1.FileType.FILE, mode, data, function (e, newFile) { if (noError(e, cb)) { cb(null, new AsyncKeyValueFile(_this, p, flag, newFile.toStats(), data)); } }); }; AsyncKeyValueFileSystem.prototype.openFile = function (p, flag, cb) { var _this = this; var tx = this.store.beginTransaction('readonly'); // Step 1: Grab the file's inode. this.findINode(tx, p, function (e, inode) { if (noError(e, cb)) { // Step 2: Grab the file's data. tx.get(inode.id, function (e, data) { if (noError(e, cb)) { if (data === undefined) { cb(api_error_1.ApiError.ENOENT(p)); } else { cb(null, new AsyncKeyValueFile(_this, p, flag, inode.toStats(), data)); } } }); } }); }; AsyncKeyValueFileSystem.prototype.unlink = function (p, cb) { this.removeEntry(p, false, cb); }; AsyncKeyValueFileSystem.prototype.rmdir = function (p, cb) { var _this = this; // Check first if directory is empty. this.readdir(p, function (err, files) { if (err) { cb(err); } else if (files.length > 0) { cb(api_error_1.ApiError.ENOTEMPTY(p)); } else { _this.removeEntry(p, true, cb); } }); }; AsyncKeyValueFileSystem.prototype.mkdir = function (p, mode, cb) { var tx = this.store.beginTransaction('readwrite'), data = Buffer.from('{}'); this.commitNewFile(tx, p, node_fs_stats_1.FileType.DIRECTORY, mode, data, cb); }; AsyncKeyValueFileSystem.prototype.readdir = function (p, cb) { var _this = this; var tx = this.store.beginTransaction('readonly'); this.findINode(tx, p, function (e, inode) { if (noError(e, cb)) { _this.getDirListing(tx, p, inode, function (e, dirListing) { if (noError(e, cb)) { cb(null, Object.keys(dirListing)); } }); } }); }; AsyncKeyValueFileSystem.prototype._sync = function (p, data, stats, cb) { var _this = this; // @todo Ensure mtime updates properly, and use that to determine if a data // update is required. var tx = this.store.beginTransaction('readwrite'); // Step 1: Get the file node's ID. this._findINode(tx, path.dirname(p), path.basename(p), function (e, fileInodeId) { if (noErrorTx(e, tx, cb)) { // Step 2: Get the file inode. _this.getINode(tx, p, fileInodeId, function (e, fileInode) { if (noErrorTx(e, tx, cb)) { var inodeChanged_1 = fileInode.update(stats); // Step 3: Sync the data. tx.put(fileInode.id, data, true, function (e) { if (noErrorTx(e, tx, cb)) { // Step 4: Sync the metadata (if it changed)! if (inodeChanged_1) { tx.put(fileInodeId, fileInode.toBuffer(), true, function (e) { if (noErrorTx(e, tx, cb)) { tx.commit(cb); } }); } else { // No need to sync metadata; return. tx.commit(cb); } } }); } }); } }); }; /** * Checks if the root directory exists. Creates it if it doesn't. */ AsyncKeyValueFileSystem.prototype.makeRootDirectory = function (cb) { var tx = this.store.beginTransaction('readwrite'); tx.get(ROOT_NODE_ID, function (e, data) { if (e || data === undefined) { // Create new inode. var currTime = (new Date()).getTime(), // Mode 0666 dirInode_1 = new inode_1.default(GenerateRandomID(), 4096, 511 | node_fs_stats_1.FileType.DIRECTORY, currTime, currTime, currTime); // If the root doesn't exist, the first random ID shouldn't exist, // either. tx.put(dirInode_1.id, getEmptyDirNode(), false, function (e) { if (noErrorTx(e, tx, cb)) { tx.put(ROOT_NODE_ID, dirInode_1.toBuffer(), false, function (e) { if (e) { tx.abort(function () { cb(e); }); } else { tx.commit(cb); } }); } }); } else { // We're good. tx.commit(cb); } }); }; /** * Helper function for findINode. * @param parent The parent directory of the file we are attempting to find. * @param filename The filename of the inode we are attempting to find, minus * the parent. * @param cb Passed an error or the ID of the file's inode in the file system. */ AsyncKeyValueFileSystem.prototype._findINode = function (tx, parent, filename, cb) { var _this = this; if (this._cache) { var id = this._cache.get(path.join(parent, filename)); if (id) { return cb(null, id); } } var handleDirectoryListings = function (e, inode, dirList) { if (e) { cb(e); } else if (dirList[filename]) { var id = dirList[filename]; if (_this._cache) { _this._cache.set(path.join(parent, filename), id); } cb(null, id); } else { cb(api_error_1.ApiError.ENOENT(path.resolve(parent, filename))); } }; if (parent === '/') { if (filename === '') { // BASE CASE #1: Return the root's ID. if (this._cache) { this._cache.set(path.join(parent, filename), ROOT_NODE_ID); } cb(null, ROOT_NODE_ID); } else { // BASE CASE #2: Find the item in the root node. this.getINode(tx, parent, ROOT_NODE_ID, function (e, inode) { if (noError(e, cb)) { _this.getDirListing(tx, parent, inode, function (e, dirList) { // handle_directory_listings will handle e for us. handleDirectoryListings(e, inode, dirList); }); } }); } } else { // Get the parent directory's INode, and find the file in its directory // listing. this.findINodeAndDirListing(tx, parent, handleDirectoryListings); } }; /** * Finds the Inode of the given path. * @param p The path to look up. * @param cb Passed an error or the Inode of the path p. * @todo memoize/cache */ AsyncKeyValueFileSystem.prototype.findINode = function (tx, p, cb) { var _this = this; this._findINode(tx, path.dirname(p), path.basename(p), function (e, id) { if (noError(e, cb)) { _this.getINode(tx, p, id, cb); } }); }; /** * Given the ID of a node, retrieves the corresponding Inode. * @param tx The transaction to use. * @param p The corresponding path to the file (used for error messages). * @param id The ID to look up. * @param cb Passed an error or the inode under the given id. */ AsyncKeyValueFileSystem.prototype.getINode = function (tx, p, id, cb) { tx.get(id, function (e, data) { if (noError(e, cb)) { if (data === undefined) { cb(api_error_1.ApiError.ENOENT(p)); } else { cb(null, inode_1.default.fromBuffer(data)); } } }); }; /** * Given the Inode of a directory, retrieves the corresponding directory * listing. */ AsyncKeyValueFileSystem.prototype.getDirListing = function (tx, p, inode, cb) { if (!inode.isDirectory()) { cb(api_error_1.ApiError.ENOTDIR(p)); } else { tx.get(inode.id, function (e, data) { if (noError(e, cb)) { try { cb(null, JSON.parse(data.toString())); } catch (e) { // Occurs when data is undefined, or corresponds to something other // than a directory listing. The latter should never occur unless // the file system is corrupted. cb(api_error_1.ApiError.ENOENT(p)); } } }); } }; /** * Given a path to a directory, retrieves the corresponding INode and * directory listing. */ AsyncKeyValueFileSystem.prototype.findINodeAndDirListing = function (tx, p, cb) { var _this = this; this.findINode(tx, p, function (e, inode) { if (noError(e, cb)) { _this.getDirListing(tx, p, inode, function (e, listing) { if (noError(e, cb)) { cb(null, inode, listing); } }); } }); }; /** * Adds a new node under a random ID. Retries 5 times before giving up in * the exceedingly unlikely chance that we try to reuse a random GUID. * @param cb Passed an error or the GUID that the data was stored under. */ AsyncKeyValueFileSystem.prototype.addNewNode = function (tx, data, cb) { var retries = 0, currId; var reroll = function () { if (++retries === 5) { // Max retries hit. Return with an error. cb(new api_error_1.ApiError(api_error_1.ErrorCode.EIO, 'Unable to commit data to key-value store.')); } else { // Try again. currId = GenerateRandomID(); tx.put(currId, data, false, function (e, committed) { if (e || !committed) { reroll(); } else { // Successfully stored under 'currId'. cb(null, currId); } }); } }; reroll(); }; /** * Commits a new file (well, a FILE or a DIRECTORY) to the file system with * the given mode. * Note: This will commit the transaction. * @param p The path to the new file. * @param type The type of the new file. * @param mode The mode to create the new file with. * @param data The data to store at the file's data node. * @param cb Passed an error or the Inode for the new file. */ AsyncKeyValueFileSystem.prototype.commitNewFile = function (tx, p, type, mode, data, cb) { var _this = this; var parentDir = path.dirname(p), fname = path.basename(p), currTime = (new Date()).getTime(); // Invariant: The root always exists. // If we don't check this prior to taking steps below, we will create a // file with name '' in root should p == '/'. if (p === '/') { return cb(api_error_1.ApiError.EEXIST(p)); } // Let's build a pyramid of code! // Step 1: Get the parent directory's inode and directory listing this.findINodeAndDirListing(tx, parentDir, function (e, parentNode, dirListing) { if (noErrorTx(e, tx, cb)) { if (dirListing[fname]) { // File already exists. tx.abort(function () { cb(api_error_1.ApiError.EEXIST(p)); }); } else { // Step 2: Commit data to store. _this.addNewNode(tx, data, function (e, dataId) { if (noErrorTx(e, tx, cb)) { // Step 3: Commit the file's inode to the store. var fileInode_1 = new inode_1.default(dataId, data.length, mode | type, currTime, currTime, currTime); _this.addNewNode(tx, fileInode_1.toBuffer(), function (e, fileInodeId) { if (noErrorTx(e, tx, cb)) { // Step 4: Update parent directory's listing. dirListing[fname] = fileInodeId; tx.put(parentNode.id, Buffer.from(JSON.stringify(dirListing)), true, function (e) { if (noErrorTx(e, tx, cb)) { // Step 5: Commit and return the new inode. tx.commit(function (e) { if (noErrorTx(e, tx, cb)) { cb(null, fileInode_1); } }); } }); } }); } }); } } }); }; /** * Remove all traces of the given path from the file system. * @param p The path to remove from the file system. * @param isDir Does the path belong to a directory, or a file? * @todo Update mtime. */ AsyncKeyValueFileSystem.prototype.removeEntry = function (p, isDir, cb) { var _this = this; // Eagerly delete from cache (harmless even if removal fails) if (this._cache) { this._cache.remove(p); } var tx = this.store.beginTransaction('readwrite'), parent = path.dirname(p), fileName = path.basename(p); // Step 1: Get parent directory's node and directory listing. this.findINodeAndDirListing(tx, parent, function (e, parentNode, parentListing) { if (noErrorTx(e, tx, cb)) { if (!parentListing[fileName]) { tx.abort(function () { cb(api_error_1.ApiError.ENOENT(p)); }); } else { // Remove from directory listing of parent. var fileNodeId_1 = parentListing[fileName]; delete parentListing[fileName]; // Step 2: Get file inode. _this.getINode(tx, p, fileNodeId_1, function (e, fileNode) { if (noErrorTx(e, tx, cb)) { if (!isDir && fileNode.isDirectory()) { tx.abort(function () { cb(api_error_1.ApiError.EISDIR(p)); }); } else if (isDir && !fileNode.isDirectory()) { tx.abort(function () { cb(api_error_1.ApiError.ENOTDIR(p)); }); } else { // Step 3: Delete data. tx.del(fileNode.id, function (e) { if (noErrorTx(e, tx, cb)) { // Step 4: Delete node. tx.del(fileNodeId_1, function (e) { if (noErrorTx(e, tx, cb)) { // Step 5: Update directory listing. tx.put(parentNode.id, Buffer.from(JSON.stringify(parentListing)), true, function (e) { if (noErrorTx(e, tx, cb)) { tx.commit(cb); } }); } }); } }); } } }); } } }); }; return AsyncKeyValueFileSystem; }(file_system_1.BaseFileSystem)); exports.AsyncKeyValueFileSystem = AsyncKeyValueFileSystem;