import * as plugins from '../plugins.js'; import type { IStorageManagerLike } from '@push.rocks/smartmta'; export class SmartMtaStorageManager implements IStorageManagerLike { private readonly resolvedRootDir: string; constructor(private rootDir: string) { this.resolvedRootDir = plugins.path.resolve(rootDir); plugins.fsUtils.ensureDirSync(this.resolvedRootDir); } private normalizeKey(key: string): string { return key.replace(/^\/+/, '').replace(/\\/g, '/'); } private resolvePathForKey(key: string): string { const normalizedKey = this.normalizeKey(key); const resolvedPath = plugins.path.resolve(this.resolvedRootDir, normalizedKey); if ( resolvedPath !== this.resolvedRootDir && !resolvedPath.startsWith(`${this.resolvedRootDir}${plugins.path.sep}`) ) { throw new Error(`Storage key escapes root directory: ${key}`); } return resolvedPath; } private toStorageKey(filePath: string): string { const relativePath = plugins.path.relative(this.resolvedRootDir, filePath).split(plugins.path.sep).join('/'); return `/${relativePath}`; } public async get(key: string): Promise { const filePath = this.resolvePathForKey(key); try { return await plugins.fs.promises.readFile(filePath, 'utf8'); } catch (error: unknown) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { return null; } throw error; } } public async set(key: string, value: string): Promise { const filePath = this.resolvePathForKey(key); await plugins.fs.promises.mkdir(plugins.path.dirname(filePath), { recursive: true }); await plugins.fs.promises.writeFile(filePath, value, 'utf8'); } public async list(prefix: string): Promise { const prefixPath = this.resolvePathForKey(prefix); try { const stat = await plugins.fs.promises.stat(prefixPath); if (stat.isFile()) { return [this.toStorageKey(prefixPath)]; } } catch (error: unknown) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { return []; } throw error; } const results: string[] = []; const walk = async (currentPath: string): Promise => { const entries = await plugins.fs.promises.readdir(currentPath, { withFileTypes: true }); for (const entry of entries) { const entryPath = plugins.path.join(currentPath, entry.name); if (entry.isDirectory()) { await walk(entryPath); } else if (entry.isFile()) { results.push(this.toStorageKey(entryPath)); } } }; await walk(prefixPath); return results.sort(); } public async delete(key: string): Promise { const targetPath = this.resolvePathForKey(key); try { const stat = await plugins.fs.promises.stat(targetPath); if (stat.isDirectory()) { await plugins.fs.promises.rm(targetPath, { recursive: true, force: true }); } else { await plugins.fs.promises.unlink(targetPath); } } catch (error: unknown) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { return; } throw error; } let currentDir = plugins.path.dirname(targetPath); while (currentDir.startsWith(this.resolvedRootDir) && currentDir !== this.resolvedRootDir) { const entries = await plugins.fs.promises.readdir(currentDir); if (entries.length > 0) { break; } await plugins.fs.promises.rmdir(currentDir); currentDir = plugins.path.dirname(currentDir); } } }