├── AwsS3V3Adapter.php ├── Filesystem.php ├── FilesystemAdapter.php ├── FilesystemManager.php ├── FilesystemServiceProvider.php ├── LICENSE.md ├── LocalFilesystemAdapter.php ├── LockableFile.php ├── ServeFile.php ├── composer.json └── functions.php /AwsS3V3Adapter.php: -------------------------------------------------------------------------------- 1 | client = $client; 34 | } 35 | 36 | /** 37 | * Get the URL for the file at the given path. 38 | * 39 | * @param string $path 40 | * @return string 41 | * 42 | * @throws \RuntimeException 43 | */ 44 | public function url($path) 45 | { 46 | // If an explicit base URL has been set on the disk configuration then we will use 47 | // it as the base URL instead of the default path. This allows the developer to 48 | // have full control over the base path for this filesystem's generated URLs. 49 | if (isset($this->config['url'])) { 50 | return $this->concatPathToUrl($this->config['url'], $this->prefixer->prefixPath($path)); 51 | } 52 | 53 | return $this->client->getObjectUrl( 54 | $this->config['bucket'], $this->prefixer->prefixPath($path) 55 | ); 56 | } 57 | 58 | /** 59 | * Determine if temporary URLs can be generated. 60 | * 61 | * @return bool 62 | */ 63 | public function providesTemporaryUrls() 64 | { 65 | return true; 66 | } 67 | 68 | /** 69 | * Get a temporary URL for the file at the given path. 70 | * 71 | * @param string $path 72 | * @param \DateTimeInterface $expiration 73 | * @param array $options 74 | * @return string 75 | */ 76 | public function temporaryUrl($path, $expiration, array $options = []) 77 | { 78 | $command = $this->client->getCommand('GetObject', array_merge([ 79 | 'Bucket' => $this->config['bucket'], 80 | 'Key' => $this->prefixer->prefixPath($path), 81 | ], $options)); 82 | 83 | $uri = $this->client->createPresignedRequest( 84 | $command, $expiration, $options 85 | )->getUri(); 86 | 87 | // If an explicit base URL has been set on the disk configuration then we will use 88 | // it as the base URL instead of the default path. This allows the developer to 89 | // have full control over the base path for this filesystem's generated URLs. 90 | if (isset($this->config['temporary_url'])) { 91 | $uri = $this->replaceBaseUrl($uri, $this->config['temporary_url']); 92 | } 93 | 94 | return (string) $uri; 95 | } 96 | 97 | /** 98 | * Get a temporary upload URL for the file at the given path. 99 | * 100 | * @param string $path 101 | * @param \DateTimeInterface $expiration 102 | * @param array $options 103 | * @return array 104 | */ 105 | public function temporaryUploadUrl($path, $expiration, array $options = []) 106 | { 107 | $command = $this->client->getCommand('PutObject', array_merge([ 108 | 'Bucket' => $this->config['bucket'], 109 | 'Key' => $this->prefixer->prefixPath($path), 110 | ], $options)); 111 | 112 | $signedRequest = $this->client->createPresignedRequest( 113 | $command, $expiration, $options 114 | ); 115 | 116 | $uri = $signedRequest->getUri(); 117 | 118 | // If an explicit base URL has been set on the disk configuration then we will use 119 | // it as the base URL instead of the default path. This allows the developer to 120 | // have full control over the base path for this filesystem's generated URLs. 121 | if (isset($this->config['temporary_url'])) { 122 | $uri = $this->replaceBaseUrl($uri, $this->config['temporary_url']); 123 | } 124 | 125 | return [ 126 | 'url' => (string) $uri, 127 | 'headers' => $signedRequest->getHeaders(), 128 | ]; 129 | } 130 | 131 | /** 132 | * Get the underlying S3 client. 133 | * 134 | * @return \Aws\S3\S3Client 135 | */ 136 | public function getClient() 137 | { 138 | return $this->client; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Filesystem.php: -------------------------------------------------------------------------------- 1 | exists($path); 41 | } 42 | 43 | /** 44 | * Get the contents of a file. 45 | * 46 | * @param string $path 47 | * @param bool $lock 48 | * @return string 49 | * 50 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 51 | */ 52 | public function get($path, $lock = false) 53 | { 54 | if ($this->isFile($path)) { 55 | return $lock ? $this->sharedGet($path) : file_get_contents($path); 56 | } 57 | 58 | throw new FileNotFoundException("File does not exist at path {$path}."); 59 | } 60 | 61 | /** 62 | * Get the contents of a file as decoded JSON. 63 | * 64 | * @param string $path 65 | * @param int $flags 66 | * @param bool $lock 67 | * @return array 68 | * 69 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 70 | */ 71 | public function json($path, $flags = 0, $lock = false) 72 | { 73 | return json_decode($this->get($path, $lock), true, 512, $flags); 74 | } 75 | 76 | /** 77 | * Get contents of a file with shared access. 78 | * 79 | * @param string $path 80 | * @return string 81 | */ 82 | public function sharedGet($path) 83 | { 84 | $contents = ''; 85 | 86 | $handle = fopen($path, 'rb'); 87 | 88 | if ($handle) { 89 | try { 90 | if (flock($handle, LOCK_SH)) { 91 | clearstatcache(true, $path); 92 | 93 | $contents = fread($handle, $this->size($path) ?: 1); 94 | 95 | flock($handle, LOCK_UN); 96 | } 97 | } finally { 98 | fclose($handle); 99 | } 100 | } 101 | 102 | return $contents; 103 | } 104 | 105 | /** 106 | * Get the returned value of a file. 107 | * 108 | * @param string $path 109 | * @param array $data 110 | * @return mixed 111 | * 112 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 113 | */ 114 | public function getRequire($path, array $data = []) 115 | { 116 | if ($this->isFile($path)) { 117 | $__path = $path; 118 | $__data = $data; 119 | 120 | return (static function () use ($__path, $__data) { 121 | extract($__data, EXTR_SKIP); 122 | 123 | return require $__path; 124 | })(); 125 | } 126 | 127 | throw new FileNotFoundException("File does not exist at path {$path}."); 128 | } 129 | 130 | /** 131 | * Require the given file once. 132 | * 133 | * @param string $path 134 | * @param array $data 135 | * @return mixed 136 | * 137 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 138 | */ 139 | public function requireOnce($path, array $data = []) 140 | { 141 | if ($this->isFile($path)) { 142 | $__path = $path; 143 | $__data = $data; 144 | 145 | return (static function () use ($__path, $__data) { 146 | extract($__data, EXTR_SKIP); 147 | 148 | return require_once $__path; 149 | })(); 150 | } 151 | 152 | throw new FileNotFoundException("File does not exist at path {$path}."); 153 | } 154 | 155 | /** 156 | * Get the contents of a file one line at a time. 157 | * 158 | * @param string $path 159 | * @return \Illuminate\Support\LazyCollection 160 | * 161 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 162 | */ 163 | public function lines($path) 164 | { 165 | if (! $this->isFile($path)) { 166 | throw new FileNotFoundException( 167 | "File does not exist at path {$path}." 168 | ); 169 | } 170 | 171 | return new LazyCollection(function () use ($path) { 172 | $file = new SplFileObject($path); 173 | 174 | $file->setFlags(SplFileObject::DROP_NEW_LINE); 175 | 176 | while (! $file->eof()) { 177 | yield $file->fgets(); 178 | } 179 | }); 180 | } 181 | 182 | /** 183 | * Get the hash of the file at the given path. 184 | * 185 | * @param string $path 186 | * @param string $algorithm 187 | * @return string|false 188 | */ 189 | public function hash($path, $algorithm = 'md5') 190 | { 191 | return hash_file($algorithm, $path); 192 | } 193 | 194 | /** 195 | * Write the contents of a file. 196 | * 197 | * @param string $path 198 | * @param string $contents 199 | * @param bool $lock 200 | * @return int|bool 201 | */ 202 | public function put($path, $contents, $lock = false) 203 | { 204 | return file_put_contents($path, $contents, $lock ? LOCK_EX : 0); 205 | } 206 | 207 | /** 208 | * Write the contents of a file, replacing it atomically if it already exists. 209 | * 210 | * @param string $path 211 | * @param string $content 212 | * @param int|null $mode 213 | * @return void 214 | */ 215 | public function replace($path, $content, $mode = null) 216 | { 217 | // If the path already exists and is a symlink, get the real path... 218 | clearstatcache(true, $path); 219 | 220 | $path = realpath($path) ?: $path; 221 | 222 | $tempPath = tempnam(dirname($path), basename($path)); 223 | 224 | // Fix permissions of tempPath because `tempnam()` creates it with permissions set to 0600... 225 | if (! is_null($mode)) { 226 | chmod($tempPath, $mode); 227 | } else { 228 | chmod($tempPath, 0777 - umask()); 229 | } 230 | 231 | file_put_contents($tempPath, $content); 232 | 233 | rename($tempPath, $path); 234 | } 235 | 236 | /** 237 | * Replace a given string within a given file. 238 | * 239 | * @param array|string $search 240 | * @param array|string $replace 241 | * @param string $path 242 | * @return void 243 | */ 244 | public function replaceInFile($search, $replace, $path) 245 | { 246 | file_put_contents($path, str_replace($search, $replace, file_get_contents($path))); 247 | } 248 | 249 | /** 250 | * Prepend to a file. 251 | * 252 | * @param string $path 253 | * @param string $data 254 | * @return int 255 | */ 256 | public function prepend($path, $data) 257 | { 258 | if ($this->exists($path)) { 259 | return $this->put($path, $data.$this->get($path)); 260 | } 261 | 262 | return $this->put($path, $data); 263 | } 264 | 265 | /** 266 | * Append to a file. 267 | * 268 | * @param string $path 269 | * @param string $data 270 | * @param bool $lock 271 | * @return int 272 | */ 273 | public function append($path, $data, $lock = false) 274 | { 275 | return file_put_contents($path, $data, FILE_APPEND | ($lock ? LOCK_EX : 0)); 276 | } 277 | 278 | /** 279 | * Get or set UNIX mode of a file or directory. 280 | * 281 | * @param string $path 282 | * @param int|null $mode 283 | * @return mixed 284 | */ 285 | public function chmod($path, $mode = null) 286 | { 287 | if ($mode) { 288 | return chmod($path, $mode); 289 | } 290 | 291 | return substr(sprintf('%o', fileperms($path)), -4); 292 | } 293 | 294 | /** 295 | * Delete the file at a given path. 296 | * 297 | * @param string|array $paths 298 | * @return bool 299 | */ 300 | public function delete($paths) 301 | { 302 | $paths = is_array($paths) ? $paths : func_get_args(); 303 | 304 | $success = true; 305 | 306 | foreach ($paths as $path) { 307 | try { 308 | if (@unlink($path)) { 309 | clearstatcache(false, $path); 310 | } else { 311 | $success = false; 312 | } 313 | } catch (ErrorException) { 314 | $success = false; 315 | } 316 | } 317 | 318 | return $success; 319 | } 320 | 321 | /** 322 | * Move a file to a new location. 323 | * 324 | * @param string $path 325 | * @param string $target 326 | * @return bool 327 | */ 328 | public function move($path, $target) 329 | { 330 | return rename($path, $target); 331 | } 332 | 333 | /** 334 | * Copy a file to a new location. 335 | * 336 | * @param string $path 337 | * @param string $target 338 | * @return bool 339 | */ 340 | public function copy($path, $target) 341 | { 342 | return copy($path, $target); 343 | } 344 | 345 | /** 346 | * Create a symlink to the target file or directory. On Windows, a hard link is created if the target is a file. 347 | * 348 | * @param string $target 349 | * @param string $link 350 | * @return bool|null 351 | */ 352 | public function link($target, $link) 353 | { 354 | if (! windows_os()) { 355 | if (function_exists('symlink')) { 356 | return symlink($target, $link); 357 | } else { 358 | return exec('ln -s '.escapeshellarg($target).' '.escapeshellarg($link)) !== false; 359 | } 360 | } 361 | 362 | $mode = $this->isDirectory($target) ? 'J' : 'H'; 363 | 364 | exec("mklink /{$mode} ".escapeshellarg($link).' '.escapeshellarg($target)); 365 | } 366 | 367 | /** 368 | * Create a relative symlink to the target file or directory. 369 | * 370 | * @param string $target 371 | * @param string $link 372 | * @return void 373 | * 374 | * @throws \RuntimeException 375 | */ 376 | public function relativeLink($target, $link) 377 | { 378 | if (! class_exists(SymfonyFilesystem::class)) { 379 | throw new RuntimeException( 380 | 'To enable support for relative links, please install the symfony/filesystem package.' 381 | ); 382 | } 383 | 384 | $relativeTarget = (new SymfonyFilesystem)->makePathRelative($target, dirname($link)); 385 | 386 | $this->link($this->isFile($target) ? rtrim($relativeTarget, '/') : $relativeTarget, $link); 387 | } 388 | 389 | /** 390 | * Extract the file name from a file path. 391 | * 392 | * @param string $path 393 | * @return string 394 | */ 395 | public function name($path) 396 | { 397 | return pathinfo($path, PATHINFO_FILENAME); 398 | } 399 | 400 | /** 401 | * Extract the trailing name component from a file path. 402 | * 403 | * @param string $path 404 | * @return string 405 | */ 406 | public function basename($path) 407 | { 408 | return pathinfo($path, PATHINFO_BASENAME); 409 | } 410 | 411 | /** 412 | * Extract the parent directory from a file path. 413 | * 414 | * @param string $path 415 | * @return string 416 | */ 417 | public function dirname($path) 418 | { 419 | return pathinfo($path, PATHINFO_DIRNAME); 420 | } 421 | 422 | /** 423 | * Extract the file extension from a file path. 424 | * 425 | * @param string $path 426 | * @return string 427 | */ 428 | public function extension($path) 429 | { 430 | return pathinfo($path, PATHINFO_EXTENSION); 431 | } 432 | 433 | /** 434 | * Guess the file extension from the mime-type of a given file. 435 | * 436 | * @param string $path 437 | * @return string|null 438 | * 439 | * @throws \RuntimeException 440 | */ 441 | public function guessExtension($path) 442 | { 443 | if (! class_exists(MimeTypes::class)) { 444 | throw new RuntimeException( 445 | 'To enable support for guessing extensions, please install the symfony/mime package.' 446 | ); 447 | } 448 | 449 | return (new MimeTypes)->getExtensions($this->mimeType($path))[0] ?? null; 450 | } 451 | 452 | /** 453 | * Get the file type of a given file. 454 | * 455 | * @param string $path 456 | * @return string 457 | */ 458 | public function type($path) 459 | { 460 | return filetype($path); 461 | } 462 | 463 | /** 464 | * Get the mime-type of a given file. 465 | * 466 | * @param string $path 467 | * @return string|false 468 | */ 469 | public function mimeType($path) 470 | { 471 | return finfo_file(finfo_open(FILEINFO_MIME_TYPE), $path); 472 | } 473 | 474 | /** 475 | * Get the file size of a given file. 476 | * 477 | * @param string $path 478 | * @return int 479 | */ 480 | public function size($path) 481 | { 482 | return filesize($path); 483 | } 484 | 485 | /** 486 | * Get the file's last modification time. 487 | * 488 | * @param string $path 489 | * @return int 490 | */ 491 | public function lastModified($path) 492 | { 493 | return filemtime($path); 494 | } 495 | 496 | /** 497 | * Determine if the given path is a directory. 498 | * 499 | * @param string $directory 500 | * @return bool 501 | */ 502 | public function isDirectory($directory) 503 | { 504 | return is_dir($directory); 505 | } 506 | 507 | /** 508 | * Determine if the given path is a directory that does not contain any other files or directories. 509 | * 510 | * @param string $directory 511 | * @param bool $ignoreDotFiles 512 | * @return bool 513 | */ 514 | public function isEmptyDirectory($directory, $ignoreDotFiles = false) 515 | { 516 | return ! Finder::create()->ignoreDotFiles($ignoreDotFiles)->in($directory)->depth(0)->hasResults(); 517 | } 518 | 519 | /** 520 | * Determine if the given path is readable. 521 | * 522 | * @param string $path 523 | * @return bool 524 | */ 525 | public function isReadable($path) 526 | { 527 | return is_readable($path); 528 | } 529 | 530 | /** 531 | * Determine if the given path is writable. 532 | * 533 | * @param string $path 534 | * @return bool 535 | */ 536 | public function isWritable($path) 537 | { 538 | return is_writable($path); 539 | } 540 | 541 | /** 542 | * Determine if two files are the same by comparing their hashes. 543 | * 544 | * @param string $firstFile 545 | * @param string $secondFile 546 | * @return bool 547 | */ 548 | public function hasSameHash($firstFile, $secondFile) 549 | { 550 | $hash = @hash_file('xxh128', $firstFile); 551 | 552 | return $hash && hash_equals($hash, (string) @hash_file('xxh128', $secondFile)); 553 | } 554 | 555 | /** 556 | * Determine if the given path is a file. 557 | * 558 | * @param string $file 559 | * @return bool 560 | */ 561 | public function isFile($file) 562 | { 563 | return is_file($file); 564 | } 565 | 566 | /** 567 | * Find path names matching a given pattern. 568 | * 569 | * @param string $pattern 570 | * @param int $flags 571 | * @return array 572 | */ 573 | public function glob($pattern, $flags = 0) 574 | { 575 | return glob($pattern, $flags); 576 | } 577 | 578 | /** 579 | * Get an array of all files in a directory. 580 | * 581 | * @param string $directory 582 | * @param bool $hidden 583 | * @return \Symfony\Component\Finder\SplFileInfo[] 584 | */ 585 | public function files($directory, $hidden = false) 586 | { 587 | return iterator_to_array( 588 | Finder::create()->files()->ignoreDotFiles(! $hidden)->in($directory)->depth(0)->sortByName(), 589 | false 590 | ); 591 | } 592 | 593 | /** 594 | * Get all of the files from the given directory (recursive). 595 | * 596 | * @param string $directory 597 | * @param bool $hidden 598 | * @return \Symfony\Component\Finder\SplFileInfo[] 599 | */ 600 | public function allFiles($directory, $hidden = false) 601 | { 602 | return iterator_to_array( 603 | Finder::create()->files()->ignoreDotFiles(! $hidden)->in($directory)->sortByName(), 604 | false 605 | ); 606 | } 607 | 608 | /** 609 | * Get all of the directories within a given directory. 610 | * 611 | * @param string $directory 612 | * @return array 613 | */ 614 | public function directories($directory) 615 | { 616 | $directories = []; 617 | 618 | foreach (Finder::create()->in($directory)->directories()->depth(0)->sortByName() as $dir) { 619 | $directories[] = $dir->getPathname(); 620 | } 621 | 622 | return $directories; 623 | } 624 | 625 | /** 626 | * Ensure a directory exists. 627 | * 628 | * @param string $path 629 | * @param int $mode 630 | * @param bool $recursive 631 | * @return void 632 | */ 633 | public function ensureDirectoryExists($path, $mode = 0755, $recursive = true) 634 | { 635 | if (! $this->isDirectory($path)) { 636 | $this->makeDirectory($path, $mode, $recursive); 637 | } 638 | } 639 | 640 | /** 641 | * Create a directory. 642 | * 643 | * @param string $path 644 | * @param int $mode 645 | * @param bool $recursive 646 | * @param bool $force 647 | * @return bool 648 | */ 649 | public function makeDirectory($path, $mode = 0755, $recursive = false, $force = false) 650 | { 651 | if ($force) { 652 | return @mkdir($path, $mode, $recursive); 653 | } 654 | 655 | return mkdir($path, $mode, $recursive); 656 | } 657 | 658 | /** 659 | * Move a directory. 660 | * 661 | * @param string $from 662 | * @param string $to 663 | * @param bool $overwrite 664 | * @return bool 665 | */ 666 | public function moveDirectory($from, $to, $overwrite = false) 667 | { 668 | if ($overwrite && $this->isDirectory($to) && ! $this->deleteDirectory($to)) { 669 | return false; 670 | } 671 | 672 | return @rename($from, $to) === true; 673 | } 674 | 675 | /** 676 | * Copy a directory from one location to another. 677 | * 678 | * @param string $directory 679 | * @param string $destination 680 | * @param int|null $options 681 | * @return bool 682 | */ 683 | public function copyDirectory($directory, $destination, $options = null) 684 | { 685 | if (! $this->isDirectory($directory)) { 686 | return false; 687 | } 688 | 689 | $options = $options ?: FilesystemIterator::SKIP_DOTS; 690 | 691 | // If the destination directory does not actually exist, we will go ahead and 692 | // create it recursively, which just gets the destination prepared to copy 693 | // the files over. Once we make the directory we'll proceed the copying. 694 | $this->ensureDirectoryExists($destination, 0777); 695 | 696 | $items = new FilesystemIterator($directory, $options); 697 | 698 | foreach ($items as $item) { 699 | // As we spin through items, we will check to see if the current file is actually 700 | // a directory or a file. When it is actually a directory we will need to call 701 | // back into this function recursively to keep copying these nested folders. 702 | $target = $destination.'/'.$item->getBasename(); 703 | 704 | if ($item->isDir()) { 705 | $path = $item->getPathname(); 706 | 707 | if (! $this->copyDirectory($path, $target, $options)) { 708 | return false; 709 | } 710 | } 711 | 712 | // If the current items is just a regular file, we will just copy this to the new 713 | // location and keep looping. If for some reason the copy fails we'll bail out 714 | // and return false, so the developer is aware that the copy process failed. 715 | elseif (! $this->copy($item->getPathname(), $target)) { 716 | return false; 717 | } 718 | } 719 | 720 | return true; 721 | } 722 | 723 | /** 724 | * Recursively delete a directory. 725 | * 726 | * The directory itself may be optionally preserved. 727 | * 728 | * @param string $directory 729 | * @param bool $preserve 730 | * @return bool 731 | */ 732 | public function deleteDirectory($directory, $preserve = false) 733 | { 734 | if (! $this->isDirectory($directory)) { 735 | return false; 736 | } 737 | 738 | $items = new FilesystemIterator($directory); 739 | 740 | foreach ($items as $item) { 741 | // If the item is a directory, we can just recurse into the function and 742 | // delete that sub-directory otherwise we'll just delete the file and 743 | // keep iterating through each file until the directory is cleaned. 744 | if ($item->isDir() && ! $item->isLink()) { 745 | $this->deleteDirectory($item->getPathname()); 746 | } 747 | 748 | // If the item is just a file, we can go ahead and delete it since we're 749 | // just looping through and waxing all of the files in this directory 750 | // and calling directories recursively, so we delete the real path. 751 | else { 752 | $this->delete($item->getPathname()); 753 | } 754 | } 755 | 756 | unset($items); 757 | 758 | if (! $preserve) { 759 | @rmdir($directory); 760 | } 761 | 762 | return true; 763 | } 764 | 765 | /** 766 | * Remove all of the directories within a given directory. 767 | * 768 | * @param string $directory 769 | * @return bool 770 | */ 771 | public function deleteDirectories($directory) 772 | { 773 | $allDirectories = $this->directories($directory); 774 | 775 | if (! empty($allDirectories)) { 776 | foreach ($allDirectories as $directoryName) { 777 | $this->deleteDirectory($directoryName); 778 | } 779 | 780 | return true; 781 | } 782 | 783 | return false; 784 | } 785 | 786 | /** 787 | * Empty the specified directory of all files and folders. 788 | * 789 | * @param string $directory 790 | * @return bool 791 | */ 792 | public function cleanDirectory($directory) 793 | { 794 | return $this->deleteDirectory($directory, true); 795 | } 796 | } 797 | -------------------------------------------------------------------------------- /FilesystemAdapter.php: -------------------------------------------------------------------------------- 1 | driver = $driver; 104 | $this->adapter = $adapter; 105 | $this->config = $config; 106 | $separator = $config['directory_separator'] ?? DIRECTORY_SEPARATOR; 107 | 108 | $this->prefixer = new PathPrefixer($config['root'] ?? '', $separator); 109 | 110 | if (isset($config['prefix'])) { 111 | $this->prefixer = new PathPrefixer($this->prefixer->prefixPath($config['prefix']), $separator); 112 | } 113 | } 114 | 115 | /** 116 | * Assert that the given file or directory exists. 117 | * 118 | * @param string|array $path 119 | * @param string|null $content 120 | * @return $this 121 | */ 122 | public function assertExists($path, $content = null) 123 | { 124 | clearstatcache(); 125 | 126 | $paths = Arr::wrap($path); 127 | 128 | foreach ($paths as $path) { 129 | PHPUnit::assertTrue( 130 | $this->exists($path), "Unable to find a file or directory at path [{$path}]." 131 | ); 132 | 133 | if (! is_null($content)) { 134 | $actual = $this->get($path); 135 | 136 | PHPUnit::assertSame( 137 | $content, 138 | $actual, 139 | "File or directory [{$path}] was found, but content [{$actual}] does not match [{$content}]." 140 | ); 141 | } 142 | } 143 | 144 | return $this; 145 | } 146 | 147 | /** 148 | * Assert that the number of files in path equals the expected count. 149 | * 150 | * @param string $path 151 | * @param int $count 152 | * @param bool $recursive 153 | * @return $this 154 | */ 155 | public function assertCount($path, $count, $recursive = false) 156 | { 157 | clearstatcache(); 158 | 159 | $actual = count($this->files($path, $recursive)); 160 | 161 | PHPUnit::assertEquals( 162 | $actual, $count, "Expected [{$count}] files at [{$path}], but found [{$actual}]." 163 | ); 164 | 165 | return $this; 166 | } 167 | 168 | /** 169 | * Assert that the given file or directory does not exist. 170 | * 171 | * @param string|array $path 172 | * @return $this 173 | */ 174 | public function assertMissing($path) 175 | { 176 | clearstatcache(); 177 | 178 | $paths = Arr::wrap($path); 179 | 180 | foreach ($paths as $path) { 181 | PHPUnit::assertFalse( 182 | $this->exists($path), "Found unexpected file or directory at path [{$path}]." 183 | ); 184 | } 185 | 186 | return $this; 187 | } 188 | 189 | /** 190 | * Assert that the given directory is empty. 191 | * 192 | * @param string $path 193 | * @return $this 194 | */ 195 | public function assertDirectoryEmpty($path) 196 | { 197 | PHPUnit::assertEmpty( 198 | $this->allFiles($path), "Directory [{$path}] is not empty." 199 | ); 200 | 201 | return $this; 202 | } 203 | 204 | /** 205 | * Determine if a file or directory exists. 206 | * 207 | * @param string $path 208 | * @return bool 209 | */ 210 | public function exists($path) 211 | { 212 | return $this->driver->has($path); 213 | } 214 | 215 | /** 216 | * Determine if a file or directory is missing. 217 | * 218 | * @param string $path 219 | * @return bool 220 | */ 221 | public function missing($path) 222 | { 223 | return ! $this->exists($path); 224 | } 225 | 226 | /** 227 | * Determine if a file exists. 228 | * 229 | * @param string $path 230 | * @return bool 231 | */ 232 | public function fileExists($path) 233 | { 234 | return $this->driver->fileExists($path); 235 | } 236 | 237 | /** 238 | * Determine if a file is missing. 239 | * 240 | * @param string $path 241 | * @return bool 242 | */ 243 | public function fileMissing($path) 244 | { 245 | return ! $this->fileExists($path); 246 | } 247 | 248 | /** 249 | * Determine if a directory exists. 250 | * 251 | * @param string $path 252 | * @return bool 253 | */ 254 | public function directoryExists($path) 255 | { 256 | return $this->driver->directoryExists($path); 257 | } 258 | 259 | /** 260 | * Determine if a directory is missing. 261 | * 262 | * @param string $path 263 | * @return bool 264 | */ 265 | public function directoryMissing($path) 266 | { 267 | return ! $this->directoryExists($path); 268 | } 269 | 270 | /** 271 | * Get the full path to the file that exists at the given relative path. 272 | * 273 | * @param string $path 274 | * @return string 275 | */ 276 | public function path($path) 277 | { 278 | return $this->prefixer->prefixPath($path); 279 | } 280 | 281 | /** 282 | * Get the contents of a file. 283 | * 284 | * @param string $path 285 | * @return string|null 286 | */ 287 | public function get($path) 288 | { 289 | try { 290 | return $this->driver->read($path); 291 | } catch (UnableToReadFile $e) { 292 | throw_if($this->throwsExceptions(), $e); 293 | 294 | $this->report($e); 295 | } 296 | } 297 | 298 | /** 299 | * Get the contents of a file as decoded JSON. 300 | * 301 | * @param string $path 302 | * @param int $flags 303 | * @return array|null 304 | */ 305 | public function json($path, $flags = 0) 306 | { 307 | $content = $this->get($path); 308 | 309 | return is_null($content) ? null : json_decode($content, true, 512, $flags); 310 | } 311 | 312 | /** 313 | * Create a streamed response for a given file. 314 | * 315 | * @param string $path 316 | * @param string|null $name 317 | * @param array $headers 318 | * @param string|null $disposition 319 | * @return \Symfony\Component\HttpFoundation\StreamedResponse 320 | */ 321 | public function response($path, $name = null, array $headers = [], $disposition = 'inline') 322 | { 323 | $response = new StreamedResponse; 324 | 325 | $headers['Content-Type'] ??= $this->mimeType($path); 326 | $headers['Content-Length'] ??= $this->size($path); 327 | 328 | if (! array_key_exists('Content-Disposition', $headers)) { 329 | $filename = $name ?? basename($path); 330 | 331 | $disposition = $response->headers->makeDisposition( 332 | $disposition, $filename, $this->fallbackName($filename) 333 | ); 334 | 335 | $headers['Content-Disposition'] = $disposition; 336 | } 337 | 338 | $response->headers->replace($headers); 339 | 340 | $response->setCallback(function () use ($path) { 341 | $stream = $this->readStream($path); 342 | fpassthru($stream); 343 | fclose($stream); 344 | }); 345 | 346 | return $response; 347 | } 348 | 349 | /** 350 | * Create a streamed download response for a given file. 351 | * 352 | * @param \Illuminate\Http\Request $request 353 | * @param string $path 354 | * @param string|null $name 355 | * @param array $headers 356 | * @return \Symfony\Component\HttpFoundation\StreamedResponse 357 | */ 358 | public function serve(Request $request, $path, $name = null, array $headers = []) 359 | { 360 | return isset($this->serveCallback) 361 | ? call_user_func($this->serveCallback, $request, $path, $headers) 362 | : $this->response($path, $name, $headers); 363 | } 364 | 365 | /** 366 | * Create a streamed download response for a given file. 367 | * 368 | * @param string $path 369 | * @param string|null $name 370 | * @param array $headers 371 | * @return \Symfony\Component\HttpFoundation\StreamedResponse 372 | */ 373 | public function download($path, $name = null, array $headers = []) 374 | { 375 | return $this->response($path, $name, $headers, 'attachment'); 376 | } 377 | 378 | /** 379 | * Convert the string to ASCII characters that are equivalent to the given name. 380 | * 381 | * @param string $name 382 | * @return string 383 | */ 384 | protected function fallbackName($name) 385 | { 386 | return str_replace('%', '', Str::ascii($name)); 387 | } 388 | 389 | /** 390 | * Write the contents of a file. 391 | * 392 | * @param string $path 393 | * @param \Psr\Http\Message\StreamInterface|\Illuminate\Http\File|\Illuminate\Http\UploadedFile|string|resource $contents 394 | * @param mixed $options 395 | * @return string|bool 396 | */ 397 | public function put($path, $contents, $options = []) 398 | { 399 | $options = is_string($options) 400 | ? ['visibility' => $options] 401 | : (array) $options; 402 | 403 | // If the given contents is actually a file or uploaded file instance than we will 404 | // automatically store the file using a stream. This provides a convenient path 405 | // for the developer to store streams without managing them manually in code. 406 | if ($contents instanceof File || 407 | $contents instanceof UploadedFile) { 408 | return $this->putFile($path, $contents, $options); 409 | } 410 | 411 | try { 412 | if ($contents instanceof StreamInterface) { 413 | $this->driver->writeStream($path, $contents->detach(), $options); 414 | 415 | return true; 416 | } 417 | 418 | is_resource($contents) 419 | ? $this->driver->writeStream($path, $contents, $options) 420 | : $this->driver->write($path, $contents, $options); 421 | } catch (UnableToWriteFile|UnableToSetVisibility $e) { 422 | throw_if($this->throwsExceptions(), $e); 423 | 424 | $this->report($e); 425 | 426 | return false; 427 | } 428 | 429 | return true; 430 | } 431 | 432 | /** 433 | * Store the uploaded file on the disk. 434 | * 435 | * @param \Illuminate\Http\File|\Illuminate\Http\UploadedFile|string $path 436 | * @param \Illuminate\Http\File|\Illuminate\Http\UploadedFile|string|array|null $file 437 | * @param mixed $options 438 | * @return string|false 439 | */ 440 | public function putFile($path, $file = null, $options = []) 441 | { 442 | if (is_null($file) || is_array($file)) { 443 | [$path, $file, $options] = ['', $path, $file ?? []]; 444 | } 445 | 446 | $file = is_string($file) ? new File($file) : $file; 447 | 448 | return $this->putFileAs($path, $file, $file->hashName(), $options); 449 | } 450 | 451 | /** 452 | * Store the uploaded file on the disk with a given name. 453 | * 454 | * @param \Illuminate\Http\File|\Illuminate\Http\UploadedFile|string $path 455 | * @param \Illuminate\Http\File|\Illuminate\Http\UploadedFile|string|array|null $file 456 | * @param string|array|null $name 457 | * @param mixed $options 458 | * @return string|false 459 | */ 460 | public function putFileAs($path, $file, $name = null, $options = []) 461 | { 462 | if (is_null($name) || is_array($name)) { 463 | [$path, $file, $name, $options] = ['', $path, $file, $name ?? []]; 464 | } 465 | 466 | $stream = fopen(is_string($file) ? $file : $file->getRealPath(), 'r'); 467 | 468 | // Next, we will format the path of the file and store the file using a stream since 469 | // they provide better performance than alternatives. Once we write the file this 470 | // stream will get closed automatically by us so the developer doesn't have to. 471 | $result = $this->put( 472 | $path = trim($path.'/'.$name, '/'), $stream, $options 473 | ); 474 | 475 | if (is_resource($stream)) { 476 | fclose($stream); 477 | } 478 | 479 | return $result ? $path : false; 480 | } 481 | 482 | /** 483 | * Get the visibility for the given path. 484 | * 485 | * @param string $path 486 | * @return string 487 | */ 488 | public function getVisibility($path) 489 | { 490 | if ($this->driver->visibility($path) == Visibility::PUBLIC) { 491 | return FilesystemContract::VISIBILITY_PUBLIC; 492 | } 493 | 494 | return FilesystemContract::VISIBILITY_PRIVATE; 495 | } 496 | 497 | /** 498 | * Set the visibility for the given path. 499 | * 500 | * @param string $path 501 | * @param string $visibility 502 | * @return bool 503 | */ 504 | public function setVisibility($path, $visibility) 505 | { 506 | try { 507 | $this->driver->setVisibility($path, $this->parseVisibility($visibility)); 508 | } catch (UnableToSetVisibility $e) { 509 | throw_if($this->throwsExceptions(), $e); 510 | 511 | $this->report($e); 512 | 513 | return false; 514 | } 515 | 516 | return true; 517 | } 518 | 519 | /** 520 | * Prepend to a file. 521 | * 522 | * @param string $path 523 | * @param string $data 524 | * @param string $separator 525 | * @return bool 526 | */ 527 | public function prepend($path, $data, $separator = PHP_EOL) 528 | { 529 | if ($this->fileExists($path)) { 530 | return $this->put($path, $data.$separator.$this->get($path)); 531 | } 532 | 533 | return $this->put($path, $data); 534 | } 535 | 536 | /** 537 | * Append to a file. 538 | * 539 | * @param string $path 540 | * @param string $data 541 | * @param string $separator 542 | * @return bool 543 | */ 544 | public function append($path, $data, $separator = PHP_EOL) 545 | { 546 | if ($this->fileExists($path)) { 547 | return $this->put($path, $this->get($path).$separator.$data); 548 | } 549 | 550 | return $this->put($path, $data); 551 | } 552 | 553 | /** 554 | * Delete the file at a given path. 555 | * 556 | * @param string|array $paths 557 | * @return bool 558 | */ 559 | public function delete($paths) 560 | { 561 | $paths = is_array($paths) ? $paths : func_get_args(); 562 | 563 | $success = true; 564 | 565 | foreach ($paths as $path) { 566 | try { 567 | $this->driver->delete($path); 568 | } catch (UnableToDeleteFile $e) { 569 | throw_if($this->throwsExceptions(), $e); 570 | 571 | $this->report($e); 572 | 573 | $success = false; 574 | } 575 | } 576 | 577 | return $success; 578 | } 579 | 580 | /** 581 | * Copy a file to a new location. 582 | * 583 | * @param string $from 584 | * @param string $to 585 | * @return bool 586 | */ 587 | public function copy($from, $to) 588 | { 589 | try { 590 | $this->driver->copy($from, $to); 591 | } catch (UnableToCopyFile $e) { 592 | throw_if($this->throwsExceptions(), $e); 593 | 594 | $this->report($e); 595 | 596 | return false; 597 | } 598 | 599 | return true; 600 | } 601 | 602 | /** 603 | * Move a file to a new location. 604 | * 605 | * @param string $from 606 | * @param string $to 607 | * @return bool 608 | */ 609 | public function move($from, $to) 610 | { 611 | try { 612 | $this->driver->move($from, $to); 613 | } catch (UnableToMoveFile $e) { 614 | throw_if($this->throwsExceptions(), $e); 615 | 616 | $this->report($e); 617 | 618 | return false; 619 | } 620 | 621 | return true; 622 | } 623 | 624 | /** 625 | * Get the file size of a given file. 626 | * 627 | * @param string $path 628 | * @return int 629 | */ 630 | public function size($path) 631 | { 632 | return $this->driver->fileSize($path); 633 | } 634 | 635 | /** 636 | * Get the checksum for a file. 637 | * 638 | * @return string|false 639 | * 640 | * @throws UnableToProvideChecksum 641 | */ 642 | public function checksum(string $path, array $options = []) 643 | { 644 | try { 645 | return $this->driver->checksum($path, $options); 646 | } catch (UnableToProvideChecksum $e) { 647 | throw_if($this->throwsExceptions(), $e); 648 | 649 | $this->report($e); 650 | 651 | return false; 652 | } 653 | } 654 | 655 | /** 656 | * Get the mime-type of a given file. 657 | * 658 | * @param string $path 659 | * @return string|false 660 | */ 661 | public function mimeType($path) 662 | { 663 | try { 664 | return $this->driver->mimeType($path); 665 | } catch (UnableToRetrieveMetadata $e) { 666 | throw_if($this->throwsExceptions(), $e); 667 | 668 | $this->report($e); 669 | } 670 | 671 | return false; 672 | } 673 | 674 | /** 675 | * Get the file's last modification time. 676 | * 677 | * @param string $path 678 | * @return int 679 | */ 680 | public function lastModified($path) 681 | { 682 | return $this->driver->lastModified($path); 683 | } 684 | 685 | /** 686 | * {@inheritdoc} 687 | */ 688 | public function readStream($path) 689 | { 690 | try { 691 | return $this->driver->readStream($path); 692 | } catch (UnableToReadFile $e) { 693 | throw_if($this->throwsExceptions(), $e); 694 | 695 | $this->report($e); 696 | } 697 | } 698 | 699 | /** 700 | * {@inheritdoc} 701 | */ 702 | public function writeStream($path, $resource, array $options = []) 703 | { 704 | try { 705 | $this->driver->writeStream($path, $resource, $options); 706 | } catch (UnableToWriteFile|UnableToSetVisibility $e) { 707 | throw_if($this->throwsExceptions(), $e); 708 | 709 | $this->report($e); 710 | 711 | return false; 712 | } 713 | 714 | return true; 715 | } 716 | 717 | /** 718 | * Get the URL for the file at the given path. 719 | * 720 | * @param string $path 721 | * @return string 722 | * 723 | * @throws \RuntimeException 724 | */ 725 | public function url($path) 726 | { 727 | if (isset($this->config['prefix'])) { 728 | $path = $this->concatPathToUrl($this->config['prefix'], $path); 729 | } 730 | 731 | $adapter = $this->adapter; 732 | 733 | if (method_exists($adapter, 'getUrl')) { 734 | return $adapter->getUrl($path); 735 | } elseif (method_exists($this->driver, 'getUrl')) { 736 | return $this->driver->getUrl($path); 737 | } elseif ($adapter instanceof FtpAdapter || $adapter instanceof SftpAdapter) { 738 | return $this->getFtpUrl($path); 739 | } elseif ($adapter instanceof LocalAdapter) { 740 | return $this->getLocalUrl($path); 741 | } else { 742 | throw new RuntimeException('This driver does not support retrieving URLs.'); 743 | } 744 | } 745 | 746 | /** 747 | * Get the URL for the file at the given path. 748 | * 749 | * @param string $path 750 | * @return string 751 | */ 752 | protected function getFtpUrl($path) 753 | { 754 | return isset($this->config['url']) 755 | ? $this->concatPathToUrl($this->config['url'], $path) 756 | : $path; 757 | } 758 | 759 | /** 760 | * Get the URL for the file at the given path. 761 | * 762 | * @param string $path 763 | * @return string 764 | */ 765 | protected function getLocalUrl($path) 766 | { 767 | // If an explicit base URL has been set on the disk configuration then we will use 768 | // it as the base URL instead of the default path. This allows the developer to 769 | // have full control over the base path for this filesystem's generated URLs. 770 | if (isset($this->config['url'])) { 771 | return $this->concatPathToUrl($this->config['url'], $path); 772 | } 773 | 774 | $path = '/storage/'.$path; 775 | 776 | // If the path contains "storage/public", it probably means the developer is using 777 | // the default disk to generate the path instead of the "public" disk like they 778 | // are really supposed to use. We will remove the public from this path here. 779 | if (str_contains($path, '/storage/public/')) { 780 | return Str::replaceFirst('/public/', '/', $path); 781 | } 782 | 783 | return $path; 784 | } 785 | 786 | /** 787 | * Determine if temporary URLs can be generated. 788 | * 789 | * @return bool 790 | */ 791 | public function providesTemporaryUrls() 792 | { 793 | return method_exists($this->adapter, 'getTemporaryUrl') || isset($this->temporaryUrlCallback); 794 | } 795 | 796 | /** 797 | * Get a temporary URL for the file at the given path. 798 | * 799 | * @param string $path 800 | * @param \DateTimeInterface $expiration 801 | * @param array $options 802 | * @return string 803 | * 804 | * @throws \RuntimeException 805 | */ 806 | public function temporaryUrl($path, $expiration, array $options = []) 807 | { 808 | if (method_exists($this->adapter, 'getTemporaryUrl')) { 809 | return $this->adapter->getTemporaryUrl($path, $expiration, $options); 810 | } 811 | 812 | if ($this->temporaryUrlCallback) { 813 | return $this->temporaryUrlCallback->bindTo($this, static::class)( 814 | $path, $expiration, $options 815 | ); 816 | } 817 | 818 | throw new RuntimeException('This driver does not support creating temporary URLs.'); 819 | } 820 | 821 | /** 822 | * Get a temporary upload URL for the file at the given path. 823 | * 824 | * @param string $path 825 | * @param \DateTimeInterface $expiration 826 | * @param array $options 827 | * @return array 828 | * 829 | * @throws \RuntimeException 830 | */ 831 | public function temporaryUploadUrl($path, $expiration, array $options = []) 832 | { 833 | if (method_exists($this->adapter, 'temporaryUploadUrl')) { 834 | return $this->adapter->temporaryUploadUrl($path, $expiration, $options); 835 | } 836 | 837 | throw new RuntimeException('This driver does not support creating temporary upload URLs.'); 838 | } 839 | 840 | /** 841 | * Concatenate a path to a URL. 842 | * 843 | * @param string $url 844 | * @param string $path 845 | * @return string 846 | */ 847 | protected function concatPathToUrl($url, $path) 848 | { 849 | return rtrim($url, '/').'/'.ltrim($path, '/'); 850 | } 851 | 852 | /** 853 | * Replace the scheme, host and port of the given UriInterface with values from the given URL. 854 | * 855 | * @param \Psr\Http\Message\UriInterface $uri 856 | * @param string $url 857 | * @return \Psr\Http\Message\UriInterface 858 | */ 859 | protected function replaceBaseUrl($uri, $url) 860 | { 861 | $parsed = parse_url($url); 862 | 863 | return $uri 864 | ->withScheme($parsed['scheme']) 865 | ->withHost($parsed['host']) 866 | ->withPort($parsed['port'] ?? null); 867 | } 868 | 869 | /** 870 | * Get an array of all files in a directory. 871 | * 872 | * @param string|null $directory 873 | * @param bool $recursive 874 | * @return array 875 | */ 876 | public function files($directory = null, $recursive = false) 877 | { 878 | return $this->driver->listContents($directory ?? '', $recursive) 879 | ->filter(function (StorageAttributes $attributes) { 880 | return $attributes->isFile(); 881 | }) 882 | ->sortByPath() 883 | ->map(function (StorageAttributes $attributes) { 884 | return $attributes->path(); 885 | }) 886 | ->toArray(); 887 | } 888 | 889 | /** 890 | * Get all of the files from the given directory (recursive). 891 | * 892 | * @param string|null $directory 893 | * @return array 894 | */ 895 | public function allFiles($directory = null) 896 | { 897 | return $this->files($directory, true); 898 | } 899 | 900 | /** 901 | * Get all of the directories within a given directory. 902 | * 903 | * @param string|null $directory 904 | * @param bool $recursive 905 | * @return array 906 | */ 907 | public function directories($directory = null, $recursive = false) 908 | { 909 | return $this->driver->listContents($directory ?? '', $recursive) 910 | ->filter(function (StorageAttributes $attributes) { 911 | return $attributes->isDir(); 912 | }) 913 | ->map(function (StorageAttributes $attributes) { 914 | return $attributes->path(); 915 | }) 916 | ->toArray(); 917 | } 918 | 919 | /** 920 | * Get all the directories within a given directory (recursive). 921 | * 922 | * @param string|null $directory 923 | * @return array 924 | */ 925 | public function allDirectories($directory = null) 926 | { 927 | return $this->directories($directory, true); 928 | } 929 | 930 | /** 931 | * Create a directory. 932 | * 933 | * @param string $path 934 | * @return bool 935 | */ 936 | public function makeDirectory($path) 937 | { 938 | try { 939 | $this->driver->createDirectory($path); 940 | } catch (UnableToCreateDirectory|UnableToSetVisibility $e) { 941 | throw_if($this->throwsExceptions(), $e); 942 | 943 | $this->report($e); 944 | 945 | return false; 946 | } 947 | 948 | return true; 949 | } 950 | 951 | /** 952 | * Recursively delete a directory. 953 | * 954 | * @param string $directory 955 | * @return bool 956 | */ 957 | public function deleteDirectory($directory) 958 | { 959 | try { 960 | $this->driver->deleteDirectory($directory); 961 | } catch (UnableToDeleteDirectory $e) { 962 | throw_if($this->throwsExceptions(), $e); 963 | 964 | $this->report($e); 965 | 966 | return false; 967 | } 968 | 969 | return true; 970 | } 971 | 972 | /** 973 | * Get the Flysystem driver. 974 | * 975 | * @return \League\Flysystem\FilesystemOperator 976 | */ 977 | public function getDriver() 978 | { 979 | return $this->driver; 980 | } 981 | 982 | /** 983 | * Get the Flysystem adapter. 984 | * 985 | * @return \League\Flysystem\FilesystemAdapter 986 | */ 987 | public function getAdapter() 988 | { 989 | return $this->adapter; 990 | } 991 | 992 | /** 993 | * Get the configuration values. 994 | * 995 | * @return array 996 | */ 997 | public function getConfig() 998 | { 999 | return $this->config; 1000 | } 1001 | 1002 | /** 1003 | * Parse the given visibility value. 1004 | * 1005 | * @param string|null $visibility 1006 | * @return string|null 1007 | * 1008 | * @throws \InvalidArgumentException 1009 | */ 1010 | protected function parseVisibility($visibility) 1011 | { 1012 | if (is_null($visibility)) { 1013 | return; 1014 | } 1015 | 1016 | return match ($visibility) { 1017 | FilesystemContract::VISIBILITY_PUBLIC => Visibility::PUBLIC, 1018 | FilesystemContract::VISIBILITY_PRIVATE => Visibility::PRIVATE, 1019 | default => throw new InvalidArgumentException("Unknown visibility: {$visibility}."), 1020 | }; 1021 | } 1022 | 1023 | /** 1024 | * Define a custom callback that generates file download responses. 1025 | * 1026 | * @param \Closure $callback 1027 | * @return void 1028 | */ 1029 | public function serveUsing(Closure $callback) 1030 | { 1031 | $this->serveCallback = $callback; 1032 | } 1033 | 1034 | /** 1035 | * Define a custom temporary URL builder callback. 1036 | * 1037 | * @param \Closure $callback 1038 | * @return void 1039 | */ 1040 | public function buildTemporaryUrlsUsing(Closure $callback) 1041 | { 1042 | $this->temporaryUrlCallback = $callback; 1043 | } 1044 | 1045 | /** 1046 | * Determine if Flysystem exceptions should be thrown. 1047 | * 1048 | * @return bool 1049 | */ 1050 | protected function throwsExceptions(): bool 1051 | { 1052 | return (bool) ($this->config['throw'] ?? false); 1053 | } 1054 | 1055 | /** 1056 | * @param Throwable $exception 1057 | * @return void 1058 | * 1059 | * @throws Throwable 1060 | */ 1061 | protected function report($exception) 1062 | { 1063 | if ($this->shouldReport() && Container::getInstance()->bound(ExceptionHandler::class)) { 1064 | Container::getInstance()->make(ExceptionHandler::class)->report($exception); 1065 | } 1066 | } 1067 | 1068 | /** 1069 | * Determine if Flysystem exceptions should be reported. 1070 | * 1071 | * @return bool 1072 | */ 1073 | protected function shouldReport(): bool 1074 | { 1075 | return (bool) ($this->config['report'] ?? false); 1076 | } 1077 | 1078 | /** 1079 | * Pass dynamic methods call onto Flysystem. 1080 | * 1081 | * @param string $method 1082 | * @param array $parameters 1083 | * @return mixed 1084 | * 1085 | * @throws \BadMethodCallException 1086 | */ 1087 | public function __call($method, $parameters) 1088 | { 1089 | if (static::hasMacro($method)) { 1090 | return $this->macroCall($method, $parameters); 1091 | } 1092 | 1093 | return $this->driver->{$method}(...$parameters); 1094 | } 1095 | } 1096 | -------------------------------------------------------------------------------- /FilesystemManager.php: -------------------------------------------------------------------------------- 1 | app = $app; 59 | } 60 | 61 | /** 62 | * Get a filesystem instance. 63 | * 64 | * @param string|null $name 65 | * @return \Illuminate\Contracts\Filesystem\Filesystem 66 | */ 67 | public function drive($name = null) 68 | { 69 | return $this->disk($name); 70 | } 71 | 72 | /** 73 | * Get a filesystem instance. 74 | * 75 | * @param string|null $name 76 | * @return \Illuminate\Contracts\Filesystem\Filesystem 77 | */ 78 | public function disk($name = null) 79 | { 80 | $name = $name ?: $this->getDefaultDriver(); 81 | 82 | return $this->disks[$name] = $this->get($name); 83 | } 84 | 85 | /** 86 | * Get a default cloud filesystem instance. 87 | * 88 | * @return \Illuminate\Contracts\Filesystem\Cloud 89 | */ 90 | public function cloud() 91 | { 92 | $name = $this->getDefaultCloudDriver(); 93 | 94 | return $this->disks[$name] = $this->get($name); 95 | } 96 | 97 | /** 98 | * Build an on-demand disk. 99 | * 100 | * @param string|array $config 101 | * @return \Illuminate\Contracts\Filesystem\Filesystem 102 | */ 103 | public function build($config) 104 | { 105 | return $this->resolve('ondemand', is_array($config) ? $config : [ 106 | 'driver' => 'local', 107 | 'root' => $config, 108 | ]); 109 | } 110 | 111 | /** 112 | * Attempt to get the disk from the local cache. 113 | * 114 | * @param string $name 115 | * @return \Illuminate\Contracts\Filesystem\Filesystem 116 | */ 117 | protected function get($name) 118 | { 119 | return $this->disks[$name] ?? $this->resolve($name); 120 | } 121 | 122 | /** 123 | * Resolve the given disk. 124 | * 125 | * @param string $name 126 | * @param array|null $config 127 | * @return \Illuminate\Contracts\Filesystem\Filesystem 128 | * 129 | * @throws \InvalidArgumentException 130 | */ 131 | protected function resolve($name, $config = null) 132 | { 133 | $config ??= $this->getConfig($name); 134 | 135 | if (empty($config['driver'])) { 136 | throw new InvalidArgumentException("Disk [{$name}] does not have a configured driver."); 137 | } 138 | 139 | $driver = $config['driver']; 140 | 141 | if (isset($this->customCreators[$driver])) { 142 | return $this->callCustomCreator($config); 143 | } 144 | 145 | $driverMethod = 'create'.ucfirst($driver).'Driver'; 146 | 147 | if (! method_exists($this, $driverMethod)) { 148 | throw new InvalidArgumentException("Driver [{$driver}] is not supported."); 149 | } 150 | 151 | return $this->{$driverMethod}($config, $name); 152 | } 153 | 154 | /** 155 | * Call a custom driver creator. 156 | * 157 | * @param array $config 158 | * @return \Illuminate\Contracts\Filesystem\Filesystem 159 | */ 160 | protected function callCustomCreator(array $config) 161 | { 162 | return $this->customCreators[$config['driver']]($this->app, $config); 163 | } 164 | 165 | /** 166 | * Create an instance of the local driver. 167 | * 168 | * @param array $config 169 | * @param string $name 170 | * @return \Illuminate\Contracts\Filesystem\Filesystem 171 | */ 172 | public function createLocalDriver(array $config, string $name = 'local') 173 | { 174 | $visibility = PortableVisibilityConverter::fromArray( 175 | $config['permissions'] ?? [], 176 | $config['directory_visibility'] ?? $config['visibility'] ?? Visibility::PRIVATE 177 | ); 178 | 179 | $links = ($config['links'] ?? null) === 'skip' 180 | ? LocalAdapter::SKIP_LINKS 181 | : LocalAdapter::DISALLOW_LINKS; 182 | 183 | $adapter = new LocalAdapter( 184 | $config['root'], $visibility, $config['lock'] ?? LOCK_EX, $links 185 | ); 186 | 187 | return (new LocalFilesystemAdapter( 188 | $this->createFlysystem($adapter, $config), $adapter, $config 189 | ))->diskName( 190 | $name 191 | )->shouldServeSignedUrls( 192 | $config['serve'] ?? false, 193 | fn () => $this->app['url'], 194 | ); 195 | } 196 | 197 | /** 198 | * Create an instance of the ftp driver. 199 | * 200 | * @param array $config 201 | * @return \Illuminate\Contracts\Filesystem\Filesystem 202 | */ 203 | public function createFtpDriver(array $config) 204 | { 205 | if (! isset($config['root'])) { 206 | $config['root'] = ''; 207 | } 208 | 209 | $adapter = new FtpAdapter(FtpConnectionOptions::fromArray($config)); 210 | 211 | return new FilesystemAdapter($this->createFlysystem($adapter, $config), $adapter, $config); 212 | } 213 | 214 | /** 215 | * Create an instance of the sftp driver. 216 | * 217 | * @param array $config 218 | * @return \Illuminate\Contracts\Filesystem\Filesystem 219 | */ 220 | public function createSftpDriver(array $config) 221 | { 222 | $provider = SftpConnectionProvider::fromArray($config); 223 | 224 | $root = $config['root'] ?? ''; 225 | 226 | $visibility = PortableVisibilityConverter::fromArray( 227 | $config['permissions'] ?? [] 228 | ); 229 | 230 | $adapter = new SftpAdapter($provider, $root, $visibility); 231 | 232 | return new FilesystemAdapter($this->createFlysystem($adapter, $config), $adapter, $config); 233 | } 234 | 235 | /** 236 | * Create an instance of the Amazon S3 driver. 237 | * 238 | * @param array $config 239 | * @return \Illuminate\Contracts\Filesystem\Cloud 240 | */ 241 | public function createS3Driver(array $config) 242 | { 243 | $s3Config = $this->formatS3Config($config); 244 | 245 | $root = (string) ($s3Config['root'] ?? ''); 246 | 247 | $visibility = new AwsS3PortableVisibilityConverter( 248 | $config['visibility'] ?? Visibility::PUBLIC 249 | ); 250 | 251 | $streamReads = $s3Config['stream_reads'] ?? false; 252 | 253 | $client = new S3Client($s3Config); 254 | 255 | $adapter = new S3Adapter($client, $s3Config['bucket'], $root, $visibility, null, $config['options'] ?? [], $streamReads); 256 | 257 | return new AwsS3V3Adapter( 258 | $this->createFlysystem($adapter, $config), $adapter, $s3Config, $client 259 | ); 260 | } 261 | 262 | /** 263 | * Format the given S3 configuration with the default options. 264 | * 265 | * @param array $config 266 | * @return array 267 | */ 268 | protected function formatS3Config(array $config) 269 | { 270 | $config += ['version' => 'latest']; 271 | 272 | if (! empty($config['key']) && ! empty($config['secret'])) { 273 | $config['credentials'] = Arr::only($config, ['key', 'secret']); 274 | 275 | if (! empty($config['token'])) { 276 | $config['credentials']['token'] = $config['token']; 277 | } 278 | } 279 | 280 | return Arr::except($config, ['token']); 281 | } 282 | 283 | /** 284 | * Create a scoped driver. 285 | * 286 | * @param array $config 287 | * @return \Illuminate\Contracts\Filesystem\Filesystem 288 | */ 289 | public function createScopedDriver(array $config) 290 | { 291 | if (empty($config['disk'])) { 292 | throw new InvalidArgumentException('Scoped disk is missing "disk" configuration option.'); 293 | } elseif (empty($config['prefix'])) { 294 | throw new InvalidArgumentException('Scoped disk is missing "prefix" configuration option.'); 295 | } 296 | 297 | return $this->build(tap( 298 | is_string($config['disk']) ? $this->getConfig($config['disk']) : $config['disk'], 299 | function (&$parent) use ($config) { 300 | if (empty($parent['prefix'])) { 301 | $parent['prefix'] = $config['prefix']; 302 | } else { 303 | $separator = $parent['directory_separator'] ?? DIRECTORY_SEPARATOR; 304 | 305 | $parentPrefix = rtrim($parent['prefix'], $separator); 306 | $scopedPrefix = ltrim($config['prefix'], $separator); 307 | 308 | $parent['prefix'] = "{$parentPrefix}{$separator}{$scopedPrefix}"; 309 | } 310 | 311 | if (isset($config['visibility'])) { 312 | $parent['visibility'] = $config['visibility']; 313 | } 314 | } 315 | )); 316 | } 317 | 318 | /** 319 | * Create a Flysystem instance with the given adapter. 320 | * 321 | * @param \League\Flysystem\FilesystemAdapter $adapter 322 | * @param array $config 323 | * @return \League\Flysystem\FilesystemOperator 324 | */ 325 | protected function createFlysystem(FlysystemAdapter $adapter, array $config) 326 | { 327 | if ($config['read-only'] ?? false === true) { 328 | $adapter = new ReadOnlyFilesystemAdapter($adapter); 329 | } 330 | 331 | if (! empty($config['prefix'])) { 332 | $adapter = new PathPrefixedAdapter($adapter, $config['prefix']); 333 | } 334 | 335 | if (str_contains($config['endpoint'] ?? '', 'r2.cloudflarestorage.com')) { 336 | $config['retain_visibility'] = false; 337 | } 338 | 339 | return new Flysystem($adapter, Arr::only($config, [ 340 | 'directory_visibility', 341 | 'disable_asserts', 342 | 'retain_visibility', 343 | 'temporary_url', 344 | 'url', 345 | 'visibility', 346 | ])); 347 | } 348 | 349 | /** 350 | * Set the given disk instance. 351 | * 352 | * @param string $name 353 | * @param mixed $disk 354 | * @return $this 355 | */ 356 | public function set($name, $disk) 357 | { 358 | $this->disks[$name] = $disk; 359 | 360 | return $this; 361 | } 362 | 363 | /** 364 | * Get the filesystem connection configuration. 365 | * 366 | * @param string $name 367 | * @return array 368 | */ 369 | protected function getConfig($name) 370 | { 371 | return $this->app['config']["filesystems.disks.{$name}"] ?: []; 372 | } 373 | 374 | /** 375 | * Get the default driver name. 376 | * 377 | * @return string 378 | */ 379 | public function getDefaultDriver() 380 | { 381 | return $this->app['config']['filesystems.default']; 382 | } 383 | 384 | /** 385 | * Get the default cloud driver name. 386 | * 387 | * @return string 388 | */ 389 | public function getDefaultCloudDriver() 390 | { 391 | return $this->app['config']['filesystems.cloud'] ?? 's3'; 392 | } 393 | 394 | /** 395 | * Unset the given disk instances. 396 | * 397 | * @param array|string $disk 398 | * @return $this 399 | */ 400 | public function forgetDisk($disk) 401 | { 402 | foreach ((array) $disk as $diskName) { 403 | unset($this->disks[$diskName]); 404 | } 405 | 406 | return $this; 407 | } 408 | 409 | /** 410 | * Disconnect the given disk and remove from local cache. 411 | * 412 | * @param string|null $name 413 | * @return void 414 | */ 415 | public function purge($name = null) 416 | { 417 | $name ??= $this->getDefaultDriver(); 418 | 419 | unset($this->disks[$name]); 420 | } 421 | 422 | /** 423 | * Register a custom driver creator Closure. 424 | * 425 | * @param string $driver 426 | * @param \Closure $callback 427 | * @return $this 428 | */ 429 | public function extend($driver, Closure $callback) 430 | { 431 | $this->customCreators[$driver] = $callback; 432 | 433 | return $this; 434 | } 435 | 436 | /** 437 | * Set the application instance used by the manager. 438 | * 439 | * @param \Illuminate\Contracts\Foundation\Application $app 440 | * @return $this 441 | */ 442 | public function setApplication($app) 443 | { 444 | $this->app = $app; 445 | 446 | return $this; 447 | } 448 | 449 | /** 450 | * Dynamically call the default driver instance. 451 | * 452 | * @param string $method 453 | * @param array $parameters 454 | * @return mixed 455 | */ 456 | public function __call($method, $parameters) 457 | { 458 | return $this->disk()->$method(...$parameters); 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /FilesystemServiceProvider.php: -------------------------------------------------------------------------------- 1 | serveFiles(); 20 | } 21 | 22 | /** 23 | * Register the service provider. 24 | * 25 | * @return void 26 | */ 27 | public function register() 28 | { 29 | $this->registerNativeFilesystem(); 30 | $this->registerFlysystem(); 31 | } 32 | 33 | /** 34 | * Register the native filesystem implementation. 35 | * 36 | * @return void 37 | */ 38 | protected function registerNativeFilesystem() 39 | { 40 | $this->app->singleton('files', function () { 41 | return new Filesystem; 42 | }); 43 | } 44 | 45 | /** 46 | * Register the driver based filesystem. 47 | * 48 | * @return void 49 | */ 50 | protected function registerFlysystem() 51 | { 52 | $this->registerManager(); 53 | 54 | $this->app->singleton('filesystem.disk', function ($app) { 55 | return $app['filesystem']->disk($this->getDefaultDriver()); 56 | }); 57 | 58 | $this->app->singleton('filesystem.cloud', function ($app) { 59 | return $app['filesystem']->disk($this->getCloudDriver()); 60 | }); 61 | } 62 | 63 | /** 64 | * Register the filesystem manager. 65 | * 66 | * @return void 67 | */ 68 | protected function registerManager() 69 | { 70 | $this->app->singleton('filesystem', function ($app) { 71 | return new FilesystemManager($app); 72 | }); 73 | } 74 | 75 | /** 76 | * Register protected file serving. 77 | * 78 | * @return void 79 | */ 80 | protected function serveFiles() 81 | { 82 | if ($this->app instanceof CachesRoutes && $this->app->routesAreCached()) { 83 | return; 84 | } 85 | 86 | foreach ($this->app['config']['filesystems.disks'] ?? [] as $disk => $config) { 87 | if (! $this->shouldServeFiles($config)) { 88 | continue; 89 | } 90 | 91 | $this->app->booted(function ($app) use ($disk, $config) { 92 | $uri = isset($config['url']) 93 | ? rtrim(parse_url($config['url'])['path'], '/') 94 | : '/storage'; 95 | 96 | $isProduction = $app->isProduction(); 97 | 98 | Route::get($uri.'/{path}', function (Request $request, string $path) use ($disk, $config, $isProduction) { 99 | return (new ServeFile( 100 | $disk, 101 | $config, 102 | $isProduction 103 | ))($request, $path); 104 | })->where('path', '.*')->name('storage.'.$disk); 105 | }); 106 | } 107 | } 108 | 109 | /** 110 | * Determine if the disk is serveable. 111 | * 112 | * @param array $config 113 | * @return bool 114 | */ 115 | protected function shouldServeFiles(array $config) 116 | { 117 | return $config['driver'] === 'local' && ($config['serve'] ?? false); 118 | } 119 | 120 | /** 121 | * Get the default file driver. 122 | * 123 | * @return string 124 | */ 125 | protected function getDefaultDriver() 126 | { 127 | return $this->app['config']['filesystems.default']; 128 | } 129 | 130 | /** 131 | * Get the default cloud based file driver. 132 | * 133 | * @return string 134 | */ 135 | protected function getCloudDriver() 136 | { 137 | return $this->app['config']['filesystems.cloud']; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Taylor Otwell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /LocalFilesystemAdapter.php: -------------------------------------------------------------------------------- 1 | temporaryUrlCallback || ( 42 | $this->shouldServeSignedUrls && $this->urlGeneratorResolver instanceof Closure 43 | ); 44 | } 45 | 46 | /** 47 | * Get a temporary URL for the file at the given path. 48 | * 49 | * @param string $path 50 | * @param \DateTimeInterface $expiration 51 | * @param array $options 52 | * @return string 53 | */ 54 | public function temporaryUrl($path, $expiration, array $options = []) 55 | { 56 | if ($this->temporaryUrlCallback) { 57 | return $this->temporaryUrlCallback->bindTo($this, static::class)( 58 | $path, $expiration, $options 59 | ); 60 | } 61 | 62 | if (! $this->providesTemporaryUrls()) { 63 | throw new RuntimeException('This driver does not support creating temporary URLs.'); 64 | } 65 | 66 | $url = call_user_func($this->urlGeneratorResolver); 67 | 68 | return $url->to($url->temporarySignedRoute( 69 | 'storage.'.$this->disk, 70 | $expiration, 71 | ['path' => $path], 72 | absolute: false 73 | )); 74 | } 75 | 76 | /** 77 | * Specify the name of the disk the adapter is managing. 78 | * 79 | * @param string $disk 80 | * @return $this 81 | */ 82 | public function diskName(string $disk) 83 | { 84 | $this->disk = $disk; 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * Indicate that signed URLs should serve the corresponding files. 91 | * 92 | * @param bool $serve 93 | * @param \Closure|null $urlGeneratorResolver 94 | * @return $this 95 | */ 96 | public function shouldServeSignedUrls(bool $serve = true, ?Closure $urlGeneratorResolver = null) 97 | { 98 | $this->shouldServeSignedUrls = $serve; 99 | $this->urlGeneratorResolver = $urlGeneratorResolver; 100 | 101 | return $this; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /LockableFile.php: -------------------------------------------------------------------------------- 1 | path = $path; 39 | 40 | $this->ensureDirectoryExists($path); 41 | $this->createResource($path, $mode); 42 | } 43 | 44 | /** 45 | * Create the file's directory if necessary. 46 | * 47 | * @param string $path 48 | * @return void 49 | */ 50 | protected function ensureDirectoryExists($path) 51 | { 52 | if (! file_exists(dirname($path))) { 53 | @mkdir(dirname($path), 0777, true); 54 | } 55 | } 56 | 57 | /** 58 | * Create the file resource. 59 | * 60 | * @param string $path 61 | * @param string $mode 62 | * @return void 63 | * 64 | * @throws \Exception 65 | */ 66 | protected function createResource($path, $mode) 67 | { 68 | $this->handle = fopen($path, $mode); 69 | } 70 | 71 | /** 72 | * Read the file contents. 73 | * 74 | * @param int|null $length 75 | * @return string 76 | */ 77 | public function read($length = null) 78 | { 79 | clearstatcache(true, $this->path); 80 | 81 | return fread($this->handle, $length ?? ($this->size() ?: 1)); 82 | } 83 | 84 | /** 85 | * Get the file size. 86 | * 87 | * @return int 88 | */ 89 | public function size() 90 | { 91 | return filesize($this->path); 92 | } 93 | 94 | /** 95 | * Write to the file. 96 | * 97 | * @param string $contents 98 | * @return $this 99 | */ 100 | public function write($contents) 101 | { 102 | fwrite($this->handle, $contents); 103 | 104 | fflush($this->handle); 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * Truncate the file. 111 | * 112 | * @return $this 113 | */ 114 | public function truncate() 115 | { 116 | rewind($this->handle); 117 | 118 | ftruncate($this->handle, 0); 119 | 120 | return $this; 121 | } 122 | 123 | /** 124 | * Get a shared lock on the file. 125 | * 126 | * @param bool $block 127 | * @return $this 128 | * 129 | * @throws \Illuminate\Contracts\Filesystem\LockTimeoutException 130 | */ 131 | public function getSharedLock($block = false) 132 | { 133 | if (! flock($this->handle, LOCK_SH | ($block ? 0 : LOCK_NB))) { 134 | throw new LockTimeoutException("Unable to acquire file lock at path [{$this->path}]."); 135 | } 136 | 137 | $this->isLocked = true; 138 | 139 | return $this; 140 | } 141 | 142 | /** 143 | * Get an exclusive lock on the file. 144 | * 145 | * @param bool $block 146 | * @return $this 147 | * 148 | * @throws \Illuminate\Contracts\Filesystem\LockTimeoutException 149 | */ 150 | public function getExclusiveLock($block = false) 151 | { 152 | if (! flock($this->handle, LOCK_EX | ($block ? 0 : LOCK_NB))) { 153 | throw new LockTimeoutException("Unable to acquire file lock at path [{$this->path}]."); 154 | } 155 | 156 | $this->isLocked = true; 157 | 158 | return $this; 159 | } 160 | 161 | /** 162 | * Release the lock on the file. 163 | * 164 | * @return $this 165 | */ 166 | public function releaseLock() 167 | { 168 | flock($this->handle, LOCK_UN); 169 | 170 | $this->isLocked = false; 171 | 172 | return $this; 173 | } 174 | 175 | /** 176 | * Close the file. 177 | * 178 | * @return bool 179 | */ 180 | public function close() 181 | { 182 | if ($this->isLocked) { 183 | $this->releaseLock(); 184 | } 185 | 186 | return fclose($this->handle); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /ServeFile.php: -------------------------------------------------------------------------------- 1 | hasValidSignature($request), 29 | $this->isProduction ? 404 : 403 30 | ); 31 | try { 32 | abort_unless(Storage::disk($this->disk)->exists($path), 404); 33 | 34 | $headers = [ 35 | 'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0', 36 | 'Content-Security-Policy' => "default-src 'none'; style-src 'unsafe-inline'; sandbox", 37 | ]; 38 | 39 | return tap( 40 | Storage::disk($this->disk)->serve($request, $path, headers: $headers), 41 | function ($response) use ($headers) { 42 | if (! $response->headers->has('Content-Security-Policy')) { 43 | $response->headers->replace($headers); 44 | } 45 | } 46 | ); 47 | } catch (PathTraversalDetected $e) { 48 | abort(404); 49 | } 50 | } 51 | 52 | /** 53 | * Determine if the request has a valid signature if applicable. 54 | */ 55 | protected function hasValidSignature(Request $request): bool 56 | { 57 | return ($this->config['visibility'] ?? 'private') === 'public' || 58 | $request->hasValidRelativeSignature(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "illuminate/filesystem", 3 | "description": "The Illuminate Filesystem package.", 4 | "license": "MIT", 5 | "homepage": "https://laravel.com", 6 | "support": { 7 | "issues": "https://github.com/laravel/framework/issues", 8 | "source": "https://github.com/laravel/framework" 9 | }, 10 | "authors": [ 11 | { 12 | "name": "Taylor Otwell", 13 | "email": "taylor@laravel.com" 14 | } 15 | ], 16 | "require": { 17 | "php": "^8.3", 18 | "illuminate/collections": "^13.0", 19 | "illuminate/contracts": "^13.0", 20 | "illuminate/macroable": "^13.0", 21 | "illuminate/support": "^13.0", 22 | "symfony/finder": "^7.2.0" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Illuminate\\Filesystem\\": "" 27 | }, 28 | "files": [ 29 | "functions.php" 30 | ] 31 | }, 32 | "extra": { 33 | "branch-alias": { 34 | "dev-master": "13.0.x-dev" 35 | } 36 | }, 37 | "suggest": { 38 | "ext-fileinfo": "Required to use the Filesystem class.", 39 | "ext-ftp": "Required to use the Flysystem FTP driver.", 40 | "ext-hash": "Required to use the Filesystem class.", 41 | "illuminate/http": "Required for handling uploaded files (^13.0).", 42 | "league/flysystem": "Required to use the Flysystem local driver (^3.25.1).", 43 | "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", 44 | "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", 45 | "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", 46 | "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", 47 | "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", 48 | "symfony/mime": "Required to enable support for guessing extensions (^7.2)." 49 | }, 50 | "config": { 51 | "sort-packages": true 52 | }, 53 | "minimum-stability": "dev" 54 | } 55 | -------------------------------------------------------------------------------- /functions.php: -------------------------------------------------------------------------------- 1 | $path) { 16 | if (empty($path) && $path !== '0') { 17 | unset($paths[$index]); 18 | } else { 19 | $paths[$index] = DIRECTORY_SEPARATOR.ltrim($path, DIRECTORY_SEPARATOR); 20 | } 21 | } 22 | 23 | return $basePath.implode('', $paths); 24 | } 25 | } 26 | --------------------------------------------------------------------------------