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