UNPKG

@ezdevlol/memfs

Version:

In-memory file-system with Node's fs API.

446 lines (445 loc) 11.9 kB
import process from './process'; import { Buffer, bufferAllocUnsafe, bufferFrom } from './internal/buffer'; import { constants } from './constants'; import { EventEmitter } from 'events'; import Stats from './Stats'; const { S_IFMT, S_IFDIR, S_IFREG, S_IFLNK, S_IFCHR, O_APPEND } = constants; const getuid = () => process.getuid?.() ?? 0; const getgid = () => process.getgid?.() ?? 0; export const SEP = '/'; /** * Node in a file system (like i-node, v-node). */ export class Node extends EventEmitter { // i-node number. ino; // User ID and group ID. _uid = getuid(); _gid = getgid(); _atime = new Date(); _mtime = new Date(); _ctime = new Date(); // data: string = ''; buf; rdev = 0; mode; // S_IFDIR, S_IFREG, etc.. // Number of hard links pointing at this Node. _nlink = 1; // Path to another node, if this is a symlink. symlink; constructor(ino, mode = 0o666) { super(); this.mode = mode; this.ino = ino; } set ctime(ctime) { this._ctime = ctime; } get ctime() { return this._ctime; } set uid(uid) { this._uid = uid; this.ctime = new Date(); } get uid() { return this._uid; } set gid(gid) { this._gid = gid; this.ctime = new Date(); } get gid() { return this._gid; } set atime(atime) { this._atime = atime; this.ctime = new Date(); } get atime() { return this._atime; } set mtime(mtime) { this._mtime = mtime; this.ctime = new Date(); } get mtime() { return this._mtime; } get perm() { return this.mode & ~S_IFMT; } set perm(perm) { this.mode = (this.mode & S_IFMT) | (perm & ~S_IFMT); this.ctime = new Date(); } set nlink(nlink) { this._nlink = nlink; this.ctime = new Date(); } get nlink() { return this._nlink; } getString(encoding = 'utf8') { this.atime = new Date(); return this.getBuffer().toString(encoding); } setString(str) { // this.setBuffer(bufferFrom(str, 'utf8')); this.buf = bufferFrom(str, 'utf8'); this.touch(); } getBuffer() { this.atime = new Date(); if (!this.buf) this.setBuffer(bufferAllocUnsafe(0)); return bufferFrom(this.buf); // Return a copy. } setBuffer(buf) { this.buf = bufferFrom(buf); // Creates a copy of data. this.touch(); } getSize() { return this.buf ? this.buf.length : 0; } setModeProperty(property) { this.mode = property; } isFile() { return (this.mode & S_IFMT) === S_IFREG; } isDirectory() { return (this.mode & S_IFMT) === S_IFDIR; } isSymlink() { // return !!this.symlink; return (this.mode & S_IFMT) === S_IFLNK; } isCharacterDevice() { return (this.mode & S_IFMT) === S_IFCHR; } makeSymlink(symlink) { this.mode = S_IFLNK | 0o666; this.symlink = symlink; } write(buf, off = 0, len = buf.length, pos = 0) { if (!this.buf) this.buf = bufferAllocUnsafe(0); if (pos + len > this.buf.length) { const newBuf = bufferAllocUnsafe(pos + len); this.buf.copy(newBuf, 0, 0, this.buf.length); this.buf = newBuf; } buf.copy(this.buf, pos, off, off + len); this.touch(); return len; } // Returns the number of bytes read. read(buf, off = 0, len = buf.byteLength, pos = 0) { this.atime = new Date(); if (!this.buf) this.buf = bufferAllocUnsafe(0); if (pos >= this.buf.length) return 0; let actualLen = len; if (actualLen > buf.byteLength) { actualLen = buf.byteLength; } if (actualLen + pos > this.buf.length) { actualLen = this.buf.length - pos; } const buf2 = buf instanceof Buffer ? buf : Buffer.from(buf.buffer); this.buf.copy(buf2, off, pos, pos + actualLen); return actualLen; } truncate(len = 0) { if (!len) this.buf = bufferAllocUnsafe(0); else { if (!this.buf) this.buf = bufferAllocUnsafe(0); if (len <= this.buf.length) { this.buf = this.buf.slice(0, len); } else { const buf = bufferAllocUnsafe(len); this.buf.copy(buf); buf.fill(0, this.buf.length); this.buf = buf; } } this.touch(); } chmod(perm) { this.mode = (this.mode & S_IFMT) | (perm & ~S_IFMT); this.touch(); } chown(uid, gid) { this.uid = uid; this.gid = gid; this.touch(); } touch() { this.mtime = new Date(); this.emit('change', this); } canRead(uid = getuid(), gid = getgid()) { if (this.perm & 4 /* S.IROTH */) { return true; } if (gid === this.gid) { if (this.perm & 32 /* S.IRGRP */) { return true; } } if (uid === this.uid) { if (this.perm & 256 /* S.IRUSR */) { return true; } } return false; } canWrite(uid = getuid(), gid = getgid()) { if (this.perm & 2 /* S.IWOTH */) { return true; } if (gid === this.gid) { if (this.perm & 16 /* S.IWGRP */) { return true; } } if (uid === this.uid) { if (this.perm & 128 /* S.IWUSR */) { return true; } } return false; } canExecute(uid = getuid(), gid = getgid()) { if (this.perm & 1 /* S.IXOTH */) { return true; } if (gid === this.gid) { if (this.perm & 8 /* S.IXGRP */) { return true; } } if (uid === this.uid) { if (this.perm & 64 /* S.IXUSR */) { return true; } } return false; } del() { this.emit('delete', this); } toJSON() { return { ino: this.ino, uid: this.uid, gid: this.gid, atime: this.atime.getTime(), mtime: this.mtime.getTime(), ctime: this.ctime.getTime(), perm: this.perm, mode: this.mode, nlink: this.nlink, symlink: this.symlink, data: this.getString(), }; } } /** * Represents a hard link that points to an i-node `node`. */ export class Link extends EventEmitter { vol; parent; children = new Map(); // Path to this node as Array: ['usr', 'bin', 'node']. _steps = []; // "i-node" of this hard link. node; // "i-node" number of the node. ino = 0; // Number of children. length = 0; name; get steps() { return this._steps; } // Recursively sync children steps, e.g. in case of dir rename set steps(val) { this._steps = val; for (const [child, link] of this.children.entries()) { if (child === '.' || child === '..') { continue; } link?.syncSteps(); } } constructor(vol, parent, name) { super(); this.vol = vol; this.parent = parent; this.name = name; this.syncSteps(); } setNode(node) { this.node = node; this.ino = node.ino; } getNode() { return this.node; } createChild(name, node = this.vol.createNode(S_IFREG | 0o666)) { const link = new Link(this.vol, this, name); link.setNode(node); if (node.isDirectory()) { link.children.set('.', link); link.getNode().nlink++; } this.setChild(name, link); return link; } setChild(name, link = new Link(this.vol, this, name)) { this.children.set(name, link); link.parent = this; this.length++; const node = link.getNode(); if (node.isDirectory()) { link.children.set('..', this); this.getNode().nlink++; } this.getNode().mtime = new Date(); this.emit('child:add', link, this); return link; } deleteChild(link) { const node = link.getNode(); if (node.isDirectory()) { link.children.delete('..'); this.getNode().nlink--; } this.children.delete(link.getName()); this.length--; this.getNode().mtime = new Date(); this.emit('child:delete', link, this); } getChild(name) { this.getNode().mtime = new Date(); return this.children.get(name); } getPath() { return this.steps.join(SEP); } getParentPath() { return this.steps.slice(0, -1).join(SEP); } getName() { return this.steps[this.steps.length - 1]; } // del() { // const parent = this.parent; // if(parent) { // parent.deleteChild(link); // } // this.parent = null; // this.vol = null; // } toJSON() { return { steps: this.steps, ino: this.ino, children: Array.from(this.children.keys()), }; } syncSteps() { this.steps = this.parent ? this.parent.steps.concat([this.name]) : [this.name]; } } /** * Represents an open file (file descriptor) that points to a `Link` (Hard-link) and a `Node`. */ export class File { fd; /** * Hard link that this file opened. * @type {any} */ link; /** * Reference to a `Node`. * @type {Node} */ node; /** * A cursor/offset position in a file, where data will be written on write. * User can "seek" this position. */ position; // Flags used when opening the file. flags; /** * Open a Link-Node pair. `node` is provided separately as that might be a different node * rather the one `link` points to, because it might be a symlink. * @param link * @param node * @param flags * @param fd */ constructor(link, node, flags, fd) { this.link = link; this.node = node; this.flags = flags; this.fd = fd; this.position = 0; if (this.flags & O_APPEND) this.position = this.getSize(); } getString(encoding = 'utf8') { return this.node.getString(); } setString(str) { this.node.setString(str); } getBuffer() { return this.node.getBuffer(); } setBuffer(buf) { this.node.setBuffer(buf); } getSize() { return this.node.getSize(); } truncate(len) { this.node.truncate(len); } seekTo(position) { this.position = position; } stats() { return Stats.build(this.node); } write(buf, offset = 0, length = buf.length, position) { if (typeof position !== 'number') position = this.position; const bytes = this.node.write(buf, offset, length, position); this.position = position + bytes; return bytes; } read(buf, offset = 0, length = buf.byteLength, position) { if (typeof position !== 'number') position = this.position; const bytes = this.node.read(buf, offset, length, position); this.position = position + bytes; return bytes; } chmod(perm) { this.node.chmod(perm); } chown(uid, gid) { this.node.chown(uid, gid); } }