From d159ff0720081d5c7e1c676028ed21ca7ed72f74 Mon Sep 17 00:00:00 2001 From: Josh Richards Date: Thu, 19 Dec 2024 17:07:14 -0500 Subject: [PATCH] fix: make deleteOldFiles / moveNewVersionInPlace more robust Signed-off-by: Josh Richards --- index.php | 87 +++++++++++++++++++++++++++++++++--------------- lib/Updater.php | 86 ++++++++++++++++++++++++++++++++--------------- updater.phar | Bin 1169025 -> 1169711 bytes 3 files changed, 121 insertions(+), 52 deletions(-) diff --git a/index.php b/index.php index b4048a7b..94f3f4d6 100644 --- a/index.php +++ b/index.php @@ -7,6 +7,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ + class UpdateException extends \Exception { /** @param list $data */ @@ -927,10 +928,13 @@ public function deleteOldFiles(): void { * @var string $path * @var \SplFileInfo $fileInfo */ + $files = []; + $directories = []; foreach ($this->getRecursiveDirectoryIterator() as $path => $fileInfo) { $currentDir = $this->baseDir . '/../'; $fileName = explode($currentDir, $path)[1]; $folderStructure = explode('/', $fileName, -1); + // Exclude the exclusions if (isset($folderStructure[0])) { if (array_search($folderStructure[0], $excludedElements) !== false) { @@ -942,18 +946,30 @@ public function deleteOldFiles(): void { } } if ($fileInfo->isFile() || $fileInfo->isLink()) { - $state = unlink($path); - if ($state === false) { - throw new \Exception('Could not unlink: '.$path); - } + $files[] = $path; } elseif ($fileInfo->isDir()) { - $state = rmdir($path); - if ($state === false) { - throw new \Exception('Could not rmdir: '.$path); - } + $directories[] = $path; } } + // + // Do the actual writes (outside the RDI to avoid problems on FreeBSD/etc) + // + + foreach ($files as $file) { + $state = unlink($file); + if ($state === false) { + throw new \Exception('Could not unlink: '.$path); + } + } + + foreach ($directories as $dir) { + $state = rmdir($dir); + if ($state === false) { + throw new \Exception('Could not rmdir: '.$path); + } + } + $this->silentLog('[info] end of deleteOldFiles()'); } @@ -967,6 +983,8 @@ private function moveWithExclusions(string $dataLocation, array $excludedElement * @var string $path * @var \SplFileInfo $fileInfo */ + $files = []; + $directories = []; foreach ($this->getRecursiveDirectoryIterator($dataLocation) as $path => $fileInfo) { $fileName = explode($dataLocation, $path)[1]; $folderStructure = explode('/', $fileName, -1); @@ -983,29 +1001,41 @@ private function moveWithExclusions(string $dataLocation, array $excludedElement } if ($fileInfo->isFile()) { - if (!file_exists($this->baseDir . '/../' . dirname($fileName))) { - $state = mkdir($this->baseDir . '/../' . dirname($fileName), 0755, true); - if ($state === false) { - throw new \Exception('Could not mkdir ' . $this->baseDir . '/../' . dirname($fileName)); - } - } - $state = rename($path, $this->baseDir . '/../' . $fileName); - if ($state === false) { - throw new \Exception( - sprintf( - 'Could not rename %s to %s', - $path, - $this->baseDir . '/../' . $fileName - ) - ); - } + $files[$path] = $fileName; } if ($fileInfo->isDir()) { - $state = rmdir($path); + $directories[] = $path; + } + } + + // + // Do the actual writes (outside the RDI to avoid problems on FreeBSD/etc) + // + + foreach ($files as $file => $fileName) { + if (!file_exists($this->baseDir . '/../' . dirname($fileName))) { + $state = mkdir($this->baseDir . '/../' . dirname($fileName), 0755, true); if ($state === false) { - throw new \Exception('Could not rmdir ' . $path); + throw new \Exception('Could not mkdir ' . $this->baseDir . '/../' . dirname($fileName)); } } + $state = rename($file, $this->baseDir . '/../' . $fileName); + if ($state === false) { + throw new \Exception( + sprintf( + 'Could not rename %s to %s', + $file, + $this->baseDir . '/../' . $fileName + ) + ); + } + } + + foreach ($directories as $dir) { + $state = rmdir($dir); + if ($state === false) { + throw new \Exception('Could not rmdir ' . $dir); + } } } @@ -1061,6 +1091,11 @@ public function finalize(): void { opcache_reset(); } + if (function_exists('memory_get_peak_usage')) { + $memUsage = round(memory_get_peak_usage() / 1024 / 1024, 2); + $this->silentLog('[info] Peak memory usage: ' . $memUsage . 'MiB'); + } + $this->silentLog('[info] end of finalize()'); } diff --git a/lib/Updater.php b/lib/Updater.php index 031ba6f7..e668a1b3 100644 --- a/lib/Updater.php +++ b/lib/Updater.php @@ -890,10 +890,13 @@ public function deleteOldFiles(): void { * @var string $path * @var \SplFileInfo $fileInfo */ + $files = []; + $directories = []; foreach ($this->getRecursiveDirectoryIterator() as $path => $fileInfo) { $currentDir = $this->baseDir . '/../'; $fileName = explode($currentDir, $path)[1]; $folderStructure = explode('/', $fileName, -1); + // Exclude the exclusions if (isset($folderStructure[0])) { if (array_search($folderStructure[0], $excludedElements) !== false) { @@ -905,18 +908,30 @@ public function deleteOldFiles(): void { } } if ($fileInfo->isFile() || $fileInfo->isLink()) { - $state = unlink($path); - if ($state === false) { - throw new \Exception('Could not unlink: '.$path); - } + $files[] = $path; } elseif ($fileInfo->isDir()) { - $state = rmdir($path); - if ($state === false) { - throw new \Exception('Could not rmdir: '.$path); - } + $directories[] = $path; } } + // + // Do the actual writes (outside the RDI to avoid problems on FreeBSD/etc) + // + + foreach ($files as $file) { + $state = unlink($file); + if ($state === false) { + throw new \Exception('Could not unlink: '.$path); + } + } + + foreach ($directories as $dir) { + $state = rmdir($dir); + if ($state === false) { + throw new \Exception('Could not rmdir: '.$path); + } + } + $this->silentLog('[info] end of deleteOldFiles()'); } @@ -930,6 +945,8 @@ private function moveWithExclusions(string $dataLocation, array $excludedElement * @var string $path * @var \SplFileInfo $fileInfo */ + $files = []; + $directories = []; foreach ($this->getRecursiveDirectoryIterator($dataLocation) as $path => $fileInfo) { $fileName = explode($dataLocation, $path)[1]; $folderStructure = explode('/', $fileName, -1); @@ -946,29 +963,41 @@ private function moveWithExclusions(string $dataLocation, array $excludedElement } if ($fileInfo->isFile()) { - if (!file_exists($this->baseDir . '/../' . dirname($fileName))) { - $state = mkdir($this->baseDir . '/../' . dirname($fileName), 0755, true); - if ($state === false) { - throw new \Exception('Could not mkdir ' . $this->baseDir . '/../' . dirname($fileName)); - } - } - $state = rename($path, $this->baseDir . '/../' . $fileName); - if ($state === false) { - throw new \Exception( - sprintf( - 'Could not rename %s to %s', - $path, - $this->baseDir . '/../' . $fileName - ) - ); - } + $files[$path] = $fileName; } if ($fileInfo->isDir()) { - $state = rmdir($path); + $directories[] = $path; + } + } + + // + // Do the actual writes (outside the RDI to avoid problems on FreeBSD/etc) + // + + foreach ($files as $file => $fileName) { + if (!file_exists($this->baseDir . '/../' . dirname($fileName))) { + $state = mkdir($this->baseDir . '/../' . dirname($fileName), 0755, true); if ($state === false) { - throw new \Exception('Could not rmdir ' . $path); + throw new \Exception('Could not mkdir ' . $this->baseDir . '/../' . dirname($fileName)); } } + $state = rename($file, $this->baseDir . '/../' . $fileName); + if ($state === false) { + throw new \Exception( + sprintf( + 'Could not rename %s to %s', + $file, + $this->baseDir . '/../' . $fileName + ) + ); + } + } + + foreach ($directories as $dir) { + $state = rmdir($dir); + if ($state === false) { + throw new \Exception('Could not rmdir ' . $dir); + } } } @@ -1024,6 +1053,11 @@ public function finalize(): void { opcache_reset(); } + if (function_exists('memory_get_peak_usage')) { + $memUsage = round(memory_get_peak_usage() / 1024 / 1024, 2); + $this->silentLog('[info] Peak memory usage: ' . $memUsage . 'MiB'); + } + $this->silentLog('[info] end of finalize()'); } diff --git a/updater.phar b/updater.phar index ad43f18a03abb267e2803ec225373de29a40bf34..b9b0827a8aae76e068ed1287459fc0e58d24ffdd 100755 GIT binary patch delta 8122 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 D(@7|< delta 7619 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