From 391fb57070907480d65daeb17e675d0f38f9f693 Mon Sep 17 00:00:00 2001 From: Bastien <53140475+bastien70@users.noreply.github.com> Date: Tue, 13 Sep 2022 09:34:33 +0200 Subject: [PATCH] Added backup restoration support (#76) --- .github/workflows/continuous-integration.yml | 17 +- README.md | 2 +- composer.lock | 3 +- config/services.yaml | 9 +- docker-compose.test.yaml | 7 + migrations/Version20220902073138.php | 31 +++ phpstan-baseline.neon | 6 +- public/assets/js/confirm-modal.js | 12 ++ src/Controller/Admin/BackupCrudController.php | 41 ++++ src/Factory/DatabaseFactory.php | 5 + src/Faker/Provider/fixture.sql | 197 +++++++++++++++++- src/Helper/FlysystemHelper.php | 8 + src/Service/BackupService.php | 42 ++++ .../bundles/EasyAdminBundle/layout.html.twig | 23 ++ tests/Faker/Provider/FileProviderTest.php | 2 +- tests/Fixtures/DataProvider.php | 131 ++++++++++++ tests/Helper/FlysystemHelperTest.php | 125 ++--------- tests/Service/BackupServiceTest.php | 20 ++ translations/messages.en.yaml | 10 + translations/messages.fr.yaml | 10 + 20 files changed, 566 insertions(+), 135 deletions(-) create mode 100644 migrations/Version20220902073138.php create mode 100644 public/assets/js/confirm-modal.js create mode 100644 templates/bundles/EasyAdminBundle/layout.html.twig create mode 100644 tests/Fixtures/DataProvider.php diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index daf4950..3c430ef 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -11,19 +11,10 @@ on: jobs: phpunit: name: PHPUnit (PHP ${{ matrix.php-version }}) - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 env: SYMFONY_DEPRECATIONS_HELPER: disabled - services: - mysql: - image: mysql:8.0 - env: - MYSQL_ROOT_PASSWORD: root - ports: - - 3307:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 - strategy: matrix: php-version: @@ -45,6 +36,9 @@ jobs: - name: Install Composer dependencies uses: ramsey/composer-install@v1 + - name: Start mariadb + run: docker-compose -f docker-compose.test.yaml up -d mariadb + - name: Start minio run: docker-compose -f docker-compose.test.yaml up -d minio createbuckets @@ -69,5 +63,8 @@ jobs: - name: Stop minio run: docker-compose -f docker-compose.test.yaml stop minio + - name: Stop mariadb + run: docker-compose -f docker-compose.test.yaml stop mariadb + - name: Upload to Codecov uses: codecov/codecov-action@v2 diff --git a/README.md b/README.md index 78ab39f..667ae23 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ To access your databases backups, click the `Backups` tab. ![Backup list](docs/images/backup-list.png?raw=true) -You will be able to download or delete a backup. +You will be able to download, restore or delete a backup. ## Update the application diff --git a/composer.lock b/composer.lock index 1f8f340..ae50475 100644 --- a/composer.lock +++ b/composer.lock @@ -9423,6 +9423,7 @@ "issues": "https://github.com/PHP-CS-Fixer/diff/issues", "source": "https://github.com/PHP-CS-Fixer/diff/tree/v2.0.2" }, + "abandoned": true, "time": "2020-10-14T08:32:19+00:00" }, { @@ -11955,5 +11956,5 @@ "ext-iconv": "*" }, "platform-dev": [], - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.3.0" } diff --git a/config/services.yaml b/config/services.yaml index db2e081..63e387b 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -29,4 +29,11 @@ services: - '../src/Kernel.php' # add more service definitions when explicit configuration is needed - # please note that last definitions always *replace* previous ones \ No newline at end of file + # please note that last definitions always *replace* previous ones + +when@test: + services: + App\Tests\Fixtures\DataProvider: + public: true + autowire: true + autoconfigure: true \ No newline at end of file diff --git a/docker-compose.test.yaml b/docker-compose.test.yaml index 7ce4f51..141c1b7 100644 --- a/docker-compose.test.yaml +++ b/docker-compose.test.yaml @@ -7,6 +7,10 @@ services: - '3307:3306' environment: MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: test_db + volumes: + - ./src/Faker/Provider/fixture.sql:/docker-entrypoint-initdb.d/fixture.sql + - db_data:/var/lib/mysql minio: image: minio/minio environment: @@ -30,3 +34,6 @@ services: /usr/bin/mc policy set public myminio/somebucketname; exit 0; " + +volumes: + db_data: diff --git a/migrations/Version20220902073138.php b/migrations/Version20220902073138.php new file mode 100644 index 0000000..6b9deeb --- /dev/null +++ b/migrations/Version20220902073138.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE `database` CHANGE options_reset_auto_increment options_reset_auto_increment TINYINT(1) NOT NULL, CHANGE options_add_drop_database options_add_drop_database TINYINT(1) NOT NULL, CHANGE options_add_drop_table options_add_drop_table TINYINT(1) NOT NULL, CHANGE options_add_drop_trigger options_add_drop_trigger TINYINT(1) NOT NULL, CHANGE options_add_locks options_add_locks TINYINT(1) NOT NULL, CHANGE options_complete_insert options_complete_insert TINYINT(1) NOT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE `database` CHANGE options_reset_auto_increment options_reset_auto_increment TINYINT(1) DEFAULT 0 NOT NULL, CHANGE options_add_drop_database options_add_drop_database TINYINT(1) DEFAULT 0 NOT NULL, CHANGE options_add_drop_table options_add_drop_table TINYINT(1) DEFAULT 0 NOT NULL, CHANGE options_add_drop_trigger options_add_drop_trigger TINYINT(1) DEFAULT 1 NOT NULL, CHANGE options_add_locks options_add_locks TINYINT(1) DEFAULT 1 NOT NULL, CHANGE options_complete_insert options_complete_insert TINYINT(1) DEFAULT 0 NOT NULL'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4bc1180..8d366c0 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -18,9 +18,9 @@ parameters: - message: "#^Call to an undefined method App\\\\Entity\\\\AdapterConfig\\:\\:setS3BucketName\\(\\)\\.$#" count: 2 - path: tests/Helper/FlysystemHelperTest.php + path: tests/Fixtures/DataProvider.php - - message: "#^Method App\\\\Tests\\\\Helper\\\\FlysystemHelperTest\\:\\:getLocalS3Adapter\\(\\)\\ should return App\\\\Entity\\\\LocalAdapter but returns App\\\\Entity\\\\AdapterConfig.$#" + message: "#^Method App\\\\Tests\\\\Fixtures\\\\DataProvider\\:\\:getLocalS3Adapter\\(\\)\\ should return App\\\\Entity\\\\LocalAdapter but returns App\\\\Entity\\\\AdapterConfig.$#" count: 1 - path: tests/Helper/FlysystemHelperTest.php + path: tests/Fixtures/DataProvider.php diff --git a/public/assets/js/confirm-modal.js b/public/assets/js/confirm-modal.js new file mode 100644 index 0000000..c2558b4 --- /dev/null +++ b/public/assets/js/confirm-modal.js @@ -0,0 +1,12 @@ +document.addEventListener("DOMContentLoaded",( + function() { + document.querySelectorAll(".confirm-action").forEach((function(e){ + e.addEventListener("click",(function(t){ + t.preventDefault(); + document.querySelector("#modal-confirm-button").addEventListener("click",(function(){ + location.replace(e.getAttribute("href")); + })); + })); + })); + } +)); \ No newline at end of file diff --git a/src/Controller/Admin/BackupCrudController.php b/src/Controller/Admin/BackupCrudController.php index f8617f0..ccc5caf 100644 --- a/src/Controller/Admin/BackupCrudController.php +++ b/src/Controller/Admin/BackupCrudController.php @@ -8,12 +8,14 @@ use App\Entity\Backup; use App\Entity\User; use App\Helper\FlysystemHelper; +use App\Service\BackupService; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection; use EasyCorp\Bundle\EasyAdminBundle\Collection\FilterCollection; use EasyCorp\Bundle\EasyAdminBundle\Config\Action; use EasyCorp\Bundle\EasyAdminBundle\Config\Actions; +use EasyCorp\Bundle\EasyAdminBundle\Config\Assets; use EasyCorp\Bundle\EasyAdminBundle\Config\Crud; use EasyCorp\Bundle\EasyAdminBundle\Config\Filters; use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext; @@ -28,7 +30,9 @@ use EasyCorp\Bundle\EasyAdminBundle\Filter\EntityFilter; use EasyCorp\Bundle\EasyAdminBundle\Filter\TextFilter; use EasyCorp\Bundle\EasyAdminBundle\Orm\EntityRepository; +use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Translation\TranslatableMessage; use Symfony\Contracts\Translation\TranslatorInterface; /** @@ -39,6 +43,8 @@ final class BackupCrudController extends AbstractCrudController public function __construct( private readonly TranslatorInterface $translator, private readonly FlysystemHelper $flysystemHelper, + private readonly BackupService $backupService, + private readonly AdminUrlGenerator $adminUrlGenerator, ) { } @@ -75,6 +81,25 @@ public function downloadBackupAction(AdminContext $context): Response return $this->flysystemHelper->download($backup); } + public function importBackupAction(AdminContext $context): Response + { + /** @var Backup $backup */ + $backup = $context->getEntity()->getInstance(); + + try { + $this->backupService->import($backup); + $this->addFlash('success', new TranslatableMessage('backup.action.import.flash_success')); + } catch (\RuntimeException $e) { + $this->addFlash('danger', new TranslatableMessage('backup.action.import.flash_error', ['%message%' => $e->getMessage()])); + } + + $url = $this->adminUrlGenerator->setController(self::class) + ->setAction(Action::INDEX) + ->generateUrl(); + + return $this->redirect($url); + } + /** * @param Backup $entityInstance */ @@ -89,8 +114,17 @@ public function configureActions(Actions $actions): Actions $downloadBackupAction = Action::new('downloadBackup', 'backup.action.download') ->linkToCrudAction('downloadBackupAction'); + $importBackupAction = Action::new('importBackup', 'backup.action.import.title') + ->linkToCrudAction('importBackupAction') + ->addCssClass('confirm-action') + ->setHtmlAttributes([ + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#modal-confirm', + ]); + return $actions ->add(Crud::PAGE_INDEX, $downloadBackupAction) + ->add(Crud::PAGE_INDEX, $importBackupAction) ->remove(Crud::PAGE_INDEX, Action::NEW) ->remove(Crud::PAGE_INDEX, Action::EDIT) ->disable(Action::NEW, Action::EDIT) @@ -106,6 +140,13 @@ public function configureCrud(Crud $crud): Crud ; } + public function configureAssets(Assets $assets): Assets + { + $assets->addJsFile('assets/js/confirm-modal.js'); + + return parent::configureAssets($assets); + } + public function configureFields(string $pageName): iterable { yield DateTimeField::new('createdAt', 'backup.field.created_at') diff --git a/src/Factory/DatabaseFactory.php b/src/Factory/DatabaseFactory.php index 4cf5938..f90f4f7 100644 --- a/src/Factory/DatabaseFactory.php +++ b/src/Factory/DatabaseFactory.php @@ -6,6 +6,7 @@ use App\Entity\Database; use App\Entity\Embed\BackupTask; +use App\Entity\Embed\Options; use App\Entity\Enum\BackupTaskPeriodicity; use App\Entity\User; use App\Repository\DatabaseRepository; @@ -81,6 +82,10 @@ protected function getDefaults(): array 'startFrom' => new \DateTime('-1 day'), 'nextIteration' => new \DateTime('-1 day'), ]), + 'options' => AnonymousFactory::new(Options::class)->create([ + 'addDropTable' => true, + 'addDropDatabase' => true, + ]), ]; } diff --git a/src/Faker/Provider/fixture.sql b/src/Faker/Provider/fixture.sql index 92c3db8..3d6bb5b 100644 --- a/src/Faker/Provider/fixture.sql +++ b/src/Faker/Provider/fixture.sql @@ -1,5 +1,192 @@ -CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, role VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB; -CREATE TABLE backup (id INT AUTO_INCREMENT NOT NULL, db_id INT NOT NULL, backup_file_name VARCHAR(255) DEFAULT NULL, mime_type VARCHAR(255) DEFAULT NULL, updated_at DATETIME NOT NULL, backup_file_size INT DEFAULT NULL, context VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL, original_name VARCHAR(255) DEFAULT NULL, INDEX IDX_3FF0D1ACA2BF053A (db_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB; -CREATE TABLE `database` (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, host VARCHAR(255) NOT NULL, port INT DEFAULT NULL, db_user VARCHAR(255) NOT NULL, db_password VARCHAR(255) NOT NULL, db_name VARCHAR(255) NOT NULL, max_backups INT NOT NULL, created_at DATETIME NOT NULL, INDEX IDX_C953062EA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB; -ALTER TABLE backup ADD CONSTRAINT FK_3FF0D1ACA2BF053A FOREIGN KEY (db_id) REFERENCES `database` (id); -ALTER TABLE `database` ADD CONSTRAINT FK_C953062EA76ED395 FOREIGN KEY (user_id) REFERENCES user (id); +-- mysqldump-php https://github.com/ifsnop/mysqldump-php +-- +-- Host: 127.0.0.1:3306 Database: dbsaver_test_fixture +-- ------------------------------------------------------ +-- Server version 8.0.30-0ubuntu0.22.04.1 +-- Date: Tue, 13 Sep 2022 08:26:14 +0200 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `messenger_messages` +-- + +DROP TABLE IF EXISTS `messenger_messages`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `messenger_messages` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `body` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `headers` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `queue_name` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` datetime NOT NULL, + `available_at` datetime NOT NULL, + `delivered_at` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `IDX_75EA56E0FB7336F0` (`queue_name`), + KEY `IDX_75EA56E0E3BD61CE` (`available_at`), + KEY `IDX_75EA56E016BA31DB` (`delivered_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `messenger_messages` +-- + +LOCK TABLES `messenger_messages` WRITE; +/*!40000 ALTER TABLE `messenger_messages` DISABLE KEYS */; +SET autocommit=0; +/*!40000 ALTER TABLE `messenger_messages` ENABLE KEYS */; +UNLOCK TABLES; +COMMIT; + +-- Dumped table `messenger_messages` with 0 row(s) +-- + +-- +-- Table structure for table `post` +-- + +DROP TABLE IF EXISTS `post`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `post` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `IDX_5A8A6C8DA76ED395` (`user_id`), + CONSTRAINT `FK_5A8A6C8DA76ED395` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=10001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `post` +-- + +LOCK TABLES `post` WRITE; +/*!40000 ALTER TABLE `post` DISABLE KEYS */; +SET autocommit=0; +INSERT INTO `post` VALUES (1,1,'Esse et quia enim.','Veniam cupiditate voluptas in harum maxime optio. Ullam dolor et ipsa fugiat rerum et. Provident neque aut labore aut ducimus non sapiente.','2015-09-29 16:46:56'),(2,14,'Odio est amet quia minima ut.','Quae voluptas est porro illum. Dolorem ut sunt voluptatum numquam unde voluptas explicabo. Dolore et quidem ipsum eos ipsam. Facilis necessitatibus aut officiis minima quae exercitationem.','2021-08-27 02:14:01'),(3,12,'Sed iste mollitia est.','Aut occaecati voluptate possimus suscipit nobis. Rem enim numquam molestias ipsa et dolorum. Ut iure nihil quia dolor.','2021-11-21 09:02:22'),(4,8,'Nam unde quo est aliquam.','Reprehenderit hic eligendi et quas est voluptas ipsa. Eos eveniet non laboriosam aut. Saepe sequi est similique modi cum.','2013-07-23 06:20:56'),(5,14,'Et sit recusandae est ut.','Pariatur possimus quia velit ex sit. Unde voluptatem nulla accusamus non. Enim et dolorem sit error voluptas. Nesciunt iure omnis autem voluptatum molestiae.','2013-11-23 13:21:30'); +/*!40000 ALTER TABLE `post` ENABLE KEYS */; +UNLOCK TABLES; +COMMIT; + +-- Dumped table `post` with 5 row(s) +-- + +-- +-- Table structure for table `post_tag` +-- + +DROP TABLE IF EXISTS `post_tag`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `post_tag` ( + `post_id` int NOT NULL, + `tag_id` int NOT NULL, + PRIMARY KEY (`post_id`,`tag_id`), + KEY `IDX_5ACE3AF04B89032C` (`post_id`), + KEY `IDX_5ACE3AF0BAD26311` (`tag_id`), + CONSTRAINT `FK_5ACE3AF04B89032C` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE CASCADE, + CONSTRAINT `FK_5ACE3AF0BAD26311` FOREIGN KEY (`tag_id`) REFERENCES `tag` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `post_tag` +-- + +LOCK TABLES `post_tag` WRITE; +/*!40000 ALTER TABLE `post_tag` DISABLE KEYS */; +SET autocommit=0; +INSERT INTO `post_tag` VALUES (1,7),(2,3),(2,9),(2,11),(2,16),(3,6),(4,11),(4,12),(4,13),(4,14),(4,17),(5,2); +/*!40000 ALTER TABLE `post_tag` ENABLE KEYS */; +UNLOCK TABLES; +COMMIT; + +-- Dumped table `post_tag` with 12 row(s) +-- + +-- +-- Table structure for table `tag` +-- + +DROP TABLE IF EXISTS `tag`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `tag` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `tag` +-- + +LOCK TABLES `tag` WRITE; +/*!40000 ALTER TABLE `tag` DISABLE KEYS */; +SET autocommit=0; +INSERT INTO `tag` VALUES (2,'est'),(3,'vero'),(6,'natus'),(7,'dolor'),(9,'quas'),(11,'voluptatum'),(12,'quod'),(13,'est'),(14,'velit'),(16,'deleniti'),(17,'nostrum'); +/*!40000 ALTER TABLE `tag` ENABLE KEYS */; +UNLOCK TABLES; +COMMIT; + +-- Dumped table `tag` with 11 row(s) +-- + +-- +-- Table structure for table `user` +-- + +DROP TABLE IF EXISTS `user`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `user` ( + `id` int NOT NULL AUTO_INCREMENT, + `email` varchar(180) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `roles` json NOT NULL, + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UNIQ_8D93D649E7927C74` (`email`) +) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `user` +-- + +LOCK TABLES `user` WRITE; +/*!40000 ALTER TABLE `user` DISABLE KEYS */; +SET autocommit=0; +INSERT INTO `user` VALUES (1,'renee.duhamel@noos.fr','[]','$2y$13$wD/tsbF52MwxXFzhrvXcae0k3AZogxaMe14uW1NsPCpcr5RvDAcV2'),(8,'franck.pinto@wanadoo.fr','[]','$2y$13$VnaoQpTf9zQ2EqCDcOqzJu9nZ7FMiO/HnY3QY0FXeX/w.cf4IshHi'),(12,'elise.dias@yahoo.fr','[]','$2y$13$OOoqlzaShroRkxa.KPKcaOO0cOmF4ShzPhf8Y.blOmcS1oVV0nAR6'),(14,'aroussel@wanadoo.fr','[]','$2y$13$IalGcpMcEXMLldn7j2F4.O7Yo7DCVxDwniBgp09dK8xVvEZgpwMV6'); +/*!40000 ALTER TABLE `user` ENABLE KEYS */; +UNLOCK TABLES; +COMMIT; + +-- Dumped table `user` with 4 row(s) +-- + +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on: Tue, 13 Sep 2022 08:26:14 +0200 diff --git a/src/Helper/FlysystemHelper.php b/src/Helper/FlysystemHelper.php index c135f45..7fe8ec8 100644 --- a/src/Helper/FlysystemHelper.php +++ b/src/Helper/FlysystemHelper.php @@ -77,6 +77,14 @@ public function upload(Backup $backup): void $filesystem->write($backup->getBackupFileName(), $backup->getBackupFile()->getContent()); } + public function getContent(Backup $backup): string + { + $adapter = $this->getAdapter($backup->getDatabase()->getAdapter()); + $filesystem = new Filesystem($adapter); + + return $filesystem->read($backup->getBackupFileName()); + } + public function download(Backup $backup): Response { $adapterConfig = $backup->getDatabase()->getAdapter(); diff --git a/src/Service/BackupService.php b/src/Service/BackupService.php index dd1033b..05ee79b 100644 --- a/src/Service/BackupService.php +++ b/src/Service/BackupService.php @@ -13,6 +13,7 @@ use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Expr\Comparison; +use Doctrine\DBAL\DriverManager; use Doctrine\ORM\EntityManagerInterface; use Ifsnop\Mysqldump\Mysqldump; use Nzo\UrlEncryptorBundle\Encryptor\Encryptor; @@ -109,6 +110,47 @@ public function backup(Database $database, string $context): BackupStatus return $backupStatus; } + public function import(Backup $backup): void + { + $content = $this->flysystemHelper->getContent($backup); + $database = $backup->getDatabase(); + + $mysqlHost = $database->getHost(); + $mysqlUser = $database->getUser(); + $mysqlPassword = $this->encryptor->decrypt($database->getPassword()); + $mysqlDatabase = $database->getName(); + $mysqlPort = $database->getPort(); + + $conn = DriverManager::getConnection([ + 'dbname' => $mysqlDatabase, + 'user' => $mysqlUser, + 'password' => $mysqlPassword, + 'host' => $mysqlHost, + 'driver' => 'mysqli', + 'port' => $mysqlPort, + ]); + + $query = ''; + $temp = tmpfile(); + fwrite($temp, $content); + $sqlScript = file(stream_get_meta_data($temp)['uri']); + + foreach ($sqlScript as $line) { + $startWith = substr(trim($line), 0, 2); + $endWith = substr(trim($line), -1, 1); + + if (empty($line) || '--' === $startWith || '//' === $startWith) { + continue; + } + + $query = $query . $line; + if (';' === $endWith) { + $conn->executeStatement($query); + $query = ''; + } + } + } + public function clean(Database $database): void { $backupCollection = new ArrayCollection( diff --git a/templates/bundles/EasyAdminBundle/layout.html.twig b/templates/bundles/EasyAdminBundle/layout.html.twig new file mode 100644 index 0000000..92f68c7 --- /dev/null +++ b/templates/bundles/EasyAdminBundle/layout.html.twig @@ -0,0 +1,23 @@ +{% extends '@!EasyAdmin/layout.html.twig' %} + +{% block content_footer_wrapper %} + +{% endblock %} \ No newline at end of file diff --git a/tests/Faker/Provider/FileProviderTest.php b/tests/Faker/Provider/FileProviderTest.php index ea23cca..a64ecb2 100644 --- a/tests/Faker/Provider/FileProviderTest.php +++ b/tests/Faker/Provider/FileProviderTest.php @@ -20,6 +20,6 @@ protected function setUp(): void public function testGetSqlFile(): void { $file = $this->fileProvider->getSqlFile(); - self::assertStringContainsString('CREATE TABLE user', $file->getContent()); + self::assertStringContainsString('CREATE TABLE `post`', $file->getContent()); } } diff --git a/tests/Fixtures/DataProvider.php b/tests/Fixtures/DataProvider.php new file mode 100644 index 0000000..cbc6ce0 --- /dev/null +++ b/tests/Fixtures/DataProvider.php @@ -0,0 +1,131 @@ +setName('minio') + ->setPrefix('backups') + ->setS3BucketName('somebucketname') + ->setS3AccessId('minio') + ->setS3AccessSecret($this->encryptor->encrypt('minio123')) + ->setS3Provider(S3Provider::OTHER) + ->setS3Endpoint('http://127.0.0.1:9004') + ->setS3Region('eu-east-1'); + } + + public function getInvalidS3Adapter(): S3Adapter + { + return (new S3Adapter()) + ->setName('minio') + ->setPrefix('backups') + ->setS3BucketName('somebucketname') + ->setS3AccessId('minio') + ->setS3AccessSecret($this->encryptor->encrypt('bad_access_secret')) + ->setS3Provider(S3Provider::OTHER) + ->setS3Endpoint('http://127.0.0.1:9004') + ->setS3Region('eu-east-1'); + } + + public function getLocalS3Adapter(): LocalAdapter + { + return (new LocalAdapter()) + ->setName('local') + ->setPrefix('backups'); + } + + public function getBackupFromS3Adapter(string $dbName = 'dbsaver_test'): Backup + { + $s3Adapter = $this->getValidS3Adapter(); + + $this->manager->persist($s3Adapter); + + $database = (new Database()) + ->setHost('127.0.0.1') + ->setUser('root') + ->setPassword($this->encryptor->encrypt('root')) + ->setPort(3307) + ->setName($dbName) + ->setMaxBackups(5) + ->setAdapter($s3Adapter) + ->setOwner(UserFactory::random()->object()) + ->setOptions( + (new Options()) + ->setAddDropDatabase(true) + ->setAddDropTable(true) + ); + + $this->setBackupTask($database); + + $this->manager->persist($database); + $this->manager->flush(); + + return BackupFactory::new() + ->withDatabase($database) + ->create() + ->object(); + } + + public function getBackupFromLocalAdapter(string $dbName = 'dbsaver_test'): Backup + { + $localAdapter = $this->getLocalS3Adapter(); + + $this->manager->persist($localAdapter); + + $database = (new Database()) + ->setHost('127.0.0.1') + ->setUser('root') + ->setPassword($this->encryptor->encrypt('root')) + ->setPort(3307) + ->setName($dbName) + ->setMaxBackups(5) + ->setAdapter($localAdapter) + ->setOwner(UserFactory::random()->object()) + ->setOptions( + (new Options()) + ->setAddDropDatabase(true) + ->setAddDropTable(true) + ); + + $this->setBackupTask($database); + + $this->manager->persist($database); + + return BackupFactory::new() + ->withDatabase($database) + ->create() + ->object(); + } + + public function setBackupTask(Database $database): void + { + $backupTask = $database->getBackupTask(); + $backupTask->setPeriodicity(BackupTaskPeriodicity::WEEK) + ->setPeriodicityNumber(1) + ->setStartFrom(new \DateTime('-1 day')) + ->setNextIteration(new \DateTime('-1 day')); + } +} diff --git a/tests/Helper/FlysystemHelperTest.php b/tests/Helper/FlysystemHelperTest.php index 6f2904e..1ab6938 100644 --- a/tests/Helper/FlysystemHelperTest.php +++ b/tests/Helper/FlysystemHelperTest.php @@ -4,18 +4,10 @@ namespace App\Tests\Helper; -use App\Entity\Backup; -use App\Entity\Database; -use App\Entity\Enum\BackupTaskPeriodicity; -use App\Entity\Enum\S3Provider; use App\Entity\LocalAdapter; -use App\Entity\S3Adapter; -use App\Factory\BackupFactory; -use App\Factory\UserFactory; use App\Helper\FlysystemHelper; -use Doctrine\ORM\EntityManagerInterface; +use App\Tests\Fixtures\DataProvider; use League\Flysystem\Filesystem; -use Nzo\UrlEncryptorBundle\Encryptor\Encryptor; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\HttpFoundation\Response; use Zenstruck\Foundry\Test\Factories; @@ -23,16 +15,15 @@ class FlysystemHelperTest extends KernelTestCase { use Factories; + private FlysystemHelper $flysystemHelper; - private Encryptor $encryptor; - private EntityManagerInterface $manager; + private DataProvider $dataProvider; protected function setUp(): void { $kernel = self::bootKernel(); $this->flysystemHelper = self::getContainer()->get(FlysystemHelper::class); - $this->encryptor = self::getContainer()->get(Encryptor::class); - $this->manager = self::getContainer()->get(EntityManagerInterface::class); + $this->dataProvider = self::getContainer()->get(DataProvider::class); } public function testLocalConnectionOk(): void @@ -46,134 +37,42 @@ public function testLocalConnectionOk(): void public function testS3ConnectionOk(): void { - self::assertTrue($this->flysystemHelper->isConnectionOk($this->getValidS3Adapter())); + self::assertTrue($this->flysystemHelper->isConnectionOk($this->dataProvider->getValidS3Adapter())); } public function testS3ConnectionNotOk(): void { - self::assertFalse($this->flysystemHelper->isConnectionOk($this->getInvalidS3Adapter())); + self::assertFalse($this->flysystemHelper->isConnectionOk($this->dataProvider->getInvalidS3Adapter())); } public function testGetFlysystemAdapter(): void { self::assertInstanceOf( Filesystem::class, - $this->flysystemHelper->getFileSystem($this->getValidS3Adapter()) + $this->flysystemHelper->getFileSystem($this->dataProvider->getValidS3Adapter()) ); self::assertInstanceOf( Filesystem::class, - $this->flysystemHelper->getFileSystem($this->getLocalS3Adapter()) + $this->flysystemHelper->getFileSystem($this->dataProvider->getLocalS3Adapter()) ); } public function testUploadDownloadRemoveValidS3Adapter(): void { - $backup = $this->getBackupFromS3Adapter(); + $backup = $this->dataProvider->getBackupFromS3Adapter(); $this->flysystemHelper->upload($backup); + self::assertStringContainsString('CREATE TABLE `post`', $this->flysystemHelper->getContent($backup)); self::assertInstanceOf(Response::class, $this->flysystemHelper->download($backup)); $this->flysystemHelper->remove($backup); } public function testUploadDownloadRemoveValidLocalAdapter(): void { - $backup = $this->getBackupFromLocalAdapter(); + $backup = $this->dataProvider->getBackupFromLocalAdapter(); $this->flysystemHelper->upload($backup); + self::assertStringContainsString('CREATE TABLE `post`', $this->flysystemHelper->getContent($backup)); self::assertInstanceOf(Response::class, $this->flysystemHelper->download($backup)); $this->flysystemHelper->remove($backup); } - - private function getValidS3Adapter(): S3Adapter - { - return (new S3Adapter()) - ->setName('minio') - ->setPrefix('backups') - ->setS3BucketName('somebucketname') - ->setS3AccessId('minio') - ->setS3AccessSecret($this->encryptor->encrypt('minio123')) - ->setS3Provider(S3Provider::OTHER) - ->setS3Endpoint('http://127.0.0.1:9004') - ->setS3Region('eu-east-1'); - } - - private function getInvalidS3Adapter(): S3Adapter - { - return (new S3Adapter()) - ->setName('minio') - ->setPrefix('backups') - ->setS3BucketName('somebucketname') - ->setS3AccessId('minio') - ->setS3AccessSecret($this->encryptor->encrypt('bad_access_secret')) - ->setS3Provider(S3Provider::OTHER) - ->setS3Endpoint('http://127.0.0.1:9004') - ->setS3Region('eu-east-1'); - } - - private function getLocalS3Adapter(): LocalAdapter - { - return (new LocalAdapter()) - ->setName('local') - ->setPrefix('backups'); - } - - private function getBackupFromS3Adapter(): Backup - { - $s3Adapter = $this->getValidS3Adapter(); - - $this->manager->persist($s3Adapter); - - $database = (new Database()) - ->setHost('127.0.0.1') - ->setUser('root') - ->setPassword($this->encryptor->encrypt('root')) - ->setPort(3307) - ->setName('dbsaver_test') - ->setMaxBackups(5) - ->setAdapter($s3Adapter) - ->setOwner(UserFactory::random()->object()); - - $this->setBackupTask($database); - - $this->manager->persist($database); - - return BackupFactory::new() - ->withDatabase($database) - ->create() - ->object(); - } - - private function getBackupFromLocalAdapter(): Backup - { - $localAdapter = $this->getLocalS3Adapter(); - - $this->manager->persist($localAdapter); - - $database = (new Database()) - ->setHost('127.0.0.1') - ->setUser('root') - ->setPassword($this->encryptor->encrypt('root')) - ->setPort(3307) - ->setName('dbsaver_test') - ->setMaxBackups(5) - ->setAdapter($localAdapter) - ->setOwner(UserFactory::random()->object()); - - $this->setBackupTask($database); - - $this->manager->persist($database); - - return BackupFactory::new() - ->withDatabase($database) - ->create() - ->object(); - } - - private function setBackupTask(Database $database): void - { - $backupTask = $database->getBackupTask(); - $backupTask->setPeriodicity(BackupTaskPeriodicity::WEEK) - ->setPeriodicityNumber(1) - ->setStartFrom(new \DateTime('-1 day')) - ->setNextIteration(new \DateTime('-1 day')); - } } diff --git a/tests/Service/BackupServiceTest.php b/tests/Service/BackupServiceTest.php index d5f6401..5b2de29 100644 --- a/tests/Service/BackupServiceTest.php +++ b/tests/Service/BackupServiceTest.php @@ -9,6 +9,8 @@ use App\Factory\LocalAdapterFactory; use App\Factory\UserFactory; use App\Repository\DatabaseRepository; +use App\Service\BackupService; +use App\Tests\Fixtures\DataProvider; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Test\Factories; @@ -19,12 +21,16 @@ class BackupServiceTest extends KernelTestCase private EntityManagerInterface $manager; private DatabaseRepository $databaseRepository; + private BackupService $backupService; + private DataProvider $dataProvider; protected function setUp(): void { $kernel = self::bootKernel(); $this->manager = self::getContainer()->get(EntityManagerInterface::class); $this->databaseRepository = self::getContainer()->get(DatabaseRepository::class); + $this->backupService = self::getContainer()->get(BackupService::class); + $this->dataProvider = self::getContainer()->get(DataProvider::class); } public function testGetDatabasesToBackup(): void @@ -43,4 +49,18 @@ public function testGetDatabasesToBackup(): void $this->manager->flush(); self::assertCount(4, $this->databaseRepository->getDatabasesToBackup()); } + + public function testImportBackupWithLocalAdapter(): void + { + self::expectNotToPerformAssertions(); + $backup = $this->dataProvider->getBackupFromLocalAdapter('test_db'); + $this->backupService->import($backup); + } + + public function testImportBackupWithS3Adapter(): void + { + self::expectNotToPerformAssertions(); + $backup = $this->dataProvider->getBackupFromS3Adapter('test_db'); + $this->backupService->import($backup); + } } diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml index dc990c6..b948cd5 100644 --- a/translations/messages.en.yaml +++ b/translations/messages.en.yaml @@ -1,3 +1,9 @@ +confirm_modal: + title: Do you want to continue? + body: This action is irreversible. + action: + continue: Continue + global: date_format: Y-m-d easy_admin_date_format: Y-MM-dd @@ -6,6 +12,10 @@ backup: title: Backup list action: download: Download + import: + title: Import + flash_success: Backup successfully restored! + flash_error: "An error happened during the backup restoration : '%message%'." field: database: Database context: Context diff --git a/translations/messages.fr.yaml b/translations/messages.fr.yaml index 060fcfe..07fa4c1 100644 --- a/translations/messages.fr.yaml +++ b/translations/messages.fr.yaml @@ -1,3 +1,9 @@ +confirm_modal: + title: Voulez-vous continuer ? + body: Cette action est irréversible. + action: + continue: Continuer + global: date_format: d-m-Y easy_admin_date_format: dd-MM-Y @@ -6,6 +12,10 @@ backup: title: Liste des sauvegardes action: download: Télécharger + import: + title: Importer + flash_success: La sauvegarde a bien été restaurée ! + flash_error: "Une erreur est survenue lors de la restauration de la sauvegarde : '%message%'." field: database: Base de données context: Contexte