fix(database): decompress gzip backups for mysql and mariadb restores

Single-database mysql/mariadb imports accepted .gz files but redirected
them straight into the client. Pipe through the existing gunzip fallback
used by dump-all restores.
This commit is contained in:
Andras Bacsai
2026-09-09 13:10:12 +02:00
parent edcd44db89
commit 7002ee95bc
3 changed files with 21 additions and 6 deletions
@@ -17,10 +17,10 @@ class DatabaseImportCommandBuilder
: 'pg_restore --exit-on-error'.($replaceExisting ? ' --clean --if-exists' : '').' -U $POSTGRES_USER -d ${POSTGRES_DB:-${POSTGRES_USER:-postgres}} '.$path,
'mysql' => $dumpAll
? $this->mysqlDumpAll('mysql', 'MYSQL', $path)
: 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE < '.$path,
: '(gunzip -cf '.$path.' 2>/dev/null || cat '.$path.') | mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE',
'mariadb' => $dumpAll
? $this->mysqlDumpAll('mariadb', 'MARIADB', $path)
: 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE < '.$path,
: '(gunzip -cf '.$path.' 2>/dev/null || cat '.$path.') | mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE',
'mongodb' => 'mongorestore --authenticationDatabase=admin --username $MONGO_INITDB_ROOT_USERNAME --password $MONGO_INITDB_ROOT_PASSWORD --uri mongodb://localhost:27017 --gzip --archive='.$path,
default => throw new InvalidArgumentException('Database import is not supported for this database type.'),
};
@@ -36,6 +36,21 @@ test('builds database-specific restore commands', function (string $class, ?stri
'service mongo' => [ServiceDatabase::class, 'mongodb', 'mongorestore'],
]);
test('decompresses gzip backups for single-database mysql and mariadb restores', function (string $class, ?string $type, string $client) {
$builder = new DatabaseImportCommandBuilder;
$command = $builder->buildRestoreCommand(importResource($class, $type), '/tmp/restore file.sql.gz', false);
expect($command)->toBe(
"(gunzip -cf '/tmp/restore file.sql.gz' 2>/dev/null || cat '/tmp/restore file.sql.gz') | {$client}"
);
})->with([
'mysql' => [StandaloneMysql::class, null, 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE'],
'mariadb' => [StandaloneMariadb::class, null, 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE'],
'service mysql' => [ServiceDatabase::class, 'mysql', 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE'],
'service mariadb' => [ServiceDatabase::class, 'mariadb', 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE'],
]);
test('builds dump-all commands and postgres safety scan', function () {
$builder = new DatabaseImportCommandBuilder;
$postgres = importResource(StandalonePostgresql::class);
@@ -132,23 +132,23 @@ test('dump-all PostgreSQL restore selects the client for the dump format', funct
test('buildRestoreCommand handles MySQL without dumpAll', function () {
$component = importFormWithResource('App\Models\StandaloneMysql');
$component->dumpAll = false;
$component->mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE';
$result = $component->buildRestoreCommand('/tmp/test.dump');
expect($result)->toContain('mysql -u $MYSQL_USER');
expect($result)->toContain("< '/tmp/test.dump'");
expect($result)->toContain("(gunzip -cf '/tmp/test.dump' 2>/dev/null || cat '/tmp/test.dump') | mysql");
expect($result)->not->toContain("< '/tmp/test.dump'");
});
test('buildRestoreCommand handles MariaDB without dumpAll', function () {
$component = importFormWithResource('App\Models\StandaloneMariadb');
$component->dumpAll = false;
$component->mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE';
$result = $component->buildRestoreCommand('/tmp/test.dump');
expect($result)->toContain('mariadb -u $MARIADB_USER');
expect($result)->toContain("< '/tmp/test.dump'");
expect($result)->toContain("(gunzip -cf '/tmp/test.dump' 2>/dev/null || cat '/tmp/test.dump') | mariadb");
expect($result)->not->toContain("< '/tmp/test.dump'");
});
test('buildRestoreCommand always appends the MongoDB archive path', function (bool $dumpAll) {