diff --git a/src/Service/DumpService.php b/src/Service/DumpService.php new file mode 100644 index 0000000..3e5dc99 --- /dev/null +++ b/src/Service/DumpService.php @@ -0,0 +1,171 @@ +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); + } +}