import { useState } from 'react'; import { SnapshotAPI, ImportMode, ImportConflict, ImportResult, } from '../api/snapshotApi'; import './SnapshotPanel.css'; export function SnapshotPanel() { const [exportPluginOutput, setExportPluginOutput] = useState(false); const [importFile, setImportFile] = useState(null); const [importMode, setImportMode] = useState('merge'); const [importConflict, setImportConflict] = useState('source'); const [importing, setImporting] = useState(false); const [importResult, setImportResult] = useState(null); const [importError, setImportError] = useState(null); const [wipeBusy, setWipeBusy] = useState(false); const [wipeMessage, setWipeMessage] = useState(null); const runImport = async (dryRun: boolean) => { if (!importFile) { setImportError('Pick a snapshot ZIP first.'); return; } setImporting(true); setImportError(null); setImportResult(null); try { const result = await SnapshotAPI.runImport(importFile, { mode: importMode, conflict: importMode === 'merge' ? importConflict : undefined, dryRun, }); setImportResult(result); } catch (e: any) { setImportError(e.message || String(e)); } finally { setImporting(false); } }; const runWipe = async () => { const ok = window.confirm( 'This deletes ALL metadata in Redis. Files on disk are not touched, but every manual ' + 'edit you made via this editor will be lost. Continue?' ); if (!ok) return; setWipeBusy(true); setWipeMessage(null); try { const r = await SnapshotAPI.wipe('metadata'); setWipeMessage(`Deleted ${r.metadataDeleted} file metadata entries.`); } catch (e: any) { setWipeMessage(`Failed: ${e.message || e}`); } finally { setWipeBusy(false); } }; return (

Snapshot

Export, import, or wipe the metadata KV store. Snapshots are portable ZIP archives — one nested-JSON file per CID, plus a manifest.

Export

Downloads every CID's metadata as a ZIP. Optional cache files can be added below.

Import

Mode
Conflict
{importError &&
{importError}
} {importResult && }

Wipe metadata

Deletes every file:* key plus file:__index__. Files on disk are untouched.

{wipeMessage &&
{wipeMessage}
}
); } function ImportResultView({ result }: { result: ImportResult }) { const totals = (result.files ?? []).reduce( (acc, f) => { acc.added += f.added; acc.updated += f.updated; acc.unchanged += f.unchanged; acc.kept += f.kept; acc.deleted += f.deleted; return acc; }, { added: 0, updated: 0, unchanged: 0, kept: 0, deleted: 0 } ); return (
{result.dryRun ? 'Dry run preview' : 'Import applied'} {result.filesOk} ok / {result.filesFailed} failed of {result.totalFiles} files mode={result.mode} {result.conflict ? `, conflict=${result.conflict}` : ''}
added updated unchanged kept (mine) deleted (replace)
{totals.added} {totals.updated} {totals.unchanged} {totals.kept} {totals.deleted}
{result.pluginOutput && (
plugin-output: {result.pluginOutput.written} written {result.pluginOutput.skipped > 0 ? `, ${result.pluginOutput.skipped} skipped (existed)` : ''}
)} {result.files && result.files.some((f) => f.error) && (
Per-file errors
    {result.files .filter((f) => f.error) .map((f) => (
  • {f.hashId}: {f.error}
  • ))}
)}
); }