Add Velum project workflow and standalone migration tool
This commit is contained in:
83
src/restore-project-files.tsx
Normal file
83
src/restore-project-files.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Action, ActionPanel, Icon, List, showInFinder, showToast, Toast, useNavigation } from "@raycast/api";
|
||||
import { promises as fs } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { basename, dirname, join } from "node:path";
|
||||
import { useEffect, useState } from "react";
|
||||
import { depseudonymizeDocument } from "./velum";
|
||||
import { isRestorableDocument, listAllRuns, resolveWithin, restorePath, restoreText } from "./lib/velum-project";
|
||||
import type { ProjectRun } from "./lib/velum-project";
|
||||
|
||||
function FilePicker(props: { run: ProjectRun; directory: string }) {
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set(props.run.files.map((file) => file.output)));
|
||||
const [busy, setBusy] = useState(false);
|
||||
function toggle(path: string) {
|
||||
setSelected((current) => {
|
||||
const next = new Set(current);
|
||||
if (next.has(path)) next.delete(path);
|
||||
else next.add(path);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
async function exportFiles() {
|
||||
if (!selected.size) return;
|
||||
setBusy(true);
|
||||
const toast = await showToast({ style: Toast.Style.Animated, title: "Klartext-Kopien werden erstellt" });
|
||||
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const targetRoot = join(homedir(), "Downloads", "Velum", `${basename(props.run.projectName)}-${stamp}`);
|
||||
try {
|
||||
await fs.mkdir(targetRoot, { recursive: true, mode: 0o700 });
|
||||
const names = new Set<string>();
|
||||
for (const file of props.run.files.filter((item) => selected.has(item.output))) {
|
||||
const restoredName = restorePath(file.output, props.run.mapping, props.run.matchModes);
|
||||
if (names.has(restoredName)) throw new Error(`Mehrdeutiger Ausgabename: ${restoredName}`);
|
||||
names.add(restoredName);
|
||||
const source = resolveWithin(join(props.directory, "files"), file.output);
|
||||
const target = resolveWithin(targetRoot, restoredName);
|
||||
const data = await fs.readFile(source);
|
||||
const result = file.kind === "document" && isRestorableDocument(file.output)
|
||||
? await depseudonymizeDocument(basename(file.output), data, props.run.mapping)
|
||||
: Buffer.from(restoreText(data.toString("utf8"), props.run.mapping, props.run.matchModes), "utf8");
|
||||
await fs.mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
||||
await fs.writeFile(target, result, { flag: "wx", mode: 0o600 });
|
||||
}
|
||||
toast.style = Toast.Style.Success;
|
||||
toast.title = `${selected.size} Klartext-Dateien in Downloads`;
|
||||
await showInFinder(targetRoot);
|
||||
} catch (error) {
|
||||
toast.style = Toast.Style.Failure;
|
||||
toast.title = "Klartext-Export fehlgeschlagen";
|
||||
toast.message = (error as Error).message;
|
||||
} finally { setBusy(false); }
|
||||
}
|
||||
return <List isLoading={busy} navigationTitle={`${props.run.projectName}: Dateien für Klartext wählen`}
|
||||
searchBarPlaceholder="Dateien filtern">
|
||||
<List.Section title={`${selected.size} von ${props.run.files.length} Dateien ausgewählt`}>
|
||||
{props.run.files.map((file) => <List.Item key={file.output}
|
||||
icon={selected.has(file.output) ? Icon.CheckCircle : Icon.Circle}
|
||||
title={restorePath(file.output, props.run.mapping, props.run.matchModes)} subtitle={file.output}
|
||||
actions={<ActionPanel>
|
||||
<Action title={selected.has(file.output) ? "Datei abwählen" : "Datei auswählen"} onAction={() => toggle(file.output)} />
|
||||
<Action title="Auswahl nach Downloads rückübersetzen" icon={Icon.Download} onAction={exportFiles} />
|
||||
<Action title="Alle auswählen" onAction={() => setSelected(new Set(props.run.files.map((item) => item.output)))} />
|
||||
<Action title="Keine auswählen" onAction={() => setSelected(new Set())} />
|
||||
</ActionPanel>} />)}
|
||||
</List.Section>
|
||||
</List>;
|
||||
}
|
||||
|
||||
export default function Command() {
|
||||
const { push } = useNavigation();
|
||||
const [runs, setRuns] = useState<Array<{ run: ProjectRun; directory: string }>>([]);
|
||||
const [busy, setBusy] = useState(true);
|
||||
useEffect(() => {
|
||||
listAllRuns().then(setRuns).catch((error: Error) => showToast({ style: Toast.Style.Failure,
|
||||
title: "Projektstände nicht lesbar", message: error.message })).finally(() => setBusy(false));
|
||||
}, []);
|
||||
return <List isLoading={busy} navigationTitle="Projektstand rückübersetzen" searchBarPlaceholder="Projekt suchen">
|
||||
<List.EmptyView title="Noch keine Projektstände" description="Zuerst Dateien mit Velum pseudonymisieren." />
|
||||
{runs.map(({ run, directory }) => <List.Item key={run.id} icon={Icon.Folder}
|
||||
title={run.projectName} subtitle={run.projectPath}
|
||||
accessories={[{ text: `${run.files.length} Dateien` }, { text: new Date(run.createdAt).toLocaleString("de-AT") }]}
|
||||
actions={<ActionPanel><Action title="Dateien wählen" onAction={() => push(<FilePicker run={run} directory={directory} />)} /></ActionPanel>} />)}
|
||||
</List>;
|
||||
}
|
||||
Reference in New Issue
Block a user