CH-J Server ManagerSerververwaltung über SSH
Menü
Veröffentlichte Quellen

CH-J Server Manager

Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.

Quellen als ZIP herunterladen
CH-J Proprietary Software License 1.14

Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.

17,1 KB · 162 ZeilenDatei herunterladen
1"use strict";
3const api = window.chjPlugin.hashing;
4const i18n = window.chjHashI18n.createI18n();
5const t = (key, values) => i18n.t(key, values);
6const state = { algorithms: [], calculateSelection: null, verifySelection: null, compareA: null, compareB: null, manifestSource: null, manifestFile: null, manifestRoot: null, activeJobId: null, activeJob: null };
7const $ = (id) => document.getElementById(id);
9function setText(id, value) { $(id).textContent = value; }
10function describeSelection(selection) { return selection?.files?.map((file) => `${file.name} (${formatBytes(file.size)})`).join(", ") || selection?.name || t("Nothing selected"); }
11function formatBytes(value) { const bytes = Number(value || 0); if (bytes < 1024) return `${bytes} B`; const units = ["KiB", "MiB", "GiB", "TiB"]; let amount = bytes; let unit = -1; do { amount /= 1024; unit += 1; } while (amount >= 1024 && unit < units.length - 1); return `${new Intl.NumberFormat(i18n.getLanguage(), { minimumFractionDigits: amount >= 100 ? 0 : amount >= 10 ? 1 : 2, maximumFractionDigits: amount >= 100 ? 0 : amount >= 10 ? 1 : 2 }).format(amount)} ${units[unit]}`; }
12function formatDuration(ms) { const seconds = Math.max(0, Number(ms || 0) / 1000); if (seconds < 60) return `${seconds.toFixed(1)} s`; return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`; }
13function errorMessage(error) { return i18n.errorMessage(error); }
14async function action(callback) { try { await callback(); } catch (error) { setText("summary", errorMessage(error)); $("summary").className = "summary failed"; } }
16function buildAlgorithmList() {
17 const root = $("algorithmList"); root.replaceChildren();
18 const groups = new Map();
19 for (const algorithm of state.algorithms) {
20 if (!groups.has(algorithm.category)) groups.set(algorithm.category, []);
21 groups.get(algorithm.category).push(algorithm);
22 }
23 for (const [name, algorithms] of groups) {
24 const group = document.createElement("section"); group.className = "algorithm-group";
25 const heading = document.createElement("h4"); heading.dataset.category = name; heading.textContent = t(`category.${name}`); group.append(heading);
26 for (const algorithm of algorithms) {
27 const row = document.createElement("label"); row.className = "algorithm";
28 const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.dataset.algorithm = algorithm.id; checkbox.checked = ["sha256", "sha512", "blake3"].includes(algorithm.id);
29 const title = document.createElement("span"); title.textContent = algorithm.name; row.append(checkbox, title);
30 if (algorithm.xof || algorithm.seeded || algorithm.keyed) {
31 const advanced = document.createElement("span"); advanced.className = "advanced";
32 const input = document.createElement("input"); input.dataset.optionFor = algorithm.id;
33 if (algorithm.xof) { input.type = "number"; input.min = algorithm.xof.minBytes; input.max = algorithm.xof.maxBytes; input.value = algorithm.xof.defaultBytes; input.title = t("Output length in bytes"); input.placeholder = t("Output bytes"); }
34 else if (algorithm.seeded) { input.value = "0"; input.title = t("{bits}-bit seed", { bits: algorithm.seedBits }); input.placeholder = t("Seed (default 0)"); }
35 else { input.value = ""; input.title = t("{bytes}-byte hexadecimal key", { bytes: algorithm.keyBytes }); input.placeholder = t("HEX key ({bytes} bytes; zero key if blank)", { bytes: algorithm.keyBytes }); }
36 advanced.append(input); row.append(advanced);
37 }
38 group.append(row);
39 }
40 root.append(group);
41 }
44function algorithmRequest(algorithm, optionRoot = document) {
45 const request = { id: algorithm.id }; const input = optionRoot.querySelector(`[data-option-for="${algorithm.id}"]`);
46 if (algorithm.xof) request.outputBytes = Number(input?.value || algorithm.xof.defaultBytes);
47 if (algorithm.seeded) request.seed = input?.value || "0";
48 if (algorithm.keyed && input?.value.trim()) request.keyHex = input.value.trim();
49 return request;
51function selectedAlgorithms() { return state.algorithms.filter((algorithm) => document.querySelector(`[data-algorithm="${algorithm.id}"]`)?.checked).map((algorithm) => algorithmRequest(algorithm)); }
52function simpleAlgorithm(id) { const algorithm = state.algorithms.find((item) => item.id === id); return algorithmRequest(algorithm); }
54function populateSelect(id, filter = () => true) {
55 const select = $(id); const first = select.querySelector("option[value='']"); select.replaceChildren(); if (first) select.append(first);
56 for (const algorithm of state.algorithms.filter(filter)) { const option = document.createElement("option"); option.value = algorithm.id; option.textContent = algorithm.name; select.append(option); }
59async function chooseFiles(multiple) { const result = await api.selectFiles(multiple); return result.canceled ? null : result; }
60async function chooseDirectory() { const result = await api.selectDirectory(); return result.canceled ? null : result; }
61function acceptJob(job) { state.activeJobId = job.jobId; state.activeJob = job; renderJob(job); }
63function renderJob(job) {
64 if (!job || (state.activeJobId && job.jobId !== state.activeJobId)) return;
65 state.activeJob = job; state.activeJobId = job.jobId;
66 setText("jobState", `${t(job.state).toLocaleUpperCase(i18n.getLanguage())} · ${job.filesCompleted}/${job.filesTotal}${job.currentFile ? ` · ${job.currentFile.name}` : ""}`);
67 $("progressBar").style.width = `${Math.max(0, Math.min(100, job.percent || 0))}%`;
68 $("progressStats").replaceChildren(...[
69 `${formatBytes(job.bytes)} / ${formatBytes(job.totalBytes)}`,
70 `${Number(job.percent || 0).toFixed(1)}%`,
71 `${formatBytes(job.throughputBytesPerSecond)}/s`,
72 t("Elapsed {duration}", { duration: formatDuration(job.elapsedMs) }),
73 t("ETA {duration}", { duration: job.etaMs == null ? "—" : formatDuration(job.etaMs) })
74 ].map((text) => { const span = document.createElement("span"); span.textContent = text; return span; }));
75 $("cancel").classList.toggle("hidden", !["queued", "running"].includes(job.state));
76 $("copyAll").classList.toggle("hidden", !job.results.some((result) => result.hashes?.length));
77 $("exportResults").classList.toggle("hidden", job.state !== "completed" || !job.results.some((result) => result.hashes?.length));
78 const summary = $("summary"); summary.className = "summary";
79 if (job.error) { summary.textContent = `${job.error.code}: ${errorMessage(job.error)}`; summary.classList.add("failed"); }
80 else if (job.mode === "compare" && job.state === "completed") { const failed = job.results.some((result) => result.status !== "COMPLETED"); summary.textContent = t(failed ? "Comparison failed" : job.comparison?.identical ? "IDENTICAL digests" : "DIFFERENT digests"); summary.classList.add(failed ? "failed" : job.comparison?.identical ? "match" : "mismatch"); }
81 else if (job.mode === "verify" && job.results[0]) { summary.textContent = t(job.results[0].status); summary.classList.add(job.results[0].status === "MATCH" ? "match" : "mismatch"); }
82 else if (job.state === "completed") { const matches = job.results.filter((result) => result.status === "MATCH").length; const failures = job.results.filter((result) => !["COMPLETED", "MATCH"].includes(result.status)).length; summary.textContent = job.mode === "verify-manifest" ? t("{matches} matched · {failures} failed", { matches, failures }) : failures ? t("{completed} completed · {failures} failed", { completed: job.results.length - failures, failures }) : t("{count} file(s) completed", { count: job.results.length }); if (failures) summary.classList.add("failed"); }
83 else summary.textContent = "";
84 renderResults(job);
87function renderResults(job) {
88 const body = $("results"); body.replaceChildren();
89 for (const result of job.results) {
90 const hashes = result.hashes?.length ? result.hashes : [null];
91 for (const hash of hashes) {
92 const row = document.createElement("tr");
93 for (const value of [result.file.relativePath, formatBytes(result.file.size), hash?.id || "—", hash?.value || (result.error ? errorMessage(result.error) : "—"), t(result.status)]) { const cell = document.createElement("td"); cell.textContent = value; if (value === hash?.value) cell.className = "digest"; row.append(cell); }
94 row.children[4].className = `status-${String(result.status).toLowerCase().replace(/\s+/g, "-")}`;
95 const controls = document.createElement("td");
96 if (hash) { const copy = document.createElement("button"); copy.className = "small ghost"; copy.textContent = t("Copy"); copy.addEventListener("click", () => action(async () => { await api.copyResult({ jobId: job.jobId, fileId: result.file.fileId, algorithmId: hash.id, compact: true }); copy.textContent = t("Copied"); setTimeout(() => { copy.textContent = t("Copy"); }, 1200); })); controls.append(copy); }
97 row.append(controls); body.append(row);
98 }
99 }
102document.querySelectorAll(".tab").forEach((button) => button.addEventListener("click", () => {
103 document.querySelectorAll(".tab").forEach((item) => item.classList.toggle("active", item === button));
104 document.querySelectorAll(".tab-panel").forEach((panel) => panel.classList.toggle("active", panel.id === button.dataset.tab));
105}));
107$("closeButton").addEventListener("click", () => window.chjPlugin.close());
108$("calculateMode").addEventListener("change", () => $("recursiveLabel").classList.toggle("hidden", $("calculateMode").value !== "directory"));
109$("calculatePick").addEventListener("click", () => action(async () => { const mode = $("calculateMode").value; state.calculateSelection = mode === "directory" ? await chooseDirectory() : await chooseFiles(mode === "multiple"); setText("calculateSelection", describeSelection(state.calculateSelection)); }));
110$("selectAll").addEventListener("click", () => document.querySelectorAll("[data-algorithm]").forEach((input) => { input.checked = true; }));
111$("selectNone").addEventListener("click", () => document.querySelectorAll("[data-algorithm]").forEach((input) => { input.checked = false; }));
112$("recommended").addEventListener("click", () => document.querySelectorAll("[data-algorithm]").forEach((input) => { input.checked = ["sha256", "sha512", "blake3"].includes(input.dataset.algorithm); }));
113$("calculateStart").addEventListener("click", () => action(async () => { if (!state.calculateSelection) throw new Error(t("Choose local input first.")); const algorithms = selectedAlgorithms(); if (!algorithms.length) throw new Error(t("Select at least one algorithm.")); acceptJob(await api.start({ selectionId: state.calculateSelection.selectionId, algorithms, recursive: $("recursive").checked, output: $("output").value })); }));
115$("verifyPick").addEventListener("click", () => action(async () => { state.verifySelection = await chooseFiles(false); setText("verifySelection", describeSelection(state.verifySelection)); }));
116$("expected").addEventListener("input", () => { const value = $("expected").value.trim(); if ($("expectedEncoding").value !== "hex" || !/^[0-9a-f]+$/i.test(value)) return setText("detection", ""); const candidates = state.algorithms.filter((algorithm) => algorithm.digestBits && algorithm.digestBits / 4 === value.length); setText("detection", candidates.length ? t("Length candidates: {names}. Length alone is not definitive.", { names: candidates.map((item) => item.name).join(", ") }) : t("No fixed-length algorithm matches this length.")); });
117$("expectedEncoding").addEventListener("change", () => $("expected").dispatchEvent(new Event("input")));
118$("verifyStart").addEventListener("click", () => action(async () => { if (!state.verifySelection) throw new Error(t("Choose a local file first.")); acceptJob(await api.verify({ selectionId: state.verifySelection.selectionId, algorithm: simpleAlgorithm($("verifyAlgorithm").value), expected: $("expected").value, expectedEncoding: $("expectedEncoding").value })); }));
120$("comparePickA").addEventListener("click", () => action(async () => { state.compareA = await chooseFiles(false); setText("compareA", describeSelection(state.compareA)); }));
121$("comparePickB").addEventListener("click", () => action(async () => { state.compareB = await chooseFiles(false); setText("compareB", describeSelection(state.compareB)); }));
122$("compareStart").addEventListener("click", () => action(async () => { if (!state.compareA || !state.compareB) throw new Error(t("Choose both local files first.")); const algorithms = [...$("compareAlgorithm").selectedOptions].map((option) => simpleAlgorithm(option.value)); if (!algorithms.length) throw new Error(t("Select at least one comparison algorithm.")); acceptJob(await api.compare({ leftSelectionId: state.compareA.selectionId, rightSelectionId: state.compareB.selectionId, algorithms })); }));
124$("manifestSourcePick").addEventListener("click", () => action(async () => { state.manifestSource = $("manifestSourceMode").value === "directory" ? await chooseDirectory() : await chooseFiles(true); setText("manifestSource", describeSelection(state.manifestSource)); }));
125$("manifestFormat").addEventListener("change", () => { if ($("manifestFormat").value === "sfv") $("manifestAlgorithm").value = "crc32"; });
126$("manifestGenerate").addEventListener("click", () => action(async () => { if (!state.manifestSource) throw new Error(t("Choose local source files first.")); const id = $("manifestAlgorithm").value; const extension = $("manifestFormat").value === "sfv" ? "sfv" : (id === "crc32" ? "checksums" : id.replace(/-/g, "")); const destination = await api.selectManifestDestination(`checksums.${extension}`); if (destination.canceled) return; acceptJob(await api.generateManifest({ selectionId: state.manifestSource.selectionId, destinationSelectionId: destination.selectionId, algorithm: simpleAlgorithm(id), format: $("manifestFormat").value, recursive: $("manifestRecursive").checked })); }));
127$("manifestPick").addEventListener("click", () => action(async () => { const result = await api.selectManifest(); state.manifestFile = result.canceled ? null : result; setText("manifestSelection", describeSelection(result.canceled ? null : { files: [result.file] })); }));
128$("manifestRootPick").addEventListener("click", () => action(async () => { state.manifestRoot = await chooseDirectory(); setText("manifestRoot", describeSelection(state.manifestRoot)); }));
129$("manifestVerify").addEventListener("click", () => action(async () => { if (!state.manifestFile || !state.manifestRoot) throw new Error(t("Choose a manifest and its local root directory.")); acceptJob(await api.verifyManifest({ manifestSelectionId: state.manifestFile.selectionId, rootSelectionId: state.manifestRoot.selectionId, algorithmId: $("manifestVerifyAlgorithm").value || undefined })); }));
131$("cancel").addEventListener("click", () => action(async () => { if (state.activeJobId) renderJob(await api.cancel(state.activeJobId)); }));
132$("copyAll").addEventListener("click", () => action(async () => { await api.copyResult({ jobId: state.activeJobId }); $("copyAll").textContent = t("Copied"); setTimeout(() => { $("copyAll").textContent = t("Copy all"); }, 1200); }));
133$("exportResults").addEventListener("click", () => action(async () => { const destination = await api.selectManifestDestination("hash-results.txt"); if (!destination.canceled) await api.exportResults({ jobId: state.activeJobId, destinationSelectionId: destination.selectionId, format: "text" }); }));
135api.onProgress((job) => renderJob(job));
137function applyLanguage(language) {
138 i18n.setLanguage(language);
139 i18n.apply(document);
140 for (const [id, selection] of Object.entries({ calculateSelection: state.calculateSelection, verifySelection: state.verifySelection, compareA: state.compareA, compareB: state.compareB, manifestSource: state.manifestSource, manifestSelection: state.manifestFile ? { files: [state.manifestFile.file] } : null, manifestRoot: state.manifestRoot })) setText(id, describeSelection(selection));
141 document.querySelectorAll("[data-category]").forEach((heading) => { heading.textContent = t(`category.${heading.dataset.category}`); });
142 for (const algorithm of state.algorithms) {
143 const input = document.querySelector(`[data-option-for="${algorithm.id}"]`);
144 if (!input) continue;
145 if (algorithm.xof) { input.title = t("Output length in bytes"); input.placeholder = t("Output bytes"); }
146 else if (algorithm.seeded) { input.title = t("{bits}-bit seed", { bits: algorithm.seedBits }); input.placeholder = t("Seed (default 0)"); }
147 else { input.title = t("{bytes}-byte hexadecimal key", { bytes: algorithm.keyBytes }); input.placeholder = t("HEX key ({bytes} bytes; zero key if blank)", { bytes: algorithm.keyBytes }); }
148 }
149 $("expected").dispatchEvent(new Event("input"));
150 if (state.activeJob) renderJob(state.activeJob);
153window.chjPlugin.ui.onLanguageChanged(applyLanguage);
154action(async () => {
155 const [language, algorithms] = await Promise.all([window.chjPlugin.ui.getLanguage(), api.getAlgorithms()]);
156 state.algorithms = algorithms;
157 applyLanguage(language);
158 buildAlgorithmList();
159 const fixed = (algorithm) => !algorithm.xof;
160 populateSelect("verifyAlgorithm", fixed); populateSelect("compareAlgorithm", fixed); populateSelect("manifestAlgorithm", (algorithm) => fixed(algorithm) && !algorithm.keyed && !algorithm.seeded); populateSelect("manifestVerifyAlgorithm", fixed);
161 $("verifyAlgorithm").value = "sha256"; $("compareAlgorithm").value = "sha256"; $("manifestAlgorithm").value = "sha256";
162});

SHA-256: 28045a7c2c55ccaaf5ad0b8d941780f71b88927a9d67469f262b183e4cb90c8a

SHA-256 des Archivs: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0