makearmy-app/app/api/files/route.ts
2025-09-22 10:37:53 -04:00

50 lines
1.8 KiB
TypeScript

import { NextResponse } from "next/server";
import fs from "node:fs/promises";
import fssync from "node:fs";
import path from "node:path";
const BASE = "/app/files"; // this is /var/www/makearmy.io/app/files on the host
function safeJoin(base: string, reqPath: string) {
const decoded = decodeURIComponent(reqPath || "/");
// normalize, strip traversal, and join under BASE
const normalized = path.posix.normalize("/" + decoded).replace(/^(\.\.(\/|\\|$))+/g, "");
const full = path.join(base, normalized);
if (!full.startsWith(base)) throw new Error("Invalid path");
return full;
}
export async function GET(req: Request) {
try {
const { searchParams } = new URL(req.url);
const p = searchParams.get("path") || "/";
const abs = safeJoin(BASE, p);
const entries = await fs.readdir(abs, { withFileTypes: true });
const rows = await Promise.all(entries.map(async (ent) => {
const full = path.join(abs, ent.name);
const stat = await fs.stat(full);
const isDir = ent.isDirectory();
return {
name: ent.name,
type: isDir ? "dir" : "file",
size: isDir ? null : stat.size,
mtime: stat.mtime.toISOString(),
path: path.posix.join(p.endsWith("/") ? p : p + "/", ent.name),
// raw download/view URL (served by /api/files/raw)
url: isDir ? null : `/api/files/raw?path=${encodeURIComponent(path.posix.join(p, ent.name))}`,
};
}));
// Sort: directories first, then files alphabetically
rows.sort((a, b) => {
if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
});
return NextResponse.json({ ok: true, path: p, items: rows });
} catch (err: any) {
return NextResponse.json({ ok: false, error: err?.message || "Error" }, { status: 400 });
}
}