import type { FileCreateRequest, FileCreateResponse, FileInfoResponse, FileListResponse, FilePasteCollision, FilePasteRequest, FilePasteResponse, FileReadResponse, FileRenameRequest, FileRenameResponse, FileRootResponse, FileToggleStarResponse, FileUnzipResponse, FileWriteRequest, FileWriteResponse, } from "@/lib/shared/contracts/files"; export type { FilePasteCollision }; type ListFilesApiResponse = { data: FileListResponse; meta: { count: number; }; }; type ReadFileApiResponse = { data: FileReadResponse; }; type RootFileApiResponse = { data: FileRootResponse; }; type WriteFileApiResponse = { data: FileWriteResponse; }; type FileCreateApiResponse = { data: FileCreateResponse; }; type FilePasteApiResponse = { data: FilePasteResponse; }; type FileRenameApiResponse = { data: FileRenameResponse; }; type FileInfoApiResponse = { data: FileInfoResponse; }; type FileToggleStarApiResponse = { data: FileToggleStarResponse; }; type FileUnzipApiResponse = { data: FileUnzipResponse; }; export function buildListEndpoint(filePath: string, includeHidden = false) { const params = new URLSearchParams(); if (filePath.length > 0) { params.set("path", filePath); } if (includeHidden) { params.set("includeHidden", "true"); } const query = params.toString(); return query.length > 0 ? `/api/v1/files?${query}` : "/api/v1/files"; } export function buildReadEndpoint(filePath: string) { const params = new URLSearchParams({ path: filePath, }); return `/api/v1/files/content?${params.toString()}`; } export function getParentPath(filePath: string) { const normalized = filePath.trim(); if (normalized.length === 0) return ""; const separatorIndex = normalized.lastIndexOf("/"); if (separatorIndex < 0) return ""; return normalized.slice(0, separatorIndex); } export function toFilePath(pathSegments: string[]) { return pathSegments.join("/"); } export function buildAssetUrl(filePath: string) { const params = new URLSearchParams({ path: filePath, }); return `/api/v1/files/asset?${params.toString()}`; } export function buildDownloadUrl(filePath: string) { const params = new URLSearchParams({ path: filePath, }); return `/api/v1/files/download?${params.toString()}`; } export function buildZipUrl(folderPath: string) { const params = new URLSearchParams({ path: folderPath, }); return `/api/v1/files/zip?${params.toString()}`; } type ErrorBody = { error?: string; code?: string; }; async function getErrorBody(response: Response) { return (await response.json().catch(() => ({}))) as ErrorBody; } export async function fetchRoot() { const response = await fetch("/api/v1/files/root", { cache: "no-store", }); if (!response.ok) { const errorBody = await getErrorBody(response); throw new Error( errorBody.error ?? `Failed to fetch files root (${response.status})${errorBody.code ? ` [${errorBody.code}]` : ""}`, ); } const json = (await response.json()) as RootFileApiResponse; return json.data; } export async function fetchFileContent(filePath: string) { const endpoint = buildReadEndpoint(filePath); const response = await fetch(endpoint, { cache: "no-store", }); if (!response.ok) { const errorBody = await getErrorBody(response); throw new Error( errorBody.error ?? `Failed to read file (${response.status})${errorBody.code ? ` [${errorBody.code}]` : ""}`, ); } const json = (await response.json()) as ReadFileApiResponse; return json.data; } export async function saveFileContent(payload: FileWriteRequest) { const response = await fetch("/api/v1/files/content", { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify(payload), }); if (!response.ok) { const errorBody = await getErrorBody(response); throw new Error( errorBody.error ?? `Failed to save file (${response.status})${errorBody.code ? ` [${errorBody.code}]` : ""}`, ); } const json = (await response.json()) as WriteFileApiResponse; return json.data; } export async function createFolder(payload: FileCreateRequest) { const response = await fetch("/api/v1/files/ops", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ action: "create_folder", ...payload, }), }); if (!response.ok) { const errorBody = await getErrorBody(response); throw new Error( errorBody.error ?? `Failed to create folder (${response.status})${errorBody.code ? ` [${errorBody.code}]` : ""}`, ); } const json = (await response.json()) as FileCreateApiResponse; return json.data; } export async function createFile(payload: FileCreateRequest) { const response = await fetch("/api/v1/files/ops", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ action: "create_file", ...payload, }), }); if (!response.ok) { const errorBody = await getErrorBody(response); throw new Error( errorBody.error ?? `Failed to create file (${response.status})${errorBody.code ? ` [${errorBody.code}]` : ""}`, ); } const json = (await response.json()) as FileCreateApiResponse; return json.data; } export async function pasteFile(payload: FilePasteRequest) { const response = await fetch("/api/v1/files/ops", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ action: "paste", ...payload, }), }); if (!response.ok) { const errorBody = await getErrorBody(response); throw new Error( errorBody.error ?? `Failed to paste (${response.status})${errorBody.code ? ` [${errorBody.code}]` : ""}`, ); } const json = (await response.json()) as FilePasteApiResponse; return json.data; } export async function renameFile(payload: FileRenameRequest) { const response = await fetch("/api/v1/files/ops", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ action: "rename", ...payload, }), }); if (!response.ok) { const errorBody = await getErrorBody(response); throw new Error( errorBody.error ?? `Failed to rename (${response.status})${errorBody.code ? ` [${errorBody.code}]` : ""}`, ); } const json = (await response.json()) as FileRenameApiResponse; return json.data; } export async function getFileInfo(pathValue: string) { const response = await fetch("/api/v1/files/ops", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ action: "get_info", path: pathValue, }), }); if (!response.ok) { const errorBody = await getErrorBody(response); throw new Error( errorBody.error ?? `Failed to read info (${response.status})${errorBody.code ? ` [${errorBody.code}]` : ""}`, ); } const json = (await response.json()) as FileInfoApiResponse; return json.data; } export async function toggleStar(pathValue: string) { const response = await fetch("/api/v1/files/ops", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ action: "toggle_star", path: pathValue, }), }); if (!response.ok) { const errorBody = await getErrorBody(response); throw new Error( errorBody.error ?? `Failed to toggle star (${response.status})${errorBody.code ? ` [${errorBody.code}]` : ""}`, ); } const json = (await response.json()) as FileToggleStarApiResponse; return json.data; } export async function unzipFile(pathValue: string) { const response = await fetch("/api/v1/files/ops", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "unzip", path: pathValue }), }); if (!response.ok) { const errorBody = await getErrorBody(response); throw new Error( errorBody.error ?? `Failed to unzip (${response.status})${errorBody.code ? ` [${errorBody.code}]` : ""}`, ); } const json = (await response.json()) as FileUnzipApiResponse; return json.data; } export async function fetchDirectory(filePath: string, includeHidden: boolean) { const endpoint = buildListEndpoint(filePath, includeHidden); const response = await fetch(endpoint, { cache: "no-store", }); if (!response.ok) { const errorBody = await getErrorBody(response); throw new Error( errorBody.error ?? `Failed to list files (${response.status})${errorBody.code ? ` [${errorBody.code}]` : ""}`, ); } const json = (await response.json()) as ListFilesApiResponse; return json.data; } export async function fetchStarredFiles() { const response = await fetch("/api/v1/files/starred", { cache: "no-store", }); if (!response.ok) { const errorBody = await getErrorBody(response); throw new Error( errorBody.error ?? `Failed to fetch starred items (${response.status})`, ); } const json = (await response.json()) as ListFilesApiResponse; return json.data; } export async function uploadFilesToPath(payload: { destinationPath: string; files: File[]; includeHidden?: boolean; onProgress?: (loaded: number, total: number) => void; signal?: AbortSignal; }) { const formData = new FormData(); formData.append("path", payload.destinationPath); if (payload.includeHidden) formData.append("includeHidden", "true"); for (const file of payload.files) { formData.append("file", file); } return new Promise<{ uploaded: { name: string; path: string; sizeBytes: number }[]; skipped: string[] }>( (resolve, reject) => { const xhr = new XMLHttpRequest(); let settled = false; function createAbortError() { const error = new Error("Upload canceled"); error.name = "AbortError"; return error; } function cleanup() { payload.signal?.removeEventListener("abort", abortUpload); } function settle(callback: () => void) { if (settled) return; settled = true; cleanup(); callback(); } function abortUpload() { xhr.abort(); } if (payload.signal?.aborted) { reject(createAbortError()); return; } xhr.open("POST", "/api/v1/files/upload"); payload.signal?.addEventListener("abort", abortUpload, { once: true }); if (payload.onProgress) { const cb = payload.onProgress; xhr.upload.addEventListener("progress", (e) => { if (e.lengthComputable) cb(e.loaded, e.total); }); } xhr.addEventListener("load", () => { if (xhr.status >= 200 && xhr.status < 300) { settle(() => { const json = JSON.parse(xhr.responseText) as { data: { uploaded: { name: string; path: string; sizeBytes: number }[]; skipped: string[] }; }; resolve(json.data); }); } else { let errorMsg = `Upload failed (${xhr.status})`; try { const body = JSON.parse(xhr.responseText) as ErrorBody; if (body.error) errorMsg = body.error; } catch { /* ignore */ } settle(() => reject(new Error(errorMsg))); } }); xhr.addEventListener("error", () => settle(() => reject(new Error("Upload failed: network error")))); xhr.addEventListener("abort", () => settle(() => reject(createAbortError()))); xhr.send(formData); }, ); } export async function searchFilesRequest(params: { query: string; basePath?: string; includeHidden?: boolean; }) { const searchParams = new URLSearchParams({ q: params.query }); if (params.basePath) searchParams.set("path", params.basePath); if (params.includeHidden) searchParams.set("includeHidden", "true"); const endpoint = `/api/v1/files/search?${searchParams.toString()}`; const response = await fetch(endpoint, { cache: "no-store" }); if (!response.ok) { const errorBody = await getErrorBody(response); throw new Error( errorBody.error ?? `Search failed (${response.status})${errorBody.code ? ` [${errorBody.code}]` : ""}`, ); } const json = (await response.json()) as ListFilesApiResponse; return json.data; }