Skip to content

Commit c9a2e4f

Browse files
authored
Calculate ZIP Size (#241)
Calculates the resulting file size with the minimal amount of work that is required. Fixes #89
1 parent ca4e123 commit c9a2e4f

8 files changed

+863
-160
lines changed

guides/ContentLength.rst

Lines changed: 36 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,47 @@
11
Adding Content-Length header
22
=============
33

4-
Adding a ``Content-Length`` header for ``ZipStream`` is not trivial since the
5-
size is not known beforehand.
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.
67

7-
The following workaround adds an approximated header:
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 neccessary.
811

9-
.. code-block:: php
12+
``SIMULATION_STRICT`` is therefore useful to make sure that the size can be
13+
calculated efficiently.
1014

11-
use ZipStream\CompressionMethod;
15+
.. code-block:: php
16+
use ZipStream\OperationMode;
1217
use ZipStream\ZipStream;
1318
14-
class Zip
15-
{
16-
private $files = [];
17-
18-
public function __construct(
19-
private readonly string $name
20-
) { }
21-
22-
public function addFile(
23-
string $name,
24-
string $data,
25-
): void {
26-
$this->files[] = ['type' => 'addFile', 'name' => $name, 'data' => $data];
27-
}
28-
29-
public function addFileFromPath(
30-
string $name,
31-
string $path,
32-
): void {
33-
$this->files[] = ['type' => 'addFileFromPath', 'name' => $name, 'path' => $path];
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('...');
3436
}
37+
);
3538
36-
public function getEstimate(): int {
37-
$estimate = 22;
38-
foreach ($this->files as $file) {
39-
$estimate += 76 + 2 * strlen($file['name']);
40-
if ($file['type'] === 'addFile') {
41-
$estimate += strlen($file['data']);
42-
}
43-
if ($file['type'] === 'addFileFromPath') {
44-
$estimate += filesize($file['path']);
45-
}
46-
}
47-
return $estimate;
48-
}
49-
50-
public function finish()
51-
{
52-
header('Content-Length: ' . $this->getEstimate());
53-
$zip = new ZipStream(
54-
outputName: $this->name,
55-
SendHttpHeaders: true,
56-
enableZip64: false,
57-
defaultCompressionMethod: CompressionMethod::STORE,
58-
);
59-
60-
foreach ($this->files as $file) {
61-
if ($file['type'] === 'addFile') {
62-
$zip->addFile(
63-
fileName: $file['name'],
64-
data: $file['data'],
65-
);
66-
}
67-
if ($file['type'] === 'addFileFromPath') {
68-
$zip->addFileFromPath(
69-
fileName: $file['name'],
70-
path: $file['path'],
71-
);
72-
}
73-
}
74-
$zip->finish();
75-
}
76-
}
77-
78-
It only works with the following constraints:
79-
80-
- All file content is known beforehand.
81-
- Content Deflation is disabled
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();
8247
83-
Thanks to
84-
`partiellkorrekt <https://github.com/maennchen/ZipStream-PHP/issues/89#issuecomment-1047949274>`_
85-
for this workaround.

psalm.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@
1818
<!-- Turn off dead code warnings for externally called functions -->
1919
<PossiblyUnusedProperty errorLevel="suppress" />
2020
<PossiblyUnusedMethod errorLevel="suppress" />
21+
<PossiblyUnusedReturnValue errorLevel="suppress" />
2122
</issueHandlers>
2223
</psalm>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ZipStream\Exception;
6+
7+
use ZipStream\Exception;
8+
9+
/**
10+
* This Exception gets invoked if a file is not as large as it was specified.
11+
*/
12+
class FileSizeIncorrectException extends Exception
13+
{
14+
/**
15+
* @internal
16+
*/
17+
public function __construct(
18+
public readonly int $expectedSize,
19+
public readonly int $actualSize
20+
) {
21+
parent::__construct("File is {$actualSize} instead of {$expectedSize} bytes large. Adjust `exactSize` parameter.");
22+
}
23+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ZipStream\Exception;
6+
7+
use ZipStream\Exception;
8+
9+
/**
10+
* This Exception gets invoked if a strict simulation is executed and the file
11+
* information can't be determined without reading the entire file.
12+
*/
13+
class SimulationFileUnknownException extends Exception
14+
{
15+
public function __construct()
16+
{
17+
parent::__construct('The details of the strict simulation file could not be determined without reading the entire file.');
18+
}
19+
}

0 commit comments

Comments
 (0)