Исправлен битый архив бэкапа - использование временного файла
shell_exec некорректно работает с бинарными данными. Теперь mysqldump пишет во временный файл, который потом стримится клиенту. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user