diff --git a/webapp/src/DataTransferObject/Shadowing/LanguageEvent.php b/webapp/src/DataTransferObject/Shadowing/LanguageEvent.php index aa5e799f4b..999bca1eed 100644 --- a/webapp/src/DataTransferObject/Shadowing/LanguageEvent.php +++ b/webapp/src/DataTransferObject/Shadowing/LanguageEvent.php @@ -4,7 +4,11 @@ class LanguageEvent implements EventData { + /** + * @param list|null $extensions + */ public function __construct( public readonly string $id, + public readonly ?array $extensions = null, ) {} } diff --git a/webapp/src/Service/ExternalContestSourceService.php b/webapp/src/Service/ExternalContestSourceService.php index bc53ca6aec..73372eb6fd 100644 --- a/webapp/src/Service/ExternalContestSourceService.php +++ b/webapp/src/Service/ExternalContestSourceService.php @@ -381,11 +381,10 @@ protected function importFromCcsApi(array $eventsToSkip, ?callable $progressRepo }; while (true) { - // A timeout of 0.0 means we get chunks immediately and the user - // can cancel at any time. try { $receivedData = false; - foreach ($this->httpClient->stream($response, 0.0) as $chunk) { + // Get a timeout chunk after 1 second so we don't hang indefinitely. + foreach ($this->httpClient->stream($response, 1.0) as $chunk) { // We first need to check for timeouts, as we can not call // ->isLast() or ->getContent() on them. if (!$chunk->isTimeout()) { @@ -833,6 +832,10 @@ protected function validateLanguage(Event $event, EventData $data): void ]); } else { $this->removeWarning($event->type, $data->id, ExternalSourceWarning::TYPE_DATA_MISMATCH); + + $toCheck = ['extensions' => $data->extensions]; + + $this->compareOrCreateValues($event, $data->id, $language, $toCheck); } } @@ -1432,6 +1435,10 @@ protected function importSubmission(Event $event, EventData $data): void $zipUrl = $data->files[0]->href; if (preg_match('/^https?:\/\//', $zipUrl) === 0) { // Relative URL, prepend the base URL. + // If the base URL ends with a slash and the zip URL starts with one, remove the slash. + if (str_ends_with($this->basePath, '/') && str_starts_with($zipUrl, '/')) { + $zipUrl = substr($zipUrl, 1); + } $zipUrl = ($this->basePath ?? '') . $zipUrl; } @@ -1461,26 +1468,31 @@ protected function importSubmission(Event $event, EventData $data): void } if ($submissionDownloadSucceeded) { - try { - $response = $this->httpClient->request('GET', $zipUrl); - $ziphandler = fopen($zipFile, 'w'); - if ($response->getStatusCode() !== 200) { - // TODO: Retry a couple of times. + $tries = 1; + do { + try { + $response = $this->httpClient->request('GET', $zipUrl); + $ziphandler = fopen($zipFile, 'w'); + if ($response->getStatusCode() !== 200) { + $this->addOrUpdateWarning($event, $data->id, ExternalSourceWarning::TYPE_SUBMISSION_ERROR, [ + 'message' => "Cannot download ZIP from $zipUrl after trying $tries times", + ]); + $submissionDownloadSucceeded = false; + // Sleep a bit before retrying + sleep(3); + } + $tries++; + } catch (TransportExceptionInterface $e) { $this->addOrUpdateWarning($event, $data->id, ExternalSourceWarning::TYPE_SUBMISSION_ERROR, [ - 'message' => 'Cannot download ZIP from ' . $zipUrl, + 'message' => "Cannot download ZIP from $zipUrl after trying $tries times: " . $e->getMessage(), ]); + if (isset($ziphandler)) { + fclose($ziphandler); + } + unlink($zipFile); $submissionDownloadSucceeded = false; } - } catch (TransportExceptionInterface $e) { - $this->addOrUpdateWarning($event, $data->id, ExternalSourceWarning::TYPE_SUBMISSION_ERROR, [ - 'message' => 'Cannot download ZIP from ' . $zipUrl . ': ' . $e->getMessage(), - ]); - if (isset($ziphandler)) { - fclose($ziphandler); - } - unlink($zipFile); - $submissionDownloadSucceeded = false; - } + } while ($tries <= 3 && !$submissionDownloadSucceeded); } if (isset($response, $ziphandler) && $submissionDownloadSucceeded) {