DbImport.php 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  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 DbImport extends Command
  8. {
  9. protected $signature = 'db:import
  10. {file : Путь к .sql или .sql.gz файлу или имя файла из storage/db}
  11. {--connection=mysql : Имя соединения из config/database.php}
  12. {--force : Не запрашивать подтверждение}
  13. {--drop : Очистить БД (DROP+CREATE) перед импортом}';
  14. protected $description = 'Импорт SQL-дампа в базу данных через mysql';
  15. public function handle(): int
  16. {
  17. $mysqlBinary = $this->resolveExecutable(['mysql', 'mariadb']);
  18. if ($mysqlBinary === null) {
  19. $this->error('Не найден mysql/mariadb client. Установите mysql-client в окружение, где запускается команда.');
  20. return self::FAILURE;
  21. }
  22. $file = $this->resolveImportFile((string) $this->argument('file'));
  23. if (!is_file($file)) {
  24. $this->error("Файл не найден: {$file}");
  25. return self::FAILURE;
  26. }
  27. $connection = $this->option('connection');
  28. $config = config("database.connections.{$connection}");
  29. if (!$config || !in_array($config['driver'] ?? null, ['mysql', 'mariadb'], true)) {
  30. $this->error("Соединение {$connection} не найдено или не MySQL/MariaDB.");
  31. return self::FAILURE;
  32. }
  33. $database = $config['database'];
  34. $host = $config['host'] ?? '127.0.0.1';
  35. $port = (string) ($config['port'] ?? '3306');
  36. $user = $config['username'] ?? 'root';
  37. $password = (string) ($config['password'] ?? '');
  38. $sslOptions = $this->resolveCliSslOptions($config);
  39. if (!$this->option('force') && !$this->confirm("Импортировать дамп в БД {$database}? Текущие данные будут перезаписаны.", false)) {
  40. $this->warn('Отменено.');
  41. return self::SUCCESS;
  42. }
  43. $authHost = sprintf(
  44. '--host=%s --port=%s --user=%s %s %s',
  45. escapeshellarg($host),
  46. escapeshellarg($port),
  47. escapeshellarg($user),
  48. $password !== '' ? '--password=' . escapeshellarg($password) : '',
  49. $sslOptions,
  50. );
  51. if ($this->option('drop')) {
  52. $this->info("Пересоздаю БД {$database}...");
  53. $sql = sprintf(
  54. 'DROP DATABASE IF EXISTS `%s`; CREATE DATABASE `%s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
  55. str_replace('`', '', $database),
  56. str_replace('`', '', $database),
  57. );
  58. $dropCmd = "mysql {$authHost} -e " . escapeshellarg($sql);
  59. $drop = Process::fromShellCommandline($dropCmd);
  60. $drop->setTimeout(null);
  61. $drop->run();
  62. if (!$drop->isSuccessful()) {
  63. $this->error('Ошибка пересоздания БД: ' . $drop->getErrorOutput());
  64. return self::FAILURE;
  65. }
  66. }
  67. $isGz = str_ends_with($file, '.gz');
  68. $reader = $isGz ? 'gunzip -c ' : 'cat ';
  69. $cmd = $reader . escapeshellarg($file) . ' | ' . escapeshellarg($mysqlBinary) . " {$authHost} --default-character-set=utf8mb4 " . escapeshellarg($database);
  70. $this->info("Импортирую {$file} → {$database}");
  71. $process = new Process(['bash', '-o', 'pipefail', '-c', $cmd]);
  72. $process->setTimeout(null);
  73. $process->run(function ($type, $buffer) {
  74. if ($type === Process::ERR) {
  75. $this->getOutput()->write($buffer);
  76. }
  77. });
  78. if (!$process->isSuccessful()) {
  79. $this->error('Ошибка импорта: ' . $process->getErrorOutput());
  80. return self::FAILURE;
  81. }
  82. $this->info('Импорт завершён.');
  83. return self::SUCCESS;
  84. }
  85. private function resolveImportFile(string $file): string
  86. {
  87. if ($file === '') {
  88. return $file;
  89. }
  90. if (is_file($file)) {
  91. return $file;
  92. }
  93. return storage_path('db/' . ltrim($file, '/'));
  94. }
  95. /**
  96. * @param array<string, mixed> $config
  97. */
  98. private function resolveCliSslOptions(array $config): string
  99. {
  100. $sslCa = $config['options'][\PDO::MYSQL_ATTR_SSL_CA] ?? null;
  101. if (is_string($sslCa) && $sslCa !== '') {
  102. return '--ssl-ca=' . escapeshellarg($sslCa);
  103. }
  104. return '--skip-ssl';
  105. }
  106. /**
  107. * @param list<string> $candidates
  108. */
  109. private function resolveExecutable(array $candidates): ?string
  110. {
  111. $finder = new ExecutableFinder();
  112. foreach ($candidates as $candidate) {
  113. $binary = $finder->find($candidate);
  114. if ($binary !== null) {
  115. return $binary;
  116. }
  117. }
  118. return null;
  119. }
  120. }