import { useEffect, useState, useRef, DragEvent, useCallback } from "react"; import { api, FileEntry, FileCapabilities, FilePreview, SearchHit, formatBytes } from "../api"; import Modal from "../components/Modal"; function FileIcon({ name }: { name: string }) { const ext = name.split(".").pop()?.toLowerCase() ?? ""; if (name === "..") return ; if (ext === "png" || ext === "jpg" || ext === "jpeg" || ext === "gif" || ext === "webp" || ext === "svg") return 🖼; if (ext === "mp4" || ext === "avi" || ext === "mkv" || ext === "mov") return 🎬; if (ext === "mp3" || ext === "wav" || ext === "ogg" || ext === "flac") return 🎵; if (ext === "zip" || ext === "tar" || ext === "gz" || ext === "rar" || ext === "7z") return 📦; if (ext === "pdf") return 📄; if (ext === "txt" || ext === "md" || ext === "log" || ext === "cfg" || ext === "conf" || ext === "json" || ext === "yaml" || ext === "yml" || ext === "toml" || ext === "xml") return 📝; return 📄; } function modeStr(mode: number): string { const perm = (mode & 0o777).toString(8).padStart(3, "0"); const type = mode & 0o170000; if (type === 0o40000) return `drwxr-xr-x`.slice(0, 10 - perm.length) + perm; if (type === 0o120000) return `lrwxr-xr-x`.slice(0, 10 - perm.length) + perm; return `-rwxr-xr-x`.slice(0, 10 - perm.length) + perm; } export default function Files() { const [roots, setRoots] = useState([]); const [unrestricted, setUnrestricted] = useState(false); const [currentPath, setCurrentPath] = useState("/"); const [entries, setEntries] = useState([]); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(false); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [capabilities, setCapabilities] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [searchResults, setSearchResults] = useState([]); const [searching, setSearching] = useState(false); const [pathInput, setPathInput] = useState(currentPath); const [editingPath, setEditingPath] = useState(false); const [showMkdir, setShowMkdir] = useState(false); const [mkdirName, setMkdirName] = useState(""); const [selectedEntry, setSelectedEntry] = useState(null); const [preview, setPreview] = useState(null); const [previewLoading, setPreviewLoading] = useState(false); const [showPreview, setShowPreview] = useState(false); const [renameState, setRenameState] = useState<{ entry: FileEntry; name: string } | null>(null); const [chmodState, setChmodState] = useState<{ entry: FileEntry; mode: string } | null>(null); const [chownState, setChownState] = useState<{ entry: FileEntry; uid: string; gid: string } | null>(null); const [isDragOver, setIsDragOver] = useState(false); const [uploading, setUploading] = useState(false); const fileInputRef = useRef(null); const LIMIT = 200; useEffect(() => { api.fileCapabilities().then(setCapabilities).catch(() => {}); api.fileRoots().then(r => { setRoots(r.roots ?? []); setUnrestricted(r.unrestricted); if ((r.roots ?? []).length > 0) { setCurrentPath(r.roots![0]); } else if (!r.unrestricted) { setCurrentPath("/"); } }).catch(() => {}); }, []); useEffect(() => { if (currentPath) { loadDir(currentPath, 1); } }, [currentPath]); useEffect(() => { setSearchResults([]); setSearchQuery(""); }, [currentPath]); useEffect(() => { if (!editingPath) setPathInput(currentPath); }, [currentPath, editingPath]); async function loadDir(path: string, pageNum: number) { setLoading(true); setError(null); try { const res = await api.listFiles(path, pageNum, LIMIT); const entries = res.entries ?? []; if (pageNum === 1) { setEntries(entries); } else { setEntries(prev => [...prev, ...entries]); } setPage(pageNum); setHasMore(res.has_more); setTotal(res.total); } catch (e) { setError(e instanceof Error ? e.message : "Error loading directory"); } finally { setLoading(false); } } function refresh() { loadDir(currentPath, 1); } function navigateTo(path: string) { setSelectedEntry(null); setPreview(null); setShowPreview(false); setCurrentPath(path); } async function handleNavigateToPath(e: React.FormEvent) { e.preventDefault(); const path = pathInput.trim() || "/"; try { const info = await api.fileInfo(path); if (!info.is_dir) { setError("La ruta no es un directorio"); return; } navigateTo(path); } catch (err) { setError(err instanceof Error ? err.message : "Ruta no accesible"); } } function handleDoubleClick(entry: FileEntry) { if (entry.is_dir) { navigateTo(entry.path); } else { openPreview(entry); } } async function openPreview(entry: FileEntry) { setSelectedEntry(entry); setPreviewLoading(true); setShowPreview(true); try { const p = await api.filePreview(entry.path); setPreview(p); } catch (e) { setPreview({ type: "binary", mime: "", size: entry.size }); } finally { setPreviewLoading(false); } } function parentDir(): string | null { if (currentPath === "/" || currentPath === "") return null; const parts = currentPath.split("/").filter(Boolean); parts.pop(); const parent = "/" + parts.join("/"); return parent === "/" ? "/" : parent; } async function handleMkdir() { if (!mkdirName.trim()) return; try { await api.mkdirFile(currentPath + "/" + mkdirName.trim()); setMkdirName(""); setShowMkdir(false); refresh(); } catch (e) { setError(e instanceof Error ? e.message : "Error creating folder"); } } async function handleRename() { if (!renameState) return; try { await api.renameFile(renameState.entry.path, renameState.name); setRenameState(null); refresh(); } catch (e) { setError(e instanceof Error ? e.message : "Error renaming"); } } async function handleChmod() { if (!chmodState) return; try { await api.chmodFile(chmodState.entry.path, chmodState.mode); setChmodState(null); refresh(); } catch (e) { setError(e instanceof Error ? e.message : "Error changing permissions"); } } async function handleChown() { if (!chownState) return; const uid = parseInt(chownState.uid, 10); const gid = parseInt(chownState.gid, 10); if (isNaN(uid) || isNaN(gid)) { setError("UID y GID deben ser numéricos"); return; } try { await api.chownFile(chownState.entry.path, uid, gid); setChownState(null); refresh(); } catch (e) { setError(e instanceof Error ? e.message : "Error changing owner"); } } async function handleDelete(entry: FileEntry) { if (!confirm(`¿Eliminar "${entry.name}"${entry.is_dir ? " y todo su contenido" : ""}?`)) return; try { await api.deleteFile(entry.path); if (selectedEntry?.path === entry.path) { setSelectedEntry(null); setPreview(null); setShowPreview(false); } refresh(); } catch (e) { setError(e instanceof Error ? e.message : "Error deleting"); } } async function handleUpload(fileList: FileList | null) { if (!fileList || fileList.length === 0) return; setUploading(true); for (const file of Array.from(fileList)) { try { await api.uploadFile(currentPath, file); } catch (e) { setError(e instanceof Error ? e.message : `Error uploading ${file.name}`); } } setUploading(false); refresh(); } async function doSearch(q: string) { if (!q.trim()) { setSearchResults([]); return; } setSearching(true); try { const res = await api.searchFiles(currentPath, q, 100); setSearchResults(res.results ?? []); } catch (e) { setError(e instanceof Error ? e.message : "Error searching"); } finally { setSearching(false); } } const handleSearchSubmit = useCallback((e: React.FormEvent) => { e.preventDefault(); doSearch(searchQuery); }, [searchQuery, currentPath]); function handleDropZone(e: DragEvent) { e.preventDefault(); setIsDragOver(false); handleUpload(e.dataTransfer.files); } function handleDragOver(e: DragEvent) { e.preventDefault(); setIsDragOver(true); } function handleDragLeave() { setIsDragOver(false); } function selectSearchResult(hit: SearchHit) { const dir = hit.path.substring(0, hit.path.lastIndexOf("/")) || "/"; navigateTo(dir); setSelectedEntry({ name: hit.name, path: hit.path, is_dir: hit.is_dir, size: hit.size, mode: "", mode_num: 0, mod_time: 0 }); } return (

Archivos

{error && (
{error}
)}
{(roots ?? []).length > 1 && ( )} {unrestricted && ( Sin restricción — acceso a todo el FS )}
setEditingPath(true)} onBlur={() => { setEditingPath(false); setPathInput(currentPath); }} onChange={e => setPathInput(e.target.value)} onKeyDown={e => { if (e.key === "Escape") { setPathInput(currentPath); (e.target as HTMLInputElement).blur(); } }} />
setSearchQuery(e.target.value)} />
{(searchResults ?? []).length > 0 && (
Resultados ({(searchResults ?? []).length})
{searchResults.map((hit, i) => (
selectSearchResult(hit)} onDoubleClick={() => { if (hit.is_dir) { navigateTo(hit.path); } else { setSelectedEntry({ name: hit.name, path: hit.path, is_dir: hit.is_dir, size: hit.size, mode: "", mode_num: 0, mod_time: 0 }); openPreview({ name: hit.name, path: hit.path, is_dir: hit.is_dir, size: hit.size, mode: "", mode_num: 0, mod_time: 0 }); } }} > {hit.path} {hit.is_dir ? "carpeta" : formatBytes(hit.size)}
))}
)}
handleUpload(e.target.files)} /> {capabilities?.chown && ( )} {capabilities?.chmod && ( )}
{isDragOver && (
Suelta para subir
)} {parentDir() !== null && ( navigateTo(parentDir()!)} > )} {entries.map(entry => ( setSelectedEntry(entry)} onDoubleClick={() => handleDoubleClick(entry)} > ))} {(entries ?? []).length === 0 && !loading && ( )} {loading && ( )}
Nombre Tamaño Permisos Modificado
..
{entry.name}
{entry.is_dir ? "—" : formatBytes(entry.size)} {modeStr(entry.mode_num)} {entry.mod_time > 0 ? new Date(entry.mod_time * 1000).toLocaleString() : "—"}
{capabilities?.chmod && ( )} {capabilities?.chown && ( )}
{searchQuery ? "Sin resultados" : "Directorio vacío"}
Cargando…
{hasMore && ( )} {showMkdir && ( setShowMkdir(false)} maxWidth="sm">
setMkdirName(e.target.value)} onKeyDown={e => e.key === "Enter" && handleMkdir()} autoFocus />
)} {renameState && ( setRenameState(null)} maxWidth="sm">
setRenameState({ ...renameState, name: e.target.value })} onKeyDown={e => e.key === "Enter" && handleRename()} autoFocus />
)} {chmodState && ( setChmodState(null)} maxWidth="sm">
setChmodState({ ...chmodState, mode: e.target.value })} onKeyDown={e => e.key === "Enter" && handleChmod()} autoFocus placeholder="755" /> octal (ej: 755, 644)
{(parseInt(chmodState.mode, 8) || 0).toString(8).padStart(3, "0")} = {[ ["r", (parseInt(chmodState.mode, 8) || 0) & 0o400], ["w", (parseInt(chmodState.mode, 8) || 0) & 0o200], ["x", (parseInt(chmodState.mode, 8) || 0) & 0o100], ].map(([c, v]) => c + (v ? "✓" : "✗")).join(" ")}
)} {chownState && ( setChownState(null)} maxWidth="sm">
setChownState({ ...chownState, uid: e.target.value })} placeholder="0" />
setChownState({ ...chownState, gid: e.target.value })} placeholder="0" />
)} {showPreview && selectedEntry && ( { setShowPreview(false); setPreview(null); }} maxWidth="3xl" >
{previewLoading ? (
Cargando…
) : preview?.type === "text" ? (
{preview.mime} · {formatBytes(preview.size)}
                  {preview.content}
                
) : preview?.type === "image" ? (
{preview.mime} · {formatBytes(preview.size)}
{selectedEntry.name}
) : (
Archivo binario
{preview?.mime} · {formatBytes(preview?.size ?? 0)}
⬇ Descargar
)} {!previewLoading && (
⬇ Descargar
)}
)}
); }