@synet/fs
Version:
Robust, battle-tested filesystem abstraction for Node.js
247 lines (246 loc) • 8.77 kB
JavaScript
"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;