UNPKG

@synet/fs

Version:

Robust, battle-tested filesystem abstraction for Node.js

247 lines (246 loc) 8.77 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WithIdFileSystem = exports.FileFormat = void 0; const node_crypto_1 = __importDefault(require("node:crypto")); const node_path_1 = __importDefault(require("node:path")); /** * Supported file formats for ID-based file operations */ var FileFormat; (function (FileFormat) { FileFormat["JSON"] = "json"; FileFormat["TXT"] = "txt"; FileFormat["PDF"] = "pdf"; FileFormat["MD"] = "md"; FileFormat["XML"] = "xml"; FileFormat["CSV"] = "csv"; FileFormat["LOG"] = "log"; FileFormat["CONFIG"] = "config"; })(FileFormat || (exports.FileFormat = FileFormat = {})); /** * WithIdFileSystem provides deterministic IDs for files while preserving original names * Files are stored with format: basename:filename-path-hash.ext * Enables access by original path, ID, or alias */ class WithIdFileSystem { constructor(baseFileSystem) { this.baseFileSystem = baseFileSystem; this.fileMap = new Map(); this.idMap = new Map(); this.aliasMap = new Map(); } /** * Generate deterministic ID for a file path */ generateId(filePath) { return node_crypto_1.default .createHash("sha256") .update(filePath) .digest("hex") .substring(0, 16); } /** * Generate alias from file path (readable identifier) */ generateAlias(filePath) { // Remove leading slash and convert path separators to hyphens const normalized = filePath.replace(/^[./]+/, "").replace(/[/\\]/g, "-"); // Remove extension const withoutExt = normalized.replace(/\.[^.]*$/, ""); return withoutExt; } /** * Get file format from extension */ getFileFormat(filePath) { const ext = node_path_1.default.extname(filePath).toLowerCase().substring(1); // Map common extensions to FileFormat enum switch (ext) { case "json": return FileFormat.JSON; case "txt": return FileFormat.TXT; case "pdf": return FileFormat.PDF; case "md": case "markdown": return FileFormat.MD; case "xml": return FileFormat.XML; case "csv": return FileFormat.CSV; case "log": return FileFormat.LOG; case "conf": case "config": case "ini": return FileFormat.CONFIG; default: return FileFormat.TXT; // Default fallback } } /** * Generate stored file path with ID format */ generateStoredPath(filePath) { const normalizedPath = filePath.replace(/\\/g, "/"); const dir = node_path_1.default.dirname(normalizedPath); const ext = node_path_1.default.extname(normalizedPath); const basename = node_path_1.default.basename(normalizedPath, ext); const id = this.generateId(normalizedPath); const alias = this.generateAlias(normalizedPath); const storedName = `${basename}:${alias}-${id}${ext}`; // Preserve the original directory structure including leading ./ if (dir === ".") { return `./${storedName}`; } return `${dir}/${storedName}`; } /** * Get file metadata for a path, creating if necessary */ getOrCreateMetadata(filePath) { const normalizedPath = filePath.replace(/\\/g, "/"); const existing = this.fileMap.get(normalizedPath); if (existing) { return existing; } const id = this.generateId(normalizedPath); const alias = this.generateAlias(normalizedPath); const storedPath = this.generateStoredPath(normalizedPath); const format = this.getFileFormat(normalizedPath); const metadata = { id, alias, originalPath: normalizedPath, storedPath, format, }; // Store in all maps this.fileMap.set(normalizedPath, metadata); this.idMap.set(id, metadata); this.aliasMap.set(alias, metadata); return metadata; } /** * Find metadata by ID or alias */ findMetadata(idOrAlias) { return this.idMap.get(idOrAlias) || this.aliasMap.get(idOrAlias); } /** * Get deterministic ID for a file path */ getId(filePath) { const metadata = this.getOrCreateMetadata(filePath); return metadata.id; } /** * Get alias (readable identifier) for a file path */ getAlias(filePath) { const metadata = this.getOrCreateMetadata(filePath); return metadata.alias; } /** * Get file content by ID or alias with optional format specification */ getByIdOrAlias(idOrAlias, expectedFormat) { const metadata = this.findMetadata(idOrAlias); if (!metadata) { throw new Error(`File not found with ID or alias: ${idOrAlias}`); } if (expectedFormat && metadata.format !== expectedFormat) { throw new Error(`File format mismatch. Expected: ${expectedFormat}, Found: ${metadata.format}`); } return this.baseFileSystem.readFileSync(metadata.storedPath); } /** * Get metadata for a file by its original path */ getMetadata(filePath) { return this.getOrCreateMetadata(filePath); } /** * List all tracked files */ listTrackedFiles() { return Array.from(this.fileMap.values()); } // IFileSystem implementation existsSync(path) { const normalizedPath = path.replace(/\\/g, "/"); const existing = this.fileMap.get(normalizedPath); if (existing) { // File is tracked, check if it exists in base filesystem return this.baseFileSystem.existsSync(existing.storedPath); } // File not tracked, check if we should track it // Generate the stored path and check if it exists const metadata = this.getOrCreateMetadata(normalizedPath); const exists = this.baseFileSystem.existsSync(metadata.storedPath); if (!exists) { // If file doesn't exist, clean up the metadata we just created this.fileMap.delete(metadata.originalPath); this.idMap.delete(metadata.id); this.aliasMap.delete(metadata.alias); } return exists; } readFileSync(path) { const metadata = this.getOrCreateMetadata(path); return this.baseFileSystem.readFileSync(metadata.storedPath); } writeFileSync(path, data) { const metadata = this.getOrCreateMetadata(path); this.baseFileSystem.writeFileSync(metadata.storedPath, data); } deleteFileSync(path) { const metadata = this.getOrCreateMetadata(path); this.baseFileSystem.deleteFileSync(metadata.storedPath); // Clean up metadata this.fileMap.delete(metadata.originalPath); this.idMap.delete(metadata.id); this.aliasMap.delete(metadata.alias); } deleteDirSync(path) { // Clean up metadata for files in the directory const normalizedPath = path.replace(/\\/g, "/"); for (const [filePath, metadata] of this.fileMap.entries()) { if (filePath.startsWith(normalizedPath)) { this.idMap.delete(metadata.id); this.aliasMap.delete(metadata.alias); this.fileMap.delete(filePath); } } this.baseFileSystem.deleteDirSync(path); } readDirSync(dirPath) { return this.baseFileSystem.readDirSync(dirPath); } ensureDirSync(path) { this.baseFileSystem.ensureDirSync(path); } chmodSync(path, mode) { const metadata = this.getOrCreateMetadata(path); this.baseFileSystem.chmodSync(metadata.storedPath, mode); } clear(dirPath) { if (this.baseFileSystem.clear) { // Clean up metadata for the directory const normalizedPath = dirPath.replace(/\\/g, "/"); for (const [filePath, metadata] of this.fileMap.entries()) { if (filePath.startsWith(normalizedPath)) { this.idMap.delete(metadata.id); this.aliasMap.delete(metadata.alias); this.fileMap.delete(filePath); } } this.baseFileSystem.clear(dirPath); } } } exports.WithIdFileSystem = WithIdFileSystem;