import path from 'path'; import Stat from './stat'; import { SystemError } from './system-error'; import { BufferEncoding } from './utils'; interface FSFile { readonly type: 'file'; readonly ino: number; readonly mtimeMs: number; readonly name: string; readonly path: string; contents: string; } interface FSLink { readonly type: 'symlink'; readonly ino: number; readonly mtimeMs: number; readonly name: string; readonly path: string; readonly linkTo: string; } interface FSDir { readonly type: 'dir'; readonly ino: number; readonly mtimeMs: number; readonly name: string; readonly path: string; readonly children: (FSFile | FSDir | FSLink)[]; } type FSEntry = FSDir | FSFile | FSLink; export class MemClient { __fs: FSEntry; __ino: 0; static createClient() { return { promises: new MemClient(), }; } constructor() { this.__ino = 0; this.__fs = { type: 'dir', path: path.normalize('/'), name: '', children: [], ino: this.__ino, mtimeMs: Date.now(), }; } async tree(baseDir = '/') { baseDir = path.normalize(baseDir); const next = async (dir: string, toPrint: string) => { const entry = this._find(dir); if (!entry) { return toPrint; } const indent = new Array((dir.match(/\//g) || []).length).join('| '); if (entry.type === 'dir') { if (entry.path !== baseDir) { toPrint += `${indent}${entry.name}/\n`; } for (const name of await this.readdir(dir)) { toPrint = await next(path.join(dir, name), toPrint); } } else { toPrint += `${indent}${entry.name}\n`; } return toPrint; }; console.log(await next(baseDir, '')); } async readFile( filePath: string, options: BufferEncoding | { encoding?: BufferEncoding } = {}, ) { filePath = path.normalize(filePath); if (typeof options === 'string') { options = { encoding: options, }; } const encoding = options ? options.encoding : null; const entry = this._assertFile(filePath); const raw = Buffer.from(entry.contents, 'base64'); if (encoding) { return raw.toString(encoding); } else { return raw; } } async writeFile( filePath: string, data: Buffer | string, options: BufferEncoding | { encoding?: BufferEncoding, flag?: string; } = {}, ) { filePath = path.normalize(filePath); if (typeof options === 'string') { options = { encoding: options, }; } const flag = options && options.flag ? options.flag : 'w'; const encoding = options && options.encoding ? options.encoding : 'utf8'; // Make sure file doesn't exist for "x" flags if (flag[1] === 'x') { await this._assertDoesNotExist(filePath); } const dirEntry: FSDir = this._assertDir(path.dirname(filePath)); let file: FSEntry | null = this._find(filePath); if (file) { file = this._assertFileEntry(file); } else { const name = path.basename(filePath); file = { name, type: 'file', ino: this.__ino++, mtimeMs: Date.now(), contents: '', path: filePath, }; dirEntry.children.push(file); } const dataBuff: Buffer = data instanceof Buffer ? data : Buffer.from(data, encoding); let newContents = Buffer.alloc(0); if (flag[0] === 'w') { newContents = dataBuff; } else if (flag[0] === 'a') { const contentsBuff: Buffer = Buffer.from(file.contents, 'base64'); newContents = Buffer.concat([contentsBuff, dataBuff]); } else { throw new SystemError({ code: 'EBADF', errno: -9, message: 'EBADF: bad file descriptor, write', path: filePath, syscall: 'write', }); } file.contents = newContents.toString('base64'); return Promise.resolve(); } async unlink(filePath: string) { filePath = path.normalize(filePath); this._remove(this._assertFile(filePath)); } async readdir(basePath: string) { basePath = path.normalize(basePath); const entry = this._assertDir(basePath); const names = entry.children.map(c => c.name); names.sort(); return names; } async mkdir(dirPath: string, options?: { recursive?: boolean }) { dirPath = path.normalize(dirPath); const doRecursive = (options || {}).recursive || false; // If not recursive, ensure parent exists if (!doRecursive) { this._assertDir(path.dirname(dirPath)); } const pathSegments = dirPath.split(path.sep).filter(s => s !== ''); // Recurse over all sub paths, ensure they are all directories, // create them if they don't exist let currentPath = '/'; for (const pathSegment of pathSegments) { const dirEntry = this._assertDir(currentPath); const nextPath = path.join(currentPath, pathSegment); // Create dir if it doesn't exist yet if (!dirEntry.children.find(e => e.name === pathSegment)) { dirEntry.children.push({ type: 'dir', ino: this.__ino++, mtimeMs: Date.now(), name: pathSegment, path: nextPath, children: [], }); } currentPath = nextPath; } } async rmdir(dirPath: string) { dirPath = path.normalize(dirPath); const dirEntry = this._assertDir(dirPath); if (dirEntry.children.length > 0) { throw new SystemError({ code: 'ENOTEMPTY', errno: -66, message: `ENOTEMPTY: directory not empty, rmdir '${dirPath}'`, path: dirPath, syscall: 'rmdir', }); } this._remove(dirEntry); } async stat(filePath: string) { filePath = path.normalize(filePath); return this._statEntry(this._assertExists(filePath)); } async lstat(filePath: string) { filePath = path.normalize(filePath); const linkEntry = this._assertExists(filePath); return this._statEntry(this._resolveLinks(linkEntry)); } async readlink(filePath: string) { filePath = path.normalize(filePath); const linkEntry = this._assertSymlink(filePath); return linkEntry.linkTo; } async symlink(target: string, filePath: string) { filePath = path.normalize(filePath); // Make sure we don't already have one there // TODO: Check what to do in this case (might be wrong) this._assertDoesNotExist(filePath); this._assertExists(target); const parentEntry = this._assertDir(path.dirname(filePath)); parentEntry.children.push({ type: 'symlink', ino: this.__ino++, mtimeMs: Date.now(), name: path.basename(filePath), path: filePath, linkTo: target, }); } _statEntry(entry: FSEntry) { return new Stat({ type: entry.type, mode: 0o777, // @ts-expect-error -- TSCONVERSION size: entry.contents ? entry.contents.length : 0, ino: entry.ino, mtimeMs: entry.mtimeMs, }); } _find(filePath: string) { filePath = path.normalize(filePath); let current = this.__fs; // Ignore empty and current directory '.' segments const pathSegments = filePath.split(path.sep).filter(s => s !== '' && s !== '.'); for (const expectedName of pathSegments) { // @ts-expect-error -- TSCONVERSION const e = (current.children || []).find(c => c.name === expectedName); if (!e) { return null; } current = e; } // It's the root return current; } _assertDoesNotExist(filePath: string) { const entry = this._find(filePath); if (entry) { throw new SystemError({ code: 'EEXIST', errno: -17, message: `EEXIST: file already exists, open '${filePath}'`, path: filePath, syscall: 'open', }); } } _assertExists(filePath: string) { const entry = this._find(filePath); if (!entry) { throw new SystemError({ code: 'ENOENT', errno: -2, message: `ENOENT: no such file or directory, scandir '${filePath}'`, path: filePath, syscall: 'scandir', }); } return entry; } _assertDirEntry(entry: FSEntry) { if (entry.type !== 'dir') { throw new SystemError({ code: 'ENOTDIR', errno: -20, message: `ENOTDIR: not a directory, scandir '${entry.path}'`, path: entry.path, syscall: 'scandir', }); } return entry; } _assertDir(filePath: string) { const entry = this._assertExists(filePath); return this._assertDirEntry(entry); } _assertSymlinkEntry(entry: FSEntry) { if (entry.type !== 'symlink') { throw new SystemError({ code: 'ENOTDIR', errno: -20, message: `ENOTDIR: not a symlink, scandir '${entry.path}'`, path: entry.path, syscall: 'scandir', }); } return entry; } _assertSymlink(filePath: string) { const entry = this._assertExists(filePath); return this._assertSymlinkEntry(entry); } _resolveLinks(entry: FSEntry): FSFile | FSDir { if (entry.type === 'symlink') { const other = this._find(entry.linkTo); if (!other) { // Should never happen throw new Error('Failed to resolve link'); } return this._resolveLinks(other); } return entry; } _assertFileEntry(entry: FSEntry) { entry = this._resolveLinks(entry); if (entry.type === 'dir') { throw new SystemError({ code: 'EISDIR', errno: -21, message: `EISDIR: illegal operation on a directory '${entry.path}'`, path: entry.path, syscall: 'open', }); } return entry; } _assertFile(filePath: string) { const entry = this._assertExists(filePath); return this._assertFileEntry(entry); } _remove(entry: FSEntry) { const parentEntry = this._assertDir(path.dirname(entry.path)); const index = parentEntry.children.findIndex(c => c === entry); if (index < 0) { // Should never happen so w/e return; } parentEntry.children.splice(index, 1); } }