import * as plugins from '../plugins.js'; import { logger } from '../logger.js'; // Promisify filesystem operations const readFile = plugins.util.promisify(plugins.fs.readFile); const writeFile = plugins.util.promisify(plugins.fs.writeFile); const unlink = plugins.util.promisify(plugins.fs.unlink); const rename = plugins.util.promisify(plugins.fs.rename); const readdir = plugins.util.promisify(plugins.fs.readdir); /** * Storage configuration interface */ export interface IStorageConfig { /** Filesystem path for storage */ fsPath?: string; /** Custom read function */ readFunction?: (key: string) => Promise; /** Custom write function */ writeFunction?: (key: string, value: string) => Promise; } /** * Storage backend type */ export type StorageBackend = 'filesystem' | 'custom' | 'memory'; /** * Central storage manager for DcRouter * Provides unified key-value storage with multiple backend support */ export class StorageManager { private backend: StorageBackend; private memoryStore: Map = new Map(); private config: IStorageConfig; private fsBasePath?: string; constructor(config?: IStorageConfig) { this.config = config || {}; // Check if both fsPath and custom functions are provided if (config?.fsPath && (config?.readFunction || config?.writeFunction)) { console.warn( '⚠️ WARNING: Both fsPath and custom read/write functions are configured.\n' + ' Using custom read/write functions. fsPath will be ignored.' ); } // Determine backend based on configuration if (config?.readFunction && config?.writeFunction) { this.backend = 'custom'; } else if (config?.fsPath) { // Set up internal read/write functions for filesystem this.backend = 'custom'; // Use custom backend with internal functions this.fsBasePath = plugins.path.resolve(config.fsPath); this.ensureDirectory(this.fsBasePath); // Set up internal filesystem read/write functions this.config.readFunction = async (key: string) => { return this.fsRead(key); }; this.config.writeFunction = async (key: string, value: string) => { await this.fsWrite(key, value); }; } else { this.backend = 'memory'; this.showMemoryWarning(); } logger.log('info', `StorageManager initialized with ${this.backend} backend`); } /** * Show warning when using memory backend */ private showMemoryWarning(): void { console.warn( '⚠️ WARNING: StorageManager is using in-memory storage.\n' + ' Data will be lost when the process restarts.\n' + ' Configure storage.fsPath or storage functions for persistence.' ); } /** * Ensure directory exists for filesystem backend */ private async ensureDirectory(dirPath: string): Promise { try { await plugins.smartfile.fs.ensureDir(dirPath); } catch (error) { logger.log('error', `Failed to create storage directory: ${error.message}`); throw error; } } /** * Validate and sanitize storage key */ private validateKey(key: string): string { if (!key || typeof key !== 'string') { throw new Error('Storage key must be a non-empty string'); } // Ensure key starts with / if (!key.startsWith('/')) { key = '/' + key; } // Remove any dangerous path elements key = key.replace(/\.\./g, '').replace(/\/+/g, '/'); return key; } /** * Convert key to filesystem path */ private keyToPath(key: string): string { if (!this.fsBasePath) { throw new Error('Filesystem base path not configured'); } // Remove leading slash and convert to path const relativePath = key.substring(1); return plugins.path.join(this.fsBasePath, relativePath); } /** * Internal filesystem read function */ private async fsRead(key: string): Promise { const filePath = this.keyToPath(key); try { const content = await readFile(filePath, 'utf8'); return content; } catch (error) { if (error.code === 'ENOENT') { return null; } throw error; } } /** * Internal filesystem write function */ private async fsWrite(key: string, value: string): Promise { const filePath = this.keyToPath(key); const dir = plugins.path.dirname(filePath); // Ensure directory exists await plugins.smartfile.fs.ensureDir(dir); // Write atomically with temp file const tempPath = `${filePath}.tmp`; await writeFile(tempPath, value, 'utf8'); await rename(tempPath, filePath); } /** * Get value by key */ async get(key: string): Promise { key = this.validateKey(key); try { switch (this.backend) { case 'custom': { if (!this.config.readFunction) { throw new Error('Read function not configured'); } try { return await this.config.readFunction(key); } catch (error) { // Assume null if read fails (key doesn't exist) return null; } } case 'memory': { return this.memoryStore.get(key) || null; } default: throw new Error(`Unknown backend: ${this.backend}`); } } catch (error) { logger.log('error', `Storage get error for key ${key}: ${error.message}`); throw error; } } /** * Set value by key */ async set(key: string, value: string): Promise { key = this.validateKey(key); if (typeof value !== 'string') { throw new Error('Storage value must be a string'); } try { switch (this.backend) { case 'filesystem': { const filePath = this.keyToPath(key); const dirPath = plugins.path.dirname(filePath); // Ensure directory exists await plugins.smartfile.fs.ensureDir(dirPath); // Write atomically const tempPath = filePath + '.tmp'; await writeFile(tempPath, value, 'utf8'); await rename(tempPath, filePath); break; } case 'custom': { if (!this.config.writeFunction) { throw new Error('Write function not configured'); } await this.config.writeFunction(key, value); break; } case 'memory': { this.memoryStore.set(key, value); break; } default: throw new Error(`Unknown backend: ${this.backend}`); } } catch (error) { logger.log('error', `Storage set error for key ${key}: ${error.message}`); throw error; } } /** * Delete value by key */ async delete(key: string): Promise { key = this.validateKey(key); try { switch (this.backend) { case 'filesystem': { const filePath = this.keyToPath(key); try { await unlink(filePath); } catch (error) { if (error.code !== 'ENOENT') { throw error; } } break; } case 'custom': { // Try to delete by setting empty value if (this.config.writeFunction) { await this.config.writeFunction(key, ''); } break; } case 'memory': { this.memoryStore.delete(key); break; } default: throw new Error(`Unknown backend: ${this.backend}`); } } catch (error) { logger.log('error', `Storage delete error for key ${key}: ${error.message}`); throw error; } } /** * List keys by prefix */ async list(prefix?: string): Promise { prefix = prefix ? this.validateKey(prefix) : '/'; try { switch (this.backend) { case 'custom': { // If we have fsBasePath, this is actually filesystem backend if (this.fsBasePath) { const basePath = this.keyToPath(prefix); const keys: string[] = []; const walkDir = async (dir: string, baseDir: string): Promise => { try { const entries = await readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = plugins.path.join(dir, entry.name); if (entry.isDirectory()) { await walkDir(fullPath, baseDir); } else if (entry.isFile()) { // Convert path back to key const relativePath = plugins.path.relative(this.fsBasePath!, fullPath); const key = '/' + relativePath.replace(/\\/g, '/'); if (key.startsWith(prefix)) { keys.push(key); } } } } catch (error) { if (error.code !== 'ENOENT') { throw error; } } }; await walkDir(basePath, basePath); return keys.sort(); } else { // True custom backends need to implement their own listing logger.log('warn', 'List operation not supported for custom backend'); return []; } } case 'memory': { const keys: string[] = []; for (const key of this.memoryStore.keys()) { if (key.startsWith(prefix)) { keys.push(key); } } return keys.sort(); } default: throw new Error(`Unknown backend: ${this.backend}`); } } catch (error) { logger.log('error', `Storage list error for prefix ${prefix}: ${error.message}`); throw error; } } /** * Check if key exists */ async exists(key: string): Promise { key = this.validateKey(key); try { const value = await this.get(key); return value !== null; } catch (error) { return false; } } /** * Get storage backend type */ getBackend(): StorageBackend { // If we're using custom backend with fsBasePath, report it as filesystem if (this.backend === 'custom' && this.fsBasePath) { return 'filesystem' as StorageBackend; } return this.backend; } /** * JSON helper: Get and parse JSON value */ async getJSON(key: string): Promise { const value = await this.get(key); if (value === null) { return null; } try { return JSON.parse(value) as T; } catch (error) { logger.log('error', `Failed to parse JSON for key ${key}: ${error.message}`); throw error; } } /** * JSON helper: Set value as JSON */ async setJSON(key: string, value: any): Promise { const jsonString = JSON.stringify(value, null, 2); await this.set(key, jsonString); } }