Added an command to backup the data of Part-DB easily

This fixes issue #86.
This commit is contained in:
Jan Böhmer 2023-01-29 00:47:03 +01:00
parent 5bf68632c3
commit 2c03a6e683
4 changed files with 352 additions and 2 deletions

View file

@ -13,10 +13,10 @@
"brick/math": "^0.8.15",
"composer/package-versions-deprecated": "1.11.99.4",
"doctrine/annotations": "^1.6",
"doctrine/dbal": "^3.4.6",
"doctrine/doctrine-bundle": "^2.0",
"doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.9",
"doctrine/dbal": "^3.4.6",
"dompdf/dompdf": "^2.0.0",
"erusev/parsedown": "^1.7",
"florianv/swap": "^4.0",
@ -25,6 +25,7 @@
"jbtronics/2fa-webauthn": "^1.0.0",
"league/html-to-markdown": "^5.0.1",
"liip/imagine-bundle": "^2.2",
"nelexa/zip": "^4.0",
"nelmio/security-bundle": "^3.0",
"nyholm/psr7": "^1.1",
"ocramius/proxy-manager": "2.2.*",
@ -38,6 +39,7 @@
"scheb/2fa-trusted-device": "^5.13",
"sensio/framework-extra-bundle": "^6.1.1",
"shivas/versioning-bundle": "^4.0",
"spatie/db-dumper": "^2.21",
"symfony/apache-pack": "^1.0",
"symfony/asset": "5.4.*",
"symfony/console": "5.4.*",

