import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises"; import path from "node:path"; import { AdapterError } from "@/domain/spreadsheet-adapter/errors"; import { assertStorageKey } from "@/domain/spreadsheet-adapter/filenames"; import type { ObjectStorage } from "./memory"; export class LocalPrivateStorage implements ObjectStorage { constructor(private readonly root: string) {} private resolve(key: string): string { assertStorageKey(key); const full = path.resolve(this.root, ...key.split("/")); const root = path.resolve(this.root); if (full !== root && !full.startsWith(root + path.sep)) { throw new AdapterError("PATH_TRAVERSAL", "Chave de armazenamento rejeitada."); } return full; } async put(key: string, body: Buffer): Promise { const full = this.resolve(key); await mkdir(path.dirname(full), { recursive: true }); await writeFile(full, body); } async get(key: string): Promise { try { return await readFile(this.resolve(key)); } catch (error) { if (error instanceof AdapterError) throw error; throw new AdapterError("NOT_FOUND", "Arquivo não encontrado.", 404); } } async delete(key: string): Promise { try { await rm(this.resolve(key), { force: true }); } catch (error) { if (error instanceof AdapterError) throw error; } } async list(prefix: string): Promise { const normalized = prefix.replace(/\/$/, ""); const start = this.resolve(normalized.includes("/") ? normalized : `${normalized}/noop`.replace("/noop", "")); const root = path.resolve(this.root); const found: string[] = []; const walk = async (dir: string) => { let entries; try { entries = await readdir(dir, { withFileTypes: true }); } catch { return; } for (const entry of entries) { const full = path.join(dir, entry.name); if (entry.isDirectory()) await walk(full); else found.push(path.relative(root, full).split(path.sep).join("/")); } }; await walk(start); return found.filter((key) => key.startsWith(prefix.replace(/\/$/, ""))); } }