From: Dan Brown Date: Sat, 6 May 2023 13:52:43 +0000 (+0100) Subject: Added support for key restore files/folders being symlinks X-Git-Url: http://source.bookstackapp.com/system-cli/commitdiff_plain/aa8d661ba7574515bd746758243f7b2224a166de Added support for key restore files/folders being symlinks Fixes #4 Added test to cover. --- diff --git a/src/Commands/RestoreCommand.php b/src/Commands/RestoreCommand.php index f46d571..04ea1bb 100644 --- a/src/Commands/RestoreCommand.php +++ b/src/Commands/RestoreCommand.php @@ -8,6 +8,7 @@ use Cli\Services\BackupZip; use Cli\Services\EnvironmentLoader; use Cli\Services\InteractiveConsole; use Cli\Services\MySqlRunner; +use Cli\Services\Paths; use Cli\Services\ProgramRunner; use Cli\Services\RequirementsValidator; use Exception; @@ -74,7 +75,7 @@ class RestoreCommand extends Command } $output->writeln("Extracting ZIP into temporary directory..."); - $extractDir = $appDir . DIRECTORY_SEPARATOR . 'restore-temp-' . time(); + $extractDir = Paths::join($appDir, 'restore-temp-' . time()); if (!mkdir($extractDir)) { throw new CommandError("Could not create temporary extraction directory at [{$extractDir}]."); } @@ -130,8 +131,8 @@ class RestoreCommand extends Command { $oldEnv = EnvironmentLoader::load($extractDir); $currentEnv = EnvironmentLoader::load($appDir); - $envContents = file_get_contents($extractDir . DIRECTORY_SEPARATOR . '.env'); - $appEnvPath = $appDir . DIRECTORY_SEPARATOR . '.env'; + $envContents = file_get_contents(Paths::join($extractDir, '.env')); + $appEnvPath = Paths::real(Paths::join($appDir, '.env')); $mysqlCurrent = MySqlRunner::fromEnvOptions($currentEnv); $mysqlOld = MySqlRunner::fromEnvOptions($oldEnv); @@ -171,16 +172,16 @@ class RestoreCommand extends Command $returnData['new_url'] = $changedUrl; } - file_put_contents($appDir . DIRECTORY_SEPARATOR . '.env', $envContents); + file_put_contents($appEnvPath, $envContents); return $returnData; } protected function restoreFolder(string $folderSubPath, string $appDir, string $extractDir): void { - $fullAppFolderPath = $appDir . DIRECTORY_SEPARATOR . $folderSubPath; + $fullAppFolderPath = Paths::real(Paths::join($appDir, $folderSubPath)); $this->deleteDirectoryAndContents($fullAppFolderPath); - rename($extractDir . DIRECTORY_SEPARATOR . $folderSubPath, $fullAppFolderPath); + rename(Paths::join($extractDir, $folderSubPath), $fullAppFolderPath); } protected function deleteDirectoryAndContents(string $dir): void @@ -200,7 +201,7 @@ class RestoreCommand extends Command protected function restoreDatabase(string $appDir, string $extractDir): void { - $dbDump = $extractDir . DIRECTORY_SEPARATOR . 'db.sql'; + $dbDump = Paths::join($extractDir, 'db.sql'); $currentEnv = EnvironmentLoader::load($appDir); $mysql = MySqlRunner::fromEnvOptions($currentEnv); diff --git a/src/Services/Paths.php b/src/Services/Paths.php index d0bb10f..a258baa 100644 --- a/src/Services/Paths.php +++ b/src/Services/Paths.php @@ -5,6 +5,21 @@ namespace Cli\Services; class Paths { + /** + * Get the full real path, resolving symbolic links, to + * the existing file/directory of the given path. + * @throws \Exception + */ + public static function real(string $path): string + { + $real = realpath($path); + if ($real === false) { + throw new \Exception("Path {$path} could not be resolved to a location on the filesystem"); + } + + return $real; + } + /** * Join together the given path components. * Does no resolving or cleaning. diff --git a/tests/Commands/RestoreCommandTest.php b/tests/Commands/RestoreCommandTest.php index a8ff2b4..df70d16 100644 --- a/tests/Commands/RestoreCommandTest.php +++ b/tests/Commands/RestoreCommandTest.php @@ -29,11 +29,8 @@ class RestoreCommandTest extends TestCase $result = $this->runCommand('restore', [ 'backup-zip' => $zipFile, - ], [ - 'yes', '1' - ]); + ], ['yes', '1']); - $result->dumpError(); $result->assertSuccessfulExit(); $result->assertStdoutContains('✔ .env Config File'); $result->assertStdoutContains('✔ Themes Folder'); @@ -98,6 +95,44 @@ class RestoreCommandTest extends TestCase $mysql->query("DROP TABLE zz_testing;"); } + public function test_restore_with_symlinked_content_folders() + { + $zipFile = $this->buildZip(function (\ZipArchive $zip) { + $zip->addFromString('.env', "APP_KEY=abc123\nAPP_URL=https://example.com"); + $zip->addFromString('public/uploads/test.txt', 'hello-public-uploads'); + $zip->addFromString('storage/uploads/test.txt', 'hello-storage-uploads'); + $zip->addFromString('themes/test.txt', 'hello-themes'); + }); + + exec('cp -r /var/www/bookstack /var/www/bookstack-symlink-restore'); + chdir('/var/www/bookstack-symlink-restore'); + mkdir('/symlinks'); + + $symlinkPaths = ['public/uploads', 'storage/uploads', '.env', 'themes']; + foreach ($symlinkPaths as $path) { + $targetFile = str_replace('/', '-', $path); + $code = 0; + $output = null; + exec("mv /var/www/bookstack-symlink-restore/{$path} /symlinks/{$targetFile}", $output, $code); + exec("ln -s /symlinks/{$targetFile} /var/www/bookstack-symlink-restore/{$path}", $output, $code); + if ($code !== 0) { + $this->fail("Error when setting up symlinks"); + } + } + + $result = $this->runCommand('restore', [ + 'backup-zip' => $zipFile, + ], ['yes', '1']); + + $result->assertSuccessfulExit(); + + $this->assertStringEqualsFile('/var/www/bookstack-symlink-restore/public/uploads/test.txt', 'hello-public-uploads'); + $this->assertStringEqualsFile('/var/www/bookstack-symlink-restore/storage/uploads/test.txt', 'hello-storage-uploads'); + $this->assertStringEqualsFile('/var/www/bookstack-symlink-restore/themes/test.txt', 'hello-themes'); + + exec('rm -rf /var/www/bookstack-symlink-restore'); + } + protected function buildZip(callable $builder): string { $zipFile = tempnam(sys_get_temp_dir(), 'cli-test');