DbExport.php 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Console\Commands;
  4. use Illuminate\Console\Command;
  5. use Symfony\Component\Process\ExecutableFinder;
  6. use Symfony\Component\Process\Process;
  7. class DbExport extends Command
  8. {
  9. protected $signature = 'db:export
  10. {--path= : Путь для сохранения файла (по умолчанию storage/db)}
  11. {--no-gzip : Не сжимать дамп gzip}
  12. {--connection=mysql : Имя соединения из config/database.php}';
  13. protected $description = 'Полный SQL-дамп базы данных через mysqldump';
  14. public function handle(): int
  15. {
  16. $dumpBinary = $this->resolveExecutable(['mysqldump', 'mariadb-dump']);
  17. if ($dumpBinary === null) {
  18. $this->error('Не найден mysqldump/mariadb-dump. Установите mysql-client в окружение, где запускается команда.');
  19. return self::FAILURE;
  20. }
  21. $connection = $this->option('connection');
  22. $config = config("database.connections.{$connection}");
  23. if (!$config || !in_array($config['driver'] ?? null, ['mysql', 'mariadb'], true)) {
  24. $this->error("Соединение {$connection} не найдено или не MySQL/MariaDB.");
  25. return self::FAILURE;
  26. }
  27. $database = $config['database'];
  28. $host = $config['host'] ?? '127.0.0.1';
  29. $port = (string) ($config['port'] ?? '3306');
  30. $user = $config['username'] ?? 'root';
  31. $password = (string) ($config['password'] ?? '');
  32. $sslOptions = $this->resolveCliSslOptions($config);
  33. $gzip = !$this->option('no-gzip');
  34. $timestamp = date('Y-m-d_His');
  35. $filename = "{$database}_{$timestamp}.sql" . ($gzip ? '.gz' : '');
  36. $dir = $this->resolveBackupDirectory();
  37. if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) {
  38. $this->error("Не удалось создать каталог: {$dir}");
  39. return self::FAILURE;
  40. }
  41. $filepath = rtrim($dir, '/') . '/' . $filename;
  42. $cmd = sprintf(
  43. '%s --host=%s --port=%s --user=%s %s %s --single-transaction --quick --routines --triggers --events --hex-blob --default-character-set=utf8mb4 %s',
  44. escapeshellarg($dumpBinary),
  45. escapeshellarg($host),
  46. escapeshellarg($port),
  47. escapeshellarg($user),
  48. $password !== '' ? '--password=' . escapeshellarg($password) : '',
  49. $sslOptions,
  50. escapeshellarg($database),
  51. );
  52. if ($gzip) {
  53. $cmd .= ' | gzip';
  54. }
  55. $cmd .= ' > ' . escapeshellarg($filepath);
  56. $this->info("Создаю дамп БД {$database} → {$filepath}");
  57. $process = new Process(['bash', '-o', 'pipefail', '-c', $cmd]);
  58. $process->setTimeout(null);
  59. $process->run(function ($type, $buffer) {
  60. if ($type === Process::ERR) {
  61. $this->getOutput()->write($buffer);
  62. }
  63. });
  64. if (!$process->isSuccessful()) {
  65. @unlink($filepath);
  66. $this->error('Ошибка mysqldump: ' . $process->getErrorOutput());
  67. return self::FAILURE;
  68. }
  69. $size = is_file($filepath) ? filesize($filepath) : 0;
  70. $this->info(sprintf('Готово. Размер: %.2f MB', $size / 1024 / 1024));
  71. $this->line($filepath);
  72. return self::SUCCESS;
  73. }
  74. private function resolveBackupDirectory(): string
  75. {
  76. $path = $this->option('path');
  77. if (is_string($path) && $path !== '') {
  78. return $path;
  79. }
  80. return storage_path('db');
  81. }
  82. /**
  83. * @param array<string, mixed> $config
  84. */
  85. private function resolveCliSslOptions(array $config): string
  86. {
  87. $sslCa = $config['options'][\PDO::MYSQL_ATTR_SSL_CA] ?? null;
  88. if (is_string($sslCa) && $sslCa !== '') {
  89. return '--ssl-ca=' . escapeshellarg($sslCa);
  90. }
  91. return '--skip-ssl';
  92. }
  93. /**
  94. * @param list<string> $candidates
  95. */
  96. private function resolveExecutable(array $candidates): ?string
  97. {
  98. $finder = new ExecutableFinder();
  99. foreach ($candidates as $candidate) {
  100. $binary = $finder->find($candidate);
  101. if ($binary !== null) {
  102. return $binary;
  103. }
  104. }
  105. return null;
  106. }
  107. }