From 01c653cab00848128a4fb2aa7d58a74451ee9d2c Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 17 Aug 2017 17:55:37 +0300 Subject: [PATCH] Fixes #44: Compress sitemap with a single gzip member per file --- Sitemap.php | 72 ++++++++++++++++++++++++++++++++++++++++--- tests/SitemapTest.php | 10 ++++++ 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/Sitemap.php b/Sitemap.php index 27d592d..7269de9 100644 --- a/Sitemap.php +++ b/Sitemap.php @@ -76,6 +76,16 @@ class Sitemap */ private $writer; + /** + * @var resource for writable incremental deflate context + */ + private $deflateContext; + + /** + * @var resource for php://temp stream + */ + private $tempFile; + /** * @param string $filePath path of the file to write to * @throws \InvalidArgumentException @@ -136,7 +146,7 @@ private function finishFile() if ($this->writer !== null) { $this->writer->endElement(); $this->writer->endDocument(); - $this->flush(); + $this->flush(true); } } @@ -150,14 +160,66 @@ public function write() /** * Flushes buffer into file + * @param bool $finishFile Pass true to close the file to write to, used only when useGzip is true */ - private function flush() + private function flush($finishFile = false) { - $filePath = $this->getCurrentFilePath(); if ($this->useGzip) { - $filePath = 'compress.zlib://' . $filePath; + $this->flushGzip($finishFile); + return; + } + file_put_contents($this->getCurrentFilePath(), $this->writer->flush(true), FILE_APPEND); + } + + /** + * Decides how to flush buffer into compressed file + * @param bool $finishFile Pass true to close the file to write to + */ + private function flushGzip($finishFile = false) { + if (function_exists('deflate_init') && function_exists('deflate_add')) { + $this->flushWithIncrementalDeflate($finishFile); + return; + } + $this->flushWithTempFileFallback($finishFile); + } + + /** + * Flushes buffer into file with incremental deflating data, available in php 7.0+ + * @param bool $finishFile Pass true to write last chunk with closing headers + */ + private function flushWithIncrementalDeflate($finishFile = false) { + $flushMode = $finishFile ? ZLIB_FINISH : ZLIB_NO_FLUSH; + + if (empty($this->deflateContext)) { + $this->deflateContext = deflate_init(ZLIB_ENCODING_GZIP); + } + + $compressedChunk = deflate_add($this->deflateContext, $this->writer->flush(true), $flushMode); + file_put_contents($this->getCurrentFilePath(), $compressedChunk, FILE_APPEND); + + if ($finishFile) { + $this->deflateContext = null; + } + } + + /** + * Flushes buffer into temporary stream and compresses stream into a file on finish + * @param bool $finishFile Pass true to compress temporary stream into desired file + */ + private function flushWithTempFileFallback($finishFile = false) { + if (empty($this->tempFile) || !is_resource($this->tempFile)) { + $this->tempFile = fopen('php://temp/', 'w'); + } + + fwrite($this->tempFile, $this->writer->flush(true)); + + if ($finishFile) { + $file = fopen('compress.zlib://' . $this->getCurrentFilePath(), 'w'); + rewind($this->tempFile); + stream_copy_to_stream($this->tempFile, $file); + fclose($file); + fclose($this->tempFile); } - file_put_contents($filePath, $this->writer->flush(true), FILE_APPEND); } /** diff --git a/tests/SitemapTest.php b/tests/SitemapTest.php index 18e7f83..38d5bb5 100644 --- a/tests/SitemapTest.php +++ b/tests/SitemapTest.php @@ -16,6 +16,14 @@ protected function assertIsValidSitemap($fileName) $this->assertTrue($xml->schemaValidate(__DIR__ . '/sitemap.xsd')); } + protected function assertIsOneMemberGzipFile($fileName) + { + $gzipMemberStartSequence = pack('H*', '1f8b08'); + $content = file_get_contents($fileName); + $isOneMemberGzipFile = (strpos($content, $gzipMemberStartSequence, 1) === false); + $this->assertTrue($isOneMemberGzipFile, "There are more than one gzip member in $fileName"); + } + public function testWritingFile() { $fileName = __DIR__ . '/sitemap_regular.xml'; @@ -129,6 +137,7 @@ public function testWritingFileGzipped() $finfo = new \finfo(FILEINFO_MIME_TYPE); $this->assertEquals('application/x-gzip', $finfo->file($fileName)); $this->assertIsValidSitemap('compress.zlib://' . $fileName); + $this->assertIsOneMemberGzipFile($fileName); unlink($fileName); } @@ -161,6 +170,7 @@ public function testMultipleFilesGzipped() $this->assertTrue(file_exists($expectedFile), "$expectedFile does not exist!"); $this->assertEquals('application/x-gzip', $finfo->file($expectedFile)); $this->assertIsValidSitemap('compress.zlib://' . $expectedFile); + $this->assertIsOneMemberGzipFile($expectedFile); unlink($expectedFile); }