O módulo usa a identidade Supabase, isola cada workspace e registra storage, TTL, runner e biblioteca XLSX como decisões ainda abertas. Co-authored-by: Cursor <cursoragent@cursor.com>
65 lines
2.1 KiB
TypeScript
65 lines
2.1 KiB
TypeScript
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<void> {
|
|
const full = this.resolve(key);
|
|
await mkdir(path.dirname(full), { recursive: true });
|
|
await writeFile(full, body);
|
|
}
|
|
|
|
async get(key: string): Promise<Buffer> {
|
|
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<void> {
|
|
try {
|
|
await rm(this.resolve(key), { force: true });
|
|
} catch (error) {
|
|
if (error instanceof AdapterError) throw error;
|
|
}
|
|
}
|
|
|
|
async list(prefix: string): Promise<string[]> {
|
|
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(/\/$/, "")));
|
|
}
|
|
}
|