@loom-io/in-memory-adapter
Version:
A file system wrapper for Node.js and Bun
242 lines (241 loc) • 8.99 kB
JavaScript
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;
}
}
}