81 lines
2.8 KiB
TypeScript
81 lines
2.8 KiB
TypeScript
// components/utilities/files/api.ts
|
|
export type FsEntry = {
|
|
name: string;
|
|
path: string; // absolute or from root, e.g. "/public" or "/public/readme.txt"
|
|
isDir: boolean;
|
|
size?: number | null; // bytes
|
|
modified?: string | null; // ISO date string
|
|
mime?: string | null; // server-provided mime, optional
|
|
};
|
|
|
|
export type ListResponse = {
|
|
cwd: string; // normalized path we listed
|
|
entries: FsEntry[]; // unsorted list
|
|
};
|
|
|
|
export type SortKey = "name" | "size" | "modified" | "type";
|
|
export type SortDir = "asc" | "desc";
|
|
|
|
export async function list(path: string): Promise<ListResponse> {
|
|
const u = new URL("/api/files/list", location.origin);
|
|
if (path) u.searchParams.set("path", path);
|
|
const res = await fetch(u.toString(), { credentials: "include", cache: "no-store" });
|
|
if (!res.ok) throw new Error(`list ${path}: ${res.status}`);
|
|
return res.json();
|
|
}
|
|
|
|
export function rawUrl(path: string): string {
|
|
const u = new URL("/api/files/raw", location.origin);
|
|
u.searchParams.set("path", path);
|
|
return u.toString();
|
|
}
|
|
|
|
export async function download(path: string): Promise<void> {
|
|
// try direct browser download via a hidden <a download>
|
|
const u = new URL("/api/files/download", location.origin);
|
|
u.searchParams.set("path", path);
|
|
const a = document.createElement("a");
|
|
a.href = u.toString();
|
|
a.rel = "noopener";
|
|
a.download = ""; // hint to save-as
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
}
|
|
|
|
export function parentDir(p: string): string {
|
|
if (!p || p === "/") return "/";
|
|
const segs = p.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
segs.pop();
|
|
return "/" + segs.join("/");
|
|
}
|
|
|
|
export function nicelyFormatBytes(n?: number | null): string {
|
|
if (!Number.isFinite(n as number) || (n as number) < 0) return "—";
|
|
const b = n as number;
|
|
if (b < 1024) return `${b} B`;
|
|
const units = ["KB","MB","GB","TB"];
|
|
let v = b / 1024, i = 0;
|
|
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
|
|
return `${v.toFixed(v >= 100 ? 0 : v >= 10 ? 1 : 2)} ${units[i]}`;
|
|
}
|
|
|
|
export function extFromName(name: string): string {
|
|
const m = /\.([^.]+)$/.exec(name || "");
|
|
return m ? m[1].toLowerCase() : "";
|
|
}
|
|
|
|
export function isPreviewableImage(mime?: string | null, name?: string): boolean {
|
|
const ext = extFromName(name || "");
|
|
return /^image\//.test(mime || "") || ["png","jpg","jpeg","gif","webp","bmp","svg"].includes(ext);
|
|
}
|
|
|
|
export function isPreviewableText(mime?: string | null, name?: string): boolean {
|
|
const ext = extFromName(name || "");
|
|
return /^text\//.test(mime || "") || ["txt","csv","md","json","log"].includes(ext);
|
|
}
|
|
|
|
export function isPreviewablePdf(mime?: string | null, name?: string): boolean {
|
|
const ext = extFromName(name || "");
|
|
return (mime || "").includes("pdf") || ext === "pdf";
|
|
}
|