135 lines
4 KiB
TypeScript
135 lines
4 KiB
TypeScript
// /var/www/makearmy.io/app/app/files/page.tsx
|
|
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { useSearchParams, useRouter } from "next/navigation";
|
|
|
|
type FileItem = {
|
|
name: string;
|
|
isDir: boolean;
|
|
size: number;
|
|
mtime: number;
|
|
};
|
|
|
|
export default function FilesPage() {
|
|
const search = useSearchParams();
|
|
const router = useRouter();
|
|
const path = useMemo(() => search.get("path") || "/", [search]);
|
|
|
|
const [items, setItems] = useState<FileItem[] | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
async function load() {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const res = await fetch(
|
|
`/api/files/list?path=${encodeURIComponent(path)}`
|
|
);
|
|
if (!res.ok) {
|
|
if (!cancelled) setError(`HTTP ${res.status}`);
|
|
return;
|
|
}
|
|
const json = await res.json();
|
|
if (!cancelled) setItems(json.items || []);
|
|
} catch (e: any) {
|
|
if (!cancelled) setError(e?.message || String(e));
|
|
} finally {
|
|
if (!cancelled) setLoading(false);
|
|
}
|
|
}
|
|
load();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [path]);
|
|
|
|
const upPath = useMemo(() => {
|
|
if (path === "/") return null;
|
|
const parts = path.replace(/\/+$/, "").split("/").filter(Boolean);
|
|
parts.pop();
|
|
return "/" + parts.join("/");
|
|
}, [path]);
|
|
|
|
return (
|
|
<div className="p-6 text-sm">
|
|
|
|
<div className="mb-3">
|
|
<span className="opacity-70 mr-1">Path:</span>
|
|
<code>{path}</code>
|
|
{upPath && (
|
|
<>
|
|
<span className="mx-2 opacity-50">•</span>
|
|
<Link href={`/files?path=${encodeURIComponent(upPath)}`}>
|
|
Up one level
|
|
</Link>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{loading && <div>Loading…</div>}
|
|
{error && (
|
|
<div className="bg-red-900/60 text-red-200 p-3 rounded border border-red-800">
|
|
Error loading files: {error}
|
|
</div>
|
|
)}
|
|
|
|
{!loading && !error && items && (
|
|
<table className="w-full text-left mt-3 border-collapse">
|
|
<thead className="opacity-70">
|
|
<tr>
|
|
<th className="py-2 pr-4">Name</th>
|
|
<th className="py-2 pr-4">Type</th>
|
|
<th className="py-2 pr-4">Size</th>
|
|
<th className="py-2 pr-4">Modified</th>
|
|
<th className="py-2 pr-4"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{items.map((it) => {
|
|
const href = it.isDir
|
|
? `/files?path=${encodeURIComponent(
|
|
(path.endsWith("/") ? path : path + "/") + it.name
|
|
)}`
|
|
: `/api/files/raw?path=${encodeURIComponent(
|
|
(path.endsWith("/") ? path : path + "/") + it.name
|
|
)}`;
|
|
const dl = it.isDir
|
|
? null
|
|
: `/api/files/download?path=${encodeURIComponent(
|
|
(path.endsWith("/") ? path : path + "/") + it.name
|
|
)}`;
|
|
|
|
return (
|
|
<tr key={it.name} className="border-t border-white/10">
|
|
<td className="py-2 pr-4">
|
|
<Link href={href}>{it.name}</Link>
|
|
</td>
|
|
<td className="py-2 pr-4">{it.isDir ? "Dir" : "File"}</td>
|
|
<td className="py-2 pr-4">
|
|
{it.isDir ? "-" : `${it.size.toLocaleString()} B`}
|
|
</td>
|
|
<td className="py-2 pr-4">
|
|
{new Date(it.mtime).toLocaleString()}
|
|
</td>
|
|
<td className="py-2 pr-4">
|
|
{!it.isDir && dl && (
|
|
<a href={dl} className="underline">
|
|
Download
|
|
</a>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|