UNPKG

@jsvfs/core

Version:

The JavaScript Virtual File System - an extensible engine for file operations.

291 lines 11.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.VirtualFileSystem = void 0; /* eslint-disable no-fallthrough */ const adapter_noop_1 = require("@jsvfs/adapter-noop"); const helpers_1 = require("./helpers"); const Item_1 = require("./Item"); /** Create a JavaScript virtual file system in memory. */ class VirtualFileSystem { /** @param {Adapter} [adapter] - The adapter for this instance; if none is provided, then it defaults to noop. */ constructor(adapter) { this.adapter = adapter; if (typeof this.adapter === 'undefined') { this.adapter = new adapter_noop_1.NoopAdapter(); } this.root = new Item_1.Root(this.adapter); this.rmCache = new Map(); } /** The separator character for this file system. */ get separator() { return helpers_1.SEPARATOR; } /** Determine if a given path is included in the rm cache. */ isRmCached(path) { const tree = helpers_1.destructure(path); let cachedPath = ''; for (const leaf of tree) { cachedPath = helpers_1.join(cachedPath, leaf); if (this.rmCache.has(cachedPath)) return true; } return false; } /** Add a given path to the rm cache. */ addRmCache(path, type) { this.rmCache.set(helpers_1.normalize(path), type); } /** * Read the contents of a file. If the adapter supports pass-through read, the file * will be read from persistent storage if it is not in the virtual dile system. */ async read(path) { try { const item = helpers_1.getItemAtPath(this.root, path); switch (item.type) { case 'file': return item.contents; default: throw new TypeError(`Expected a file, encountered a folder at path ${path}`); } } catch (error) { if (error instanceof ReferenceError && error.message.startsWith('Item does not exist at path') && typeof this.adapter.read === 'function') { const file = new Item_1.File({ adapter: this.adapter, contents: await this.adapter.read(path), name: helpers_1.basename(path), path }); helpers_1.setItemAtPath(this.root, file); return file.contents; } else { throw error; } } } /** Write the contents of a file; creating directories in the tree as needed. Optionally append to the file. */ write(path, contents, append = false) { if (typeof contents === 'undefined') { contents = Buffer.alloc(0); } else if (contents instanceof String || typeof contents === 'string') { contents = Buffer.from(contents, 'utf8'); } let item; try { item = helpers_1.getItemAtPath(this.root, path); } catch (error) { // Ignore errors; will create directory tree and encounter errors later as needed. } if (append && typeof item !== 'undefined') { switch (item.type) { case 'file': item.contents = Buffer.concat([item.contents, contents]); item.committed = false; break; default: throw new TypeError(`Expected a file, encountered a folder at path ${path}`); } } else { if (typeof item === 'object') { switch (item.type) { case 'file': item.contents = Buffer.from(contents); item.committed = false; break; default: throw new ReferenceError(`Item of type ${item.type} already exists at path ${path}`); } } else { helpers_1.setItemAtPath(this.root, new Item_1.File({ adapter: this.adapter, contents: Buffer.from(contents), name: helpers_1.basename(path), path })); } } if (this.isRmCached(path)) this.rmCache.delete(helpers_1.normalize(path)); } /** Delete a file; if the target is a hardlink, also deletes the link contents. Returns false if the item does not exist. */ delete(path) { let item; try { item = helpers_1.getItemAtPath(this.root, path, false); } catch (error) { } if (typeof item === 'undefined') { this.addRmCache(path, 'file'); return false; } switch (item.type) { case 'file': this.addRmCache(path, item.type); return item.parent.delete(item.name); case 'hardlink': if (item.contents.type === 'file') { this.addRmCache(path, item.type); this.rmCache.set(helpers_1.normalize(item.contents.path), item.contents.type); return item.parent.delete(item.name) && item.contents.parent.delete(item.contents.name); } case 'softlink': if (item.contents.type === 'file') { this.addRmCache(path, item.type); return item.parent.delete(item.name); } default: throw new TypeError(`Expected a file, encountered a folder at path ${path}`); } } /** Make a directory or directory tree; silently continues if path already exists. */ mkdir(path) { if (!this.exists(path)) { helpers_1.setItemAtPath(this.root, new Item_1.Folder({ adapter: this.adapter, name: helpers_1.basename(path), path })); if (this.isRmCached(path)) this.rmCache.delete(helpers_1.normalize(path)); } } readdir(path, long = false) { let item; try { item = helpers_1.getItemAtPath(this.root, path); } catch (error) { } if (typeof item === 'undefined') return []; switch (item.type) { case 'folder': case 'root': return item.list(long); default: throw new TypeError(`Expected a folder, encountered a file at path ${path}`); } } /** Remove a directory and it's contents. If the path is a folder link, both the link and the link target will be removed. */ rmdir(path) { let item; try { item = helpers_1.getItemAtPath(this.root, path, false); } catch (error) { } if (typeof item === 'undefined') { this.addRmCache(path, 'folder'); return false; } switch (item.type) { case 'folder': case 'root': this.addRmCache(path, item.type); return item.parent.delete(item.name); case 'hardlink': case 'softlink': switch (item.contents.type) { case 'folder': case 'root': this.addRmCache(path, item.type); this.rmCache.set(helpers_1.normalize(item.contents.path), item.contents.type); return item.parent.delete(item.name) && item.contents.parent.delete(item.contents.name); } default: throw new TypeError(`Expected a file, encountered a folder at path ${path}`); } } /** Create a link to a folder or file; optionally create as a hardlink. Returns false if the link cannot be created. */ link(from, to, type = 'softlink') { if (!this.exists(from)) { try { const item = helpers_1.getItemAtPath(this.root, to); helpers_1.setItemAtPath(this.root, new Item_1.Link({ adapter: this.adapter, contents: item, name: helpers_1.basename(from), path: from, type })); return true; } catch (error) { return false; } } return false; } /** Remove a link; returns false if not a link. Does not delete files, this is not a Node API. */ unlink(path) { let item; try { item = helpers_1.getItemAtPath(this.root, path, false); } catch (error) { } if (typeof item === 'undefined') { this.addRmCache(path, 'softlink'); return false; } switch (item.type) { case 'hardlink': case 'softlink': this.addRmCache(path, item.type); return item.parent.delete(item.name); } return false; } /** Check to see if a path exists in the virtual file system tree. */ exists(path) { let item; try { item = helpers_1.getItemAtPath(this.root, path, false); } catch (error) { // Ignore errors; just checking for existence. } return typeof item !== 'undefined'; } /** Prepare the file system with a snapshot of the underlying persistent file system. */ async snapshot() { const links = []; for await (const [path, data] of this.adapter.snapshot()) { switch (data.type) { case 'folder': this.mkdir(path); break; case 'file': this.write(path, data.contents); break; case 'hardlink': case 'softlink': // Save links until all "real" data has been written to memory. links.push([path, data]); break; } } if (links.length > 0) { for (const [path, data] of links) { this.link(path, data.contents, data.type); } } } /** Commit the current state of the file system to persistent storage. */ async commit() { await this.adapter.flush(); await this.root.commit(); for (const [path, type] of this.rmCache) { await this.adapter.remove(path, type); } } } exports.VirtualFileSystem = VirtualFileSystem; //# sourceMappingURL=VirtualFileSystem.js.map