├── .editorconfig ├── .phive └── phars.xml ├── .php-cs-fixer.dist.php ├── .phpdoc └── template │ └── base.html.twig ├── .tool-versions ├── LICENSE ├── README.md ├── composer.json ├── guides ├── ContentLength.rst ├── FlySystem.rst ├── Nginx.rst ├── Options.rst ├── PSR7Streams.rst ├── StreamOutput.rst ├── Symfony.rst ├── Varnish.rst └── index.rst ├── phpdoc.dist.xml ├── phpunit.xml.dist ├── psalm.xml ├── src ├── CentralDirectoryFileHeader.php ├── CompressionMethod.php ├── DataDescriptor.php ├── EndOfCentralDirectory.php ├── Exception.php ├── Exception │ ├── DosTimeOverflowException.php │ ├── FileNotFoundException.php │ ├── FileNotReadableException.php │ ├── FileSizeIncorrectException.php │ ├── OverflowException.php │ ├── ResourceActionException.php │ ├── SimulationFileUnknownException.php │ ├── StreamNotReadableException.php │ └── StreamNotSeekableException.php ├── File.php ├── GeneralPurposeBitFlag.php ├── LocalFileHeader.php ├── OperationMode.php ├── PackField.php ├── Time.php ├── Version.php ├── Zip64 │ ├── DataDescriptor.php │ ├── EndOfCentralDirectory.php │ ├── EndOfCentralDirectoryLocator.php │ └── ExtendedInformationExtraField.php ├── ZipStream.php └── Zs │ └── ExtendedInformationExtraField.php └── test ├── Assertions.php ├── CentralDirectoryFileHeaderTest.php ├── DataDescriptorTest.php ├── EndOfCentralDirectoryTest.php ├── EndlessCycleStream.php ├── FaultInjectionResource.php ├── LocalFileHeaderTest.php ├── PackFieldTest.php ├── ResourceStream.php ├── Tempfile.php ├── TimeTest.php ├── Util.php ├── Zip64 ├── DataDescriptorTest.php ├── EndOfCentralDirectoryLocatorTest.php ├── EndOfCentralDirectoryTest.php └── ExtendedInformationExtraFieldTest.php ├── ZipStreamTest.php ├── Zs └── ExtendedInformationExtraFieldTest.php └── bootstrap.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | 8 | [*.{yml,md,xml}] 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.{rst,php}] 13 | indent_style = space 14 | indent_size = 4 15 | 16 | [composer.json] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [composer.lock] 21 | indent_style = space 22 | indent_size = 4 23 | -------------------------------------------------------------------------------- /.phive/phars.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright 2022 Nicolas CARPi 9 | * @see https://github.com/maennchen/ZipStream-PHP 10 | * @license MIT 11 | * @package maennchen/ZipStream-PHP 12 | */ 13 | 14 | use PhpCsFixer\Config; 15 | use PhpCsFixer\Finder; 16 | use PhpCsFixer\Runner; 17 | 18 | $finder = Finder::create() 19 | ->exclude('.github') 20 | ->exclude('.phpdoc') 21 | ->exclude('docs') 22 | ->exclude('tools') 23 | ->exclude('vendor') 24 | ->in(__DIR__); 25 | 26 | $config = new Config(); 27 | return $config->setRules([ 28 | '@PER' => true, 29 | '@PER:risky' => true, 30 | '@PHP83Migration' => true, 31 | '@PHP84Migration' => true, 32 | '@PHPUnit84Migration:risky' => true, 33 | 'array_syntax' => ['syntax' => 'short'], 34 | 'class_attributes_separation' => true, 35 | 'declare_strict_types' => true, 36 | 'dir_constant' => true, 37 | 'is_null' => true, 38 | 'no_homoglyph_names' => true, 39 | 'no_null_property_initialization' => true, 40 | 'no_php4_constructor' => true, 41 | 'no_unused_imports' => true, 42 | 'no_useless_else' => true, 43 | 'non_printable_character' => true, 44 | 'ordered_imports' => true, 45 | 'ordered_class_elements' => true, 46 | 'php_unit_construct' => true, 47 | 'pow_to_exponentiation' => true, 48 | 'psr_autoloading' => true, 49 | 'random_api_migration' => true, 50 | 'return_assignment' => true, 51 | 'self_accessor' => true, 52 | 'semicolon_after_instruction' => true, 53 | 'short_scalar_cast' => true, 54 | 'simplified_null_return' => true, 55 | 'single_class_element_per_statement' => true, 56 | 'single_line_comment_style' => true, 57 | 'single_quote' => true, 58 | 'space_after_semicolon' => true, 59 | 'standardize_not_equals' => true, 60 | 'strict_param' => true, 61 | 'ternary_operator_spaces' => true, 62 | 'trailing_comma_in_multiline' => true, 63 | 'trim_array_spaces' => true, 64 | 'unary_operator_spaces' => true, 65 | 'global_namespace_import' => [ 66 | 'import_classes' => true, 67 | 'import_functions' => true, 68 | 'import_constants' => true, 69 | ], 70 | ]) 71 | ->setFinder($finder) 72 | ->setRiskyAllowed(true) 73 | ->setParallelConfig(Runner\Parallel\ParallelConfigFactory::detect()); 74 | -------------------------------------------------------------------------------- /.phpdoc/template/base.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% set topMenu = { 4 | "menu": [ 5 | { "name": "Guides", "url": "https://maennchen.dev/ZipStream-PHP/guide/index.html"}, 6 | { "name": "API", "url": "https://maennchen.dev/ZipStream-PHP/classes/ZipStream-ZipStream.html"}, 7 | { "name": "Issues", "url": "https://github.com/maennchen/ZipStream-PHP/issues"}, 8 | ], 9 | "social": [ 10 | { "iconClass": "fab fa-github", "url": "https://github.com/maennchen/ZipStream-PHP"}, 11 | { "iconClass": "fas fa-envelope-open-text", "url": "https://github.com/maennchen/ZipStream-PHP/discussions"}, 12 | { "iconClass": "fas fa-money-bill", "url": "https://github.com/sponsors/maennchen"}, 13 | ] 14 | } 15 | %} -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | php 8.4.5 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (C) 2007-2009 Paul Duncan 4 | Copyright (C) 2014 Jonatan Männchen 5 | Copyright (C) 2014 Jesse G. Donat 6 | Copyright (C) 2018 Nicolas CARPi 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZipStream-PHP 2 | 3 | [![Main Branch](https://github.com/maennchen/ZipStream-PHP/actions/workflows/branch_main.yml/badge.svg)](https://github.com/maennchen/ZipStream-PHP/actions/workflows/branch_main.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/maennchen/ZipStream-PHP/badge.svg?branch=main)](https://coveralls.io/github/maennchen/ZipStream-PHP?branch=main) 5 | [![Latest Stable Version](https://poser.pugx.org/maennchen/zipstream-php/v/stable)](https://packagist.org/packages/maennchen/zipstream-php) 6 | [![Total Downloads](https://poser.pugx.org/maennchen/zipstream-php/downloads)](https://packagist.org/packages/maennchen/zipstream-php) 7 | [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9524/badge)](https://www.bestpractices.dev/projects/9524) 8 | [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/maennchen/ZipStream-PHP/badge)](https://scorecard.dev/viewer/?uri=github.com/maennchen/ZipStream-PHP) 9 | 10 | ## Unstable Branch 11 | 12 | The `main` branch is not stable. Please see the 13 | [releases](https://github.com/maennchen/ZipStream-PHP/releases) for a stable 14 | version. 15 | 16 | ## Overview 17 | 18 | A fast and simple streaming zip file downloader for PHP. Using this library will 19 | save you from having to write the Zip to disk. You can directly send it to the 20 | user, which is much faster. It can work with S3 buckets or any PSR7 Stream. 21 | 22 | Please see the [LICENSE](LICENSE) file for licensing and warranty information. 23 | 24 | ## Installation 25 | 26 | Simply add a dependency on maennchen/zipstream-php to your project's 27 | `composer.json` file if you use Composer to manage the dependencies of your 28 | project. Use following command to add the package to your project's dependencies: 29 | 30 | ```bash 31 | composer require maennchen/zipstream-php 32 | ``` 33 | 34 | ## Usage 35 | 36 | For detailed instructions, please check the 37 | [Documentation](https://maennchen.github.io/ZipStream-PHP/). 38 | 39 | ```php 40 | // Autoload the dependencies 41 | require 'vendor/autoload.php'; 42 | 43 | // create a new zipstream object 44 | $zip = new ZipStream\ZipStream( 45 | outputName: 'example.zip', 46 | 47 | // enable output of HTTP headers 48 | sendHttpHeaders: true, 49 | ); 50 | 51 | // create a file named 'hello.txt' 52 | $zip->addFile( 53 | fileName: 'hello.txt', 54 | data: 'This is the contents of hello.txt', 55 | ); 56 | 57 | // add a file named 'some_image.jpg' from a local file 'path/to/image.jpg' 58 | $zip->addFileFromPath( 59 | fileName: 'some_image.jpg', 60 | path: 'path/to/image.jpg', 61 | ); 62 | 63 | // finish the zip stream 64 | $zip->finish(); 65 | ``` 66 | 67 | ## Questions 68 | 69 | **💬 Questions? Please Read This First!** 70 | 71 | If you have a question about using this library, please *do not email the 72 | authors directly*. Instead, head over to the 73 | [GitHub Discussions](https://github.com/maennchen/ZipStream-PHP/discussions) 74 | page — your question might already be answered there! Using Discussions helps 75 | build a shared knowledge base, so others can also benefit from the answers. If 76 | you need dedicated 1:1 support, check out the options available on 77 | [@maennchen's sponsorship page](https://github.com/sponsors/maennchen?frequency=one-time&sponsor=maennchen). 78 | 79 | ## Upgrade to version 3.1.2 80 | 81 | - Minimum PHP Version: `8.2` 82 | 83 | ## Upgrade to version 3.0.0 84 | 85 | ### General 86 | 87 | - Minimum PHP Version: `8.1` 88 | - Only 64bit Architecture is supported. 89 | - The class `ZipStream\Option\Method` has been replaced with the enum 90 | `ZipStream\CompressionMethod`. 91 | - Most classes have been flagged as `@internal` and should not be used from the 92 | outside. 93 | If you're using internal resources to extend this library, please open an 94 | issue so that a clean interface can be added & published. 95 | The externally available classes & enums are: 96 | - `ZipStream\CompressionMethod` 97 | - `ZipStream\Exception*` 98 | - `ZipStream\ZipStream` 99 | 100 | ### Archive Options 101 | 102 | - The class `ZipStream\Option\Archive` has been replaced in favor of named 103 | arguments in the `ZipStream\ZipStream` constructor. 104 | - The archive options `largeFileSize` & `largeFileMethod` has been removed. If 105 | you want different `compressionMethods` based on the file size, you'll have to 106 | implement this yourself. 107 | - The archive option `httpHeaderCallback` changed the type from `callable` to 108 | `Closure`. 109 | - The archive option `zeroHeader` has been replaced with the option 110 | `defaultEnableZeroHeader` and can be overridden for every file. Its default 111 | value changed from `false` to `true`. 112 | - The archive option `statFiles` was removed since the library no longer checks 113 | filesizes this way. 114 | - The archive option `deflateLevel` has been replaced with the option 115 | `defaultDeflateLevel` and can be overridden for every file. 116 | - The first argument (`name`) of the `ZipStream\ZipStream` constructor has been 117 | replaced with the named argument `outputName`. 118 | - Headers are now also sent if the `outputName` is empty. If you do not want to 119 | automatically send http headers, set `sendHttpHeaders` to `false`. 120 | 121 | ### File Options 122 | 123 | - The class `ZipStream\Option\File` has been replaced in favor of named 124 | arguments in the `ZipStream\ZipStream->addFile*` functions. 125 | - The file option `method` has been renamed to `compressionMethod`. 126 | - The file option `time` has been renamed to `lastModificationDateTime`. 127 | - The file option `size` has been renamed to `maxSize`. 128 | 129 | ## Upgrade to version 2.0.0 130 | 131 | https://github.com/maennchen/ZipStream-PHP/tree/2.0.0#upgrade-to-version-200 132 | 133 | ## Upgrade to version 1.0.0 134 | 135 | https://github.com/maennchen/ZipStream-PHP/tree/2.0.0#upgrade-to-version-100 136 | 137 | ## Contributing 138 | 139 | ZipStream-PHP is a collaborative project. Please take a look at the 140 | [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) file. 141 | 142 | ## Version Support 143 | 144 | Versions are supported according to the table below. 145 | 146 | Please do not open any pull requests contradicting the current version support 147 | status. 148 | 149 | Careful: Always check the `README` on `main` for up-to-date information. 150 | 151 | | Version | New Features | Bugfixes | Security | 152 | |---------|--------------|----------|----------| 153 | | *3* | ✓ | ✓ | ✓ | 154 | | *2* | ✗ | ✗ | ✓ | 155 | | *1* | ✗ | ✗ | ✗ | 156 | | *0* | ✗ | ✗ | ✗ | 157 | 158 | This library aligns itself with the PHP core support. New features and bugfixes 159 | will only target PHP versions according to their current status. 160 | 161 | See: https://www.php.net/supported-versions.php 162 | 163 | ## About the Authors 164 | 165 | - Paul Duncan - https://pablotron.org/ 166 | - Jonatan Männchen - https://maennchen.dev 167 | - Jesse G. Donat - https://donatstudios.com 168 | - Nicolas CARPi - https://www.deltablot.com 169 | - Nik Barham - https://www.brokencube.co.uk 170 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "maennchen/zipstream-php", 3 | "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", 4 | "keywords": ["zip", "stream"], 5 | "type": "library", 6 | "license": "MIT", 7 | "authors": [{ 8 | "name": "Paul Duncan", 9 | "email": "pabs@pablotron.org" 10 | }, 11 | { 12 | "name": "Jonatan Männchen", 13 | "email": "jonatan@maennchen.ch" 14 | }, 15 | { 16 | "name": "Jesse Donat", 17 | "email": "donatj@gmail.com" 18 | }, 19 | { 20 | "name": "András Kolesár", 21 | "email": "kolesar@kolesar.hu" 22 | } 23 | ], 24 | "require": { 25 | "php-64bit": "^8.3", 26 | "ext-mbstring": "*", 27 | "ext-zlib": "*" 28 | }, 29 | "require-dev": { 30 | "phpunit/phpunit": "^12.0", 31 | "guzzlehttp/guzzle": "^7.5", 32 | "ext-zip": "*", 33 | "mikey179/vfsstream": "^1.6", 34 | "php-coveralls/php-coveralls": "^2.5", 35 | "friendsofphp/php-cs-fixer": "^3.16", 36 | "vimeo/psalm": "^6.0", 37 | "brianium/paratest": "^7.7" 38 | }, 39 | "suggest": { 40 | "psr/http-message": "^2.0", 41 | "guzzlehttp/psr7": "^2.4" 42 | }, 43 | "scripts": { 44 | "format": "php-cs-fixer fix", 45 | "test": [ 46 | "@test:unit", 47 | "@test:formatted", 48 | "@test:lint" 49 | ], 50 | "test:unit:setup-cov": "@putenv XDEBUG_MODE=coverage", 51 | "test:unit": "paratest --functional", 52 | "test:unit:cov": ["@test:unit:setup-cov", "@test:unit --coverage-clover=coverage.clover.xml --coverage-html cov"], 53 | "test:unit:slow": "@test:unit --group slow", 54 | "test:unit:slow:cov": ["@test:unit:setup-cov", "@test:unit --coverage-clover=coverage.clover.xml --coverage-html cov --group slow"], 55 | "test:unit:fast": "@test:unit --exclude-group slow", 56 | "test:unit:fast:cov": ["@test:unit:setup-cov", "@test:unit --coverage-clover=coverage.clover.xml --coverage-html cov --exclude-group slow"], 57 | "test:formatted": "@format --dry-run --stop-on-violation --using-cache=no", 58 | "test:lint": "psalm --stats --show-info=true --find-unused-psalm-suppress", 59 | "coverage:report": "php-coveralls --coverage_clover=coverage.clover.xml --json_path=coveralls-upload.json --insecure", 60 | "install:tools": "phive install --trust-gpg-keys 0x67F861C3D889C656 --trust-gpg-keys 0x8AC0BAA79732DD42 --trust-gpg-keys 0x6DA3ACC4991FFAE5", 61 | "docs:generate": "tools/phpdocumentor --sourcecode" 62 | }, 63 | "autoload": { 64 | "psr-4": { 65 | "ZipStream\\": "src/" 66 | } 67 | }, 68 | "autoload-dev": { 69 | "psr-4": { "ZipStream\\Test\\": "test/" } 70 | }, 71 | "archive": { 72 | "exclude": [ 73 | "/composer.lock", 74 | "/docs", 75 | "/.gitattributes", 76 | "/.github", 77 | "/.gitignore", 78 | "/guides", 79 | "/.phive", 80 | "/.php-cs-fixer.cache", 81 | "/.php-cs-fixer.dist.php", 82 | "/.phpdoc", 83 | "/phpdoc.dist.xml", 84 | "/.phpunit.result.cache", 85 | "/phpunit.xml.dist", 86 | "/psalm.xml", 87 | "/test", 88 | "/tools", 89 | "/.tool-versions", 90 | "/vendor" 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /guides/ContentLength.rst: -------------------------------------------------------------------------------- 1 | Adding Content-Length header 2 | ============= 3 | 4 | Adding a ``Content-Length`` header for ``ZipStream`` can be achieved by 5 | using the options ``SIMULATION_STRICT`` or ``SIMULATION_LAX`` in the 6 | ``operationMode`` parameter. 7 | 8 | In the ``SIMULATION_STRICT`` mode, ``ZipStream`` will not allow to calculate the 9 | size based on reading the whole file. ``SIMULATION_LAX`` will read the whole 10 | file if necessary. 11 | 12 | ``SIMULATION_STRICT`` is therefore useful to make sure that the size can be 13 | calculated efficiently. 14 | 15 | .. code-block:: php 16 | use ZipStream\OperationMode; 17 | use ZipStream\ZipStream; 18 | 19 | $zip = new ZipStream( 20 | operationMode: OperationMode::SIMULATE_STRICT, // or SIMULATE_LAX 21 | defaultEnableZeroHeader: false, 22 | sendHttpHeaders: true, 23 | outputStream: $stream, 24 | ); 25 | 26 | // Normally add files 27 | $zip->addFile('sample.txt', 'Sample String Data'); 28 | 29 | // Use addFileFromCallback and exactSize if you want to defer opening of 30 | // the file resource 31 | $zip->addFileFromCallback( 32 | 'sample.txt', 33 | exactSize: 18, 34 | callback: function () { 35 | return fopen('...'); 36 | } 37 | ); 38 | 39 | // Read resulting file size 40 | $size = $zip->finish(); 41 | 42 | // Tell it to the browser 43 | header('Content-Length: '. $size); 44 | 45 | // Execute the Simulation and stream the actual zip to the client 46 | $zip->executeSimulation(); 47 | 48 | -------------------------------------------------------------------------------- /guides/FlySystem.rst: -------------------------------------------------------------------------------- 1 | Usage with FlySystem 2 | =============== 3 | 4 | For saving or uploading the generated zip, you can use the 5 | `Flysystem `_ package, and its many 6 | adapters. 7 | 8 | For that you will need to provide another stream than the ``php://output`` 9 | default one, and pass it to Flysystem ``putStream`` method. 10 | 11 | .. code-block:: php 12 | 13 | // Open Stream only once for read and write since it's a memory stream and 14 | // the content is lost when closing the stream / opening another one 15 | $tempStream = fopen('php://memory', 'w+'); 16 | 17 | // Create Zip Archive 18 | $zipStream = new ZipStream( 19 | outputStream: $tempStream, 20 | outputName: 'test.zip', 21 | ); 22 | $zipStream->addFile('test.txt', 'text'); 23 | $zipStream->finish(); 24 | 25 | // Store File 26 | // (see Flysystem documentation, and all its framework integration) 27 | // Can be any adapter (AWS, Google, Ftp, etc.) 28 | $adapter = new Local(__DIR__.'/path/to/folder'); 29 | $filesystem = new Filesystem($adapter); 30 | 31 | $filesystem->writeStream('test.zip', $tempStream) 32 | 33 | // Close Stream 34 | fclose($tempStream); 35 | -------------------------------------------------------------------------------- /guides/Nginx.rst: -------------------------------------------------------------------------------- 1 | Usage with nginx 2 | ============= 3 | 4 | If you are using nginx as a webserver, it will try to buffer the response. 5 | So you'll want to disable this with a custom header: 6 | 7 | .. code-block:: php 8 | header('X-Accel-Buffering: no'); 9 | # or with the Response class from Symfony 10 | $response->headers->set('X-Accel-Buffering', 'no'); 11 | 12 | Alternatively, you can tweak the 13 | `fastcgi cache parameters `_ 14 | within nginx config. 15 | 16 | See `original issue `_. -------------------------------------------------------------------------------- /guides/Options.rst: -------------------------------------------------------------------------------- 1 | Available options 2 | =============== 3 | 4 | Here is the full list of options available to you. You can also have a look at 5 | ``src/ZipStream.php`` file. 6 | 7 | .. code-block:: php 8 | 9 | use ZipStream\ZipStream; 10 | 11 | require_once 'vendor/autoload.php'; 12 | 13 | $zip = new ZipStream( 14 | // Define output stream 15 | // (argument is either a resource or implementing 16 | // `Psr\Http\Message\StreamInterface`) 17 | // 18 | // Setup with `psr/http-message` & `guzzlehttp/psr7` dependencies 19 | // required when using `Psr\Http\Message\StreamInterface`. 20 | outputStream: $filePointer, 21 | 22 | // Set the deflate level (default is 6; use -1 to disable it) 23 | defaultDeflateLevel: 6, 24 | 25 | // Add a comment to the zip file 26 | comment: 'This is a comment.', 27 | 28 | // Send http headers (default is true) 29 | sendHttpHeaders: false, 30 | 31 | // HTTP Content-Disposition. 32 | // Defaults to 'attachment', where FILENAME is the specified filename. 33 | // Note that this does nothing if you are not sending HTTP headers. 34 | contentDisposition: 'attachment', 35 | 36 | // Output Name for HTTP Content-Disposition 37 | // Defaults to no name 38 | outputName: "example.zip", 39 | 40 | // HTTP Content-Type. 41 | // Defaults to 'application/x-zip'. 42 | // Note that this does nothing if you are not sending HTTP headers. 43 | contentType: 'application/x-zip', 44 | 45 | // Set the function called for setting headers. 46 | // Default is the `header()` of PHP 47 | httpHeaderCallback: header(...), 48 | 49 | // Enable streaming files with single read where general purpose bit 3 50 | // indicates local file header contain zero values in crc and size 51 | // fields, these appear only after file contents in data descriptor 52 | // block. 53 | // Set to true if your input stream is remote 54 | // (used with addFileFromStream()). 55 | // Default is false. 56 | defaultEnableZeroHeader: false, 57 | 58 | // Enable zip64 extension, allowing very large archives 59 | // (> 4Gb or file count > 64k) 60 | // Default is true 61 | enableZip64: true, 62 | 63 | // Flush output buffer after every write 64 | // Default is false 65 | flushOutput: true, 66 | ); 67 | -------------------------------------------------------------------------------- /guides/PSR7Streams.rst: -------------------------------------------------------------------------------- 1 | Usage with PSR 7 Streams 2 | =============== 3 | 4 | PSR-7 streams are `standardized streams `_. 5 | 6 | ZipStream-PHP supports working with these streams with the function 7 | ``addFileFromPsr7Stream``. 8 | 9 | For all parameters of the function see the API documentation. 10 | 11 | Example 12 | --------------- 13 | 14 | .. code-block:: php 15 | 16 | $stream = $response->getBody(); 17 | // add a file named 'streamfile.txt' from the content of the stream 18 | $zip->addFileFromPsr7Stream( 19 | fileName: 'streamfile.txt', 20 | stream: $stream, 21 | ); 22 | -------------------------------------------------------------------------------- /guides/StreamOutput.rst: -------------------------------------------------------------------------------- 1 | Stream Output 2 | =============== 3 | 4 | Stream to S3 Bucket 5 | --------------- 6 | 7 | .. code-block:: php 8 | 9 | use Aws\S3\S3Client; 10 | use Aws\Credentials\CredentialProvider; 11 | use ZipStream\ZipStream; 12 | 13 | $bucket = 'your bucket name'; 14 | $client = new S3Client([ 15 | 'region' => 'your region', 16 | 'version' => 'latest', 17 | 'bucketName' => $bucket, 18 | 'credentials' => CredentialProvider::defaultProvider(), 19 | ]); 20 | $client->registerStreamWrapper(); 21 | 22 | $zipFile = fopen("s3://$bucket/example.zip", 'w'); 23 | 24 | $zip = new ZipStream( 25 | enableZip64: false, 26 | outputStream: $zipFile, 27 | ); 28 | 29 | $zip->addFile( 30 | fileName: 'file1.txt', 31 | data: 'File1 data', 32 | ); 33 | $zip->addFile( 34 | fileName: 'file2.txt', 35 | data: 'File2 data', 36 | ); 37 | $zip->finish(); 38 | 39 | fclose($zipFile); 40 | -------------------------------------------------------------------------------- /guides/Symfony.rst: -------------------------------------------------------------------------------- 1 | Usage with Symfony 2 | =============== 3 | 4 | Overview for using ZipStream in Symfony 5 | -------- 6 | 7 | Using ZipStream in Symfony requires use of Symfony's ``StreamedResponse`` when 8 | used in controller actions. 9 | 10 | Wrap your call to the relevant ``ZipStream`` stream method (i.e. ``addFile``, 11 | ``addFileFromPath``, ``addFileFromStream``) in Symfony's ``StreamedResponse`` 12 | function passing in any required arguments for your use case. 13 | 14 | Using Symfony's ``StreamedResponse`` will allow Symfony to stream output from 15 | ZipStream correctly to users' browsers and avoid a corrupted final zip landing 16 | on the users' end. 17 | 18 | Example for using ``ZipStream`` in a controller action to zip stream files 19 | stored in an AWS S3 bucket by key: 20 | 21 | .. code-block:: php 22 | 23 | use Symfony\Component\HttpFoundation\StreamedResponse; 24 | use Aws\S3\S3Client; 25 | use ZipStream; 26 | 27 | //... 28 | 29 | /** 30 | * @Route("/zipstream", name="zipstream") 31 | */ 32 | public function zipStreamAction() 33 | { 34 | // sample test file on s3 35 | $s3keys = array( 36 | "ziptestfolder/file1.txt" 37 | ); 38 | 39 | $s3Client = $this->get('app.amazon.s3'); //s3client service 40 | $s3Client->registerStreamWrapper(); //required 41 | 42 | // using StreamedResponse to wrap ZipStream functionality 43 | // for files on AWS s3. 44 | $response = new StreamedResponse(function() use($s3keys, $s3Client) 45 | { 46 | // Define suitable options for ZipStream Archive. 47 | // this is needed to prevent issues with truncated zip files 48 | //initialise zipstream with output zip filename and options. 49 | $zip = new ZipStream\ZipStream( 50 | outputName: 'test.zip', 51 | defaultEnableZeroHeader: true, 52 | contentType: 'application/octet-stream', 53 | ); 54 | 55 | //loop keys - useful for multiple files 56 | foreach ($s3keys as $key) { 57 | // Get the file name in S3 key so we can save it to the zip 58 | //file using the same name. 59 | $fileName = basename($key); 60 | 61 | // concatenate s3path. 62 | // replace with your bucket name or get from parameters file. 63 | $bucket = 'bucketname'; 64 | $s3path = "s3://" . $bucket . "/" . $key; 65 | 66 | //addFileFromStream 67 | if ($streamRead = fopen($s3path, 'r')) { 68 | $zip->addFileFromStream( 69 | fileName: $fileName, 70 | stream: $streamRead, 71 | ); 72 | } else { 73 | die('Could not open stream for reading'); 74 | } 75 | } 76 | 77 | $zip->finish(); 78 | 79 | }); 80 | 81 | return $response; 82 | } 83 | 84 | In the above example, files on AWS S3 are being streamed from S3 to the Symfon 85 | application via ``fopen`` call when the s3Client has ``registerStreamWrapper`` 86 | applied. This stream is then passed to ``ZipStream`` via the 87 | ``addFileFromStream`` function, which ZipStream then streams as a zip to the 88 | client browser via Symfony's ``StreamedResponse``. No Zip is created server 89 | side, which makes this approach a more efficient solution for streaming zips to 90 | the client browser especially for larger files. 91 | 92 | For the above use case you will need to have installed 93 | `aws/aws-sdk-php-symfony `_ to 94 | support accessing S3 objects in your Symfony web application. This is not 95 | required for locally stored files on you server you intend to stream via 96 | ``ZipStream``. 97 | 98 | See official Symfony documentation for details on 99 | `Symfony's StreamedResponse `_ 100 | ``Symfony\Component\HttpFoundation\StreamedResponse``. 101 | 102 | Note from `S3 documentation `_: 103 | 104 | Streams opened in "r" mode only allow data to be read from the stream, and 105 | are not seekable by default. This is so that data can be downloaded from 106 | Amazon S3 in a truly streaming manner, where previously read bytes do not 107 | need to be buffered into memory. If you need a stream to be seekable, you 108 | can pass seekable into the stream context options of a function. 109 | 110 | Make sure to configure your S3 context correctly! 111 | 112 | Uploading a file 113 | -------- 114 | 115 | You need to add correct permissions 116 | (see `#120 `_) 117 | 118 | **example code** 119 | 120 | 121 | .. code-block:: php 122 | 123 | $path = "s3://{$adapter->getBucket()}/{$this->getArchivePath()}"; 124 | 125 | // the important bit 126 | $outputContext = stream_context_create([ 127 | 's3' => ['ACL' => 'public-read'], 128 | ]); 129 | 130 | fopen($path, 'w', null, $outputContext); 131 | -------------------------------------------------------------------------------- /guides/Varnish.rst: -------------------------------------------------------------------------------- 1 | Usage with Varnish 2 | ============= 3 | 4 | Serving a big zip with varnish in between can cause random stream close. 5 | This can be solved by adding attached code to the vcl file. 6 | 7 | To avoid the problem, add the following to your varnish config file: 8 | 9 | .. code-block:: 10 | sub vcl_recv { 11 | # Varnish can’t intercept the discussion anymore 12 | # helps for streaming big zips 13 | if (req.url ~ "\.(tar|gz|zip|7z|exe)$") { 14 | return (pipe); 15 | } 16 | } 17 | # Varnish can’t intercept the discussion anymore 18 | # helps for streaming big zips 19 | sub vcl_pipe { 20 | set bereq.http.connection = "close"; 21 | return (pipe); 22 | } 23 | -------------------------------------------------------------------------------- /guides/index.rst: -------------------------------------------------------------------------------- 1 | ZipStream PHP 2 | ============= 3 | 4 | A fast and simple streaming zip file downloader for PHP. Using this library will 5 | save you from having to write the Zip to disk. You can directly send it to the 6 | user, which is much faster. It can work with S3 buckets or any PSR7 Stream. 7 | 8 | .. toctree:: 9 | 10 | index 11 | Symfony 12 | Options 13 | StreamOutput 14 | FlySystem 15 | PSR7Streams 16 | Nginx 17 | Varnish 18 | ContentLength 19 | 20 | Installation 21 | --------------- 22 | 23 | Simply add a dependency on ``maennchen/zipstream-php`` to your project's 24 | ``composer.json`` file if you use Composer to manage the dependencies of your 25 | project. Use following command to add the package to your project's 26 | dependencies: 27 | 28 | .. code-block:: sh 29 | composer require maennchen/zipstream-php 30 | 31 | If you want to use``addFileFromPsr7Stream``` 32 | (``Psr\Http\Message\StreamInterface``) or use a stream instead of a 33 | ``resource`` as ``outputStream``, the following dependencies must be installed 34 | as well: 35 | 36 | .. code-block:: sh 37 | composer require psr/http-message guzzlehttp/psr7 38 | 39 | If ``composer install`` yields the following error, your installation is missing 40 | the `mbstring extension `_, 41 | either `install it `_ 42 | or run the following command: 43 | 44 | .. code-block:: 45 | Your requirements could not be resolved to an installable set of packages. 46 | 47 | Problem 1 48 | - Root composer.json requires PHP extension ext-mbstring * but it is 49 | missing from your system. Install or enable PHP's mbstrings extension. 50 | 51 | .. code-block:: sh 52 | composer require symfony/polyfill-mbstring 53 | 54 | Usage Intro 55 | --------------- 56 | 57 | Here's a simple example: 58 | 59 | .. code-block:: php 60 | 61 | // Autoload the dependencies 62 | require 'vendor/autoload.php'; 63 | 64 | // create a new zipstream object 65 | $zip = new ZipStream\ZipStream( 66 | outputName: 'example.zip', 67 | 68 | // enable output of HTTP headers 69 | sendHttpHeaders: true, 70 | ); 71 | 72 | // create a file named 'hello.txt' 73 | $zip->addFile( 74 | fileName: 'hello.txt', 75 | data: 'This is the contents of hello.txt', 76 | ); 77 | 78 | // add a file named 'some_image.jpg' from a local file 'path/to/image.jpg' 79 | $zip->addFileFromPath( 80 | fileName: 'some_image.jpg', 81 | path: 'path/to/image.jpg', 82 | ); 83 | 84 | // add a file named 'goodbye.txt' from an open stream resource 85 | $filePointer = tmpfile(); 86 | fwrite($filePointer, 'The quick brown fox jumped over the lazy dog.'); 87 | rewind($filePointer); 88 | $zip->addFileFromStream( 89 | fileName: 'goodbye.txt', 90 | stream: $filePointer, 91 | ); 92 | fclose($filePointer); 93 | 94 | // add a file named 'streamfile.txt' from the body of a `guzzle` response 95 | // Setup with `psr/http-message` & `guzzlehttp/psr7` dependencies required. 96 | $zip->addFileFromPsr7Stream( 97 | fileName: 'streamfile.txt', 98 | stream: $response->getBody(), 99 | ); 100 | 101 | // finish the zip stream 102 | $zip->finish(); 103 | 104 | You can also add comments, modify file timestamps, and customize (or 105 | disable) the HTTP headers. It is also possible to specify the storage method 106 | when adding files, the current default storage method is ``DEFLATE`` 107 | i.e files are stored with Compression mode 0x08. 108 | 109 | Known Issues 110 | --------------- 111 | 112 | The native Mac OS archive extraction tool prior to macOS 10.15 might not open 113 | archives in some conditions. A workaround is to disable the Zip64 feature with 114 | the option ``enableZip64: false``. This limits the archive to 4 Gb and 64k files 115 | but will allow users on macOS 10.14 and below to open them without issue. 116 | See `#116 `_. 117 | 118 | The linux ``unzip`` utility might not handle properly unicode characters. 119 | It is recommended to extract with another tool like 120 | `7-zip `_. 121 | See `#146 `_. 122 | 123 | It is the responsibility of the client code to make sure that files are not 124 | saved with the same path, as it is not possible for the library to figure it out 125 | while streaming a zip. 126 | See `#154 `_. 127 | -------------------------------------------------------------------------------- /phpdoc.dist.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 💾 ZipStream-PHP 9 | 10 | docs 11 | 12 | 13 | latest 14 | 15 | 16 | src 17 | 18 | api 19 | 23 | 24 | php 25 | 26 | public 27 | ZipStream 28 | true 29 | 30 | 31 | 32 | guides 33 | 34 | guide 35 | 36 | 37 | 38 |