From c5db8bc222a3329998411694bae694b93cb92445 Mon Sep 17 00:00:00 2001 From: Jon Gotlin Date: Tue, 31 Jan 2017 13:10:57 +0100 Subject: [PATCH] Restore command (#110) * Added command that lets you restore the database from previous backup. * Travis test * Bugfix with filename * Added dropbox support for download * Bugfix * Optional * Optional * Throw exception if some parameters are not set --- .travis.yml | 3 +- Client/DownloadableClientInterface.php | 12 ++ Client/DropboxSdkClient.php | 55 ++++++++- Client/GaufretteClient.php | 57 ++++++++- Command/RestoreCommand.php | 84 +++++++++++++ Database/MySQL.php | 52 +++++++- Database/RestorableDatabaseInterface.php | 11 ++ DependencyInjection/Configuration.php | 1 + .../DizdaCloudBackupExtension.php | 1 + Event/RestoreEvent.php | 31 +++++ Event/RestoreFailedEvent.php | 31 +++++ Exception/InvalidConfigurationException.php | 13 ++ .../MissingDownloadableClientsException.php | 13 ++ .../MissingRestorableDatabaseException.php | 13 ++ Exception/RestoringNotAvailableException.php | 13 ++ .../UncompressionNotSupportedException.php | 6 + Exception/UploadException.php | 8 +- Listener/CleanRestoreWorkspaceListener.php | 51 ++++++++ Listener/LogRestoreCompletedListener.php | 29 +++++ Listener/LogRestoreFailedListener.php | 29 +++++ Manager/ClientManager.php | 18 +++ Manager/DatabaseManager.php | 17 +++ Manager/ProcessorManager.php | 27 ++++- Manager/RestoreManager.php | 97 +++++++++++++++ .../UncompressableProcessorInterface.php | 16 +++ Processor/ZipProcessor.php | 10 +- README.md | 9 ++ Resources/config/services.yml | 55 +++++++++ Tests/Client/GaufretteClientTest.php | 53 +++++++++ Tests/Database/MySQLTest.php | 85 ++++++++++++-- Tests/Manager/ClientManagerTest.php | 44 +++++++ Tests/Manager/RestoreManagerTest.php | 111 ++++++++++++++++++ composer.json | 4 +- 33 files changed, 1035 insertions(+), 24 deletions(-) create mode 100644 Client/DownloadableClientInterface.php create mode 100644 Command/RestoreCommand.php create mode 100644 Database/RestorableDatabaseInterface.php create mode 100644 Event/RestoreEvent.php create mode 100644 Event/RestoreFailedEvent.php create mode 100644 Exception/InvalidConfigurationException.php create mode 100644 Exception/MissingDownloadableClientsException.php create mode 100644 Exception/MissingRestorableDatabaseException.php create mode 100644 Exception/RestoringNotAvailableException.php create mode 100644 Exception/UncompressionNotSupportedException.php create mode 100644 Listener/CleanRestoreWorkspaceListener.php create mode 100644 Listener/LogRestoreCompletedListener.php create mode 100644 Listener/LogRestoreFailedListener.php create mode 100644 Manager/RestoreManager.php create mode 100644 Processor/UncompressableProcessorInterface.php create mode 100644 Tests/Client/GaufretteClientTest.php create mode 100644 Tests/Manager/ClientManagerTest.php create mode 100644 Tests/Manager/RestoreManagerTest.php diff --git a/.travis.yml b/.travis.yml index 5ff9af8..9586c1c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,13 +2,12 @@ language: php sudo: false php: - - 5.4 - 5.5 - 5.6 - 7.0 - hhvm -before_script: composer install --dev --prefer-source +before_script: composer install --prefer-source script: phpunit --debug --coverage-text diff --git a/Client/DownloadableClientInterface.php b/Client/DownloadableClientInterface.php new file mode 100644 index 0000000..f0d9e01 --- /dev/null +++ b/Client/DownloadableClientInterface.php @@ -0,0 +1,12 @@ +restoreFolder = $restoreFolder; + $this->localFilesystem = $localFilesystem; $params = $params['dropbox_sdk']; $this->access_token = $params['access_token']; $this->remotePath = $params['remote_path']; @@ -46,6 +64,37 @@ public function upload($archive) fclose($fp); } + public function download() + { + if (!$this->restoreFolder) { + throw InvalidConfigurationException::create('$restoreFolder'); + } + $pathError = Dropbox\Path::findErrorNonRoot($this->remotePath); + if ($pathError !== null) { + throw new UploadException(sprintf('Invalid path "%s".', $this->remotePath)); + } + $client = new Dropbox\Client($this->access_token, 'CloudBackupBundle'); + $entry = $client->getMetadataWithChildren($this->remotePath); + if (!$entry['is_dir']) { + throw RestoringNotAvailableException::create(); + } + + // Fetch the latest file + $file = end($entry['contents']); + $fileName = substr($file['path'], 1+strrpos($file['path'], '/')); + $stream = fopen('php://temp', 'r+'); + $client->getFile($file['path'], $stream); + fseek($stream, 0); + $content = stream_get_contents($stream); + + $splFile = new \SplFileInfo($this->restoreFolder . $fileName); + + $this->localFilesystem->dumpFile($splFile->getPathname(), $content); + + return $splFile; + } + + /** * {@inheritdoc} */ diff --git a/Client/GaufretteClient.php b/Client/GaufretteClient.php index 625c5f4..02438da 100644 --- a/Client/GaufretteClient.php +++ b/Client/GaufretteClient.php @@ -1,8 +1,9 @@ */ -class GaufretteClient implements ClientInterface +class GaufretteClient implements ClientInterface, DownloadableClientInterface { + /** + * @var Filesystem[] + */ private $filesystems; + /** + * @var string + */ + private $restoreFolder; + + /** + * @var LocalFilesystem + */ + private $localFilesystem; + + /** + * @param string $restoreFolder + * @param LocalFilesystem $localFilesystem + */ + public function __construct($restoreFolder = null, LocalFilesystem $localFilesystem = null) + { + $this->restoreFolder = $restoreFolder; + $this->localFilesystem = $localFilesystem; + } + /** * {@inheritdoc} */ @@ -35,6 +59,14 @@ public function addFilesystem(Filesystem $filesystem) $this->filesystems[] = $filesystem; } + /** + * @return Filesystem + */ + private function getFirstFilesystem() + { + return $this->filesystems[0]; + } + /** * {@inheritdoc} */ @@ -42,4 +74,25 @@ public function getName() { return 'Gaufrette'; } + + /** + * {@inheritdoc} + */ + public function download() + { + if (!$this->restoreFolder) { + throw InvalidConfigurationException::create('$restoreFolder'); + } + $fileSystem = $this->getFirstFilesystem(); + + $files = $fileSystem->keys(); + $fileName = end($files); + + $content = $fileSystem->get($fileName)->getContent(); + $splFile = new \SplFileInfo($this->restoreFolder . $fileName); + + $this->localFilesystem->dumpFile($splFile->getPathname(), $content); + + return $splFile; + } } diff --git a/Command/RestoreCommand.php b/Command/RestoreCommand.php new file mode 100644 index 0000000..81a49e5 --- /dev/null +++ b/Command/RestoreCommand.php @@ -0,0 +1,84 @@ +doRestore = $doRestore; + $this->restoreManager = $restoreManager; + + parent::__construct(); + } + + /** + * Configure the command. + */ + protected function configure() + { + $this + ->setName('dizda:backup:restore') + ->setDescription('Download latest backup, uncompress and restore.') + ->addOption('force', null, InputOption::VALUE_NONE) + ; + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + if (!$this->doRestore) { + $output->writeln('Restore is not available. Enable by setting dizda_cloud_backup.restore: true in config.yml.'); + + return self::RETURN_STATUS_NOT_AVAILABLE; + } + + if (!$input->getOption('force')) { + $output->writeln('Run command with --force to execute.'); + + return self::RETURN_STATUS_MISSING_FORCE_FLAG; + } + + $output->writeln('Restoring backup started'); + + if ($this->restoreManager->execute()) { + $output->writeln('Restoring backup completed'); + + return self::RETURN_STATUS_SUCCESS; + } else { + $output->writeln('Something went wrong'); + + return self::RETURN_STATUS_EXCEPTION_OCCURRED; + } + } +} diff --git a/Database/MySQL.php b/Database/MySQL.php index f048497..7b49a74 100644 --- a/Database/MySQL.php +++ b/Database/MySQL.php @@ -1,6 +1,7 @@ */ -class MySQL extends BaseDatabase +class MySQL extends BaseDatabase implements RestorableDatabaseInterface { const DB_PATH = 'mysql'; const CONFIGURATION_FILE_NAME = 'mysql.cnf'; @@ -20,14 +21,20 @@ class MySQL extends BaseDatabase private $params; /** - * DB Auth. - * + * @var string + */ + private $restoreFolder; + + /** * @param array $params * @param string $basePath + * @param string $restoreFolder */ - public function __construct($params, $basePath) + public function __construct($params, $basePath, $restoreFolder = null) { parent::__construct($basePath); + + $this->restoreFolder = $restoreFolder; $this->params = $params['mysql']; } @@ -135,6 +142,14 @@ public function dump() $this->removeConfigurationFile(); } + /** + * {@inheritdoc} + */ + public function restore() + { + $this->execute($this->getRestoreCommand()); + } + /** * {@inheritdoc} */ @@ -148,6 +163,35 @@ protected function getCommand() ); } + /** + * {@inheritdoc} + */ + protected function getRestoreCommand() + { + if (!$this->restoreFolder) { + throw InvalidConfigurationException::create('$restoreFolder'); + } + + $restoreAuth = ''; + if ($this->params['db_user']) { + $restoreAuth = sprintf('-u%s', $this->params['db_user']); + + if ($this->params['db_password']) { + $restoreAuth = $restoreAuth . sprintf(" --password=\"%s\"", $this->params['db_password']); + } + } + + $this->prepareFileName(); + + $command = sprintf('mysql %s %s < %s', + $restoreAuth, + $this->params['database'], + ProcessUtils::escapeArgument(sprintf('%smysql/%s', $this->restoreFolder, $this->fileName)) + ); + + return $command; + } + /** * {@inheritdoc} */ diff --git a/Database/RestorableDatabaseInterface.php b/Database/RestorableDatabaseInterface.php new file mode 100644 index 0000000..e732a8c --- /dev/null +++ b/Database/RestorableDatabaseInterface.php @@ -0,0 +1,11 @@ +end() ->end() ->end() + ->booleanNode('restore')->defaultFalse()->end() ->end(); return $treeBuilder; diff --git a/DependencyInjection/DizdaCloudBackupExtension.php b/DependencyInjection/DizdaCloudBackupExtension.php index f01d6de..558242c 100644 --- a/DependencyInjection/DizdaCloudBackupExtension.php +++ b/DependencyInjection/DizdaCloudBackupExtension.php @@ -29,6 +29,7 @@ public function load(array $configs, ContainerBuilder $container) /* Config output file */ $container->setParameter('dizda_cloud_backup.root_folder', $container->getParameter('kernel.root_dir').'/../'); $container->setParameter('dizda_cloud_backup.output_folder', $container->getParameter('kernel.cache_dir').'/backup/'); + $container->setParameter('dizda_cloud_backup.restore_folder', $container->getParameter('kernel.cache_dir').'/restore/'); /* Assign all config vars */ foreach ($config as $k => $v) { diff --git a/Event/RestoreEvent.php b/Event/RestoreEvent.php new file mode 100644 index 0000000..50c61b0 --- /dev/null +++ b/Event/RestoreEvent.php @@ -0,0 +1,31 @@ +file = $file; + } + + /** + * @return \SplFileInfo + */ + public function getFile() + { + return $this->file; + } +} diff --git a/Event/RestoreFailedEvent.php b/Event/RestoreFailedEvent.php new file mode 100644 index 0000000..5ff3746 --- /dev/null +++ b/Event/RestoreFailedEvent.php @@ -0,0 +1,31 @@ +exception = $exception; + } + + /** + * @return \Exception + */ + public function getException() + { + return $this->exception; + } +} diff --git a/Exception/InvalidConfigurationException.php b/Exception/InvalidConfigurationException.php new file mode 100644 index 0000000..fdd2353 --- /dev/null +++ b/Exception/InvalidConfigurationException.php @@ -0,0 +1,13 @@ +restoreFolder = $restoreFolder; + $this->filesystem = $filesystem; + } + + /** + * @param RestoreEvent $event + */ + public function whenRestoreIsCompleted(RestoreEvent $event) + { + $this->clean(); + } + + /** + * @param RestoreFailedEvent $event + */ + public function whenRestoreIsFailed(RestoreFailedEvent $event) + { + $this->clean(); + } + + private function clean() + { + $this->filesystem->remove($this->restoreFolder); + } +} diff --git a/Listener/LogRestoreCompletedListener.php b/Listener/LogRestoreCompletedListener.php new file mode 100644 index 0000000..0e07387 --- /dev/null +++ b/Listener/LogRestoreCompletedListener.php @@ -0,0 +1,29 @@ +logger = $logger; + } + + /** + * @param RestoreEvent $event + */ + public function whenRestoreIsCompleted(RestoreEvent $event) + { + $this->logger->info(sprintf('[dizda-backup] Restoring %s is completed', $event->getFile()->getFilename())); + } +} diff --git a/Listener/LogRestoreFailedListener.php b/Listener/LogRestoreFailedListener.php new file mode 100644 index 0000000..a987fe6 --- /dev/null +++ b/Listener/LogRestoreFailedListener.php @@ -0,0 +1,29 @@ +logger = $logger; + } + + /** + * @param RestoreFailedEvent $event + */ + public function whenRestoreIsFailed(RestoreFailedEvent $event) + { + $this->logger->critical('[dizda-backup] Restoring database failed', ['exception' => $event->getException()]); + } +} diff --git a/Manager/ClientManager.php b/Manager/ClientManager.php index 12fb94f..e8fcb1c 100644 --- a/Manager/ClientManager.php +++ b/Manager/ClientManager.php @@ -3,6 +3,8 @@ namespace Dizda\CloudBackupBundle\Manager; use Dizda\CloudBackupBundle\Client\ClientInterface; +use Dizda\CloudBackupBundle\Client\DownloadableClientInterface; +use Dizda\CloudBackupBundle\Exception\MissingDownloadableClientsException; use Psr\Log\LoggerInterface; /** @@ -70,4 +72,20 @@ public function upload($files) throw $exception; } } + + /** + * @return \SplFileInfo + * + * @throws MissingDownloadableClientsException + */ + public function download() + { + foreach ($this->children as $child) { + if ($child instanceof DownloadableClientInterface) { + return $child->download(); + } + } + + throw MissingDownloadableClientsException::create(); + } } diff --git a/Manager/DatabaseManager.php b/Manager/DatabaseManager.php index 697c281..de7afbb 100644 --- a/Manager/DatabaseManager.php +++ b/Manager/DatabaseManager.php @@ -3,6 +3,8 @@ namespace Dizda\CloudBackupBundle\Manager; use Dizda\CloudBackupBundle\Database\DatabaseInterface; +use Dizda\CloudBackupBundle\Database\RestorableDatabaseInterface; +use Dizda\CloudBackupBundle\Exception\MissingRestorableDatabaseException; use Psr\Log\LoggerInterface; /** @@ -52,4 +54,19 @@ public function dump() $child->dump(); } } + + /** + * @throws MissingRestorableDatabaseException + */ + public function restore() + { + foreach ($this->children as $child) { + if ($child instanceof RestorableDatabaseInterface) { + $child->restore(); + return; + } + } + + throw MissingRestorableDatabaseException::create(); + } } diff --git a/Manager/ProcessorManager.php b/Manager/ProcessorManager.php index 929a8a8..f7ca969 100644 --- a/Manager/ProcessorManager.php +++ b/Manager/ProcessorManager.php @@ -2,7 +2,9 @@ namespace Dizda\CloudBackupBundle\Manager; +use Dizda\CloudBackupBundle\Exception\UncompressionNotSupportedException; use Dizda\CloudBackupBundle\Processor\ProcessorInterface; +use Dizda\CloudBackupBundle\Processor\UncompressableProcessorInterface; use Dizda\CloudBackupBundle\Splitter\BaseSplitter; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Process\Process; @@ -66,14 +68,20 @@ class ProcessorManager */ protected $splitter; + /** + * @var string + */ + protected $restoreFolder; + /** * @param string $rootPath Path to root folder * @param string $outputPath Path to folder with archived files * @param string $filePrefix Prefix for archive file (e.g. sitename) * @param array $properties Date function format * @param array $folders Array of folders to archive (relative to $rootPath) + * @param string $restoreFolder Path to restore folder */ - public function __construct($rootPath, $outputPath, $filePrefix, $properties, array $folders = array()) + public function __construct($rootPath, $outputPath, $filePrefix, $properties, array $folders = array(), $restoreFolder = null) { $this->rootPath = $rootPath; $this->outputPath = $outputPath; @@ -81,6 +89,7 @@ public function __construct($rootPath, $outputPath, $filePrefix, $properties, ar $this->folders = $folders; $this->properties = $properties; $this->compressedArchivePath = $this->outputPath.'../backup_compressed/'; + $this->restoreFolder = $restoreFolder; $this->filesystem = new Filesystem(); } @@ -126,6 +135,22 @@ public function compress() } } + /** + * @param \SplFileInfo $file + */ + public function uncompress(\SplFileInfo $file) + { + if (!$this->processor instanceof UncompressableProcessorInterface) { + throw new UncompressionNotSupportedException( + sprintf('Uncompression is not supported for %s.', $this->processor->getName()) + ); + } + + $this->archivePath = $this->compressedArchivePath . $this->buildArchiveFilename(); + $archive = $this->processor->getUncompressCommand($this->restoreFolder, $file->getPathname(), $this->restoreFolder); + $this->execute($archive); + } + /** * Return the archive file name. * diff --git a/Manager/RestoreManager.php b/Manager/RestoreManager.php new file mode 100644 index 0000000..b13a70d --- /dev/null +++ b/Manager/RestoreManager.php @@ -0,0 +1,97 @@ +databaseManager = $databaseManager; + $this->clientManager = $clientManager; + $this->processorManager = $processorManager; + $this->eventDispatcher = $eventDispatcher; + $this->restoreFolder = $restoreFolder; + $this->filesystem = $filesystem; + $this->doRestore = (boolean) $doRestore; + } + + /** + * @return bool + */ + public function execute() + { + if (!$this->doRestore) { + throw RestoringNotAvailableException::create(); + } + + try { + $this->filesystem->mkdir($this->restoreFolder); + $file = $this->clientManager->download(); + $this->processorManager->uncompress($file); + $this->databaseManager->restore(); + $this->eventDispatcher->dispatch(RestoreEvent::RESTORE_COMPLETED, new RestoreEvent($file)); + + return true; + } catch (\Exception $e) { + $this->eventDispatcher->dispatch(RestoreFailedEvent::RESTORE_FAILED, new RestoreFailedEvent($e)); + } + + return false; + } +} diff --git a/Processor/UncompressableProcessorInterface.php b/Processor/UncompressableProcessorInterface.php new file mode 100644 index 0000000..c29acf0 --- /dev/null +++ b/Processor/UncompressableProcessorInterface.php @@ -0,0 +1,16 @@ +getMock(LocalFilesystem::class); + $localFilesystemMock->expects($this->once())->method('dumpFile') + ->with('/tmp/restore/db_2016-10-19.zip', 'foo bar'); + $client = new GaufretteClient('/tmp/restore/', $localFilesystemMock); + $fileMock = $this + ->getMockBuilder(File::class) + ->disableOriginalConstructor() + ->setMethods(['getContent']) + ->getMock() + ; + $fileMock->method('getContent')->willReturn('foo bar'); + $fileSystemMock = $this + ->getMockBuilder(Filesystem::class) + ->disableOriginalConstructor() + ->setMethods(['keys', 'get']) + ->getMock(); + $fileSystemMock->method('keys')->willReturn(['db_2016-10-19.zip']); + $fileSystemMock->method('get')->with('db_2016-10-19.zip')->willReturn($fileMock); + $client->addFilesystem($fileSystemMock); + + $file = $client->download(); + + $this->assertInstanceOf('\SplFileInfo', $file); + $this->assertEquals('/tmp/restore/db_2016-10-19.zip', $file->getPathname()); + } + + /** + * @test + * @expectedException \Dizda\CloudBackupBundle\Exception\InvalidConfigurationException + * @expectedExceptionMessage Parameter "$restoreFolder" is not set. + */ + public function throwExceptionIfRestoreFolderIsNotConfigured() + { + $client = new GaufretteClient(null, $this->getMock(LocalFilesystem::class)); + $client->download(); + } +} diff --git a/Tests/Database/MySQLTest.php b/Tests/Database/MySQLTest.php index a93f209..cedf35e 100644 --- a/Tests/Database/MySQLTest.php +++ b/Tests/Database/MySQLTest.php @@ -36,7 +36,7 @@ public function shouldDumpAllDatabases() 'db_user' => 'root', 'db_password' => 'test', ), - ), '/tmp/backup/'); + )); $this->assertEquals($mysql->getCommand(), "mysqldump --defaults-extra-file=\"/tmp/backup/mysql/mysql.cnf\" --all-databases > '/tmp/backup/mysql/all-databases.sql'"); $this->checkConfigurationFileExistsAndValid('root', 'test', 'localhost', '3306'); @@ -56,7 +56,7 @@ public function shouldDumpSpecifiedDatabase() 'db_user' => 'root', 'db_password' => 'test', ), - ), '/tmp/backup/'); + )); $this->assertEquals($mysql1->getCommand(), "mysqldump --defaults-extra-file=\"/tmp/backup/mysql/mysql.cnf\" dizbdd > '/tmp/backup/mysql/dizbdd.sql'"); $this->checkConfigurationFileExistsAndValid('root', 'test', 'localhost', '3306'); @@ -70,7 +70,7 @@ public function shouldDumpSpecifiedDatabase() 'db_user' => 'mysql', 'db_password' => 'somepwd', ), - ), '/tmp/backup/'); + )); $this->assertEquals($mysql2->getCommand(), "mysqldump --defaults-extra-file=\"/tmp/backup/mysql/mysql.cnf\" somebdd > '/tmp/backup/mysql/somebdd.sql'"); $this->checkConfigurationFileExistsAndValid('mysql', 'somepwd', 'somehost', '2222'); @@ -85,7 +85,7 @@ public function shouldDumpSpecifiedDatabase() 'db_user' => null, 'db_password' => null, ), - ), '/tmp/backup/'); + )); $this->assertEquals($mysql->getCommand(), 'mysqldump --defaults-extra-file="/tmp/backup/mysql/mysql.cnf" somebdd > \'/tmp/backup/mysql/somebdd.sql\''); $this->checkConfigurationFileExistsAndValid(null, null, 'somehost', '2222'); @@ -106,7 +106,7 @@ public function shouldDumpAllDatabasesWithNoAuth() 'db_user' => null, 'db_password' => null, ), - ), '/tmp/backup/'); + )); $this->assertEquals($mysql->getCommand(), 'mysqldump --defaults-extra-file="/tmp/backup/mysql/mysql.cnf" --all-databases > \'/tmp/backup/mysql/all-databases.sql\''); $this->checkConfigurationFileExistsAndValid(null, null, 'somehost', '2222'); @@ -127,7 +127,7 @@ public function shouldIgnoreSpecifiedTablesForSpecifiedDatabase() 'db_password' => 'test', 'ignore_tables' => array('table1', 'table2'), ), - ), '/tmp/backup/'); + )); $this->assertEquals($mysql->getCommand(), "mysqldump --defaults-extra-file=\"/tmp/backup/mysql/mysql.cnf\" dizbdd --ignore-table=dizbdd.table1 --ignore-table=dizbdd.table2 > '/tmp/backup/mysql/dizbdd.sql'"); $this->checkConfigurationFileExistsAndValid('root', 'test', 'localhost', '3306'); @@ -148,7 +148,7 @@ public function shouldIgnoreSpecifiedTablesForAllDatabase() 'db_password' => 'test', 'ignore_tables' => array('db1.table1', 'db2.table2'), ), - ), '/tmp/backup/'); + )); $this->assertEquals($mysql->getCommand(), "mysqldump --defaults-extra-file=\"/tmp/backup/mysql/mysql.cnf\" --all-databases --ignore-table=db1.table1 --ignore-table=db2.table2 > '/tmp/backup/mysql/all-databases.sql'"); $this->checkConfigurationFileExistsAndValid('root', 'test', 'localhost', '3306'); @@ -170,17 +170,86 @@ public function shouldThrowExceptionIfDatabaseIsNotSpecifiedForIgnoredTableDumpi 'db_password' => 'test', 'ignore_tables' => array('table1'), ), - ), '/tmp/backup/'); + )); $mysql->getCommand(); } + + /** + * @test + */ + public function shouldReturnRestoreCommand() + { + $mysql = new MySQLDummy([ + 'mysql' => [ + 'all_databases' => false, + 'db_host' => 'localhost', + 'db_port' => 3306, + 'database' => 'dizbdd', + 'db_user' => 'root', + 'db_password' => null, + ], + ]); + + $this->assertEquals('mysql -uroot dizbdd < \'/tmp/restore/mysql/dizbdd.sql\'', $mysql->getRestoreCommand()); + } + + /** + * @test + */ + public function shouldReturnRestoreCommandWithPassword() + { + $mysql = new MySQLDummy([ + 'mysql' => [ + 'all_databases' => false, + 'db_host' => 'localhost', + 'db_port' => 3306, + 'database' => 'dizbdd', + 'db_user' => 'root', + 'db_password' => 'foobar', + ], + ]); + + $this->assertEquals('mysql -uroot --password="foobar" dizbdd < \'/tmp/restore/mysql/dizbdd.sql\'', $mysql->getRestoreCommand()); + } + + /** + * @test + * @expectedException \Dizda\CloudBackupBundle\Exception\InvalidConfigurationException + * @expectedExceptionMessage Parameter "$restoreFolder" is not set. + */ + public function throwExceptionIfRestoreFolderIsNotConfigured() + { + $mysql = new MySQLDummy([ + 'mysql' => [ + 'all_databases' => false, + 'db_host' => 'localhost', + 'db_port' => 3306, + 'database' => 'dizbdd', + 'db_user' => 'root', + 'db_password' => 'foobar', + ], + ], '/tmp/backup/', null); + + $mysql->getRestoreCommand(); + } } class MySQLDummy extends MySQL { + public function __construct(array $params, $basePath = '/tmp/backup/', $restoreFolder = '/tmp/restore/') + { + parent::__construct($params, $basePath, $restoreFolder); + } + public function getCommand() { $this->prepareEnvironment(); return parent::getCommand(); } + + public function getRestoreCommand() + { + return parent::getRestoreCommand(); + } } diff --git a/Tests/Manager/ClientManagerTest.php b/Tests/Manager/ClientManagerTest.php new file mode 100644 index 0000000..6386ec3 --- /dev/null +++ b/Tests/Manager/ClientManagerTest.php @@ -0,0 +1,44 @@ +getMock(ClientInterface::class); + $clientMock = $this->getMock(DownloadableClientInterface::class); + $fileMock = $this + ->getMockBuilder(\SplFileInfo::class) + ->setConstructorArgs([tempnam(sys_get_temp_dir(), '')]) + ->getMock(); + $clientMock->expects($this->once())->method('download')->willReturn($fileMock); + $clients[] = $clientMock; + + $clientManager = new ClientManager($this->getMock(LoggerInterface::class), $clients); + $this->assertSame($fileMock, $clientManager->download()); + } + + /** + * @test + * @expectedException \Dizda\CloudBackupBundle\Exception\MissingDownloadableClientsException + * @expectedExceptionMessage No downloadable client is registered. + */ + public function shouldThrowExceptionIfNoChildIsADownloadableClient() + { + $clients = []; + $clients[] = $this->getMock(ClientInterface::class); + $clients[] = $this->getMock(ClientInterface::class); + + $clientManager = new ClientManager($this->getMock(LoggerInterface::class), $clients); + $clientManager->download(); + } +} diff --git a/Tests/Manager/RestoreManagerTest.php b/Tests/Manager/RestoreManagerTest.php new file mode 100644 index 0000000..6a1a5e4 --- /dev/null +++ b/Tests/Manager/RestoreManagerTest.php @@ -0,0 +1,111 @@ +getMockBuilder(\SplFileInfo::class) + ->setConstructorArgs([tempnam(sys_get_temp_dir(), '')]) + ->getMock(); + + $databaseManagerMock = $this->getMockBuilder(DatabaseManager::class)->disableOriginalConstructor()->getMock(); + $databaseManagerMock->expects($this->once())->method('restore'); + $clientManagerMock = $this->getMockBuilder(ClientManager::class)->disableOriginalConstructor()->getMock(); + $clientManagerMock->expects($this->once())->method('download')->willReturn($fileMock); + $processorManagerMock = $this->getMockBuilder(ProcessorManager::class)->disableOriginalConstructor()->getMock(); + $processorManagerMock->expects($this->once())->method('uncompress'); + $eventDispatcherMock = $this->getMock(EventDispatcherInterface::class); + $eventDispatcherMock->expects($this->once())->method('dispatch')->with(RestoreEvent::RESTORE_COMPLETED); + $filesystemMock = $this->getMock(Filesystem::class); + + $restoreManager = new RestoreManager( + $databaseManagerMock, + $clientManagerMock, + $processorManagerMock, + $eventDispatcherMock, + '', + $filesystemMock, + true + ); + + $restoreManager->execute(); + } + + /** + * @test + * @expectedException \Dizda\CloudBackupBundle\Exception\RestoringNotAvailableException + * @expectedExceptionMessage Restoring is not available. + */ + public function shouldNotRestoreDatabase() + { + $databaseManagerMock = $this->getMockBuilder(DatabaseManager::class)->disableOriginalConstructor()->getMock(); + $databaseManagerMock->expects($this->never())->method('restore'); + $clientManagerMock = $this->getMockBuilder(ClientManager::class)->disableOriginalConstructor()->getMock(); + $clientManagerMock->expects($this->never())->method('download'); + $processorManagerMock = $this->getMockBuilder(ProcessorManager::class)->disableOriginalConstructor()->getMock(); + $processorManagerMock->expects($this->never())->method('uncompress'); + $eventDispatcherMock = $this->getMock(EventDispatcherInterface::class); + $eventDispatcherMock->expects($this->never())->method('dispatch'); + $filesystemMock = $this->getMock(Filesystem::class); + + $restoreManager = new RestoreManager( + $databaseManagerMock, + $clientManagerMock, + $processorManagerMock, + $eventDispatcherMock, + '', + $filesystemMock, + false + ); + + $restoreManager->execute(); + } + + /** + * @test + */ + public function shouldDispachRestoreFailedEventIfExceptionOccur() + { + $databaseManagerMock = $this->getMockBuilder(DatabaseManager::class)->disableOriginalConstructor()->getMock(); + $databaseManagerMock->expects($this->never())->method('restore'); + $clientManagerMock = $this->getMockBuilder(ClientManager::class)->disableOriginalConstructor()->getMock(); + $clientManagerMock->expects($this->never())->method('download'); + $processorManagerMock = $this->getMockBuilder(ProcessorManager::class)->disableOriginalConstructor()->getMock(); + $processorManagerMock->expects($this->never())->method('uncompress'); + $eventDispatcherMock = $this->getMock(EventDispatcherInterface::class); + $eventDispatcherMock->expects($this->any())->method('dispatch')->with( + new \PHPUnit_Framework_Constraint_Not(RestoreEvent::RESTORE_COMPLETED) + ); + $eventDispatcherMock->expects($this->once())->method('dispatch')->with(RestoreFailedEvent::RESTORE_FAILED); + $filesystemMock = $this->getMock(Filesystem::class); + $filesystemMock->expects($this->once())->method('mkdir')->will($this->throwException(new \Exception())); + + $restoreManager = new RestoreManager( + $databaseManagerMock, + $clientManagerMock, + $processorManagerMock, + $eventDispatcherMock, + '', + $filesystemMock, + true + ); + + $restoreManager->execute(); + } +} diff --git a/composer.json b/composer.json index 71534f3..d2949f5 100644 --- a/composer.json +++ b/composer.json @@ -19,8 +19,8 @@ ], "require": { - "php": "^5.4 || ^7.0", - "symfony/framework-bundle": "^2.1 || ^3.0", + "php": "^5.5 || ^7.0", + "symfony/framework-bundle": "^2.3 || ^3.0", "psr/log": "^1.0.1" },