diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 28d35ef..62a218a 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -1832,6 +1832,115 @@ export class DownloadManager extends EventEmitter { this.renamePathWithExdevFallback(sourcePath, targetPath); } + private cleanupNonMkvResidualFiles(rootDir: string, targetDir: string): number { + if (!rootDir || !this.existsSyncSafe(rootDir)) { + return 0; + } + + let removed = 0; + const stack = [rootDir]; + while (stack.length > 0) { + const current = stack.pop() as string; + let entries: fs.Dirent[] = []; + try { + entries = fs.readdirSync(current, { withFileTypes: true }); + } catch { + continue; + } + + for (const entry of entries) { + const fullPath = path.join(current, entry.name); + if (entry.isDirectory()) { + if (isPathInsideDir(fullPath, targetDir)) { + continue; + } + stack.push(fullPath); + continue; + } + if (!entry.isFile()) { + continue; + } + const extension = path.extname(entry.name).toLowerCase(); + if (extension === ".mkv") { + continue; + } + try { + fs.rmSync(toWindowsLongPathIfNeeded(fullPath), { force: true }); + removed += 1; + } catch { + // ignore and keep file + } + } + } + + return removed; + } + + private cleanupRemainingArchiveArtifacts(packageDir: string): number { + if (this.settings.cleanupMode === "none") { + return 0; + } + const candidates = findArchiveCandidates(packageDir); + if (candidates.length === 0) { + return 0; + } + + let removed = 0; + const dirFilesCache = new Map(); + const targets = new Set(); + for (const sourceFile of candidates) { + const dir = path.dirname(sourceFile); + let filesInDir = dirFilesCache.get(dir); + if (!filesInDir) { + try { + filesInDir = fs.readdirSync(dir, { withFileTypes: true }) + .filter((entry) => entry.isFile()) + .map((entry) => entry.name); + } catch { + filesInDir = []; + } + dirFilesCache.set(dir, filesInDir); + } + for (const target of collectArchiveCleanupTargets(sourceFile, filesInDir)) { + targets.add(target); + } + } + + for (const targetPath of targets) { + try { + if (!this.existsSyncSafe(targetPath)) { + continue; + } + if (this.settings.cleanupMode === "trash") { + const parsed = path.parse(targetPath); + const trashDir = path.join(parsed.dir, ".rd-trash"); + fs.mkdirSync(trashDir, { recursive: true }); + let moved = false; + for (let index = 0; index <= 1000; index += 1) { + const suffix = index === 0 ? "" : `-${index}`; + const candidate = path.join(trashDir, `${parsed.base}.${Date.now()}${suffix}`); + if (this.existsSyncSafe(candidate)) { + continue; + } + this.renamePathWithExdevFallback(targetPath, candidate); + moved = true; + break; + } + if (moved) { + removed += 1; + } + continue; + } + fs.rmSync(toWindowsLongPathIfNeeded(targetPath), { force: true }); + removed += 1; + } catch { + // ignore + } + } + + return removed; + } + private buildUniqueFlattenTargetPath(targetDir: string, sourcePath: string, reserved: Set): string { const parsed = path.parse(path.basename(sourcePath)); const extension = parsed.ext || ".mkv"; @@ -1912,6 +2021,10 @@ export class DownloadManager extends EventEmitter { } if (moved > 0 && fs.existsSync(sourceDir)) { + const removedResidual = this.cleanupNonMkvResidualFiles(sourceDir, targetDir); + if (removedResidual > 0) { + logger.info(`MKV-Sammelordner entfernte Restdateien: pkg=${pkg.name}, entfernt=${removedResidual}`); + } const removedDirs = this.removeEmptyDirectoryTree(sourceDir); if (removedDirs > 0) { logger.info(`MKV-Sammelordner entfernte leere Ordner: pkg=${pkg.name}, entfernt=${removedDirs}`); @@ -4488,7 +4601,15 @@ export class DownloadManager extends EventEmitter { } else { pkg.status = "completed"; } - if (pkg.status === "completed") { + + if (this.settings.autoExtract && alreadyMarkedExtracted && failed === 0 && success > 0 && this.settings.cleanupMode !== "none") { + const removedArchives = this.cleanupRemainingArchiveArtifacts(pkg.outputDir); + if (removedArchives > 0) { + logger.info(`Hybrid-Post-Cleanup entfernte Archive: pkg=${pkg.name}, entfernt=${removedArchives}`); + } + } + + if (success > 0 && (pkg.status === "completed" || pkg.status === "failed")) { this.collectMkvFilesToLibrary(packageId, pkg); } if (this.runPackageIds.has(packageId)) { diff --git a/src/main/update.ts b/src/main/update.ts index 0cddf39..84a3598 100644 --- a/src/main/update.ts +++ b/src/main/update.ts @@ -435,30 +435,54 @@ function deriveUpdateFileName(check: UpdateCheckResult, url: string): string { type ExpectedDigest = { algorithm: "sha256" | "sha512"; digest: string; + encoding: "hex" | "base64"; }; +function normalizeBase64Digest(raw: string): string { + return String(raw || "") + .trim() + .replace(/-/g, "+") + .replace(/_/g, "/"); +} + function parseExpectedDigest(raw: string): ExpectedDigest | null { const text = String(raw || "").trim(); const prefixed256 = text.match(/^sha256:([a-fA-F0-9]{64})$/i); if (prefixed256) { - return { algorithm: "sha256", digest: prefixed256[1].toLowerCase() }; + return { algorithm: "sha256", digest: prefixed256[1].toLowerCase(), encoding: "hex" }; } const prefixed512 = text.match(/^sha512:([a-fA-F0-9]{128})$/i); if (prefixed512) { - return { algorithm: "sha512", digest: prefixed512[1].toLowerCase() }; + return { algorithm: "sha512", digest: prefixed512[1].toLowerCase(), encoding: "hex" }; + } + const prefixed512Base64 = text.match(/^sha512:([A-Za-z0-9+/_-]{80,}={0,2})$/i); + if (prefixed512Base64) { + return { algorithm: "sha512", digest: normalizeBase64Digest(prefixed512Base64[1]), encoding: "base64" }; + } + const prefixed256Base64 = text.match(/^sha256:([A-Za-z0-9+/_-]{40,}={0,2})$/i); + if (prefixed256Base64) { + return { algorithm: "sha256", digest: normalizeBase64Digest(prefixed256Base64[1]), encoding: "base64" }; } const plain256 = text.match(/^([a-fA-F0-9]{64})$/); if (plain256) { - return { algorithm: "sha256", digest: plain256[1].toLowerCase() }; + return { algorithm: "sha256", digest: plain256[1].toLowerCase(), encoding: "hex" }; } const plain512 = text.match(/^([a-fA-F0-9]{128})$/); if (plain512) { - return { algorithm: "sha512", digest: plain512[1].toLowerCase() }; + return { algorithm: "sha512", digest: plain512[1].toLowerCase(), encoding: "hex" }; + } + const plain512Base64 = text.match(/^([A-Za-z0-9+/_-]{80,}={0,2})$/i); + if (plain512Base64) { + return { algorithm: "sha512", digest: normalizeBase64Digest(plain512Base64[1]), encoding: "base64" }; + } + const plain256Base64 = text.match(/^([A-Za-z0-9+/_-]{40,}={0,2})$/i); + if (plain256Base64) { + return { algorithm: "sha256", digest: normalizeBase64Digest(plain256Base64[1]), encoding: "base64" }; } return null; } -async function hashFile(filePath: string, algorithm: "sha256" | "sha512"): Promise { +async function hashFile(filePath: string, algorithm: "sha256" | "sha512", encoding: "hex" | "base64"): Promise { const hash = crypto.createHash(algorithm); const stream = fs.createReadStream(filePath, { highWaterMark: 1024 * 1024 }); return await new Promise((resolve, reject) => { @@ -466,7 +490,13 @@ async function hashFile(filePath: string, algorithm: "sha256" | "sha512"): Promi hash.update(typeof chunk === "string" ? Buffer.from(chunk) : chunk); }); stream.on("error", reject); - stream.on("end", () => resolve(hash.digest("hex").toLowerCase())); + stream.on("end", () => { + if (encoding === "base64") { + resolve(hash.digest("base64")); + return; + } + resolve(hash.digest("hex").toLowerCase()); + }); }); } @@ -496,8 +526,14 @@ async function verifyDownloadedInstaller(targetPath: string, expectedDigestRaw: logger.warn("Update-Asset ohne SHA-Digest; nur EXE-Basisprüfung durchgeführt"); return; } - const actualDigest = await hashFile(targetPath, expected.algorithm); - if (actualDigest !== expected.digest) { + const actualDigestRaw = await hashFile(targetPath, expected.algorithm, expected.encoding); + const actualDigest = expected.encoding === "base64" + ? normalizeBase64Digest(actualDigestRaw).replace(/=+$/g, "") + : actualDigestRaw; + const expectedDigest = expected.encoding === "base64" + ? normalizeBase64Digest(expected.digest).replace(/=+$/g, "") + : expected.digest; + if (actualDigest !== expectedDigest) { throw new Error(`Update-Integritätsprüfung fehlgeschlagen (${expected.algorithm.toUpperCase()} mismatch)`); } }