Skip to content

Commit b7afd33

Browse files
authored
Merge pull request #637 from nextcloud/feat/allow_several_downloads_urls
2 parents bc4d8f6 + b8d50f6 commit b7afd33

File tree

5 files changed

+206
-133
lines changed

5 files changed

+206
-133
lines changed

index.php

Lines changed: 102 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class LogException extends \Exception {
2727
}
2828

2929

30+
use CurlHandle;
31+
3032
class Updater {
3133
private string $nextcloudDir;
3234
private array $configValues = [];
@@ -517,20 +519,7 @@ private function getUpdateServerResponse(): array {
517519
$this->silentLog('[info] updateURL: ' . $updateURL);
518520

519521
// Download update response
520-
$curl = curl_init();
521-
curl_setopt_array($curl, [
522-
CURLOPT_RETURNTRANSFER => 1,
523-
CURLOPT_URL => $updateURL,
524-
CURLOPT_USERAGENT => 'Nextcloud Updater',
525-
]);
526-
527-
if ($this->getConfigOption('proxy') !== null) {
528-
curl_setopt_array($curl, [
529-
CURLOPT_PROXY => $this->getConfigOptionString('proxy'),
530-
CURLOPT_PROXYUSERPWD => $this->getConfigOptionString('proxyuserpwd'),
531-
CURLOPT_HTTPPROXYTUNNEL => $this->getConfigOption('proxy') ? 1 : 0,
532-
]);
533-
}
522+
$curl = $this->getCurl($updateURL);
534523

535524
/** @var false|string $response */
536525
$response = curl_exec($curl);
@@ -568,16 +557,10 @@ private function getUpdateServerResponse(): array {
568557
public function downloadUpdate(): void {
569558
$this->silentLog('[info] downloadUpdate()');
570559

571-
$response = $this->getUpdateServerResponse();
572-
if (!isset($response['url']) || !is_string($response['url'])) {
573-
throw new \Exception('Response from update server is missing url');
574-
}
560+
$downloadURLs = $this->getDownloadURLs();
561+
$this->silentLog('[info] will try to download archive from: ' . implode(', ', $downloadURLs));
575562

576563
$storageLocation = $this->getUpdateDirectoryLocation() . '/updater-' . $this->getConfigOptionMandatoryString('instanceid') . '/downloads/';
577-
$saveLocation = $storageLocation . basename($response['url']);
578-
$this->previousProgress = 0;
579-
580-
$ch = curl_init($response['url']);
581564

582565
if (!file_exists($storageLocation)) {
583566
$state = mkdir($storageLocation, 0750, true);
@@ -592,21 +575,61 @@ public function downloadUpdate(): void {
592575
$this->silentLog('[info] extracted Archive location exists');
593576
$this->recursiveDelete($storageLocation . 'nextcloud/');
594577
}
595-
// see if there's an existing incomplete download to resume
596-
if (is_file($saveLocation)) {
597-
$size = filesize($saveLocation);
598-
$range = $size . '-';
599-
curl_setopt($ch, CURLOPT_RANGE, $range);
600-
$this->silentLog('[info] previous download found; resuming from ' . $this->formatBytes($size));
578+
}
579+
580+
foreach ($downloadURLs as $url) {
581+
$this->previousProgress = 0;
582+
$saveLocation = $storageLocation . basename($url);
583+
if ($this->downloadArchive($url, $saveLocation)) {
584+
return;
601585
}
602586
}
603587

604-
$fp = fopen($saveLocation, 'a');
588+
throw new \Exception('All downloads failed. See updater logs for more information.');
589+
}
590+
591+
private function getDownloadURLs(): array {
592+
$response = $this->getUpdateServerResponse();
593+
$downloadURLs = [];
594+
if (!isset($response['downloads']) || !is_array($response['downloads'])) {
595+
if (isset($response['url']) && is_string($response['url'])) {
596+
// Compatibility with previous verison of updater_server
597+
$ext = pathinfo($response['url'], PATHINFO_EXTENSION);
598+
$response['downloads'] = [
599+
$ext => [$response['url']]
600+
];
601+
} else {
602+
throw new \Exception('Response from update server is missing download URLs');
603+
}
604+
}
605+
foreach ($response['downloads'] as $format => $urls) {
606+
if (!$this->isAbleToDecompress($format)) {
607+
continue;
608+
}
609+
foreach ($urls as $url) {
610+
if (!is_string($url)) {
611+
continue;
612+
}
613+
$downloadURLs[] = $url;
614+
}
615+
}
616+
617+
if (empty($downloadURLs)) {
618+
throw new \Exception('Your PHP install is not able to decompress any archive. Try to install modules like zip or bzip.');
619+
}
620+
621+
return array_unique($downloadURLs);
622+
623+
}
624+
625+
private function getCurl(string $url): CurlHandle {
626+
$ch = curl_init($url);
627+
if ($ch === false) {
628+
throw new \Exception('Fail to open cUrl handler');
629+
}
630+
605631
curl_setopt_array($ch, [
606632
CURLOPT_RETURNTRANSFER => true,
607-
CURLOPT_NOPROGRESS => false,
608-
CURLOPT_PROGRESSFUNCTION => [$this, 'downloadProgressCallback'],
609-
CURLOPT_FILE => $fp,
610633
CURLOPT_USERAGENT => 'Nextcloud Updater',
611634
CURLOPT_FOLLOWLOCATION => 1,
612635
CURLOPT_MAXREDIRS => 2,
@@ -620,47 +643,61 @@ public function downloadUpdate(): void {
620643
]);
621644
}
622645

646+
return $ch;
647+
}
648+
649+
private function downloadArchive(string $fromUrl, string $toLocation): bool {
650+
$ch = $this->getCurl($fromUrl);
651+
652+
// see if there's an existing incomplete download to resume
653+
if (is_file($toLocation)) {
654+
$size = (int)filesize($toLocation);
655+
$range = $size . '-';
656+
curl_setopt($ch, CURLOPT_RANGE, $range);
657+
$this->silentLog('[info] previous download found; resuming from ' . $this->formatBytes($size));
658+
}
659+
660+
$fp = fopen($toLocation, 'ab');
661+
if ($fp === false) {
662+
throw new \Exception('Fail to open file in ' . $toLocation);
663+
}
664+
665+
curl_setopt_array($ch, [
666+
CURLOPT_NOPROGRESS => false,
667+
CURLOPT_PROGRESSFUNCTION => [$this, 'downloadProgressCallback'],
668+
CURLOPT_FILE => $fp,
669+
]);
670+
623671
if (curl_exec($ch) === false) {
624672
throw new \Exception('Curl error: ' . curl_error($ch));
625673
}
674+
626675
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
627676
if ($httpCode !== 200 && $httpCode !== 206) {
628-
$statusCodes = [
629-
400 => 'Bad request',
630-
401 => 'Unauthorized',
631-
403 => 'Forbidden',
632-
404 => 'Not Found',
633-
500 => 'Internal Server Error',
634-
502 => 'Bad Gateway',
635-
503 => 'Service Unavailable',
636-
504 => 'Gateway Timeout',
637-
];
638-
639-
$message = 'Download failed';
640-
if (is_int($httpCode) && isset($statusCodes[$httpCode])) {
641-
$message .= ' - ' . $statusCodes[$httpCode] . ' (HTTP ' . $httpCode . ')';
642-
} else {
643-
$message .= ' - HTTP status code: ' . (string)$httpCode;
644-
}
645-
646-
$curlErrorMessage = curl_error($ch);
647-
if (!empty($curlErrorMessage)) {
648-
$message .= ' - curl error message: ' . $curlErrorMessage;
649-
}
677+
fclose($fp);
678+
unlink($toLocation);
679+
$this->silentLog('[warn] fail to download archive from ' . $fromUrl . '. Error: ' . $httpCode . ' ' . curl_error($ch));
680+
curl_close($ch);
650681

651-
$message .= ' - URL: ' . htmlentities($response['url']);
652-
653-
throw new \Exception($message);
654-
} else {
655-
// download succeeded
656-
$info = curl_getinfo($ch);
657-
$this->silentLog('[info] download stats: size=' . $this->formatBytes((int)$info['size_download']) . ' bytes; total_time=' . round($info['total_time'], 2) . ' secs; avg speed=' . $this->formatBytes((int)$info['speed_download']) . '/sec');
682+
return false;
658683
}
684+
// download succeeded
685+
$info = curl_getinfo($ch);
686+
$this->silentLog('[info] download stats: size=' . $this->formatBytes((int)$info['size_download']) . ' bytes; total_time=' . round($info['total_time'], 2) . ' secs; avg speed=' . $this->formatBytes((int)$info['speed_download']) . '/sec');
659687

660688
curl_close($ch);
661689
fclose($fp);
662690

663691
$this->silentLog('[info] end of downloadUpdate()');
692+
return true;
693+
}
694+
695+
/**
696+
* Check if PHP is able to decompress archive format
697+
*/
698+
private function isAbleToDecompress(string $ext): bool {
699+
// Only zip is supported for now
700+
return $ext === 'zip' && extension_loaded($ext);
664701
}
665702

666703
private function downloadProgressCallback(\CurlHandle $resource, int $download_size, int $downloaded, int $upload_size, int $uploaded): void {
@@ -677,7 +714,7 @@ private function downloadProgressCallback(\CurlHandle $resource, int $download_s
677714
}
678715

679716
private function formatBytes(int $bytes, int $precision = 2): string {
680-
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
717+
$units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
681718

682719
$bytes = max($bytes, 0);
683720
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
@@ -699,13 +736,14 @@ private function getDownloadedFilePath(): string {
699736

700737
$filesInStorageLocation = scandir($storageLocation);
701738
$files = array_values(array_filter($filesInStorageLocation, function (string $path) {
702-
return $path !== '.' && $path !== '..';
739+
// Match files with - in the name and extension (*-*.*)
740+
return preg_match('/^.*-.*\..*$/i', $path);
703741
}));
704742
// only the downloaded archive
705743
if (count($files) !== 1) {
706744
throw new \Exception('There are more files than the downloaded archive in the downloads/ folder.');
707745
}
708-
return $storageLocation . '/' . $files[0];
746+
return $storageLocation . $files[0];
709747
}
710748

711749
/**

0 commit comments

Comments
 (0)