mirror of
https://github.com/DirectoryLister/DirectoryLister.git
synced 2025-08-20 12:51:30 +02:00
Move to ZipStream integrated size calculation
This commit is contained in:
@@ -5,15 +5,16 @@ namespace App\Controllers;
|
||||
use App\CallbackStream;
|
||||
use App\Config;
|
||||
use App\Support\Str;
|
||||
use Exception;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use RuntimeException;
|
||||
use Slim\Psr7\Request;
|
||||
use Slim\Psr7\Response;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use ZipStream\Option\Archive;
|
||||
use ZipStream\Option\File;
|
||||
use ZipStream\Option\Method;
|
||||
use ZipStream\CompressionMethod;
|
||||
use ZipStream\OperationMode;
|
||||
use ZipStream\ZipStream;
|
||||
|
||||
class ZipController
|
||||
@@ -25,7 +26,11 @@ class ZipController
|
||||
private TranslatorInterface $translator
|
||||
) {}
|
||||
|
||||
/** Invoke the ZipHandler. */
|
||||
/** Invoke the ZipHandler.
|
||||
* @throws \ZipStream\Exception\FileNotFoundException
|
||||
* @throws \ZipStream\Exception\FileNotReadableException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __invoke(Request $request, Response $response): ResponseInterface
|
||||
{
|
||||
$path = $request->getQueryParams()['zip'];
|
||||
@@ -44,144 +49,63 @@ class ZipController
|
||||
|
||||
$files = $this->finder->in($path)->files();
|
||||
|
||||
$response = $this->augmentHeadersWithEstimatedSize($response, $path, $files);
|
||||
$zip = $this->createZip($path, $files);
|
||||
$size = $zip->finish();
|
||||
|
||||
return $response->withBody(new CallbackStream(function () use ($path, $files) {
|
||||
$this->createZip($path, $files);
|
||||
}));
|
||||
$response = $this
|
||||
->augmentHeadersWithEstimatedSize($response, $size)
|
||||
->withBody(new CallbackStream(function () use ($zip) {
|
||||
$zip->executeSimulation();
|
||||
}));
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/** Create a zip stream from a directory.
|
||||
*
|
||||
* @throws \ZipStream\Exception\FileNotFoundException
|
||||
* @throws \ZipStream\Exception\FileNotReadableException
|
||||
* @throws \ZipStream\Exception\OverflowException
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function createZip(string $path, Finder $files): void
|
||||
protected function createZip(string $path, Finder $files): ZipStream
|
||||
{
|
||||
$compressionMethod = $this->config->get('zip_compress') ? Method::DEFLATE() : Method::STORE();
|
||||
$compressionMethod = $this->config->get('zip_compress') ? CompressionMethod::DEFLATE : CompressionMethod::STORE;
|
||||
|
||||
$zipStreamOptions = new Archive;
|
||||
$zipStreamOptions->setLargeFileMethod($compressionMethod);
|
||||
$zipStreamOptions->setSendHttpHeaders(false);
|
||||
$zipStreamOptions->setFlushOutput(true);
|
||||
$zipStreamOptions->setEnableZip64(true);
|
||||
|
||||
$zip = new ZipStream(null, $zipStreamOptions);
|
||||
$zip = new ZipStream(
|
||||
sendHttpHeaders: false,
|
||||
operationMode: OperationMode::SIMULATE_LAX
|
||||
);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$fileOption = new File;
|
||||
$fileOption->setMethod($compressionMethod);
|
||||
$fileOption->setSize($file->getSize());
|
||||
$modifiedTime = null;
|
||||
|
||||
try {
|
||||
$creationTime = (int) $file->getMTime();
|
||||
$fileOption->setTime(new \DateTime('@' . $creationTime));
|
||||
} catch (\Exception $e) {
|
||||
// We couldn't get the creation time, so we don't set it
|
||||
$modifiedTime = new \DateTime('@' . (int) $file->getMTime());
|
||||
} catch (RuntimeException $e) {
|
||||
$lstat_data = lstat($file->getPathname());
|
||||
if ($lstat_data) {
|
||||
$modifiedTime = new \DateTime('@' . (int) ['mtime']);
|
||||
}
|
||||
}
|
||||
$zip->addFileFromPath($this->stripPath($file, $path), (string) $file->getRealPath(), $fileOption);
|
||||
$zip->addFileFromPath(
|
||||
$this->stripPath($file, $path),
|
||||
(string) $file->getRealPath(),
|
||||
compressionMethod: $compressionMethod,
|
||||
lastModificationDateTime: $modifiedTime,
|
||||
exactSize: $file->getSize()
|
||||
);
|
||||
}
|
||||
|
||||
$zip->finish();
|
||||
return $zip;
|
||||
}
|
||||
|
||||
protected function augmentHeadersWithEstimatedSize(Response $response, string $path, Finder $files): Response
|
||||
protected function augmentHeadersWithEstimatedSize(Response $response, int $size): Response
|
||||
{
|
||||
if (! $this->config->get('zip_compress')) {
|
||||
$totalSize = 0;
|
||||
$hasUtf8 = false;
|
||||
$filesMeta = [];
|
||||
foreach ($files as $file) {
|
||||
$fileSize = $file->getSize();
|
||||
$totalSize += $fileSize;
|
||||
$fileName = $this->filterFilename($this->stripPath($file, $path));
|
||||
if (! mb_check_encoding(preg_replace('/^\\/+/', '', $fileName), 'ASCII')) {
|
||||
$hasUtf8 = true;
|
||||
}
|
||||
$filesMeta[] = [$fileName, $fileSize];
|
||||
}
|
||||
// If there is more than 4 GB or 2^16 files, it will be a ZIP64, changing the estimation method
|
||||
if (
|
||||
$totalSize >= pow(2, 32) ||
|
||||
count($filesMeta) >= 0xFFFF ||
|
||||
$hasUtf8
|
||||
) {
|
||||
$estimate = $this->calculateZip64Size($filesMeta);
|
||||
} else {
|
||||
$estimate = $this->calculateZipSize($filesMeta);
|
||||
}
|
||||
|
||||
$response = $response->withHeader('Content-Length', (string) $estimate);
|
||||
}
|
||||
$response = $response->withHeader('Content-Length', (string) $size);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
protected function calculateZipSize(array $filesMeta): int
|
||||
{
|
||||
$estimate = 22;
|
||||
foreach ($filesMeta as $fileMeta) {
|
||||
$estimate += 76 + 2 * strlen($fileMeta[0]) + $fileMeta[1];
|
||||
}
|
||||
|
||||
return $estimate;
|
||||
}
|
||||
|
||||
protected function calculateZip64Size(array $filesMeta): int
|
||||
{
|
||||
$estimate = 0;
|
||||
foreach ($filesMeta as $fileMeta) {
|
||||
$beginning = $estimate;
|
||||
// This is similar from standard Zip
|
||||
// Adding header size and filename
|
||||
$header = 30 + strlen($fileMeta[0]);
|
||||
// With Zip64, the size of the header is variable, so we need to calculate it
|
||||
$header += $this->calculateZip64ExtraBlockSize($fileMeta, 0);
|
||||
// Adding file content to the size
|
||||
$content = $fileMeta[1];
|
||||
// Default footer size (data descriptor) including filename
|
||||
$footer = 46 + strlen($fileMeta[0]);
|
||||
// This block also gets added at the end of the file, but offsets are differents, so we need to calculate it again
|
||||
$footer += $this->calculateZip64ExtraBlockSize($fileMeta, $beginning);
|
||||
$estimate += $header + $content + $footer;
|
||||
}
|
||||
// Size of the CDR64 EOF calculated by ZipStream is always 44 + 12 for signature and the size itself
|
||||
$estimate += 56;
|
||||
// Size of the CDR64 locator
|
||||
$estimate += 20;
|
||||
// Size of the CDR EOF locator
|
||||
$estimate += 22;
|
||||
|
||||
return $estimate;
|
||||
}
|
||||
|
||||
protected function calculateZip64ExtraBlockSize(array $fileMeta, int $currentOffset): int
|
||||
{
|
||||
// This is where it gets funky
|
||||
$zip64ExtraBlockSize = 0;
|
||||
if ($fileMeta[1] >= pow(2, 32)) {
|
||||
// If file size is more than 2^32, add it to the extra block
|
||||
$zip64ExtraBlockSize += 16; // 8 for size + 8 for compressed size
|
||||
}
|
||||
|
||||
// Offset
|
||||
if ($currentOffset >= pow(2, 32)) {
|
||||
$zip64ExtraBlockSize += 8; // if offset is more than 2^32, then we add it to the extra block
|
||||
}
|
||||
|
||||
if ($zip64ExtraBlockSize != 0) {
|
||||
$zip64ExtraBlockSize += 4; // 2 for header ID + 2 for the block size
|
||||
}
|
||||
|
||||
// If the filename or path does not fit into ASCII, then ZipStream will add the two remaining fields with special values
|
||||
if (! mb_check_encoding(preg_replace('/^\\/+/', '', $fileMeta[0]), 'ASCII')) {
|
||||
$zip64ExtraBlockSize += 4;
|
||||
}
|
||||
|
||||
return $zip64ExtraBlockSize;
|
||||
}
|
||||
|
||||
/** Return the path to a file with the preceding root path stripped. */
|
||||
protected function stripPath(SplFileInfo $file, string $path): string
|
||||
{
|
||||
@@ -201,14 +125,4 @@ class ZipController
|
||||
|
||||
return $filename == '.' ? 'Home' : $filename;
|
||||
}
|
||||
|
||||
/** Filter a file name to remove invalid characters inside a Zip */
|
||||
protected function filterFilename(string $filename): string
|
||||
{
|
||||
// strip leading slashes from file name
|
||||
// (fixes bug in windows archive viewer)
|
||||
$filename = (string) preg_replace('/^\\/+/', '', $filename);
|
||||
|
||||
return str_replace(['\\', ':', '*', '?', '"', '<', '>', '|'], '_', $filename);
|
||||
}
|
||||
}
|
||||
|
@@ -42,7 +42,6 @@
|
||||
"phpstan/phpstan": "^1.0",
|
||||
"psy/psysh": "^0.12.0",
|
||||
"symfony/var-dumper": "^6.0",
|
||||
"timeweb/phpstan-enum": "^3.1",
|
||||
"yoast/phpunit-polyfills": "^2.0"
|
||||
},
|
||||
"suggest": {
|
||||
|
@@ -39,4 +39,3 @@ parameters:
|
||||
includes:
|
||||
- phpstan-baseline.neon
|
||||
- phpstan-ignores.neon
|
||||
- app/vendor/timeweb/phpstan-enum/extension.neon
|
||||
|
Reference in New Issue
Block a user