UNPKG

@loom-io/in-memory-adapter

Version:

A file system wrapper for Node.js and Bun

242 lines (241 loc) 8.99 kB
import { FileHandler } from './file-handler.js'; import { PathNotFoundException, DirectoryNotEmptyException } from '@loom-io/core'; import { AlreadyExistsException, NotFoundException } from '../exceptions.js'; import { ObjectDirent } from './object-dirent.js'; import { MEMORY_TYPE } from '../definitions.js'; import { isMemoryDirectoryAndMatchNamePrepared } from '../utils/validations.js'; import { removePrecedingAndTrailingSlash, splitTailingPath } from '@loom-io/common'; import { basename, dirname, sep } from 'node:path'; export class Adapter { storage; constructor() { this.storage = { $type: MEMORY_TYPE.ROOT, content: [] }; } compareNameAndType(item, name, type) { const isFile = (item) => item.$type === MEMORY_TYPE.FILE && item.name === name; const isDir = (item) => item.$type === MEMORY_TYPE.DIRECTORY && item.name === name; if (type === MEMORY_TYPE.FILE) { return isFile(item); } else if (type === MEMORY_TYPE.DIRECTORY) { return isDir(item); } else { return isFile(item) || isDir(item); } } getLastPartOfPath(path, ref) { path = path?.trim(); if (path === undefined || path === '' || path === sep || path === '.') { return this.storage; } const parts = removePrecedingAndTrailingSlash(path).split(sep); const lastPart = parts.pop(); if (lastPart === undefined) { return this.storage; } let currentStack = this.storage.content; let lastObject = this.storage; for (let x = 0; x < parts.length; x++) { const part = parts[x]; const element = currentStack.find(isMemoryDirectoryAndMatchNamePrepared(part)); if (element === undefined) { throw new NotFoundException(path, lastObject, x); } else { currentStack = element.content; lastObject = element; } } const searchResult = currentStack.find((item) => this.compareNameAndType(item, lastPart, ref)); if (searchResult === undefined) { throw new NotFoundException(path, lastObject, parts.length); } return searchResult; } createMissingDirectories(ref, pathParts) { const currentPart = pathParts.shift(); if (currentPart === undefined) { return ref; } const newDir = this.createDirectory(currentPart); ref.content.push(newDir); return this.createMissingDirectories(newDir, pathParts); } createDirectory(name, content = []) { return { $type: MEMORY_TYPE.DIRECTORY, name, content }; } createFile(name, content = Buffer.alloc(0)) { const parts = name.split('.'); const ext = parts.length > 1 ? parts[parts.length - 1] : undefined; return { $type: MEMORY_TYPE.FILE, ext, name, mtime: new Date(), birthtime: new Date(), content }; } createObject(path, ref) { const parts = removePrecedingAndTrailingSlash(path).split(sep); try { const lastPart = this.getLastPartOfPath(path); throw new AlreadyExistsException(path, lastPart); } catch (err) { if (err instanceof NotFoundException) { const { last, depth } = err; if (ref === MEMORY_TYPE.FILE) { const lastDir = this.createMissingDirectories(last, parts.slice(depth, -1)); const file = this.createFile(parts[parts.length - 1]); lastDir.content.push(file); return file; } else { return this.createMissingDirectories(last, parts.slice(depth)); } } throw err; } } exists(path, ref) { try { this.getLastPartOfPath(path, ref); return true; } catch { return false; } } fileExists(path) { return this.exists(path, MEMORY_TYPE.FILE); } dirExists(path) { return this.exists(path, MEMORY_TYPE.DIRECTORY); } mkdir(path) { path = path.trim(); if (path === sep || path === '') { return; } try { this.createObject(path, MEMORY_TYPE.DIRECTORY); } catch (err) { if (err instanceof AlreadyExistsException) { return; } throw err; } } readdir(path) { const dir = this.getLastPartOfPath(path, MEMORY_TYPE.DIRECTORY); return dir.content.map((item) => new ObjectDirent(item, path.toString())); } rmdir(path, options = {}) { try { const [subPath, tail] = splitTailingPath(removePrecedingAndTrailingSlash(path)); const subElement = this.getLastPartOfPath(subPath, MEMORY_TYPE.DIRECTORY); if (!tail) { // Handle root directory if (subElement.content.length > 0 && (!options.recursive && !options.force)) { throw new DirectoryNotEmptyException(path); } subElement.content = []; return; } if ((!options.recursive && !options.force)) { const element = subElement.content.find(isMemoryDirectoryAndMatchNamePrepared(tail)); if (element == null) { throw new PathNotFoundException(path); } else if (element.content.length > 0) { throw new DirectoryNotEmptyException(path); } } subElement.content = subElement.content.filter((item) => !this.compareNameAndType(item, tail, MEMORY_TYPE.DIRECTORY)); } catch (err) { if (err instanceof NotFoundException) { throw new PathNotFoundException(path); } throw err; } } async stat(path) { const file = this.getLastPartOfPath(path, MEMORY_TYPE.FILE); return { size: file.content.length, mtime: file.mtime, birthtime: file.birthtime, }; } readFile(path, encoding) { const file = this.getLastPartOfPath(path, MEMORY_TYPE.FILE); return encoding === undefined ? file.content : file.content.toString(encoding); } writeFile(path, content, encoding = 'utf-8') { try { const file = this.getLastPartOfPath(path, MEMORY_TYPE.FILE); file.content = Buffer.isBuffer(content) ? content : Buffer.from(content, encoding); file.mtime = new Date(); } catch (err) { if (err instanceof NotFoundException) { if (removePrecedingAndTrailingSlash(path).split(sep).length === err.depth + 1) { const [, tail] = splitTailingPath(path); if (tail === undefined) { throw new Error('Invalid path'); // TODO: Create a custom exception } const file = this.createFile(tail, Buffer.isBuffer(content) ? content : Buffer.from(content, encoding)); err.last.content.push(file); return; } throw new PathNotFoundException(path); } throw err; } } deleteFile(path) { const [subPath, tail] = splitTailingPath(path); if (tail === undefined) { throw new Error('Invalid path'); // TODO: Create a custom exception } const subElement = this.getLastPartOfPath(subPath, MEMORY_TYPE.DIRECTORY); subElement.content = subElement.content.filter((item) => !this.compareNameAndType(item, tail, MEMORY_TYPE.FILE)); } openFile(path, mode = 'r') { const file = this.getLastPartOfPath(path, MEMORY_TYPE.FILE); return new FileHandler(file, mode); } isCopyable(adapter) { if (adapter instanceof Adapter) { return adapter === this; } return false; } copyFile(from, to) { let fromExists = false; try { const file = this.getLastPartOfPath(from, MEMORY_TYPE.FILE); fromExists = true; const target = dirname(to); const newFileName = basename(to); const subElement = this.getLastPartOfPath(target, MEMORY_TYPE.DIRECTORY); subElement.content.push(this.createFile(newFileName, file.content)); } catch (err) { if (err instanceof NotFoundException) { throw new PathNotFoundException(fromExists ? dirname(to) : from); } throw err; } } }