ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
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 private string $zip_output_file = '';
36 protected \ZipArchive $zip;
37 private int $iteration_limit;
38 private int $store_counter = 1;
39 private int $path_counter = 1;
40
44 private array $streams;
45
46 public function __construct(
47 protected ZipOptions $options,
48 ...$streams
49 ) {
50 $this->streams = array_filter($streams, fn($stream): bool => $stream instanceof FileStream);
51
52 if ($options->getZipOutputPath() !== null && $options->getZipOutputName() !== null) {
53 $this->zip_output_file = $this->ensureDirectorySeperator(
54 $options->getZipOutputPath()
55 ) . $options->getZipOutputName();
56 } else {
57 $this->zip_output_file = $this->buildTempPath();
58 $this->registerShutdownFunction(function (): void {
59 $this->destroy();
60 });
61 }
62 $system_limit = (int) shell_exec('ulimit -n') ?: 0;
63
64 $this->iteration_limit = $system_limit < 10 ? 100 : min(
65 $system_limit / 2,
66 5000
67 );
68
69 $this->zip = new \ZipArchive();
70 if (!file_exists($this->zip_output_file)) {
71 touch($this->zip_output_file);
72 }
73 if ($this->zip->open($this->zip_output_file, \ZipArchive::OVERWRITE) !== true) {
74 throw new \Exception("cannot open <$this->zip_output_file>\n");
75 }
76 }
77
78 private function buildTempPath(): string
79 {
80 $directory = defined('CLIENT_DATA_DIR') ? \CLIENT_DATA_DIR . '/temp' : sys_get_temp_dir();
81 $tempnam = tempnam($directory, 'zip');
82 if (is_file($tempnam)) {
83 return $tempnam;
84 }
85 if (is_dir($tempnam)) {
86 rmdir($tempnam);
87 touch($tempnam);
88 }
89 return $tempnam;
90 }
91
92 private function registerShutdownFunction(\Closure $c): void
93 {
94 register_shutdown_function($c);
95 }
96
97 private function storeZIPtoFilesystem(): void
98 {
99 foreach ($this->streams as $path_inside_zip => $stream) {
100 $path = $stream->getMetadata('uri');
101 if ($this->store_counter === 0) {
102 $this->zip->open($this->zip_output_file);
103 }
104 if (is_int($path_inside_zip)) {
105 $path_inside_zip = basename((string) $path);
106 }
107
108 if ($path === 'php://memory') {
109 $this->zip->addFromString($path_inside_zip, (string) $stream);
110 $stream->close();
111 } elseif (is_file($path)) {
112 $this->zip->addFile($path, $path_inside_zip);
113 $stream->close();
114 } else {
115 continue;
116 }
117
118 if (
119 $this->store_counter === $this->iteration_limit
120 || count(get_resources('stream')) > ($this->iteration_limit * 0.9)
121 ) {
122 $this->zip->close();
123 $this->store_counter = 0;
124 } else {
125 $this->store_counter++;
126 }
127 }
128 }
129
130 public function get(): Stream
131 {
132 $this->storeZIPtoFilesystem();
133
134 $this->zip->close();
135
136 return Streams::ofResource(fopen($this->zip_output_file, 'rb'));
137 }
138
144 public function destroy(): void
145 {
146 if (file_exists($this->zip_output_file)) {
147 unlink($this->zip_output_file);
148 }
149 }
150
157 public function addPath(string $path, ?string $path_inside_zip = null): void
158 {
159 $path_inside_zip ??= basename($path);
160
161 // create directory if it does not exist
162 $this->zip->addEmptyDir(rtrim(dirname($path_inside_zip), '/') . '/');
163
164 $this->addStream(
165 Streams::ofResource(fopen($path, 'rb')),
166 $path_inside_zip
167 );
168 }
169
170 public function addStream(FileStream $stream, string $path_inside_zip): void
171 {
172 // we remove the "empty zip file" now if possible
173 if (count($this->streams) === 1 && isset($this->streams[self::DOT_EMPTY])) {
174 unset($this->streams[self::DOT_EMPTY]);
175 }
176
177 // we must store the ZIP to e temporary files every 1000 files, otherwise we will get a Too Many Open Files error
178 $this->streams[$path_inside_zip] = $stream;
179
180 if (
181 $this->path_counter === $this->iteration_limit
182 || count(get_resources('stream')) > ($this->iteration_limit * 0.9)
183 ) {
184 $this->storeZIPtoFilesystem();
185 $this->streams = [];
186 $this->path_counter = 0;
187 } else {
188 $this->path_counter++;
189 }
190 }
191
198 public function addDirectory(string $directory_to_zip): void
199 {
200 $directory_to_zip = $this->normalizePath(rtrim($directory_to_zip, '/'));
201 // find all files in the directory recursively
202 $files = new \RecursiveIteratorIterator(
203 new \RecursiveDirectoryIterator($directory_to_zip),
204 \RecursiveIteratorIterator::SELF_FIRST
205 );
206
207 switch ($this->options->getDirectoryHandling()) {
208 case ZipDirectoryHandling::KEEP_STRUCTURE:
209 $pattern = null;
210 $prefix = '';
211 break;
212 case ZipDirectoryHandling::ENSURE_SINGLE_TOP_DIR:
213 $prefix = basename($directory_to_zip) . '/';
214 $pattern = '/^' . preg_quote($prefix, '/') . '/';
215 break;
216 }
217
218 foreach ($files as $file) {
219 $pathname = $file->getPathname();
220 $path_inside_zip = str_replace($directory_to_zip . '/', '', $pathname);
221 if ($pattern !== null) {
222 $path_inside_zip = $prefix . preg_replace($pattern, '', $path_inside_zip);
223 }
224
226 if ($file->isDir()) {
227 // add directory to zip if it's empty
228 $sub_items = array_filter(scandir($pathname), static fn($d): bool => !str_contains((string) $d, '.DS_Store'));
229 if (count($sub_items) === 2) {
230 $this->zip->addEmptyDir($path_inside_zip);
231 }
232 continue;
233 }
234
235 if ($this->isPathIgnored($pathname, $this->options)) {
236 continue;
237 }
238
239 $this->addPath(realpath($pathname), $path_inside_zip);
240 }
241 }
242}
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,... $streams)
Definition: Zip.php:46
addPath(string $path, ?string $path_inside_zip=null)
Definition: Zip.php:157
destroy()
@description Explicitly close the zip file and remove the file from the filesystem.
Definition: Zip.php:144
registerShutdownFunction(\Closure $c)
Definition: Zip.php:92
addStream(FileStream $stream, string $path_inside_zip)
Definition: Zip.php:170
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