@jsvfs/core
Version:
The JavaScript Virtual File System - an extensible engine for file operations.
291 lines • 11.1 kB
JavaScript
"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