ILIAS  trunk Revision v11.0_alpha-2638-g80c1d007f79
Zip.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
22 
26 
30 class 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 }
destroy()
Explicitly close the zip file and remove the file from the filesystem.
Definition: Zip.php:144
addStream(FileStream $stream, string $path_inside_zip)
Definition: Zip.php:170
registerShutdownFunction(\Closure $c)
Definition: Zip.php:92
$c
Definition: deliver.php:25
$path
Definition: ltiservices.php:29
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
addPath(string $path, ?string $path_inside_zip=null)
Definition: Zip.php:157
const CLIENT_DATA_DIR
Definition: constants.php:46
The base interface for all filesystem streams.
Definition: FileStream.php:31
__construct(protected ZipOptions $options,... $streams)
Definition: Zip.php:46