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;
|
return expected > 0 || binaryLike;
|
||||||
}
|
}
|
||||||
if (size < 512) {
|
if (size < 512) {
|
||||||
|
if (expected > 0 && size >= expected && binaryLike) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (size >= MINI_DOWNLOAD_RETRY_THRESHOLD_BYTES) {
|
if (size >= MINI_DOWNLOAD_RETRY_THRESHOLD_BYTES) {
|
||||||
@ -600,6 +603,11 @@ function shouldAutoRetryExtraction(statusText: string): boolean {
|
|||||||
return !isExtractedLabel(statusText) && !isExtractErrorLabel(statusText);
|
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 {
|
function formatExtractDone(elapsedMs: number): string {
|
||||||
if (elapsedMs < 1000) return "Entpackt - Done (<1s)";
|
if (elapsedMs < 1000) return "Entpackt - Done (<1s)";
|
||||||
const secs = elapsedMs / 1000;
|
const secs = elapsedMs / 1000;
|
||||||
@ -1818,6 +1826,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
const stats = {
|
const stats = {
|
||||||
totalDownloaded: this.sessionDownloadedBytes,
|
totalDownloaded: this.sessionDownloadedBytes,
|
||||||
totalDownloadedAllTime: this.settings.totalDownloadedAllTime,
|
totalDownloadedAllTime: this.settings.totalDownloadedAllTime,
|
||||||
|
totalFiles: this.sessionCompletedFiles,
|
||||||
totalFilesSession: this.sessionCompletedFiles,
|
totalFilesSession: this.sessionCompletedFiles,
|
||||||
totalFilesAllTime: this.settings.totalCompletedFilesAllTime,
|
totalFilesAllTime: this.settings.totalCompletedFilesAllTime,
|
||||||
totalPackages: this.session.packageOrder.length,
|
totalPackages: this.session.packageOrder.length,
|
||||||
@ -1858,6 +1867,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
const now = nowMs();
|
const now = nowMs();
|
||||||
if (!this.foldRuntimeIntoSettings(now)) {
|
if (!this.foldRuntimeIntoSettings(now)) {
|
||||||
|
if (sync && !fs.existsSync(this.storagePaths.configFile)) {
|
||||||
|
saveSettings(this.storagePaths, this.settings);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.lastSettingsPersistAt = now;
|
this.lastSettingsPersistAt = now;
|
||||||
@ -1873,13 +1885,51 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.statsCacheAt = 0;
|
this.statsCacheAt = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private resetSessionTotalsIfQueueEmpty(): void {
|
private resetSessionTotalsIfQueueEmpty(force = false): void {
|
||||||
if (this.itemCount > 0 || this.session.packageOrder.length > 0) {
|
if (this.itemCount > 0 || this.session.packageOrder.length > 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Object.keys(this.session.items).length > 0 || Object.keys(this.session.packages).length > 0) {
|
if (Object.keys(this.session.items).length > 0 || Object.keys(this.session.packages).length > 0) {
|
||||||
return;
|
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 {
|
private getPackagePostProcessVersion(packageId: string): number {
|
||||||
@ -2223,7 +2273,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.packagePostProcessWaiters = [];
|
this.packagePostProcessWaiters = [];
|
||||||
this.summary = null;
|
this.summary = null;
|
||||||
this.nonResumableActive = 0;
|
this.nonResumableActive = 0;
|
||||||
this.resetSessionTotalsIfQueueEmpty();
|
this.resetSessionTotalsIfQueueEmpty(true);
|
||||||
this.persistNow();
|
this.persistNow();
|
||||||
this.emitState(true);
|
this.emitState(true);
|
||||||
}
|
}
|
||||||
@ -4663,7 +4713,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
pkg.status = "completed";
|
pkg.status = "completed";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.resetSessionTotalsIfQueueEmpty();
|
this.resetSessionTotalsIfQueueEmpty(true);
|
||||||
this.persistSoon();
|
this.persistSoon();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5178,6 +5228,10 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// file doesn't exist — reset to queued so it gets re-downloaded
|
// 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`);
|
logger.warn(`revalidateCompleted: ${item.fileName} Datei nicht gefunden, setze auf queued`);
|
||||||
item.status = "queued";
|
item.status = "queued";
|
||||||
item.fullStatus = "Wartet";
|
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(
|
private tryFinalizeItemFromDisk(
|
||||||
pkg: PackageEntry,
|
pkg: PackageEntry,
|
||||||
item: DownloadItem,
|
item: DownloadItem,
|
||||||
@ -5943,7 +6014,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
pkg.updatedAt = nowMs();
|
pkg.updatedAt = nowMs();
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.status === "completed" && shouldAutoRetryExtraction(item.fullStatus)) {
|
if (item.status === "completed" && shouldAutoRetryExtraction(item.fullStatus)) {
|
||||||
item.fullStatus = "Entpacken - Ausstehend";
|
if (!shouldPreserveExtractionResumeLabel(item.fullStatus)) {
|
||||||
|
item.fullStatus = "Entpacken - Ausstehend";
|
||||||
|
}
|
||||||
item.updatedAt = nowMs();
|
item.updatedAt = nowMs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5964,7 +6037,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
pkg.updatedAt = nowMs();
|
pkg.updatedAt = nowMs();
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.status === "completed" && shouldAutoRetryExtraction(item.fullStatus)) {
|
if (item.status === "completed" && shouldAutoRetryExtraction(item.fullStatus)) {
|
||||||
item.fullStatus = "Entpacken - Ausstehend";
|
if (!shouldPreserveExtractionResumeLabel(item.fullStatus)) {
|
||||||
|
item.fullStatus = "Entpacken - Ausstehend";
|
||||||
|
}
|
||||||
item.updatedAt = nowMs();
|
item.updatedAt = nowMs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -8749,6 +8824,16 @@ export class DownloadManager extends EventEmitter {
|
|||||||
snippet = await fs.promises.readFile(effectiveTargetPath, "utf8");
|
snippet = await fs.promises.readFile(effectiveTargetPath, "utf8");
|
||||||
snippet = snippet.slice(0, 200).replace(/[\r\n]+/g, " ").trim();
|
snippet = snippet.slice(0, 200).replace(/[\r\n]+/g, " ").trim();
|
||||||
} catch { /* ignore */ }
|
} 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}"`);
|
logger.warn(`Tiny download erkannt (${written} B): "${snippet}"`);
|
||||||
try {
|
try {
|
||||||
await fs.promises.rm(effectiveTargetPath, { force: true });
|
await fs.promises.rm(effectiveTargetPath, { force: true });
|
||||||
@ -8758,6 +8843,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
item.downloadedBytes = 0;
|
item.downloadedBytes = 0;
|
||||||
item.progressPercent = 0;
|
item.progressPercent = 0;
|
||||||
throw new Error(`Download zu klein (${written} B) – Hoster-Fehlerseite?${snippet ? ` Inhalt: "${snippet}"` : ""}`);
|
throw new Error(`Download zu klein (${written} B) – Hoster-Fehlerseite?${snippet ? ` Inhalt: "${snippet}"` : ""}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const exactLengthRequired = isLargeBinaryLikePath(item.fileName || effectiveTargetPath);
|
const exactLengthRequired = isLargeBinaryLikePath(item.fileName || effectiveTargetPath);
|
||||||
|
|||||||
@ -42,6 +42,7 @@ export interface BandwidthScheduleEntry {
|
|||||||
export interface DownloadStats {
|
export interface DownloadStats {
|
||||||
totalDownloaded: number;
|
totalDownloaded: number;
|
||||||
totalDownloadedAllTime: number;
|
totalDownloadedAllTime: number;
|
||||||
|
totalFiles: number;
|
||||||
totalFilesSession: number;
|
totalFilesSession: number;
|
||||||
totalFilesAllTime: number;
|
totalFilesAllTime: number;
|
||||||
totalPackages: number;
|
totalPackages: number;
|
||||||
|
|||||||
@ -6120,12 +6120,12 @@ describe("download manager", () => {
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 140));
|
await new Promise((resolve) => setTimeout(resolve, 140));
|
||||||
expect(fs.existsSync(extractDir)).toBe(false);
|
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 snapshot = manager.getSnapshot();
|
||||||
const item = Object.values(snapshot.session.items)[0];
|
const item = Object.values(snapshot.session.items)[0];
|
||||||
expect(item?.status).toBe("completed");
|
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(extractDir)).toBe(true);
|
||||||
expect(fs.existsSync(path.join(extractDir, "inside.txt"))).toBe(true);
|
expect(fs.existsSync(path.join(extractDir, "inside.txt"))).toBe(true);
|
||||||
} finally {
|
} finally {
|
||||||
@ -7111,7 +7111,7 @@ describe("download manager", () => {
|
|||||||
await waitFor(() => fs.existsSync(path.join(extractDir, "episode.txt")), 25000);
|
await waitFor(() => fs.existsSync(path.join(extractDir, "episode.txt")), 25000);
|
||||||
const snapshot = manager.getSnapshot();
|
const snapshot = manager.getSnapshot();
|
||||||
expect(snapshot.session.packages[packageId]?.status).toBe("completed");
|
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 () => {
|
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);
|
await waitFor(() => manager.getSnapshot().session.items[itemId]?.fullStatus.startsWith("Entpackt"), 12000);
|
||||||
const snapshot = manager.getSnapshot();
|
const snapshot = manager.getSnapshot();
|
||||||
expect(snapshot.session.packages[packageId]?.status).toBe("completed");
|
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 () => {
|
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);
|
await waitFor(() => fs.existsSync(expectedPath), 12000);
|
||||||
const snapshot = manager.getSnapshot();
|
const snapshot = manager.getSnapshot();
|
||||||
expect(snapshot.session.packages[packageId]?.status).toBe("completed");
|
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(expectedPath)).toBe(true);
|
||||||
expect(fs.existsSync(originalExtractedPath)).toBe(false);
|
expect(fs.existsSync(originalExtractedPath)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user