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); } }