use Cli\Services\BackupZip;
use Cli\Services\EnvironmentLoader;
use Cli\Services\InteractiveConsole;
+use Cli\Services\MySqlRunner;
+use Cli\Services\ProgramRunner;
use Cli\Services\RequirementsValidator;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
$appDir = AppLocator::require($input->getOption('app-directory'));
$output->writeln("<info>Checking system requirements...</info>");
RequirementsValidator::validate();
+ (new ProgramRunner('mysql', '/usr/bin/mysql'))->ensureFound();
$zipPath = realpath($input->getArgument('backup-zip'));
$zip = new BackupZip($zipPath);
$output->writeln("<info>The checked elements will be restored into [{$appDir}].</info>");
$output->writeln("<info>Existing content may be overwritten.</info>");
- $output->writeln("<info>Do you want to continue?</info>");
if (!$interactions->confirm("Do you want to continue?")) {
$output->writeln("<info>Stopping restore operation.</info>");
}
$zip->extractInto($extractDir);
- // TODO - Cleanup temp extract dir
-
- // TODO - Environment handling
- // - Restore of old .env
- // - Prompt for correct DB details (Test before serving?)
- // - Prompt for correct URL (Allow entry of new?)
-
- // TODO - Restore folders from backup
+ $envChanges = [];
+ if ($contents['env']['exists']) {
+ $output->writeln("<info>Restoring and merging .env file...</info>");
+ $envChanges = $this->restoreEnv($extractDir, $appDir, $output, $interactions);
+ }
- // TODO - Restore database from backup
+ $folderLocations = ['themes', 'public/uploads', 'storage/uploads'];
+ foreach ($folderLocations as $folderSubPath) {
+ if ($contents[$folderSubPath]['exists']) {
+ $output->writeln("<info>Restoring {$folderSubPath} folder...</info>");
+ $this->restoreFolder($folderSubPath, $appDir, $extractDir);
+ }
+ }
- $output->writeln("<info>Running database migrations...</info>");
$artisan = (new ArtisanRunner($appDir));
- $artisan->run(['migrate', '--force']);
+ if ($contents['db']['exists']) {
+ $output->writeln("<info>Restoring database from SQL dump...</info>");
+ $this->restoreDatabase($appDir, $extractDir);
+
+ $output->writeln("<info>Running database migrations...</info>");
+ $artisan->run(['migrate', '--force']);
+ }
- // TODO - Update system URL (via BookStack artisan command) if
- // there's been a change from old backup env
+ if ($envChanges && $envChanges['old_url'] !== $envChanges['new_url']) {
+ $output->writeln("<info>App URL change made, Updating database with URL change...</info>");
+ $artisan->run([
+ 'bookstack:update-url',
+ $envChanges['old_url'], $envChanges['new_url'],
+ ]);
+ }
$output->writeln("<info>Clearing app caches...</info>");
$artisan->run(['cache:clear']);
$artisan->run(['config:clear']);
$artisan->run(['view:clear']);
+ $output->writeln("<info>Cleaning up extract directory...</info>");
+ $this->deleteDirectoryAndContents($extractDir);
+
+ $output->writeln("<info>\nRestore operation complete!</info>");
+
return Command::SUCCESS;
}
- protected function restoreEnv(string $extractDir, string $appDir, InteractiveConsole $interactions)
+ protected function restoreEnv(string $extractDir, string $appDir, OutputInterface $output, InteractiveConsole $interactions): array
{
- $extractEnv = EnvironmentLoader::load($extractDir);
- $appEnv = EnvironmentLoader::load($appDir); // TODO - Probably pass in since we'll need the APP_URL later on.
+ $oldEnv = EnvironmentLoader::load($extractDir);
+ $currentEnv = EnvironmentLoader::load($appDir);
+ $envContents = file_get_contents($extractDir . DIRECTORY_SEPARATOR . '.env');
+ $appEnvPath = $appDir . DIRECTORY_SEPARATOR . '.env';
+
+ $mysqlCurrent = MySqlRunner::fromEnvOptions($currentEnv);
+ $mysqlOld = MySqlRunner::fromEnvOptions($oldEnv);
+ if (!$mysqlOld->testConnection()) {
+ $currentWorking = $mysqlCurrent->testConnection();
+ if (!$currentWorking) {
+ throw new CommandError("Could not find a working database configuration");
+ }
- // TODO - Create mysql runner to take variables to a programrunner instance.
- // Then test each, backup existing env, then replace env with old then overwrite
- // db options if the new app env options are the valid ones.
+ // Copy across new env details to old env
+ $currentEnvContents = file_get_contents($appEnvPath);
+ $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) {
+ return !str_starts_with($line, 'DB_');
+ }));
+ $envContents = implode("\n", [
+ '# Database credentials merged from existing .env file',
+ ...$currentEnvDbLines,
+ ...$oldEnvLines
+ ]);
+ copy($appEnvPath, $appEnvPath . '.backup');
+ }
+
+ $oldUrl = $oldEnv['APP_URL'] ?? '';
+ $newUrl = $currentEnv['APP_URL'] ?? '';
+ $returnData = [
+ 'old_url' => $oldUrl,
+ 'new_url' => $oldUrl,
+ ];
+
+ 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);
+ $returnData['new_url'] = $changedUrl;
+ }
+
+ file_put_contents($appDir . DIRECTORY_SEPARATOR . '.env', $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)
+ {
+ $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);
+ }
+
+ protected function restoreDatabase(string $appDir, string $extractDir): void
+ {
+ $dbDump = $extractDir . DIRECTORY_SEPARATOR . 'db.sql';
+ $currentEnv = EnvironmentLoader::load($appDir);
+ $mysql = MySqlRunner::fromEnvOptions($currentEnv);
+ $mysql->importSqlFile($dbDump);
}
}