@firesystem/memory
Version:
In-memory implementation of Virtual File System
624 lines (622 loc) • 19.7 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
MemoryFileSystem: () => MemoryFileSystem
});
module.exports = __toCommonJS(index_exports);
// src/MemoryFileSystem.ts
var import_core = require("@firesystem/core");
var MemoryFileSystem = class extends import_core.BaseFileSystem {
constructor(options) {
super();
this.files = /* @__PURE__ */ new Map();
this.watchers = [];
this.events = new import_core.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 = (0, import_core.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: (0, import_core.dirname)(normalized)
});
}
}
}
async initialize() {
const startTime = Date.now();
this.events.emit(import_core.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(import_core.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(import_core.FileSystemEvents.DIR_READ, {
path: file.path,
count: childCount
});
}
this.events.emit(import_core.FileSystemEvents.INIT_PROGRESS, {
loaded: i + 1,
total: totalFiles,
phase: "loading-files"
});
}
this.events.emit(import_core.FileSystemEvents.INITIALIZED, {
duration: Date.now() - startTime
});
} catch (error) {
this.events.emit(import_core.FileSystemEvents.INIT_ERROR, {
error
});
throw error;
}
}
async readFile(path) {
const normalized = (0, import_core.normalizePath)(path);
const startTime = Date.now();
const operationId = `read-${normalized}-${startTime}`;
this.events.emit(import_core.FileSystemEvents.OPERATION_START, {
operation: "readFile",
path: normalized,
id: operationId
});
this.events.emit(import_core.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: (0, import_core.basename)(file.path),
type: file.type,
size: file.size,
created: file.created,
modified: file.modified,
metadata: file.metadata,
content: file.content
};
this.events.emit(import_core.FileSystemEvents.FILE_READ, {
path: normalized,
size: file.size
});
this.events.emit(import_core.FileSystemEvents.OPERATION_END, {
operation: "readFile",
path: normalized,
id: operationId,
duration: Date.now() - startTime
});
return result;
} catch (error) {
this.events.emit(import_core.FileSystemEvents.OPERATION_ERROR, {
operation: "readFile",
path: normalized,
error
});
throw error;
}
}
async writeFile(path, content, metadata) {
const normalized = (0, import_core.normalizePath)(path);
const startTime = Date.now();
const operationId = `write-${normalized}-${startTime}`;
const size = this.calculateSize(content);
this.events.emit(import_core.FileSystemEvents.OPERATION_START, {
operation: "writeFile",
path: normalized,
id: operationId
});
this.events.emit(import_core.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: (0, import_core.dirname)(normalized)
};
const isUpdate = !!existing;
this.files.set(normalized, file);
const result = {
path: file.path,
name: (0, import_core.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(import_core.FileSystemEvents.FILE_WRITTEN, {
path: normalized,
size
});
this.events.emit(import_core.FileSystemEvents.OPERATION_END, {
operation: "writeFile",
path: normalized,
id: operationId,
duration: Date.now() - startTime
});
return result;
} catch (error) {
this.events.emit(import_core.FileSystemEvents.OPERATION_ERROR, {
operation: "writeFile",
path: normalized,
error
});
throw error;
}
}
async deleteFile(path) {
const normalized = (0, import_core.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 = (0, import_core.normalizePath)(path);
return this.files.has(normalized);
}
async readDir(path) {
const normalized = (0, import_core.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: (0, import_core.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 = (0, import_core.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 = (0, import_core.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: (0, import_core.dirname)(normalized)
};
this.files.set(normalized, dir);
this.notifyWatchers({
type: "created",
path: normalized,
timestamp: now
});
return {
path: dir.path,
name: (0, import_core.basename)(dir.path),
type: dir.type,
size: 0,
created: now,
modified: now
};
}
async rmdir(path, recursive = false) {
const normalized = (0, import_core.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 = (0, import_core.normalizePath)(oldPath);
const newNormalized = (0, import_core.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: (0, import_core.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: (0, import_core.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: (0, import_core.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: (0, import_core.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 = (0, import_core.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 = (0, import_core.basename)(sourcePath);
const newPath = (0, import_core.join)(targetNormalized, name);
await this.rename(sourcePath, newPath);
}
}
async copy(sourcePath, targetPath) {
const sourceNormalized = (0, import_core.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 = (0, import_core.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(import_core.FileSystemEvents.OPERATION_START, {
operation: "clear",
id: operationId
});
this.events.emit(import_core.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(import_core.FileSystemEvents.STORAGE_CLEARED, { duration });
this.events.emit(import_core.FileSystemEvents.OPERATION_END, {
operation: "clear",
id: operationId,
duration
});
} catch (error) {
this.events.emit(import_core.FileSystemEvents.OPERATION_ERROR, {
operation: "clear",
error
});
throw error;
}
}
async size() {
const startTime = Date.now();
const operationId = `size-${startTime}`;
this.events.emit(import_core.FileSystemEvents.OPERATION_START, {
operation: "size",
id: operationId
});
try {
let totalSize = 0;
for (const file of this.files.values()) {
totalSize += file.size;
}
this.events.emit(import_core.FileSystemEvents.STORAGE_SIZE_CALCULATED, {
size: totalSize
});
this.events.emit(import_core.FileSystemEvents.OPERATION_END, {
operation: "size",
id: operationId,
duration: Date.now() - startTime
});
return totalSize;
} catch (error) {
this.events.emit(import_core.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 = (0, import_core.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 (0, import_core.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);
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
MemoryFileSystem
});
//# sourceMappingURL=index.cjs.map