See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]
1 <?php 2 declare(strict_types=1); 3 4 namespace ZipStream; 5 6 use Psr\Http\Message\StreamInterface; 7 use ZipStream\Exception\OverflowException; 8 use ZipStream\Option\Archive as ArchiveOptions; 9 use ZipStream\Option\File as FileOptions; 10 use ZipStream\Option\Version; 11 12 /** 13 * ZipStream 14 * 15 * Streamed, dynamically generated zip archives. 16 * 17 * Usage: 18 * 19 * Streaming zip archives is a simple, three-step process: 20 * 21 * 1. Create the zip stream: 22 * 23 * $zip = new ZipStream('example.zip'); 24 * 25 * 2. Add one or more files to the archive: 26 * 27 * * add first file 28 * $data = file_get_contents('some_file.gif'); 29 * $zip->addFile('some_file.gif', $data); 30 * 31 * * add second file 32 * $data = file_get_contents('some_file.gif'); 33 * $zip->addFile('another_file.png', $data); 34 * 35 * 3. Finish the zip stream: 36 * 37 * $zip->finish(); 38 * 39 * You can also add an archive comment, add comments to individual files, 40 * and adjust the timestamp of files. See the API documentation for each 41 * method below for additional information. 42 * 43 * Example: 44 * 45 * // create a new zip stream object 46 * $zip = new ZipStream('some_files.zip'); 47 * 48 * // list of local files 49 * $files = array('foo.txt', 'bar.jpg'); 50 * 51 * // read and add each file to the archive 52 * foreach ($files as $path) 53 * $zip->addFile($path, file_get_contents($path)); 54 * 55 * // write archive footer to stream 56 * $zip->finish(); 57 */ 58 class ZipStream 59 { 60 /** 61 * This number corresponds to the ZIP version/OS used (2 bytes) 62 * From: https://www.iana.org/assignments/media-types/application/zip 63 * The upper byte (leftmost one) indicates the host system (OS) for the 64 * file. Software can use this information to determine 65 * the line record format for text files etc. The current 66 * mappings are: 67 * 68 * 0 - MS-DOS and OS/2 (F.A.T. file systems) 69 * 1 - Amiga 2 - VAX/VMS 70 * 3 - *nix 4 - VM/CMS 71 * 5 - Atari ST 6 - OS/2 H.P.F.S. 72 * 7 - Macintosh 8 - Z-System 73 * 9 - CP/M 10 thru 255 - unused 74 * 75 * The lower byte (rightmost one) indicates the version number of the 76 * software used to encode the file. The value/10 77 * indicates the major version number, and the value 78 * mod 10 is the minor version number. 79 * Here we are using 6 for the OS, indicating OS/2 H.P.F.S. 80 * to prevent file permissions issues upon extract (see #84) 81 * 0x603 is 00000110 00000011 in binary, so 6 and 3 82 */ 83 const ZIP_VERSION_MADE_BY = 0x603; 84 85 /** 86 * The following signatures end with 0x4b50, which in ASCII isĀ PK, 87 * the initials of the inventor Phil Katz. 88 * See https://en.wikipedia.org/wiki/Zip_(file_format)#File_headers 89 */ 90 const FILE_HEADER_SIGNATURE = 0x04034b50; 91 const CDR_FILE_SIGNATURE = 0x02014b50; 92 const CDR_EOF_SIGNATURE = 0x06054b50; 93 const DATA_DESCRIPTOR_SIGNATURE = 0x08074b50; 94 const ZIP64_CDR_EOF_SIGNATURE = 0x06064b50; 95 const ZIP64_CDR_LOCATOR_SIGNATURE = 0x07064b50; 96 97 /** 98 * Global Options 99 * 100 * @var ArchiveOptions 101 */ 102 public $opt; 103 104 /** 105 * @var array 106 */ 107 public $files = []; 108 109 /** 110 * @var Bigint 111 */ 112 public $cdr_ofs; 113 114 /** 115 * @var Bigint 116 */ 117 public $ofs; 118 119 /** 120 * @var bool 121 */ 122 protected $need_headers; 123 124 /** 125 * @var null|String 126 */ 127 protected $output_name; 128 129 /** 130 * Create a new ZipStream object. 131 * 132 * Parameters: 133 * 134 * @param String $name - Name of output file (optional). 135 * @param ArchiveOptions $opt - Archive Options 136 * 137 * Large File Support: 138 * 139 * By default, the method addFileFromPath() will send send files 140 * larger than 20 megabytes along raw rather than attempting to 141 * compress them. You can change both the maximum size and the 142 * compression behavior using the largeFile* options above, with the 143 * following caveats: 144 * 145 * * For "small" files (e.g. files smaller than largeFileSize), the 146 * memory use can be up to twice that of the actual file. In other 147 * words, adding a 10 megabyte file to the archive could potentially 148 * occupy 20 megabytes of memory. 149 * 150 * * Enabling compression on large files (e.g. files larger than 151 * large_file_size) is extremely slow, because ZipStream has to pass 152 * over the large file once to calculate header information, and then 153 * again to compress and send the actual data. 154 * 155 * Examples: 156 * 157 * // create a new zip file named 'foo.zip' 158 * $zip = new ZipStream('foo.zip'); 159 * 160 * // create a new zip file named 'bar.zip' with a comment 161 * $opt->setComment = 'this is a comment for the zip file.'; 162 * $zip = new ZipStream('bar.zip', $opt); 163 * 164 * Notes: 165 * 166 * In order to let this library send HTTP headers, a filename must be given 167 * _and_ the option `sendHttpHeaders` must be `true`. This behavior is to 168 * allow software to send its own headers (including the filename), and 169 * still use this library. 170 */ 171 public function __construct(?string $name = null, ?ArchiveOptions $opt = null) 172 { 173 $this->opt = $opt ?: new ArchiveOptions(); 174 175 $this->output_name = $name; 176 $this->need_headers = $name && $this->opt->isSendHttpHeaders(); 177 178 $this->cdr_ofs = new Bigint(); 179 $this->ofs = new Bigint(); 180 } 181 182 /** 183 * addFile 184 * 185 * Add a file to the archive. 186 * 187 * @param String $name - path of file in archive (including directory). 188 * @param String $data - contents of file 189 * @param FileOptions $options 190 * 191 * File Options: 192 * time - Last-modified timestamp (seconds since the epoch) of 193 * this file. Defaults to the current time. 194 * comment - Comment related to this file. 195 * method - Storage method for file ("store" or "deflate") 196 * 197 * Examples: 198 * 199 * // add a file named 'foo.txt' 200 * $data = file_get_contents('foo.txt'); 201 * $zip->addFile('foo.txt', $data); 202 * 203 * // add a file named 'bar.jpg' with a comment and a last-modified 204 * // time of two hours ago 205 * $data = file_get_contents('bar.jpg'); 206 * $opt->setTime = time() - 2 * 3600; 207 * $opt->setComment = 'this is a comment about bar.jpg'; 208 * $zip->addFile('bar.jpg', $data, $opt); 209 */ 210 public function addFile(string $name, string $data, ?FileOptions $options = null): void 211 { 212 $options = $options ?: new FileOptions(); 213 $options->defaultTo($this->opt); 214 215 $file = new File($this, $name, $options); 216 $file->processData($data); 217 } 218 219 /** 220 * addFileFromPath 221 * 222 * Add a file at path to the archive. 223 * 224 * Note that large files may be compressed differently than smaller 225 * files; see the "Large File Support" section above for more 226 * information. 227 * 228 * @param String $name - name of file in archive (including directory path). 229 * @param String $path - path to file on disk (note: paths should be encoded using 230 * UNIX-style forward slashes -- e.g '/path/to/some/file'). 231 * @param FileOptions $options 232 * 233 * File Options: 234 * time - Last-modified timestamp (seconds since the epoch) of 235 * this file. Defaults to the current time. 236 * comment - Comment related to this file. 237 * method - Storage method for file ("store" or "deflate") 238 * 239 * Examples: 240 * 241 * // add a file named 'foo.txt' from the local file '/tmp/foo.txt' 242 * $zip->addFileFromPath('foo.txt', '/tmp/foo.txt'); 243 * 244 * // add a file named 'bigfile.rar' from the local file 245 * // '/usr/share/bigfile.rar' with a comment and a last-modified 246 * // time of two hours ago 247 * $path = '/usr/share/bigfile.rar'; 248 * $opt->setTime = time() - 2 * 3600; 249 * $opt->setComment = 'this is a comment about bar.jpg'; 250 * $zip->addFileFromPath('bigfile.rar', $path, $opt); 251 * 252 * @return void 253 * @throws \ZipStream\Exception\FileNotFoundException 254 * @throws \ZipStream\Exception\FileNotReadableException 255 */ 256 public function addFileFromPath(string $name, string $path, ?FileOptions $options = null): void 257 { 258 $options = $options ?: new FileOptions(); 259 $options->defaultTo($this->opt); 260 261 $file = new File($this, $name, $options); 262 $file->processPath($path); 263 } 264 265 /** 266 * addFileFromStream 267 * 268 * Add an open stream to the archive. 269 * 270 * @param String $name - path of file in archive (including directory). 271 * @param resource $stream - contents of file as a stream resource 272 * @param FileOptions $options 273 * 274 * File Options: 275 * time - Last-modified timestamp (seconds since the epoch) of 276 * this file. Defaults to the current time. 277 * comment - Comment related to this file. 278 * 279 * Examples: 280 * 281 * // create a temporary file stream and write text to it 282 * $fp = tmpfile(); 283 * fwrite($fp, 'The quick brown fox jumped over the lazy dog.'); 284 * 285 * // add a file named 'streamfile.txt' from the content of the stream 286 * $x->addFileFromStream('streamfile.txt', $fp); 287 * 288 * @return void 289 */ 290 public function addFileFromStream(string $name, $stream, ?FileOptions $options = null): void 291 { 292 $options = $options ?: new FileOptions(); 293 $options->defaultTo($this->opt); 294 295 $file = new File($this, $name, $options); 296 $file->processStream(new DeflateStream($stream)); 297 } 298 299 /** 300 * addFileFromPsr7Stream 301 * 302 * Add an open stream to the archive. 303 * 304 * @param String $name - path of file in archive (including directory). 305 * @param StreamInterface $stream - contents of file as a stream resource 306 * @param FileOptions $options 307 * 308 * File Options: 309 * time - Last-modified timestamp (seconds since the epoch) of 310 * this file. Defaults to the current time. 311 * comment - Comment related to this file. 312 * 313 * Examples: 314 * 315 * $stream = $response->getBody(); 316 * // add a file named 'streamfile.txt' from the content of the stream 317 * $x->addFileFromPsr7Stream('streamfile.txt', $stream); 318 * 319 * @return void 320 */ 321 public function addFileFromPsr7Stream( 322 string $name, 323 StreamInterface $stream, 324 ?FileOptions $options = null 325 ): void { 326 $options = $options ?: new FileOptions(); 327 $options->defaultTo($this->opt); 328 329 $file = new File($this, $name, $options); 330 $file->processStream($stream); 331 } 332 333 /** 334 * finish 335 * 336 * Write zip footer to stream. 337 * 338 * Example: 339 * 340 * // add a list of files to the archive 341 * $files = array('foo.txt', 'bar.jpg'); 342 * foreach ($files as $path) 343 * $zip->addFile($path, file_get_contents($path)); 344 * 345 * // write footer to stream 346 * $zip->finish(); 347 * @return void 348 * 349 * @throws OverflowException 350 */ 351 public function finish(): void 352 { 353 // add trailing cdr file records 354 foreach ($this->files as $cdrFile) { 355 $this->send($cdrFile); 356 $this->cdr_ofs = $this->cdr_ofs->add(Bigint::init(strlen($cdrFile))); 357 } 358 359 // Add 64bit headers (if applicable) 360 if (count($this->files) >= 0xFFFF || 361 $this->cdr_ofs->isOver32() || 362 $this->ofs->isOver32()) { 363 if (!$this->opt->isEnableZip64()) { 364 throw new OverflowException(); 365 } 366 367 $this->addCdr64Eof(); 368 $this->addCdr64Locator(); 369 } 370 371 // add trailing cdr eof record 372 $this->addCdrEof(); 373 374 // The End 375 $this->clear(); 376 } 377 378 /** 379 * Send ZIP64 CDR EOF (Central Directory Record End-of-File) record. 380 * 381 * @return void 382 */ 383 protected function addCdr64Eof(): void 384 { 385 $num_files = count($this->files); 386 $cdr_length = $this->cdr_ofs; 387 $cdr_offset = $this->ofs; 388 389 $fields = [ 390 ['V', static::ZIP64_CDR_EOF_SIGNATURE], // ZIP64 end of central file header signature 391 ['P', 44], // Length of data below this header (length of block - 12) = 44 392 ['v', static::ZIP_VERSION_MADE_BY], // Made by version 393 ['v', Version::ZIP64], // Extract by version 394 ['V', 0x00], // disk number 395 ['V', 0x00], // no of disks 396 ['P', $num_files], // no of entries on disk 397 ['P', $num_files], // no of entries in cdr 398 ['P', $cdr_length], // CDR size 399 ['P', $cdr_offset], // CDR offset 400 ]; 401 402 $ret = static::packFields($fields); 403 $this->send($ret); 404 } 405 406 /** 407 * Create a format string and argument list for pack(), then call 408 * pack() and return the result. 409 * 410 * @param array $fields 411 * @return string 412 */ 413 public static function packFields(array $fields): string 414 { 415 $fmt = ''; 416 $args = []; 417 418 // populate format string and argument list 419 foreach ($fields as [$format, $value]) { 420 if ($format === 'P') { 421 $fmt .= 'VV'; 422 if ($value instanceof Bigint) { 423 $args[] = $value->getLow32(); 424 $args[] = $value->getHigh32(); 425 } else { 426 $args[] = $value; 427 $args[] = 0; 428 } 429 } else { 430 if ($value instanceof Bigint) { 431 $value = $value->getLow32(); 432 } 433 $fmt .= $format; 434 $args[] = $value; 435 } 436 } 437 438 // prepend format string to argument list 439 array_unshift($args, $fmt); 440 441 // build output string from header and compressed data 442 return pack(...$args); 443 } 444 445 /** 446 * Send string, sending HTTP headers if necessary. 447 * Flush output after write if configure option is set. 448 * 449 * @param String $str 450 * @return void 451 */ 452 public function send(string $str): void 453 { 454 if ($this->need_headers) { 455 $this->sendHttpHeaders(); 456 } 457 $this->need_headers = false; 458 459 $outputStream = $this->opt->getOutputStream(); 460 461 if ($outputStream instanceof StreamInterface) { 462 $outputStream->write($str); 463 } else { 464 fwrite($outputStream, $str); 465 } 466 467 if ($this->opt->isFlushOutput()) { 468 // flush output buffer if it is on and flushable 469 $status = ob_get_status(); 470 if (isset($status['flags']) && ($status['flags'] & PHP_OUTPUT_HANDLER_FLUSHABLE)) { 471 ob_flush(); 472 } 473 474 // Flush system buffers after flushing userspace output buffer 475 flush(); 476 } 477 } 478 479 /** 480 * Send HTTP headers for this stream. 481 * 482 * @return void 483 */ 484 protected function sendHttpHeaders(): void 485 { 486 // grab content disposition 487 $disposition = $this->opt->getContentDisposition(); 488 489 if ($this->output_name) { 490 // Various different browsers dislike various characters here. Strip them all for safety. 491 $safe_output = trim(str_replace(['"', "'", '\\', ';', "\n", "\r"], '', $this->output_name)); 492 493 // Check if we need to UTF-8 encode the filename 494 $urlencoded = rawurlencode($safe_output); 495 $disposition .= "; filename*=UTF-8''{$urlencoded}"; 496 } 497 498 $headers = array( 499 'Content-Type' => $this->opt->getContentType(), 500 'Content-Disposition' => $disposition, 501 'Pragma' => 'public', 502 'Cache-Control' => 'public, must-revalidate', 503 'Content-Transfer-Encoding' => 'binary' 504 ); 505 506 $call = $this->opt->getHttpHeaderCallback(); 507 foreach ($headers as $key => $val) { 508 $call("$key: $val"); 509 } 510 } 511 512 /** 513 * Send ZIP64 CDR Locator (Central Directory Record Locator) record. 514 * 515 * @return void 516 */ 517 protected function addCdr64Locator(): void 518 { 519 $cdr_offset = $this->ofs->add($this->cdr_ofs); 520 521 $fields = [ 522 ['V', static::ZIP64_CDR_LOCATOR_SIGNATURE], // ZIP64 end of central file header signature 523 ['V', 0x00], // Disc number containing CDR64EOF 524 ['P', $cdr_offset], // CDR offset 525 ['V', 1], // Total number of disks 526 ]; 527 528 $ret = static::packFields($fields); 529 $this->send($ret); 530 } 531 532 /** 533 * Send CDR EOF (Central Directory Record End-of-File) record. 534 * 535 * @return void 536 */ 537 protected function addCdrEof(): void 538 { 539 $num_files = count($this->files); 540 $cdr_length = $this->cdr_ofs; 541 $cdr_offset = $this->ofs; 542 543 // grab comment (if specified) 544 $comment = $this->opt->getComment(); 545 546 $fields = [ 547 ['V', static::CDR_EOF_SIGNATURE], // end of central file header signature 548 ['v', 0x00], // disk number 549 ['v', 0x00], // no of disks 550 ['v', min($num_files, 0xFFFF)], // no of entries on disk 551 ['v', min($num_files, 0xFFFF)], // no of entries in cdr 552 ['V', $cdr_length->getLowFF()], // CDR size 553 ['V', $cdr_offset->getLowFF()], // CDR offset 554 ['v', strlen($comment)], // Zip Comment size 555 ]; 556 557 $ret = static::packFields($fields) . $comment; 558 $this->send($ret); 559 } 560 561 /** 562 * Clear all internal variables. Note that the stream object is not 563 * usable after this. 564 * 565 * @return void 566 */ 567 protected function clear(): void 568 { 569 $this->files = []; 570 $this->ofs = new Bigint(); 571 $this->cdr_ofs = new Bigint(); 572 $this->opt = new ArchiveOptions(); 573 } 574 575 /** 576 * Is this file larger than large_file_size? 577 * 578 * @param string $path 579 * @return bool 580 */ 581 public function isLargeFile(string $path): bool 582 { 583 if (!$this->opt->isStatFiles()) { 584 return false; 585 } 586 $stat = stat($path); 587 return $stat['size'] > $this->opt->getLargeFileSize(); 588 } 589 590 /** 591 * Save file attributes for trailing CDR record. 592 * 593 * @param File $file 594 * @return void 595 */ 596 public function addToCdr(File $file): void 597 { 598 $file->ofs = $this->ofs; 599 $this->ofs = $this->ofs->add($file->getTotalLength()); 600 $this->files[] = $file->getCdrFile(); 601 } 602 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body