feat(dump): create and delete dumps with retention config
This commit is contained in:
@@ -0,0 +1,171 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user