109 lines
3.5 KiB
TypeScript
109 lines
3.5 KiB
TypeScript
|
|
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<string | null> {
|
||
|
|
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<void> {
|
||
|
|
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<string[]> {
|
||
|
|
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<void> => {
|
||
|
|
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<void> {
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|