@@ -47,6 +47,8 @@ public function accept(): bool {
4747}
4848
4949
50+ use CurlHandle ;
51+
5052class 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