Intelligence
Artifacts
Browse the repository, read documents, and manage the governance folders. Source, runtime, and infrastructure are read-only.
Repository
repositories/aaf-holdings/hq01/lib/files/manager.ts
9.2 KB
import fs from "node:fs";
import path from "node:path";
import { CONTENT_ROOT } from "@/lib/content/config";
import { sanitizeFilename } from "@/lib/uploads/upload";
import { ALLOWED_EXTENSIONS, MANAGED_ROOTS, MAX_UPLOAD_BYTES } from "./config";
/**
* The document file-manager — create/delete folders and documents within the
* managed governance areas. Server-side only. This is the single place that
* performs these mutations, and every path passes through {@link resolveManaged}
* so nothing outside the managed roots can ever be written or removed.
*/
export class FileOpError extends Error {
constructor(
message: string,
readonly status = 400,
) {
super(message);
this.name = "FileOpError";
}
}
/** Strip leading separators and normalise; reject any traversal. */
function normalizeRel(relPath: string): string {
const norm = path
.normalize(relPath ?? "")
.replace(/^([/\\])+/, "")
.replace(/[/\\]+$/, "");
return norm.split(path.sep).join("/");
}
/** The managed root a path belongs to, or null if it is outside all of them. */
export function managedRootOf(relPath: string): string | null {
const norm = normalizeRel(relPath);
if (!norm) return null;
const first = norm.split("/")[0];
return (MANAGED_ROOTS as readonly string[]).includes(first) ? first : null;
}
export function isManaged(relPath: string): boolean {
try {
resolveManaged(relPath);
return true;
} catch {
return false;
}
}
/**
* Resolve a workspace-relative path to an absolute path, asserting it lives
* inside one of the managed roots. Throws {@link FileOpError} otherwise.
*
* @param allowRoot when false, the path may not BE a top-level managed root
* (used by delete, so a root like `constitutions/` can never be removed).
*/
export function resolveManaged(
relPath: string,
{ allowRoot = true }: { allowRoot?: boolean } = {},
): string {
const norm = normalizeRel(relPath);
if (!norm || norm.split("/").includes("..")) {
throw new FileOpError("Invalid path.", 400);
}
const root = managedRootOf(norm);
if (!root) {
throw new FileOpError("Path is outside the manageable area.", 403);
}
const abs = path.join(CONTENT_ROOT, norm);
const rootAbs = path.join(CONTENT_ROOT, root);
const rootWithSep = rootAbs.endsWith(path.sep) ? rootAbs : rootAbs + path.sep;
if (abs !== rootAbs && !abs.startsWith(rootWithSep)) {
throw new FileOpError("Path escapes the managed root.", 403);
}
if (!allowRoot && abs === rootAbs) {
throw new FileOpError("A top-level folder cannot be removed.", 403);
}
return abs;
}
function relOf(abs: string): string {
return path.relative(CONTENT_ROOT, abs).split(path.sep).join("/");
}
/** Sanitise a folder name to a single safe path segment. */
export function sanitizeFoldername(raw: string): string {
const base = (raw ?? "").split(/[/\\]/).pop() ?? "";
let name = base
.normalize("NFKD")
.replace(/[^A-Za-z0-9._ -]+/g, "-")
.replace(/\s+/g, "-")
.replace(/-{2,}/g, "-")
.replace(/^[.\-_]+/, "")
.replace(/[.\-_]+$/, "");
return name.slice(0, 60);
}
export interface DirEntry {
name: string;
type: "file" | "dir";
path: string;
ext?: string;
size?: number;
/** For folders: number of descendant files (used in delete confirmations). */
files?: number;
/** For folders: number of descendant sub-folders. */
folders?: number;
}
export interface ManagedDir {
path: string;
isRoot: boolean;
parent: string | null;
entries: DirEntry[];
}
/** List the immediate contents of a managed folder, for the manager UI. */
export function listManagedDir(relPath: string): ManagedDir {
const abs = resolveManaged(relPath);
let stat: fs.Stats;
try {
stat = fs.statSync(abs);
} catch {
throw new FileOpError("Folder not found.", 404);
}
if (!stat.isDirectory()) throw new FileOpError("Not a folder.", 400);
const norm = relOf(abs);
const entries: DirEntry[] = fs
.readdirSync(abs, { withFileTypes: true })
.filter((e) => !e.name.startsWith("."))
.map((e) => {
const childRel = `${norm}/${e.name}`;
if (e.isDirectory()) {
const c = countTree(path.join(abs, e.name));
return {
name: e.name,
type: "dir" as const,
path: childRel,
files: c.files,
folders: c.folders,
};
}
let size: number | undefined;
try {
size = fs.statSync(path.join(abs, e.name)).size;
} catch {
size = undefined;
}
return {
name: e.name,
type: "file" as const,
path: childRel,
ext: path.extname(e.name).toLowerCase(),
size,
};
})
.sort((a, b) => {
if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
return a.name.localeCompare(b.name);
});
const isRoot = managedRootOf(norm) === norm;
return {
path: norm,
isRoot,
parent: isRoot ? null : norm.split("/").slice(0, -1).join("/"),
entries,
};
}
/** Create a sub-folder inside a managed folder. Returns its relative path. */
export function createFolder(parentRel: string, name: string): string {
const parentAbs = resolveManaged(parentRel);
let pstat: fs.Stats;
try {
pstat = fs.statSync(parentAbs);
} catch {
throw new FileOpError("Parent folder not found.", 404);
}
if (!pstat.isDirectory()) throw new FileOpError("Parent is not a folder.", 400);
const safe = sanitizeFoldername(name);
if (!safe) throw new FileOpError("Enter a valid folder name.", 400);
const targetAbs = path.join(parentAbs, safe);
// Re-validate the composed target stays inside the managed area.
resolveManaged(relOf(targetAbs));
if (fs.existsSync(targetAbs)) {
throw new FileOpError("A folder with that name already exists.", 409);
}
fs.mkdirSync(targetAbs);
return relOf(targetAbs);
}
/** Count of files and folders beneath an absolute path (excludes the path itself). */
function countTree(abs: string): { files: number; folders: number } {
let files = 0;
let folders = 0;
const walk = (dir: string) => {
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
for (const e of entries) {
if (e.isDirectory()) {
folders += 1;
walk(path.join(dir, e.name));
} else {
files += 1;
}
}
};
walk(abs);
return { files, folders };
}
/** Recursively delete a managed sub-folder (never a top-level root). */
export function deleteFolder(relPath: string): { files: number; folders: number } {
const abs = resolveManaged(relPath, { allowRoot: false });
let stat: fs.Stats;
try {
stat = fs.statSync(abs);
} catch {
throw new FileOpError("Folder not found.", 404);
}
if (!stat.isDirectory()) throw new FileOpError("Not a folder.", 400);
const counts = countTree(abs);
fs.rmSync(abs, { recursive: true, force: true });
return counts;
}
/** Delete a single managed document. */
export function deleteDocument(relPath: string): void {
const abs = resolveManaged(relPath, { allowRoot: false });
let stat: fs.Stats;
try {
stat = fs.statSync(abs);
} catch {
throw new FileOpError("File not found.", 404);
}
if (!stat.isFile()) throw new FileOpError("Not a file.", 400);
fs.unlinkSync(abs);
}
export interface SavedDocument {
name: string;
relativePath: string;
size: number;
}
/** Validate and save an uploaded document into a managed folder. */
export function saveToFolder(
dirRel: string,
filename: string,
size: number,
data: Buffer,
): SavedDocument {
const dirAbs = resolveManaged(dirRel);
let dstat: fs.Stats;
try {
dstat = fs.statSync(dirAbs);
} catch {
throw new FileOpError("Target folder not found.", 404);
}
if (!dstat.isDirectory()) throw new FileOpError("Target is not a folder.", 400);
if (size <= 0 || data.length === 0) throw new FileOpError("The file is empty.", 400);
if (size > MAX_UPLOAD_BYTES || data.length > MAX_UPLOAD_BYTES) {
throw new FileOpError("File exceeds the 25MB limit.", 413);
}
const safeName = sanitizeFilename(filename);
const ext = path.extname(safeName).toLowerCase();
if (!ALLOWED_EXTENSIONS.has(ext)) {
throw new FileOpError(
`Unsupported file type "${ext || "(none)"}". Allowed: ${[...ALLOWED_EXTENSIONS].join(", ")}.`,
415,
);
}
const stem = safeName.slice(0, safeName.length - ext.length);
const dirWithSep = dirAbs.endsWith(path.sep) ? dirAbs : dirAbs + path.sep;
for (let attempt = 0; attempt < 1000; attempt += 1) {
const finalName = attempt === 0 ? safeName : `${stem}-${attempt}${ext}`;
const target = path.join(dirAbs, finalName);
if (!target.startsWith(dirWithSep)) {
throw new FileOpError("Resolved path escapes the folder.", 400);
}
try {
const fd = fs.openSync(target, "wx");
try {
fs.writeFileSync(fd, data);
} finally {
fs.closeSync(fd);
}
return { name: finalName, relativePath: relOf(target), size: data.length };
} catch (err) {
if ((err as NodeJS.ErrnoException)?.code === "EEXIST") continue;
throw new FileOpError(
`Failed to write file: ${err instanceof Error ? err.message : String(err)}`,
500,
);
}
}
throw new FileOpError("Could not find a free filename.", 409);
}
root · /srv/aaf