use Cli\Services\AppLocator;
use Cli\Services\ArtisanRunner;
use Cli\Services\BackupZip;
+use Cli\Services\Directories;
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;
-use RecursiveDirectoryIterator;
-use RecursiveIteratorIterator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
RequirementsValidator::validate();
(new ProgramRunner('mysql', '/usr/bin/mysql'))->ensureFound();
- $zipPath = realpath($input->getArgument('backup-zip'));
+ $providedZipPath = $input->getArgument('backup-zip');
+ $zipPath = realpath($providedZipPath);
+ if (!$zipPath || !file_exists($zipPath)) {
+ $pathToDisplay = $zipPath ?: $providedZipPath;
+ throw new CommandError("Could not find ZIP file for restoration at provided path [{$pathToDisplay}].");
+ }
+
$zip = new BackupZip($zipPath);
$contents = $zip->getContentsOverview();
}
$output->writeln("<info>Extracting ZIP into temporary directory...</info>");
- $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}].");
}
}
if ($envChanges && $envChanges['old_url'] !== $envChanges['new_url']) {
- $output->writeln("<info>App URL change made, Updating database with URL change...</info>");
+ $output->writeln("<info>App URL change made, updating database with URL change...</info>");
$artisan->run([
- 'bookstack:update-url',
+ 'bookstack:update-url', '--force',
$envChanges['old_url'], $envChanges['new_url'],
]);
}
$artisan->run(['view:clear']);
$output->writeln("<info>Cleaning up extract directory...</info>");
- $this->deleteDirectoryAndContents($extractDir);
+ Directories::delete($extractDir);
$output->writeln("<success>\nRestore operation complete!</success>");
+ $output->writeln("<info>You may need to fix file/folder permissions so that the webserver has</info>");
+ $output->writeln("<info>the required read/write access to the necessary directories & files.</info>");
return Command::SUCCESS;
}
protected function restoreEnv(string $extractDir, string $appDir, OutputInterface $output, InteractiveConsole $interactions): array
{
- $oldEnv = EnvironmentLoader::load($extractDir);
+ $oldEnv = EnvironmentLoader::loadMergedWithCurrentEnv($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);
$currentEnvDbLines = array_values(array_filter(explode("\n", $currentEnvContents), function (string $line) {
return str_starts_with($line, 'DB_');
}));
- $oldEnvLines = array_values(array_filter(explode("\n", $currentEnvContents), function (string $line) {
+ $oldEnvLines = array_values(array_filter(explode("\n", $envContents), function (string $line) {
return !str_starts_with($line, 'DB_');
}));
$envContents = implode("\n", [
];
if ($oldUrl !== $newUrl) {
- $output->writeln("Found different APP_URL values:");
- $changedUrl = $interactions->choice('Which would you like to use?', array_filter([$oldUrl, $newUrl]));
- $envContents = preg_replace('/^APP_URL=.*?$/', 'APP_URL="' . $changedUrl . '"', $envContents);
+ $question = 'Found different APP_URL values, which would you like to use?';
+ $changedUrl = $interactions->choice($question, array_filter([$oldUrl, $newUrl]));
+ $envContents = preg_replace('/^APP_URL=.*?$/m', 'APP_URL="' . $changedUrl . '"', $envContents);
$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;
- $this->deleteDirectoryAndContents($fullAppFolderPath);
- rename($extractDir . DIRECTORY_SEPARATOR . $folderSubPath, $fullAppFolderPath);
- }
-
- protected function deleteDirectoryAndContents(string $dir): void
- {
- $files = new RecursiveIteratorIterator(
- new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
- RecursiveIteratorIterator::CHILD_FIRST
- );
-
- foreach ($files as $fileinfo) {
- $path = $fileinfo->getRealPath();
- $fileinfo->isDir() ? rmdir($path) : unlink($path);
- }
-
- rmdir($dir);
+ $fullAppFolderPath = Paths::real(Paths::join($appDir, $folderSubPath));
+ Directories::delete($fullAppFolderPath);
+ Directories::move(Paths::join($extractDir, $folderSubPath), $fullAppFolderPath);
}
protected function restoreDatabase(string $appDir, string $extractDir): void
{
- $dbDump = $extractDir . DIRECTORY_SEPARATOR . 'db.sql';
- $currentEnv = EnvironmentLoader::load($appDir);
+ $dbDump = Paths::join($extractDir, 'db.sql');
+ $currentEnv = EnvironmentLoader::loadMergedWithCurrentEnv($appDir);
$mysql = MySqlRunner::fromEnvOptions($currentEnv);
// Drop existing tables