Fix MKV collection cleanup and updater digest verification
This commit is contained in:
parent
b0dc7b80ab
commit
7795208332
@ -1832,6 +1832,115 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.renamePathWithExdevFallback(sourcePath, targetPath);
|
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<string, string[]>();
|
||||||
|
const targets = new Set<string>();
|
||||||
|
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>): string {
|
private buildUniqueFlattenTargetPath(targetDir: string, sourcePath: string, reserved: Set<string>): string {
|
||||||
const parsed = path.parse(path.basename(sourcePath));
|
const parsed = path.parse(path.basename(sourcePath));
|
||||||
const extension = parsed.ext || ".mkv";
|
const extension = parsed.ext || ".mkv";
|
||||||
@ -1912,6 +2021,10 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (moved > 0 && fs.existsSync(sourceDir)) {
|
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);
|
const removedDirs = this.removeEmptyDirectoryTree(sourceDir);
|
||||||
if (removedDirs > 0) {
|
if (removedDirs > 0) {
|
||||||
logger.info(`MKV-Sammelordner entfernte leere Ordner: pkg=${pkg.name}, entfernt=${removedDirs}`);
|
logger.info(`MKV-Sammelordner entfernte leere Ordner: pkg=${pkg.name}, entfernt=${removedDirs}`);
|
||||||
@ -4488,7 +4601,15 @@ export class DownloadManager extends EventEmitter {
|
|||||||
} else {
|
} else {
|
||||||
pkg.status = "completed";
|
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);
|
this.collectMkvFilesToLibrary(packageId, pkg);
|
||||||
}
|
}
|
||||||
if (this.runPackageIds.has(packageId)) {
|
if (this.runPackageIds.has(packageId)) {
|
||||||
|
|||||||
@ -435,30 +435,54 @@ function deriveUpdateFileName(check: UpdateCheckResult, url: string): string {
|
|||||||
type ExpectedDigest = {
|
type ExpectedDigest = {
|
||||||
algorithm: "sha256" | "sha512";
|
algorithm: "sha256" | "sha512";
|
||||||
digest: string;
|
digest: string;
|
||||||
|
encoding: "hex" | "base64";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function normalizeBase64Digest(raw: string): string {
|
||||||
|
return String(raw || "")
|
||||||
|
.trim()
|
||||||
|
.replace(/-/g, "+")
|
||||||
|
.replace(/_/g, "/");
|
||||||
|
}
|
||||||
|
|
||||||
function parseExpectedDigest(raw: string): ExpectedDigest | null {
|
function parseExpectedDigest(raw: string): ExpectedDigest | null {
|
||||||
const text = String(raw || "").trim();
|
const text = String(raw || "").trim();
|
||||||
const prefixed256 = text.match(/^sha256:([a-fA-F0-9]{64})$/i);
|
const prefixed256 = text.match(/^sha256:([a-fA-F0-9]{64})$/i);
|
||||||
if (prefixed256) {
|
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);
|
const prefixed512 = text.match(/^sha512:([a-fA-F0-9]{128})$/i);
|
||||||
if (prefixed512) {
|
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})$/);
|
const plain256 = text.match(/^([a-fA-F0-9]{64})$/);
|
||||||
if (plain256) {
|
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})$/);
|
const plain512 = text.match(/^([a-fA-F0-9]{128})$/);
|
||||||
if (plain512) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function hashFile(filePath: string, algorithm: "sha256" | "sha512"): Promise<string> {
|
async function hashFile(filePath: string, algorithm: "sha256" | "sha512", encoding: "hex" | "base64"): Promise<string> {
|
||||||
const hash = crypto.createHash(algorithm);
|
const hash = crypto.createHash(algorithm);
|
||||||
const stream = fs.createReadStream(filePath, { highWaterMark: 1024 * 1024 });
|
const stream = fs.createReadStream(filePath, { highWaterMark: 1024 * 1024 });
|
||||||
return await new Promise<string>((resolve, reject) => {
|
return await new Promise<string>((resolve, reject) => {
|
||||||
@ -466,7 +490,13 @@ async function hashFile(filePath: string, algorithm: "sha256" | "sha512"): Promi
|
|||||||
hash.update(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
hash.update(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
||||||
});
|
});
|
||||||
stream.on("error", reject);
|
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");
|
logger.warn("Update-Asset ohne SHA-Digest; nur EXE-Basisprüfung durchgeführt");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const actualDigest = await hashFile(targetPath, expected.algorithm);
|
const actualDigestRaw = await hashFile(targetPath, expected.algorithm, expected.encoding);
|
||||||
if (actualDigest !== expected.digest) {
|
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)`);
|
throw new Error(`Update-Integritätsprüfung fehlgeschlagen (${expected.algorithm.toUpperCase()} mismatch)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user