From 8cf30040dbd17870a2a63e44e3e3ecd8d2fa1c0f Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Fri, 1 Mar 2024 12:01:43 -0500 Subject: [PATCH] [1.x] Upgrade to Flysystem 3 (#442) * Starting the process to update flysystem to 3 * Wrapping up flysystem support for filesystem adapter * Wrapping up flysystem upgrade * Wrapping up flysystem migration --- composer.json | 5 +- .../adapter/class-aws-s3-adapter.php | 156 +++++++ .../adapter/class-local-adapter.php | 33 ++ .../class-file-not-found-exception.php | 15 + .../filesystem/class-filesystem-adapter.php | 432 ++++++++++-------- .../filesystem/class-filesystem-manager.php | 182 ++++---- .../class-filesystem-service-provider.php | 14 +- src/mantle/filesystem/class-filesystem.php | 15 +- src/mantle/http/class-uploaded-file.php | 2 +- tests/Filesystem/FilesystemAdapterTest.php | 168 +++---- tests/Filesystem/FilesystemManagerTest.php | 70 ++- tests/Filesystem/FilesystemTest.php | 6 +- 12 files changed, 682 insertions(+), 416 deletions(-) create mode 100644 src/mantle/filesystem/adapter/class-aws-s3-adapter.php create mode 100644 src/mantle/filesystem/adapter/class-local-adapter.php create mode 100644 src/mantle/filesystem/class-file-not-found-exception.php diff --git a/composer.json b/composer.json index 42b55820..74159147 100644 --- a/composer.json +++ b/composer.json @@ -23,8 +23,7 @@ "illuminate/view": "^9.52.15", "laravel/serializable-closure": "^1.3.1", "league/commonmark": "^2.4.0", - "league/flysystem": "^1.1.10", - "league/flysystem-cached-adapter": "^1.1", + "league/flysystem": "^3.15", "monolog/monolog": "^2.9.1", "nesbot/carbon": "^2.68.1", "nette/php-generator": "^4.1", @@ -51,7 +50,7 @@ "alleyinteractive/alley-coding-standards": "^1.0.1", "alleyinteractive/wp-match-blocks": "^1.0 || ^2.0 || ^3.0", "guzzlehttp/guzzle": "^7.7", - "league/flysystem-aws-s3-v3": "^1.0", + "league/flysystem-aws-s3-v3": "^3.15", "mockery/mockery": "^1.6.6", "php-stubs/wp-cli-stubs": "^2.8", "phpstan/phpdoc-parser": "^1.23.1", diff --git a/src/mantle/filesystem/adapter/class-aws-s3-adapter.php b/src/mantle/filesystem/adapter/class-aws-s3-adapter.php new file mode 100644 index 00000000..7a9a71fe --- /dev/null +++ b/src/mantle/filesystem/adapter/class-aws-s3-adapter.php @@ -0,0 +1,156 @@ +client = $client; + } + + /** + * Get the URL for the file at the given path. + * + * @param string $path + * @return string + */ + public function url( string $path ): string { + // If an explicit base URL has been set on the disk configuration then we will use + // it as the base URL instead of the default path. This allows the developer to + // have full control over the base path for this filesystem's generated URLs. + if ( isset( $this->config['url'] ) ) { + return $this->concat_path_to_url( $this->config['url'], $this->prefixer->prefixPath( $path ) ); + } + + return $this->client->getObjectUrl( + $this->config['bucket'], + $this->prefixer->prefixPath( $path ) + ); + } + + /** + * Determine if temporary URLs can be generated. + * + * @return bool + */ + public function provides_temporary_urls(): bool { + return true; + } + + /** + * Get a temporary URL for the file at the given path. + * + * @param string $path + * @param \DateTimeInterface $expiration + * @param array $options + * @return string + */ + public function temporary_url( string $path, $expiration, array $options = [] ): string { + $command = $this->client->getCommand( + 'GetObject', + array_merge( + [ + 'Bucket' => $this->config['bucket'], + 'Key' => $this->prefixer->prefixPath( $path ), + ], + $options + ) + ); + + $uri = $this->client->createPresignedRequest( + $command, + $expiration, + $options + )->getUri(); + + // If an explicit base URL has been set on the disk configuration then we will use + // it as the base URL instead of the default path. This allows the developer to + // have full control over the base path for this filesystem's generated URLs. + if ( isset( $this->config['temporary_url'] ) ) { + $uri = $this->replace_base_url( $uri, $this->config['temporary_url'] ); + } + + return (string) $uri; + } + + /** + * Get a temporary upload URL for the file at the given path. + * + * @param string $path + * @param \DateTimeInterface $expiration + * @param array $options + * @return array + */ + public function temporary_upload_url( string $path, $expiration, array $options = [] ): array { + $command = $this->client->getCommand( + 'PutObject', + array_merge( + [ + 'Bucket' => $this->config['bucket'], + 'Key' => $this->prefixer->prefixPath( $path ), + ], + $options + ) + ); + + $signed_request = $this->client->createPresignedRequest( + $command, + $expiration, + $options + ); + + $uri = $signed_request->getUri(); + + // If an explicit base URL has been set on the disk configuration then we will use + // it as the base URL instead of the default path. This allows the developer to + // have full control over the base path for this filesystem's generated URLs. + if ( isset( $this->config['temporary_url'] ) ) { + $uri = $this->replace_base_url( $uri, $this->config['temporary_url'] ); + } + + return [ + 'url' => (string) $uri, + 'headers' => $signed_request->getHeaders(), + ]; + } + + /** + * Get the underlying S3 client. + * + * @return \Aws\S3\S3Client + */ + public function get_client(): S3Client { + return $this->client; + } +} diff --git a/src/mantle/filesystem/adapter/class-local-adapter.php b/src/mantle/filesystem/adapter/class-local-adapter.php new file mode 100644 index 00000000..d0fdfba8 --- /dev/null +++ b/src/mantle/filesystem/adapter/class-local-adapter.php @@ -0,0 +1,33 @@ +config['url'] ) ) { + return $this->concat_path_to_url( $this->config['url'], $path ); + } + + return rtrim( wp_upload_dir()['baseurl'], '/' ) . '/' . ltrim( $path, '/' ); + } +} diff --git a/src/mantle/filesystem/class-file-not-found-exception.php b/src/mantle/filesystem/class-file-not-found-exception.php new file mode 100644 index 00000000..24f037aa --- /dev/null +++ b/src/mantle/filesystem/class-file-not-found-exception.php @@ -0,0 +1,15 @@ +driver = $driver; + public function __construct( + protected FilesystemOperator $driver, + protected FilesystemAdapter $adapter, + protected array $config = [], + ) { + $separator = $config['directory_separator'] ?? DIRECTORY_SEPARATOR; + + $this->prefixer = new PathPrefixer( $this->config['root'] ?? '', $separator ); + + if ( isset( $config['prefix'] ) ) { + $this->prefixer = new PathPrefixer( $this->prefixer->prefixPath( $config['prefix'] ), $separator ); + } } /** @@ -101,12 +122,17 @@ public function all_directories( string $directory = null ): array { * * @param string $directory Directory name. * @param bool $recursive Flag if it should be recursive. - * @return array + * @return array */ public function directories( string $directory = null, bool $recursive = false ): array { - $contents = $this->driver->listContents( $directory, $recursive ); - - return $this->filter_contents_by_type( $contents, 'dir' ); + return $this->driver->listContents( $directory, $recursive ) + ->filter( + fn ( StorageAttributes $attributes ) => $attributes->isDir() + ) + ->map( + fn ( StorageAttributes $attributes ) => $attributes->path() + ) + ->toArray(); } /** @@ -116,7 +142,15 @@ public function directories( string $directory = null, bool $recursive = false ) * @return bool */ public function make_directory( string $path ): bool { - return $this->driver->createDir( $path ); + try { + $this->driver->createDirectory( $path ); + } catch ( UnableToCreateDirectory $e ) { + throw_if( $this->throws_exceptions(), $e ); + + return false; + } + + return true; } /** @@ -126,7 +160,15 @@ public function make_directory( string $path ): bool { * @return bool */ public function delete_directory( string $directory ): bool { - return $this->driver->deleteDir( $directory ); + try { + $this->driver->deleteDirectory( $directory ); + } catch ( UnableToDeleteDirectory $e ) { + throw_if( $this->throws_exceptions(), $e ); + + return false; + } + + return true; } /** @@ -147,8 +189,15 @@ public function all_files( string $directory = null ): array { * @return string[] */ public function files( string $directory = null, bool $recursive = false ): array { - $contents = $this->driver->listContents( $directory, $recursive ); - return $this->filter_contents_by_type( $contents, 'file' ); + return $this->driver->listContents( $directory, $recursive ) + ->filter( + fn ( StorageAttributes $attributes ) => $attributes->isFile() + ) + ->sortByPath() + ->map( + fn ( StorageAttributes $attributes ) => $attributes->path() + ) + ->toArray(); } /** @@ -159,7 +208,15 @@ public function files( string $directory = null, bool $recursive = false ): arra * @return bool */ public function copy( string $from, string $to ): bool { - return $this->driver->copy( $from, $to ); + try { + $this->driver->copy( $from, $to ); + } catch ( UnableToCopyFile $e ) { + throw_if( $this->throws_exceptions(), $e ); + + return false; + } + + return true; } /** @@ -170,7 +227,15 @@ public function copy( string $from, string $to ): bool { * @return bool */ public function move( string $from, string $to ): bool { - return $this->driver->rename( $from, $to ); + try { + $this->driver->move( $from, $to ); + } catch ( UnableToMoveFile $e ) { + throw_if( $this->throws_exceptions(), $e ); + + return false; + } + + return true; } /** @@ -185,10 +250,10 @@ public function delete( $paths ): bool { foreach ( $paths as $path ) { try { - if ( ! $this->driver->delete( $path ) ) { - $success = false; - } - } catch ( FileNotFoundException $e ) { + $this->driver->delete( $path ); + } catch ( UnableToDeleteFile $e ) { + throw_if( $this->throws_exceptions(), $e ); + $success = false; } } @@ -223,27 +288,23 @@ public function missing( string $path ): bool { * @return string */ public function path( string $path ): string { - $adapter = $this->driver->getAdapter(); - - if ( $adapter instanceof CachedAdapter ) { - $adapter = $adapter->getAdapter(); - } - - if ( method_exists( $adapter, 'getPathPrefix' ) ) { - return $adapter->getPathPrefix() . $path; - } - - return $path; + return $this->prefixer->prefixPath( $path ); } /** * Get the contents of a file. * * @param string $path File path. - * @return string|bool + * @return string|null */ public function get( string $path ) { - return $this->driver->read( $path ); + try { + return $this->driver->read( $path ); + } catch ( UnableToReadFile $e ) { + throw_if( $this->throws_exceptions(), $e ); + } + + return null; } /** @@ -257,27 +318,34 @@ public function get( string $path ) { */ public function response( string $path, ?string $name = null, array $headers = [], string $disposition = 'inline' ): StreamedResponse { $response = new StreamedResponse(); - $filename = $name ?? basename( $path ); - $disposition = $response->headers->makeDisposition( - $disposition, - $filename, - $this->fallback_name( $filename ) - ); + if ( ! array_key_exists( 'Content-Type', $headers ) ) { + $headers['Content-Type'] = $this->mimeType( $path ); + } - $response->headers->replace( - $headers + [ - 'Content-Disposition' => $disposition, - 'Content-Length' => $this->size( $path ), - 'Content-Type' => $this->mime_type( $path ), - ] - ); + if ( ! array_key_exists( 'Content-Length', $headers ) ) { + $headers['Content-Length'] = $this->size( $path ); + } + + if ( ! array_key_exists( 'Content-Disposition', $headers ) ) { + $filename = $name ?? basename( $path ); + + $disposition = $response->headers->makeDisposition( + $disposition, + $filename, + $this->fallback_name( $filename ) + ); + + $headers['Content-Disposition'] = $disposition; + } + + $response->headers->replace( $headers ); $response->setCallback( function () use ( $path ) { $stream = $this->readStream( $path ); fpassthru( $stream ); - fclose( $stream ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose + fclose( $stream ); } ); @@ -313,7 +381,7 @@ protected function fallback_name( string $name ): string { * @return int|bool */ public function last_modified( string $path ) { - return $this->driver->getTimestamp( $path ); + return $this->driver->lastModified( $path ); } /** @@ -323,7 +391,7 @@ public function last_modified( string $path ) { * @return string|false */ public function mime_type( string $path ) { - return $this->driver->getMimetype( $path ); + return $this->driver->mimeType( $path ); } /** @@ -343,16 +411,20 @@ public function put( string $path, $contents, $options = [] ): bool { $contents instanceof File || $contents instanceof Uploaded_File ) { - return $this->put_file( $path, $contents, $options ); + return $this->put_file( $path, $contents, $options ) ? true : false; } if ( $contents instanceof StreamInterface ) { - return $this->driver->putStream( $path, $contents->detach(), $options ); + $this->driver->writeStream( $path, $contents->detach(), $options ); + + return true; } - return is_resource( $contents ) - ? $this->driver->putStream( $path, $contents, $options ) - : $this->driver->put( $path, $contents, $options ); + is_resource( $contents ) + ? $this->driver->writeStream( $path, $contents, $options ) + : $this->driver->write( $path, $contents, $options ); + + return true; } /** @@ -363,7 +435,7 @@ public function put( string $path, $contents, $options = [] ): bool { * @param mixed $options Options. * @return string|false */ - public function put_file( string $path, $file, $options = [] ) { + public function put_file( string $path, $file, $options = [] ): string|bool { $file = is_string( $file ) ? new File( $file ) : $file; return $this->put_file_as( $path, $file, $file->hash_name(), $options ); @@ -378,7 +450,7 @@ public function put_file( string $path, $file, $options = [] ) { * @param mixed $options Options. * @return string|false */ - public function put_file_as( string $path, $file, string $name, $options = [] ) { + public function put_file_as( string $path, $file, string $name, $options = [] ): string|bool { $stream = fopen( is_string( $file ) ? $file : $file->getRealPath(), 'r' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen $path = trim( $path . '/' . $name, '/' ); @@ -405,7 +477,20 @@ public function put_file_as( string $path, $file, string $name, $options = [] ) * @return int|bool */ public function size( string $path ) { - return $this->driver->getSize( $path ); + return $this->driver->fileSize( $path ); + } + + /** + * {@inheritdoc} + */ + public function readStream( $path ) { + try { + return $this->driver->readStream( $path ); + } catch ( UnableToReadFile $e ) { + throw_if( $this->throws_exceptions(), $e ); + + return false; + } } /** @@ -415,7 +500,26 @@ public function size( string $path ) { * @return resource|false The path resource or false on failure. */ public function read_stream( string $path ) { - return $this->driver->readStream( $path ); + return $this->readStream( $path ); + } + + /** + * {@inheritdoc} + */ + public function writeStream( string $path, $resource, $options = [] ): bool { + $options = is_string( $options ) + ? [ 'visibility' => $options ] + : (array) $options; + + try { + $this->driver->writeStream( $path, $resource, $options ); + } catch ( UnableToWriteFile | UnableToSetVisibility $e ) { + throw_if( $this->throws_exceptions(), $e ); + + return false; + } + + return true; } /** @@ -427,11 +531,7 @@ public function read_stream( string $path ) { * @return bool */ public function write_stream( string $path, $resource, $options = [] ): bool { - $options = is_string( $options ) - ? [ 'visibility' => $options ] - : (array) $options; - - return $this->driver->writeStream( $path, $resource, $options ); + return $this->writeStream( $path, $resource, $options ); } /** @@ -441,22 +541,39 @@ public function write_stream( string $path, $resource, $options = [] ): bool { * @return string */ public function get_visibility( string $path ): string { - if ( $this->driver->getVisibility( $path ) === AdapterInterface::VISIBILITY_PUBLIC ) { - return Filesystem::VISIBILITY_PUBLIC; + return $this->driver->visibility( $path ) === Visibility::PUBLIC + ? Filesystem::VISIBILITY_PUBLIC + : Filesystem::VISIBILITY_PRIVATE; + } + + /** + * Set the visibility for a file. + * + * @param string $path Path to set. + * @param string $visibility Visibility to set. + * @return bool + */ + public function setVisibility( string $path, string $visibility ): bool { + try { + $this->driver->setVisibility( $path, $this->parse_visibility( $visibility ) ); + } catch ( UnableToSetVisibility $e ) { + throw_if( $this->throws_exceptions(), $e ); + + return false; } - return Filesystem::VISIBILITY_PRIVATE; + return true; } /** - * Set the visibility for a file. + * Set the visibility for a file (alias). * * @param string $path Path to set. * @param string $visibility Visibility to set. * @return bool */ public function set_visibility( string $path, string $visibility ): bool { - return $this->driver->setVisibility( $path, $this->parse_visibility( $visibility ) ); + return $this->setVisibility( $path, $visibility ); } /** @@ -500,81 +617,32 @@ public function append( $path, $data, $separator = PHP_EOL ) { * @throws RuntimeException Thrown on invalid filesystem adapter. */ public function url( string $path ): ?string { - $adapter = $this->driver->getAdapter(); - - if ( $adapter instanceof CachedAdapter ) { - $adapter = $adapter->getAdapter(); + if ( isset( $this->config['prefix'] ) ) { + $path = $this->concat_path_to_url( $this->config['prefix'], $path ); } + $adapter = $this->adapter; + if ( method_exists( $adapter, 'getUrl' ) ) { return $adapter->getUrl( $path ); + } elseif ( method_exists( $adapter, 'get_url' ) ) { + return $adapter->get_url( $path ); } elseif ( method_exists( $this->driver, 'getUrl' ) ) { return $this->driver->getUrl( $path ); - } elseif ( $adapter instanceof AwsS3Adapter ) { - return $this->get_aws_url( $adapter, $path ); - } elseif ( $adapter instanceof Ftp ) { - return $this->get_ftp_url( $path ); - } elseif ( $adapter instanceof Local ) { - return $this->get_local_url( $path ); + } elseif ( method_exists( $this->driver, 'get_url' ) ) { + return $this->driver->get_url( $path ); } else { throw new RuntimeException( 'This driver does not support retrieving URLs.' ); } } /** - * Get the URL for the file at the given path. - * - * @param \League\Flysystem\AwsS3v3\AwsS3Adapter $adapter Filesystem adapter. - * @param string $path File path. - * @return string - */ - protected function get_aws_url( AwsS3Adapter $adapter, string $path ): string { - // If an explicit base URL has been set on the disk configuration then we will use - // it as the base URL instead of the default path. This allows the developer to - // have full control over the base path for this filesystem's generated - // URLs. - $url = $this->driver->getConfig()->get( 'url' ); - if ( ! is_null( $url ) ) { - return $this->concatPathToUrl( $url, $adapter->getPathPrefix() . $path ); - } - - return $adapter->getClient()->getObjectUrl( - $adapter->getBucket(), - $adapter->getPathPrefix() . $path - ); - } - - /** - * Get the URL for the file at the given path. - * - * @param string $path File path. - * @return string - */ - protected function get_ftp_url( $path ) { - $config = $this->driver->getConfig(); - - return $config->has( 'url' ) - ? $this->concatPathToUrl( $config->get( 'url' ), $path ) - : $path; - } - - /** - * Get the URL for the file at the given path. + * Determine if temporary URLs can be generated. * - * @param string $path File path. - * @return string + * @return bool */ - protected function get_local_url( $path ) { - $config = $this->driver->getConfig(); - - // If an explicit base URL has been set on the disk configuration then we will use - // it as the base URL instead of the default path. This allows the developer to - // have full control over the base path for this filesystem's generated URLs. - if ( $config->has( 'url' ) ) { - return $this->concatPathToUrl( $config->get( 'url' ), $path ); - } - - return wp_upload_dir()['baseurl'] . $path; + public function provides_temporary_urls(): bool { + return method_exists( $this->adapter, 'getTemporaryUrl' ) || method_exists( $this->adapter, 'get_temporary_url' ); } /** @@ -588,59 +656,40 @@ protected function get_local_url( $path ) { * @throws RuntimeException Thrown on missing temporary URL. */ public function temporary_url( string $path, $expiration, array $options = [] ): string { - $adapter = $this->driver->getAdapter(); - - if ( $adapter instanceof CachedAdapter ) { - $adapter = $adapter->getAdapter(); + if ( method_exists( $this->adapter, 'getTemporaryUrl' ) ) { + return $this->adapter->getTemporaryUrl( $path, $expiration, $options ); + } elseif ( method_exists( $this->adapter, 'get_temporary_url' ) ) { + return $this->adapter->get_temporary_url( $path, $expiration, $options ); } - if ( method_exists( $adapter, 'getTemporaryUrl' ) ) { - return $adapter->getTemporaryUrl( $path, $expiration, $options ); - } elseif ( $adapter instanceof AwsS3Adapter ) { - return $this->getAwsTemporaryUrl( $adapter, $path, $expiration, $options ); - } else { - throw new RuntimeException( 'This driver does not support creating temporary URLs.' ); - } + throw new RuntimeException( 'This driver does not support creating temporary URLs.' ); } /** - * Get a temporary URL for the file at the given path. + * Concatenate a path to a URL. * - * @param \League\Flysystem\AwsS3v3\AwsS3Adapter $adapter - * @param string $path - * @param \DateTimeInterface $expiration - * @param array $options + * @param string $url + * @param string $path * @return string */ - public function getAwsTemporaryUrl( $adapter, $path, $expiration, $options ) { - $client = $adapter->getClient(); - - $command = $client->getCommand( - 'GetObject', - array_merge( - [ - 'Bucket' => $adapter->getBucket(), - 'Key' => $adapter->getPathPrefix() . $path, - ], - $options - ) - ); - - return (string) $client->createPresignedRequest( - $command, - $expiration - )->getUri(); + protected function concat_path_to_url( string $url, string $path ): string { + return rtrim( $url, '/' ) . '/' . ltrim( $path, '/' ); } /** - * Concatenate a path to a URL. + * Replace the scheme, host and port of the given UriInterface with values from the given URL. * - * @param string $url - * @param string $path - * @return string + * @param \Psr\Http\Message\UriInterface $uri + * @param string $url + * @return \Psr\Http\Message\UriInterface */ - protected function concatPathToUrl( $url, $path ) { - return rtrim( $url, '/' ) . '/' . ltrim( $path, '/' ); + protected function replace_base_url( UriInterface $uri, string $url ): UriInterface { + $parsed = wp_parse_url( $url ); + + return $uri + ->withScheme( $parsed['scheme'] ) + ->withHost( $parsed['host'] ) + ->withPort( $parsed['port'] ?? null ); } /** @@ -652,29 +701,20 @@ protected function concatPathToUrl( $url, $path ) { * @throws InvalidArgumentException Thrown on invalid visibility. */ protected function parse_visibility( string $visibility ): string { - switch ( $visibility ) { - case Filesystem::VISIBILITY_PUBLIC: - return AdapterInterface::VISIBILITY_PUBLIC; - case Filesystem::VISIBILITY_PRIVATE: - return AdapterInterface::VISIBILITY_PRIVATE; - } - - throw new InvalidArgumentException( "Unknown visibility: {$visibility}." ); + return match ( $visibility ) { + Filesystem::VISIBILITY_PUBLIC => Visibility::PUBLIC, + Filesystem::VISIBILITY_PRIVATE => Visibility::PRIVATE, + default => throw new InvalidArgumentException( "Unknown visibility: {$visibility}." ), + }; } /** - * Filter directory contents by type. + * Determine if Flysystem exceptions should be thrown. * - * @param array $contents Content sto filter. - * @param string $type - * @return array + * @return bool */ - protected function filter_contents_by_type( array $contents, string $type ): array { - return collect( $contents ) - ->where( 'type', $type ) - ->pluck( 'path' ) - ->values() - ->all(); + protected function throws_exceptions(): bool { + return (bool) ( $this->config['throw'] ?? false ); } /** diff --git a/src/mantle/filesystem/class-filesystem-manager.php b/src/mantle/filesystem/class-filesystem-manager.php index f3bc545a..147a7da2 100644 --- a/src/mantle/filesystem/class-filesystem-manager.php +++ b/src/mantle/filesystem/class-filesystem-manager.php @@ -10,14 +10,13 @@ use Aws\S3\S3Client; use Closure; use InvalidArgumentException; -use League\Flysystem\Adapter\Local; -use League\Flysystem\AdapterInterface; -use League\Flysystem\AwsS3v3\AwsS3Adapter; -use League\Flysystem\Cached\CachedAdapter; -use League\Flysystem\Cached\Storage\AbstractCache; -use League\Flysystem\Cached\Storage\Memory as MemoryStore; +use League\Flysystem\AwsS3V3\AwsS3V3Adapter as S3Adapter; +use League\Flysystem\AwsS3V3\PortableVisibilityConverter as AwsS3PortableVisibilityConverter; use League\Flysystem\Filesystem as Flysystem; -use League\Flysystem\FilesystemInterface; +use League\Flysystem\FilesystemAdapter; +use League\Flysystem\Local\LocalFilesystemAdapter as LocalAdapter; +use League\Flysystem\UnixVisibility\PortableVisibilityConverter; +use League\Flysystem\Visibility; use Mantle\Contracts\Application; use Mantle\Contracts\Filesystem\Filesystem_Manager as Filesystem_Manager_Contract; use Mantle\Contracts\Filesystem\Filesystem; @@ -30,13 +29,6 @@ * @mixin \Mantle\Contracts\Filesystem\Filesystem */ class Filesystem_Manager implements Filesystem_Manager_Contract { - /** - * Application instance - * - * @var Application - */ - protected $app; - /** * Disk storage. * @@ -56,9 +48,7 @@ class Filesystem_Manager implements Filesystem_Manager_Contract { * * @param Application $app Application instance. */ - public function __construct( Application $app ) { - $this->app = $app; - } + public function __construct( protected Application $app ) {} /** * Retrieve a filesystem disk. @@ -85,6 +75,7 @@ protected function resolve_disk( string $name ): Filesystem { } $config = $this->get_config( $name ); + if ( empty( $config['driver'] ) ) { throw new InvalidArgumentException( "Disk [{$name}] does not have a configured driver." ); } @@ -93,8 +84,7 @@ protected function resolve_disk( string $name ): Filesystem { // Call a custom driver callback. if ( isset( $this->custom_drivers[ $driver ] ) ) { - $this->disks[ $name ] = $this->call_custom_driver( $driver, $config ); - return $this->disks[ $name ]; + return $this->disks[ $name ] = $this->call_custom_driver( $driver, $config ); } $driver_method = 'create_' . strtolower( $driver ) . '_driver'; @@ -103,8 +93,7 @@ protected function resolve_disk( string $name ): Filesystem { throw new InvalidArgumentException( "Disk [{$name}] uses a driver [{$driver}] that is not supported." ); } - $this->disks[ $name ] = $this->{$driver_method}( $config ); - return $this->disks[ $name ]; + return $this->disks[ $name ] = $this->{$driver_method}( $config ); } /** @@ -129,12 +118,13 @@ protected function get_default_disk(): string { /** * Add a custom driver to the filesystem. * - * @param string $driver Driver name. - * @param Closure $callback Callback to invoke to create an instance of the driver. + * @param string $driver Driver name. + * @param \Closure(\Mantle\Contracts\Application, array): \Mantle\Contracts\Filesystem\Filesystem $callback Callback to create the driver. * @return static */ public function extend( string $driver, Closure $callback ) { $this->custom_drivers[ $driver ] = $callback; + return $this; } @@ -146,117 +136,109 @@ public function extend( string $driver, Closure $callback ) { * @return Filesystem */ protected function call_custom_driver( string $driver, array $config ): Filesystem { - $instance = $this->custom_drivers[ $driver ]( $this->app, $config ); - - if ( $instance instanceof AdapterInterface ) { - $instance = $this->create_flysystem( $instance, $config ); - } - - if ( $instance instanceof Flysystem ) { - $instance = $this->adapt( $instance ); - } - - return $instance; - } - - /** - * Adapt a adapter instance. - * - * @param Flysystem $filesystem Filesystem instance. - * @return Filesystem_Adapter - */ - protected function adapt( Flysystem $filesystem ) { - return new Filesystem_Adapter( $filesystem ); + return $this->custom_drivers[ $driver ]( $this->app, $config ); } /** * Create a Flysystem instance with the given adapter. * - * @param AdapterInterface $adapter - * @param array $config Adapter configuration. + * @param FilesystemAdapter $adapter + * @param array $config Adapter configuration. * @return Flysystem - * - * @throws RuntimeException Thrown on missing CachedAdapter. */ - protected function create_flysystem( AdapterInterface $adapter, array $config = [] ): Flysystem { - $cache = Arr::pull( $config, 'cache' ); - $config = Arr::only( $config, [ 'visibility', 'disable_asserts', 'url' ] ); - - if ( $cache ) { - if ( ! class_exists( CachedAdapter::class ) ) { - throw new RuntimeException( 'CachedAdapter class is not loaded.' ); - } - - $adapter = new CachedAdapter( $adapter, $this->create_cache_store( $cache ) ); - } - + protected function create_flysystem( FilesystemAdapter $adapter, array $config = [] ): Flysystem { return new Flysystem( $adapter, $config ); } /** - * Create a cache store instance. + * Create an instance of the local driver. * - * @param mixed $config Adapter configuration. - * @return AbstractCache + * @param array $config Configuration. + * @return Filesystem_Adapter * - * @todo Add support for other caching adapters. + * @throws InvalidArgumentException Thrown on missing WordPress. */ - protected function create_cache_store( $config ): AbstractCache { - return new MemoryStore(); - } + public function create_local_driver( array $config ): Filesystem_Adapter { + if ( ! function_exists( 'wp_upload_dir' ) ) { + throw new InvalidArgumentException( 'The local filesystem cannot be used outside of a WordPress environment.' ); + } - /** - * Create an instance of the local driver. - * - * @param array $config - * @return \Mantle\Contracts\Filesystem\Filesystem - */ - public function create_local_driver( array $config ) { - $permissions = $config['permissions'] ?? []; + $visibility = PortableVisibilityConverter::fromArray( + $config['permissions'] ?? [], + $config['directory_visibility'] ?? $config['visibility'] ?? Visibility::PRIVATE + ); $links = ( $config['links'] ?? null ) === 'skip' - ? Local::SKIP_LINKS - : Local::DISALLOW_LINKS; - - return $this->adapt( - $this->create_flysystem( - new Local( - $config['root'] ?? wp_upload_dir()['basedir'], - $config['lock'] ?? LOCK_EX, - $links, - $permissions - ), - $config - ) + ? LocalAdapter::SKIP_LINKS + : LocalAdapter::DISALLOW_LINKS; + + $upload_dir = wp_upload_dir(); + + // Default the root to the WordPress uploads directory. + $root = (string) ( $config['root'] ?? $upload_dir['basedir'] ); + + /** + * Filter the local filesystem root directory. + * + * @param string $root Root path. + * @param array $config Configuration. + */ + $root = (string) apply_filters( 'mantle_filesystem_local_root', $root, $config ); + + // Ensure the root configuration has a base URL. + $config['root'] = $root; + $config['url'] = $config['url'] ?? $upload_dir['baseurl']; + $config['visibility'] = $config['visibility'] ?? Visibility::PUBLIC; + + /** + * Filter the local filesystem configuration. + * + * @param array $config Configuration. + */ + $config = (array) apply_filters( 'mantle_filesystem_local_config', $config ); + + $adapter = new LocalAdapter( + $root, + $visibility, + $config['lock'] ?? LOCK_EX, + $links ); + + return new Adapter\Local_Adapter( $this->create_flysystem( $adapter, $config ), $adapter, $config ); } /** * Create an instance of the Amazon S3 driver. * * @param array $config S3 configuration. - * @return Filesystem_Adapter + * @return Adapter\AWS_S3_Adapter * * @throws RuntimeException Thrown on missing dependency. */ - public function create_s3_driver( array $config ) { - if ( ! class_exists( AwsS3Adapter::class ) ) { - throw new RuntimeException( 'AwsS3Adapter class not found. Run `composer require league/flysystem-aws-s3-v3`.' ); + public function create_s3_driver( array $config ): Adapter\AWS_S3_Adapter { + if ( ! class_exists( S3Adapter::class ) ) { + throw new RuntimeException( S3Adapter::class . ' class not found. Run `composer require league/flysystem-aws-s3-v3`.' ); } $s3_config = $this->format_s3_config( $config ); - $root = $s3_config['root'] ?? null; + $root = (string) ( $s3_config['root'] ?? '' ); + + $visibility = new AwsS3PortableVisibilityConverter( + $config['visibility'] ?? Visibility::PUBLIC + ); + + $stream_reads = $s3_config['stream_reads'] ?? false; - $options = $config['options'] ?? []; + $client = new S3Client( $s3_config ); - $stream_reads = $config['stream_reads'] ?? false; + $adapter = new S3Adapter( $client, $s3_config['bucket'], $root, $visibility, null, $config['options'] ?? [], $stream_reads ); - return $this->adapt( - $this->create_flysystem( - new AwsS3Adapter( new S3Client( $s3_config ), $s3_config['bucket'], $root, $options, $stream_reads ), - $config - ) + return new Adapter\AWS_S3_Adapter( + $this->create_flysystem( $adapter, $config ), + $adapter, + $s3_config, + $client ); } diff --git a/src/mantle/filesystem/class-filesystem-service-provider.php b/src/mantle/filesystem/class-filesystem-service-provider.php index 5f062d8f..59156c8b 100644 --- a/src/mantle/filesystem/class-filesystem-service-provider.php +++ b/src/mantle/filesystem/class-filesystem-service-provider.php @@ -31,24 +31,14 @@ public function register() { * @return void */ protected function register_native_filesystem() { - $this->app->singleton( - 'files', - function( $app ) { - return new Filesystem(); - } - ); + $this->app->singleton( 'files', fn () => new Filesystem() ); } /** * Register the Flysystem Manager */ public function register_flysystem() { - $this->app->singleton( - 'filesystem', - function ( $app ) { - return new Filesystem_Manager( $app ); - } - ); + $this->app->singleton( 'filesystem', fn ( $app ) => new Filesystem_Manager( $app ) ); } /** diff --git a/src/mantle/filesystem/class-filesystem.php b/src/mantle/filesystem/class-filesystem.php index 360e5236..87d9ef18 100644 --- a/src/mantle/filesystem/class-filesystem.php +++ b/src/mantle/filesystem/class-filesystem.php @@ -11,7 +11,6 @@ use ErrorException; use FilesystemIterator; -use League\Flysystem\FileNotFoundException; use Mantle\Support\Str; use Mantle\Support\Traits\Macroable; use RuntimeException; @@ -51,14 +50,14 @@ public function missing( $path ) { * @param bool $lock * @return string * - * @throws FileNotFoundException Thrown on missing file. + * @throws File_Not_Found_Exception Thrown on missing file. */ - public function get( $path, $lock = false ) { + public function get( string $path, bool $lock = false ): string { if ( $this->is_file( $path ) ) { return $lock ? $this->shared_get( $path ) : file_get_contents( $path ); // phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown } - throw new FileNotFoundException( "File does not exist at path {$path}." ); + throw new File_Not_Found_Exception( "File does not exist at path {$path}." ); } /** @@ -96,7 +95,7 @@ public function shared_get( $path ) { * @param array $data * @return mixed * - * @throws FileNotFoundException Thrown on missing file. + * @throws File_Not_Found_Exception Thrown on missing file. */ public function get_require( $path, array $data = [] ) { if ( $this->is_file( $path ) ) { @@ -110,7 +109,7 @@ public function get_require( $path, array $data = [] ) { } )(); } - throw new FileNotFoundException( "File does not exist at path {$path}." ); + throw new File_Not_Found_Exception( "File does not exist at path {$path}." ); } /** @@ -120,7 +119,7 @@ public function get_require( $path, array $data = [] ) { * @param array $data * @return mixed * - * @throws FileNotFoundException Thrown on missing file. + * @throws File_Not_Found_Exception Thrown on missing file. */ public function require_once( $path, array $data = [] ) { if ( $this->is_file( $path ) ) { @@ -134,7 +133,7 @@ public function require_once( $path, array $data = [] ) { } )(); } - throw new FileNotFoundException( "File does not exist at path {$path}." ); + throw new File_Not_Found_Exception( "File does not exist at path {$path}." ); } /** diff --git a/src/mantle/http/class-uploaded-file.php b/src/mantle/http/class-uploaded-file.php index 6eec1c27..56271dca 100644 --- a/src/mantle/http/class-uploaded-file.php +++ b/src/mantle/http/class-uploaded-file.php @@ -82,7 +82,7 @@ public function store_as( $path, $name, $options = [] ) { $disk = Arr::pull( $options, 'disk' ); - return Container::getInstance()->make( Filesystem_Manager::class )->drive( $disk )->put_file_as( + return Container::get_instance()->make( Filesystem_Manager::class )->drive( $disk )->put_file_as( $path, $this, $name, diff --git a/tests/Filesystem/FilesystemAdapterTest.php b/tests/Filesystem/FilesystemAdapterTest.php index a790fc65..f006d4df 100644 --- a/tests/Filesystem/FilesystemAdapterTest.php +++ b/tests/Filesystem/FilesystemAdapterTest.php @@ -3,10 +3,10 @@ namespace Mantle\Tests\Filesystem; use InvalidArgumentException; -use League\Flysystem\Adapter\Local; -use League\Flysystem\FileExistsException; -use League\Flysystem\FileNotFoundException; +use League\Flysystem\Local\LocalFilesystemAdapter; use League\Flysystem\Filesystem as Flysystem; +use League\Flysystem\FilesystemAdapter; +use League\Flysystem\UnableToReadFile; use Mantle\Filesystem\Filesystem; use Mantle\Filesystem\Filesystem_Adapter; use Mantle\Http\Uploaded_File; @@ -18,14 +18,15 @@ class FilesystemAdapterTest extends TestCase { private $temp_dir; - /** - * @var Flysystem - */ - private $filesystem; + private Flysystem $filesystem; + + private FilesystemAdapter $adapter; protected function setUp(): void { $this->temp_dir = get_temp_dir() . '/mantle-fs-adp'; - $this->filesystem = new Flysystem( new Local( $this->temp_dir ) ); + $this->filesystem = new Flysystem( + $this->adapter = new LocalFilesystemAdapter( $this->temp_dir ), + ); $files = new Filesystem(); $files->ensure_directory_exists( $this->temp_dir ); @@ -34,13 +35,16 @@ protected function setUp(): void { } protected function tearDown(): void { - $this->filesystem->deleteDir( $this->temp_dir ); + ( new Filesystem() )->delete_directory( $this->temp_dir ); + m::close(); + + parent::tearDown(); } public function testResponse() { $this->filesystem->write( 'file.txt', 'Hello World' ); - $files = new Filesystem_Adapter( $this->filesystem ); + $files = new Filesystem_Adapter( $this->filesystem, $this->adapter );; $response = $files->response( 'file.txt' ); ob_start(); @@ -54,7 +58,7 @@ public function testResponse() { public function testDownload() { $this->filesystem->write( 'file.txt', 'Hello World' ); - $files = new Filesystem_Adapter( $this->filesystem ); + $files = new Filesystem_Adapter( $this->filesystem, $this->adapter );; $response = $files->download( 'file.txt', 'hello.txt' ); $this->assertInstanceOf( StreamedResponse::class, $response ); $this->assertSame( 'attachment; filename=hello.txt', $response->headers->get( 'content-disposition' ) ); @@ -62,7 +66,7 @@ public function testDownload() { public function testDownloadNonAsciiFilename() { $this->filesystem->write( 'file.txt', 'Hello World' ); - $files = new Filesystem_Adapter( $this->filesystem ); + $files = new Filesystem_Adapter( $this->filesystem, $this->adapter );; $response = $files->download( 'file.txt', 'пиздюк.txt' ); $this->assertInstanceOf( StreamedResponse::class, $response ); $this->assertSame( "attachment; filename=pizdiuk.txt; filename*=utf-8''%D0%BF%D0%B8%D0%B7%D0%B4%D1%8E%D0%BA.txt", $response->headers->get( 'content-disposition' ) ); @@ -70,7 +74,7 @@ public function testDownloadNonAsciiFilename() { public function testDownloadNonAsciiEmptyFilename() { $this->filesystem->write( 'пиздюк.txt', 'Hello World' ); - $files = new Filesystem_Adapter( $this->filesystem ); + $files = new Filesystem_Adapter( $this->filesystem, $this->adapter );; $response = $files->download( 'пиздюк.txt' ); $this->assertInstanceOf( StreamedResponse::class, $response ); $this->assertSame( 'attachment; filename=pizdiuk.txt; filename*=utf-8\'\'%D0%BF%D0%B8%D0%B7%D0%B4%D1%8E%D0%BA.txt', $response->headers->get( 'content-disposition' ) ); @@ -78,7 +82,7 @@ public function testDownloadNonAsciiEmptyFilename() { public function testDownloadPercentInFilename() { $this->filesystem->write( 'Hello%World.txt', 'Hello World' ); - $files = new Filesystem_Adapter( $this->filesystem ); + $files = new Filesystem_Adapter( $this->filesystem, $this->adapter );; $response = $files->download( 'Hello%World.txt', 'Hello%World.txt' ); $this->assertInstanceOf( StreamedResponse::class, $response ); $this->assertSame( 'attachment; filename=HelloWorld.txt; filename*=utf-8\'\'Hello%25World.txt', $response->headers->get( 'content-disposition' ) ); @@ -86,72 +90,72 @@ public function testDownloadPercentInFilename() { public function testExists() { $this->filesystem->write( 'file.txt', 'Hello World' ); - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); - $this->assertTrue( $Filesystem_Adapter->exists( 'file.txt' ) ); + $filesystem_adapter = new Filesystem_Adapter( $this->filesystem, $this->adapter );; + $this->assertTrue( $filesystem_adapter->exists( 'file.txt' ) ); } public function testMissing() { - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); - $this->assertTrue( $Filesystem_Adapter->missing( 'file.txt' ) ); + $filesystem_adapter = new Filesystem_Adapter( $this->filesystem, $this->adapter );; + $this->assertTrue( $filesystem_adapter->missing( 'file.txt' ) ); } public function testPath() { $this->filesystem->write( 'file.txt', 'Hello World' ); - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); - $this->assertEquals( $this->temp_dir . DIRECTORY_SEPARATOR . 'file.txt', $Filesystem_Adapter->path( 'file.txt' ) ); + $filesystem_adapter = new Filesystem_Adapter( + $this->filesystem, + $this->adapter, + [ + 'root' => $this->temp_dir . DIRECTORY_SEPARATOR, + ] + ); + $this->assertEquals( $this->temp_dir . DIRECTORY_SEPARATOR . 'file.txt', $filesystem_adapter->path( 'file.txt' ) ); } public function testGet() { $this->filesystem->write( 'file.txt', 'Hello World' ); - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); - $this->assertSame( 'Hello World', $Filesystem_Adapter->get( 'file.txt' ) ); + $filesystem_adapter = new Filesystem_Adapter( $this->filesystem, $this->adapter );; + $this->assertSame( 'Hello World', $filesystem_adapter->get( 'file.txt' ) ); } public function testGetFileNotFound() { - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); - $this->expectException( FileNotFoundException::class ); - $Filesystem_Adapter->get( 'file.txt' ); + $filesystem_adapter = new Filesystem_Adapter( $this->filesystem, $this->adapter );; + $this->assertNull( $filesystem_adapter->get( 'file.txt' ) ); } public function testPut() { - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); - $Filesystem_Adapter->put( 'file.txt', 'Something inside' ); + $filesystem_adapter = new Filesystem_Adapter( $this->filesystem, $this->adapter );; + $filesystem_adapter->put( 'file.txt', 'Something inside' ); $this->assertStringEqualsFile( $this->temp_dir . '/file.txt', 'Something inside' ); } public function testPrepend() { file_put_contents( $this->temp_dir . '/file.txt', 'World' ); - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); - $Filesystem_Adapter->prepend( 'file.txt', 'Hello ' ); + $filesystem_adapter = new Filesystem_Adapter( $this->filesystem, $this->adapter );; + $filesystem_adapter->prepend( 'file.txt', 'Hello ' ); $this->assertStringEqualsFile( $this->temp_dir . '/file.txt', 'Hello ' . PHP_EOL . 'World' ); } public function testAppend() { file_put_contents( $this->temp_dir . '/file.txt', 'Hello ' ); - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); - $Filesystem_Adapter->append( 'file.txt', 'Moon' ); + $filesystem_adapter = new Filesystem_Adapter( $this->filesystem, $this->adapter );; + $filesystem_adapter->append( 'file.txt', 'Moon' ); $this->assertStringEqualsFile( $this->temp_dir . '/file.txt', 'Hello ' . PHP_EOL . 'Moon' ); } public function testDelete() { file_put_contents( $this->temp_dir . '/file.txt', 'Hello World' ); - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); - $this->assertTrue( $Filesystem_Adapter->delete( 'file.txt' ) ); + $filesystem_adapter = new Filesystem_Adapter( $this->filesystem, $this->adapter );; + $this->assertTrue( $filesystem_adapter->delete( 'file.txt' ) ); $this->assertFileDoesNotExist( $this->temp_dir . '/file.txt' ); } - public function testDeleteReturnsFalseWhenFileNotFound() { - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); - $this->assertFalse( $Filesystem_Adapter->delete( 'file.txt' ) ); - } - public function testCopy() { $data = '33232'; mkdir( $this->temp_dir . '/foo' ); file_put_contents( $this->temp_dir . '/foo/foo.txt', $data ); - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); - $Filesystem_Adapter->copy( '/foo/foo.txt', '/foo/foo2.txt' ); + $filesystem_adapter = new Filesystem_Adapter( $this->filesystem, $this->adapter );; + $filesystem_adapter->copy( '/foo/foo.txt', '/foo/foo2.txt' ); $this->assertFileExists( $this->temp_dir . '/foo/foo.txt' ); $this->assertEquals( $data, file_get_contents( $this->temp_dir . '/foo/foo.txt' ) ); @@ -165,8 +169,8 @@ public function testMove() { mkdir( $this->temp_dir . '/foo' ); file_put_contents( $this->temp_dir . '/foo/foo.txt', $data ); - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); - $Filesystem_Adapter->move( '/foo/foo.txt', '/foo/foo2.txt' ); + $filesystem_adapter = new Filesystem_Adapter( $this->filesystem, $this->adapter );; + $filesystem_adapter->move( '/foo/foo.txt', '/foo/foo2.txt' ); $this->assertFileDoesNotExist( $this->temp_dir . '/foo/foo.txt' ); @@ -176,103 +180,107 @@ public function testMove() { public function testStream() { $this->filesystem->write( 'file.txt', $original_content = 'Hello World' ); - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); - $readStream = $Filesystem_Adapter->readStream( 'file.txt' ); - $Filesystem_Adapter->writeStream( 'copy.txt', $readStream ); - $this->assertEquals( $original_content, $Filesystem_Adapter->get( 'copy.txt' ) ); + $filesystem_adapter = new Filesystem_Adapter( $this->filesystem, $this->adapter );; + $readStream = $filesystem_adapter->readStream( 'file.txt' ); + $filesystem_adapter->writeStream( 'copy.txt', $readStream ); + $this->assertEquals( $original_content, $filesystem_adapter->get( 'copy.txt' ) ); } public function testStreamBetweenFilesystems() { - $secondFilesystem = new Flysystem( new Local( $this->temp_dir . '/second' ) ); + $secondFilesystem = new Flysystem( $adapter = new LocalFilesystemAdapter( $this->temp_dir . '/second' ) ); $this->filesystem->write( 'file.txt', $original_content = 'Hello World' ); - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); - $secondFilesystem_Adapter = new Filesystem_Adapter( $secondFilesystem ); - $readStream = $Filesystem_Adapter->readStream( 'file.txt' ); + $filesystem_adapter = new Filesystem_Adapter( $this->filesystem, $this->adapter ); + $secondFilesystem_Adapter = new Filesystem_Adapter( $secondFilesystem, $adapter ); + $readStream = $filesystem_adapter->readStream( 'file.txt' ); $secondFilesystem_Adapter->writeStream( 'copy.txt', $readStream ); $this->assertEquals( $original_content, $secondFilesystem_Adapter->get( 'copy.txt' ) ); } - public function testStreamToExistingFileThrows() { - $this->expectException( FileExistsException::class ); + public function testStreamToExistingFileOverwrites() { $this->filesystem->write( 'file.txt', 'Hello World' ); $this->filesystem->write( 'existing.txt', 'Dear Kate' ); - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); - $readStream = $Filesystem_Adapter->readStream( 'file.txt' ); - $Filesystem_Adapter->writeStream( 'existing.txt', $readStream ); + + $filesystem_adapter = new Filesystem_Adapter( $this->filesystem, $this->adapter, [ 'throw' => false ] );; + $readStream = $filesystem_adapter->readStream( 'file.txt' ); + + $this->assertTrue( + $filesystem_adapter->writeStream( 'existing.txt', $readStream ), + ); + } + + public function testReadStreamNonExistentFile() { + $filesystem_adapter = new Filesystem_Adapter( $this->filesystem, $this->adapter ); + + $this->assertFalse( $filesystem_adapter->readStream( 'nonexistent.txt' ) ); } public function testReadStreamNonExistentFileThrows() { - $this->expectException( FileNotFoundException::class ); - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); - $Filesystem_Adapter->readStream( 'nonexistent.txt' ); + $this->expectException( UnableToReadFile::class ); + + $filesystem_adapter = new Filesystem_Adapter( $this->filesystem, $this->adapter, [ 'throw' => true ] ); + + $filesystem_adapter->readStream( 'nonexistent.txt' ); } public function testStreamInvalidResourceThrows() { $this->expectException( InvalidArgumentException::class ); - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); - $Filesystem_Adapter->writeStream( 'file.txt', 'foo bar' ); + $filesystem_adapter = new Filesystem_Adapter( $this->filesystem, $this->adapter );; + $filesystem_adapter->writeStream( 'file.txt', 'foo bar' ); } public function testPutFileAs() { file_put_contents( $filePath = $this->temp_dir . '/foo.txt', 'uploaded file content' ); - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); + $filesystem_adapter = new Filesystem_Adapter( $this->filesystem, $this->adapter );; $uploadedFile = new Uploaded_File( $filePath, 'org.txt', null, null, true ); - $storagePath = $Filesystem_Adapter->put_file_as( '/', $uploadedFile, 'new.txt' ); + $storagePath = $filesystem_adapter->put_file_as( '/', $uploadedFile, 'new.txt' ); $this->assertSame( 'new.txt', $storagePath ); $this->assertFileExists( $filePath ); - $Filesystem_Adapter->assertExists( $storagePath ); + $filesystem_adapter->assertExists( $storagePath ); - $this->assertSame( 'uploaded file content', $Filesystem_Adapter->read( $storagePath ) ); + $this->assertSame( 'uploaded file content', $filesystem_adapter->read( $storagePath ) ); } public function testPutFileAsWithAbsoluteFilePath() { file_put_contents( $filePath = $this->temp_dir . '/foo.txt', 'normal file content' ); - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); + $filesystem_adapter = new Filesystem_Adapter( $this->filesystem, $this->adapter );; - $storagePath = $Filesystem_Adapter->put_file_as( '/', $filePath, 'new.txt' ); + $storagePath = $filesystem_adapter->put_file_as( '/', $filePath, 'new.txt' ); - $this->assertSame( 'normal file content', $Filesystem_Adapter->read( $storagePath ) ); + $this->assertSame( 'normal file content', $filesystem_adapter->read( $storagePath ) ); } public function testPutFile() { file_put_contents( $filePath = $this->temp_dir . '/foo.txt', 'uploaded file content' ); - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); + $filesystem_adapter = new Filesystem_Adapter( $this->filesystem, $this->adapter );; $uploadedFile = new Uploaded_File( $filePath, 'org.txt', null, null, true ); - $storagePath = $Filesystem_Adapter->put_file( '/', $uploadedFile ); + $storagePath = $filesystem_adapter->put_file( '/', $uploadedFile ); $this->assertSame( 44, strlen( $storagePath ) ); // random 40 characters + ".txt" $this->assertFileExists( $filePath ); - $Filesystem_Adapter->assertExists( $storagePath ); + $filesystem_adapter->assertExists( $storagePath ); } public function testPutFileWithAbsoluteFilePath() { file_put_contents( $filePath = $this->temp_dir . '/foo.txt', 'uploaded file content' ); - $Filesystem_Adapter = new Filesystem_Adapter( $this->filesystem ); + $filesystem_adapter = new Filesystem_Adapter( $this->filesystem, $this->adapter ); - $storagePath = $Filesystem_Adapter->put_file( '/', $filePath ); + $storagePath = $filesystem_adapter->put_file( '/', $filePath ); $this->assertSame( 44, strlen( $storagePath ) ); // random 40 characters + ".txt" - $Filesystem_Adapter->assertExists( $storagePath ); - } - - public function test_url() { - $this->assertEquals( - home_url( '/wp-content/uploads/foo.txt' ), - ( new Filesystem_Adapter( $this->filesystem ) )->url( '/foo.txt' ) - ); + $filesystem_adapter->assertExists( $storagePath ); } } diff --git a/tests/Filesystem/FilesystemManagerTest.php b/tests/Filesystem/FilesystemManagerTest.php index 0eef4382..48c484a4 100644 --- a/tests/Filesystem/FilesystemManagerTest.php +++ b/tests/Filesystem/FilesystemManagerTest.php @@ -2,15 +2,46 @@ namespace Mantle\Tests\Filesystem; use InvalidArgumentException; -use League\Flysystem\Adapter\NullAdapter; -use League\Flysystem\Filesystem; use Mantle\Application\Application; +use Mantle\Filesystem\Filesystem; use Mantle\Filesystem\Filesystem_Manager; +use Mantle\Testing\Framework_Test_Case; use PHPUnit\Framework\TestCase; -use Mantle\Contracts\Filesystem\Filesystem as Filesystem_Contract; +use Mockery as m; + use function Mantle\Support\Helpers\tap; -class FilesystemManagerTest extends TestCase { +class FilesystemManagerTest extends Framework_Test_Case { + protected function tearDown(): void { + parent::tearDown(); + + m::close(); + + ( new Filesystem() )->delete( wp_upload_dir()['basedir'] . '/file.txt' ); + } + + public function test_default_disk() { + $this->expectApplied( 'mantle_filesystem_local_config' )->once()->andReturnArray(); + + $filesystem = $this->app->make( Filesystem_Manager::class ); + + $drive = $filesystem->drive(); + + $drive->put( 'file.txt', 'contents' ); + + $this->assertTrue( $drive->exists( 'file.txt' ) ); + $this->assertEquals( 'contents', $drive->read( 'file.txt' ) ); + + // Attempt to read the URL/path for the file. + $this->assertEquals( home_url( '/wp-content/uploads/file.txt' ), $drive->url( 'file.txt' ) ); + $this->assertEquals( wp_upload_dir()['basedir'] . '/file.txt', $drive->path( 'file.txt' ) ); + $this->assertTrue( file_exists( $drive->path( 'file.txt' ) ) ); + + $drive->delete( 'file.txt' ); + + $this->assertFalse( $drive->exists( 'file.txt' ) ); + } + public function test_invalid_disk() { $this->expectException( InvalidArgumentException::class ); $this->expectExceptionMessage( 'Disk [unsupported] does not have a configured driver.' ); @@ -57,26 +88,39 @@ function( Application $app ) { $app['config'] = [ 'filesystem.disks.custom-driver' => [ 'driver' => 'custom-driver', + 'extra_config' => 'value', + 'root' => '/path', ], ]; } ) ); + $adapter = m::mock( \Mantle\Contracts\Filesystem\Filesystem::class ); + + $adapter->shouldReceive( 'exists' ) + ->once() + ->with( '/path' ) + ->andReturn( true ); + + $_SERVER['__custom_driver_called'] = 0; + $filesystem->extend( 'custom-driver', - function () { + function ( $app, array $config ) use ( $adapter ) { + $this->assertInstanceof( \Mantle\Contracts\Application::class, $app ); + $this->assertEquals( [ 'driver' => 'custom-driver', 'extra_config' => 'value', 'root' => '/path' ], $config ); + $_SERVER['__custom_driver_called']++; - return new NullAdapter(); - } - ); - $this->assertInstanceOf( Filesystem_Contract::class, $filesystem->drive( 'custom-driver' ) ); + return $adapter; + }, + ); - // Invoke the disk again and see if the variable is incremented. - $drive = $filesystem->drive( 'custom-driver' ); + $this->assertInstanceOf( $adapter::class, $filesystem->drive( 'custom-driver' ) ); + $this->assertTrue( $filesystem->drive( 'custom-driver' )->exists( '/path' ) ); - $this->assertEquals( 1, $_SERVER['__custom_driver_called'], 'Disk should be reused.' ); - $this->assertFalse( $drive->exists( '/path' ) ); + // Ensure that the custom driver was instantiated once. + $this->assertEquals( 1, $_SERVER['__custom_driver_called'] ); } } diff --git a/tests/Filesystem/FilesystemTest.php b/tests/Filesystem/FilesystemTest.php index e54cb081..4c63692d 100644 --- a/tests/Filesystem/FilesystemTest.php +++ b/tests/Filesystem/FilesystemTest.php @@ -2,12 +2,12 @@ namespace Mantle\Tests\Filesystem; +use Mantle\Filesystem\File_Not_Found_Exception; use Mantle\Filesystem\Filesystem; use Mantle\Testing\Assert; use Mockery as m; use PHPUnit\Framework\TestCase; use SplFileInfo; -use League\Flysystem\FileNotFoundException; class FilesystemTest extends TestCase { @@ -271,7 +271,7 @@ public function testMoveDirectoryReturnsFalseWhileOverwritingAndUnableToDeleteDe } public function testGetThrowsExceptionNonexisitingFile() { - $this->expectException( FileNotFoundException::class ); + $this->expectException( File_Not_Found_Exception::class ); $files = new Filesystem(); $files->get( static::$temp_dir . '/unknown-file.txt' ); @@ -284,7 +284,7 @@ public function testGetRequireReturnsProperly() { } public function testGetRequireThrowsExceptionNonExistingFile() { - $this->expectException( FileNotFoundException::class ); + $this->expectException( File_Not_Found_Exception::class ); $files = new Filesystem(); $files->get_require( static::$temp_dir . '/file.php' );