mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-05-10 22:01:16 +00:00
fix: improve download/upload reliability and fix FILE_PARTS_INVALID
- Add downloadStarted flag to prevent false "stopped unexpectedly" errors when TDLib emits initial updateFile before download is active - Add 5-minute stall detection for both downloads and uploads - Reduce max split part size from 2GiB to 1950MiB to stay under Telegram's internal upload part count limits - Increase timeouts from max(10min, 15min/GB) to max(15min, 20min/GB) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,8 +6,12 @@ import { childLogger } from "../util/logger.js";
|
|||||||
|
|
||||||
const log = childLogger("split");
|
const log = childLogger("split");
|
||||||
|
|
||||||
/** 2GB in bytes — Telegram's file size limit */
|
/**
|
||||||
const MAX_PART_SIZE = 2n * 1024n * 1024n * 1024n;
|
* 1950 MiB — safely under Telegram's 2GB upload limit.
|
||||||
|
* At exactly 2GiB, TDLib's internal 512KB chunking can exceed Telegram's
|
||||||
|
* 4000-part threshold, causing FILE_PARTS_INVALID errors.
|
||||||
|
*/
|
||||||
|
const MAX_PART_SIZE = 1950n * 1024n * 1024n;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split a file into ≤2GB parts using byte-level splitting.
|
* Split a file into ≤2GB parts using byte-level splitting.
|
||||||
|
|||||||
@@ -353,11 +353,14 @@ export async function downloadFile(
|
|||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
let lastLoggedPercent = 0;
|
let lastLoggedPercent = 0;
|
||||||
let settled = false;
|
let settled = false;
|
||||||
|
let downloadStarted = false; // True once TDLib reports is_downloading_active
|
||||||
|
let lastProgressBytes = 0;
|
||||||
|
let lastProgressTime = Date.now();
|
||||||
|
|
||||||
// Timeout: 15 minutes per GB, minimum 10 minutes
|
// Timeout: 20 minutes per GB, minimum 15 minutes
|
||||||
const timeoutMs = Math.max(
|
const timeoutMs = Math.max(
|
||||||
10 * 60_000,
|
15 * 60_000,
|
||||||
(totalBytes / (1024 * 1024 * 1024)) * 15 * 60_000
|
(totalBytes / (1024 * 1024 * 1024)) * 20 * 60_000
|
||||||
);
|
);
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
if (!settled) {
|
if (!settled) {
|
||||||
@@ -371,6 +374,23 @@ export async function downloadFile(
|
|||||||
}
|
}
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
|
|
||||||
|
// Stall detection: no progress for 5 minutes after download started → reject
|
||||||
|
const STALL_TIMEOUT_MS = 5 * 60_000;
|
||||||
|
const stallChecker = setInterval(() => {
|
||||||
|
if (settled || !downloadStarted) return;
|
||||||
|
const stallMs = Date.now() - lastProgressTime;
|
||||||
|
if (stallMs >= STALL_TIMEOUT_MS) {
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Download stalled for ${fileName} — no progress for ${Math.round(stallMs / 60_000)}min ` +
|
||||||
|
`(${lastProgressBytes}/${totalBytes} bytes)`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, 30_000);
|
||||||
|
|
||||||
// Listen for file update events to track progress
|
// Listen for file update events to track progress
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const handleUpdate = (update: any) => {
|
const handleUpdate = (update: any) => {
|
||||||
@@ -382,6 +402,17 @@ export async function downloadFile(
|
|||||||
const percent =
|
const percent =
|
||||||
totalBytes > 0 ? Math.round((downloaded / totalBytes) * 100) : 0;
|
totalBytes > 0 ? Math.round((downloaded / totalBytes) * 100) : 0;
|
||||||
|
|
||||||
|
// Track whether the download has actually started
|
||||||
|
if (file.local.is_downloading_active) {
|
||||||
|
downloadStarted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset stall timer when bytes advance
|
||||||
|
if (downloaded > lastProgressBytes) {
|
||||||
|
lastProgressBytes = downloaded;
|
||||||
|
lastProgressTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
// Log at every 10% increment
|
// Log at every 10% increment
|
||||||
if (percent >= lastLoggedPercent + 10) {
|
if (percent >= lastLoggedPercent + 10) {
|
||||||
lastLoggedPercent = percent - (percent % 10);
|
lastLoggedPercent = percent - (percent % 10);
|
||||||
@@ -412,8 +443,11 @@ export async function downloadFile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download stopped without completing (network error, cancelled, etc.)
|
// Download stopped without completing — only if it had actually started.
|
||||||
|
// TDLib may emit an initial updateFile with is_downloading_active=false
|
||||||
|
// before the download begins; ignoring that prevents false positives.
|
||||||
if (
|
if (
|
||||||
|
downloadStarted &&
|
||||||
!file.local.is_downloading_active &&
|
!file.local.is_downloading_active &&
|
||||||
!file.local.is_downloading_completed
|
!file.local.is_downloading_completed
|
||||||
) {
|
) {
|
||||||
@@ -432,6 +466,7 @@ export async function downloadFile(
|
|||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
|
clearInterval(stallChecker);
|
||||||
client.off("update", handleUpdate);
|
client.off("update", handleUpdate);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -93,11 +93,13 @@ async function sendAndWaitForUpload(
|
|||||||
let settled = false;
|
let settled = false;
|
||||||
let lastLoggedPercent = 0;
|
let lastLoggedPercent = 0;
|
||||||
let tempMsgId: number | null = null;
|
let tempMsgId: number | null = null;
|
||||||
|
let uploadStarted = false;
|
||||||
|
let lastProgressTime = Date.now();
|
||||||
|
|
||||||
// Timeout: 15 minutes per GB, minimum 10 minutes
|
// Timeout: 20 minutes per GB, minimum 15 minutes
|
||||||
const timeoutMs = Math.max(
|
const timeoutMs = Math.max(
|
||||||
10 * 60_000,
|
15 * 60_000,
|
||||||
(fileSizeMB / 1024) * 15 * 60_000
|
(fileSizeMB / 1024) * 20 * 60_000
|
||||||
);
|
);
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@@ -112,12 +114,31 @@ async function sendAndWaitForUpload(
|
|||||||
}
|
}
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
|
|
||||||
|
// Stall detection: no progress for 5 minutes after upload started → reject
|
||||||
|
const STALL_TIMEOUT_MS = 5 * 60_000;
|
||||||
|
const stallChecker = setInterval(() => {
|
||||||
|
if (settled || !uploadStarted) return;
|
||||||
|
const stallMs = Date.now() - lastProgressTime;
|
||||||
|
if (stallMs >= STALL_TIMEOUT_MS) {
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Upload stalled for ${fileName} — no progress for ${Math.round(stallMs / 60_000)}min`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, 30_000);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const handleUpdate = (update: any) => {
|
const handleUpdate = (update: any) => {
|
||||||
// Track upload progress via updateFile events
|
// Track upload progress via updateFile events
|
||||||
if (update?._ === "updateFile") {
|
if (update?._ === "updateFile") {
|
||||||
const file = update.file;
|
const file = update.file;
|
||||||
if (file?.remote?.is_uploading_active && file.expected_size > 0) {
|
if (file?.remote?.is_uploading_active && file.expected_size > 0) {
|
||||||
|
uploadStarted = true;
|
||||||
|
lastProgressTime = Date.now();
|
||||||
|
|
||||||
const uploaded = file.remote.uploaded_size ?? 0;
|
const uploaded = file.remote.uploaded_size ?? 0;
|
||||||
const total = file.expected_size;
|
const total = file.expected_size;
|
||||||
const percent = Math.round((uploaded / total) * 100);
|
const percent = Math.round((uploaded / total) * 100);
|
||||||
@@ -165,6 +186,7 @@ async function sendAndWaitForUpload(
|
|||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
|
clearInterval(stallChecker);
|
||||||
client.off("update", handleUpdate);
|
client.off("update", handleUpdate);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -999,7 +999,7 @@ async function processOneArchiveSet(
|
|||||||
(sum, p) => sum + p.fileSize,
|
(sum, p) => sum + p.fileSize,
|
||||||
0n
|
0n
|
||||||
);
|
);
|
||||||
const MAX_UPLOAD_SIZE = 2n * 1024n * 1024n * 1024n;
|
const MAX_UPLOAD_SIZE = 1950n * 1024n * 1024n; // Match split.ts MAX_PART_SIZE
|
||||||
const hasOversizedPart = archiveSet.parts.some((p) => p.fileSize > MAX_UPLOAD_SIZE);
|
const hasOversizedPart = archiveSet.parts.some((p) => p.fileSize > MAX_UPLOAD_SIZE);
|
||||||
|
|
||||||
if (hasOversizedPart) {
|
if (hasOversizedPart) {
|
||||||
|
|||||||
Reference in New Issue
Block a user