Harden startup recovery and stats edge cases
This commit is contained in:
parent
484819325e
commit
17604910b5
@ -406,6 +406,9 @@ function shouldRejectSuspiciousSmallDownload(
|
||||
return expected > 0 || binaryLike;
|
||||
}
|
||||
if (size < 512) {
|
||||
if (expected > 0 && size >= expected && binaryLike) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (size >= MINI_DOWNLOAD_RETRY_THRESHOLD_BYTES) {
|
||||
@ -600,6 +603,11 @@ function shouldAutoRetryExtraction(statusText: string): boolean {
|
||||
return !isExtractedLabel(statusText) && !isExtractErrorLabel(statusText);
|
||||
}
|
||||
|
||||
function shouldPreserveExtractionResumeLabel(statusText: string): boolean {
|
||||
const text = String(statusText || "").trim();
|
||||
return isTransientExtractStatus(text) || /^entpacken abgebrochen\b/i.test(text);
|
||||
}
|
||||
|
||||
function formatExtractDone(elapsedMs: number): string {
|
||||
if (elapsedMs < 1000) return "Entpackt - Done (<1s)";
|
||||
const secs = elapsedMs / 1000;
|
||||
@ -1818,6 +1826,7 @@ export class DownloadManager extends EventEmitter {
|
||||
const stats = {
|
||||
totalDownloaded: this.sessionDownloadedBytes,
|
||||
totalDownloadedAllTime: this.settings.totalDownloadedAllTime,
|
||||
totalFiles: this.sessionCompletedFiles,
|
||||
totalFilesSession: this.sessionCompletedFiles,
|
||||
totalFilesAllTime: this.settings.totalCompletedFilesAllTime,
|
||||
totalPackages: this.session.packageOrder.length,
|
||||
@ -1858,6 +1867,9 @@ export class DownloadManager extends EventEmitter {
|
||||
}
|
||||
const now = nowMs();
|
||||
if (!this.foldRuntimeIntoSettings(now)) {
|
||||
if (sync && !fs.existsSync(this.storagePaths.configFile)) {
|
||||
saveSettings(this.storagePaths, this.settings);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.lastSettingsPersistAt = now;
|
||||
@ -1873,13 +1885,51 @@ export class DownloadManager extends EventEmitter {
|
||||
this.statsCacheAt = 0;
|
||||
}
|
||||
|
||||
private resetSessionTotalsIfQueueEmpty(): void {
|
||||
private resetSessionTotalsIfQueueEmpty(force = false): void {
|
||||
if (this.itemCount > 0 || this.session.packageOrder.length > 0) {
|
||||
return;
|
||||
}
|
||||
if (Object.keys(this.session.items).length > 0 || Object.keys(this.session.packages).length > 0) {
|
||||
return;
|
||||
}
|
||||
if (!force && (this.sessionDownloadedBytes > 0 || this.sessionCompletedFiles > 0 || this.itemContributedBytes.size > 0)) {
|
||||
return;
|
||||
}
|
||||
let changed = false;
|
||||
if (this.session.totalDownloadedBytes !== 0) {
|
||||
this.session.totalDownloadedBytes = 0;
|
||||
changed = true;
|
||||
}
|
||||
if (this.sessionDownloadedBytes !== 0) {
|
||||
this.sessionDownloadedBytes = 0;
|
||||
changed = true;
|
||||
}
|
||||
if (this.sessionCompletedFiles !== 0) {
|
||||
this.sessionCompletedFiles = 0;
|
||||
changed = true;
|
||||
}
|
||||
if (this.session.runStartedAt !== 0) {
|
||||
this.session.runStartedAt = 0;
|
||||
changed = true;
|
||||
}
|
||||
if (this.session.summaryText) {
|
||||
this.session.summaryText = "";
|
||||
changed = true;
|
||||
}
|
||||
if (this.lastGlobalProgressBytes !== 0) {
|
||||
this.lastGlobalProgressBytes = 0;
|
||||
changed = true;
|
||||
}
|
||||
if (this.speedEvents.length > 0 || this.speedBytesLastWindow !== 0 || this.speedBytesPerPackage.size > 0) {
|
||||
this.speedEvents = [];
|
||||
this.speedBytesLastWindow = 0;
|
||||
this.speedBytesPerPackage.clear();
|
||||
this.speedEventsHead = 0;
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
this.invalidateStatsCache();
|
||||
}
|
||||
}
|
||||
|
||||
private getPackagePostProcessVersion(packageId: string): number {
|
||||
@ -2223,7 +2273,7 @@ export class DownloadManager extends EventEmitter {
|
||||
this.packagePostProcessWaiters = [];
|
||||
this.summary = null;
|
||||
this.nonResumableActive = 0;
|
||||
this.resetSessionTotalsIfQueueEmpty();
|
||||
this.resetSessionTotalsIfQueueEmpty(true);
|
||||
this.persistNow();
|
||||
this.emitState(true);
|
||||
}
|
||||
@ -4663,7 +4713,7 @@ export class DownloadManager extends EventEmitter {
|
||||
pkg.status = "completed";
|
||||
}
|
||||
}
|
||||
this.resetSessionTotalsIfQueueEmpty();
|
||||
this.resetSessionTotalsIfQueueEmpty(true);
|
||||
this.persistSoon();
|
||||
}
|
||||
|
||||
@ -5178,6 +5228,10 @@ export class DownloadManager extends EventEmitter {
|
||||
}
|
||||
} catch {
|
||||
// file doesn't exist — reset to queued so it gets re-downloaded
|
||||
if (archiveLike && this.shouldPreserveMissingCompletedArchiveForStartupRecovery(item)) {
|
||||
logger.info(`revalidateCompleted: ${item.fileName} Quelle fehlt, belasse fuer Startup-Recovery`);
|
||||
continue;
|
||||
}
|
||||
logger.warn(`revalidateCompleted: ${item.fileName} Datei nicht gefunden, setze auf queued`);
|
||||
item.status = "queued";
|
||||
item.fullStatus = "Wartet";
|
||||
@ -5200,6 +5254,23 @@ export class DownloadManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
private shouldPreserveMissingCompletedArchiveForStartupRecovery(item: DownloadItem): boolean {
|
||||
if (!this.settings.autoExtract) {
|
||||
return false;
|
||||
}
|
||||
const pkg = this.session.packages[item.packageId];
|
||||
if (!pkg || pkg.cancelled || pkg.enabled === false) {
|
||||
return false;
|
||||
}
|
||||
const statusText = String(item.fullStatus || "").trim();
|
||||
if (isExtractedLabel(statusText) || isExtractErrorLabel(statusText)) {
|
||||
return false;
|
||||
}
|
||||
return /^Fertig\b/i.test(statusText)
|
||||
|| shouldPreserveExtractionResumeLabel(statusText)
|
||||
|| shouldAutoRetryExtraction(statusText);
|
||||
}
|
||||
|
||||
private tryFinalizeItemFromDisk(
|
||||
pkg: PackageEntry,
|
||||
item: DownloadItem,
|
||||
@ -5943,7 +6014,9 @@ export class DownloadManager extends EventEmitter {
|
||||
pkg.updatedAt = nowMs();
|
||||
for (const item of items) {
|
||||
if (item.status === "completed" && shouldAutoRetryExtraction(item.fullStatus)) {
|
||||
if (!shouldPreserveExtractionResumeLabel(item.fullStatus)) {
|
||||
item.fullStatus = "Entpacken - Ausstehend";
|
||||
}
|
||||
item.updatedAt = nowMs();
|
||||
}
|
||||
}
|
||||
@ -5964,7 +6037,9 @@ export class DownloadManager extends EventEmitter {
|
||||
pkg.updatedAt = nowMs();
|
||||
for (const item of items) {
|
||||
if (item.status === "completed" && shouldAutoRetryExtraction(item.fullStatus)) {
|
||||
if (!shouldPreserveExtractionResumeLabel(item.fullStatus)) {
|
||||
item.fullStatus = "Entpacken - Ausstehend";
|
||||
}
|
||||
item.updatedAt = nowMs();
|
||||
}
|
||||
}
|
||||
@ -8749,6 +8824,16 @@ export class DownloadManager extends EventEmitter {
|
||||
snippet = await fs.promises.readFile(effectiveTargetPath, "utf8");
|
||||
snippet = snippet.slice(0, 200).replace(/[\r\n]+/g, " ").trim();
|
||||
} catch { /* ignore */ }
|
||||
const exactTinyBinary = Boolean(
|
||||
item.totalBytes
|
||||
&& item.totalBytes > 0
|
||||
&& written >= item.totalBytes
|
||||
&& isLargeBinaryLikePath(item.fileName || effectiveTargetPath)
|
||||
);
|
||||
const snippetSuggestsError = /<(?:!doctype|html|body)\b|\b(?:forbidden|access denied|error|not found|expired|unavailable)\b/i.test(snippet);
|
||||
if (exactTinyBinary && !snippetSuggestsError) {
|
||||
logger.info(`Tiny Binary akzeptiert (${written} B): ${item.fileName || effectiveTargetPath}`);
|
||||
} else {
|
||||
logger.warn(`Tiny download erkannt (${written} B): "${snippet}"`);
|
||||
try {
|
||||
await fs.promises.rm(effectiveTargetPath, { force: true });
|
||||
@ -8759,6 +8844,7 @@ export class DownloadManager extends EventEmitter {
|
||||
item.progressPercent = 0;
|
||||
throw new Error(`Download zu klein (${written} B) – Hoster-Fehlerseite?${snippet ? ` Inhalt: "${snippet}"` : ""}`);
|
||||
}
|
||||
}
|
||||
|
||||
const exactLengthRequired = isLargeBinaryLikePath(item.fileName || effectiveTargetPath);
|
||||
if (item.totalBytes && item.totalBytes > 0 && written < item.totalBytes) {
|
||||
|
||||
@ -42,6 +42,7 @@ export interface BandwidthScheduleEntry {
|
||||
export interface DownloadStats {
|
||||
totalDownloaded: number;
|
||||
totalDownloadedAllTime: number;
|
||||
totalFiles: number;
|
||||
totalFilesSession: number;
|
||||
totalFilesAllTime: number;
|
||||
totalPackages: number;
|
||||
|
||||
@ -6120,12 +6120,12 @@ describe("download manager", () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 140));
|
||||
expect(fs.existsSync(extractDir)).toBe(false);
|
||||
|
||||
await waitFor(() => !manager.getSnapshot().session.running, 30000);
|
||||
await waitFor(() => fs.existsSync(path.join(extractDir, "inside.txt")), 30000);
|
||||
|
||||
const snapshot = manager.getSnapshot();
|
||||
const item = Object.values(snapshot.session.items)[0];
|
||||
expect(item?.status).toBe("completed");
|
||||
expect(item?.fullStatus).toBe("Entpackt - Done");
|
||||
expect(item?.fullStatus.startsWith("Entpackt - Done")).toBe(true);
|
||||
expect(fs.existsSync(extractDir)).toBe(true);
|
||||
expect(fs.existsSync(path.join(extractDir, "inside.txt"))).toBe(true);
|
||||
} finally {
|
||||
@ -7111,7 +7111,7 @@ describe("download manager", () => {
|
||||
await waitFor(() => fs.existsSync(path.join(extractDir, "episode.txt")), 25000);
|
||||
const snapshot = manager.getSnapshot();
|
||||
expect(snapshot.session.packages[packageId]?.status).toBe("completed");
|
||||
expect(snapshot.session.items[itemId]?.fullStatus).toBe("Entpackt - Done");
|
||||
expect(snapshot.session.items[itemId]?.fullStatus.startsWith("Entpackt - Done")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not fail startup post-processing when source package dir is missing but extract output exists", async () => {
|
||||
@ -7178,7 +7178,7 @@ describe("download manager", () => {
|
||||
await waitFor(() => manager.getSnapshot().session.items[itemId]?.fullStatus.startsWith("Entpackt"), 12000);
|
||||
const snapshot = manager.getSnapshot();
|
||||
expect(snapshot.session.packages[packageId]?.status).toBe("completed");
|
||||
expect(snapshot.session.items[itemId]?.fullStatus).toBe("Entpackt - Done");
|
||||
expect(snapshot.session.items[itemId]?.fullStatus.startsWith("Entpackt - Done")).toBe(true);
|
||||
});
|
||||
|
||||
it("marks missing source package dir as extracted instead of failed", async () => {
|
||||
@ -7850,7 +7850,7 @@ describe("download manager", () => {
|
||||
await waitFor(() => fs.existsSync(expectedPath), 12000);
|
||||
const snapshot = manager.getSnapshot();
|
||||
expect(snapshot.session.packages[packageId]?.status).toBe("completed");
|
||||
expect(snapshot.session.items[itemId]?.fullStatus).toBe("Entpackt - Done");
|
||||
expect(snapshot.session.items[itemId]?.fullStatus.startsWith("Entpackt - Done")).toBe(true);
|
||||
expect(fs.existsSync(expectedPath)).toBe(true);
|
||||
expect(fs.existsSync(originalExtractedPath)).toBe(false);
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user