Filesystem.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803
  1. <?php
  2. namespace Illuminate\Filesystem;
  3. use ErrorException;
  4. use FilesystemIterator;
  5. use Illuminate\Contracts\Filesystem\FileNotFoundException;
  6. use Illuminate\Support\LazyCollection;
  7. use Illuminate\Support\Traits\Conditionable;
  8. use Illuminate\Support\Traits\Macroable;
  9. use RuntimeException;
  10. use SplFileObject;
  11. use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem;
  12. use Symfony\Component\Finder\Finder;
  13. use Symfony\Component\Mime\MimeTypes;
  14. class Filesystem
  15. {
  16. use Conditionable, Macroable;
  17. /**
  18. * Determine if a file or directory exists.
  19. *
  20. * @param string $path
  21. * @return bool
  22. */
  23. public function exists($path)
  24. {
  25. return file_exists($path);
  26. }
  27. /**
  28. * Determine if a file or directory is missing.
  29. *
  30. * @param string $path
  31. * @return bool
  32. */
  33. public function missing($path)
  34. {
  35. return ! $this->exists($path);
  36. }
  37. /**
  38. * Get the contents of a file.
  39. *
  40. * @param string $path
  41. * @param bool $lock
  42. * @return string
  43. *
  44. * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
  45. */
  46. public function get($path, $lock = false)
  47. {
  48. if ($this->isFile($path)) {
  49. return $lock ? $this->sharedGet($path) : file_get_contents($path);
  50. }
  51. throw new FileNotFoundException("File does not exist at path {$path}.");
  52. }
  53. /**
  54. * Get the contents of a file as decoded JSON.
  55. *
  56. * @param string $path
  57. * @param int $flags
  58. * @param bool $lock
  59. * @return array
  60. *
  61. * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
  62. */
  63. public function json($path, $flags = 0, $lock = false)
  64. {
  65. return json_decode($this->get($path, $lock), true, 512, $flags);
  66. }
  67. /**
  68. * Get contents of a file with shared access.
  69. *
  70. * @param string $path
  71. * @return string
  72. */
  73. public function sharedGet($path)
  74. {
  75. $contents = '';
  76. $handle = fopen($path, 'rb');
  77. if ($handle) {
  78. try {
  79. if (flock($handle, LOCK_SH)) {
  80. clearstatcache(true, $path);
  81. $contents = stream_get_contents($handle);
  82. flock($handle, LOCK_UN);
  83. }
  84. } finally {
  85. fclose($handle);
  86. }
  87. }
  88. return $contents;
  89. }
  90. /**
  91. * Get the returned value of a file.
  92. *
  93. * @param string $path
  94. * @param array $data
  95. * @return mixed
  96. *
  97. * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
  98. */
  99. public function getRequire($path, array $data = [])
  100. {
  101. if ($this->isFile($path)) {
  102. $__path = $path;
  103. $__data = $data;
  104. return (static function () use ($__path, $__data) {
  105. extract($__data, EXTR_SKIP);
  106. return require $__path;
  107. })();
  108. }
  109. throw new FileNotFoundException("File does not exist at path {$path}.");
  110. }
  111. /**
  112. * Require the given file once.
  113. *
  114. * @param string $path
  115. * @param array $data
  116. * @return mixed
  117. *
  118. * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
  119. */
  120. public function requireOnce($path, array $data = [])
  121. {
  122. if ($this->isFile($path)) {
  123. $__path = $path;
  124. $__data = $data;
  125. return (static function () use ($__path, $__data) {
  126. extract($__data, EXTR_SKIP);
  127. return require_once $__path;
  128. })();
  129. }
  130. throw new FileNotFoundException("File does not exist at path {$path}.");
  131. }
  132. /**
  133. * Get the contents of a file one line at a time.
  134. *
  135. * @param string $path
  136. * @return \Illuminate\Support\LazyCollection
  137. *
  138. * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
  139. */
  140. public function lines($path)
  141. {
  142. if (! $this->isFile($path)) {
  143. throw new FileNotFoundException(
  144. "File does not exist at path {$path}."
  145. );
  146. }
  147. return new LazyCollection(function () use ($path) {
  148. $file = new SplFileObject($path);
  149. $file->setFlags(SplFileObject::DROP_NEW_LINE);
  150. while (! $file->eof()) {
  151. yield $file->fgets();
  152. }
  153. });
  154. }
  155. /**
  156. * Get the hash of the file at the given path.
  157. *
  158. * @param string $path
  159. * @param string $algorithm
  160. * @return string|false
  161. */
  162. public function hash($path, $algorithm = 'md5')
  163. {
  164. return hash_file($algorithm, $path);
  165. }
  166. /**
  167. * Write the contents of a file.
  168. *
  169. * @param string $path
  170. * @param string $contents
  171. * @param bool $lock
  172. * @return int|bool
  173. */
  174. public function put($path, $contents, $lock = false)
  175. {
  176. return file_put_contents($path, $contents, $lock ? LOCK_EX : 0);
  177. }
  178. /**
  179. * Write the contents of a file, replacing it atomically if it already exists.
  180. *
  181. * @param string $path
  182. * @param string $content
  183. * @param int|null $mode
  184. * @return void
  185. */
  186. public function replace($path, $content, $mode = null)
  187. {
  188. // If the path already exists and is a symlink, get the real path...
  189. clearstatcache(true, $path);
  190. $path = realpath($path) ?: $path;
  191. $tempPath = tempnam(dirname($path), basename($path));
  192. // Fix permissions of tempPath because `tempnam()` creates it with permissions set to 0600...
  193. if (! is_null($mode)) {
  194. @chmod($tempPath, $mode);
  195. } else {
  196. @chmod($tempPath, 0777 - umask());
  197. }
  198. file_put_contents($tempPath, $content);
  199. rename($tempPath, $path);
  200. }
  201. /**
  202. * Replace a given string within a given file.
  203. *
  204. * @param array|string $search
  205. * @param array|string $replace
  206. * @param string $path
  207. * @return void
  208. */
  209. public function replaceInFile($search, $replace, $path)
  210. {
  211. file_put_contents($path, str_replace($search, $replace, file_get_contents($path)));
  212. }
  213. /**
  214. * Prepend to a file.
  215. *
  216. * @param string $path
  217. * @param string $data
  218. * @return int
  219. */
  220. public function prepend($path, $data)
  221. {
  222. if ($this->exists($path)) {
  223. return $this->put($path, $data.$this->get($path));
  224. }
  225. return $this->put($path, $data);
  226. }
  227. /**
  228. * Append to a file.
  229. *
  230. * @param string $path
  231. * @param string $data
  232. * @param bool $lock
  233. * @return int
  234. */
  235. public function append($path, $data, $lock = false)
  236. {
  237. return file_put_contents($path, $data, FILE_APPEND | ($lock ? LOCK_EX : 0));
  238. }
  239. /**
  240. * Get or set UNIX mode of a file or directory.
  241. *
  242. * @param string $path
  243. * @param int|null $mode
  244. * @return mixed
  245. */
  246. public function chmod($path, $mode = null)
  247. {
  248. if ($mode) {
  249. return chmod($path, $mode);
  250. }
  251. return substr(sprintf('%o', fileperms($path)), -4);
  252. }
  253. /**
  254. * Delete the file at a given path.
  255. *
  256. * @param string|array $paths
  257. * @return bool
  258. */
  259. public function delete($paths)
  260. {
  261. $paths = is_array($paths) ? $paths : func_get_args();
  262. $success = true;
  263. foreach ($paths as $path) {
  264. try {
  265. if (@unlink($path)) {
  266. clearstatcache(false, $path);
  267. } else {
  268. $success = false;
  269. }
  270. } catch (ErrorException) {
  271. $success = false;
  272. }
  273. }
  274. return $success;
  275. }
  276. /**
  277. * Move a file to a new location.
  278. *
  279. * @param string $path
  280. * @param string $target
  281. * @return bool
  282. */
  283. public function move($path, $target)
  284. {
  285. return rename($path, $target);
  286. }
  287. /**
  288. * Copy a file to a new location.
  289. *
  290. * @param string $path
  291. * @param string $target
  292. * @return bool
  293. */
  294. public function copy($path, $target)
  295. {
  296. return copy($path, $target);
  297. }
  298. /**
  299. * Create a symlink to the target file or directory. On Windows, a hard link is created if the target is a file.
  300. *
  301. * @param string $target
  302. * @param string $link
  303. * @return bool|null
  304. */
  305. public function link($target, $link)
  306. {
  307. if (! windows_os()) {
  308. if (function_exists('symlink')) {
  309. return symlink($target, $link);
  310. } else {
  311. return exec('ln -s '.escapeshellarg($target).' '.escapeshellarg($link)) !== false;
  312. }
  313. }
  314. $mode = $this->isDirectory($target) ? 'J' : 'H';
  315. exec("mklink /{$mode} ".escapeshellarg($link).' '.escapeshellarg($target));
  316. }
  317. /**
  318. * Create a relative symlink to the target file or directory.
  319. *
  320. * @param string $target
  321. * @param string $link
  322. * @return void
  323. *
  324. * @throws \RuntimeException
  325. */
  326. public function relativeLink($target, $link)
  327. {
  328. if (! class_exists(SymfonyFilesystem::class)) {
  329. throw new RuntimeException(
  330. 'To enable support for relative links, please install the symfony/filesystem package.'
  331. );
  332. }
  333. $relativeTarget = (new SymfonyFilesystem)->makePathRelative($target, dirname($link));
  334. $this->link($this->isFile($target) ? rtrim($relativeTarget, '/') : $relativeTarget, $link);
  335. }
  336. /**
  337. * Extract the file name from a file path.
  338. *
  339. * @param string $path
  340. * @return string
  341. */
  342. public function name($path)
  343. {
  344. return pathinfo($path, PATHINFO_FILENAME);
  345. }
  346. /**
  347. * Extract the trailing name component from a file path.
  348. *
  349. * @param string $path
  350. * @return string
  351. */
  352. public function basename($path)
  353. {
  354. return pathinfo($path, PATHINFO_BASENAME);
  355. }
  356. /**
  357. * Extract the parent directory from a file path.
  358. *
  359. * @param string $path
  360. * @return string
  361. */
  362. public function dirname($path)
  363. {
  364. return pathinfo($path, PATHINFO_DIRNAME);
  365. }
  366. /**
  367. * Extract the file extension from a file path.
  368. *
  369. * @param string $path
  370. * @return string
  371. */
  372. public function extension($path)
  373. {
  374. return pathinfo($path, PATHINFO_EXTENSION);
  375. }
  376. /**
  377. * Guess the file extension from the MIME type of a given file.
  378. *
  379. * @param string $path
  380. * @return string|null
  381. *
  382. * @throws \RuntimeException
  383. */
  384. public function guessExtension($path)
  385. {
  386. if (! class_exists(MimeTypes::class)) {
  387. throw new RuntimeException(
  388. 'To enable support for guessing extensions, please install the symfony/mime package.'
  389. );
  390. }
  391. return (new MimeTypes)->getExtensions($this->mimeType($path))[0] ?? null;
  392. }
  393. /**
  394. * Get the file type of a given file.
  395. *
  396. * @param string $path
  397. * @return string|false
  398. */
  399. public function type($path)
  400. {
  401. return filetype($path);
  402. }
  403. /**
  404. * Get the MIME type of a given file.
  405. *
  406. * @param string $path
  407. * @return string|false
  408. */
  409. public function mimeType($path)
  410. {
  411. return finfo_file(finfo_open(FILEINFO_MIME_TYPE), $path);
  412. }
  413. /**
  414. * Get the file size of a given file.
  415. *
  416. * @param string $path
  417. * @return int
  418. */
  419. public function size($path)
  420. {
  421. return filesize($path);
  422. }
  423. /**
  424. * Get the file's last modification time.
  425. *
  426. * @param string $path
  427. * @return int
  428. */
  429. public function lastModified($path)
  430. {
  431. return filemtime($path);
  432. }
  433. /**
  434. * Determine if the given path is a directory.
  435. *
  436. * @param string $directory
  437. * @return bool
  438. */
  439. public function isDirectory($directory)
  440. {
  441. return is_dir($directory);
  442. }
  443. /**
  444. * Determine if the given path is a directory that does not contain any other files or directories.
  445. *
  446. * @param string $directory
  447. * @param bool $ignoreDotFiles
  448. * @return bool
  449. */
  450. public function isEmptyDirectory($directory, $ignoreDotFiles = false)
  451. {
  452. return ! Finder::create()->ignoreDotFiles($ignoreDotFiles)->in($directory)->depth(0)->hasResults();
  453. }
  454. /**
  455. * Determine if the given path is readable.
  456. *
  457. * @param string $path
  458. * @return bool
  459. */
  460. public function isReadable($path)
  461. {
  462. return is_readable($path);
  463. }
  464. /**
  465. * Determine if the given path is writable.
  466. *
  467. * @param string $path
  468. * @return bool
  469. */
  470. public function isWritable($path)
  471. {
  472. return is_writable($path);
  473. }
  474. /**
  475. * Determine if two files are the same by comparing their hashes.
  476. *
  477. * @param string $firstFile
  478. * @param string $secondFile
  479. * @return bool
  480. */
  481. public function hasSameHash($firstFile, $secondFile)
  482. {
  483. $hash = @hash_file('xxh128', $firstFile);
  484. return $hash && hash_equals($hash, (string) @hash_file('xxh128', $secondFile));
  485. }
  486. /**
  487. * Determine if the given path is a file.
  488. *
  489. * @param string $file
  490. * @return bool
  491. */
  492. public function isFile($file)
  493. {
  494. return is_file($file);
  495. }
  496. /**
  497. * Find path names matching a given pattern.
  498. *
  499. * @param string $pattern
  500. * @param int $flags
  501. * @return array
  502. */
  503. public function glob($pattern, $flags = 0)
  504. {
  505. return glob($pattern, $flags);
  506. }
  507. /**
  508. * Get an array of all files in a directory.
  509. *
  510. * @param string $directory
  511. * @param bool $hidden
  512. * @return \Symfony\Component\Finder\SplFileInfo[]
  513. */
  514. public function files($directory, $hidden = false, array|string|int $depth = 0)
  515. {
  516. return iterator_to_array(
  517. Finder::create()->files()->ignoreDotFiles(! $hidden)->in($directory)->depth($depth)->sortByName(),
  518. false
  519. );
  520. }
  521. /**
  522. * Get all of the files from the given directory (recursive).
  523. *
  524. * @param string $directory
  525. * @param bool $hidden
  526. * @return \Symfony\Component\Finder\SplFileInfo[]
  527. */
  528. public function allFiles($directory, $hidden = false)
  529. {
  530. return $this->files($directory, $hidden, []);
  531. }
  532. /**
  533. * Get all of the directories within a given directory.
  534. *
  535. * @param string $directory
  536. * @return array
  537. */
  538. public function directories($directory, array|string|int $depth = 0)
  539. {
  540. $directories = [];
  541. foreach (Finder::create()->in($directory)->directories()->depth($depth)->sortByName() as $dir) {
  542. $directories[] = $dir->getPathname();
  543. }
  544. return $directories;
  545. }
  546. /**
  547. * Get all the directories within a given directory (recursive).
  548. *
  549. * @return array
  550. */
  551. public function allDirectories(string $directory): array
  552. {
  553. return $this->directories($directory, []);
  554. }
  555. /**
  556. * Ensure a directory exists.
  557. *
  558. * @param string $path
  559. * @param int $mode
  560. * @param bool $recursive
  561. * @return void
  562. */
  563. public function ensureDirectoryExists($path, $mode = 0755, $recursive = true)
  564. {
  565. if (! $this->isDirectory($path)) {
  566. $this->makeDirectory($path, $mode, $recursive);
  567. }
  568. }
  569. /**
  570. * Create a directory.
  571. *
  572. * @param string $path
  573. * @param int $mode
  574. * @param bool $recursive
  575. * @param bool $force
  576. * @return bool
  577. */
  578. public function makeDirectory($path, $mode = 0755, $recursive = false, $force = false)
  579. {
  580. if ($force) {
  581. return @mkdir($path, $mode, $recursive);
  582. }
  583. return mkdir($path, $mode, $recursive);
  584. }
  585. /**
  586. * Move a directory.
  587. *
  588. * @param string $from
  589. * @param string $to
  590. * @param bool $overwrite
  591. * @return bool
  592. */
  593. public function moveDirectory($from, $to, $overwrite = false)
  594. {
  595. if ($overwrite && $this->isDirectory($to) && ! $this->deleteDirectory($to)) {
  596. return false;
  597. }
  598. return @rename($from, $to) === true;
  599. }
  600. /**
  601. * Copy a directory from one location to another.
  602. *
  603. * @param string $directory
  604. * @param string $destination
  605. * @param int|null $options
  606. * @return bool
  607. */
  608. public function copyDirectory($directory, $destination, $options = null)
  609. {
  610. if (! $this->isDirectory($directory)) {
  611. return false;
  612. }
  613. $options = $options ?: FilesystemIterator::SKIP_DOTS;
  614. // If the destination directory does not actually exist, we will go ahead and
  615. // create it recursively, which just gets the destination prepared to copy
  616. // the files over. Once we make the directory we'll proceed the copying.
  617. $this->ensureDirectoryExists($destination, 0777);
  618. $items = new FilesystemIterator($directory, $options);
  619. foreach ($items as $item) {
  620. // As we spin through items, we will check to see if the current file is actually
  621. // a directory or a file. When it is actually a directory we will need to call
  622. // back into this function recursively to keep copying these nested folders.
  623. $target = $destination.'/'.$item->getBasename();
  624. if ($item->isDir()) {
  625. $path = $item->getPathname();
  626. if (! $this->copyDirectory($path, $target, $options)) {
  627. return false;
  628. }
  629. }
  630. // If the current items is just a regular file, we will just copy this to the new
  631. // location and keep looping. If for some reason the copy fails we'll bail out
  632. // and return false, so the developer is aware that the copy process failed.
  633. elseif (! $this->copy($item->getPathname(), $target)) {
  634. return false;
  635. }
  636. }
  637. return true;
  638. }
  639. /**
  640. * Recursively delete a directory.
  641. *
  642. * The directory itself may be optionally preserved.
  643. *
  644. * @param string $directory
  645. * @param bool $preserve
  646. * @return bool
  647. */
  648. public function deleteDirectory($directory, $preserve = false)
  649. {
  650. if (! $this->isDirectory($directory)) {
  651. return false;
  652. }
  653. $items = new FilesystemIterator($directory);
  654. foreach ($items as $item) {
  655. // If the item is a directory, we can just recurse into the function and
  656. // delete that sub-directory otherwise we'll just delete the file and
  657. // keep iterating through each file until the directory is cleaned.
  658. if ($item->isDir() && ! $item->isLink()) {
  659. $this->deleteDirectory($item->getPathname());
  660. }
  661. // If the item is just a file, we can go ahead and delete it since we're
  662. // just looping through and waxing all of the files in this directory
  663. // and calling directories recursively, so we delete the real path.
  664. else {
  665. $this->delete($item->getPathname());
  666. }
  667. }
  668. unset($items);
  669. if (! $preserve) {
  670. @rmdir($directory);
  671. }
  672. return true;
  673. }
  674. /**
  675. * Remove all of the directories within a given directory.
  676. *
  677. * @param string $directory
  678. * @return bool
  679. */
  680. public function deleteDirectories($directory)
  681. {
  682. $allDirectories = $this->directories($directory);
  683. if (! empty($allDirectories)) {
  684. foreach ($allDirectories as $directoryName) {
  685. $this->deleteDirectory($directoryName);
  686. }
  687. return true;
  688. }
  689. return false;
  690. }
  691. /**
  692. * Empty the specified directory of all files and folders.
  693. *
  694. * @param string $directory
  695. * @return bool
  696. */
  697. public function cleanDirectory($directory)
  698. {
  699. return $this->deleteDirectory($directory, true);
  700. }
  701. }