Исправлен битый архив бэкапа - использование временного файла

shell_exec некорректно работает с бинарными данными.
Теперь mysqldump пишет во временный файл, который потом стримится клиенту.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-23 22:24:11 +03:00
parent cf275bba9c
commit 978f6b3cd8
2 changed files with 46 additions and 20 deletions

View File

@@ -363,14 +363,22 @@ $app->get('/api/backup/all', function (Request $request, Response $response) use
try {
$pdo = $container->get('db');
$backup = new \App\BackupService($_SESSION['db_user'], $_SESSION['db_pass']);
$data = $backup->dumpAllDatabases($pdo);
$tempFile = $backup->dumpAllDatabases($pdo);
$filename = 'backup_all_' . date('Y-m-d_H-i-s') . '.sql.gz';
$response->getBody()->write($data);
$stream = fopen($tempFile, 'rb');
$filesize = filesize($tempFile);
// Удаляем временный файл после отправки
register_shutdown_function(function() use ($tempFile) {
if (file_exists($tempFile)) unlink($tempFile);
});
return $response
->withHeader('Content-Type', 'application/gzip')
->withHeader('Content-Disposition', 'attachment; filename="' . $filename . '"')
->withHeader('Content-Length', strlen($data));
->withHeader('Content-Length', $filesize)
->withBody(new \Slim\Psr7\Stream($stream));
} catch (\Exception $e) {
$response->getBody()->write(json_encode(['error' => $e->getMessage()]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(500);
@@ -382,14 +390,21 @@ $app->get('/api/backup/database/{name}', function (Request $request, Response $r
try {
$database = $args['name'];
$backup = new \App\BackupService($_SESSION['db_user'], $_SESSION['db_pass']);
$data = $backup->dumpDatabase($database);
$tempFile = $backup->dumpDatabase($database);
$filename = $database . '_' . date('Y-m-d_H-i-s') . '.sql.gz';
$response->getBody()->write($data);
$stream = fopen($tempFile, 'rb');
$filesize = filesize($tempFile);
register_shutdown_function(function() use ($tempFile) {
if (file_exists($tempFile)) unlink($tempFile);
});
return $response
->withHeader('Content-Type', 'application/gzip')
->withHeader('Content-Disposition', 'attachment; filename="' . $filename . '"')
->withHeader('Content-Length', strlen($data));
->withHeader('Content-Length', $filesize)
->withBody(new \Slim\Psr7\Stream($stream));
} catch (\Exception $e) {
$response->getBody()->write(json_encode(['error' => $e->getMessage()]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(500);

View File

@@ -23,30 +23,36 @@ class BackupService
}
/**
* Создать дамп и вернуть содержимое (gzip сжатое)
* Создать дамп и вернуть путь к временному файлу
*/
public function dumpDatabase(string $database): string
{
$tempFile = sys_get_temp_dir() . '/backup_' . $database . '_' . uniqid() . '.sql.gz';
$cmd = sprintf(
'mysqldump --host=%s --port=%s --user=%s --password=%s --single-transaction --routines --triggers %s 2>/dev/null | gzip',
'mysqldump --host=%s --port=%s --user=%s --password=%s --single-transaction --routines --triggers %s 2>&1 | gzip > %s',
escapeshellarg($this->host),
escapeshellarg($this->port),
escapeshellarg($this->user),
escapeshellarg($this->pass),
escapeshellarg($database)
escapeshellarg($database),
escapeshellarg($tempFile)
);
$output = shell_exec($cmd);
exec($cmd, $output, $returnCode);
if ($output === null || $output === '') {
throw new \RuntimeException("Failed to create dump for database: $database");
if ($returnCode !== 0 || !file_exists($tempFile) || filesize($tempFile) < 100) {
if (file_exists($tempFile)) {
unlink($tempFile);
}
throw new \RuntimeException("mysqldump failed: " . implode("\n", $output));
}
return $output;
return $tempFile;
}
/**
* Создать дамп всех баз данных (один файл)
* Создать дамп всех баз данных
*/
public function dumpAllDatabases(\PDO $pdo): string
{
@@ -56,23 +62,28 @@ class BackupService
throw new \RuntimeException("No databases available for backup");
}
$tempFile = sys_get_temp_dir() . '/backup_all_' . uniqid() . '.sql.gz';
$dbList = implode(' ', array_map('escapeshellarg', $databases));
$cmd = sprintf(
'mysqldump --host=%s --port=%s --user=%s --password=%s --single-transaction --routines --triggers --databases %s 2>/dev/null | gzip',
'mysqldump --host=%s --port=%s --user=%s --password=%s --single-transaction --routines --triggers --databases %s 2>&1 | gzip > %s',
escapeshellarg($this->host),
escapeshellarg($this->port),
escapeshellarg($this->user),
escapeshellarg($this->pass),
$dbList
$dbList,
escapeshellarg($tempFile)
);
$output = shell_exec($cmd);
exec($cmd, $output, $returnCode);
if ($output === null || $output === '') {
throw new \RuntimeException("Failed to create dump");
if ($returnCode !== 0 || !file_exists($tempFile) || filesize($tempFile) < 100) {
if (file_exists($tempFile)) {
unlink($tempFile);
}
throw new \RuntimeException("mysqldump failed: " . implode("\n", $output));
}
return $output;
return $tempFile;
}
}