vendor/symfony/filesystem/Filesystem.php line 694

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Filesystem;
  11. use Symfony\Component\Filesystem\Exception\FileNotFoundException;
  12. use Symfony\Component\Filesystem\Exception\InvalidArgumentException;
  13. use Symfony\Component\Filesystem\Exception\IOException;
  14. /**
  15.  * Provides basic utility to manipulate the file system.
  16.  *
  17.  * @author Fabien Potencier <fabien@symfony.com>
  18.  */
  19. class Filesystem
  20. {
  21.     private static $lastError;
  22.     /**
  23.      * Copies a file.
  24.      *
  25.      * If the target file is older than the origin file, it's always overwritten.
  26.      * If the target file is newer, it is overwritten only when the
  27.      * $overwriteNewerFiles option is set to true.
  28.      *
  29.      * @throws FileNotFoundException When originFile doesn't exist
  30.      * @throws IOException           When copy fails
  31.      */
  32.     public function copy(string $originFilestring $targetFilebool $overwriteNewerFiles false)
  33.     {
  34.         $originIsLocal stream_is_local($originFile) || === stripos($originFile'file://');
  35.         if ($originIsLocal && !is_file($originFile)) {
  36.             throw new FileNotFoundException(sprintf('Failed to copy "%s" because file does not exist.'$originFile), 0null$originFile);
  37.         }
  38.         $this->mkdir(\dirname($targetFile));
  39.         $doCopy true;
  40.         if (!$overwriteNewerFiles && null === parse_url($originFile\PHP_URL_HOST) && is_file($targetFile)) {
  41.             $doCopy filemtime($originFile) > filemtime($targetFile);
  42.         }
  43.         if ($doCopy) {
  44.             // https://bugs.php.net/64634
  45.             if (!$source self::box('fopen'$originFile'r')) {
  46.                 throw new IOException(sprintf('Failed to copy "%s" to "%s" because source file could not be opened for reading: '$originFile$targetFile).self::$lastError0null$originFile);
  47.             }
  48.             // Stream context created to allow files overwrite when using FTP stream wrapper - disabled by default
  49.             if (!$target self::box('fopen'$targetFile'w'falsestream_context_create(['ftp' => ['overwrite' => true]]))) {
  50.                 throw new IOException(sprintf('Failed to copy "%s" to "%s" because target file could not be opened for writing: '$originFile$targetFile).self::$lastError0null$originFile);
  51.             }
  52.             $bytesCopied stream_copy_to_stream($source$target);
  53.             fclose($source);
  54.             fclose($target);
  55.             unset($source$target);
  56.             if (!is_file($targetFile)) {
  57.                 throw new IOException(sprintf('Failed to copy "%s" to "%s".'$originFile$targetFile), 0null$originFile);
  58.             }
  59.             if ($originIsLocal) {
  60.                 // Like `cp`, preserve executable permission bits
  61.                 self::box('chmod'$targetFilefileperms($targetFile) | (fileperms($originFile) & 0111));
  62.                 // Like `cp`, preserve the file modification time
  63.                 self::box('touch'$targetFilefilemtime($originFile));
  64.                 if ($bytesCopied !== $bytesOrigin filesize($originFile)) {
  65.                     throw new IOException(sprintf('Failed to copy the whole content of "%s" to "%s" (%g of %g bytes copied).'$originFile$targetFile$bytesCopied$bytesOrigin), 0null$originFile);
  66.                 }
  67.             }
  68.         }
  69.     }
  70.     /**
  71.      * Creates a directory recursively.
  72.      *
  73.      * @param string|iterable $dirs The directory path
  74.      *
  75.      * @throws IOException On any directory creation failure
  76.      */
  77.     public function mkdir($dirsint $mode 0777)
  78.     {
  79.         foreach ($this->toIterable($dirs) as $dir) {
  80.             if (is_dir($dir)) {
  81.                 continue;
  82.             }
  83.             if (!self::box('mkdir'$dir$modetrue) && !is_dir($dir)) {
  84.                 throw new IOException(sprintf('Failed to create "%s": '$dir).self::$lastError0null$dir);
  85.             }
  86.         }
  87.     }
  88.     /**
  89.      * Checks the existence of files or directories.
  90.      *
  91.      * @param string|iterable $files A filename, an array of files, or a \Traversable instance to check
  92.      *
  93.      * @return bool
  94.      */
  95.     public function exists($files)
  96.     {
  97.         $maxPathLength \PHP_MAXPATHLEN 2;
  98.         foreach ($this->toIterable($files) as $file) {
  99.             if (\strlen($file) > $maxPathLength) {
  100.                 throw new IOException(sprintf('Could not check if file exist because path length exceeds %d characters.'$maxPathLength), 0null$file);
  101.             }
  102.             if (!file_exists($file)) {
  103.                 return false;
  104.             }
  105.         }
  106.         return true;
  107.     }
  108.     /**
  109.      * Sets access and modification time of file.
  110.      *
  111.      * @param string|iterable $files A filename, an array of files, or a \Traversable instance to create
  112.      * @param int|null        $time  The touch time as a Unix timestamp, if not supplied the current system time is used
  113.      * @param int|null        $atime The access time as a Unix timestamp, if not supplied the current system time is used
  114.      *
  115.      * @throws IOException When touch fails
  116.      */
  117.     public function touch($files, ?int $time null, ?int $atime null)
  118.     {
  119.         foreach ($this->toIterable($files) as $file) {
  120.             if (!($time self::box('touch'$file$time$atime) : self::box('touch'$file))) {
  121.                 throw new IOException(sprintf('Failed to touch "%s": '$file).self::$lastError0null$file);
  122.             }
  123.         }
  124.     }
  125.     /**
  126.      * Removes files or directories.
  127.      *
  128.      * @param string|iterable $files A filename, an array of files, or a \Traversable instance to remove
  129.      *
  130.      * @throws IOException When removal fails
  131.      */
  132.     public function remove($files)
  133.     {
  134.         if ($files instanceof \Traversable) {
  135.             $files iterator_to_array($filesfalse);
  136.         } elseif (!\is_array($files)) {
  137.             $files = [$files];
  138.         }
  139.         self::doRemove($filesfalse);
  140.     }
  141.     private static function doRemove(array $filesbool $isRecursive): void
  142.     {
  143.         $files array_reverse($files);
  144.         foreach ($files as $file) {
  145.             if (is_link($file)) {
  146.                 // See https://bugs.php.net/52176
  147.                 if (!(self::box('unlink'$file) || '\\' !== \DIRECTORY_SEPARATOR || self::box('rmdir'$file)) && file_exists($file)) {
  148.                     throw new IOException(sprintf('Failed to remove symlink "%s": '$file).self::$lastError);
  149.                 }
  150.             } elseif (is_dir($file)) {
  151.                 if (!$isRecursive) {
  152.                     $tmpName \dirname(realpath($file)).'/.'.strrev(strtr(base64_encode(random_bytes(2)), '/=''-_'));
  153.                     if (file_exists($tmpName)) {
  154.                         try {
  155.                             self::doRemove([$tmpName], true);
  156.                         } catch (IOException $e) {
  157.                         }
  158.                     }
  159.                     if (!file_exists($tmpName) && self::box('rename'$file$tmpName)) {
  160.                         $origFile $file;
  161.                         $file $tmpName;
  162.                     } else {
  163.                         $origFile null;
  164.                     }
  165.                 }
  166.                 $files = new \FilesystemIterator($file\FilesystemIterator::CURRENT_AS_PATHNAME \FilesystemIterator::SKIP_DOTS);
  167.                 self::doRemove(iterator_to_array($filestrue), true);
  168.                 if (!self::box('rmdir'$file) && file_exists($file) && !$isRecursive) {
  169.                     $lastError self::$lastError;
  170.                     if (null !== $origFile && self::box('rename'$file$origFile)) {
  171.                         $file $origFile;
  172.                     }
  173.                     throw new IOException(sprintf('Failed to remove directory "%s": '$file).$lastError);
  174.                 }
  175.             } elseif (!self::box('unlink'$file) && ((self::$lastError && str_contains(self::$lastError'Permission denied')) || file_exists($file))) {
  176.                 throw new IOException(sprintf('Failed to remove file "%s": '$file).self::$lastError);
  177.             }
  178.         }
  179.     }
  180.     /**
  181.      * Change mode for an array of files or directories.
  182.      *
  183.      * @param string|iterable $files     A filename, an array of files, or a \Traversable instance to change mode
  184.      * @param int             $mode      The new mode (octal)
  185.      * @param int             $umask     The mode mask (octal)
  186.      * @param bool            $recursive Whether change the mod recursively or not
  187.      *
  188.      * @throws IOException When the change fails
  189.      */
  190.     public function chmod($filesint $modeint $umask 0000bool $recursive false)
  191.     {
  192.         foreach ($this->toIterable($files) as $file) {
  193.             if ((\PHP_VERSION_ID 80000 || \is_int($mode)) && !self::box('chmod'$file$mode & ~$umask)) {
  194.                 throw new IOException(sprintf('Failed to chmod file "%s": '$file).self::$lastError0null$file);
  195.             }
  196.             if ($recursive && is_dir($file) && !is_link($file)) {
  197.                 $this->chmod(new \FilesystemIterator($file), $mode$umasktrue);
  198.             }
  199.         }
  200.     }
  201.     /**
  202.      * Change the owner of an array of files or directories.
  203.      *
  204.      * @param string|iterable $files     A filename, an array of files, or a \Traversable instance to change owner
  205.      * @param string|int      $user      A user name or number
  206.      * @param bool            $recursive Whether change the owner recursively or not
  207.      *
  208.      * @throws IOException When the change fails
  209.      */
  210.     public function chown($files$userbool $recursive false)
  211.     {
  212.         foreach ($this->toIterable($files) as $file) {
  213.             if ($recursive && is_dir($file) && !is_link($file)) {
  214.                 $this->chown(new \FilesystemIterator($file), $usertrue);
  215.             }
  216.             if (is_link($file) && \function_exists('lchown')) {
  217.                 if (!self::box('lchown'$file$user)) {
  218.                     throw new IOException(sprintf('Failed to chown file "%s": '$file).self::$lastError0null$file);
  219.                 }
  220.             } else {
  221.                 if (!self::box('chown'$file$user)) {
  222.                     throw new IOException(sprintf('Failed to chown file "%s": '$file).self::$lastError0null$file);
  223.                 }
  224.             }
  225.         }
  226.     }
  227.     /**
  228.      * Change the group of an array of files or directories.
  229.      *
  230.      * @param string|iterable $files     A filename, an array of files, or a \Traversable instance to change group
  231.      * @param string|int      $group     A group name or number
  232.      * @param bool            $recursive Whether change the group recursively or not
  233.      *
  234.      * @throws IOException When the change fails
  235.      */
  236.     public function chgrp($files$groupbool $recursive false)
  237.     {
  238.         foreach ($this->toIterable($files) as $file) {
  239.             if ($recursive && is_dir($file) && !is_link($file)) {
  240.                 $this->chgrp(new \FilesystemIterator($file), $grouptrue);
  241.             }
  242.             if (is_link($file) && \function_exists('lchgrp')) {
  243.                 if (!self::box('lchgrp'$file$group)) {
  244.                     throw new IOException(sprintf('Failed to chgrp file "%s": '$file).self::$lastError0null$file);
  245.                 }
  246.             } else {
  247.                 if (!self::box('chgrp'$file$group)) {
  248.                     throw new IOException(sprintf('Failed to chgrp file "%s": '$file).self::$lastError0null$file);
  249.                 }
  250.             }
  251.         }
  252.     }
  253.     /**
  254.      * Renames a file or a directory.
  255.      *
  256.      * @throws IOException When target file or directory already exists
  257.      * @throws IOException When origin cannot be renamed
  258.      */
  259.     public function rename(string $originstring $targetbool $overwrite false)
  260.     {
  261.         // we check that target does not exist
  262.         if (!$overwrite && $this->isReadable($target)) {
  263.             throw new IOException(sprintf('Cannot rename because the target "%s" already exists.'$target), 0null$target);
  264.         }
  265.         if (!self::box('rename'$origin$target)) {
  266.             if (is_dir($origin)) {
  267.                 // See https://bugs.php.net/54097 & https://php.net/rename#113943
  268.                 $this->mirror($origin$targetnull, ['override' => $overwrite'delete' => $overwrite]);
  269.                 $this->remove($origin);
  270.                 return;
  271.             }
  272.             throw new IOException(sprintf('Cannot rename "%s" to "%s": '$origin$target).self::$lastError0null$target);
  273.         }
  274.     }
  275.     /**
  276.      * Tells whether a file exists and is readable.
  277.      *
  278.      * @throws IOException When windows path is longer than 258 characters
  279.      */
  280.     private function isReadable(string $filename): bool
  281.     {
  282.         $maxPathLength \PHP_MAXPATHLEN 2;
  283.         if (\strlen($filename) > $maxPathLength) {
  284.             throw new IOException(sprintf('Could not check if file is readable because path length exceeds %d characters.'$maxPathLength), 0null$filename);
  285.         }
  286.         return is_readable($filename);
  287.     }
  288.     /**
  289.      * Creates a symbolic link or copy a directory.
  290.      *
  291.      * @throws IOException When symlink fails
  292.      */
  293.     public function symlink(string $originDirstring $targetDirbool $copyOnWindows false)
  294.     {
  295.         self::assertFunctionExists('symlink');
  296.         if ('\\' === \DIRECTORY_SEPARATOR) {
  297.             $originDir strtr($originDir'/''\\');
  298.             $targetDir strtr($targetDir'/''\\');
  299.             if ($copyOnWindows) {
  300.                 $this->mirror($originDir$targetDir);
  301.                 return;
  302.             }
  303.         }
  304.         $this->mkdir(\dirname($targetDir));
  305.         if (is_link($targetDir)) {
  306.             if (readlink($targetDir) === $originDir) {
  307.                 return;
  308.             }
  309.             $this->remove($targetDir);
  310.         }
  311.         if (!self::box('symlink'$originDir$targetDir)) {
  312.             $this->linkException($originDir$targetDir'symbolic');
  313.         }
  314.     }
  315.     /**
  316.      * Creates a hard link, or several hard links to a file.
  317.      *
  318.      * @param string|string[] $targetFiles The target file(s)
  319.      *
  320.      * @throws FileNotFoundException When original file is missing or not a file
  321.      * @throws IOException           When link fails, including if link already exists
  322.      */
  323.     public function hardlink(string $originFile$targetFiles)
  324.     {
  325.         self::assertFunctionExists('link');
  326.         if (!$this->exists($originFile)) {
  327.             throw new FileNotFoundException(null0null$originFile);
  328.         }
  329.         if (!is_file($originFile)) {
  330.             throw new FileNotFoundException(sprintf('Origin file "%s" is not a file.'$originFile));
  331.         }
  332.         foreach ($this->toIterable($targetFiles) as $targetFile) {
  333.             if (is_file($targetFile)) {
  334.                 if (fileinode($originFile) === fileinode($targetFile)) {
  335.                     continue;
  336.                 }
  337.                 $this->remove($targetFile);
  338.             }
  339.             if (!self::box('link'$originFile$targetFile)) {
  340.                 $this->linkException($originFile$targetFile'hard');
  341.             }
  342.         }
  343.     }
  344.     /**
  345.      * @param string $linkType Name of the link type, typically 'symbolic' or 'hard'
  346.      */
  347.     private function linkException(string $originstring $targetstring $linkType)
  348.     {
  349.         if (self::$lastError) {
  350.             if ('\\' === \DIRECTORY_SEPARATOR && str_contains(self::$lastError'error code(1314)')) {
  351.                 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), 0null$target);
  352.             }
  353.         }
  354.         throw new IOException(sprintf('Failed to create "%s" link from "%s" to "%s": '$linkType$origin$target).self::$lastError0null$target);
  355.     }
  356.     /**
  357.      * Resolves links in paths.
  358.      *
  359.      * With $canonicalize = false (default)
  360.      *      - if $path does not exist or is not a link, returns null
  361.      *      - if $path is a link, returns the next direct target of the link without considering the existence of the target
  362.      *
  363.      * With $canonicalize = true
  364.      *      - if $path does not exist, returns null
  365.      *      - if $path exists, returns its absolute fully resolved final version
  366.      *
  367.      * @return string|null
  368.      */
  369.     public function readlink(string $pathbool $canonicalize false)
  370.     {
  371.         if (!$canonicalize && !is_link($path)) {
  372.             return null;
  373.         }
  374.         if ($canonicalize) {
  375.             if (!$this->exists($path)) {
  376.                 return null;
  377.             }
  378.             if ('\\' === \DIRECTORY_SEPARATOR && \PHP_VERSION_ID 70410) {
  379.                 $path readlink($path);
  380.             }
  381.             return realpath($path);
  382.         }
  383.         if ('\\' === \DIRECTORY_SEPARATOR && \PHP_VERSION_ID 70400) {
  384.             return realpath($path);
  385.         }
  386.         return readlink($path);
  387.     }
  388.     /**
  389.      * Given an existing path, convert it to a path relative to a given starting path.
  390.      *
  391.      * @return string
  392.      */
  393.     public function makePathRelative(string $endPathstring $startPath)
  394.     {
  395.         if (!$this->isAbsolutePath($startPath)) {
  396.             throw new InvalidArgumentException(sprintf('The start path "%s" is not absolute.'$startPath));
  397.         }
  398.         if (!$this->isAbsolutePath($endPath)) {
  399.             throw new InvalidArgumentException(sprintf('The end path "%s" is not absolute.'$endPath));
  400.         }
  401.         // Normalize separators on Windows
  402.         if ('\\' === \DIRECTORY_SEPARATOR) {
  403.             $endPath str_replace('\\''/'$endPath);
  404.             $startPath str_replace('\\''/'$startPath);
  405.         }
  406.         $splitDriveLetter = function ($path) {
  407.             return (\strlen($path) > && ':' === $path[1] && '/' === $path[2] && ctype_alpha($path[0]))
  408.                 ? [substr($path2), strtoupper($path[0])]
  409.                 : [$pathnull];
  410.         };
  411.         $splitPath = function ($path) {
  412.             $result = [];
  413.             foreach (explode('/'trim($path'/')) as $segment) {
  414.                 if ('..' === $segment) {
  415.                     array_pop($result);
  416.                 } elseif ('.' !== $segment && '' !== $segment) {
  417.                     $result[] = $segment;
  418.                 }
  419.             }
  420.             return $result;
  421.         };
  422.         [$endPath$endDriveLetter] = $splitDriveLetter($endPath);
  423.         [$startPath$startDriveLetter] = $splitDriveLetter($startPath);
  424.         $startPathArr $splitPath($startPath);
  425.         $endPathArr $splitPath($endPath);
  426.         if ($endDriveLetter && $startDriveLetter && $endDriveLetter != $startDriveLetter) {
  427.             // End path is on another drive, so no relative path exists
  428.             return $endDriveLetter.':/'.($endPathArr implode('/'$endPathArr).'/' '');
  429.         }
  430.         // Find for which directory the common path stops
  431.         $index 0;
  432.         while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) {
  433.             ++$index;
  434.         }
  435.         // Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels)
  436.         if (=== \count($startPathArr) && '' === $startPathArr[0]) {
  437.             $depth 0;
  438.         } else {
  439.             $depth \count($startPathArr) - $index;
  440.         }
  441.         // Repeated "../" for each level need to reach the common path
  442.         $traverser str_repeat('../'$depth);
  443.         $endPathRemainder implode('/'\array_slice($endPathArr$index));
  444.         // Construct $endPath from traversing to the common path, then to the remaining $endPath
  445.         $relativePath $traverser.('' !== $endPathRemainder $endPathRemainder.'/' '');
  446.         return '' === $relativePath './' $relativePath;
  447.     }
  448.     /**
  449.      * Mirrors a directory to another.
  450.      *
  451.      * Copies files and directories from the origin directory into the target directory. By default:
  452.      *
  453.      *  - existing files in the target directory will be overwritten, except if they are newer (see the `override` option)
  454.      *  - files in the target directory that do not exist in the source directory will not be deleted (see the `delete` option)
  455.      *
  456.      * @param \Traversable|null $iterator Iterator that filters which files and directories to copy, if null a recursive iterator is created
  457.      * @param array             $options  An array of boolean options
  458.      *                                    Valid options are:
  459.      *                                    - $options['override'] If true, target files newer than origin files are overwritten (see copy(), defaults to false)
  460.      *                                    - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink(), defaults to false)
  461.      *                                    - $options['delete'] Whether to delete files that are not in the source directory (defaults to false)
  462.      *
  463.      * @throws IOException When file type is unknown
  464.      */
  465.     public function mirror(string $originDirstring $targetDir, ?\Traversable $iterator null, array $options = [])
  466.     {
  467.         $targetDir rtrim($targetDir'/\\');
  468.         $originDir rtrim($originDir'/\\');
  469.         $originDirLen \strlen($originDir);
  470.         if (!$this->exists($originDir)) {
  471.             throw new IOException(sprintf('The origin directory specified "%s" was not found.'$originDir), 0null$originDir);
  472.         }
  473.         // Iterate in destination folder to remove obsolete entries
  474.         if ($this->exists($targetDir) && isset($options['delete']) && $options['delete']) {
  475.             $deleteIterator $iterator;
  476.             if (null === $deleteIterator) {
  477.                 $flags \FilesystemIterator::SKIP_DOTS;
  478.                 $deleteIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($targetDir$flags), \RecursiveIteratorIterator::CHILD_FIRST);
  479.             }
  480.             $targetDirLen \strlen($targetDir);
  481.             foreach ($deleteIterator as $file) {
  482.                 $origin $originDir.substr($file->getPathname(), $targetDirLen);
  483.                 if (!$this->exists($origin)) {
  484.                     $this->remove($file);
  485.                 }
  486.             }
  487.         }
  488.         $copyOnWindows $options['copy_on_windows'] ?? false;
  489.         if (null === $iterator) {
  490.             $flags $copyOnWindows \FilesystemIterator::SKIP_DOTS \FilesystemIterator::FOLLOW_SYMLINKS \FilesystemIterator::SKIP_DOTS;
  491.             $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($originDir$flags), \RecursiveIteratorIterator::SELF_FIRST);
  492.         }
  493.         $this->mkdir($targetDir);
  494.         $filesCreatedWhileMirroring = [];
  495.         foreach ($iterator as $file) {
  496.             if ($file->getPathname() === $targetDir || $file->getRealPath() === $targetDir || isset($filesCreatedWhileMirroring[$file->getRealPath()])) {
  497.                 continue;
  498.             }
  499.             $target $targetDir.substr($file->getPathname(), $originDirLen);
  500.             $filesCreatedWhileMirroring[$target] = true;
  501.             if (!$copyOnWindows && is_link($file)) {
  502.                 $this->symlink($file->getLinkTarget(), $target);
  503.             } elseif (is_dir($file)) {
  504.                 $this->mkdir($target);
  505.             } elseif (is_file($file)) {
  506.                 $this->copy($file$target$options['override'] ?? false);
  507.             } else {
  508.                 throw new IOException(sprintf('Unable to guess "%s" file type.'$file), 0null$file);
  509.             }
  510.         }
  511.     }
  512.     /**
  513.      * Returns whether the file path is an absolute path.
  514.      *
  515.      * @return bool
  516.      */
  517.     public function isAbsolutePath(string $file)
  518.     {
  519.         return '' !== $file && (strspn($file'/\\'01)
  520.             || (\strlen($file) > && ctype_alpha($file[0])
  521.                 && ':' === $file[1]
  522.                 && strspn($file'/\\'21)
  523.             )
  524.             || null !== parse_url($file\PHP_URL_SCHEME)
  525.         );
  526.     }
  527.     /**
  528.      * Creates a temporary file with support for custom stream wrappers.
  529.      *
  530.      * @param string $prefix The prefix of the generated temporary filename
  531.      *                       Note: Windows uses only the first three characters of prefix
  532.      * @param string $suffix The suffix of the generated temporary filename
  533.      *
  534.      * @return string The new temporary filename (with path), or throw an exception on failure
  535.      */
  536.     public function tempnam(string $dirstring $prefix/* , string $suffix = '' */)
  537.     {
  538.         $suffix \func_num_args() > func_get_arg(2) : '';
  539.         [$scheme$hierarchy] = $this->getSchemeAndHierarchy($dir);
  540.         // If no scheme or scheme is "file" or "gs" (Google Cloud) create temp file in local filesystem
  541.         if ((null === $scheme || 'file' === $scheme || 'gs' === $scheme) && '' === $suffix) {
  542.             // If tempnam failed or no scheme return the filename otherwise prepend the scheme
  543.             if ($tmpFile self::box('tempnam'$hierarchy$prefix)) {
  544.                 if (null !== $scheme && 'gs' !== $scheme) {
  545.                     return $scheme.'://'.$tmpFile;
  546.                 }
  547.                 return $tmpFile;
  548.             }
  549.             throw new IOException('A temporary file could not be created: '.self::$lastError);
  550.         }
  551.         // Loop until we create a valid temp file or have reached 10 attempts
  552.         for ($i 0$i 10; ++$i) {
  553.             // Create a unique filename
  554.             $tmpFile $dir.'/'.$prefix.uniqid(mt_rand(), true).$suffix;
  555.             // Use fopen instead of file_exists as some streams do not support stat
  556.             // Use mode 'x+' to atomically check existence and create to avoid a TOCTOU vulnerability
  557.             if (!$handle self::box('fopen'$tmpFile'x+')) {
  558.                 continue;
  559.             }
  560.             // Close the file if it was successfully opened
  561.             self::box('fclose'$handle);
  562.             return $tmpFile;
  563.         }
  564.         throw new IOException('A temporary file could not be created: '.self::$lastError);
  565.     }
  566.     /**
  567.      * Atomically dumps content into a file.
  568.      *
  569.      * @param string|resource $content The data to write into the file
  570.      *
  571.      * @throws IOException if the file cannot be written to
  572.      */
  573.     public function dumpFile(string $filename$content)
  574.     {
  575.         if (\is_array($content)) {
  576.             throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.'__METHOD__));
  577.         }
  578.         $dir \dirname($filename);
  579.         if (is_link($filename) && $linkTarget $this->readlink($filename)) {
  580.             $this->dumpFile(Path::makeAbsolute($linkTarget$dir), $content);
  581.             return;
  582.         }
  583.         if (!is_dir($dir)) {
  584.             $this->mkdir($dir);
  585.         }
  586.         // Will create a temp file with 0600 access rights
  587.         // when the filesystem supports chmod.
  588.         $tmpFile $this->tempnam($dirbasename($filename));
  589.         try {
  590.             if (false === self::box('file_put_contents'$tmpFile$content)) {
  591.                 throw new IOException(sprintf('Failed to write file "%s": '$filename).self::$lastError0null$filename);
  592.             }
  593.             self::box('chmod'$tmpFile, @fileperms($filename) ?: 0666 & ~umask());
  594.             $this->rename($tmpFile$filenametrue);
  595.         } finally {
  596.             if (file_exists($tmpFile)) {
  597.                 self::box('unlink'$tmpFile);
  598.             }
  599.         }
  600.     }
  601.     /**
  602.      * Appends content to an existing file.
  603.      *
  604.      * @param string|resource $content The content to append
  605.      * @param bool            $lock    Whether the file should be locked when writing to it
  606.      *
  607.      * @throws IOException If the file is not writable
  608.      */
  609.     public function appendToFile(string $filename$content/* , bool $lock = false */)
  610.     {
  611.         if (\is_array($content)) {
  612.             throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.'__METHOD__));
  613.         }
  614.         $dir \dirname($filename);
  615.         if (!is_dir($dir)) {
  616.             $this->mkdir($dir);
  617.         }
  618.         $lock \func_num_args() > && func_get_arg(2);
  619.         if (false === self::box('file_put_contents'$filename$content\FILE_APPEND | ($lock \LOCK_EX 0))) {
  620.             throw new IOException(sprintf('Failed to write file "%s": '$filename).self::$lastError0null$filename);
  621.         }
  622.     }
  623.     private function toIterable($files): iterable
  624.     {
  625.         return is_iterable($files) ? $files : [$files];
  626.     }
  627.     /**
  628.      * Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> [file, tmp]).
  629.      */
  630.     private function getSchemeAndHierarchy(string $filename): array
  631.     {
  632.         $components explode('://'$filename2);
  633.         return === \count($components) ? [$components[0], $components[1]] : [null$components[0]];
  634.     }
  635.     private static function assertFunctionExists(string $func): void
  636.     {
  637.         if (!\function_exists($func)) {
  638.             throw new IOException(sprintf('Unable to perform filesystem operation because the "%s()" function has been disabled.'$func));
  639.         }
  640.     }
  641.     /**
  642.      * @param mixed ...$args
  643.      *
  644.      * @return mixed
  645.      */
  646.     private static function box(string $func, ...$args)
  647.     {
  648.         self::assertFunctionExists($func);
  649.         self::$lastError null;
  650.         set_error_handler(__CLASS__.'::handleError');
  651.         try {
  652.             return $func(...$args);
  653.         } finally {
  654.             restore_error_handler();
  655.         }
  656.     }
  657.     /**
  658.      * @internal
  659.      */
  660.     public static function handleError(int $typestring $msg)
  661.     {
  662.         self::$lastError $msg;
  663.     }
  664. }