├── INFO.md ├── LICENSE ├── composer.json ├── readme.md └── src ├── CalculateChecksumFromStream.php ├── ChecksumAlgoIsNotSupported.php ├── ChecksumProvider.php ├── Config.php ├── CorruptedPathDetected.php ├── DecoratedAdapter.php ├── DirectoryAttributes.php ├── DirectoryListing.php ├── FileAttributes.php ├── Filesystem.php ├── FilesystemAdapter.php ├── FilesystemException.php ├── FilesystemOperationFailed.php ├── FilesystemOperator.php ├── FilesystemReader.php ├── FilesystemWriter.php ├── InvalidStreamProvided.php ├── InvalidVisibilityProvided.php ├── MountManager.php ├── PathNormalizer.php ├── PathPrefixer.php ├── PathTraversalDetected.php ├── PortableVisibilityGuard.php ├── ProxyArrayAccessToProperties.php ├── ResolveIdenticalPathConflict.php ├── StorageAttributes.php ├── SymbolicLinkEncountered.php ├── UnableToCheckDirectoryExistence.php ├── UnableToCheckExistence.php ├── UnableToCheckFileExistence.php ├── UnableToCopyFile.php ├── UnableToCreateDirectory.php ├── UnableToDeleteDirectory.php ├── UnableToDeleteFile.php ├── UnableToGeneratePublicUrl.php ├── UnableToGenerateTemporaryUrl.php ├── UnableToListContents.php ├── UnableToMountFilesystem.php ├── UnableToMoveFile.php ├── UnableToProvideChecksum.php ├── UnableToReadFile.php ├── UnableToResolveFilesystemMount.php ├── UnableToRetrieveMetadata.php ├── UnableToSetVisibility.php ├── UnableToWriteFile.php ├── UnixVisibility ├── PortableVisibilityConverter.php └── VisibilityConverter.php ├── UnreadableFileEncountered.php ├── UrlGeneration ├── ChainedPublicUrlGenerator.php ├── PrefixPublicUrlGenerator.php ├── PublicUrlGenerator.php ├── ShardedPrefixPublicUrlGenerator.php └── TemporaryUrlGenerator.php ├── Visibility.php └── WhitespacePathNormalizer.php /INFO.md: -------------------------------------------------------------------------------- 1 | View the docs at: https://flysystem.thephpleague.com/docs/ 2 | Changelog at: https://github.com/thephpleague/flysystem/blob/3.x/CHANGELOG.md 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2024 Frank de Jonge 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "league/flysystem", 3 | "description": "File storage abstraction for PHP", 4 | "keywords": [ 5 | "filesystem", "filesystems", "files", "storage", "aws", 6 | "s3", "ftp", "sftp", "webdav", "file", "cloud" 7 | ], 8 | "scripts": { 9 | "phpstan": "vendor/bin/phpstan analyse -l 6 src" 10 | }, 11 | "type": "library", 12 | "minimum-stability": "dev", 13 | "prefer-stable": true, 14 | "autoload": { 15 | "psr-4": { 16 | "League\\Flysystem\\": "src" 17 | } 18 | }, 19 | "require": { 20 | "php": "^8.0.2", 21 | "league/flysystem-local": "^3.0.0", 22 | "league/mime-type-detection": "^1.0.0" 23 | }, 24 | "require-dev": { 25 | "ext-zip": "*", 26 | "ext-fileinfo": "*", 27 | "ext-ftp": "*", 28 | "ext-mongodb": "^1.3|^2", 29 | "microsoft/azure-storage-blob": "^1.1", 30 | "phpunit/phpunit": "^9.5.11|^10.0", 31 | "phpstan/phpstan": "^1.10", 32 | "phpseclib/phpseclib": "^3.0.36", 33 | "aws/aws-sdk-php": "^3.295.10", 34 | "composer/semver": "^3.0", 35 | "friendsofphp/php-cs-fixer": "^3.5", 36 | "google/cloud-storage": "^1.23", 37 | "async-aws/s3": "^1.5 || ^2.0", 38 | "async-aws/simple-s3": "^1.1 || ^2.0", 39 | "mongodb/mongodb": "^1.2|^2", 40 | "sabre/dav": "^4.6.0", 41 | "guzzlehttp/psr7": "^2.6" 42 | }, 43 | "conflict": { 44 | "async-aws/core": "<1.19.0", 45 | "async-aws/s3": "<1.14.0", 46 | "symfony/http-client": "<5.2", 47 | "guzzlehttp/ringphp": "<1.1.1", 48 | "guzzlehttp/guzzle": "<7.0", 49 | "aws/aws-sdk-php": "3.209.31 || 3.210.0", 50 | "phpseclib/phpseclib": "3.0.15" 51 | }, 52 | "license": "MIT", 53 | "authors": [ 54 | { 55 | "name": "Frank de Jonge", 56 | "email": "info@frankdejonge.nl" 57 | } 58 | ], 59 | "repositories": [ 60 | { 61 | "type": "package", 62 | "package": { 63 | "name": "league/flysystem-local", 64 | "version": "3.0.0", 65 | "dist": { 66 | "type": "path", 67 | "url": "src/Local" 68 | } 69 | } 70 | } 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # League\Flysystem 2 | 3 | [![Author](https://img.shields.io/badge/author-@frankdejonge-blue.svg)](https://twitter.com/frankdejonge) 4 | [![Source Code](https://img.shields.io/badge/source-thephpleague/flysystem-blue.svg)](https://github.com/thephpleague/flysystem) 5 | [![Latest Version](https://img.shields.io/github/tag/thephpleague/flysystem.svg)](https://github.com/thephpleague/flysystem/releases) 6 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://github.com/thephpleague/flysystem/blob/master/LICENSE) 7 | [![Quality Assurance](https://github.com/thephpleague/flysystem/workflows/Quality%20Assurance/badge.svg?branch=2.x)](https://github.com/thephpleague/flysystem/actions?query=workflow%3A%22Quality+Assurance%22) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/league/flysystem.svg)](https://packagist.org/packages/league/flysystem) 9 | ![php 7.2+](https://img.shields.io/badge/php-min%208.0.2-red.svg) 10 | 11 | ## About Flysystem 12 | 13 | Flysystem is a file storage library for PHP. It provides one interface to 14 | interact with many types of filesystems. When you use Flysystem, you're 15 | not only protected from vendor lock-in, you'll also have a consistent experience 16 | for which ever storage is right for you. 17 | 18 | ## Getting Started 19 | 20 | * **[New in V3](https://flysystem.thephpleague.com/docs/what-is-new/)**: What is new in Flysystem V2/V3? 21 | * **[Architecture](https://flysystem.thephpleague.com/docs/architecture/)**: Flysystem's internal architecture 22 | * **[Flysystem API](https://flysystem.thephpleague.com/docs/usage/filesystem-api/)**: How to interact with your Flysystem instance 23 | * **[Upgrade from 1x](https://flysystem.thephpleague.com/docs/upgrade-from-1.x/)**: How to upgrade from 1.x/2.x 24 | 25 | ### Officially supported adapters 26 | 27 | * **[Local](https://flysystem.thephpleague.com/docs/adapter/local/)** 28 | * **[FTP](https://flysystem.thephpleague.com/docs/adapter/ftp/)** 29 | * **[SFTP](https://flysystem.thephpleague.com/docs/adapter/sftp-v3/)** 30 | * **[Memory](https://flysystem.thephpleague.com/docs/adapter/in-memory/)** 31 | * **[AWS S3](https://flysystem.thephpleague.com/docs/adapter/aws-s3-v3/)** 32 | * **[AsyncAws S3](https://flysystem.thephpleague.com/docs/adapter/async-aws-s3/)** 33 | * **[Google Cloud Storage](https://flysystem.thephpleague.com/docs/adapter/google-cloud-storage/)** 34 | * **[MongoDB GridFS](https://flysystem.thephpleague.com/docs/adapter/gridfs/)** 35 | * **[WebDAV](https://flysystem.thephpleague.com/docs/adapter/webdav/)** 36 | * **[ZipArchive](https://flysystem.thephpleague.com/docs/adapter/zip-archive/)** 37 | 38 | ### Third party Adapters 39 | 40 | * **[Azure Blob Storage](https://github.com/Azure-OSS/azure-storage-php-adapter-flysystem)** 41 | * **[Gitlab](https://github.com/RoyVoetman/flysystem-gitlab-storage)** 42 | * **[Google Drive (using regular paths)](https://github.com/masbug/flysystem-google-drive-ext)** 43 | * **[bunny.net / BunnyCDN](https://github.com/PlatformCommunity/flysystem-bunnycdn/tree/v3)** 44 | * **[Sharepoint 365 / One Drive (Using MS Graph)](https://github.com/shitware-ltd/flysystem-msgraph)** 45 | * **[OneDrive](https://github.com/doerffler/flysystem-onedrive)** 46 | * **[Dropbox](https://github.com/spatie/flysystem-dropbox)** 47 | * **[ReplicateAdapter](https://github.com/ajgarlag/flysystem-replicate)** 48 | * **[Uploadcare](https://github.com/vormkracht10/flysystem-uploadcare)** 49 | * **[Useful adapters (FallbackAdapter, LogAdapter, ReadWriteAdapter, RetryAdapter)](https://github.com/ElGigi/FlysystemUsefulAdapters)** 50 | * **[Metadata Cache](https://github.com/jgivoni/flysystem-cache-adapter)** 51 | 52 | 53 | You can always [create an adapter](https://flysystem.thephpleague.com/docs/advanced/creating-an-adapter/) yourself. 54 | 55 | ## Security 56 | 57 | If you discover any security related issues, please email info@frankdejonge.nl instead of using the issue tracker. 58 | 59 | ## Enjoy 60 | 61 | Oh, and if you've come down this far, you might as well follow me on [twitter](https://twitter.com/frankdejonge). 62 | -------------------------------------------------------------------------------- /src/CalculateChecksumFromStream.php: -------------------------------------------------------------------------------- 1 | readStream($path); 16 | $algo = (string) $config->get('checksum_algo', 'md5'); 17 | $context = hash_init($algo); 18 | hash_update_stream($context, $stream); 19 | 20 | return hash_final($context); 21 | } catch (FilesystemException $exception) { 22 | throw new UnableToProvideChecksum($exception->getMessage(), $path, $exception); 23 | } 24 | } 25 | 26 | /** 27 | * @return resource 28 | */ 29 | abstract public function readStream(string $path); 30 | } 31 | -------------------------------------------------------------------------------- /src/ChecksumAlgoIsNotSupported.php: -------------------------------------------------------------------------------- 1 | options[$property] ?? $default; 31 | } 32 | 33 | public function extend(array $options): Config 34 | { 35 | return new Config(array_merge($this->options, $options)); 36 | } 37 | 38 | public function withDefaults(array $defaults): Config 39 | { 40 | return new Config($this->options + $defaults); 41 | } 42 | 43 | public function toArray(): array 44 | { 45 | return $this->options; 46 | } 47 | 48 | public function withSetting(string $property, mixed $setting): Config 49 | { 50 | return $this->extend([$property => $setting]); 51 | } 52 | 53 | public function withoutSettings(string ...$settings): Config 54 | { 55 | return new Config(array_diff_key($this->options, array_flip($settings))); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/CorruptedPathDetected.php: -------------------------------------------------------------------------------- 1 | adapter->fileExists($path); 16 | } 17 | 18 | public function directoryExists(string $path): bool 19 | { 20 | return $this->adapter->directoryExists($path); 21 | } 22 | 23 | public function write(string $path, string $contents, Config $config): void 24 | { 25 | $this->adapter->write($path, $contents, $config); 26 | } 27 | 28 | public function writeStream(string $path, $contents, Config $config): void 29 | { 30 | $this->adapter->writeStream($path, $contents, $config); 31 | } 32 | 33 | public function read(string $path): string 34 | { 35 | return $this->adapter->read($path); 36 | } 37 | 38 | public function readStream(string $path) 39 | { 40 | return $this->adapter->readStream($path); 41 | } 42 | 43 | public function delete(string $path): void 44 | { 45 | $this->adapter->delete($path); 46 | } 47 | 48 | public function deleteDirectory(string $path): void 49 | { 50 | $this->adapter->deleteDirectory($path); 51 | } 52 | 53 | public function createDirectory(string $path, Config $config): void 54 | { 55 | $this->adapter->createDirectory($path, $config); 56 | } 57 | 58 | public function setVisibility(string $path, string $visibility): void 59 | { 60 | $this->adapter->setVisibility($path, $visibility); 61 | } 62 | 63 | public function visibility(string $path): FileAttributes 64 | { 65 | return $this->adapter->visibility($path); 66 | } 67 | 68 | public function mimeType(string $path): FileAttributes 69 | { 70 | return $this->adapter->mimeType($path); 71 | } 72 | 73 | public function lastModified(string $path): FileAttributes 74 | { 75 | return $this->adapter->lastModified($path); 76 | } 77 | 78 | public function fileSize(string $path): FileAttributes 79 | { 80 | return $this->adapter->fileSize($path); 81 | } 82 | 83 | public function listContents(string $path, bool $deep): iterable 84 | { 85 | return $this->adapter->listContents($path, $deep); 86 | } 87 | 88 | public function move(string $source, string $destination, Config $config): void 89 | { 90 | $this->adapter->move($source, $destination, $config); 91 | } 92 | 93 | public function copy(string $source, string $destination, Config $config): void 94 | { 95 | $this->adapter->copy($source, $destination, $config); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/DirectoryAttributes.php: -------------------------------------------------------------------------------- 1 | path = trim($this->path, '/'); 19 | } 20 | 21 | public function path(): string 22 | { 23 | return $this->path; 24 | } 25 | 26 | public function type(): string 27 | { 28 | return $this->type; 29 | } 30 | 31 | public function visibility(): ?string 32 | { 33 | return $this->visibility; 34 | } 35 | 36 | public function lastModified(): ?int 37 | { 38 | return $this->lastModified; 39 | } 40 | 41 | public function extraMetadata(): array 42 | { 43 | return $this->extraMetadata; 44 | } 45 | 46 | public function isFile(): bool 47 | { 48 | return false; 49 | } 50 | 51 | public function isDir(): bool 52 | { 53 | return true; 54 | } 55 | 56 | public function withPath(string $path): self 57 | { 58 | $clone = clone $this; 59 | $clone->path = $path; 60 | 61 | return $clone; 62 | } 63 | 64 | public static function fromArray(array $attributes): self 65 | { 66 | return new DirectoryAttributes( 67 | $attributes[StorageAttributes::ATTRIBUTE_PATH], 68 | $attributes[StorageAttributes::ATTRIBUTE_VISIBILITY] ?? null, 69 | $attributes[StorageAttributes::ATTRIBUTE_LAST_MODIFIED] ?? null, 70 | $attributes[StorageAttributes::ATTRIBUTE_EXTRA_METADATA] ?? [] 71 | ); 72 | } 73 | 74 | /** 75 | * @inheritDoc 76 | */ 77 | public function jsonSerialize(): array 78 | { 79 | return [ 80 | StorageAttributes::ATTRIBUTE_TYPE => $this->type, 81 | StorageAttributes::ATTRIBUTE_PATH => $this->path, 82 | StorageAttributes::ATTRIBUTE_VISIBILITY => $this->visibility, 83 | StorageAttributes::ATTRIBUTE_LAST_MODIFIED => $this->lastModified, 84 | StorageAttributes::ATTRIBUTE_EXTRA_METADATA => $this->extraMetadata, 85 | ]; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/DirectoryListing.php: -------------------------------------------------------------------------------- 1 | $listing 19 | */ 20 | public function __construct(private iterable $listing) 21 | { 22 | } 23 | 24 | /** 25 | * @param callable(T): bool $filter 26 | * 27 | * @return DirectoryListing 28 | */ 29 | public function filter(callable $filter): DirectoryListing 30 | { 31 | $generator = (static function (iterable $listing) use ($filter): Generator { 32 | foreach ($listing as $item) { 33 | if ($filter($item)) { 34 | yield $item; 35 | } 36 | } 37 | })($this->listing); 38 | 39 | return new DirectoryListing($generator); 40 | } 41 | 42 | /** 43 | * @template R 44 | * 45 | * @param callable(T): R $mapper 46 | * 47 | * @return DirectoryListing 48 | */ 49 | public function map(callable $mapper): DirectoryListing 50 | { 51 | $generator = (static function (iterable $listing) use ($mapper): Generator { 52 | foreach ($listing as $item) { 53 | yield $mapper($item); 54 | } 55 | })($this->listing); 56 | 57 | return new DirectoryListing($generator); 58 | } 59 | 60 | /** 61 | * @return DirectoryListing 62 | */ 63 | public function sortByPath(): DirectoryListing 64 | { 65 | $listing = $this->toArray(); 66 | 67 | usort($listing, function (StorageAttributes $a, StorageAttributes $b) { 68 | return $a->path() <=> $b->path(); 69 | }); 70 | 71 | return new DirectoryListing($listing); 72 | } 73 | 74 | /** 75 | * @return Traversable 76 | */ 77 | public function getIterator(): Traversable 78 | { 79 | return $this->listing instanceof Traversable 80 | ? $this->listing 81 | : new ArrayIterator($this->listing); 82 | } 83 | 84 | /** 85 | * @return T[] 86 | */ 87 | public function toArray(): array 88 | { 89 | return $this->listing instanceof Traversable 90 | ? iterator_to_array($this->listing, false) 91 | : (array) $this->listing; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/FileAttributes.php: -------------------------------------------------------------------------------- 1 | path = ltrim($this->path, '/'); 21 | } 22 | 23 | public function type(): string 24 | { 25 | return $this->type; 26 | } 27 | 28 | public function path(): string 29 | { 30 | return $this->path; 31 | } 32 | 33 | public function fileSize(): ?int 34 | { 35 | return $this->fileSize; 36 | } 37 | 38 | public function visibility(): ?string 39 | { 40 | return $this->visibility; 41 | } 42 | 43 | public function lastModified(): ?int 44 | { 45 | return $this->lastModified; 46 | } 47 | 48 | public function mimeType(): ?string 49 | { 50 | return $this->mimeType; 51 | } 52 | 53 | public function extraMetadata(): array 54 | { 55 | return $this->extraMetadata; 56 | } 57 | 58 | public function isFile(): bool 59 | { 60 | return true; 61 | } 62 | 63 | public function isDir(): bool 64 | { 65 | return false; 66 | } 67 | 68 | public function withPath(string $path): self 69 | { 70 | $clone = clone $this; 71 | $clone->path = $path; 72 | 73 | return $clone; 74 | } 75 | 76 | public static function fromArray(array $attributes): self 77 | { 78 | return new FileAttributes( 79 | $attributes[StorageAttributes::ATTRIBUTE_PATH], 80 | $attributes[StorageAttributes::ATTRIBUTE_FILE_SIZE] ?? null, 81 | $attributes[StorageAttributes::ATTRIBUTE_VISIBILITY] ?? null, 82 | $attributes[StorageAttributes::ATTRIBUTE_LAST_MODIFIED] ?? null, 83 | $attributes[StorageAttributes::ATTRIBUTE_MIME_TYPE] ?? null, 84 | $attributes[StorageAttributes::ATTRIBUTE_EXTRA_METADATA] ?? [] 85 | ); 86 | } 87 | 88 | public function jsonSerialize(): array 89 | { 90 | return [ 91 | StorageAttributes::ATTRIBUTE_TYPE => self::TYPE_FILE, 92 | StorageAttributes::ATTRIBUTE_PATH => $this->path, 93 | StorageAttributes::ATTRIBUTE_FILE_SIZE => $this->fileSize, 94 | StorageAttributes::ATTRIBUTE_VISIBILITY => $this->visibility, 95 | StorageAttributes::ATTRIBUTE_LAST_MODIFIED => $this->lastModified, 96 | StorageAttributes::ATTRIBUTE_MIME_TYPE => $this->mimeType, 97 | StorageAttributes::ATTRIBUTE_EXTRA_METADATA => $this->extraMetadata, 98 | ]; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Filesystem.php: -------------------------------------------------------------------------------- 1 | config = new Config($config); 33 | $this->pathNormalizer = $pathNormalizer ?? new WhitespacePathNormalizer(); 34 | } 35 | 36 | public function fileExists(string $location): bool 37 | { 38 | return $this->adapter->fileExists($this->pathNormalizer->normalizePath($location)); 39 | } 40 | 41 | public function directoryExists(string $location): bool 42 | { 43 | return $this->adapter->directoryExists($this->pathNormalizer->normalizePath($location)); 44 | } 45 | 46 | public function has(string $location): bool 47 | { 48 | $path = $this->pathNormalizer->normalizePath($location); 49 | 50 | return $this->adapter->fileExists($path) || $this->adapter->directoryExists($path); 51 | } 52 | 53 | public function write(string $location, string $contents, array $config = []): void 54 | { 55 | $this->adapter->write( 56 | $this->pathNormalizer->normalizePath($location), 57 | $contents, 58 | $this->config->extend($config) 59 | ); 60 | } 61 | 62 | public function writeStream(string $location, $contents, array $config = []): void 63 | { 64 | /* @var resource $contents */ 65 | $this->assertIsResource($contents); 66 | $this->rewindStream($contents); 67 | $this->adapter->writeStream( 68 | $this->pathNormalizer->normalizePath($location), 69 | $contents, 70 | $this->config->extend($config) 71 | ); 72 | } 73 | 74 | public function read(string $location): string 75 | { 76 | return $this->adapter->read($this->pathNormalizer->normalizePath($location)); 77 | } 78 | 79 | public function readStream(string $location) 80 | { 81 | return $this->adapter->readStream($this->pathNormalizer->normalizePath($location)); 82 | } 83 | 84 | public function delete(string $location): void 85 | { 86 | $this->adapter->delete($this->pathNormalizer->normalizePath($location)); 87 | } 88 | 89 | public function deleteDirectory(string $location): void 90 | { 91 | $this->adapter->deleteDirectory($this->pathNormalizer->normalizePath($location)); 92 | } 93 | 94 | public function createDirectory(string $location, array $config = []): void 95 | { 96 | $this->adapter->createDirectory( 97 | $this->pathNormalizer->normalizePath($location), 98 | $this->config->extend($config) 99 | ); 100 | } 101 | 102 | public function listContents(string $location, bool $deep = self::LIST_SHALLOW): DirectoryListing 103 | { 104 | $path = $this->pathNormalizer->normalizePath($location); 105 | $listing = $this->adapter->listContents($path, $deep); 106 | 107 | return new DirectoryListing($this->pipeListing($location, $deep, $listing)); 108 | } 109 | 110 | private function pipeListing(string $location, bool $deep, iterable $listing): Generator 111 | { 112 | try { 113 | foreach ($listing as $item) { 114 | yield $item; 115 | } 116 | } catch (Throwable $exception) { 117 | throw UnableToListContents::atLocation($location, $deep, $exception); 118 | } 119 | } 120 | 121 | public function move(string $source, string $destination, array $config = []): void 122 | { 123 | $config = $this->resolveConfigForMoveAndCopy($config); 124 | $from = $this->pathNormalizer->normalizePath($source); 125 | $to = $this->pathNormalizer->normalizePath($destination); 126 | 127 | if ($from === $to) { 128 | $resolutionStrategy = $config->get(Config::OPTION_MOVE_IDENTICAL_PATH, ResolveIdenticalPathConflict::TRY); 129 | 130 | if ($resolutionStrategy === ResolveIdenticalPathConflict::FAIL) { 131 | throw UnableToMoveFile::sourceAndDestinationAreTheSame($source, $destination); 132 | } elseif ($resolutionStrategy === ResolveIdenticalPathConflict::IGNORE) { 133 | return; 134 | } 135 | } 136 | 137 | $this->adapter->move($from, $to, $config); 138 | } 139 | 140 | public function copy(string $source, string $destination, array $config = []): void 141 | { 142 | $config = $this->resolveConfigForMoveAndCopy($config); 143 | $from = $this->pathNormalizer->normalizePath($source); 144 | $to = $this->pathNormalizer->normalizePath($destination); 145 | 146 | if ($from === $to) { 147 | $resolutionStrategy = $config->get(Config::OPTION_COPY_IDENTICAL_PATH, ResolveIdenticalPathConflict::TRY); 148 | 149 | if ($resolutionStrategy === ResolveIdenticalPathConflict::FAIL) { 150 | throw UnableToCopyFile::sourceAndDestinationAreTheSame($source, $destination); 151 | } elseif ($resolutionStrategy === ResolveIdenticalPathConflict::IGNORE) { 152 | return; 153 | } 154 | } 155 | 156 | $this->adapter->copy($from, $to, $config); 157 | } 158 | 159 | public function lastModified(string $path): int 160 | { 161 | return $this->adapter->lastModified($this->pathNormalizer->normalizePath($path))->lastModified(); 162 | } 163 | 164 | public function fileSize(string $path): int 165 | { 166 | return $this->adapter->fileSize($this->pathNormalizer->normalizePath($path))->fileSize(); 167 | } 168 | 169 | public function mimeType(string $path): string 170 | { 171 | return $this->adapter->mimeType($this->pathNormalizer->normalizePath($path))->mimeType(); 172 | } 173 | 174 | public function setVisibility(string $path, string $visibility): void 175 | { 176 | $this->adapter->setVisibility($this->pathNormalizer->normalizePath($path), $visibility); 177 | } 178 | 179 | public function visibility(string $path): string 180 | { 181 | return $this->adapter->visibility($this->pathNormalizer->normalizePath($path))->visibility(); 182 | } 183 | 184 | public function publicUrl(string $path, array $config = []): string 185 | { 186 | $this->publicUrlGenerator ??= $this->resolvePublicUrlGenerator() 187 | ?? throw UnableToGeneratePublicUrl::noGeneratorConfigured($path); 188 | $config = $this->config->extend($config); 189 | 190 | return $this->publicUrlGenerator->publicUrl( 191 | $this->pathNormalizer->normalizePath($path), 192 | $config, 193 | ); 194 | } 195 | 196 | public function temporaryUrl(string $path, DateTimeInterface $expiresAt, array $config = []): string 197 | { 198 | $generator = $this->temporaryUrlGenerator ?? $this->adapter; 199 | 200 | if ($generator instanceof TemporaryUrlGenerator) { 201 | return $generator->temporaryUrl( 202 | $this->pathNormalizer->normalizePath($path), 203 | $expiresAt, 204 | $this->config->extend($config) 205 | ); 206 | } 207 | 208 | throw UnableToGenerateTemporaryUrl::noGeneratorConfigured($path); 209 | } 210 | 211 | public function checksum(string $path, array $config = []): string 212 | { 213 | $config = $this->config->extend($config); 214 | 215 | if ( ! $this->adapter instanceof ChecksumProvider) { 216 | return $this->calculateChecksumFromStream($path, $config); 217 | } 218 | 219 | try { 220 | return $this->adapter->checksum( 221 | $this->pathNormalizer->normalizePath($path), 222 | $config, 223 | ); 224 | } catch (ChecksumAlgoIsNotSupported) { 225 | return $this->calculateChecksumFromStream( 226 | $this->pathNormalizer->normalizePath($path), 227 | $config, 228 | ); 229 | } 230 | } 231 | 232 | private function resolvePublicUrlGenerator(): ?PublicUrlGenerator 233 | { 234 | if ($publicUrl = $this->config->get('public_url')) { 235 | return match (true) { 236 | is_array($publicUrl) => new ShardedPrefixPublicUrlGenerator($publicUrl), 237 | default => new PrefixPublicUrlGenerator($publicUrl), 238 | }; 239 | } 240 | 241 | if ($this->adapter instanceof PublicUrlGenerator) { 242 | return $this->adapter; 243 | } 244 | 245 | return null; 246 | } 247 | 248 | /** 249 | * @param mixed $contents 250 | */ 251 | private function assertIsResource($contents): void 252 | { 253 | if (is_resource($contents) === false) { 254 | throw new InvalidStreamProvided( 255 | "Invalid stream provided, expected stream resource, received " . gettype($contents) 256 | ); 257 | } elseif ($type = get_resource_type($contents) !== 'stream') { 258 | throw new InvalidStreamProvided( 259 | "Invalid stream provided, expected stream resource, received resource of type " . $type 260 | ); 261 | } 262 | } 263 | 264 | /** 265 | * @param resource $resource 266 | */ 267 | private function rewindStream($resource): void 268 | { 269 | if (ftell($resource) !== 0 && stream_get_meta_data($resource)['seekable']) { 270 | rewind($resource); 271 | } 272 | } 273 | 274 | private function resolveConfigForMoveAndCopy(array $config): Config 275 | { 276 | $retainVisibility = $this->config->get(Config::OPTION_RETAIN_VISIBILITY, $config[Config::OPTION_RETAIN_VISIBILITY] ?? true); 277 | $fullConfig = $this->config->extend($config); 278 | 279 | /* 280 | * By default, we retain visibility. When we do not retain visibility, the visibility setting 281 | * from the default configuration is ignored. Only when it is set explicitly, we propagate the 282 | * setting. 283 | */ 284 | if ($retainVisibility && ! array_key_exists(Config::OPTION_VISIBILITY, $config)) { 285 | $fullConfig = $fullConfig->withoutSettings(Config::OPTION_VISIBILITY)->extend($config); 286 | } 287 | 288 | return $fullConfig; 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/FilesystemAdapter.php: -------------------------------------------------------------------------------- 1 | 99 | * 100 | * @throws FilesystemException 101 | */ 102 | public function listContents(string $path, bool $deep): iterable; 103 | 104 | /** 105 | * @throws UnableToMoveFile 106 | * @throws FilesystemException 107 | */ 108 | public function move(string $source, string $destination, Config $config): void; 109 | 110 | /** 111 | * @throws UnableToCopyFile 112 | * @throws FilesystemException 113 | */ 114 | public function copy(string $source, string $destination, Config $config): void; 115 | } 116 | -------------------------------------------------------------------------------- /src/FilesystemException.php: -------------------------------------------------------------------------------- 1 | 56 | * 57 | * @throws FilesystemException 58 | * @throws UnableToListContents 59 | */ 60 | public function listContents(string $location, bool $deep = self::LIST_SHALLOW): DirectoryListing; 61 | 62 | /** 63 | * @throws UnableToRetrieveMetadata 64 | * @throws FilesystemException 65 | */ 66 | public function lastModified(string $path): int; 67 | 68 | /** 69 | * @throws UnableToRetrieveMetadata 70 | * @throws FilesystemException 71 | */ 72 | public function fileSize(string $path): int; 73 | 74 | /** 75 | * @throws UnableToRetrieveMetadata 76 | * @throws FilesystemException 77 | */ 78 | public function mimeType(string $path): string; 79 | 80 | /** 81 | * @throws UnableToRetrieveMetadata 82 | * @throws FilesystemException 83 | */ 84 | public function visibility(string $path): string; 85 | } 86 | -------------------------------------------------------------------------------- /src/FilesystemWriter.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private $filesystems = []; 20 | 21 | /** 22 | * @var Config 23 | */ 24 | private $config; 25 | 26 | /** 27 | * MountManager constructor. 28 | * 29 | * @param array $filesystems 30 | */ 31 | public function __construct(array $filesystems = [], array $config = []) 32 | { 33 | $this->mountFilesystems($filesystems); 34 | $this->config = new Config($config); 35 | } 36 | 37 | /** 38 | * It is not recommended to mount filesystems after creation because interacting 39 | * with the Mount Manager becomes unpredictable. Use this as an escape hatch. 40 | */ 41 | public function dangerouslyMountFilesystems(string $key, FilesystemOperator $filesystem): void 42 | { 43 | $this->mountFilesystem($key, $filesystem); 44 | } 45 | 46 | /** 47 | * @param array $filesystems 48 | */ 49 | public function extend(array $filesystems, array $config = []): MountManager 50 | { 51 | $clone = clone $this; 52 | $clone->config = $this->config->extend($config); 53 | $clone->mountFilesystems($filesystems); 54 | 55 | return $clone; 56 | } 57 | 58 | public function fileExists(string $location): bool 59 | { 60 | /** @var FilesystemOperator $filesystem */ 61 | [$filesystem, $path] = $this->determineFilesystemAndPath($location); 62 | 63 | try { 64 | return $filesystem->fileExists($path); 65 | } catch (Throwable $exception) { 66 | throw UnableToCheckFileExistence::forLocation($location, $exception); 67 | } 68 | } 69 | 70 | public function has(string $location): bool 71 | { 72 | /** @var FilesystemOperator $filesystem */ 73 | [$filesystem, $path] = $this->determineFilesystemAndPath($location); 74 | 75 | try { 76 | return $filesystem->fileExists($path) || $filesystem->directoryExists($path); 77 | } catch (Throwable $exception) { 78 | throw UnableToCheckExistence::forLocation($location, $exception); 79 | } 80 | } 81 | 82 | public function directoryExists(string $location): bool 83 | { 84 | /** @var FilesystemOperator $filesystem */ 85 | [$filesystem, $path] = $this->determineFilesystemAndPath($location); 86 | 87 | try { 88 | return $filesystem->directoryExists($path); 89 | } catch (Throwable $exception) { 90 | throw UnableToCheckDirectoryExistence::forLocation($location, $exception); 91 | } 92 | } 93 | 94 | public function read(string $location): string 95 | { 96 | /** @var FilesystemOperator $filesystem */ 97 | [$filesystem, $path] = $this->determineFilesystemAndPath($location); 98 | 99 | try { 100 | return $filesystem->read($path); 101 | } catch (UnableToReadFile $exception) { 102 | throw UnableToReadFile::fromLocation($location, $exception->reason(), $exception); 103 | } 104 | } 105 | 106 | public function readStream(string $location) 107 | { 108 | /** @var FilesystemOperator $filesystem */ 109 | [$filesystem, $path] = $this->determineFilesystemAndPath($location); 110 | 111 | try { 112 | return $filesystem->readStream($path); 113 | } catch (UnableToReadFile $exception) { 114 | throw UnableToReadFile::fromLocation($location, $exception->reason(), $exception); 115 | } 116 | } 117 | 118 | public function listContents(string $location, bool $deep = self::LIST_SHALLOW): DirectoryListing 119 | { 120 | /** @var FilesystemOperator $filesystem */ 121 | [$filesystem, $path, $mountIdentifier] = $this->determineFilesystemAndPath($location); 122 | 123 | return 124 | $filesystem 125 | ->listContents($path, $deep) 126 | ->map( 127 | function (StorageAttributes $attributes) use ($mountIdentifier) { 128 | return $attributes->withPath(sprintf('%s://%s', $mountIdentifier, $attributes->path())); 129 | } 130 | ); 131 | } 132 | 133 | public function lastModified(string $location): int 134 | { 135 | /** @var FilesystemOperator $filesystem */ 136 | [$filesystem, $path] = $this->determineFilesystemAndPath($location); 137 | 138 | try { 139 | return $filesystem->lastModified($path); 140 | } catch (UnableToRetrieveMetadata $exception) { 141 | throw UnableToRetrieveMetadata::lastModified($location, $exception->reason(), $exception); 142 | } 143 | } 144 | 145 | public function fileSize(string $location): int 146 | { 147 | /** @var FilesystemOperator $filesystem */ 148 | [$filesystem, $path] = $this->determineFilesystemAndPath($location); 149 | 150 | try { 151 | return $filesystem->fileSize($path); 152 | } catch (UnableToRetrieveMetadata $exception) { 153 | throw UnableToRetrieveMetadata::fileSize($location, $exception->reason(), $exception); 154 | } 155 | } 156 | 157 | public function mimeType(string $location): string 158 | { 159 | /** @var FilesystemOperator $filesystem */ 160 | [$filesystem, $path] = $this->determineFilesystemAndPath($location); 161 | 162 | try { 163 | return $filesystem->mimeType($path); 164 | } catch (UnableToRetrieveMetadata $exception) { 165 | throw UnableToRetrieveMetadata::mimeType($location, $exception->reason(), $exception); 166 | } 167 | } 168 | 169 | public function visibility(string $path): string 170 | { 171 | /** @var FilesystemOperator $filesystem */ 172 | [$filesystem, $location] = $this->determineFilesystemAndPath($path); 173 | 174 | try { 175 | return $filesystem->visibility($location); 176 | } catch (UnableToRetrieveMetadata $exception) { 177 | throw UnableToRetrieveMetadata::visibility($path, $exception->reason(), $exception); 178 | } 179 | } 180 | 181 | public function write(string $location, string $contents, array $config = []): void 182 | { 183 | /** @var FilesystemOperator $filesystem */ 184 | [$filesystem, $path] = $this->determineFilesystemAndPath($location); 185 | 186 | try { 187 | $filesystem->write($path, $contents, $this->config->extend($config)->toArray()); 188 | } catch (UnableToWriteFile $exception) { 189 | throw UnableToWriteFile::atLocation($location, $exception->reason(), $exception); 190 | } 191 | } 192 | 193 | public function writeStream(string $location, $contents, array $config = []): void 194 | { 195 | /** @var FilesystemOperator $filesystem */ 196 | [$filesystem, $path] = $this->determineFilesystemAndPath($location); 197 | $filesystem->writeStream($path, $contents, $this->config->extend($config)->toArray()); 198 | } 199 | 200 | public function setVisibility(string $path, string $visibility): void 201 | { 202 | /** @var FilesystemOperator $filesystem */ 203 | [$filesystem, $path] = $this->determineFilesystemAndPath($path); 204 | $filesystem->setVisibility($path, $visibility); 205 | } 206 | 207 | public function delete(string $location): void 208 | { 209 | /** @var FilesystemOperator $filesystem */ 210 | [$filesystem, $path] = $this->determineFilesystemAndPath($location); 211 | 212 | try { 213 | $filesystem->delete($path); 214 | } catch (UnableToDeleteFile $exception) { 215 | throw UnableToDeleteFile::atLocation($location, $exception->reason(), $exception); 216 | } 217 | } 218 | 219 | public function deleteDirectory(string $location): void 220 | { 221 | /** @var FilesystemOperator $filesystem */ 222 | [$filesystem, $path] = $this->determineFilesystemAndPath($location); 223 | 224 | try { 225 | $filesystem->deleteDirectory($path); 226 | } catch (UnableToDeleteDirectory $exception) { 227 | throw UnableToDeleteDirectory::atLocation($location, $exception->reason(), $exception); 228 | } 229 | } 230 | 231 | public function createDirectory(string $location, array $config = []): void 232 | { 233 | /** @var FilesystemOperator $filesystem */ 234 | [$filesystem, $path] = $this->determineFilesystemAndPath($location); 235 | 236 | try { 237 | $filesystem->createDirectory($path, $this->config->extend($config)->toArray()); 238 | } catch (UnableToCreateDirectory $exception) { 239 | throw UnableToCreateDirectory::dueToFailure($location, $exception); 240 | } 241 | } 242 | 243 | public function move(string $source, string $destination, array $config = []): void 244 | { 245 | /** @var FilesystemOperator $sourceFilesystem */ 246 | /* @var FilesystemOperator $destinationFilesystem */ 247 | [$sourceFilesystem, $sourcePath] = $this->determineFilesystemAndPath($source); 248 | [$destinationFilesystem, $destinationPath] = $this->determineFilesystemAndPath($destination); 249 | 250 | $sourceFilesystem === $destinationFilesystem ? $this->moveInTheSameFilesystem( 251 | $sourceFilesystem, 252 | $sourcePath, 253 | $destinationPath, 254 | $source, 255 | $destination, 256 | $config, 257 | ) : $this->moveAcrossFilesystems($source, $destination, $config); 258 | } 259 | 260 | public function copy(string $source, string $destination, array $config = []): void 261 | { 262 | /** @var FilesystemOperator $sourceFilesystem */ 263 | /* @var FilesystemOperator $destinationFilesystem */ 264 | [$sourceFilesystem, $sourcePath] = $this->determineFilesystemAndPath($source); 265 | [$destinationFilesystem, $destinationPath] = $this->determineFilesystemAndPath($destination); 266 | 267 | $sourceFilesystem === $destinationFilesystem ? $this->copyInSameFilesystem( 268 | $sourceFilesystem, 269 | $sourcePath, 270 | $destinationPath, 271 | $source, 272 | $destination, 273 | $config, 274 | ) : $this->copyAcrossFilesystem( 275 | $sourceFilesystem, 276 | $sourcePath, 277 | $destinationFilesystem, 278 | $destinationPath, 279 | $source, 280 | $destination, 281 | $config, 282 | ); 283 | } 284 | 285 | public function publicUrl(string $path, array $config = []): string 286 | { 287 | /** @var FilesystemOperator $filesystem */ 288 | [$filesystem, $path] = $this->determineFilesystemAndPath($path); 289 | 290 | if ( ! method_exists($filesystem, 'publicUrl')) { 291 | throw new UnableToGeneratePublicUrl(sprintf('%s does not support generating public urls.', $filesystem::class), $path); 292 | } 293 | 294 | return $filesystem->publicUrl($path, $config); 295 | } 296 | 297 | public function temporaryUrl(string $path, DateTimeInterface $expiresAt, array $config = []): string 298 | { 299 | /** @var FilesystemOperator $filesystem */ 300 | [$filesystem, $path] = $this->determineFilesystemAndPath($path); 301 | 302 | if ( ! method_exists($filesystem, 'temporaryUrl')) { 303 | throw new UnableToGenerateTemporaryUrl(sprintf('%s does not support generating public urls.', $filesystem::class), $path); 304 | } 305 | 306 | return $filesystem->temporaryUrl($path, $expiresAt, $this->config->extend($config)->toArray()); 307 | } 308 | 309 | public function checksum(string $path, array $config = []): string 310 | { 311 | /** @var FilesystemOperator $filesystem */ 312 | [$filesystem, $path] = $this->determineFilesystemAndPath($path); 313 | 314 | if ( ! method_exists($filesystem, 'checksum')) { 315 | throw new UnableToProvideChecksum(sprintf('%s does not support providing checksums.', $filesystem::class), $path); 316 | } 317 | 318 | return $filesystem->checksum($path, $this->config->extend($config)->toArray()); 319 | } 320 | 321 | private function mountFilesystems(array $filesystems): void 322 | { 323 | foreach ($filesystems as $key => $filesystem) { 324 | $this->guardAgainstInvalidMount($key, $filesystem); 325 | /* @var string $key */ 326 | /* @var FilesystemOperator $filesystem */ 327 | $this->mountFilesystem($key, $filesystem); 328 | } 329 | } 330 | 331 | private function guardAgainstInvalidMount(mixed $key, mixed $filesystem): void 332 | { 333 | if ( ! is_string($key)) { 334 | throw UnableToMountFilesystem::becauseTheKeyIsNotValid($key); 335 | } 336 | 337 | if ( ! $filesystem instanceof FilesystemOperator) { 338 | throw UnableToMountFilesystem::becauseTheFilesystemWasNotValid($filesystem); 339 | } 340 | } 341 | 342 | private function mountFilesystem(string $key, FilesystemOperator $filesystem): void 343 | { 344 | $this->filesystems[$key] = $filesystem; 345 | } 346 | 347 | /** 348 | * @param string $path 349 | * 350 | * @return array{0:FilesystemOperator, 1:string, 2:string} 351 | */ 352 | private function determineFilesystemAndPath(string $path): array 353 | { 354 | if (strpos($path, '://') < 1) { 355 | throw UnableToResolveFilesystemMount::becauseTheSeparatorIsMissing($path); 356 | } 357 | 358 | /** @var string $mountIdentifier */ 359 | /** @var string $mountPath */ 360 | [$mountIdentifier, $mountPath] = explode('://', $path, 2); 361 | 362 | if ( ! array_key_exists($mountIdentifier, $this->filesystems)) { 363 | throw UnableToResolveFilesystemMount::becauseTheMountWasNotRegistered($mountIdentifier); 364 | } 365 | 366 | return [$this->filesystems[$mountIdentifier], $mountPath, $mountIdentifier]; 367 | } 368 | 369 | private function copyInSameFilesystem( 370 | FilesystemOperator $sourceFilesystem, 371 | string $sourcePath, 372 | string $destinationPath, 373 | string $source, 374 | string $destination, 375 | array $config, 376 | ): void { 377 | try { 378 | $sourceFilesystem->copy($sourcePath, $destinationPath, $this->config->extend($config)->toArray()); 379 | } catch (UnableToCopyFile $exception) { 380 | throw UnableToCopyFile::fromLocationTo($source, $destination, $exception); 381 | } 382 | } 383 | 384 | private function copyAcrossFilesystem( 385 | FilesystemOperator $sourceFilesystem, 386 | string $sourcePath, 387 | FilesystemOperator $destinationFilesystem, 388 | string $destinationPath, 389 | string $source, 390 | string $destination, 391 | array $config, 392 | ): void { 393 | $config = $this->config->extend($config); 394 | $retainVisibility = (bool) $config->get(Config::OPTION_RETAIN_VISIBILITY, true); 395 | $visibility = $config->get(Config::OPTION_VISIBILITY); 396 | 397 | try { 398 | if ($visibility == null && $retainVisibility) { 399 | $visibility = $sourceFilesystem->visibility($sourcePath); 400 | $config = $config->extend(compact('visibility')); 401 | } 402 | 403 | $stream = $sourceFilesystem->readStream($sourcePath); 404 | $destinationFilesystem->writeStream($destinationPath, $stream, $config->toArray()); 405 | } catch (UnableToRetrieveMetadata | UnableToReadFile | UnableToWriteFile $exception) { 406 | throw UnableToCopyFile::fromLocationTo($source, $destination, $exception); 407 | } 408 | } 409 | 410 | private function moveInTheSameFilesystem( 411 | FilesystemOperator $sourceFilesystem, 412 | string $sourcePath, 413 | string $destinationPath, 414 | string $source, 415 | string $destination, 416 | array $config, 417 | ): void { 418 | try { 419 | $sourceFilesystem->move($sourcePath, $destinationPath, $this->config->extend($config)->toArray()); 420 | } catch (UnableToMoveFile $exception) { 421 | throw UnableToMoveFile::fromLocationTo($source, $destination, $exception); 422 | } 423 | } 424 | 425 | private function moveAcrossFilesystems(string $source, string $destination, array $config = []): void 426 | { 427 | try { 428 | $this->copy($source, $destination, $config); 429 | $this->delete($source); 430 | } catch (UnableToCopyFile | UnableToDeleteFile $exception) { 431 | throw UnableToMoveFile::fromLocationTo($source, $destination, $exception); 432 | } 433 | } 434 | } 435 | -------------------------------------------------------------------------------- /src/PathNormalizer.php: -------------------------------------------------------------------------------- 1 | prefix = rtrim($prefix, '\\/'); 18 | 19 | if ($this->prefix !== '' || $prefix === $separator) { 20 | $this->prefix .= $separator; 21 | } 22 | } 23 | 24 | public function prefixPath(string $path): string 25 | { 26 | return $this->prefix . ltrim($path, '\\/'); 27 | } 28 | 29 | public function stripPrefix(string $path): string 30 | { 31 | /* @var string */ 32 | return substr($path, strlen($this->prefix)); 33 | } 34 | 35 | public function stripDirectoryPrefix(string $path): string 36 | { 37 | return rtrim($this->stripPrefix($path), '\\/'); 38 | } 39 | 40 | public function prefixDirectoryPath(string $path): string 41 | { 42 | $prefixedPath = $this->prefixPath(rtrim($path, '\\/')); 43 | 44 | if ($prefixedPath === '' || substr($prefixedPath, -1) === $this->separator) { 45 | return $prefixedPath; 46 | } 47 | 48 | return $prefixedPath . $this->separator; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/PathTraversalDetected.php: -------------------------------------------------------------------------------- 1 | path; 16 | } 17 | 18 | public static function forPath(string $path): PathTraversalDetected 19 | { 20 | $e = new PathTraversalDetected("Path traversal detected: {$path}"); 21 | $e->path = $path; 22 | 23 | return $e; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/PortableVisibilityGuard.php: -------------------------------------------------------------------------------- 1 | formatPropertyName((string) $offset); 27 | 28 | return isset($this->{$property}); 29 | } 30 | 31 | /** 32 | * @param mixed $offset 33 | * 34 | * @return mixed 35 | */ 36 | #[\ReturnTypeWillChange] 37 | public function offsetGet($offset) 38 | { 39 | $property = $this->formatPropertyName((string) $offset); 40 | 41 | return $this->{$property}; 42 | } 43 | 44 | /** 45 | * @param mixed $offset 46 | * @param mixed $value 47 | */ 48 | #[\ReturnTypeWillChange] 49 | public function offsetSet($offset, $value): void 50 | { 51 | throw new RuntimeException('Properties can not be manipulated'); 52 | } 53 | 54 | /** 55 | * @param mixed $offset 56 | */ 57 | #[\ReturnTypeWillChange] 58 | public function offsetUnset($offset): void 59 | { 60 | throw new RuntimeException('Properties can not be manipulated'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/ResolveIdenticalPathConflict.php: -------------------------------------------------------------------------------- 1 | location; 16 | } 17 | 18 | public static function atLocation(string $pathName): SymbolicLinkEncountered 19 | { 20 | $e = new static("Unsupported symbolic link encountered at location $pathName"); 21 | $e->location = $pathName; 22 | 23 | return $e; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/UnableToCheckDirectoryExistence.php: -------------------------------------------------------------------------------- 1 | source; 25 | } 26 | 27 | public function destination(): string 28 | { 29 | return $this->destination; 30 | } 31 | 32 | public static function fromLocationTo( 33 | string $sourcePath, 34 | string $destinationPath, 35 | ?Throwable $previous = null 36 | ): UnableToCopyFile { 37 | $e = new static("Unable to copy file from $sourcePath to $destinationPath", 0 , $previous); 38 | $e->source = $sourcePath; 39 | $e->destination = $destinationPath; 40 | 41 | return $e; 42 | } 43 | 44 | public static function sourceAndDestinationAreTheSame(string $source, string $destination): UnableToCopyFile 45 | { 46 | return UnableToCopyFile::because('Source and destination are the same', $source, $destination); 47 | } 48 | 49 | public static function because(string $reason, string $sourcePath, string $destinationPath): UnableToCopyFile 50 | { 51 | $e = new static("Unable to copy file from $sourcePath to $destinationPath, because $reason"); 52 | $e->source = $sourcePath; 53 | $e->destination = $destinationPath; 54 | 55 | return $e; 56 | } 57 | 58 | public function operation(): string 59 | { 60 | return FilesystemOperationFailed::OPERATION_COPY; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/UnableToCreateDirectory.php: -------------------------------------------------------------------------------- 1 | location = $dirname; 20 | $e->reason = $errorMessage; 21 | 22 | return $e; 23 | } 24 | 25 | public static function dueToFailure(string $dirname, Throwable $previous): UnableToCreateDirectory 26 | { 27 | $reason = $previous instanceof UnableToCreateDirectory ? $previous->reason() : ''; 28 | $message = "Unable to create a directory at $dirname. $reason"; 29 | $e = new static(rtrim($message), 0, $previous); 30 | $e->location = $dirname; 31 | $e->reason = $reason ?: $message; 32 | 33 | return $e; 34 | } 35 | 36 | public function operation(): string 37 | { 38 | return FilesystemOperationFailed::OPERATION_CREATE_DIRECTORY; 39 | } 40 | 41 | public function reason(): string 42 | { 43 | return $this->reason; 44 | } 45 | 46 | public function location(): string 47 | { 48 | return $this->location; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/UnableToDeleteDirectory.php: -------------------------------------------------------------------------------- 1 | location = $location; 29 | $e->reason = $reason; 30 | 31 | return $e; 32 | } 33 | 34 | public function operation(): string 35 | { 36 | return FilesystemOperationFailed::OPERATION_DELETE_DIRECTORY; 37 | } 38 | 39 | public function reason(): string 40 | { 41 | return $this->reason; 42 | } 43 | 44 | public function location(): string 45 | { 46 | return $this->location; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/UnableToDeleteFile.php: -------------------------------------------------------------------------------- 1 | location = $location; 26 | $e->reason = $reason; 27 | 28 | return $e; 29 | } 30 | 31 | public function operation(): string 32 | { 33 | return FilesystemOperationFailed::OPERATION_DELETE; 34 | } 35 | 36 | public function reason(): string 37 | { 38 | return $this->reason; 39 | } 40 | 41 | public function location(): string 42 | { 43 | return $this->location; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/UnableToGeneratePublicUrl.php: -------------------------------------------------------------------------------- 1 | getMessage(), $path, $exception); 20 | } 21 | 22 | public static function noGeneratorConfigured(string $path, string $extraReason = ''): static 23 | { 24 | return new static('No generator was configured ' . $extraReason, $path); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/UnableToGenerateTemporaryUrl.php: -------------------------------------------------------------------------------- 1 | getMessage(), $path, $exception); 20 | } 21 | 22 | public static function noGeneratorConfigured(string $path, string $extraReason = ''): static 23 | { 24 | return new static('No generator was configured ' . $extraReason, $path); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/UnableToListContents.php: -------------------------------------------------------------------------------- 1 | getMessage(); 16 | 17 | return new UnableToListContents($message, 0, $previous); 18 | } 19 | 20 | public function operation(): string 21 | { 22 | return self::OPERATION_LIST_CONTENTS; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/UnableToMountFilesystem.php: -------------------------------------------------------------------------------- 1 | source; 30 | } 31 | 32 | public function destination(): string 33 | { 34 | return $this->destination; 35 | } 36 | 37 | public static function fromLocationTo( 38 | string $sourcePath, 39 | string $destinationPath, 40 | ?Throwable $previous = null 41 | ): UnableToMoveFile { 42 | $message = $previous?->getMessage() ?? "Unable to move file from $sourcePath to $destinationPath"; 43 | $e = new static($message, 0, $previous); 44 | $e->source = $sourcePath; 45 | $e->destination = $destinationPath; 46 | 47 | return $e; 48 | } 49 | 50 | public static function because( 51 | string $reason, 52 | string $sourcePath, 53 | string $destinationPath, 54 | ): UnableToMoveFile { 55 | $message = "Unable to move file from $sourcePath to $destinationPath, because $reason"; 56 | $e = new static($message); 57 | $e->source = $sourcePath; 58 | $e->destination = $destinationPath; 59 | 60 | return $e; 61 | } 62 | 63 | public function operation(): string 64 | { 65 | return FilesystemOperationFailed::OPERATION_MOVE; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/UnableToProvideChecksum.php: -------------------------------------------------------------------------------- 1 | location = $location; 26 | $e->reason = $reason; 27 | 28 | return $e; 29 | } 30 | 31 | public function operation(): string 32 | { 33 | return FilesystemOperationFailed::OPERATION_READ; 34 | } 35 | 36 | public function reason(): string 37 | { 38 | return $this->reason; 39 | } 40 | 41 | public function location(): string 42 | { 43 | return $this->location; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/UnableToResolveFilesystemMount.php: -------------------------------------------------------------------------------- 1 | reason = $reason; 51 | $e->location = $location; 52 | $e->metadataType = $type; 53 | 54 | return $e; 55 | } 56 | 57 | public function reason(): string 58 | { 59 | return $this->reason; 60 | } 61 | 62 | public function location(): string 63 | { 64 | return $this->location; 65 | } 66 | 67 | public function metadataType(): string 68 | { 69 | return $this->metadataType; 70 | } 71 | 72 | public function operation(): string 73 | { 74 | return FilesystemOperationFailed::OPERATION_RETRIEVE_METADATA; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/UnableToSetVisibility.php: -------------------------------------------------------------------------------- 1 | reason; 28 | } 29 | 30 | public static function atLocation(string $filename, string $extraMessage = '', ?Throwable $previous = null): self 31 | { 32 | $message = "Unable to set visibility for file {$filename}. $extraMessage"; 33 | $e = new static(rtrim($message), 0, $previous); 34 | $e->reason = $extraMessage; 35 | $e->location = $filename; 36 | 37 | return $e; 38 | } 39 | 40 | public function operation(): string 41 | { 42 | return FilesystemOperationFailed::OPERATION_SET_VISIBILITY; 43 | } 44 | 45 | public function location(): string 46 | { 47 | return $this->location; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/UnableToWriteFile.php: -------------------------------------------------------------------------------- 1 | location = $location; 26 | $e->reason = $reason; 27 | 28 | return $e; 29 | } 30 | 31 | public function operation(): string 32 | { 33 | return FilesystemOperationFailed::OPERATION_WRITE; 34 | } 35 | 36 | public function reason(): string 37 | { 38 | return $this->reason; 39 | } 40 | 41 | public function location(): string 42 | { 43 | return $this->location; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/UnixVisibility/PortableVisibilityConverter.php: -------------------------------------------------------------------------------- 1 | filePublic 27 | : $this->filePrivate; 28 | } 29 | 30 | public function forDirectory(string $visibility): int 31 | { 32 | PortableVisibilityGuard::guardAgainstInvalidInput($visibility); 33 | 34 | return $visibility === Visibility::PUBLIC 35 | ? $this->directoryPublic 36 | : $this->directoryPrivate; 37 | } 38 | 39 | public function inverseForFile(int $visibility): string 40 | { 41 | if ($visibility === $this->filePublic) { 42 | return Visibility::PUBLIC; 43 | } elseif ($visibility === $this->filePrivate) { 44 | return Visibility::PRIVATE; 45 | } 46 | 47 | return Visibility::PUBLIC; // default 48 | } 49 | 50 | public function inverseForDirectory(int $visibility): string 51 | { 52 | if ($visibility === $this->directoryPublic) { 53 | return Visibility::PUBLIC; 54 | } elseif ($visibility === $this->directoryPrivate) { 55 | return Visibility::PRIVATE; 56 | } 57 | 58 | return Visibility::PUBLIC; // default 59 | } 60 | 61 | public function defaultForDirectories(): int 62 | { 63 | return $this->defaultForDirectories === Visibility::PUBLIC ? $this->directoryPublic : $this->directoryPrivate; 64 | } 65 | 66 | /** 67 | * @param array $permissionMap 68 | */ 69 | public static function fromArray(array $permissionMap, string $defaultForDirectories = Visibility::PRIVATE): PortableVisibilityConverter 70 | { 71 | return new PortableVisibilityConverter( 72 | $permissionMap['file']['public'] ?? 0644, 73 | $permissionMap['file']['private'] ?? 0600, 74 | $permissionMap['dir']['public'] ?? 0755, 75 | $permissionMap['dir']['private'] ?? 0700, 76 | $defaultForDirectories 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/UnixVisibility/VisibilityConverter.php: -------------------------------------------------------------------------------- 1 | location; 19 | } 20 | 21 | public static function atLocation(string $location): UnreadableFileEncountered 22 | { 23 | $e = new static("Unreadable file encountered at location {$location}."); 24 | $e->location = $location; 25 | 26 | return $e; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/UrlGeneration/ChainedPublicUrlGenerator.php: -------------------------------------------------------------------------------- 1 | generators as $generator) { 22 | try { 23 | return $generator->publicUrl($path, $config); 24 | } catch (UnableToGeneratePublicUrl) { 25 | } 26 | } 27 | 28 | throw new UnableToGeneratePublicUrl('No supported public url generator found.', $path); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/UrlGeneration/PrefixPublicUrlGenerator.php: -------------------------------------------------------------------------------- 1 | prefixer = new PathPrefixer($urlPrefix, '/'); 17 | } 18 | 19 | public function publicUrl(string $path, Config $config): string 20 | { 21 | return $this->prefixer->prefixPath($path); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/UrlGeneration/PublicUrlGenerator.php: -------------------------------------------------------------------------------- 1 | count = count($prefixes); 25 | 26 | if ($this->count === 0) { 27 | throw new InvalidArgumentException('At least one prefix is required.'); 28 | } 29 | 30 | $this->prefixes = array_map(static fn (string $prefix) => new PathPrefixer($prefix, '/'), $prefixes); 31 | } 32 | 33 | public function publicUrl(string $path, Config $config): string 34 | { 35 | $index = abs(crc32($path)) % $this->count; 36 | 37 | return $this->prefixes[$index]->prefixPath($path); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/UrlGeneration/TemporaryUrlGenerator.php: -------------------------------------------------------------------------------- 1 | rejectFunkyWhiteSpace($path); 13 | 14 | return $this->normalizeRelativePath($path); 15 | } 16 | 17 | private function rejectFunkyWhiteSpace(string $path): void 18 | { 19 | if (preg_match('#\p{C}+#u', $path)) { 20 | throw CorruptedPathDetected::forPath($path); 21 | } 22 | } 23 | 24 | private function normalizeRelativePath(string $path): string 25 | { 26 | $parts = []; 27 | 28 | foreach (explode('/', $path) as $part) { 29 | switch ($part) { 30 | case '': 31 | case '.': 32 | break; 33 | 34 | case '..': 35 | if (empty($parts)) { 36 | throw PathTraversalDetected::forPath($path); 37 | } 38 | array_pop($parts); 39 | break; 40 | 41 | default: 42 | $parts[] = $part; 43 | break; 44 | } 45 | } 46 | 47 | return implode('/', $parts); 48 | } 49 | } 50 | --------------------------------------------------------------------------------