50 lines
1.8 KiB
TypeScript
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 });
|
|
}
|
|
}
|
|
|