@firesystem/memory
Version:
In-memory implementation of Virtual File System
611 lines (610 loc) • 18 kB
JavaScript
// src/MemoryFileSystem.ts
import {
normalizePath,
dirname,
basename,
join,
TypedEventEmitter,
FileSystemEvents,
BaseFileSystem,
} from "@firesystem/core";
var MemoryFileSystem = class extends BaseFileSystem {
constructor(options) {
super();
this.files = /* @__PURE__ */ new Map();
this.watchers = [];
this.events = new TypedEventEmitter();
// Capabilities do sistema em memória
this.capabilities = {
readonly: false,
caseSensitive: true,
atomicRename: true,
supportsWatch: true,
supportsMetadata: true,
maxFileSize: 50 * 1024 * 1024,
// 50MB limite prático
maxPathLength: 1024,
};
this.files.set("/", {
path: "/",
type: "directory",
size: 0,
created: /* @__PURE__ */ new Date(),
modified: /* @__PURE__ */ new Date(),
parent: "",
});
if (options?.initialFiles) {
for (const file of options.initialFiles) {
const normalized = normalizePath(file.path);
this.files.set(normalized, {
path: normalized,
type: file.type,
content: file.content,
size: file.size || this.calculateSize(file.content),
created: file.created || /* @__PURE__ */ new Date(),
modified: file.modified || /* @__PURE__ */ new Date(),
metadata: file.metadata,
parent: dirname(normalized),
});
}
}
}
async initialize() {
const startTime = Date.now();
this.events.emit(FileSystemEvents.INITIALIZING, void 0);
try {
const allFiles = Array.from(this.files.values()).filter(
(f) => f.path !== "/",
);
const totalFiles = allFiles.length;
for (let i = 0; i < allFiles.length; i++) {
const file = allFiles[i];
if (file.type === "file") {
this.events.emit(FileSystemEvents.FILE_READ, {
path: file.path,
size: file.size,
});
} else {
const childCount = Array.from(this.files.values()).filter(
(f) => f.parent === file.path,
).length;
this.events.emit(FileSystemEvents.DIR_READ, {
path: file.path,
count: childCount,
});
}
this.events.emit(FileSystemEvents.INIT_PROGRESS, {
loaded: i + 1,
total: totalFiles,
phase: "loading-files",
});
}
this.events.emit(FileSystemEvents.INITIALIZED, {
duration: Date.now() - startTime,
});
} catch (error) {
this.events.emit(FileSystemEvents.INIT_ERROR, {
error,
});
throw error;
}
}
async readFile(path) {
const normalized = normalizePath(path);
const startTime = Date.now();
const operationId = `read-${normalized}-${startTime}`;
this.events.emit(FileSystemEvents.OPERATION_START, {
operation: "readFile",
path: normalized,
id: operationId,
});
this.events.emit(FileSystemEvents.FILE_READING, { path: normalized });
try {
const file = this.files.get(normalized);
if (!file) {
throw new Error(`ENOENT: no such file or directory, open '${path}'`);
}
if (file.type === "directory") {
throw new Error(
`EISDIR: illegal operation on a directory, read '${path}'`,
);
}
const result = {
path: file.path,
name: basename(file.path),
type: file.type,
size: file.size,
created: file.created,
modified: file.modified,
metadata: file.metadata,
content: file.content,
};
this.events.emit(FileSystemEvents.FILE_READ, {
path: normalized,
size: file.size,
});
this.events.emit(FileSystemEvents.OPERATION_END, {
operation: "readFile",
path: normalized,
id: operationId,
duration: Date.now() - startTime,
});
return result;
} catch (error) {
this.events.emit(FileSystemEvents.OPERATION_ERROR, {
operation: "readFile",
path: normalized,
error,
});
throw error;
}
}
async writeFile(path, content, metadata) {
const normalized = normalizePath(path);
const startTime = Date.now();
const operationId = `write-${normalized}-${startTime}`;
const size = this.calculateSize(content);
this.events.emit(FileSystemEvents.OPERATION_START, {
operation: "writeFile",
path: normalized,
id: operationId,
});
this.events.emit(FileSystemEvents.FILE_WRITING, { path: normalized, size });
try {
await this.ensureParentExists(normalized);
const existing = this.files.get(normalized);
const now = /* @__PURE__ */ new Date();
const file = {
path: normalized,
type: "file",
content,
size,
created: existing?.created || now,
modified: now,
metadata,
parent: dirname(normalized),
};
const isUpdate = !!existing;
this.files.set(normalized, file);
const result = {
path: file.path,
name: basename(file.path),
type: file.type,
size: file.size,
created: file.created,
modified: file.modified,
metadata: file.metadata,
content: file.content,
};
this.notifyWatchers({
type: isUpdate ? "updated" : "created",
path: normalized,
timestamp: now,
});
this.events.emit(FileSystemEvents.FILE_WRITTEN, {
path: normalized,
size,
});
this.events.emit(FileSystemEvents.OPERATION_END, {
operation: "writeFile",
path: normalized,
id: operationId,
duration: Date.now() - startTime,
});
return result;
} catch (error) {
this.events.emit(FileSystemEvents.OPERATION_ERROR, {
operation: "writeFile",
path: normalized,
error,
});
throw error;
}
}
async deleteFile(path) {
const normalized = normalizePath(path);
const file = this.files.get(normalized);
if (!file) {
throw new Error(`ENOENT: no such file or directory, unlink '${path}'`);
}
if (file.type === "directory") {
const hasChildren = Array.from(this.files.values()).some(
(f) => f.parent === normalized,
);
if (hasChildren) {
throw new Error(`ENOTEMPTY: directory not empty, rmdir '${path}'`);
}
}
this.files.delete(normalized);
this.notifyWatchers({
type: "deleted",
path: normalized,
timestamp: /* @__PURE__ */ new Date(),
});
}
async exists(path) {
const normalized = normalizePath(path);
return this.files.has(normalized);
}
async readDir(path) {
const normalized = normalizePath(path);
if (normalized !== "/" && !this.files.has(normalized)) {
throw new Error(`ENOENT: no such file or directory, scandir '${path}'`);
}
const entries = [];
for (const file of this.files.values()) {
if (file.parent === normalized) {
entries.push({
path: file.path,
name: basename(file.path),
type: file.type,
size: file.size,
created: file.created,
modified: file.modified,
metadata: file.metadata,
});
}
}
return entries;
}
async mkdir(path, recursive = false) {
const normalized = normalizePath(path);
if (normalized === "/") {
throw new Error(`EEXIST: file already exists, mkdir '${path}'`);
}
if (this.files.has(normalized)) {
throw new Error(`EEXIST: file already exists, mkdir '${path}'`);
}
const parent = dirname(normalized);
if (!recursive && parent !== "/" && !this.files.has(parent)) {
throw new Error(`ENOENT: no such file or directory, mkdir '${path}'`);
}
if (recursive && parent !== "/" && !this.files.has(parent)) {
await this.mkdir(parent, true);
}
const now = /* @__PURE__ */ new Date();
const dir = {
path: normalized,
type: "directory",
size: 0,
created: now,
modified: now,
parent: dirname(normalized),
};
this.files.set(normalized, dir);
this.notifyWatchers({
type: "created",
path: normalized,
timestamp: now,
});
return {
path: dir.path,
name: basename(dir.path),
type: dir.type,
size: 0,
created: now,
modified: now,
};
}
async rmdir(path, recursive = false) {
const normalized = normalizePath(path);
if (normalized === "/") {
throw new Error(`EBUSY: resource busy or locked, rmdir '${path}'`);
}
const file = this.files.get(normalized);
if (!file) {
throw new Error(`ENOENT: no such file or directory, rmdir '${path}'`);
}
if (file.type !== "directory") {
throw new Error(`ENOTDIR: not a directory, rmdir '${path}'`);
}
const contents = await this.readDir(normalized);
if (contents.length > 0 && !recursive) {
throw new Error(`ENOTEMPTY: directory not empty, rmdir '${path}'`);
}
if (recursive) {
await this.deleteRecursive(normalized);
}
this.files.delete(normalized);
this.notifyWatchers({
type: "deleted",
path: normalized,
timestamp: /* @__PURE__ */ new Date(),
});
}
async rename(oldPath, newPath) {
const oldNormalized = normalizePath(oldPath);
const newNormalized = normalizePath(newPath);
const file = this.files.get(oldNormalized);
if (!file) {
throw new Error(`ENOENT: no such file or directory, rename '${oldPath}'`);
}
await this.ensureParentExists(newNormalized);
if (this.files.has(newNormalized)) {
throw new Error(
`EEXIST: file already exists, rename '${oldPath}' -> '${newPath}'`,
);
}
const now = /* @__PURE__ */ new Date();
if (file.type === "directory") {
const toUpdate = [];
for (const [path, f] of this.files) {
if (path === oldNormalized || path.startsWith(oldNormalized + "/")) {
const newFilePath = path.replace(oldNormalized, newNormalized);
toUpdate.push([
path,
{
...f,
path: newFilePath,
parent: dirname(newFilePath),
modified: path === oldNormalized ? now : f.modified,
},
]);
}
}
for (const [oldPath2] of toUpdate) {
this.files.delete(oldPath2);
}
for (const [, newFile] of toUpdate) {
this.files.set(newFile.path, newFile);
}
const renamedDir = this.files.get(newNormalized);
this.notifyWatchers({
type: "renamed",
path: newNormalized,
oldPath: oldNormalized,
timestamp: now,
});
return {
path: renamedDir.path,
name: basename(renamedDir.path),
type: renamedDir.type,
size: renamedDir.size,
created: renamedDir.created,
modified: renamedDir.modified,
metadata: renamedDir.metadata,
};
} else {
const newFile = {
...file,
path: newNormalized,
parent: dirname(newNormalized),
modified: now,
};
this.files.delete(oldNormalized);
this.files.set(newNormalized, newFile);
this.notifyWatchers({
type: "renamed",
path: newNormalized,
oldPath: oldNormalized,
timestamp: now,
});
return {
path: newFile.path,
name: basename(newFile.path),
type: newFile.type,
size: newFile.size,
created: newFile.created,
modified: newFile.modified,
metadata: newFile.metadata,
content: newFile.content,
};
}
}
async move(sourcePaths, targetPath) {
const targetNormalized = normalizePath(targetPath);
const target = this.files.get(targetNormalized);
if (!target || target.type !== "directory") {
throw new Error(`ENOTDIR: not a directory, move to '${targetPath}'`);
}
for (const sourcePath of sourcePaths) {
const name = basename(sourcePath);
const newPath = join(targetNormalized, name);
await this.rename(sourcePath, newPath);
}
}
async copy(sourcePath, targetPath) {
const sourceNormalized = normalizePath(sourcePath);
const source = this.files.get(sourceNormalized);
if (!source) {
throw new Error(
`ENOENT: no such file or directory, open '${sourcePath}'`,
);
}
if (source.type === "directory") {
throw new Error(
`EISDIR: illegal operation on a directory, read '${sourcePath}'`,
);
}
return this.writeFile(targetPath, source.content, source.metadata);
}
watch(pattern, callback) {
const id = this.generateWatcherId();
const listener = { id, pattern, callback };
this.watchers.push(listener);
return {
dispose: () => {
const index = this.watchers.findIndex((w) => w.id === id);
if (index !== -1) {
this.watchers.splice(index, 1);
}
},
};
}
async stat(path) {
const normalized = normalizePath(path);
const file = this.files.get(normalized);
if (!file) {
throw new Error(`ENOENT: no such file or directory, stat '${path}'`);
}
return {
path: file.path,
size: file.size,
type: file.type,
created: file.created,
modified: file.modified,
readonly: false,
// Sempre false em memória
};
}
async glob(pattern) {
const paths = [];
for (const file of this.files.values()) {
if (this.matchesPattern(file.path, pattern)) {
paths.push(file.path);
}
}
return paths.sort();
}
async clear() {
const startTime = Date.now();
const operationId = `clear-${startTime}`;
this.events.emit(FileSystemEvents.OPERATION_START, {
operation: "clear",
id: operationId,
});
this.events.emit(FileSystemEvents.STORAGE_CLEARING, void 0);
try {
this.files.clear();
this.files.set("/", {
path: "/",
type: "directory",
size: 0,
created: /* @__PURE__ */ new Date(),
modified: /* @__PURE__ */ new Date(),
parent: "",
});
const duration = Date.now() - startTime;
this.events.emit(FileSystemEvents.STORAGE_CLEARED, { duration });
this.events.emit(FileSystemEvents.OPERATION_END, {
operation: "clear",
id: operationId,
duration,
});
} catch (error) {
this.events.emit(FileSystemEvents.OPERATION_ERROR, {
operation: "clear",
error,
});
throw error;
}
}
async size() {
const startTime = Date.now();
const operationId = `size-${startTime}`;
this.events.emit(FileSystemEvents.OPERATION_START, {
operation: "size",
id: operationId,
});
try {
let totalSize = 0;
for (const file of this.files.values()) {
totalSize += file.size;
}
this.events.emit(FileSystemEvents.STORAGE_SIZE_CALCULATED, {
size: totalSize,
});
this.events.emit(FileSystemEvents.OPERATION_END, {
operation: "size",
id: operationId,
duration: Date.now() - startTime,
});
return totalSize;
} catch (error) {
this.events.emit(FileSystemEvents.OPERATION_ERROR, {
operation: "size",
error,
});
throw error;
}
}
// Helper methods
calculateSize(content) {
if (content === null || content === void 0) {
return 0;
}
if (typeof content === "string") {
return content.length * 2;
}
if (content instanceof ArrayBuffer) {
return content.byteLength;
}
if (typeof content === "object") {
return JSON.stringify(content).length * 2;
}
return 0;
}
async ensureParentExists(path) {
const parent = dirname(path);
if (parent === "/" || parent === path) return;
if (!this.files.has(parent)) {
throw new Error(`ENOENT: no such file or directory, open '${path}'`);
}
}
async deleteRecursive(path) {
const toDelete = [];
for (const file of this.files.values()) {
if (file.path.startsWith(path + "/")) {
toDelete.push(file.path);
}
}
toDelete.sort((a, b) => b.length - a.length);
for (const filePath of toDelete) {
this.files.delete(filePath);
}
}
matchesPattern(path, pattern) {
if (pattern === "**" || pattern === "**/*") {
return true;
}
if (pattern === "*") {
return path.split("/").length === 2 && path !== "/";
}
let regex = pattern
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
.replace(/\*\*/g, "___GLOBSTAR___")
.replace(/\*/g, "[^/]*")
.replace(/\?/g, "[^/]")
.replace(/___GLOBSTAR___\//g, "(.*/)?")
.replace(/\/___GLOBSTAR___/g, "(/.*)?")
.replace(/___GLOBSTAR___/g, ".*");
regex = "^" + regex + "$";
return new RegExp(regex).test(path);
}
notifyWatchers(event) {
for (const watcher of this.watchers) {
if (this.matchesPattern(event.path, watcher.pattern)) {
try {
watcher.callback(event);
} catch (error) {
console.error(
`Error in watch callback for pattern "${watcher.pattern}":`,
error,
);
}
}
}
}
generateWatcherId() {
return `watch_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// Sobrescrever métodos de permissão (sempre permitido em memória)
async canModify(path) {
return true;
}
async canCreateIn(parentPath) {
try {
const stat = await this.stat(parentPath);
return stat.type === "directory";
} catch {
return normalizePath(parentPath) === "/";
}
}
// writeFileAtomic já está implementado na classe base
// Mas podemos otimizar para memória
async writeFileAtomic(path, content, metadata) {
return this.writeFile(path, content, metadata);
}
};
export { MemoryFileSystem };
//# sourceMappingURL=index.js.map