feat(update): implement automatic retry for GitHub raw file downloads

- Added functionality to retry downloads from GitHub on encountering rate limit and server error status codes (429, 502, 503, 504) with exponential backoff.
- Updated the changelog to reflect the new panel updater feature that enhances locale sync reliability during large updates.
- Included tests for the new retry logic to ensure proper handling of retryable status codes.
This commit is contained in:
UNITRONIX
2026-07-06 20:22:18 +02:00
parent c907b422dd
commit e4fb2fd1b0
3 changed files with 48 additions and 2 deletions
+1
View File
@@ -3,6 +3,7 @@
### Added
- **Web Remote — File Transfer drop zone:** left panel shows a drag-and-drop upload box (click to pick files, multi-file supported). Uploads go to the folder currently open on the remote side; progress appears in the Transfers column.
- **Web Remote — RustDesk file transfer parity:** 128 KB blocks, zstd upload compression, resume via `offset_blk` / `transferred_size`, overwrite confirmation dialog (Skip / Overwrite / apply to all), remote context menu (rename, delete, new folder), hidden-files toggle, and Resume in the transfer queue after errors.
- **Panel updater:** GitHub raw file downloads retry automatically on HTTP 429/502/503/504 (exponential backoff), reducing failed locale syncs during large updates.
### Changed
- _(none yet)_
+36 -2
View File
@@ -349,9 +349,17 @@ function ghGet(urlPath, { bypassCache = false } = {}) {
}
/**
* Download raw file content from GitHub (binary-safe).
* Download raw file content from GitHub (binary-safe), with retry on rate limits.
*/
function ghDownloadFile(owner, repo, ref, filePath) {
function isRetryableDownloadStatus(statusCode) {
return statusCode === 429 || statusCode === 502 || statusCode === 503 || statusCode === 504;
}
function getDownloadRetryDelayMs(attempt) {
return Math.min(1000 * Math.pow(2, Math.max(0, attempt - 1)), 15000);
}
function ghDownloadFileOnce(owner, repo, ref, filePath) {
const url = `https://raw.githubusercontent.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${encodeURIComponent(ref)}/${filePath}`;
return new Promise((resolve, reject) => {
const headers = { 'User-Agent': USER_AGENT };
@@ -376,6 +384,30 @@ function ghDownloadFile(owner, repo, ref, filePath) {
});
}
async function ghDownloadFile(owner, repo, ref, filePath, opts = {}) {
const maxAttempts = Number(opts.maxAttempts) > 0 ? Number(opts.maxAttempts) : 4;
let lastErr;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await ghDownloadFileOnce(owner, repo, ref, filePath);
} catch (err) {
lastErr = err;
const match = /Download failed \((\d+)\)/.exec(err.message || '');
const statusCode = match ? Number(match[1]) : 0;
if (!isRetryableDownloadStatus(statusCode) || attempt >= maxAttempts) {
throw err;
}
const delayMs = getDownloadRetryDelayMs(attempt);
console.warn(
`[UPDATE] Download retry ${attempt}/${maxAttempts} for ${filePath}`
+ ` after ${delayMs}ms (${err.message})`
);
await new Promise((r) => setTimeout(r, delayMs));
}
}
throw lastErr;
}
// ======================== Docker image deployment ========================
function shasMatch(a, b) {
@@ -3256,6 +3288,8 @@ module.exports = {
isNonCriticalUpdateFailure,
GITHUB_COMPARE_FILE_LIMIT,
isCompareLikelyTruncated,
isRetryableDownloadStatus,
getDownloadRetryDelayMs,
resolveConsoleRequire,
collectConsoleRequiredFiles,
isResolvedByIndexModule,
@@ -4,6 +4,8 @@ const { createConsoleDeployGraph } = require('../lib/consoleDeployGraph');
const {
GITHUB_COMPARE_FILE_LIMIT,
isCompareLikelyTruncated,
isRetryableDownloadStatus,
getDownloadRetryDelayMs,
} = require('../services/updateService');
describe('updateService console sync helpers', () => {
@@ -50,4 +52,13 @@ describe('updateService console sync helpers', () => {
expect(required.has('routes/auth.routes.js')).toBe(true);
expect(required.has('services/serverAttestation.js')).toBe(true);
});
test('retries GitHub raw downloads on rate limit status codes', () => {
expect(isRetryableDownloadStatus(429)).toBe(true);
expect(isRetryableDownloadStatus(503)).toBe(true);
expect(isRetryableDownloadStatus(404)).toBe(false);
expect(getDownloadRetryDelayMs(1)).toBe(1000);
expect(getDownloadRetryDelayMs(2)).toBe(2000);
expect(getDownloadRetryDelayMs(6)).toBe(15000);
});
});