From 609516125647e980136e4407042460b1ecf527f3 Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Mon, 30 Nov 2020 17:23:02 +0100 Subject: [PATCH] [9.x] Flysystem v2 (#33612) * Flysystem v2 * Dynamic separator for Windows * Update namespace FTP adapter * Bump minimum alpha * Remove ^2.0 constraint from ftp driver * Update composer.json * Update composer.json --- composer.json | 12 +- .../Filesystem/FileExistsException.php | 10 - .../Contracts/Filesystem/Filesystem.php | 9 +- src/Illuminate/Filesystem/AwsS3V3Adapter.php | 75 ++++ src/Illuminate/Filesystem/Cache.php | 71 ---- .../Filesystem/FilesystemAdapter.php | 328 +++++++++--------- .../Filesystem/FilesystemManager.php | 120 +++---- src/Illuminate/Filesystem/composer.json | 8 +- tests/Filesystem/FilesystemAdapterTest.php | 120 ++++--- tests/Filesystem/FilesystemTest.php | 23 -- 10 files changed, 373 insertions(+), 403 deletions(-) delete mode 100644 src/Illuminate/Contracts/Filesystem/FileExistsException.php create mode 100644 src/Illuminate/Filesystem/AwsS3V3Adapter.php delete mode 100644 src/Illuminate/Filesystem/Cache.php diff --git a/composer.json b/composer.json index 7aa57c27ffa8..2f5533c97fe0 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "dragonmantank/cron-expression": "^3.0.2", "egulias/email-validator": "^2.1.10", "league/commonmark": "^1.3", - "league/flysystem": "^1.1", + "league/flysystem": "^2.0", "monolog/monolog": "^2.0", "nesbot/carbon": "^2.31", "opis/closure": "^3.6", @@ -82,7 +82,9 @@ "doctrine/dbal": "^2.6|^3.0", "filp/whoops": "^2.8", "guzzlehttp/guzzle": "^7.2", - "league/flysystem-cached-adapter": "^1.0", + "league/flysystem-aws-s3-v3": "^2.0", + "league/flysystem-ftp": "^2.0", + "league/flysystem-sftp": "^2.0", "mockery/mockery": "^1.4.2", "orchestra/testbench-core": "^7.0", "pda/pheanstalk": "^4.0", @@ -134,9 +136,9 @@ "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", "guzzlehttp/guzzle": "Required to use the HTTP Client, Mailgun mail driver and the ping methods on schedules (^7.2).", "laravel/tinker": "Required to use the tinker console command (^2.0).", - "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^1.0).", - "league/flysystem-cached-adapter": "Required to use the Flysystem cache (^1.0).", - "league/flysystem-sftp": "Required to use the Flysystem SFTP driver (^1.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^2.0).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^2.0).", + "league/flysystem-sftp": "Required to use the Flysystem SFTP driver (^2.0).", "mockery/mockery": "Required to use mocking (^1.4.2).", "nyholm/psr7": "Required to use PSR-7 bridging features (^1.2).", "pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).", diff --git a/src/Illuminate/Contracts/Filesystem/FileExistsException.php b/src/Illuminate/Contracts/Filesystem/FileExistsException.php deleted file mode 100644 index 9027892faa00..000000000000 --- a/src/Illuminate/Contracts/Filesystem/FileExistsException.php +++ /dev/null @@ -1,10 +0,0 @@ -client = $client; + } + + /** + * Get the URL for the file at the given path. + * + * @param string $path + * @return string + * + * @throws \RuntimeException + */ + public function url($path) + { + // 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->concatPathToUrl($this->config['url'], $this->prefixer->prefixPath($path)); + } + + return $this->client->getObjectUrl( + $this->config['bucket'], $this->prefixer->prefixPath($path) + ); + } + + /** + * Get a temporary URL for the file at the given path. + * + * @param string $path + * @param \DateTimeInterface $expiration + * @param array $options + * @return string + */ + public function temporaryUrl($path, $expiration, array $options = []) + { + $command = $this->client->getCommand('GetObject', array_merge([ + 'Bucket' => $this->config['bucket'], + 'Key' => $this->prefixer->prefixPath($path), + ], $options)); + + return (string) $this->client->createPresignedRequest( + $command, $expiration + )->getUri(); + } +} diff --git a/src/Illuminate/Filesystem/Cache.php b/src/Illuminate/Filesystem/Cache.php deleted file mode 100644 index 8ae2486dabf8..000000000000 --- a/src/Illuminate/Filesystem/Cache.php +++ /dev/null @@ -1,71 +0,0 @@ -key = $key; - $this->expire = $expire; - $this->repository = $repository; - } - - /** - * Load the cache. - * - * @return void - */ - public function load() - { - $contents = $this->repository->get($this->key); - - if (! is_null($contents)) { - $this->setFromStorage($contents); - } - } - - /** - * Persist the cache. - * - * @return void - */ - public function save() - { - $contents = $this->getForStorage(); - - $this->repository->put($this->key, $contents, $this->expire); - } -} diff --git a/src/Illuminate/Filesystem/FilesystemAdapter.php b/src/Illuminate/Filesystem/FilesystemAdapter.php index 1c33b9892676..29a7badbb7b4 100644 --- a/src/Illuminate/Filesystem/FilesystemAdapter.php +++ b/src/Illuminate/Filesystem/FilesystemAdapter.php @@ -3,49 +3,81 @@ namespace Illuminate\Filesystem; use Illuminate\Contracts\Filesystem\Cloud as CloudFilesystemContract; -use Illuminate\Contracts\Filesystem\FileExistsException as ContractFileExistsException; -use Illuminate\Contracts\Filesystem\FileNotFoundException as ContractFileNotFoundException; use Illuminate\Contracts\Filesystem\Filesystem as FilesystemContract; use Illuminate\Http\File; use Illuminate\Http\UploadedFile; use Illuminate\Support\Arr; -use Illuminate\Support\Collection; use Illuminate\Support\Str; use InvalidArgumentException; -use League\Flysystem\Adapter\Ftp; -use League\Flysystem\Adapter\Local as LocalAdapter; -use League\Flysystem\AdapterInterface; -use League\Flysystem\AwsS3v3\AwsS3Adapter; -use League\Flysystem\Cached\CachedAdapter; -use League\Flysystem\FileExistsException; -use League\Flysystem\FileNotFoundException; -use League\Flysystem\FilesystemInterface; +use League\Flysystem\FilesystemAdapter as FlysystemAdapter; +use League\Flysystem\FilesystemOperator; +use League\Flysystem\Ftp\FtpAdapter; +use League\Flysystem\Local\LocalFilesystemAdapter as LocalAdapter; +use League\Flysystem\PathPrefixer; +use League\Flysystem\StorageAttributes; +use League\Flysystem\UnableToCopyFile; +use League\Flysystem\UnableToCreateDirectory; +use League\Flysystem\UnableToDeleteDirectory; +use League\Flysystem\UnableToDeleteFile; +use League\Flysystem\UnableToMoveFile; +use League\Flysystem\UnableToReadFile; +use League\Flysystem\UnableToSetVisibility; +use League\Flysystem\UnableToWriteFile; +use League\Flysystem\Visibility; use PHPUnit\Framework\Assert as PHPUnit; use Psr\Http\Message\StreamInterface; use RuntimeException; use Symfony\Component\HttpFoundation\StreamedResponse; /** - * @mixin \League\Flysystem\FilesystemInterface + * @mixin \League\Flysystem\FilesystemOperator */ class FilesystemAdapter implements CloudFilesystemContract { /** * The Flysystem filesystem implementation. * - * @var \League\Flysystem\FilesystemInterface + * @var \League\Flysystem\FilesystemOperator */ protected $driver; + /** + * The Flysystem adapter implementation. + * + * @var \League\Flysystem\FilesystemAdapter + */ + protected $adapter; + + /** + * The filesystem configuration. + * + * @var array + */ + protected $config; + + /** + * The Flysystem PathPrefixer instance. + * + * @var \League\Flysystem\PathPrefixer + */ + protected $prefixer; + /** * Create a new filesystem adapter instance. * - * @param \League\Flysystem\FilesystemInterface $driver + * @param \League\Flysystem\FilesystemOperator $driver + * @param \League\Flysystem\FilesystemAdapter $adapter + * @param array $config * @return void */ - public function __construct(FilesystemInterface $driver) + public function __construct(FilesystemOperator $driver, FlysystemAdapter $adapter, array $config = []) { $this->driver = $driver; + $this->adapter = $adapter; + $this->config = $config; + $this->prefixer = new PathPrefixer( + $config['root'] ?? '', $config['directory_separator'] ?? DIRECTORY_SEPARATOR + ); } /** @@ -105,7 +137,7 @@ public function assertMissing($path) */ public function exists($path) { - return $this->driver->has($path); + return $this->driver->fileExists($path); } /** @@ -127,29 +159,21 @@ public function missing($path) */ public function path($path) { - $adapter = $this->driver->getAdapter(); - - if ($adapter instanceof CachedAdapter) { - $adapter = $adapter->getAdapter(); - } - - return $adapter->getPathPrefix().$path; + return $this->prefixer->prefixPath($path); } /** * Get the contents of a file. * * @param string $path - * @return string - * - * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + * @return string|null */ public function get($path) { try { return $this->driver->read($path); - } catch (FileNotFoundException $e) { - throw new ContractFileNotFoundException($e->getMessage(), $e->getCode(), $e); + } catch (UnableToReadFile $e) { + // } } @@ -233,13 +257,21 @@ public function put($path, $contents, $options = []) return $this->putFile($path, $contents, $options); } - if ($contents instanceof StreamInterface) { - return $this->driver->putStream($path, $contents->detach(), $options); + try { + if ($contents instanceof StreamInterface) { + $this->driver->writeStream($path, $contents->detach(), $options); + + return true; + } + + is_resource($contents) + ? $this->driver->writeStream($path, $contents, $options) + : $this->driver->write($path, $contents, $options); + } catch (UnableToWriteFile $e) { + return false; } - return is_resource($contents) - ? $this->driver->putStream($path, $contents, $options) - : $this->driver->put($path, $contents, $options); + return true; } /** @@ -292,7 +324,7 @@ public function putFileAs($path, $file, $name, $options = []) */ public function getVisibility($path) { - if ($this->driver->getVisibility($path) == AdapterInterface::VISIBILITY_PUBLIC) { + if ($this->driver->visibility($path) == Visibility::PUBLIC) { return FilesystemContract::VISIBILITY_PUBLIC; } @@ -308,7 +340,13 @@ public function getVisibility($path) */ public function setVisibility($path, $visibility) { - return $this->driver->setVisibility($path, $this->parseVisibility($visibility)); + try { + $this->driver->setVisibility($path, $this->parseVisibility($visibility)); + } catch (UnableToSetVisibility $e) { + return false; + } + + return true; } /** @@ -359,10 +397,8 @@ public function delete($paths) foreach ($paths as $path) { try { - if (! $this->driver->delete($path)) { - $success = false; - } - } catch (FileNotFoundException $e) { + $this->driver->delete($path); + } catch (UnableToDeleteFile $e) { $success = false; } } @@ -379,7 +415,13 @@ public function delete($paths) */ public function copy($from, $to) { - return $this->driver->copy($from, $to); + try { + $this->driver->copy($from, $to); + } catch (UnableToCopyFile $e) { + return false; + } + + return true; } /** @@ -391,7 +433,13 @@ public function copy($from, $to) */ public function move($from, $to) { - return $this->driver->rename($from, $to); + try { + $this->driver->move($from, $to); + } catch (UnableToMoveFile $e) { + return false; + } + + return true; } /** @@ -402,7 +450,7 @@ public function move($from, $to) */ public function size($path) { - return $this->driver->getSize($path); + return $this->driver->fileSize($path); } /** @@ -413,7 +461,7 @@ public function size($path) */ public function mimeType($path) { - return $this->driver->getMimetype($path); + return $this->driver->mimeType($path); } /** @@ -424,38 +472,7 @@ public function mimeType($path) */ public function lastModified($path) { - return $this->driver->getTimestamp($path); - } - - /** - * Get the URL for the file at the given path. - * - * @param string $path - * @return string - * - * @throws \RuntimeException - */ - public function url($path) - { - $adapter = $this->driver->getAdapter(); - - if ($adapter instanceof CachedAdapter) { - $adapter = $adapter->getAdapter(); - } - - if (method_exists($adapter, 'getUrl')) { - return $adapter->getUrl($path); - } elseif (method_exists($this->driver, 'getUrl')) { - return $this->driver->getUrl($path); - } elseif ($adapter instanceof AwsS3Adapter) { - return $this->getAwsUrl($adapter, $path); - } elseif ($adapter instanceof Ftp) { - return $this->getFtpUrl($path); - } elseif ($adapter instanceof LocalAdapter) { - return $this->getLocalUrl($path); - } else { - throw new RuntimeException('This driver does not support retrieving URLs.'); - } + return $this->driver->lastModified($path); } /** @@ -464,9 +481,9 @@ public function url($path) public function readStream($path) { try { - return $this->driver->readStream($path) ?: null; - } catch (FileNotFoundException $e) { - throw new ContractFileNotFoundException($e->getMessage(), $e->getCode(), $e); + return $this->driver->readStream($path); + } catch (UnableToReadFile $e) { + // } } @@ -476,31 +493,37 @@ public function readStream($path) public function writeStream($path, $resource, array $options = []) { try { - return $this->driver->writeStream($path, $resource, $options); - } catch (FileExistsException $e) { - throw new ContractFileExistsException($e->getMessage(), $e->getCode(), $e); + $this->driver->writeStream($path, $resource, $options); + } catch (UnableToWriteFile $e) { + return false; } + + return true; } /** * Get the URL for the file at the given path. * - * @param \League\Flysystem\AwsS3v3\AwsS3Adapter $adapter * @param string $path * @return string + * + * @throws \RuntimeException */ - protected function getAwsUrl($adapter, $path) + public function url($path) { - // 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 (! is_null($url = $this->driver->getConfig()->get('url'))) { - return $this->concatPathToUrl($url, $adapter->getPathPrefix().$path); - } + $adapter = $this->adapter; - return $adapter->getClient()->getObjectUrl( - $adapter->getBucket(), $adapter->getPathPrefix().$path - ); + if (method_exists($adapter, 'getUrl')) { + return $adapter->getUrl($path); + } elseif (method_exists($this->driver, 'getUrl')) { + return $this->driver->getUrl($path); + } elseif ($adapter instanceof FtpAdapter) { + return $this->getFtpUrl($path); + } elseif ($adapter instanceof LocalAdapter) { + return $this->getLocalUrl($path); + } else { + throw new RuntimeException('This driver does not support retrieving URLs.'); + } } /** @@ -511,10 +534,8 @@ protected function getAwsUrl($adapter, $path) */ protected function getFtpUrl($path) { - $config = $this->driver->getConfig(); - - return $config->has('url') - ? $this->concatPathToUrl($config->get('url'), $path) + return isset($this->config['url']) + ? $this->concatPathToUrl($this->config['url'], $path) : $path; } @@ -526,13 +547,11 @@ protected function getFtpUrl($path) */ protected function getLocalUrl($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); + if (isset($this->config['url'])) { + return $this->concatPathToUrl($this->config['url'], $path); } $path = '/storage/'.$path; @@ -559,42 +578,11 @@ protected function getLocalUrl($path) */ public function temporaryUrl($path, $expiration, array $options = []) { - $adapter = $this->driver->getAdapter(); - - if ($adapter instanceof CachedAdapter) { - $adapter = $adapter->getAdapter(); - } - - if (method_exists($adapter, 'getTemporaryUrl')) { - return $adapter->getTemporaryUrl($path, $expiration, $options); - } elseif ($adapter instanceof AwsS3Adapter) { - return $this->getAwsTemporaryUrl($adapter, $path, $expiration, $options); - } else { + if (! method_exists($this->adapter, 'getTemporaryUrl')) { throw new RuntimeException('This driver does not support creating temporary URLs.'); } - } - /** - * Get a temporary URL for the file at the given path. - * - * @param \League\Flysystem\AwsS3v3\AwsS3Adapter $adapter - * @param string $path - * @param \DateTimeInterface $expiration - * @param array $options - * @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(); + return $this->adapter->getTemporaryUrl($path, $expiration, $options); } /** @@ -618,9 +606,14 @@ protected function concatPathToUrl($url, $path) */ public function files($directory = null, $recursive = false) { - $contents = $this->driver->listContents($directory, $recursive); - - return $this->filterContentsByType($contents, 'file'); + return $this->driver->listContents($directory, $recursive) + ->filter(function (StorageAttributes $attributes) { + return $attributes->isFile(); + }) + ->map(function (StorageAttributes $attributes) { + return $attributes->path(); + }) + ->toArray(); } /** @@ -643,9 +636,14 @@ public function allFiles($directory = null) */ public function directories($directory = null, $recursive = false) { - $contents = $this->driver->listContents($directory, $recursive); - - return $this->filterContentsByType($contents, 'dir'); + return $this->driver->listContents($directory, $recursive) + ->filter(function (StorageAttributes $attributes) { + return $attributes->isDir(); + }) + ->map(function (StorageAttributes $attributes) { + return $attributes->path(); + }) + ->toArray(); } /** @@ -667,7 +665,13 @@ public function allDirectories($directory = null) */ public function makeDirectory($path) { - return $this->driver->createDir($path); + try { + $this->driver->createDirectory($path); + } catch (UnableToCreateDirectory $e) { + return false; + } + + return true; } /** @@ -678,47 +682,43 @@ public function makeDirectory($path) */ public function deleteDirectory($directory) { - return $this->driver->deleteDir($directory); + try { + $this->driver->deleteDirectory($directory); + } catch (UnableToDeleteDirectory $e) { + return false; + } + + return true; } /** - * Flush the Flysystem cache. + * Get the Flysystem driver. * - * @return void + * @return \League\Flysystem\FilesystemOperator */ - public function flushCache() + public function getDriver() { - $adapter = $this->driver->getAdapter(); - - if ($adapter instanceof CachedAdapter) { - $adapter->getCache()->flush(); - } + return $this->driver; } /** - * Get the Flysystem driver. + * Get the Flysystem adapter. * - * @return \League\Flysystem\FilesystemInterface + * @return \League\Flysystem\FilesystemAdapter */ - public function getDriver() + public function getAdapter() { - return $this->driver; + return $this->adapter; } /** - * Filter directory contents by type. + * Get the configuration values. * - * @param array $contents - * @param string $type * @return array */ - protected function filterContentsByType($contents, $type) + public function getConfig() { - return Collection::make($contents) - ->where('type', $type) - ->pluck('path') - ->values() - ->all(); + return $this->config; } /** @@ -737,9 +737,9 @@ protected function parseVisibility($visibility) switch ($visibility) { case FilesystemContract::VISIBILITY_PUBLIC: - return AdapterInterface::VISIBILITY_PUBLIC; + return Visibility::PUBLIC; case FilesystemContract::VISIBILITY_PRIVATE: - return AdapterInterface::VISIBILITY_PRIVATE; + return Visibility::PRIVATE; } throw new InvalidArgumentException("Unknown visibility: {$visibility}."); diff --git a/src/Illuminate/Filesystem/FilesystemManager.php b/src/Illuminate/Filesystem/FilesystemManager.php index 5575439418fc..14c953fb892d 100644 --- a/src/Illuminate/Filesystem/FilesystemManager.php +++ b/src/Illuminate/Filesystem/FilesystemManager.php @@ -7,15 +7,17 @@ use Illuminate\Contracts\Filesystem\Factory as FactoryContract; use Illuminate\Support\Arr; use InvalidArgumentException; -use League\Flysystem\Adapter\Ftp as FtpAdapter; -use League\Flysystem\Adapter\Local as LocalAdapter; -use League\Flysystem\AdapterInterface; -use League\Flysystem\AwsS3v3\AwsS3Adapter as S3Adapter; -use League\Flysystem\Cached\CachedAdapter; -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\Sftp\SftpAdapter; +use League\Flysystem\FilesystemAdapter as FlysystemAdapter; +use League\Flysystem\Ftp\FtpAdapter as FtpAdapter; +use League\Flysystem\Ftp\FtpConnectionOptions; +use League\Flysystem\Local\LocalFilesystemAdapter as LocalAdapter; +use League\Flysystem\PHPSecLibV2\SftpAdapter; +use League\Flysystem\PHPSecLibV2\SftpConnectionProvider; +use League\Flysystem\UnixVisibility\PortableVisibilityConverter; +use League\Flysystem\Visibility; /** * @mixin \Illuminate\Contracts\Filesystem\Filesystem @@ -140,13 +142,7 @@ protected function resolve($name) */ protected function callCustomCreator(array $config) { - $driver = $this->customCreators[$config['driver']]($this->app, $config); - - if ($driver instanceof FilesystemInterface) { - return $this->adapt($driver); - } - - return $driver; + return $this->customCreators[$config['driver']]($this->app, $config); } /** @@ -157,15 +153,19 @@ protected function callCustomCreator(array $config) */ public function createLocalDriver(array $config) { - $permissions = $config['permissions'] ?? []; + $visibility = PortableVisibilityConverter::fromArray( + $config['permissions'] ?? [] + ); $links = ($config['links'] ?? null) === 'skip' ? LocalAdapter::SKIP_LINKS : LocalAdapter::DISALLOW_LINKS; - return $this->adapt($this->createFlysystem(new LocalAdapter( - $config['root'], $config['lock'] ?? LOCK_EX, $links, $permissions - ), $config)); + $adapter = new LocalAdapter( + $config['root'], $visibility, $config['lock'] ?? LOCK_EX, $links + ); + + return new FilesystemAdapter($this->createFlysystem($adapter, $config), $adapter, $config); } /** @@ -176,9 +176,9 @@ public function createLocalDriver(array $config) */ public function createFtpDriver(array $config) { - return $this->adapt($this->createFlysystem( - new FtpAdapter($config), $config - )); + $adapter = new FtpAdapter(FtpConnectionOptions::fromArray($config)); + + return new FilesystemAdapter($this->createFlysystem($adapter, $config), $adapter, $config); } /** @@ -189,9 +189,17 @@ public function createFtpDriver(array $config) */ public function createSftpDriver(array $config) { - return $this->adapt($this->createFlysystem( - new SftpAdapter($config), $config - )); + $provider = SftpConnectionProvider::fromArray($config); + + $root = $config['root'] ?? '/'; + + $visibility = PortableVisibilityConverter::fromArray( + $config['permissions'] ?? [] + ); + + $adapter = new SftpAdapter($provider, $root, $visibility); + + return new FilesystemAdapter($this->createFlysystem($adapter, $config), $adapter, $config); } /** @@ -206,13 +214,19 @@ public function createS3Driver(array $config) $root = $s3Config['root'] ?? null; - $options = $config['options'] ?? []; + $visibility = new AwsS3PortableVisibilityConverter( + $config['visibility'] ?? Visibility::PUBLIC + ); + + $streamReads = $s3Config['stream_reads'] ?? false; + + $client = new S3Client($s3Config); - $streamReads = $config['stream_reads'] ?? false; + $adapter = new S3Adapter($client, $s3Config['bucket'], $root, $visibility, null, [], $streamReads); - return $this->adapt($this->createFlysystem( - new S3Adapter(new S3Client($s3Config), $s3Config['bucket'], $root, $options, $streamReads), $config - )); + return new AwsS3V3Adapter( + $this->createFlysystem($adapter, $config), $adapter, $s3Config, $client + ); } /** @@ -235,53 +249,15 @@ protected function formatS3Config(array $config) /** * Create a Flysystem instance with the given adapter. * - * @param \League\Flysystem\AdapterInterface $adapter + * @param \League\Flysystem\FilesystemAdapter $adapter * @param array $config - * @return \League\Flysystem\FilesystemInterface + * @return \League\Flysystem\FilesystemOperator */ - protected function createFlysystem(AdapterInterface $adapter, array $config) + protected function createFlysystem(FlysystemAdapter $adapter, array $config) { - $cache = Arr::pull($config, 'cache'); - $config = Arr::only($config, ['visibility', 'disable_asserts', 'url']); - if ($cache) { - $adapter = new CachedAdapter($adapter, $this->createCacheStore($cache)); - } - - return new Flysystem($adapter, count($config) > 0 ? $config : null); - } - - /** - * Create a cache store instance. - * - * @param mixed $config - * @return \League\Flysystem\Cached\CacheInterface - * - * @throws \InvalidArgumentException - */ - protected function createCacheStore($config) - { - if ($config === true) { - return new MemoryStore; - } - - return new Cache( - $this->app['cache']->store($config['store']), - $config['prefix'] ?? 'flysystem', - $config['expire'] ?? null - ); - } - - /** - * Adapt the filesystem implementation. - * - * @param \League\Flysystem\FilesystemInterface $filesystem - * @return \Illuminate\Contracts\Filesystem\Filesystem - */ - protected function adapt(FilesystemInterface $filesystem) - { - return new FilesystemAdapter($filesystem); + return new Flysystem($adapter, $config); } /** diff --git a/src/Illuminate/Filesystem/composer.json b/src/Illuminate/Filesystem/composer.json index b3655d43fe02..7d10b4aeb439 100644 --- a/src/Illuminate/Filesystem/composer.json +++ b/src/Illuminate/Filesystem/composer.json @@ -34,10 +34,10 @@ "suggest": { "ext-ftp": "Required to use the Flysystem FTP driver.", "illuminate/http": "Required for handling uploaded files (^7.0).", - "league/flysystem": "Required to use the Flysystem local and FTP drivers (^1.1).", - "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^1.0).", - "league/flysystem-cached-adapter": "Required to use the Flysystem cache (^1.0).", - "league/flysystem-sftp": "Required to use the Flysystem SFTP driver (^1.0).", + "league/flysystem": "Required to use the Flysystem local driver (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^2.0).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^2.0).", + "league/flysystem-sftp": "Required to use the Flysystem SFTP driver (^2.0).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "symfony/filesystem": "Required to enable support for relative symbolic links (^5.2).", "symfony/mime": "Required to enable support for guessing extensions (^5.2)." diff --git a/tests/Filesystem/FilesystemAdapterTest.php b/tests/Filesystem/FilesystemAdapterTest.php index 8e2cac163ce0..ed35ff6bdb52 100644 --- a/tests/Filesystem/FilesystemAdapterTest.php +++ b/tests/Filesystem/FilesystemAdapterTest.php @@ -3,14 +3,15 @@ namespace Illuminate\Tests\Filesystem; use GuzzleHttp\Psr7\Stream; -use Illuminate\Contracts\Filesystem\FileExistsException; -use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Filesystem\FilesystemAdapter; +use Illuminate\Filesystem\FilesystemManager; +use Illuminate\Foundation\Application; use Illuminate\Http\UploadedFile; use Illuminate\Testing\Assert; use InvalidArgumentException; -use League\Flysystem\Adapter\Local; use League\Flysystem\Filesystem; +use League\Flysystem\Ftp\FtpAdapter; +use League\Flysystem\Local\LocalFilesystemAdapter; use Mockery as m; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -19,24 +20,31 @@ class FilesystemAdapterTest extends TestCase { private $tempDir; private $filesystem; + private $adapter; protected function setUp(): void { $this->tempDir = __DIR__.'/tmp'; - $this->filesystem = new Filesystem(new Local($this->tempDir)); + $this->filesystem = new Filesystem( + $this->adapter = new LocalFilesystemAdapter($this->tempDir) + ); } protected function tearDown(): void { - $filesystem = new Filesystem(new Local(dirname($this->tempDir))); - $filesystem->deleteDir(basename($this->tempDir)); + $filesystem = new Filesystem( + $this->adapter = new LocalFilesystemAdapter(dirname($this->tempDir)) + ); + $filesystem->deleteDirectory(basename($this->tempDir)); m::close(); + + unset($this->tempDir, $this->filesystem, $this->adapter); } public function testResponse() { $this->filesystem->write('file.txt', 'Hello World'); - $files = new FilesystemAdapter($this->filesystem); + $files = new FilesystemAdapter($this->filesystem, $this->adapter); $response = $files->response('file.txt'); ob_start(); @@ -51,7 +59,7 @@ public function testResponse() public function testDownload() { $this->filesystem->write('file.txt', 'Hello World'); - $files = new FilesystemAdapter($this->filesystem); + $files = new FilesystemAdapter($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')); @@ -60,7 +68,7 @@ public function testDownload() public function testDownloadNonAsciiFilename() { $this->filesystem->write('file.txt', 'Hello World'); - $files = new FilesystemAdapter($this->filesystem); + $files = new FilesystemAdapter($this->filesystem, $this->adapter); $response = $files->download('file.txt', 'пиздюк.txt'); $this->assertInstanceOf(StreamedResponse::class, $response); $this->assertSame("attachment; filename=pizdyuk.txt; filename*=utf-8''%D0%BF%D0%B8%D0%B7%D0%B4%D1%8E%D0%BA.txt", $response->headers->get('content-disposition')); @@ -69,7 +77,7 @@ public function testDownloadNonAsciiFilename() public function testDownloadNonAsciiEmptyFilename() { $this->filesystem->write('пиздюк.txt', 'Hello World'); - $files = new FilesystemAdapter($this->filesystem); + $files = new FilesystemAdapter($this->filesystem, $this->adapter); $response = $files->download('пиздюк.txt'); $this->assertInstanceOf(StreamedResponse::class, $response); $this->assertSame('attachment; filename=pizdyuk.txt; filename*=utf-8\'\'%D0%BF%D0%B8%D0%B7%D0%B4%D1%8E%D0%BA.txt', $response->headers->get('content-disposition')); @@ -78,7 +86,7 @@ public function testDownloadNonAsciiEmptyFilename() public function testDownloadPercentInFilename() { $this->filesystem->write('Hello%World.txt', 'Hello World'); - $files = new FilesystemAdapter($this->filesystem); + $files = new FilesystemAdapter($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')); @@ -87,40 +95,41 @@ public function testDownloadPercentInFilename() public function testExists() { $this->filesystem->write('file.txt', 'Hello World'); - $filesystemAdapter = new FilesystemAdapter($this->filesystem); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); $this->assertTrue($filesystemAdapter->exists('file.txt')); } public function testMissing() { - $filesystemAdapter = new FilesystemAdapter($this->filesystem); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); $this->assertTrue($filesystemAdapter->missing('file.txt')); } public function testPath() { $this->filesystem->write('file.txt', 'Hello World'); - $filesystemAdapter = new FilesystemAdapter($this->filesystem); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter, [ + 'root' => $this->tempDir.DIRECTORY_SEPARATOR, + ]); $this->assertEquals($this->tempDir.DIRECTORY_SEPARATOR.'file.txt', $filesystemAdapter->path('file.txt')); } public function testGet() { $this->filesystem->write('file.txt', 'Hello World'); - $filesystemAdapter = new FilesystemAdapter($this->filesystem); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); $this->assertSame('Hello World', $filesystemAdapter->get('file.txt')); } public function testGetFileNotFound() { - $filesystemAdapter = new FilesystemAdapter($this->filesystem); - $this->expectException(FileNotFoundException::class); - $filesystemAdapter->get('file.txt'); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); + $this->assertNull($filesystemAdapter->get('file.txt')); } public function testPut() { - $filesystemAdapter = new FilesystemAdapter($this->filesystem); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); $filesystemAdapter->put('file.txt', 'Something inside'); $this->assertStringEqualsFile($this->tempDir.'/file.txt', 'Something inside'); } @@ -128,7 +137,7 @@ public function testPut() public function testPrepend() { file_put_contents($this->tempDir.'/file.txt', 'World'); - $filesystemAdapter = new FilesystemAdapter($this->filesystem); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); $filesystemAdapter->prepend('file.txt', 'Hello '); $this->assertStringEqualsFile($this->tempDir.'/file.txt', 'Hello '.PHP_EOL.'World'); } @@ -136,7 +145,7 @@ public function testPrepend() public function testAppend() { file_put_contents($this->tempDir.'/file.txt', 'Hello '); - $filesystemAdapter = new FilesystemAdapter($this->filesystem); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); $filesystemAdapter->append('file.txt', 'Moon'); $this->assertStringEqualsFile($this->tempDir.'/file.txt', 'Hello '.PHP_EOL.'Moon'); } @@ -144,15 +153,15 @@ public function testAppend() public function testDelete() { file_put_contents($this->tempDir.'/file.txt', 'Hello World'); - $filesystemAdapter = new FilesystemAdapter($this->filesystem); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); $this->assertTrue($filesystemAdapter->delete('file.txt')); Assert::assertFileDoesNotExist($this->tempDir.'/file.txt'); } - public function testDeleteReturnsFalseWhenFileNotFound() + public function testDeleteReturnsTrueWhenFileNotFound() { - $filesystemAdapter = new FilesystemAdapter($this->filesystem); - $this->assertFalse($filesystemAdapter->delete('file.txt')); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); + $this->assertTrue($filesystemAdapter->delete('file.txt')); } public function testCopy() @@ -161,7 +170,7 @@ public function testCopy() mkdir($this->tempDir.'/foo'); file_put_contents($this->tempDir.'/foo/foo.txt', $data); - $filesystemAdapter = new FilesystemAdapter($this->filesystem); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); $filesystemAdapter->copy('/foo/foo.txt', '/foo/foo2.txt'); $this->assertFileExists($this->tempDir.'/foo/foo.txt'); @@ -177,7 +186,7 @@ public function testMove() mkdir($this->tempDir.'/foo'); file_put_contents($this->tempDir.'/foo/foo.txt', $data); - $filesystemAdapter = new FilesystemAdapter($this->filesystem); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); $filesystemAdapter->move('/foo/foo.txt', '/foo/foo2.txt'); Assert::assertFileDoesNotExist($this->tempDir.'/foo/foo.txt'); @@ -189,7 +198,7 @@ public function testMove() public function testStream() { $this->filesystem->write('file.txt', $original_content = 'Hello World'); - $filesystemAdapter = new FilesystemAdapter($this->filesystem); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); $readStream = $filesystemAdapter->readStream('file.txt'); $filesystemAdapter->writeStream('copy.txt', $readStream); $this->assertEquals($original_content, $filesystemAdapter->get('copy.txt')); @@ -197,51 +206,48 @@ public function testStream() public function testStreamBetweenFilesystems() { - $secondFilesystem = new Filesystem(new Local($this->tempDir.'/second')); + $secondFilesystem = new Filesystem(new LocalFilesystemAdapter($this->tempDir.'/second')); $this->filesystem->write('file.txt', $original_content = 'Hello World'); - $filesystemAdapter = new FilesystemAdapter($this->filesystem); - $secondFilesystemAdapter = new FilesystemAdapter($secondFilesystem); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); + $secondFilesystemAdapter = new FilesystemAdapter($secondFilesystem, $this->adapter); $readStream = $filesystemAdapter->readStream('file.txt'); $secondFilesystemAdapter->writeStream('copy.txt', $readStream); $this->assertEquals($original_content, $secondFilesystemAdapter->get('copy.txt')); } - public function testStreamToExistingFileThrows() + public function testStreamToExistingFileOverwrites() { - $this->expectException(FileExistsException::class); $this->filesystem->write('file.txt', 'Hello World'); $this->filesystem->write('existing.txt', 'Dear Kate'); - $filesystemAdapter = new FilesystemAdapter($this->filesystem); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); $readStream = $filesystemAdapter->readStream('file.txt'); $filesystemAdapter->writeStream('existing.txt', $readStream); + $this->assertSame('Hello World', $filesystemAdapter->read('existing.txt')); } - public function testReadStreamNonExistentFileThrows() + public function testReadStreamNonExistentFileReturnsNull() { - $this->expectException(FileNotFoundException::class); - $filesystemAdapter = new FilesystemAdapter($this->filesystem); - $filesystemAdapter->readStream('nonexistent.txt'); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); + $this->assertNull($filesystemAdapter->readStream('nonexistent.txt')); } public function testStreamInvalidResourceThrows() { $this->expectException(InvalidArgumentException::class); - $filesystemAdapter = new FilesystemAdapter($this->filesystem); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); $filesystemAdapter->writeStream('file.txt', 'foo bar'); } public function testPutWithStreamInterface() { file_put_contents($this->tempDir.'/foo.txt', 'some-data'); - $spy = m::spy($this->filesystem); - $filesystemAdapter = new FilesystemAdapter($spy); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); $stream = fopen($this->tempDir.'/foo.txt', 'r'); $guzzleStream = new Stream($stream); $filesystemAdapter->put('bar.txt', $guzzleStream); fclose($stream); - $spy->shouldHaveReceived('putStream'); $this->assertSame('some-data', $filesystemAdapter->get('bar.txt')); } @@ -249,7 +255,7 @@ public function testPutFileAs() { file_put_contents($filePath = $this->tempDir.'/foo.txt', 'uploaded file content'); - $filesystemAdapter = new FilesystemAdapter($this->filesystem); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); $uploadedFile = new UploadedFile($filePath, 'org.txt', null, null, true); @@ -273,7 +279,7 @@ public function testPutFileAsWithAbsoluteFilePath() { file_put_contents($filePath = $this->tempDir.'/foo.txt', 'normal file content'); - $filesystemAdapter = new FilesystemAdapter($this->filesystem); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); $storagePath = $filesystemAdapter->putFileAs('/', $filePath, 'new.txt'); @@ -284,7 +290,7 @@ public function testPutFile() { file_put_contents($filePath = $this->tempDir.'/foo.txt', 'uploaded file content'); - $filesystemAdapter = new FilesystemAdapter($this->filesystem); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); $uploadedFile = new UploadedFile($filePath, 'org.txt', null, null, true); @@ -306,7 +312,7 @@ public function testPutFileWithAbsoluteFilePath() { file_put_contents($filePath = $this->tempDir.'/foo.txt', 'uploaded file content'); - $filesystemAdapter = new FilesystemAdapter($this->filesystem); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); $storagePath = $filesystemAdapter->putFile('/', $filePath); @@ -319,4 +325,26 @@ public function testPutFileWithAbsoluteFilePath() 'uploaded file content' ); } + + /** + * @requires extension ftp + */ + public function testCreateFtpDriver() + { + $filesystem = new FilesystemManager(new Application); + + $driver = $filesystem->createFtpDriver([ + 'host' => 'ftp.example.com', + 'username' => 'admin', + 'permPublic' => 0700, + 'unsupportedParam' => true, + ]); + + $this->assertInstanceOf(FtpAdapter::class, $driver->getAdapter()); + + $config = $driver->getConfig(); + $this->assertEquals(0700, $config['permPublic']); + $this->assertSame('ftp.example.com', $config['host']); + $this->assertSame('admin', $config['username']); + } } diff --git a/tests/Filesystem/FilesystemTest.php b/tests/Filesystem/FilesystemTest.php index d1ff89f680ce..eb2a156687eb 100755 --- a/tests/Filesystem/FilesystemTest.php +++ b/tests/Filesystem/FilesystemTest.php @@ -4,8 +4,6 @@ use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Filesystem\Filesystem; -use Illuminate\Filesystem\FilesystemManager; -use Illuminate\Foundation\Application; use Illuminate\Support\LazyCollection; use Illuminate\Testing\Assert; use Mockery as m; @@ -568,27 +566,6 @@ public function testAllFilesReturnsFileInfoObjects() $this->assertContainsOnlyInstancesOf(SplFileInfo::class, $files->allFiles(self::$tempDir)); } - /** - * @requires extension ftp - */ - public function testCreateFtpDriver() - { - $filesystem = new FilesystemManager(new Application); - - $driver = $filesystem->createFtpDriver([ - 'host' => 'ftp.example.com', - 'username' => 'admin', - 'permPublic' => 0700, - 'unsupportedParam' => true, - ]); - - /** @var \League\Flysystem\Adapter\Ftp $adapter */ - $adapter = $driver->getAdapter(); - $this->assertEquals(0700, $adapter->getPermPublic()); - $this->assertSame('ftp.example.com', $adapter->getHost()); - $this->assertSame('admin', $adapter->getUsername()); - } - public function testHash() { file_put_contents(self::$tempDir.'/foo.txt', 'foo');