|
|
@@ -1,15 +1,17 @@
|
|
|
<?php
|
|
|
|
|
|
+declare(strict_types=1);
|
|
|
+
|
|
|
namespace App\Console\Commands;
|
|
|
|
|
|
use Illuminate\Console\Command;
|
|
|
-use Illuminate\Support\Facades\Storage;
|
|
|
+use Symfony\Component\Process\ExecutableFinder;
|
|
|
use Symfony\Component\Process\Process;
|
|
|
|
|
|
class DbExport extends Command
|
|
|
{
|
|
|
protected $signature = 'db:export
|
|
|
- {--path= : Путь для сохранения файла (по умолчанию storage/app/db-backups)}
|
|
|
+ {--path= : Путь для сохранения файла (по умолчанию storage/db)}
|
|
|
{--no-gzip : Не сжимать дамп gzip}
|
|
|
{--connection=mysql : Имя соединения из config/database.php}';
|
|
|
|
|
|
@@ -17,6 +19,13 @@ class DbExport extends Command
|
|
|
|
|
|
public function handle(): int
|
|
|
{
|
|
|
+ $dumpBinary = $this->resolveExecutable(['mysqldump', 'mariadb-dump']);
|
|
|
+
|
|
|
+ if ($dumpBinary === null) {
|
|
|
+ $this->error('Не найден mysqldump/mariadb-dump. Установите mysql-client в окружение, где запускается команда.');
|
|
|
+ return self::FAILURE;
|
|
|
+ }
|
|
|
+
|
|
|
$connection = $this->option('connection');
|
|
|
$config = config("database.connections.{$connection}");
|
|
|
|
|
|
@@ -30,12 +39,13 @@ class DbExport extends Command
|
|
|
$port = (string) ($config['port'] ?? '3306');
|
|
|
$user = $config['username'] ?? 'root';
|
|
|
$password = (string) ($config['password'] ?? '');
|
|
|
+ $sslOptions = $this->resolveCliSslOptions($config);
|
|
|
|
|
|
$gzip = !$this->option('no-gzip');
|
|
|
$timestamp = date('Y-m-d_His');
|
|
|
$filename = "{$database}_{$timestamp}.sql" . ($gzip ? '.gz' : '');
|
|
|
|
|
|
- $dir = $this->option('path') ?: storage_path('app/db-backups');
|
|
|
+ $dir = $this->resolveBackupDirectory();
|
|
|
if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) {
|
|
|
$this->error("Не удалось создать каталог: {$dir}");
|
|
|
return self::FAILURE;
|
|
|
@@ -44,11 +54,13 @@ class DbExport extends Command
|
|
|
$filepath = rtrim($dir, '/') . '/' . $filename;
|
|
|
|
|
|
$cmd = sprintf(
|
|
|
- 'mysqldump --host=%s --port=%s --user=%s %s --single-transaction --quick --routines --triggers --events --hex-blob --default-character-set=utf8mb4 %s',
|
|
|
+ '%s --host=%s --port=%s --user=%s %s %s --single-transaction --quick --routines --triggers --events --hex-blob --default-character-set=utf8mb4 %s',
|
|
|
+ escapeshellarg($dumpBinary),
|
|
|
escapeshellarg($host),
|
|
|
escapeshellarg($port),
|
|
|
escapeshellarg($user),
|
|
|
$password !== '' ? '--password=' . escapeshellarg($password) : '',
|
|
|
+ $sslOptions,
|
|
|
escapeshellarg($database),
|
|
|
);
|
|
|
|
|
|
@@ -59,7 +71,7 @@ class DbExport extends Command
|
|
|
|
|
|
$this->info("Создаю дамп БД {$database} → {$filepath}");
|
|
|
|
|
|
- $process = Process::fromShellCommandline($cmd);
|
|
|
+ $process = new Process(['bash', '-o', 'pipefail', '-c', $cmd]);
|
|
|
$process->setTimeout(null);
|
|
|
$process->run(function ($type, $buffer) {
|
|
|
if ($type === Process::ERR) {
|
|
|
@@ -79,4 +91,47 @@ class DbExport extends Command
|
|
|
|
|
|
return self::SUCCESS;
|
|
|
}
|
|
|
+
|
|
|
+ private function resolveBackupDirectory(): string
|
|
|
+ {
|
|
|
+ $path = $this->option('path');
|
|
|
+
|
|
|
+ if (is_string($path) && $path !== '') {
|
|
|
+ return $path;
|
|
|
+ }
|
|
|
+
|
|
|
+ return storage_path('db');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param array<string, mixed> $config
|
|
|
+ */
|
|
|
+ private function resolveCliSslOptions(array $config): string
|
|
|
+ {
|
|
|
+ $sslCa = $config['options'][\PDO::MYSQL_ATTR_SSL_CA] ?? null;
|
|
|
+
|
|
|
+ if (is_string($sslCa) && $sslCa !== '') {
|
|
|
+ return '--ssl-ca=' . escapeshellarg($sslCa);
|
|
|
+ }
|
|
|
+
|
|
|
+ return '--skip-ssl';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param list<string> $candidates
|
|
|
+ */
|
|
|
+ private function resolveExecutable(array $candidates): ?string
|
|
|
+ {
|
|
|
+ $finder = new ExecutableFinder();
|
|
|
+
|
|
|
+ foreach ($candidates as $candidate) {
|
|
|
+ $binary = $finder->find($candidate);
|
|
|
+
|
|
|
+ if ($binary !== null) {
|
|
|
+ return $binary;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
}
|