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