1 <?php 2 3 declare(strict_types=1); 4 5 namespace OpenSpout\Writer\Common\Helper; 6 7 use RecursiveDirectoryIterator; 8 use RecursiveIteratorIterator; 9 use SplFileInfo; 10 use ZipArchive; 11 12 /** 13 * @internal 14 */ 15 final class ZipHelper 16 { 17 public const ZIP_EXTENSION = '.zip'; 18 19 /** 20 * Controls what to do when trying to add an existing file. 21 */ 22 public const EXISTING_FILES_SKIP = 'skip'; 23 public const EXISTING_FILES_OVERWRITE = 'overwrite'; 24 25 /** 26 * Returns a new ZipArchive instance pointing at the given path. 27 * 28 * @param string $tmpFolderPath Path of the temp folder where the zip file will be created 29 */ 30 public function createZip(string $tmpFolderPath): ZipArchive 31 { 32 $zip = new ZipArchive(); 33 $zipFilePath = $tmpFolderPath.self::ZIP_EXTENSION; 34 35 $zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE); 36 37 return $zip; 38 } 39 40 /** 41 * @param ZipArchive $zip An opened zip archive object 42 * 43 * @return string Path where the zip file of the given folder will be created 44 */ 45 public function getZipFilePath(ZipArchive $zip): string 46 { 47 return $zip->filename; 48 } 49 50 /** 51 * Adds the given file, located under the given root folder to the archive. 52 * The file will be compressed. 53 * 54 * Example of use: 55 * addFileToArchive($zip, '/tmp/xlsx/foo', 'bar/baz.xml'); 56 * => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml' 57 * 58 * @param ZipArchive $zip An opened zip archive object 59 * @param string $rootFolderPath path of the root folder that will be ignored in the archive tree 60 * @param string $localFilePath Path of the file to be added, under the root folder 61 * @param string $existingFileMode Controls what to do when trying to add an existing file 62 */ 63 public function addFileToArchive(ZipArchive $zip, string $rootFolderPath, string $localFilePath, string $existingFileMode = self::EXISTING_FILES_OVERWRITE): void 64 { 65 $this->addFileToArchiveWithCompressionMethod( 66 $zip, 67 $rootFolderPath, 68 $localFilePath, 69 $existingFileMode, 70 ZipArchive::CM_DEFAULT 71 ); 72 } 73 74 /** 75 * Adds the given file, located under the given root folder to the archive. 76 * The file will NOT be compressed. 77 * 78 * Example of use: 79 * addUncompressedFileToArchive($zip, '/tmp/xlsx/foo', 'bar/baz.xml'); 80 * => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml' 81 * 82 * @param ZipArchive $zip An opened zip archive object 83 * @param string $rootFolderPath path of the root folder that will be ignored in the archive tree 84 * @param string $localFilePath Path of the file to be added, under the root folder 85 * @param string $existingFileMode Controls what to do when trying to add an existing file 86 */ 87 public function addUncompressedFileToArchive(ZipArchive $zip, string $rootFolderPath, string $localFilePath, string $existingFileMode = self::EXISTING_FILES_OVERWRITE): void 88 { 89 $this->addFileToArchiveWithCompressionMethod( 90 $zip, 91 $rootFolderPath, 92 $localFilePath, 93 $existingFileMode, 94 ZipArchive::CM_STORE 95 ); 96 } 97 98 /** 99 * @param ZipArchive $zip An opened zip archive object 100 * @param string $folderPath Path to the folder to be zipped 101 * @param string $existingFileMode Controls what to do when trying to add an existing file 102 */ 103 public function addFolderToArchive(ZipArchive $zip, string $folderPath, string $existingFileMode = self::EXISTING_FILES_OVERWRITE): void 104 { 105 $folderRealPath = $this->getNormalizedRealPath($folderPath).'/'; 106 $itemIterator = new RecursiveIteratorIterator( 107 new RecursiveDirectoryIterator($folderPath, RecursiveDirectoryIterator::SKIP_DOTS), 108 RecursiveIteratorIterator::SELF_FIRST 109 ); 110 111 foreach ($itemIterator as $itemInfo) { 112 \assert($itemInfo instanceof SplFileInfo); 113 $itemRealPath = $this->getNormalizedRealPath($itemInfo->getPathname()); 114 $itemLocalPath = str_replace($folderRealPath, '', $itemRealPath); 115 116 if ($itemInfo->isFile() && !$this->shouldSkipFile($zip, $itemLocalPath, $existingFileMode)) { 117 $zip->addFile($itemRealPath, $itemLocalPath); 118 } 119 } 120 } 121 122 /** 123 * Closes the archive and copies it into the given stream. 124 * 125 * @param ZipArchive $zip An opened zip archive object 126 * @param resource $streamPointer Pointer to the stream to copy the zip 127 */ 128 public function closeArchiveAndCopyToStream(ZipArchive $zip, $streamPointer): void 129 { 130 $zipFilePath = $zip->filename; 131 $zip->close(); 132 133 $this->copyZipToStream($zipFilePath, $streamPointer); 134 } 135 136 /** 137 * Adds the given file, located under the given root folder to the archive. 138 * The file will NOT be compressed. 139 * 140 * Example of use: 141 * addUncompressedFileToArchive($zip, '/tmp/xlsx/foo', 'bar/baz.xml'); 142 * => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml' 143 * 144 * @param ZipArchive $zip An opened zip archive object 145 * @param string $rootFolderPath path of the root folder that will be ignored in the archive tree 146 * @param string $localFilePath Path of the file to be added, under the root folder 147 * @param string $existingFileMode Controls what to do when trying to add an existing file 148 * @param int $compressionMethod The compression method 149 */ 150 private function addFileToArchiveWithCompressionMethod(ZipArchive $zip, string $rootFolderPath, string $localFilePath, string $existingFileMode, int $compressionMethod): void 151 { 152 $normalizedLocalFilePath = str_replace('\\', '/', $localFilePath); 153 if (!$this->shouldSkipFile($zip, $normalizedLocalFilePath, $existingFileMode)) { 154 $normalizedFullFilePath = $this->getNormalizedRealPath($rootFolderPath.'/'.$normalizedLocalFilePath); 155 $zip->addFile($normalizedFullFilePath, $normalizedLocalFilePath); 156 157 $zip->setCompressionName($normalizedLocalFilePath, $compressionMethod); 158 } 159 } 160 161 /** 162 * @return bool Whether the file should be added to the archive or skipped 163 */ 164 private function shouldSkipFile(ZipArchive $zip, string $itemLocalPath, string $existingFileMode): bool 165 { 166 // Skip files if: 167 // - EXISTING_FILES_SKIP mode chosen 168 // - File already exists in the archive 169 return self::EXISTING_FILES_SKIP === $existingFileMode && false !== $zip->locateName($itemLocalPath); 170 } 171 172 /** 173 * Returns canonicalized absolute pathname, containing only forward slashes. 174 * 175 * @param string $path Path to normalize 176 * 177 * @return string Normalized and canonicalized path 178 */ 179 private function getNormalizedRealPath(string $path): string 180 { 181 $realPath = realpath($path); 182 \assert(false !== $realPath); 183 184 return str_replace(\DIRECTORY_SEPARATOR, '/', $realPath); 185 } 186 187 /** 188 * Streams the contents of the zip file into the given stream. 189 * 190 * @param string $zipFilePath Path of the zip file 191 * @param resource $pointer Pointer to the stream to copy the zip 192 */ 193 private function copyZipToStream(string $zipFilePath, $pointer): void 194 { 195 $zipFilePointer = fopen($zipFilePath, 'r'); 196 \assert(false !== $zipFilePointer); 197 stream_copy_to_stream($zipFilePointer, $pointer); 198 fclose($zipFilePointer); 199 } 200 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body