Files
AeonDumpManager/src/Service/DumpService.php
T

189 lines
5.9 KiB
PHP

<?php declare(strict_types=1);
namespace AeonDumpManager\Service;
use AeonDumpManager\Service\Exception\DumpNotFoundException;
use AeonDumpManager\Struct\DumpFile;
use Doctrine\DBAL\Connection;
use League\Flysystem\FilesystemOperator;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
/**
* Orchestrates dump create/delete — the single entry point every caller
* (console commands, the async Messenger handler, the retention task) goes
* through, so maxDumps/retantionDays enforcement only lives in one place.
*/
class DumpService
{
public function __construct(
private readonly DumpLister $dumpLister,
private readonly FilesystemOperator $aeonDumpManagerFilesystemPrivate,
private readonly DumpBinaryLocator $dumpBinaryLocator,
private readonly Connection $connection,
private readonly SystemConfigService $systemConfigService,
private readonly DumpFilenameParser $filenameParser,
) {
}
public function createSync(): DumpFile
{
$dumpFile = $this->create();
$this->enforceMaxDumps();
return $dumpFile;
}
public function createAsync(string $jobId, DumpJobStatusService $jobStatus): void
{
$estimatedTotalBytes = $this->estimateTotalBytes();
$lastUpdate = 0.0;
$dumpFile = $this->create(function (int $bytesWritten) use ($jobStatus, $jobId, $estimatedTotalBytes, &$lastUpdate): void {
$now = microtime(true);
if ($now - $lastUpdate < 1.0) {
return;
}
$lastUpdate = $now;
$percent = $estimatedTotalBytes > 0
? (int) min(99, ($bytesWritten / $estimatedTotalBytes) * 100)
: 0;
$jobStatus->updateProgress($jobId, $percent);
});
$jobStatus->markDone($jobId, $dumpFile->filename);
$this->enforceMaxDumps();
}
public function delete(string $filename): void
{
if (!$this->filenameParser->isValid($filename)) {
throw DumpNotFoundException::create($filename);
}
$path = DumpLister::path($filename);
if (!$this->aeonDumpManagerFilesystemPrivate->fileExists($path)) {
throw DumpNotFoundException::create($filename);
}
$this->aeonDumpManagerFilesystemPrivate->delete($path);
}
/**
* @return resource
*/
public function readStream(string $filename)
{
if (!$this->filenameParser->isValid($filename)) {
throw DumpNotFoundException::create($filename);
}
$path = DumpLister::path($filename);
if (!$this->aeonDumpManagerFilesystemPrivate->fileExists($path)) {
throw DumpNotFoundException::create($filename);
}
return $this->aeonDumpManagerFilesystemPrivate->readStream($path);
}
public function enforceMaxDumps(): void
{
$maxDumps = $this->systemConfigService->getInt('AeonDumpManager.config.maxDumps');
if ($maxDumps <= 0) {
return;
}
$dumps = $this->dumpLister->list(); // newest first
$excess = \array_slice($dumps, $maxDumps);
foreach ($excess as $dumpFile) {
$this->aeonDumpManagerFilesystemPrivate->delete(DumpLister::path($dumpFile->filename));
}
}
public function purgeOlderThan(\DateTimeImmutable $threshold): void
{
foreach ($this->dumpLister->list() as $dumpFile) {
if ($dumpFile->datetime < $threshold) {
$this->aeonDumpManagerFilesystemPrivate->delete(DumpLister::path($dumpFile->filename));
}
}
}
/**
* @param (callable(int $bytesWritten): void)|null $onProgress
*/
private function create(?callable $onProgress = null): DumpFile
{
$dateTime = new \DateTimeImmutable();
$filename = $this->filenameParser->format($dateTime);
$tempPath = sys_get_temp_dir() . '/' . uniqid('aeon_dump_', true) . '.sql.gz';
$process = $this->buildProcess();
$gzHandle = gzopen($tempPath, 'wb6');
try {
$process->run(function (string $type, string $buffer) use ($gzHandle, $onProgress, $tempPath): void {
if ($type !== Process::OUT) {
return;
}
gzwrite($gzHandle, $buffer);
if ($onProgress !== null) {
clearstatcache(true, $tempPath);
$onProgress((int) filesize($tempPath));
}
});
} finally {
gzclose($gzHandle);
}
if (!$process->isSuccessful()) {
@unlink($tempPath);
throw new ProcessFailedException($process);
}
$sizeBytes = (int) filesize($tempPath);
$stream = fopen($tempPath, 'rb');
$this->aeonDumpManagerFilesystemPrivate->writeStream(DumpLister::path($filename), $stream);
fclose($stream);
@unlink($tempPath);
return new DumpFile($filename, $dateTime, $sizeBytes);
}
private function buildProcess(): Process
{
$binary = $this->dumpBinaryLocator->locate();
$params = $this->connection->getParams();
$command = [
$binary,
'--single-transaction',
'--quick',
'-h', (string) ($params['host'] ?? '127.0.0.1'),
'-P', (string) ($params['port'] ?? 3306),
'-u', (string) ($params['user'] ?? 'root'),
(string) ($params['dbname'] ?? ''),
];
$process = new Process($command, null, ['MYSQL_PWD' => (string) ($params['password'] ?? '')]);
$process->setTimeout(null);
return $process;
}
private function estimateTotalBytes(): int
{
$bytes = $this->connection->fetchOne(
'SELECT SUM(data_length) FROM information_schema.tables WHERE table_schema = DATABASE()'
);
return (int) ($bytes ?? 0);
}
}