ILIAS  trunk Revision v12.0_alpha-1227-g7ff6d300864
Zip.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
26
30class Zip
31{
32 use PathHelper;
33
34 public const DOT_EMPTY = '.empty';
35 public const ITERATION_FACTOR = 0.9;
36 private string $zip_output_file = '';
37 protected \ZipArchive $zip;
38 private int $iteration_limit;
39 private int $store_counter = 1;
40 private int $path_counter = 1;
41
42 private bool $zip_opened = false;
43
47 private array $streams = [];
48
49 public function __construct(
50 protected ZipOptions $options,
52 ) {
53 if ($options->getZipOutputPath() !== null && $options->getZipOutputName() !== null) {
54 $this->zip_output_file = $this->ensureDirectorySeperator(
55 $options->getZipOutputPath()
56 ) . $options->getZipOutputName();
57 } else {
58 $this->zip_output_file = $this->buildTempPath();
59 $this->registerShutdownFunction(function (): void {
60 $this->destroy();
61 });
62 }
63 $system_limit = (int) shell_exec('ulimit -n') ?: 0;
64
65 $this->iteration_limit = $system_limit < 10 ? 100 : min(
66 $system_limit / 2,
67 5000
68 );
69
70 $this->zip = new \ZipArchive();
71 if (!file_exists($this->zip_output_file)) {
72 touch($this->zip_output_file);
73 }
74
75 $this->maybeOpenZip(\ZipArchive::OVERWRITE);
76 foreach ($streams as $path_inside_zip => $stream) {
77 $path_inside_zip = is_int($path_inside_zip) ? basename((string) $stream->getMetadata('uri')) : $path_inside_zip;
78 $this->addStream($stream, basename($path_inside_zip));
79 }
80 }
81
82 private function maybeOpenZip(int $flags = 0): void
83 {
84 if (!$this->zip_opened) {
85 if ($flags === 0) {
86 $this->zip_opened = $this->zip->open($this->zip_output_file) === true;
87 } else {
88 $this->zip_opened = $this->zip->open($this->zip_output_file, $flags) === true;
89 }
90 }
91 if (!$this->zip_opened) {
92 throw new \Exception("cannot open <$this->zip_output_file>\n");
93 }
94 }
95
96 private function buildTempPath(): string
97 {
98 $directory = defined('CLIENT_DATA_DIR') ? \CLIENT_DATA_DIR . '/temp' : sys_get_temp_dir();
99 $tempnam = tempnam($directory, 'zip');
100 if (is_file($tempnam)) {
101 return $tempnam;
102 }
103 if (is_dir($tempnam)) {
104 rmdir($tempnam);
105 touch($tempnam);
106 }
107 return $tempnam;
108 }
109
110 private function registerShutdownFunction(\Closure $c): void
111 {
112 register_shutdown_function($c);
113 }
114
115 private function storeZIPtoFilesystem(): void
116 {
117 foreach ($this->streams as $path_inside_zip => $stream) {
118 $path = $stream->getMetadata('uri');
119 if ($this->store_counter === 0) {
120 $this->maybeOpenZip();
121 }
122 if (is_int($path_inside_zip)) {
123 $path_inside_zip = basename((string) $path);
124 }
125
126 if ($path === 'php://memory') {
127 $this->zip->addFromString($path_inside_zip, (string) $stream);
128 $stream->close();
129 } elseif (is_file($path)) {
130 $this->zip->addFile($path, $path_inside_zip);
131 $stream->close();
132 } else {
133 continue;
134 }
135
136 if (
137 $this->store_counter === $this->iteration_limit
138 || count(get_resources('stream')) > ($this->iteration_limit * self::ITERATION_FACTOR)
139 ) {
140 $this->zip->close();
141 $this->zip_opened = false;
142 $this->store_counter = 0;
143 } else {
144 $this->store_counter++;
145 }
146 }
147 }
148
149 public function get(): Stream
150 {
151 $this->maybeOpenZip();
152 $this->storeZIPtoFilesystem();
153
154 $this->zip->close();
155 $this->zip_opened = false;
156
157 return Streams::ofResource(fopen($this->zip_output_file, 'rb'));
158 }
159
165 public function destroy(): void
166 {
167 if (file_exists($this->zip_output_file)) {
168 unlink($this->zip_output_file);
169 }
170 }
171
178 public function addPath(string $path, ?string $path_inside_zip = null): void
179 {
180 $path_inside_zip ??= basename($path);
181
182 $this->maybeOpenZip();
183
184 // create directory if it does not exist
185 $this->zip->addEmptyDir(rtrim(dirname($path_inside_zip), '/') . '/');
186
187 $this->addStream(
188 Streams::ofResource(fopen($path, 'rb')),
189 $path_inside_zip
190 );
191 }
192
193 public function addStream(FileStream $stream, string $path_inside_zip): void
194 {
195 // we remove the "empty zip file" now if possible
196 if (isset($this->streams[self::DOT_EMPTY])) {
197 unset($this->streams[self::DOT_EMPTY]);
198 }
199
200 // we must store the ZIP to e temporary files every 1000 files, otherwise we will get a Too Many Open Files error
201 $this->streams[$path_inside_zip] = $stream;
202
203 if (
204 $this->path_counter === $this->iteration_limit
205 || count(get_resources('stream')) > ($this->iteration_limit * self::ITERATION_FACTOR)
206 ) {
207 $this->storeZIPtoFilesystem();
208 $this->streams = [];
209 $this->path_counter = 0;
210 } else {
211 $this->path_counter++;
212 }
213 }
214
221 public function addDirectory(string $directory_to_zip): void
222 {
223 $directory_to_zip = $this->normalizePath(rtrim($directory_to_zip, '/'));
224 // find all files in the directory recursively
225 $files = new \RecursiveIteratorIterator(
226 new \RecursiveDirectoryIterator($directory_to_zip),
227 \RecursiveIteratorIterator::SELF_FIRST
228 );
229
230 switch ($this->options->getDirectoryHandling()) {
231 case ZipDirectoryHandling::KEEP_STRUCTURE:
232 $pattern = null;
233 $prefix = '';
234 break;
235 case ZipDirectoryHandling::ENSURE_SINGLE_TOP_DIR:
236 $prefix = basename($directory_to_zip) . '/';
237 $pattern = '/^' . preg_quote($prefix, '/') . '/';
238 break;
239 }
240
241 foreach ($files as $file) {
242 $pathname = $file->getPathname();
243 $path_inside_zip = str_replace($directory_to_zip . '/', '', $pathname);
244 if ($pattern !== null) {
245 $path_inside_zip = $prefix . preg_replace($pattern, '', $path_inside_zip);
246 }
247
249 if ($file->isDir()) {
250 // add directory to zip if it's empty
251 $sub_items = array_filter(scandir($pathname), static fn($d): bool => !str_contains($d, '.DS_Store'));
252 if (count($sub_items) === 2) {
253 $this->zip->addEmptyDir($path_inside_zip);
254 }
255 continue;
256 }
257
258 if ($this->isPathIgnored($pathname, $this->options)) {
259 continue;
260 }
261
262 $this->addPath(realpath($pathname), $path_inside_zip);
263 }
264 }
265}
Stream factory which enables the user to create streams without the knowledge of the concrete class.
Definition: Streams.php:32
static ofResource($resource)
Wraps an already created resource with the stream abstraction.
Definition: Streams.php:64
__construct(protected ZipOptions $options, FileStream ... $streams)
Definition: Zip.php:49
addPath(string $path, ?string $path_inside_zip=null)
Definition: Zip.php:178
destroy()
@description Explicitly close the zip file and remove the file from the filesystem.
Definition: Zip.php:165
maybeOpenZip(int $flags=0)
Definition: Zip.php:82
registerShutdownFunction(\Closure $c)
Definition: Zip.php:110
addStream(FileStream $stream, string $path_inside_zip)
Definition: Zip.php:193
const CLIENT_DATA_DIR
Definition: constants.php:46
$c
Definition: deliver.php:25
The base interface for all filesystem streams.
Definition: FileStream.php:32
$path
Definition: ltiservices.php:30