import { useMemo, useRef, useState } from "react"; import { downloadBlob, outputStem } from "../../archive/downloads"; import { errorMessage } from "../../archive/errors"; import { ARCHIVE_LIMITS, formatBytes } from "../../archive/limits"; import { previewEntry } from "../../archive/preview"; import { createSafeSelectionZip, inspectArchive, reportJson, } from "../../archive/service"; import type { ArchiveDocument, ArchiveEntryRecord, PreviewResult, ProgressUpdate, } from "../../archive/types"; import { ArchiveSummaryView } from "./ArchiveSummaryView"; import { PreviewPane } from "./PreviewPane"; import { StatusMessage } from "./StatusMessage"; type EntryFilter = "all" | "safe" | "blocked"; export function InspectWorkspace() { const [document, setDocument] = useState(); const [selected, setSelected] = useState>(new Set()); const [query, setQuery] = useState(""); const [filter, setFilter] = useState("all"); const [busy, setBusy] = useState(false); const [status, setStatus] = useState(); const [error, setError] = useState(); const [activeEntry, setActiveEntry] = useState(); const [preview, setPreview] = useState(); const [previewBusy, setPreviewBusy] = useState(false); const [previewError, setPreviewError] = useState(); const operation = useRef(undefined); const generation = useRef(0); const visible = useMemo(() => { if (!document) return []; const normalizedQuery = query.trim().toLocaleLowerCase("en-US"); return document.entries .filter( (entry) => !normalizedQuery || entry.path.toLocaleLowerCase("en-US").includes(normalizedQuery), ) .filter( (entry) => filter === "all" || (filter === "safe" ? entry.extractable : !entry.extractable && entry.kind !== "directory"), ) .slice(0, ARCHIVE_LIMITS.maxDisplayedEntries); }, [document, filter, query]); async function openFile(file?: File) { if (!file) return; operation.current?.abort(); const controller = new AbortController(); operation.current = controller; const current = ++generation.current; setBusy(true); setPreviewBusy(false); setError(undefined); setStatus(`Inspecting ${file.name}…`); try { const next = await inspectArchive(file, controller.signal); if (current !== generation.current) return; setDocument(next); setSelected(new Set()); setActiveEntry(undefined); setPreview(undefined); setPreviewError(undefined); setStatus( `Inspected ${next.entries.length.toLocaleString()} entries locally.`, ); } catch (reason) { if (controller.signal.aborted) return; if (current === generation.current) setError(errorMessage(reason)); } finally { if (current === generation.current) setBusy(false); } } async function showPreview(entry: ArchiveEntryRecord) { if (!document) return; operation.current?.abort(); const controller = new AbortController(); operation.current = controller; const current = ++generation.current; setActiveEntry(entry); setPreview(undefined); setPreviewBusy(true); setPreviewError(undefined); try { const next = await previewEntry(document, entry, controller.signal); if (current === generation.current) setPreview(next); } catch (reason) { if (!controller.signal.aborted && current === generation.current) setPreviewError(errorMessage(reason)); } finally { if (current === generation.current) setPreviewBusy(false); } } async function extractSelection() { if (!document) return; operation.current?.abort(); const controller = new AbortController(); operation.current = controller; const current = ++generation.current; setBusy(true); setPreviewBusy(false); setError(undefined); try { const blob = await createSafeSelectionZip( document, [...selected], controller.signal, progressStatus(setStatus), ); if (current !== generation.current) return; downloadBlob(blob, `${outputStem(document.name)}-safe-selection.zip`); setStatus( `Verified and repackaged ${selected.size} safe files into a new ZIP.`, ); } catch (reason) { if (!controller.signal.aborted && current === generation.current) setError(errorMessage(reason)); } finally { if (current === generation.current) setBusy(false); } } function toggle(entry: ArchiveEntryRecord) { setSelected((current) => { const next = new Set(current); if (next.has(entry.id)) next.delete(entry.id); else next.add(entry.id); return next; }); } function cancel() { generation.current += 1; operation.current?.abort( new DOMException("Operation cancelled.", "AbortError"), ); setBusy(false); setPreviewBusy(false); setStatus("Operation cancelled."); } const safeVisible = visible.filter((entry) => entry.extractable); return (

ZIP · TAR · gzip

Inspect and safely repackage

{busy || previewBusy ? ( ) : null}

Encrypted and multipart ZIPs, nested expansion, symlinks, hardlinks, devices and FIFOs are intentionally unsupported.

{document ? ( ) : (
Open a local archive It is inspected in this browser under fixed path, count, size and ratio limits.
)} {document ? (
{selected.size} selected
{visible.map((entry) => ( ))}
Select Path Kind Size State
toggle(entry)} /> {entry.kind} {formatBytes(entry.size)} issue.message) .join(" ")} > {entry.extractable ? "Safe file" : entry.kind === "directory" ? "Directory" : (entry.issues[0]?.message ?? "Blocked")}
{document.entries.length > ARCHIVE_LIMITS.maxDisplayedEntries ? (

Display capped at{" "} {ARCHIVE_LIMITS.maxDisplayedEntries.toLocaleString()} entries; use search to narrow the inventory.

) : null}
) : null}
); } function progressStatus(setStatus: (value: string) => void) { return (update: ProgressUpdate) => setStatus( `${update.stage}: ${update.completed.toLocaleString()} / ${update.total.toLocaleString()}`, ); }