Skip to content

Commit c47c8a5

Browse files
authored
Merge pull request #654 from nextcloud/backport/637/stable31
2 parents 28e95dd + 3e2d669 commit c47c8a5

6 files changed

Lines changed: 297 additions & 116 deletions

File tree

index.php

Lines changed: 146 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public function accept(): bool {
4747
}
4848

4949

50+
use CurlHandle;
51+
5052
class Updater {
5153
private string $baseDir;
5254
private array $configValues = [];
@@ -55,6 +57,7 @@ class Updater {
5557
private bool $updateAvailable = false;
5658
private ?string $requestID = null;
5759
private bool $disabled = false;
60+
private int $previousProgress = 0;
5861

5962
/**
6063
* Updater constructor
@@ -278,6 +281,7 @@ private function getExpectedElementsList(): array {
278281
'COPYING-AGPL',
279282
'occ',
280283
'db_structure.xml',
284+
'REUSE.toml',
281285
];
282286
return array_merge($expected, $this->getAppDirectories());
283287
}
@@ -514,20 +518,7 @@ private function getUpdateServerResponse(): array {
514518
$this->silentLog('[info] updateURL: ' . $updateURL);
515519

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

532523
/** @var false|string $response */
533524
$response = curl_exec($curl);
@@ -558,26 +549,79 @@ private function getUpdateServerResponse(): array {
558549
public function downloadUpdate(): void {
559550
$this->silentLog('[info] downloadUpdate()');
560551

561-
$response = $this->getUpdateServerResponse();
552+
$downloadURLs = $this->getDownloadURLs();
553+
$this->silentLog('[info] will try to download archive from: ' . implode(', ', $downloadURLs));
562554

563-
$storageLocation = $this->getUpdateDirectoryLocation() . '/updater-'.$this->getConfigOptionMandatoryString('instanceid') . '/downloads/';
564-
if (file_exists($storageLocation)) {
565-
$this->silentLog('[info] storage location exists');
566-
$this->recursiveDelete($storageLocation);
555+
$storageLocation = $this->getUpdateDirectoryLocation() . '/updater-' . $this->getConfigOptionMandatoryString('instanceid') . '/downloads/';
556+
557+
if (!file_exists($storageLocation)) {
558+
$state = mkdir($storageLocation, 0750, true);
559+
if ($state === false) {
560+
throw new \Exception('Could not mkdir storage location');
561+
}
562+
$this->silentLog('[info] storage location created');
563+
} else {
564+
$this->silentLog('[info] storage location already exists');
565+
// clean-up leftover extracted content from any prior runs, but leave any downloaded Archives alone
566+
if (file_exists($storageLocation . 'nextcloud/')) {
567+
$this->silentLog('[info] extracted Archive location exists');
568+
$this->recursiveDelete($storageLocation . 'nextcloud/');
569+
}
567570
}
568-
$state = mkdir($storageLocation, 0750, true);
569-
if ($state === false) {
570-
throw new \Exception('Could not mkdir storage location');
571+
572+
foreach ($downloadURLs as $url) {
573+
$this->previousProgress = 0;
574+
$saveLocation = $storageLocation . basename($url);
575+
if ($this->downloadArchive($url, $saveLocation)) {
576+
return;
577+
}
578+
}
579+
580+
throw new \Exception('All downloads failed. See updater logs for more information.');
581+
}
582+
583+
private function getDownloadURLs(): array {
584+
$response = $this->getUpdateServerResponse();
585+
$downloadURLs = [];
586+
if (!isset($response['downloads']) || !is_array($response['downloads'])) {
587+
if (isset($response['url']) && is_string($response['url'])) {
588+
// Compatibility with previous verison of updater_server
589+
$ext = pathinfo($response['url'], PATHINFO_EXTENSION);
590+
$response['downloads'] = [
591+
$ext => [$response['url']]
592+
];
593+
} else {
594+
throw new \Exception('Response from update server is missing download URLs');
595+
}
596+
}
597+
foreach ($response['downloads'] as $format => $urls) {
598+
if (!$this->isAbleToDecompress($format)) {
599+
continue;
600+
}
601+
foreach ($urls as $url) {
602+
if (!is_string($url)) {
603+
continue;
604+
}
605+
$downloadURLs[] = $url;
606+
}
571607
}
572608

573-
if (!isset($response['url']) || !is_string($response['url'])) {
574-
throw new \Exception('Response from update server is missing url');
609+
if (empty($downloadURLs)) {
610+
throw new \Exception('Your PHP install is not able to decompress any archive. Try to install modules like zip or bzip.');
611+
}
612+
613+
return array_unique($downloadURLs);
614+
615+
}
616+
617+
private function getCurl(string $url): CurlHandle {
618+
$ch = curl_init($url);
619+
if ($ch === false) {
620+
throw new \Exception('Fail to open cUrl handler');
575621
}
576622

577-
$fp = fopen($storageLocation . basename($response['url']), 'w+');
578-
$ch = curl_init($response['url']);
579623
curl_setopt_array($ch, [
580-
CURLOPT_FILE => $fp,
624+
CURLOPT_RETURNTRANSFER => true,
581625
CURLOPT_USERAGENT => 'Nextcloud Updater',
582626
CURLOPT_FOLLOWLOCATION => 1,
583627
CURLOPT_MAXREDIRS => 2,
@@ -591,42 +635,88 @@ public function downloadUpdate(): void {
591635
]);
592636
}
593637

638+
return $ch;
639+
}
640+
641+
private function downloadArchive(string $fromUrl, string $toLocation): bool {
642+
$ch = $this->getCurl($fromUrl);
643+
644+
// see if there's an existing incomplete download to resume
645+
if (is_file($toLocation)) {
646+
$size = (int)filesize($toLocation);
647+
$range = $size . '-';
648+
curl_setopt($ch, CURLOPT_RANGE, $range);
649+
$this->silentLog('[info] previous download found; resuming from ' . $this->formatBytes($size));
650+
}
651+
652+
$fp = fopen($toLocation, 'ab');
653+
if ($fp === false) {
654+
throw new \Exception('Fail to open file in ' . $toLocation);
655+
}
656+
657+
curl_setopt_array($ch, [
658+
CURLOPT_NOPROGRESS => false,
659+
CURLOPT_PROGRESSFUNCTION => [$this, 'downloadProgressCallback'],
660+
CURLOPT_FILE => $fp,
661+
]);
662+
594663
if (curl_exec($ch) === false) {
595664
throw new \Exception('Curl error: ' . curl_error($ch));
596665
}
597-
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
598-
if ($httpCode !== 200) {
599-
$statusCodes = [
600-
400 => 'Bad request',
601-
401 => 'Unauthorized',
602-
403 => 'Forbidden',
603-
404 => 'Not Found',
604-
500 => 'Internal Server Error',
605-
502 => 'Bad Gateway',
606-
503 => 'Service Unavailable',
607-
504 => 'Gateway Timeout',
608-
];
609-
610-
$message = 'Download failed';
611-
if (is_int($httpCode) && isset($statusCodes[$httpCode])) {
612-
$message .= ' - ' . $statusCodes[$httpCode] . ' (HTTP ' . $httpCode . ')';
613-
} else {
614-
$message .= ' - HTTP status code: ' . (string)$httpCode;
615-
}
616-
617-
$curlErrorMessage = curl_error($ch);
618-
if (!empty($curlErrorMessage)) {
619-
$message .= ' - curl error message: ' . $curlErrorMessage;
620-
}
621666

622-
$message .= ' - URL: ' . htmlentities($response['url']);
667+
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
668+
if ($httpCode !== 200 && $httpCode !== 206) {
669+
fclose($fp);
670+
unlink($toLocation);
671+
$this->silentLog('[warn] fail to download archive from ' . $fromUrl . '. Error: ' . $httpCode . ' ' . curl_error($ch));
672+
curl_close($ch);
623673

624-
throw new \Exception($message);
674+
return false;
625675
}
676+
// download succeeded
677+
$info = curl_getinfo($ch);
678+
$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');
679+
626680
curl_close($ch);
627681
fclose($fp);
628682

629683
$this->silentLog('[info] end of downloadUpdate()');
684+
return true;
685+
}
686+
687+
/**
688+
* Check if PHP is able to decompress archive format
689+
*/
690+
private function isAbleToDecompress(string $ext): bool {
691+
// Only zip is supported for now
692+
return $ext === 'zip' && extension_loaded($ext);
693+
}
694+
695+
private function downloadProgressCallback(CurlHandle $resource, int $download_size, int $downloaded, int $upload_size, int $uploaded): void {
696+
if ($download_size !== 0) {
697+
$progress = (int)round($downloaded * 100 / $download_size);
698+
if ($progress > $this->previousProgress) {
699+
$this->previousProgress = $progress;
700+
// log every 2% increment for the first 10% then only log every 10% increment after that
701+
if ($progress % 10 === 0 || ($progress < 10 && $progress % 2 === 0)) {
702+
$this->silentLog("[info] download progress: $progress% (" . $this->formatBytes($downloaded) . ' of ' . $this->formatBytes($download_size) . ')');
703+
}
704+
}
705+
}
706+
}
707+
708+
private function formatBytes(int $bytes, int $precision = 2): string {
709+
$units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
710+
711+
$bytes = max($bytes, 0);
712+
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
713+
$pow = min($pow, count($units) - 1);
714+
715+
// Uncomment one of the following alternatives
716+
$bytes /= pow(1024, $pow);
717+
// $bytes /= (1 << (10 * $pow));
718+
719+
return round($bytes, $precision) . $units[(int)$pow];
630720
}
631721

632722
/**
@@ -638,13 +728,14 @@ private function getDownloadedFilePath(): string {
638728

639729
$filesInStorageLocation = scandir($storageLocation);
640730
$files = array_values(array_filter($filesInStorageLocation, function (string $path) {
641-
return $path !== '.' && $path !== '..';
731+
// Match files with - in the name and extension (*-*.*)
732+
return preg_match('/^.*-.*\..*$/i', $path);
642733
}));
643734
// only the downloaded archive
644735
if (count($files) !== 1) {
645736
throw new \Exception('There are more files than the downloaded archive in the downloads/ folder.');
646737
}
647-
return $storageLocation . '/' . $files[0];
738+
return $storageLocation . $files[0];
648739
}
649740

650741
/**

0 commit comments

Comments
 (0)