Verzeichnisstruktur phpBB-3.3.15


Veröffentlicht
28.08.2024

So funktioniert es


Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück

Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis.
Auf den Verzeichnisnamen klicken, dies zeigt nur das Verzeichnis mit Inhalt an

(Beispiel Datei-Icons)

Auf das Icon klicken um den Quellcode anzuzeigen

Filesystem.php

Zuletzt modifiziert: 02.04.2025, 15:02 - Dateigröße: 29.07 KiB


001  <?php
002   
003  /*
004   * This file is part of the Symfony package.
005   *
006   * (c) Fabien Potencier <fabien@symfony.com>
007   *
008   * For the full copyright and license information, please view the LICENSE
009   * file that was distributed with this source code.
010   */
011   
012  namespace Symfony\Component\Filesystem;
013   
014  use Symfony\Component\Filesystem\Exception\FileNotFoundException;
015  use Symfony\Component\Filesystem\Exception\IOException;
016   
017  /**
018   * Provides basic utility to manipulate the file system.
019   *
020   * @author Fabien Potencier <fabien@symfony.com>
021   */
022  class Filesystem
023  {
024      private static $lastError;
025   
026      /**
027       * Copies a file.
028       *
029       * If the target file is older than the origin file, it's always overwritten.
030       * If the target file is newer, it is overwritten only when the
031       * $overwriteNewerFiles option is set to true.
032       *
033       * @param string $originFile          The original filename
034       * @param string $targetFile          The target filename
035       * @param bool   $overwriteNewerFiles If true, target files newer than origin files are overwritten
036       *
037       * @throws FileNotFoundException When originFile doesn't exist
038       * @throws IOException           When copy fails
039       */
040      public function copy($originFile, $targetFile, $overwriteNewerFiles = false)
041      {
042          $originIsLocal = stream_is_local($originFile) || 0 === stripos($originFile, 'file://');
043          if ($originIsLocal && !is_file($originFile)) {
044              throw new FileNotFoundException(sprintf('Failed to copy "%s" because file does not exist.', $originFile), 0, null, $originFile);
045          }
046   
047          $this->mkdir(\dirname($targetFile));
048   
049          $doCopy = true;
050          if (!$overwriteNewerFiles && null === parse_url($originFile, \PHP_URL_HOST) && is_file($targetFile)) {
051              $doCopy = filemtime($originFile) > filemtime($targetFile);
052          }
053   
054          if ($doCopy) {
055              // https://bugs.php.net/64634
056              if (false === $source = @fopen($originFile, 'r')) {
057                  throw new IOException(sprintf('Failed to copy "%s" to "%s" because source file could not be opened for reading.', $originFile, $targetFile), 0, null, $originFile);
058              }
059   
060              // Stream context created to allow files overwrite when using FTP stream wrapper - disabled by default
061              if (false === $target = @fopen($targetFile, 'w', null, stream_context_create(['ftp' => ['overwrite' => true]]))) {
062                  throw new IOException(sprintf('Failed to copy "%s" to "%s" because target file could not be opened for writing.', $originFile, $targetFile), 0, null, $originFile);
063              }
064   
065              $bytesCopied = stream_copy_to_stream($source, $target);
066              fclose($source);
067              fclose($target);
068              unset($source, $target);
069   
070              if (!is_file($targetFile)) {
071                  throw new IOException(sprintf('Failed to copy "%s" to "%s".', $originFile, $targetFile), 0, null, $originFile);
072              }
073   
074              if ($originIsLocal) {
075                  // Like `cp`, preserve executable permission bits
076                  @chmod($targetFile, fileperms($targetFile) | (fileperms($originFile) & 0111));
077   
078                  if ($bytesCopied !== $bytesOrigin = filesize($originFile)) {
079                      throw new IOException(sprintf('Failed to copy the whole content of "%s" to "%s" (%g of %g bytes copied).', $originFile, $targetFile, $bytesCopied, $bytesOrigin), 0, null, $originFile);
080                  }
081              }
082          }
083      }
084   
085      /**
086       * Creates a directory recursively.
087       *
088       * @param string|iterable $dirs The directory path
089       * @param int             $mode The directory mode
090       *
091       * @throws IOException On any directory creation failure
092       */
093      public function mkdir($dirs, $mode = 0777)
094      {
095          foreach ($this->toIterable($dirs) as $dir) {
096              if (is_dir($dir)) {
097                  continue;
098              }
099   
100              if (!self::box('mkdir', $dir, $mode, true)) {
101                  if (!is_dir($dir)) {
102                      // The directory was not created by a concurrent process. Let's throw an exception with a developer friendly error message if we have one
103                      if (self::$lastError) {
104                          throw new IOException(sprintf('Failed to create "%s": ', $dir).self::$lastError, 0, null, $dir);
105                      }
106                      throw new IOException(sprintf('Failed to create "%s".', $dir), 0, null, $dir);
107                  }
108              }
109          }
110      }
111   
112      /**
113       * Checks the existence of files or directories.
114       *
115       * @param string|iterable $files A filename, an array of files, or a \Traversable instance to check
116       *
117       * @return bool true if the file exists, false otherwise
118       */
119      public function exists($files)
120      {
121          $maxPathLength = \PHP_MAXPATHLEN - 2;
122   
123          foreach ($this->toIterable($files) as $file) {
124              if (\strlen($file) > $maxPathLength) {
125                  throw new IOException(sprintf('Could not check if file exist because path length exceeds %d characters.', $maxPathLength), 0, null, $file);
126              }
127   
128              if (!file_exists($file)) {
129                  return false;
130              }
131          }
132   
133          return true;
134      }
135   
136      /**
137       * Sets access and modification time of file.
138       *
139       * @param string|iterable $files A filename, an array of files, or a \Traversable instance to create
140       * @param int|null        $time  The touch time as a Unix timestamp, if not supplied the current system time is used
141       * @param int|null        $atime The access time as a Unix timestamp, if not supplied the current system time is used
142       *
143       * @throws IOException When touch fails
144       */
145      public function touch($files, $time = null, $atime = null)
146      {
147          foreach ($this->toIterable($files) as $file) {
148              $touch = $time ? @touch($file, $time, $atime) : @touch($file);
149              if (true !== $touch) {
150                  throw new IOException(sprintf('Failed to touch "%s".', $file), 0, null, $file);
151              }
152          }
153      }
154   
155      /**
156       * Removes files or directories.
157       *
158       * @param string|iterable $files A filename, an array of files, or a \Traversable instance to remove
159       *
160       * @throws IOException When removal fails
161       */
162      public function remove($files)
163      {
164          if ($files instanceof \Traversable) {
165              $files = iterator_to_array($files, false);
166          } elseif (!\is_array($files)) {
167              $files = [$files];
168          }
169          $files = array_reverse($files);
170          foreach ($files as $file) {
171              if (is_link($file)) {
172                  // See https://bugs.php.net/52176
173                  if (!(self::box('unlink', $file) || '\\' !== \DIRECTORY_SEPARATOR || self::box('rmdir', $file)) && file_exists($file)) {
174                      throw new IOException(sprintf('Failed to remove symlink "%s": ', $file).self::$lastError);
175                  }
176              } elseif (is_dir($file)) {
177                  $this->remove(new \FilesystemIterator($file, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS));
178   
179                  if (!self::box('rmdir', $file) && file_exists($file)) {
180                      throw new IOException(sprintf('Failed to remove directory "%s": ', $file).self::$lastError);
181                  }
182              } elseif (!self::box('unlink', $file) && (false !== strpos(self::$lastError, 'Permission denied') || file_exists($file))) {
183                  throw new IOException(sprintf('Failed to remove file "%s": ', $file).self::$lastError);
184              }
185          }
186      }
187   
188      /**
189       * Change mode for an array of files or directories.
190       *
191       * @param string|iterable $files     A filename, an array of files, or a \Traversable instance to change mode
192       * @param int             $mode      The new mode (octal)
193       * @param int             $umask     The mode mask (octal)
194       * @param bool            $recursive Whether change the mod recursively or not
195       *
196       * @throws IOException When the change fails
197       */
198      public function chmod($files, $mode, $umask = 0000, $recursive = false)
199      {
200          foreach ($this->toIterable($files) as $file) {
201              if ((\PHP_VERSION_ID < 80000 || \is_int($mode)) && true !== @chmod($file, $mode & ~$umask)) {
202                  throw new IOException(sprintf('Failed to chmod file "%s".', $file), 0, null, $file);
203              }
204              if ($recursive && is_dir($file) && !is_link($file)) {
205                  $this->chmod(new \FilesystemIterator($file), $mode, $umask, true);
206              }
207          }
208      }
209   
210      /**
211       * Change the owner of an array of files or directories.
212       *
213       * @param string|iterable $files     A filename, an array of files, or a \Traversable instance to change owner
214       * @param string|int      $user      A user name or number
215       * @param bool            $recursive Whether change the owner recursively or not
216       *
217       * @throws IOException When the change fails
218       */
219      public function chown($files, $user, $recursive = false)
220      {
221          foreach ($this->toIterable($files) as $file) {
222              if ($recursive && is_dir($file) && !is_link($file)) {
223                  $this->chown(new \FilesystemIterator($file), $user, true);
224              }
225              if (is_link($file) && \function_exists('lchown')) {
226                  if (true !== @lchown($file, $user)) {
227                      throw new IOException(sprintf('Failed to chown file "%s".', $file), 0, null, $file);
228                  }
229              } else {
230                  if (true !== @chown($file, $user)) {
231                      throw new IOException(sprintf('Failed to chown file "%s".', $file), 0, null, $file);
232                  }
233              }
234          }
235      }
236   
237      /**
238       * Change the group of an array of files or directories.
239       *
240       * @param string|iterable $files     A filename, an array of files, or a \Traversable instance to change group
241       * @param string|int      $group     A group name or number
242       * @param bool            $recursive Whether change the group recursively or not
243       *
244       * @throws IOException When the change fails
245       */
246      public function chgrp($files, $group, $recursive = false)
247      {
248          foreach ($this->toIterable($files) as $file) {
249              if ($recursive && is_dir($file) && !is_link($file)) {
250                  $this->chgrp(new \FilesystemIterator($file), $group, true);
251              }
252              if (is_link($file) && \function_exists('lchgrp')) {
253                  if (true !== @lchgrp($file, $group) || (\defined('HHVM_VERSION') && !posix_getgrnam($group))) {
254                      throw new IOException(sprintf('Failed to chgrp file "%s".', $file), 0, null, $file);
255                  }
256              } else {
257                  if (true !== @chgrp($file, $group)) {
258                      throw new IOException(sprintf('Failed to chgrp file "%s".', $file), 0, null, $file);
259                  }
260              }
261          }
262      }
263   
264      /**
265       * Renames a file or a directory.
266       *
267       * @param string $origin    The origin filename or directory
268       * @param string $target    The new filename or directory
269       * @param bool   $overwrite Whether to overwrite the target if it already exists
270       *
271       * @throws IOException When target file or directory already exists
272       * @throws IOException When origin cannot be renamed
273       */
274      public function rename($origin, $target, $overwrite = false)
275      {
276          // we check that target does not exist
277          if (!$overwrite && $this->isReadable($target)) {
278              throw new IOException(sprintf('Cannot rename because the target "%s" already exists.', $target), 0, null, $target);
279          }
280   
281          if (true !== @rename($origin, $target)) {
282              if (is_dir($origin)) {
283                  // See https://bugs.php.net/54097 & https://php.net/rename#113943
284                  $this->mirror($origin, $target, null, ['override' => $overwrite, 'delete' => $overwrite]);
285                  $this->remove($origin);
286   
287                  return;
288              }
289              throw new IOException(sprintf('Cannot rename "%s" to "%s".', $origin, $target), 0, null, $target);
290          }
291      }
292   
293      /**
294       * Tells whether a file exists and is readable.
295       *
296       * @param string $filename Path to the file
297       *
298       * @return bool
299       *
300       * @throws IOException When windows path is longer than 258 characters
301       */
302      private function isReadable($filename)
303      {
304          $maxPathLength = \PHP_MAXPATHLEN - 2;
305   
306          if (\strlen($filename) > $maxPathLength) {
307              throw new IOException(sprintf('Could not check if file is readable because path length exceeds %d characters.', $maxPathLength), 0, null, $filename);
308          }
309   
310          return is_readable($filename);
311      }
312   
313      /**
314       * Creates a symbolic link or copy a directory.
315       *
316       * @param string $originDir     The origin directory path
317       * @param string $targetDir     The symbolic link name
318       * @param bool   $copyOnWindows Whether to copy files if on Windows
319       *
320       * @throws IOException When symlink fails
321       */
322      public function symlink($originDir, $targetDir, $copyOnWindows = false)
323      {
324          if ('\\' === \DIRECTORY_SEPARATOR) {
325              $originDir = strtr($originDir, '/', '\\');
326              $targetDir = strtr($targetDir, '/', '\\');
327   
328              if ($copyOnWindows) {
329                  $this->mirror($originDir, $targetDir);
330   
331                  return;
332              }
333          }
334   
335          $this->mkdir(\dirname($targetDir));
336   
337          if (is_link($targetDir)) {
338              if (readlink($targetDir) === $originDir) {
339                  return;
340              }
341              $this->remove($targetDir);
342          }
343   
344          if (!self::box('symlink', $originDir, $targetDir)) {
345              $this->linkException($originDir, $targetDir, 'symbolic');
346          }
347      }
348   
349      /**
350       * Creates a hard link, or several hard links to a file.
351       *
352       * @param string          $originFile  The original file
353       * @param string|string[] $targetFiles The target file(s)
354       *
355       * @throws FileNotFoundException When original file is missing or not a file
356       * @throws IOException           When link fails, including if link already exists
357       */
358      public function hardlink($originFile, $targetFiles)
359      {
360          if (!$this->exists($originFile)) {
361              throw new FileNotFoundException(null, 0, null, $originFile);
362          }
363   
364          if (!is_file($originFile)) {
365              throw new FileNotFoundException(sprintf('Origin file "%s" is not a file.', $originFile));
366          }
367   
368          foreach ($this->toIterable($targetFiles) as $targetFile) {
369              if (is_file($targetFile)) {
370                  if (fileinode($originFile) === fileinode($targetFile)) {
371                      continue;
372                  }
373                  $this->remove($targetFile);
374              }
375   
376              if (!self::box('link', $originFile, $targetFile)) {
377                  $this->linkException($originFile, $targetFile, 'hard');
378              }
379          }
380      }
381   
382      /**
383       * @param string $origin
384       * @param string $target
385       * @param string $linkType Name of the link type, typically 'symbolic' or 'hard'
386       */
387      private function linkException($origin, $target, $linkType)
388      {
389          if (self::$lastError) {
390              if ('\\' === \DIRECTORY_SEPARATOR && false !== strpos(self::$lastError, 'error code(1314)')) {
391                  throw new IOException(sprintf('Unable to create "%s" link due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?', $linkType), 0, null, $target);
392              }
393          }
394          throw new IOException(sprintf('Failed to create "%s" link from "%s" to "%s".', $linkType, $origin, $target), 0, null, $target);
395      }
396   
397      /**
398       * Resolves links in paths.
399       *
400       * With $canonicalize = false (default)
401       *      - if $path does not exist or is not a link, returns null
402       *      - if $path is a link, returns the next direct target of the link without considering the existence of the target
403       *
404       * With $canonicalize = true
405       *      - if $path does not exist, returns null
406       *      - if $path exists, returns its absolute fully resolved final version
407       *
408       * @param string $path         A filesystem path
409       * @param bool   $canonicalize Whether or not to return a canonicalized path
410       *
411       * @return string|null
412       */
413      public function readlink($path, $canonicalize = false)
414      {
415          if (!$canonicalize && !is_link($path)) {
416              return null;
417          }
418   
419          if ($canonicalize) {
420              if (!$this->exists($path)) {
421                  return null;
422              }
423   
424              if ('\\' === \DIRECTORY_SEPARATOR) {
425                  $path = readlink($path);
426              }
427   
428              return realpath($path);
429          }
430   
431          if ('\\' === \DIRECTORY_SEPARATOR) {
432              return realpath($path);
433          }
434   
435          return readlink($path);
436      }
437   
438      /**
439       * Given an existing path, convert it to a path relative to a given starting path.
440       *
441       * @param string $endPath   Absolute path of target
442       * @param string $startPath Absolute path where traversal begins
443       *
444       * @return string Path of target relative to starting path
445       */
446      public function makePathRelative($endPath, $startPath)
447      {
448          if (!$this->isAbsolutePath($endPath) || !$this->isAbsolutePath($startPath)) {
449              @trigger_error(sprintf('Support for passing relative paths to %s() is deprecated since Symfony 3.4 and will be removed in 4.0.', __METHOD__), \E_USER_DEPRECATED);
450          }
451   
452          // Normalize separators on Windows
453          if ('\\' === \DIRECTORY_SEPARATOR) {
454              $endPath = str_replace('\\', '/', $endPath);
455              $startPath = str_replace('\\', '/', $startPath);
456          }
457   
458          $splitDriveLetter = function ($path) {
459              return (\strlen($path) > 2 && ':' === $path[1] && '/' === $path[2] && ctype_alpha($path[0]))
460                  ? [substr($path, 2), strtoupper($path[0])]
461                  : [$path, null];
462          };
463   
464          $splitPath = function ($path, $absolute) {
465              $result = [];
466   
467              foreach (explode('/', trim($path, '/')) as $segment) {
468                  if ('..' === $segment && ($absolute || \count($result))) {
469                      array_pop($result);
470                  } elseif ('.' !== $segment && '' !== $segment) {
471                      $result[] = $segment;
472                  }
473              }
474   
475              return $result;
476          };
477   
478          list($endPath, $endDriveLetter) = $splitDriveLetter($endPath);
479          list($startPath, $startDriveLetter) = $splitDriveLetter($startPath);
480   
481          $startPathArr = $splitPath($startPath, static::isAbsolutePath($startPath));
482          $endPathArr = $splitPath($endPath, static::isAbsolutePath($endPath));
483   
484          if ($endDriveLetter && $startDriveLetter && $endDriveLetter != $startDriveLetter) {
485              // End path is on another drive, so no relative path exists
486              return $endDriveLetter.':/'.($endPathArr ? implode('/', $endPathArr).'/' : '');
487          }
488   
489          // Find for which directory the common path stops
490          $index = 0;
491          while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) {
492              ++$index;
493          }
494   
495          // Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels)
496          if (1 === \count($startPathArr) && '' === $startPathArr[0]) {
497              $depth = 0;
498          } else {
499              $depth = \count($startPathArr) - $index;
500          }
501   
502          // Repeated "../" for each level need to reach the common path
503          $traverser = str_repeat('../', $depth);
504   
505          $endPathRemainder = implode('/', \array_slice($endPathArr, $index));
506   
507          // Construct $endPath from traversing to the common path, then to the remaining $endPath
508          $relativePath = $traverser.('' !== $endPathRemainder ? $endPathRemainder.'/' : '');
509   
510          return '' === $relativePath ? './' : $relativePath;
511      }
512   
513      /**
514       * Mirrors a directory to another.
515       *
516       * Copies files and directories from the origin directory into the target directory. By default:
517       *
518       *  - existing files in the target directory will be overwritten, except if they are newer (see the `override` option)
519       *  - files in the target directory that do not exist in the source directory will not be deleted (see the `delete` option)
520       *
521       * @param string            $originDir The origin directory
522       * @param string            $targetDir The target directory
523       * @param \Traversable|null $iterator  Iterator that filters which files and directories to copy, if null a recursive iterator is created
524       * @param array             $options   An array of boolean options
525       *                                     Valid options are:
526       *                                     - $options['override'] If true, target files newer than origin files are overwritten (see copy(), defaults to false)
527       *                                     - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink(), defaults to false)
528       *                                     - $options['delete'] Whether to delete files that are not in the source directory (defaults to false)
529       *
530       * @throws IOException When file type is unknown
531       */
532      public function mirror($originDir, $targetDir, \Traversable $iterator = null, $options = [])
533      {
534          $targetDir = rtrim($targetDir, '/\\');
535          $originDir = rtrim($originDir, '/\\');
536          $originDirLen = \strlen($originDir);
537   
538          // Iterate in destination folder to remove obsolete entries
539          if ($this->exists($targetDir) && isset($options['delete']) && $options['delete']) {
540              $deleteIterator = $iterator;
541              if (null === $deleteIterator) {
542                  $flags = \FilesystemIterator::SKIP_DOTS;
543                  $deleteIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($targetDir, $flags), \RecursiveIteratorIterator::CHILD_FIRST);
544              }
545              $targetDirLen = \strlen($targetDir);
546              foreach ($deleteIterator as $file) {
547                  $origin = $originDir.substr($file->getPathname(), $targetDirLen);
548                  if (!$this->exists($origin)) {
549                      $this->remove($file);
550                  }
551              }
552          }
553   
554          $copyOnWindows = false;
555          if (isset($options['copy_on_windows'])) {
556              $copyOnWindows = $options['copy_on_windows'];
557          }
558   
559          if (null === $iterator) {
560              $flags = $copyOnWindows ? \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS : \FilesystemIterator::SKIP_DOTS;
561              $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($originDir, $flags), \RecursiveIteratorIterator::SELF_FIRST);
562          }
563   
564          if ($this->exists($originDir)) {
565              $this->mkdir($targetDir);
566          }
567   
568          foreach ($iterator as $file) {
569              $target = $targetDir.substr($file->getPathname(), $originDirLen);
570   
571              if ($copyOnWindows) {
572                  if (is_file($file)) {
573                      $this->copy($file, $target, isset($options['override']) ? $options['override'] : false);
574                  } elseif (is_dir($file)) {
575                      $this->mkdir($target);
576                  } else {
577                      throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file);
578                  }
579              } else {
580                  if (is_link($file)) {
581                      $this->symlink($file->getLinkTarget(), $target);
582                  } elseif (is_dir($file)) {
583                      $this->mkdir($target);
584                  } elseif (is_file($file)) {
585                      $this->copy($file, $target, isset($options['override']) ? $options['override'] : false);
586                  } else {
587                      throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file);
588                  }
589              }
590          }
591      }
592   
593      /**
594       * Returns whether the file path is an absolute path.
595       *
596       * @param string $file A file path
597       *
598       * @return bool
599       */
600      public function isAbsolutePath($file)
601      {
602          return '' !== (string) $file && (strspn($file, '/\\', 0, 1)
603              || (\strlen($file) > 3 && ctype_alpha($file[0])
604                  && ':' === $file[1]
605                  && strspn($file, '/\\', 2, 1)
606              )
607              || null !== parse_url($file, \PHP_URL_SCHEME)
608          );
609      }
610   
611      /**
612       * Creates a temporary file with support for custom stream wrappers.
613       *
614       * @param string $dir    The directory where the temporary filename will be created
615       * @param string $prefix The prefix of the generated temporary filename
616       *                       Note: Windows uses only the first three characters of prefix
617       *
618       * @return string The new temporary filename (with path), or throw an exception on failure
619       */
620      public function tempnam($dir, $prefix)
621      {
622          list($scheme, $hierarchy) = $this->getSchemeAndHierarchy($dir);
623   
624          // If no scheme or scheme is "file" or "gs" (Google Cloud) create temp file in local filesystem
625          if (null === $scheme || 'file' === $scheme || 'gs' === $scheme) {
626              $tmpFile = @tempnam($hierarchy, $prefix);
627   
628              // If tempnam failed or no scheme return the filename otherwise prepend the scheme
629              if (false !== $tmpFile) {
630                  if (null !== $scheme && 'gs' !== $scheme) {
631                      return $scheme.'://'.$tmpFile;
632                  }
633   
634                  return $tmpFile;
635              }
636   
637              throw new IOException('A temporary file could not be created.');
638          }
639   
640          // Loop until we create a valid temp file or have reached 10 attempts
641          for ($i = 0; $i < 10; ++$i) {
642              // Create a unique filename
643              $tmpFile = $dir.'/'.$prefix.uniqid(mt_rand(), true);
644   
645              // Use fopen instead of file_exists as some streams do not support stat
646              // Use mode 'x+' to atomically check existence and create to avoid a TOCTOU vulnerability
647              $handle = @fopen($tmpFile, 'x+');
648   
649              // If unsuccessful restart the loop
650              if (false === $handle) {
651                  continue;
652              }
653   
654              // Close the file if it was successfully opened
655              @fclose($handle);
656   
657              return $tmpFile;
658          }
659   
660          throw new IOException('A temporary file could not be created.');
661      }
662   
663      /**
664       * Atomically dumps content into a file.
665       *
666       * @param string $filename The file to be written to
667       * @param string $content  The data to write into the file
668       *
669       * @throws IOException if the file cannot be written to
670       */
671      public function dumpFile($filename, $content)
672      {
673          $dir = \dirname($filename);
674   
675          if (!is_dir($dir)) {
676              $this->mkdir($dir);
677          }
678   
679          if (!is_writable($dir)) {
680              throw new IOException(sprintf('Unable to write to the "%s" directory.', $dir), 0, null, $dir);
681          }
682   
683          // Will create a temp file with 0600 access rights
684          // when the filesystem supports chmod.
685          $tmpFile = $this->tempnam($dir, basename($filename));
686   
687          if (false === @file_put_contents($tmpFile, $content)) {
688              throw new IOException(sprintf('Failed to write file "%s".', $filename), 0, null, $filename);
689          }
690   
691          @chmod($tmpFile, file_exists($filename) ? fileperms($filename) : 0666 & ~umask());
692   
693          $this->rename($tmpFile, $filename, true);
694      }
695   
696      /**
697       * Appends content to an existing file.
698       *
699       * @param string $filename The file to which to append content
700       * @param string $content  The content to append
701       *
702       * @throws IOException If the file is not writable
703       */
704      public function appendToFile($filename, $content)
705      {
706          $dir = \dirname($filename);
707   
708          if (!is_dir($dir)) {
709              $this->mkdir($dir);
710          }
711   
712          if (!is_writable($dir)) {
713              throw new IOException(sprintf('Unable to write to the "%s" directory.', $dir), 0, null, $dir);
714          }
715   
716          if (false === @file_put_contents($filename, $content, \FILE_APPEND)) {
717              throw new IOException(sprintf('Failed to write file "%s".', $filename), 0, null, $filename);
718          }
719      }
720   
721      /**
722       * @param mixed $files
723       *
724       * @return array|\Traversable
725       */
726      private function toIterable($files)
727      {
728          return \is_array($files) || $files instanceof \Traversable ? $files : [$files];
729      }
730   
731      /**
732       * Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> [file, tmp]).
733       *
734       * @param string $filename The filename to be parsed
735       *
736       * @return array The filename scheme and hierarchical part
737       */
738      private function getSchemeAndHierarchy($filename)
739      {
740          $components = explode('://', $filename, 2);
741   
742          return 2 === \count($components) ? [$components[0], $components[1]] : [null, $components[0]];
743      }
744   
745      /**
746       * @param callable $func
747       *
748       * @return mixed
749       */
750      private static function box($func)
751      {
752          self::$lastError = null;
753          set_error_handler(__CLASS__.'::handleError');
754          try {
755              $result = \call_user_func_array($func, \array_slice(\func_get_args(), 1));
756              restore_error_handler();
757   
758              return $result;
759          } catch (\Throwable $e) {
760          } catch (\Exception $e) {
761          }
762          restore_error_handler();
763   
764          throw $e;
765      }
766   
767      /**
768       * @internal
769       */
770      public static function handleError($type, $msg)
771      {
772          self::$lastError = $msg;
773      }
774  }
775