135
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c6dc9c0385baeff44cb2bf8ee4c1e656",
"content-hash": "433a25b4df056e2a121ed5cbf442b172",
"packages": [
{
"name": "beberlei/assert",
@ -3270,6 +3270,79 @@
],
"time": "2022-07-24T11:55:47+00:00"
},
{
"name": "nelexa/zip",
"version": "4.0.2",
"source": {
"type": "git",
"url": "https://github.com/Ne-Lexa/php-zip.git",
"reference": "88a1b6549be813278ff2dd3b6b2ac188827634a7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Ne-Lexa/php-zip/zipball/88a1b6549be813278ff2dd3b6b2ac188827634a7",
"reference": "88a1b6549be813278ff2dd3b6b2ac188827634a7",
"shasum": ""
},
"require": {
"ext-zlib": "*",
"php": "^7.4 || ^8.0",
"psr/http-message": "*",
"symfony/finder": "*"
},
"require-dev": {
"ext-bz2": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-iconv": "*",
"ext-openssl": "*",
"ext-xml": "*",
"friendsofphp/php-cs-fixer": "^3.4.0",
"guzzlehttp/psr7": "^1.6",
"phpunit/phpunit": "^9",
"symfony/http-foundation": "*",
"symfony/var-dumper": "*",
"vimeo/psalm": "^4.6"
},
"suggest": {
"ext-bz2": "Needed to support BZIP2 compression",
"ext-fileinfo": "Needed to get mime-type file",
"ext-iconv": "Needed to support convert zip entry name to requested character encoding",
"ext-openssl": "Needed to support encrypt zip entries or use ext-mcrypt"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpZip\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ne-Lexa",
"email": "alexey@nelexa.ru",
"role": "Developer"
}
],
"description": "PhpZip is a php-library for extended work with ZIP-archives. Open, create, update, delete, extract and get info tool. Supports appending to existing ZIP files, WinZip AES encryption, Traditional PKWARE Encryption, BZIP2 compression, external file attributes and ZIP64 extensions. Alternative ZipArchive. It does not require php-zip extension.",
"homepage": "https://github.com/Ne-Lexa/php-zip",
"keywords": [
"archive",
"extract",
"unzip",
"winzip",
"zip",
"ziparchive"
],
"support": {
"issues": "https://github.com/Ne-Lexa/php-zip/issues",
"source": "https://github.com/Ne-Lexa/php-zip/tree/4.0.2"
},
"time": "2022-06-17T11:17:46+00:00"
},
{
"name": "nelmio/security-bundle",
"version": "v3.0.0",
@ -5699,6 +5772,66 @@
},
"time": "2022-07-10T17:39:01+00:00"
},
{
"name": "spatie/db-dumper",
"version": "2.21.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/db-dumper.git",
"reference": "05e5955fb882008a8947c5a45146d86cfafa10d1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/db-dumper/zipball/05e5955fb882008a8947c5a45146d86cfafa10d1",
"reference": "05e5955fb882008a8947c5a45146d86cfafa10d1",
"shasum": ""
},
"require": {
"php": "^7.2|^8.0",
"symfony/process": "^4.2|^5.0"
},
"require-dev": {
"phpunit/phpunit": "^7.0|^8.0|^9.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Spatie\\DbDumper\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"description": "Dump databases",
"homepage": "https://github.com/spatie/db-dumper",
"keywords": [
"database",
"db-dumper",
"dump",
"mysqldump",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/db-dumper/issues",
"source": "https://github.com/spatie/db-dumper/tree/2.21.1"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2021-02-24T14:56:42+00:00"
},
{
"name": "spomky-labs/base64url",
"version": "v2.0.4",

View file

@ -233,3 +233,7 @@ services:
# We are needing this service inside of a migration, where only the container is injected. So we need to define it as public, to access it from the container.
App\Services\UserSystem\PermissionPresetsHelper:
public: true
App\Command\BackupCommand:
arguments:
$project_dir: '%kernel.project_dir%'

View file

@ -0,0 +1,211 @@
<?php
namespace App\Command;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\ORM\EntityManagerInterface;
use PhpZip\Constants\ZipCompressionMethod;
use PhpZip\Exception\ZipException;
use PhpZip\ZipFile;
use Spatie\DbDumper\Databases\MySql;
use Spatie\DbDumper\DbDumper;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\Input;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class BackupCommand extends Command
{
protected static $defaultName = 'partdb:backup';
protected static $defaultDescription = 'Backup the files and the database of Part-DB';
private string $project_dir;
private EntityManagerInterface $entityManager;
public function __construct(string $project_dir, EntityManagerInterface $entityManager)
{
$this->project_dir = $project_dir;
$this->entityManager = $entityManager;
parent::__construct();
}
protected function configure(): void
{
$this->setHelp('This command allows you to backup the files and database of Part-DB.');
$this
->addArgument('output', InputArgument::REQUIRED, 'The file to which the backup should be written')
->addOption('overwrite', null, InputOption::VALUE_NONE, 'Overwrite the output file, if it already exists without asking')
->addOption('database', null, InputOption::VALUE_NONE, 'Backup the database')
->addOption('attachments', null, InputOption::VALUE_NONE, 'Backup the attachments files')
->addOption('config', null, InputOption::VALUE_NONE, 'Backup the config files')
->addOption('full', null, InputOption::VALUE_NONE, 'Backup database, attachments and config files')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$output_filepath = $input->getArgument('output');
$backup_database = $input->getOption('database');
$backup_attachments = $input->getOption('attachments');
$backup_config = $input->getOption('config');
$backup_full = $input->getOption('full');
if ($backup_full) {
$backup_database = true;
$backup_attachments = true;
$backup_config = true;
}
//When all options are false, we abort and show an error message
if (! $backup_database && ! $backup_attachments && ! $backup_config) {
$io->error('You have to select at least one option what to backup! Use --full to backup everything.');
return Command::FAILURE;
}
$io->info('Backup Part-DB to '.$output_filepath);
//Check if the file already exists
if (file_exists($output_filepath)) {
//Then ask the user, if he wants to overwrite the file
if (!$io->confirm('The file '.realpath($output_filepath).' already exists. Do you want to overwrite it?', false)) {
$io->error('Backup aborted!');
return Command::FAILURE;
}
}
$io->note('Starting backup...');
//Open ZIP file
$zip = new ZipFile();
if ($backup_config) {
$this->backupConfig($zip, $io);
}
if ($backup_attachments) {
$this->backupAttachments($zip, $io);
}
if ($backup_database) {
$this->backupDatabase($zip, $io);
}
$zip->setArchiveComment('Part-DB Backup of '.date('Y-m-d H:i:s'));
//Write and close ZIP file
try {
$zip->saveAsFile($output_filepath);
} catch (ZipException $e) {
$io->error('Could not write ZIP file: '.$e->getMessage());
return Command::FAILURE;
}
$zip->close();
$io->success('Backup finished! You can find the backup file at '.$output_filepath);
return Command::SUCCESS;
}
/**
* Constructs the MySQL PDO DSN.
* Taken from https://github.com/doctrine/dbal/blob/3.5.x/src/Driver/PDO/MySQL/Driver.php
*
* @param mixed[] $params
*/
private function configureDumper(array $params, DbDumper $dumper): void
{
if (isset($params['host']) && $params['host'] !== '') {
$dumper->setHost($params['host']);
}
if (isset($params['port'])) {
$dumper->setPort($params['port']);
}
if (isset($params['dbname'])) {
$dumper->setDbName($params['dbname']);
}
if (isset($params['unix_socket'])) {
$dumper->setSocket($params['unix_socket']);
}
if (isset($params['user'])) {
$dumper->setUserName($params['user']);
}
if (isset($params['password'])) {
$dumper->setPassword($params['password']);
}
}
protected function backupDatabase(ZipFile $zip, SymfonyStyle $io): void
{
$io->note('Backup database...');
//Determine if we use MySQL or SQLite
$connection = $this->entityManager->getConnection();
if ($connection->getDatabasePlatform() instanceof AbstractMySQLPlatform) {
try {
$io->note('MySQL database detected. Dump DB to SQL using mysqldump...');
$params = $connection->getParams();
$dumper = MySql::create();
$this->configureDumper($params, $dumper);
$tmp_file = tempnam(sys_get_temp_dir(), 'partdb_sql_dump');
$dumper->dumpToFile($tmp_file);
$zip->addFile($tmp_file, 'mysql_dump.sql');
} catch (\Exception $e) {
$io->error('Could not dump database: '.$e->getMessage());
$io->error('This can maybe be fixed by installing the mysqldump binary and adding it to the PATH variable!');
}
} elseif ($connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\SqlitePlatform) {
$io->note('SQLite database detected. Copy DB file to ZIP...');
$params = $connection->getParams();
$zip->addFile($params['path'], 'var/app.db');
} else {
$io->error('Unknown database platform. Could not backup database!');
}
}
protected function backupConfig(ZipFile $zip, SymfonyStyle $io): void
{
$io->note('Backing up config files...');
//Add .env.local file if it exists
$env_local_filepath = $this->project_dir.'/.env.local';
if (file_exists($env_local_filepath)) {
$zip->addFile($env_local_filepath, '.env.local');
} else {
$io->warning('Could not find .env.local file. You maybe use env variables, then you have to backup them manually!!');
}
//Add config/parameters.yaml and config/banner.md files
$config_dir = $this->project_dir.'/config';
$zip->addFile($config_dir.'/parameters.yaml', 'config/parameters.yaml');
$zip->addFile($config_dir.'/banner.md', 'config/banner.md');
}
protected function backupAttachments(ZipFile $zip, SymfonyStyle $io): void
{
$io->note('Backing up attachments files...');
//Add public attachments directory
$attachments_dir = $this->project_dir.'/public/media/';
$zip->addDirRecursive($attachments_dir, 'public/media/', ZipCompressionMethod::DEFLATED);
//Add private attachments directory
$attachments_dir = $this->project_dir.'/uploads/';
$zip->addDirRecursive($attachments_dir, 'uploads/', ZipCompressionMethod::DEFLATED);
}
}