@@ -27,6 +27,8 @@ class LogException extends \Exception {
2727}
2828
2929
30+ use CurlHandle ;
31+
3032class 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