| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871 |
- <?php
- declare(strict_types=1);
- namespace ZipStream;
- use Closure;
- use DateTimeImmutable;
- use DateTimeInterface;
- use GuzzleHttp\Psr7\StreamWrapper;
- use Psr\Http\Message\StreamInterface;
- use RuntimeException;
- use ZipStream\Exception\FileNotFoundException;
- use ZipStream\Exception\FileNotReadableException;
- use ZipStream\Exception\OverflowException;
- use ZipStream\Exception\ResourceActionException;
- /**
- * Streamed, dynamically generated zip archives.
- *
- * ## Usage
- *
- * Streaming zip archives is a simple, three-step process:
- *
- * 1. Create the zip stream:
- *
- * ```php
- * $zip = new ZipStream(outputName: 'example.zip');
- * ```
- *
- * 2. Add one or more files to the archive:
- *
- * ```php
- * // add first file
- * $zip->addFile(fileName: 'world.txt', data: 'Hello World');
- *
- * // add second file
- * $zip->addFile(fileName: 'moon.txt', data: 'Hello Moon');
- * ```
- *
- * 3. Finish the zip stream:
- *
- * ```php
- * $zip->finish();
- * ```
- *
- * You can also add an archive comment, add comments to individual files,
- * and adjust the timestamp of files. See the API documentation for each
- * method below for additional information.
- *
- * ## Example
- *
- * ```php
- * // create a new zip stream object
- * $zip = new ZipStream(outputName: 'some_files.zip');
- *
- * // list of local files
- * $files = array('foo.txt', 'bar.jpg');
- *
- * // read and add each file to the archive
- * foreach ($files as $path)
- * $zip->addFileFromPath(fileName: $path, $path);
- *
- * // write archive footer to stream
- * $zip->finish();
- * ```
- */
- class ZipStream
- {
- /**
- * This number corresponds to the ZIP version/OS used (2 bytes)
- * From: https://www.iana.org/assignments/media-types/application/zip
- * The upper byte (leftmost one) indicates the host system (OS) for the
- * file. Software can use this information to determine
- * the line record format for text files etc. The current
- * mappings are:
- *
- * 0 - MS-DOS and OS/2 (F.A.T. file systems)
- * 1 - Amiga 2 - VAX/VMS
- * 3 - *nix 4 - VM/CMS
- * 5 - Atari ST 6 - OS/2 H.P.F.S.
- * 7 - Macintosh 8 - Z-System
- * 9 - CP/M 10 thru 255 - unused
- *
- * The lower byte (rightmost one) indicates the version number of the
- * software used to encode the file. The value/10
- * indicates the major version number, and the value
- * mod 10 is the minor version number.
- * Here we are using 6 for the OS, indicating OS/2 H.P.F.S.
- * to prevent file permissions issues upon extract (see #84)
- * 0x603 is 00000110 00000011 in binary, so 6 and 3
- *
- * @internal
- */
- public const ZIP_VERSION_MADE_BY = 0x603;
- private bool $ready = true;
- private int $offset = 0;
- /**
- * @var string[]
- */
- private array $centralDirectoryRecords = [];
- /**
- * @var resource
- */
- private $outputStream;
- private readonly Closure $httpHeaderCallback;
- /**
- * @var File[]
- */
- private array $recordedSimulation = [];
- /**
- * Create a new ZipStream object.
- *
- * ##### Examples
- *
- * ```php
- * // create a new zip file named 'foo.zip'
- * $zip = new ZipStream(outputName: 'foo.zip');
- *
- * // create a new zip file named 'bar.zip' with a comment
- * $zip = new ZipStream(
- * outputName: 'bar.zip',
- * comment: 'this is a comment for the zip file.',
- * );
- * ```
- *
- * @param OperationMode $operationMode
- * The mode can be used to switch between `NORMAL` and `SIMULATION_*` modes.
- * For details see the `OperationMode` documentation.
- *
- * Default to `NORMAL`.
- *
- * @param string $comment
- * Archive Level Comment
- *
- * @param StreamInterface|resource|null $outputStream
- * Override the output of the archive to a different target.
- *
- * By default the archive is sent to `STDOUT`.
- *
- * @param CompressionMethod $defaultCompressionMethod
- * How to handle file compression. Legal values are
- * `CompressionMethod::DEFLATE` (the default), or
- * `CompressionMethod::STORE`. `STORE` sends the file raw and is
- * significantly faster, while `DEFLATE` compresses the file and
- * is much, much slower.
- *
- * @param int $defaultDeflateLevel
- * Default deflation level. Only relevant if `compressionMethod`
- * is `DEFLATE`.
- *
- * See details of [`deflate_init`](https://www.php.net/manual/en/function.deflate-init.php#refsect1-function.deflate-init-parameters)
- *
- * @param bool $enableZip64
- * Enable Zip64 extension, supporting very large
- * archives (any size > 4 GB or file count > 64k)
- *
- * @param bool $defaultEnableZeroHeader
- * Enable streaming files with single read.
- *
- * When the zero header is set, the file is streamed into the output
- * and the size & checksum are added at the end of the file. This is the
- * fastest method and uses the least memory. Unfortunately not all
- * ZIP clients fully support this and can lead to clients reporting
- * the generated ZIP files as corrupted in combination with other
- * circumstances. (Zip64 enabled, using UTF8 in comments / names etc.)
- *
- * When the zero header is not set, the length & checksum need to be
- * defined before the file is actually added. To prevent loading all
- * the data into memory, the data has to be read twice. If the data
- * which is added is not seekable, this call will fail.
- *
- * @param bool $sendHttpHeaders
- * Boolean indicating whether or not to send
- * the HTTP headers for this file.
- *
- * @param ?Closure $httpHeaderCallback
- * The method called to send HTTP headers
- *
- * @param string|null $outputName
- * The name of the created archive.
- *
- * Only relevant if `$sendHttpHeaders = true`.
- *
- * @param string $contentDisposition
- * HTTP Content-Disposition
- *
- * Only relevant if `sendHttpHeaders = true`.
- *
- * @param string $contentType
- * HTTP Content Type
- *
- * Only relevant if `sendHttpHeaders = true`.
- *
- * @param bool $flushOutput
- * Enable flush after every write to output stream.
- *
- * @return self
- */
- public function __construct(
- private OperationMode $operationMode = OperationMode::NORMAL,
- private readonly string $comment = '',
- $outputStream = null,
- private readonly CompressionMethod $defaultCompressionMethod = CompressionMethod::DEFLATE,
- private readonly int $defaultDeflateLevel = 6,
- private readonly bool $enableZip64 = true,
- private readonly bool $defaultEnableZeroHeader = true,
- private bool $sendHttpHeaders = true,
- ?Closure $httpHeaderCallback = null,
- private readonly ?string $outputName = null,
- private readonly string $contentDisposition = 'attachment',
- private readonly string $contentType = 'application/x-zip',
- private bool $flushOutput = false,
- ) {
- $this->outputStream = self::normalizeStream($outputStream);
- $this->httpHeaderCallback = $httpHeaderCallback ?? header(...);
- }
- /**
- * Add a file to the archive.
- *
- * ##### File Options
- *
- * See {@see addFileFromPsr7Stream()}
- *
- * ##### Examples
- *
- * ```php
- * // add a file named 'world.txt'
- * $zip->addFile(fileName: 'world.txt', data: 'Hello World!');
- *
- * // add a file named 'bar.jpg' with a comment and a last-modified
- * // time of two hours ago
- * $zip->addFile(
- * fileName: 'bar.jpg',
- * data: $data,
- * comment: 'this is a comment about bar.jpg',
- * lastModificationDateTime: new DateTime('2 hours ago'),
- * );
- * ```
- *
- * @param string $data
- *
- * contents of file
- */
- public function addFile(
- string $fileName,
- string $data,
- string $comment = '',
- ?CompressionMethod $compressionMethod = null,
- ?int $deflateLevel = null,
- ?DateTimeInterface $lastModificationDateTime = null,
- ?int $maxSize = null,
- ?int $exactSize = null,
- ?bool $enableZeroHeader = null,
- ): void {
- $this->addFileFromCallback(
- fileName: $fileName,
- callback: fn() => $data,
- comment: $comment,
- compressionMethod: $compressionMethod,
- deflateLevel: $deflateLevel,
- lastModificationDateTime: $lastModificationDateTime,
- maxSize: $maxSize,
- exactSize: $exactSize,
- enableZeroHeader: $enableZeroHeader,
- );
- }
- /**
- * Add a file at path to the archive.
- *
- * ##### File Options
- *
- * See {@see addFileFromPsr7Stream()}
- *
- * ###### Examples
- *
- * ```php
- * // add a file named 'foo.txt' from the local file '/tmp/foo.txt'
- * $zip->addFileFromPath(
- * fileName: 'foo.txt',
- * path: '/tmp/foo.txt',
- * );
- *
- * // add a file named 'bigfile.rar' from the local file
- * // '/usr/share/bigfile.rar' with a comment and a last-modified
- * // time of two hours ago
- * $zip->addFileFromPath(
- * fileName: 'bigfile.rar',
- * path: '/usr/share/bigfile.rar',
- * comment: 'this is a comment about bigfile.rar',
- * lastModificationDateTime: new DateTime('2 hours ago'),
- * );
- * ```
- *
- * @throws \ZipStream\Exception\FileNotFoundException
- * @throws \ZipStream\Exception\FileNotReadableException
- */
- public function addFileFromPath(
- /**
- * name of file in archive (including directory path).
- */
- string $fileName,
- /**
- * path to file on disk (note: paths should be encoded using
- * UNIX-style forward slashes -- e.g '/path/to/some/file').
- */
- string $path,
- string $comment = '',
- ?CompressionMethod $compressionMethod = null,
- ?int $deflateLevel = null,
- ?DateTimeInterface $lastModificationDateTime = null,
- ?int $maxSize = null,
- ?int $exactSize = null,
- ?bool $enableZeroHeader = null,
- ): void {
- if (!is_readable($path)) {
- if (!file_exists($path)) {
- throw new FileNotFoundException($path);
- }
- throw new FileNotReadableException($path);
- }
- $fileTime = filemtime($path);
- if ($fileTime !== false) {
- $lastModificationDateTime ??= (new DateTimeImmutable())->setTimestamp($fileTime);
- }
- $this->addFileFromCallback(
- fileName: $fileName,
- callback: function () use ($path) {
- $stream = fopen($path, 'rb');
- if (!$stream) {
- // @codeCoverageIgnoreStart
- throw new ResourceActionException('fopen');
- // @codeCoverageIgnoreEnd
- }
- return $stream;
- },
- comment: $comment,
- compressionMethod: $compressionMethod,
- deflateLevel: $deflateLevel,
- lastModificationDateTime: $lastModificationDateTime,
- maxSize: $maxSize,
- exactSize: $exactSize,
- enableZeroHeader: $enableZeroHeader,
- );
- }
- /**
- * Add an open stream (resource) to the archive.
- *
- * ##### File Options
- *
- * See {@see addFileFromPsr7Stream()}
- *
- * ##### Examples
- *
- * ```php
- * // create a temporary file stream and write text to it
- * $filePointer = tmpfile();
- * fwrite($filePointer, 'The quick brown fox jumped over the lazy dog.');
- *
- * // add a file named 'streamfile.txt' from the content of the stream
- * $archive->addFileFromStream(
- * fileName: 'streamfile.txt',
- * stream: $filePointer,
- * );
- * ```
- *
- * @param resource $stream contents of file as a stream resource
- */
- public function addFileFromStream(
- string $fileName,
- $stream,
- string $comment = '',
- ?CompressionMethod $compressionMethod = null,
- ?int $deflateLevel = null,
- ?DateTimeInterface $lastModificationDateTime = null,
- ?int $maxSize = null,
- ?int $exactSize = null,
- ?bool $enableZeroHeader = null,
- ): void {
- $this->addFileFromCallback(
- fileName: $fileName,
- callback: fn() => $stream,
- comment: $comment,
- compressionMethod: $compressionMethod,
- deflateLevel: $deflateLevel,
- lastModificationDateTime: $lastModificationDateTime,
- maxSize: $maxSize,
- exactSize: $exactSize,
- enableZeroHeader: $enableZeroHeader,
- );
- }
- /**
- * Add an open stream to the archive.
- *
- * ##### Examples
- *
- * ```php
- * $stream = $response->getBody();
- * // add a file named 'streamfile.txt' from the content of the stream
- * $archive->addFileFromPsr7Stream(
- * fileName: 'streamfile.txt',
- * stream: $stream,
- * );
- * ```
- *
- * @param string $fileName
- * path of file in archive (including directory)
- *
- * @param StreamInterface $stream
- * contents of file as a stream resource
- *
- * @param string $comment
- * ZIP comment for this file
- *
- * @param ?CompressionMethod $compressionMethod
- * Override `defaultCompressionMethod`
- *
- * See {@see __construct()}
- *
- * @param ?int $deflateLevel
- * Override `defaultDeflateLevel`
- *
- * See {@see __construct()}
- *
- * @param ?DateTimeInterface $lastModificationDateTime
- * Set last modification time of file.
- *
- * Default: `now`
- *
- * @param ?int $maxSize
- * Only read `maxSize` bytes from file.
- *
- * The file is considered done when either reaching `EOF`
- * or the `maxSize`.
- *
- * @param ?int $exactSize
- * Read exactly `exactSize` bytes from file.
- * If `EOF` is reached before reading `exactSize` bytes, an error will be
- * thrown. The parameter allows for faster size calculations if the `stream`
- * does not support `fstat` size or is slow and otherwise known beforehand.
- *
- * @param ?bool $enableZeroHeader
- * Override `defaultEnableZeroHeader`
- *
- * See {@see __construct()}
- */
- public function addFileFromPsr7Stream(
- string $fileName,
- StreamInterface $stream,
- string $comment = '',
- ?CompressionMethod $compressionMethod = null,
- ?int $deflateLevel = null,
- ?DateTimeInterface $lastModificationDateTime = null,
- ?int $maxSize = null,
- ?int $exactSize = null,
- ?bool $enableZeroHeader = null,
- ): void {
- $this->addFileFromCallback(
- fileName: $fileName,
- callback: fn() => $stream,
- comment: $comment,
- compressionMethod: $compressionMethod,
- deflateLevel: $deflateLevel,
- lastModificationDateTime: $lastModificationDateTime,
- maxSize: $maxSize,
- exactSize: $exactSize,
- enableZeroHeader: $enableZeroHeader,
- );
- }
- /**
- * Add a file based on a callback.
- *
- * This is useful when you want to simulate a lot of files without keeping
- * all of the file handles open at the same time.
- *
- * ##### Examples
- *
- * ```php
- * foreach($files as $name => $size) {
- * $archive->addFileFromCallback(
- * fileName: 'streamfile.txt',
- * exactSize: $size,
- * callback: function() use($name): Psr\Http\Message\StreamInterface {
- * $response = download($name);
- * return $response->getBody();
- * }
- * );
- * }
- * ```
- *
- * @param string $fileName
- * path of file in archive (including directory)
- *
- * @param Closure $callback
- * @psalm-param Closure(): (resource|StreamInterface|string) $callback
- * A callback to get the file contents in the shape of a PHP stream,
- * a Psr StreamInterface implementation, or a string.
- *
- * @param string $comment
- * ZIP comment for this file
- *
- * @param ?CompressionMethod $compressionMethod
- * Override `defaultCompressionMethod`
- *
- * See {@see __construct()}
- *
- * @param ?int $deflateLevel
- * Override `defaultDeflateLevel`
- *
- * See {@see __construct()}
- *
- * @param ?DateTimeInterface $lastModificationDateTime
- * Set last modification time of file.
- *
- * Default: `now`
- *
- * @param ?int $maxSize
- * Only read `maxSize` bytes from file.
- *
- * The file is considered done when either reaching `EOF`
- * or the `maxSize`.
- *
- * @param ?int $exactSize
- * Read exactly `exactSize` bytes from file.
- * If `EOF` is reached before reading `exactSize` bytes, an error will be
- * thrown. The parameter allows for faster size calculations if the `stream`
- * does not support `fstat` size or is slow and otherwise known beforehand.
- *
- * @param ?bool $enableZeroHeader
- * Override `defaultEnableZeroHeader`
- *
- * See {@see __construct()}
- */
- public function addFileFromCallback(
- string $fileName,
- Closure $callback,
- string $comment = '',
- ?CompressionMethod $compressionMethod = null,
- ?int $deflateLevel = null,
- ?DateTimeInterface $lastModificationDateTime = null,
- ?int $maxSize = null,
- ?int $exactSize = null,
- ?bool $enableZeroHeader = null,
- ): void {
- $file = new File(
- dataCallback: function () use ($callback, $maxSize) {
- $data = $callback();
- if (is_resource($data)) {
- return $data;
- }
- if ($data instanceof StreamInterface) {
- return StreamWrapper::getResource($data);
- }
- $stream = fopen('php://memory', 'rw+');
- if ($stream === false) {
- // @codeCoverageIgnoreStart
- throw new ResourceActionException('fopen');
- // @codeCoverageIgnoreEnd
- }
- if ($maxSize !== null && fwrite($stream, $data, $maxSize) === false) {
- // @codeCoverageIgnoreStart
- throw new ResourceActionException('fwrite', $stream);
- // @codeCoverageIgnoreEnd
- } elseif (fwrite($stream, $data) === false) {
- // @codeCoverageIgnoreStart
- throw new ResourceActionException('fwrite', $stream);
- // @codeCoverageIgnoreEnd
- }
- if (rewind($stream) === false) {
- // @codeCoverageIgnoreStart
- throw new ResourceActionException('rewind', $stream);
- // @codeCoverageIgnoreEnd
- }
- return $stream;
- },
- send: $this->send(...),
- recordSentBytes: $this->recordSentBytes(...),
- operationMode: $this->operationMode,
- fileName: $fileName,
- startOffset: $this->offset,
- compressionMethod: $compressionMethod ?? $this->defaultCompressionMethod,
- comment: $comment,
- deflateLevel: $deflateLevel ?? $this->defaultDeflateLevel,
- lastModificationDateTime: $lastModificationDateTime ?? new DateTimeImmutable(),
- maxSize: $maxSize,
- exactSize: $exactSize,
- enableZip64: $this->enableZip64,
- enableZeroHeader: $enableZeroHeader ?? $this->defaultEnableZeroHeader,
- );
- if ($this->operationMode !== OperationMode::NORMAL) {
- $this->recordedSimulation[] = $file;
- }
- $this->centralDirectoryRecords[] = $file->process();
- }
- /**
- * Add a directory to the archive.
- *
- * ##### File Options
- *
- * See {@see addFileFromPsr7Stream()}
- *
- * ##### Examples
- *
- * ```php
- * // add a directory named 'world/'
- * $zip->addDirectory(fileName: 'world/');
- * ```
- */
- public function addDirectory(
- string $fileName,
- string $comment = '',
- ?DateTimeInterface $lastModificationDateTime = null,
- ): void {
- if (!str_ends_with($fileName, '/')) {
- $fileName .= '/';
- }
- $this->addFile(
- fileName: $fileName,
- data: '',
- comment: $comment,
- compressionMethod: CompressionMethod::STORE,
- deflateLevel: null,
- lastModificationDateTime: $lastModificationDateTime,
- maxSize: 0,
- exactSize: 0,
- enableZeroHeader: false,
- );
- }
- /**
- * Executes a previously calculated simulation.
- *
- * ##### Example
- *
- * ```php
- * $zip = new ZipStream(
- * outputName: 'foo.zip',
- * operationMode: OperationMode::SIMULATE_STRICT,
- * );
- *
- * $zip->addFile('test.txt', 'Hello World');
- *
- * $size = $zip->finish();
- *
- * header('Content-Length: '. $size);
- *
- * $zip->executeSimulation();
- * ```
- */
- public function executeSimulation(): void
- {
- if ($this->operationMode !== OperationMode::NORMAL) {
- throw new RuntimeException('Zip simulation is not finished.');
- }
- foreach ($this->recordedSimulation as $file) {
- $this->centralDirectoryRecords[] = $file->cloneSimulationExecution()->process();
- }
- $this->finish();
- }
- /**
- * Write zip footer to stream.
- *
- * The clase is left in an unusable state after `finish`.
- *
- * ##### Example
- *
- * ```php
- * // write footer to stream
- * $zip->finish();
- * ```
- */
- public function finish(): int
- {
- $centralDirectoryStartOffsetOnDisk = $this->offset;
- $sizeOfCentralDirectory = 0;
- // add trailing cdr file records
- foreach ($this->centralDirectoryRecords as $centralDirectoryRecord) {
- $this->send($centralDirectoryRecord);
- $sizeOfCentralDirectory += strlen($centralDirectoryRecord);
- }
- // Add 64bit headers (if applicable)
- if (count($this->centralDirectoryRecords) >= 0xFFFF ||
- $centralDirectoryStartOffsetOnDisk > 0xFFFFFFFF ||
- $sizeOfCentralDirectory > 0xFFFFFFFF) {
- if (!$this->enableZip64) {
- throw new OverflowException();
- }
- $this->send(Zip64\EndOfCentralDirectory::generate(
- versionMadeBy: self::ZIP_VERSION_MADE_BY,
- versionNeededToExtract: Version::ZIP64->value,
- numberOfThisDisk: 0,
- numberOfTheDiskWithCentralDirectoryStart: 0,
- numberOfCentralDirectoryEntriesOnThisDisk: count($this->centralDirectoryRecords),
- numberOfCentralDirectoryEntries: count($this->centralDirectoryRecords),
- sizeOfCentralDirectory: $sizeOfCentralDirectory,
- centralDirectoryStartOffsetOnDisk: $centralDirectoryStartOffsetOnDisk,
- extensibleDataSector: '',
- ));
- $this->send(Zip64\EndOfCentralDirectoryLocator::generate(
- numberOfTheDiskWithZip64CentralDirectoryStart: 0x00,
- zip64centralDirectoryStartOffsetOnDisk: $centralDirectoryStartOffsetOnDisk + $sizeOfCentralDirectory,
- totalNumberOfDisks: 1,
- ));
- }
- // add trailing cdr eof record
- $numberOfCentralDirectoryEntries = min(count($this->centralDirectoryRecords), 0xFFFF);
- $this->send(EndOfCentralDirectory::generate(
- numberOfThisDisk: 0x00,
- numberOfTheDiskWithCentralDirectoryStart: 0x00,
- numberOfCentralDirectoryEntriesOnThisDisk: $numberOfCentralDirectoryEntries,
- numberOfCentralDirectoryEntries: $numberOfCentralDirectoryEntries,
- sizeOfCentralDirectory: min($sizeOfCentralDirectory, 0xFFFFFFFF),
- centralDirectoryStartOffsetOnDisk: min($centralDirectoryStartOffsetOnDisk, 0xFFFFFFFF),
- zipFileComment: $this->comment,
- ));
- $size = $this->offset;
- // The End
- $this->clear();
- return $size;
- }
- /**
- * @param StreamInterface|resource|null $outputStream
- * @return resource
- */
- private static function normalizeStream($outputStream)
- {
- if ($outputStream instanceof StreamInterface) {
- return StreamWrapper::getResource($outputStream);
- }
- if (is_resource($outputStream)) {
- return $outputStream;
- }
- $resource = fopen('php://output', 'wb');
- if ($resource === false) {
- throw new RuntimeException('fopen of php://output failed');
- }
- return $resource;
- }
- /**
- * Record sent bytes
- */
- private function recordSentBytes(int $sentBytes): void
- {
- $this->offset += $sentBytes;
- }
- /**
- * Send string, sending HTTP headers if necessary.
- * Flush output after write if configure option is set.
- */
- private function send(string $data): void
- {
- if (!$this->ready) {
- throw new RuntimeException('Archive is already finished');
- }
- if ($this->operationMode === OperationMode::NORMAL && $this->sendHttpHeaders) {
- $this->sendHttpHeaders();
- $this->sendHttpHeaders = false;
- }
- $this->recordSentBytes(strlen($data));
- if ($this->operationMode === OperationMode::NORMAL) {
- if (fwrite($this->outputStream, $data) === false) {
- throw new ResourceActionException('fwrite', $this->outputStream);
- }
- if ($this->flushOutput) {
- // flush output buffer if it is on and flushable
- $status = ob_get_status();
- if (isset($status['flags']) && is_int($status['flags']) && ($status['flags'] & PHP_OUTPUT_HANDLER_FLUSHABLE)) {
- ob_flush();
- }
- // Flush system buffers after flushing userspace output buffer
- flush();
- }
- }
- }
- /**
- * Send HTTP headers for this stream.
- */
- private function sendHttpHeaders(): void
- {
- // grab content disposition
- $disposition = $this->contentDisposition;
- if ($this->outputName !== null) {
- // Various different browsers dislike various characters here. Strip them all for safety.
- $safeOutput = trim(str_replace(['"', "'", '\\', ';', "\n", "\r"], '', $this->outputName));
- // Check if we need to UTF-8 encode the filename
- $urlencoded = rawurlencode($safeOutput);
- $disposition .= "; filename*=UTF-8''{$urlencoded}";
- }
- $headers = [
- 'Content-Type' => $this->contentType,
- 'Content-Disposition' => $disposition,
- 'Pragma' => 'public',
- 'Cache-Control' => 'public, must-revalidate',
- 'Content-Transfer-Encoding' => 'binary',
- ];
- foreach ($headers as $key => $val) {
- ($this->httpHeaderCallback)("$key: $val");
- }
- }
- /**
- * Clear all internal variables. Note that the stream object is not
- * usable after this.
- */
- private function clear(): void
- {
- $this->centralDirectoryRecords = [];
- $this->offset = 0;
- if ($this->operationMode === OperationMode::NORMAL) {
- $this->ready = false;
- $this->recordedSimulation = [];
- } else {
- $this->operationMode = OperationMode::NORMAL;
- }
- }
- }
|