Skip to content

Commit e02e052

Browse files
committed
Fix issue #7970: Add retry logic and validation to release download
- Add 3-attempt retry logic with progressive backoff (1s, 2s delays) - Validate download success (detect file_get_contents failures) - Validate downloaded file is not empty (prevents 0-byte ZIP issue) - Warn on suspiciously small files (<1MB) - Validate file write success - Log download size for debugging - User-friendly localized error messages
1 parent ddaf790 commit e02e052

File tree

1 file changed

+79
-3
lines changed

1 file changed

+79
-3
lines changed

src/ChurchCRM/dto/ChurchCRMReleaseManager.php

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,13 +226,89 @@ public static function downloadRelease(ChurchCRMRelease $release): array
226226
$logger->debug('Using temp directory: ' . $UpgradeDir);
227227
$logger->info('Downloading release from: ' . $url . ' to: ' . $UpgradeDir . '/' . basename($url));
228228
$executionTime = new ExecutionTime();
229-
file_put_contents($UpgradeDir . '/' . basename($url), file_get_contents($url));
229+
230+
// Download the file with retry logic (3 attempts total)
231+
$maxAttempts = 3;
232+
$downloadContent = false;
233+
$lastError = null;
234+
235+
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
236+
$logger->info('Download attempt ' . $attempt . ' of ' . $maxAttempts);
237+
238+
// Clear any previous error
239+
@error_clear_last();
240+
241+
$downloadContent = @file_get_contents($url);
242+
243+
// Check if download succeeded and content is not empty
244+
if ($downloadContent !== false && strlen($downloadContent) > 0) {
245+
$logger->info('Download succeeded on attempt ' . $attempt);
246+
break;
247+
}
248+
249+
// Capture the error for logging
250+
$lastError = error_get_last();
251+
$errorMsg = $lastError['message'] ?? 'Unknown error or empty response';
252+
253+
if ($attempt < $maxAttempts) {
254+
$logger->warning('Download attempt ' . $attempt . ' failed, retrying...', [
255+
'url' => $url,
256+
'error' => $errorMsg,
257+
]);
258+
// Wait before retry (1 second, then 2 seconds)
259+
sleep($attempt);
260+
}
261+
}
262+
263+
// Check if all attempts failed
264+
if ($downloadContent === false) {
265+
$errorMsg = $lastError['message'] ?? 'Unknown error';
266+
$logger->error('Failed to download release file after ' . $maxAttempts . ' attempts', [
267+
'url' => $url,
268+
'error' => $errorMsg,
269+
]);
270+
throw new \Exception(gettext('Failed to download the release file from GitHub after multiple attempts. Please check your server\'s internet connection and try again.') . ' Error: ' . $errorMsg);
271+
}
272+
273+
// Check if downloaded content is empty
274+
$downloadSize = strlen($downloadContent);
275+
if ($downloadSize === 0) {
276+
$logger->error('Downloaded release file is empty after ' . $maxAttempts . ' attempts', [
277+
'url' => $url,
278+
'size' => 0,
279+
]);
280+
throw new \Exception(gettext('Downloaded release file is empty. This may be due to network issues or GitHub rate limiting. Please try again later.'));
281+
}
282+
283+
// Minimum expected size for a ChurchCRM release ZIP (at least 1MB)
284+
$minExpectedSize = 1024 * 1024;
285+
if ($downloadSize < $minExpectedSize) {
286+
$logger->warning('Downloaded file is smaller than expected', [
287+
'url' => $url,
288+
'size' => $downloadSize,
289+
'minExpectedSize' => $minExpectedSize,
290+
]);
291+
}
292+
293+
$destPath = $UpgradeDir . '/' . basename($url);
294+
$writeResult = file_put_contents($destPath, $downloadContent);
295+
296+
if ($writeResult === false) {
297+
$logger->error('Failed to write downloaded file to disk', [
298+
'destPath' => $destPath,
299+
'downloadSize' => $downloadSize,
300+
]);
301+
throw new \Exception(gettext('Failed to save the downloaded release file. Please check disk space and permissions.'));
302+
}
303+
230304
$logger->info('Finished downloading file. Execution time: ' . $executionTime->getMilliseconds() . ' ms');
305+
$logger->info('Downloaded file size: ' . $downloadSize . ' bytes');
306+
231307
$returnFile = [];
232308
$returnFile['fileName'] = basename($url);
233309
$returnFile['releaseNotes'] = $release->getReleaseNotes();
234-
$returnFile['fullPath'] = $UpgradeDir . '/' . basename($url);
235-
$returnFile['sha1'] = sha1_file($UpgradeDir . '/' . basename($url));
310+
$returnFile['fullPath'] = $destPath;
311+
$returnFile['sha1'] = sha1_file($destPath);
236312
$logger->info('SHA1 hash for ' . $returnFile['fullPath'] . ': ' . $returnFile['sha1']);
237313
$logger->info('Release notes: ' . $returnFile['releaseNotes']);
238314

0 commit comments

Comments
 (0)