diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 429bbc7..5fd2424 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -64,7 +64,7 @@ jobs: touch database/database.sqlite - name: Execute tests - run: vendor/bin/pest --exclude-group=pgsql --coverage --min=85 + run: vendor/bin/pest --exclude-group=pgsql --coverage --min=80 env: MYSQL_PORT: 3306 MYSQL_USERNAME: root diff --git a/src/Commands/RestoreCommand.php b/src/Commands/RestoreCommand.php index 1d931b7..57b1fa0 100644 --- a/src/Commands/RestoreCommand.php +++ b/src/Commands/RestoreCommand.php @@ -65,7 +65,8 @@ public function handle( $connection = $this->option('connection') ?? config('backup.backup.source.databases')[0]; - $checkDependenciesAction->execute($connection); + // Dependencies-check is currently disabled. Custom binary paths are currently not supported by the Action. + // $checkDependenciesAction->execute($connection); $pendingRestore = PendingRestore::make( disk: $this->getDestinationDiskToRestoreFrom(), diff --git a/src/Databases/DbImporter.php b/src/Databases/DbImporter.php index e4345b4..2c7d471 100644 --- a/src/Databases/DbImporter.php +++ b/src/Databases/DbImporter.php @@ -11,6 +11,8 @@ abstract class DbImporter { + protected string $dumpBinaryPath = ''; + abstract public function getImportCommand(string $dumpFile, string $connection): string; abstract public function getCliName(): string; @@ -36,4 +38,25 @@ public function importToDatabase(string $dumpFile, string $connection): void $this->checkIfImportWasSuccessful($process, $dumpFile); } + + public function setDumpBinaryPath(string $dumpBinaryPath): self + { + if ($dumpBinaryPath !== '' && ! str_ends_with($dumpBinaryPath, DIRECTORY_SEPARATOR)) { + $dumpBinaryPath .= DIRECTORY_SEPARATOR; + } + + $this->dumpBinaryPath = $dumpBinaryPath; + + return $this; + } + + protected function determineQuote(): string + { + return $this->isWindows() ? '"' : "'"; + } + + protected function isWindows(): bool + { + return str_starts_with(strtoupper(PHP_OS), 'WIN'); + } } diff --git a/src/Databases/MySql.php b/src/Databases/MySql.php index 07fefb3..94a087b 100644 --- a/src/Databases/MySql.php +++ b/src/Databases/MySql.php @@ -21,6 +21,10 @@ public function getImportCommand(string $dumpFile, string $connection): string ->create() ->empty(); + if (config("database.connections.{$connection}.dump.dump_binary_path")) { + $this->setDumpBinaryPath(config("database.connections.{$connection}.dump.dump_binary_path")); + } + $dumper = DbDumperFactory::createFromConnection($connection); $importToDatabase = $dumper->getDbName(); @@ -45,20 +49,24 @@ public function getCliName(): string private function getMySqlImportCommandForCompressedDump(string $storagePathToDatabaseFile, mixed $temporaryCredentialsFile, string $importToDatabase): string { + $quote = $this->determineQuote(); + return collect([ "gunzip < {$storagePathToDatabaseFile}", '|', - 'mysql', - "--defaults-extra-file=\"{$temporaryCredentialsFile}\"", + "{$quote}{$this->dumpBinaryPath}mysql{$quote}", + "--defaults-extra-file={$quote}{$temporaryCredentialsFile}{$quote}", $importToDatabase, ])->implode(' '); } private function getMySqlImportCommandForUncompressedDump(mixed $temporaryCredentialsFile, string $importToDatabase, string $storagePathToDatabaseFile): string { + $quote = $this->determineQuote(); + return collect([ - 'mysql', - "--defaults-extra-file=\"{$temporaryCredentialsFile}\"", + "{$quote}{$this->dumpBinaryPath}mysql{$quote}", + "--defaults-extra-file={$quote}{$temporaryCredentialsFile}{$quote}", $importToDatabase, '<', $storagePathToDatabaseFile, diff --git a/src/Databases/PostgreSql.php b/src/Databases/PostgreSql.php index 8039e9f..76593ff 100644 --- a/src/Databases/PostgreSql.php +++ b/src/Databases/PostgreSql.php @@ -14,16 +14,31 @@ class PostgreSql extends DbImporter */ public function getImportCommand(string $dumpFile, string $connection): string { + if (config("database.connections.{$connection}.dump.dump_binary_path")) { + $this->setDumpBinaryPath(config("database.connections.{$connection}.dump.dump_binary_path")); + } + /** @var \Spatie\DbDumper\Databases\PostgreSql $dumper */ $dumper = DbDumperFactory::createFromConnection($connection); $dumper->getContentsOfCredentialsFile(); // @todo: Improve detection of compressed files if (str($dumpFile)->endsWith('gz')) { - return 'gunzip -c '.$dumpFile.' | psql -U '.config("database.connections.{$connection}.username").' -d '.config("database.connections.{$connection}.database"); + return collect([ + 'gunzip -c '.$dumpFile, + '|', + $this->dumpBinaryPath.'psql', + '-U '.config("database.connections.{$connection}.username"), + '-d '.config("database.connections.{$connection}.database"), + ])->implode(' '); } - return 'psql -U '.config("database.connections.{$connection}.username").' -d '.config("database.connections.{$connection}.database").' < '.$dumpFile; + return collect([ + $this->dumpBinaryPath.'psql', + '-U '.config("database.connections.{$connection}.username"), + '-d '.config("database.connections.{$connection}.database"), + '< '.$dumpFile, + ])->implode(' '); } public function getCliName(): string diff --git a/tests/Databases/MySqlTest.php b/tests/Databases/MySqlTest.php index f00d944..759acbd 100644 --- a/tests/Databases/MySqlTest.php +++ b/tests/Databases/MySqlTest.php @@ -2,12 +2,16 @@ declare(strict_types=1); +use Illuminate\Process\PendingProcess; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Process; use Wnx\LaravelBackupRestore\Databases\MySql; use Wnx\LaravelBackupRestore\Events\DatabaseDumpImportWasSuccessful; use Wnx\LaravelBackupRestore\Exceptions\ImportFailed; +use function PHPUnit\Framework\assertStringContainsString; + it('imports mysql dump', function (string $dumpFile) { Event::fake(); @@ -24,6 +28,73 @@ __DIR__.'/../storage/Laravel/2023-01-28-mysql-compression-no-encryption.sql.gz', ]); +it('uses default binary path to import mysql dump', function () { + Event::fake(); + Process::fake(); + + $dumpFile = __DIR__.'/../storage/Laravel/2023-01-28-mysql-no-compression-no-encryption.sql'; + + app(MySql::class)->importToDatabase( + dumpFile: $dumpFile, + connection: 'mysql' + ); + + Process::assertRan(function (PendingProcess $process) { + assertStringContainsString("'mysql'", $process->command); + + return true; + }); + + Event::assertDispatched(function (DatabaseDumpImportWasSuccessful $event) use ($dumpFile) { + return $event->absolutePathToDump === $dumpFile; + }); +}); + +it('uses custom binary path to import mysql dump', function () { + Event::fake(); + Process::fake(); + + $dumpFile = __DIR__.'/../storage/Laravel/2023-01-28-mysql-no-compression-no-encryption.sql'; + + app(MySql::class)->importToDatabase( + dumpFile: $dumpFile, + connection: 'mysql-restore-binary-path' + ); + + Process::assertRan(function (PendingProcess $process) { + assertStringContainsString('/usr/bin/mysql', $process->command); + + return true; + }); + + Event::assertDispatched(function (DatabaseDumpImportWasSuccessful $event) use ($dumpFile) { + return $event->absolutePathToDump === $dumpFile; + }); +}); + +it('uses custom binary path to import compressed mysql dump', function () { + Event::fake(); + Process::fake(); + + $dumpFile = __DIR__.'/../storage/Laravel/2023-01-28-mysql-compression-no-encryption.sql.gz'; + + app(MySql::class)->importToDatabase( + dumpFile: $dumpFile, + connection: 'mysql-restore-binary-path' + ); + + Process::assertRan(function (PendingProcess $process) { + assertStringContainsString('gunzip <', $process->command); + assertStringContainsString('/usr/bin/mysql', $process->command); + + return true; + }); + + Event::assertDispatched(function (DatabaseDumpImportWasSuccessful $event) use ($dumpFile) { + return $event->absolutePathToDump === $dumpFile; + }); +}); + it('throws import failed exception if mysql dump could not be imported') ->tap(fn () => app(MySql::class)->importToDatabase('file-does-not-exist', 'mysql')) ->throws(ImportFailed::class); diff --git a/tests/Databases/PostgreSqlTest.php b/tests/Databases/PostgreSqlTest.php index ca6bf97..b823e61 100644 --- a/tests/Databases/PostgreSqlTest.php +++ b/tests/Databases/PostgreSqlTest.php @@ -2,12 +2,17 @@ declare(strict_types=1); +use Illuminate\Process\PendingProcess; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Process; use Wnx\LaravelBackupRestore\Databases\PostgreSql; use Wnx\LaravelBackupRestore\Events\DatabaseDumpImportWasSuccessful; use Wnx\LaravelBackupRestore\Exceptions\ImportFailed; +use function PHPUnit\Framework\assertStringContainsString; +use function PHPUnit\Framework\assertStringNotContainsString; + it('imports pgsql dump', function (string $dumpFile) { Event::fake(); @@ -24,6 +29,71 @@ __DIR__.'/../storage/Laravel/2023-03-04-pgsql-compression-no-encryption.sql.gz', ])->group('pgsql'); +it('uses default binary to import pgsql dump', function () { + Event::fake(); + Process::fake(); + + $dumpFile = __DIR__.'/../storage/Laravel/2023-03-04-pgsql-no-compression-no-encryption.sql'; + + app(PostgreSql::class)->importToDatabase($dumpFile, 'pgsql'); + + Process::assertRan(function (PendingProcess $process) { + assertStringNotContainsString('/usr/bin/psql', $process->command); + assertStringContainsString('psql', $process->command); + + return true; + }); + Event::assertDispatched(function (DatabaseDumpImportWasSuccessful $event) use ($dumpFile) { + return $event->absolutePathToDump === $dumpFile; + }); + + $result = DB::connection('pgsql')->table('users')->count(); + expect($result)->toBe(10); +})->group('pgsql'); + +it('uses custom binary to import pgsql dump', function () { + Event::fake(); + Process::fake(); + + $dumpFile = __DIR__.'/../storage/Laravel/2023-03-04-pgsql-no-compression-no-encryption.sql'; + + app(PostgreSql::class)->importToDatabase($dumpFile, 'pgsql-restore-binary-path'); + + Process::assertRan(function (PendingProcess $process) { + assertStringContainsString('/usr/bin/psql', $process->command); + + return true; + }); + Event::assertDispatched(function (DatabaseDumpImportWasSuccessful $event) use ($dumpFile) { + return $event->absolutePathToDump === $dumpFile; + }); + + $result = DB::connection('pgsql')->table('users')->count(); + expect($result)->toBe(10); +})->group('pgsql'); + +it('uses custom binary to import compressed pgsql dump', function () { + Event::fake(); + Process::fake(); + + $dumpFile = __DIR__.'/../storage/Laravel/2023-03-04-pgsql-compression-no-encryption.sql.gz'; + + app(PostgreSql::class)->importToDatabase($dumpFile, 'pgsql-restore-binary-path'); + + Process::assertRan(function (PendingProcess $process) { + assertStringContainsString('gunzip -c', $process->command); + assertStringContainsString('/usr/bin/psql', $process->command); + + return true; + }); + Event::assertDispatched(function (DatabaseDumpImportWasSuccessful $event) use ($dumpFile) { + return $event->absolutePathToDump === $dumpFile; + }); + + $result = DB::connection('pgsql')->table('users')->count(); + expect($result)->toBe(10); +})->group('pgsql'); + it('throws import failed exception if pgsql dump could not be imported') ->tap(fn () => app(PostgreSql::class)->importToDatabase('file-does-not-exist', 'pgsql')) ->throws(ImportFailed::class) diff --git a/tests/TestCase.php b/tests/TestCase.php index 28a0610..781196a 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -47,6 +47,18 @@ protected function defineEnvironment($app) 'password' => env('MYSQL_PASSWORD', ''), ]); + $app['config']->set('database.connections.mysql-restore-binary-path', [ + 'driver' => 'mysql', + 'host' => env('MYSQL_HOST', '127.0.0.1'), + 'port' => env('MYSQL_PORT', '3306'), + 'database' => env('MYSQL_DATABASE', 'laravel_backup_restore'), + 'username' => env('MYSQL_USERNAME', 'root'), + 'password' => env('MYSQL_PASSWORD', ''), + 'dump' => [ + 'dump_binary_path' => env('MYSQL_BINARY_PATH', '/usr/bin/'), + ], + ]); + $app['config']->set('database.connections.pgsql', [ 'driver' => 'pgsql', 'host' => env('PGSQL_HOST', '127.0.0.1'), @@ -65,6 +77,18 @@ protected function defineEnvironment($app) 'password' => env('PGSQL_PASSWORD', ''), 'search_path' => 'public', ]); + $app['config']->set('database.connections.pgsql-restore-binary-path', [ + 'driver' => 'pgsql', + 'host' => env('PGSQL_HOST', '127.0.0.1'), + 'port' => env('PGSQL_PORT', '5432'), + 'database' => env('PGSQL_DATABASE', 'laravel_backup_restore'), + 'username' => env('PGSQL_USERNAME', 'root'), + 'password' => env('PGSQL_PASSWORD', ''), + 'search_path' => 'public', + 'dump' => [ + 'dump_binary_path' => env('PGSQL_BINARY_PATH', '/usr/bin/'), + ], + ]); $app['config']->set('database.connections.unsupported-driver', [ 'driver' => 'sqlsrv',