diff --git a/composer.json b/composer.json index 81a39c4ed..77e41a3d6 100644 --- a/composer.json +++ b/composer.json @@ -17,8 +17,8 @@ } }, "require": { - "php": "^8.0.2", - "league/flysystem-local": "^3.0.0", + "php": "^8.1", + "league/flysystem-local": "^4.0.0", "league/mime-type-detection": "^1.0.0" }, "require-dev": { @@ -61,7 +61,7 @@ "type": "package", "package": { "name": "league/flysystem-local", - "version": "3.0.0", + "version": "4.0.0", "dist": { "type": "path", "url": "src/Local" diff --git a/src/AdapterTestUtilities/FilesystemAdapterTestCase.php b/src/AdapterTestUtilities/FilesystemAdapterTestCase.php index 25fc93cea..c9555916e 100644 --- a/src/AdapterTestUtilities/FilesystemAdapterTestCase.php +++ b/src/AdapterTestUtilities/FilesystemAdapterTestCase.php @@ -4,6 +4,7 @@ namespace League\Flysystem\AdapterTestUtilities; +use function json_encode; use const PHP_EOL; use DateInterval; use DateTimeImmutable; @@ -225,6 +226,35 @@ public function writing_a_file_with_an_empty_stream(): void }); } + /** + * @test + */ + public function listing_metadata_for_a_file(): void + { + $this->givenWeHaveAnExistingFile('something.csv', ''); + + $metadata = $this->adapter()->metadata('something.csv', new Config); + + $this->assertFalse($metadata->isDir()); + $this->assertTrue($metadata->isFile()); + } + + /** + * @test + */ + public function retrieving_metadata_for_a_directory(): void + { + $this->runScenario(function () { + $adapter = $this->adapter(); + $adapter->createDirectory('somewhere/here/', new Config()); + + $metadata = $adapter->metadata('somewhere/here/', new Config()); + + $this->assertTrue($metadata->isDir()); + $this->assertFalse($metadata->isFile()); + }); + } + /** * @test */ @@ -539,7 +569,7 @@ public function listing_a_toplevel_directory(): void $this->runScenario(function () { $contents = iterator_to_array($this->adapter()->listContents('', true)); - $this->assertCount(2, $contents); + $this->assertCount(2, $contents, json_encode($contents)); }); } diff --git a/src/AdapterTestUtilities/RetryOnTestException.php b/src/AdapterTestUtilities/RetryOnTestException.php index 1b52fc51f..f4fe2985f 100644 --- a/src/AdapterTestUtilities/RetryOnTestException.php +++ b/src/AdapterTestUtilities/RetryOnTestException.php @@ -4,6 +4,10 @@ namespace League\Flysystem\AdapterTestUtilities; +use function debug_backtrace; +use function fwrite; +use function get_class; +use const DEBUG_BACKTRACE_IGNORE_ARGS; use const PHP_EOL; use const STDOUT; use League\Flysystem\FilesystemException; @@ -68,6 +72,9 @@ protected function runScenario(callable $scenario): void return; } + $prevCaller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS|DEBUG_BACKTRACE_PROVIDE_OBJECT)[1]; + $className = get_class($prevCaller['object']); + $caller = "$className::{$prevCaller['function']}"; $firstTryAt = \time(); $lastTryAt = $firstTryAt + 60; @@ -80,7 +87,8 @@ protected function runScenario(callable $scenario): void if ( ! $exception instanceof $this->exceptionTypeToRetryOn) { throw $exception; } - fwrite(STDOUT, 'Retrying ...' . PHP_EOL); + fwrite(STDOUT, $exception . PHP_EOL); + fwrite(STDOUT, "Retrying $caller..." . PHP_EOL); sleep($this->timeoutForExceptionRetry); } } diff --git a/src/AdapterTestUtilities/composer.json b/src/AdapterTestUtilities/composer.json index cd8c00f13..f20178635 100644 --- a/src/AdapterTestUtilities/composer.json +++ b/src/AdapterTestUtilities/composer.json @@ -13,7 +13,7 @@ ] }, "require": { - "php": "^8.0.2", + "php": "^8.1", "league/flysystem": "^4.0.0" }, "license": "MIT", diff --git a/src/AsyncAwsS3/AsyncAwsS3Adapter.php b/src/AsyncAwsS3/AsyncAwsS3Adapter.php index ab52be10d..bb4d9f255 100644 --- a/src/AsyncAwsS3/AsyncAwsS3Adapter.php +++ b/src/AsyncAwsS3/AsyncAwsS3Adapter.php @@ -281,6 +281,20 @@ public function directoryExists(string $path): bool } } + public function metadata(string $path, Config $config): StorageAttributes + { + $arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)]; + + try { + $result = $this->client->headObject($arguments); + $result->resolve(); + } catch (Throwable $exception) { + throw UnableToRetrieveMetadata::create($path, StorageAttributes::ATTRIBUTE_METADATA, $exception->getMessage(), $exception); + } + + return $this->mapS3ObjectMetadata($result, $path); + } + public function listContents(string $path, bool $deep): iterable { $path = trim($path, '/'); diff --git a/src/AsyncAwsS3/composer.json b/src/AsyncAwsS3/composer.json index 6e48948fe..eddb7a4dd 100644 --- a/src/AsyncAwsS3/composer.json +++ b/src/AsyncAwsS3/composer.json @@ -9,7 +9,7 @@ } }, "require": { - "php": "^8.0.2", + "php": "^8.1", "league/flysystem": "^4.0.0", "league/mime-type-detection": "^1.0.0", "async-aws/s3": "^1.5 || ^2.0" diff --git a/src/AwsS3V3/AwsS3V3Adapter.php b/src/AwsS3V3/AwsS3V3Adapter.php index c0203352d..025e4164b 100644 --- a/src/AwsS3V3/AwsS3V3Adapter.php +++ b/src/AwsS3V3/AwsS3V3Adapter.php @@ -286,16 +286,7 @@ public function visibility(string $path): FileAttributes private function fetchFileMetadata(string $path, string $type): FileAttributes { - $arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)]; - $command = $this->client->getCommand('HeadObject', $arguments); - - try { - $result = $this->client->execute($command); - } catch (Throwable $exception) { - throw UnableToRetrieveMetadata::create($path, $type, '', $exception); - } - - $attributes = $this->mapS3ObjectMetadata($result->toArray(), $path); + $attributes = $this->fetchMetadata($path, $type); if ( ! $attributes instanceof FileAttributes) { throw UnableToRetrieveMetadata::create($path, $type, ''); @@ -372,6 +363,25 @@ public function fileSize(string $path): FileAttributes return $attributes; } + public function metadata(string $path, Config $config): StorageAttributes + { + return $this->fetchMetadata($path, StorageAttributes::ATTRIBUTE_METADATA); + } + + private function fetchMetadata(string $path, string $type): StorageAttributes + { + $arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)]; + $command = $this->client->getCommand('HeadObject', $arguments); + + try { + $result = $this->client->execute($command); + } catch (Throwable $exception) { + throw UnableToRetrieveMetadata::create($path, $type, '', $exception); + } + + return $this->mapS3ObjectMetadata($result->toArray(), $path); + } + public function listContents(string $path, bool $deep): iterable { $prefix = trim($this->prefixer->prefixPath($path), '/'); diff --git a/src/AwsS3V3/composer.json b/src/AwsS3V3/composer.json index dbead73b1..7706bae1d 100644 --- a/src/AwsS3V3/composer.json +++ b/src/AwsS3V3/composer.json @@ -9,7 +9,7 @@ } }, "require": { - "php": "^8.0.2", + "php": "^8.1", "league/flysystem": "^4.0.0", "league/mime-type-detection": "^1.0.0", "aws/aws-sdk-php": "^3.295.10" diff --git a/src/AzureBlobStorage/AzureBlobStorageAdapter.php b/src/AzureBlobStorage/AzureBlobStorageAdapter.php index c29b65c68..2be39fffc 100644 --- a/src/AzureBlobStorage/AzureBlobStorageAdapter.php +++ b/src/AzureBlobStorage/AzureBlobStorageAdapter.php @@ -13,6 +13,7 @@ use League\Flysystem\FileAttributes; use League\Flysystem\FilesystemAdapter; use League\Flysystem\PathPrefixer; +use League\Flysystem\StorageAttributes; use League\Flysystem\UnableToCheckDirectoryExistence; use League\Flysystem\UnableToCheckFileExistence; use League\Flysystem\UnableToCopyFile; @@ -347,6 +348,11 @@ public function publicUrl(string $path, Config $config): string return $this->client->getBlobUrl($this->container, $location); } + public function metadata(string $path, Config $config): StorageAttributes + { + return $this->fetchMetadata($this->prefixer->prefixPath($path)); + } + public function checksum(string $path, Config $config): string { $algo = $config->get('checksum_algo', 'md5'); diff --git a/src/AzureBlobStorage/composer.json b/src/AzureBlobStorage/composer.json index 2120a4d42..b03375efc 100644 --- a/src/AzureBlobStorage/composer.json +++ b/src/AzureBlobStorage/composer.json @@ -6,7 +6,7 @@ } }, "require": { - "php": "^8.0.2", + "php": "^8.1", "league/flysystem": "^4.0.0", "microsoft/azure-storage-blob": "^1.1" }, diff --git a/src/DecoratedAdapter.php b/src/DecoratedAdapter.php index 6428d1cb2..921d663be 100644 --- a/src/DecoratedAdapter.php +++ b/src/DecoratedAdapter.php @@ -20,6 +20,11 @@ public function directoryExists(string $path): bool return $this->adapter->directoryExists($path); } + public function metadata(string $path, Config $config): StorageAttributes + { + return $this->adapter->metadata($path, $config); + } + public function write(string $path, string $contents, Config $config): void { $this->adapter->write($path, $contents, $config); diff --git a/src/Filesystem.php b/src/Filesystem.php index a23399ed7..448c9df16 100644 --- a/src/Filesystem.php +++ b/src/Filesystem.php @@ -181,9 +181,12 @@ public function visibility(string $path): string return $this->adapter->visibility($this->pathNormalizer->normalizePath($path))->visibility(); } - public function metadata(string $path): StorageAttributes + public function metadata(string $path, array $config = []): StorageAttributes { - return $this->adapter->metadata($this->pathNormalizer->normalizePath($path)); + return $this->adapter->metadata( + $this->pathNormalizer->normalizePath($path), + $this->config->extend($config), + ); } public function publicUrl(string $path, array $config = []): string diff --git a/src/FilesystemAdapter.php b/src/FilesystemAdapter.php index c43fd231b..dc17db1d6 100644 --- a/src/FilesystemAdapter.php +++ b/src/FilesystemAdapter.php @@ -98,7 +98,7 @@ public function fileSize(string $path): FileAttributes; * @throws UnableToRetrieveMetadata * @throws FilesystemException */ - public function metadata(string $path): StorageAttributes; + public function metadata(string $path, Config $config): StorageAttributes; /** * @return iterable diff --git a/src/FilesystemReader.php b/src/FilesystemReader.php index ababd5dc6..883297e23 100644 --- a/src/FilesystemReader.php +++ b/src/FilesystemReader.php @@ -83,7 +83,7 @@ public function visibility(string $path): string; * @throws UnableToRetrieveMetadata * @throws FilesystemException */ - public function metadata(string $path): StorageAttributes; + public function metadata(string $path, array $config = []): StorageAttributes; /** * @throws UnableToGeneratePublicUrl diff --git a/src/Ftp/ConnectionProvider.php b/src/Ftp/ConnectionProvider.php index 608bade2d..6ad9d772e 100644 --- a/src/Ftp/ConnectionProvider.php +++ b/src/Ftp/ConnectionProvider.php @@ -4,10 +4,9 @@ namespace League\Flysystem\Ftp; +use FTP\Connection; + interface ConnectionProvider { - /** - * @return resource - */ - public function createConnection(FtpConnectionOptions $options); + public function createConnection(FtpConnectionOptions $options): Connection; } diff --git a/src/Ftp/ConnectivityChecker.php b/src/Ftp/ConnectivityChecker.php index ca5a8407c..8d4ca23d0 100644 --- a/src/Ftp/ConnectivityChecker.php +++ b/src/Ftp/ConnectivityChecker.php @@ -4,10 +4,9 @@ namespace League\Flysystem\Ftp; +use FTP\Connection; + interface ConnectivityChecker { - /** - * @param resource $connection - */ - public function isConnected($connection): bool; + public function isConnected(Connection $connection): bool; } diff --git a/src/Ftp/ConnectivityCheckerThatCanFail.php b/src/Ftp/ConnectivityCheckerThatCanFail.php index 3bdbf264d..c57eccf48 100644 --- a/src/Ftp/ConnectivityCheckerThatCanFail.php +++ b/src/Ftp/ConnectivityCheckerThatCanFail.php @@ -4,6 +4,8 @@ namespace League\Flysystem\Ftp; +use FTP\Connection; + class ConnectivityCheckerThatCanFail implements ConnectivityChecker { private bool $failNextCall = false; @@ -17,10 +19,7 @@ public function failNextCall(): void $this->failNextCall = true; } - /** - * @inheritDoc - */ - public function isConnected($connection): bool + public function isConnected(Connection $connection): bool { if ($this->failNextCall) { $this->failNextCall = false; diff --git a/src/Ftp/FtpAdapter.php b/src/Ftp/FtpAdapter.php index b15add8dc..12ae573a6 100644 --- a/src/Ftp/FtpAdapter.php +++ b/src/Ftp/FtpAdapter.php @@ -5,6 +5,7 @@ namespace League\Flysystem\Ftp; use DateTime; +use FTP\Connection; use Generator; use League\Flysystem\Config; use League\Flysystem\DirectoryAttributes; @@ -33,6 +34,8 @@ use function ftp_chdir; use function ftp_close; use function is_string; +use function preg_match; +use function var_dump; class FtpAdapter implements FilesystemAdapter { @@ -43,7 +46,7 @@ class FtpAdapter implements FilesystemAdapter private ConnectivityChecker $connectivityChecker; /** - * @var resource|false|\FTP\Connection + * @var false|\FTP\Connection */ private mixed $connection = false; private PathPrefixer $prefixer; @@ -79,10 +82,7 @@ public function __destruct() $this->disconnect(); } - /** - * @return resource - */ - private function connection() + private function connection(): Connection { start: if ( ! $this->hasFtpConnection()) { @@ -347,17 +347,34 @@ public function fileSize(string $path): FileAttributes return new FileAttributes($path, $fileSize); } + public function metadata(string $path, Config $config): StorageAttributes + { + $location = $this->prefixer()->prefixPath($path); + $connection = $this->connection(); + if ($path === '' || @ftp_chdir($connection, $location) === true) { + return new DirectoryAttributes($path); + } + + $object = ftp_raw($connection, 'STAT ' . $this->escapePath($location)); + + if ( ! $object || count($object) < 3 || str_starts_with($object[1], 'ftpd:')) { + throw UnableToRetrieveMetadata::metadata($path, 'No result found'); + } + + return $this->normalizeObject($object[1], ''); + } + public function listContents(string $path, bool $deep): iterable { $path = ltrim($path, '/'); $path = $path === '' ? $path : trim($path, '/') . '/'; if ($deep && $this->connectionOptions->recurseManually()) { - yield from $this->listDirectoryContentsRecursive($path); + yield from $this->listDirectoryContentsRecursive($path, $this->connection()); } else { $location = $this->prefixer()->prefixPath($path); $options = $deep ? '-alnR' : '-aln'; - $listing = $this->ftpRawlist($options, $location); + $listing = $this->ftpRawlist($options, $location, $this->connection()); yield from $this->normalizeListing($listing, $path); } } @@ -499,10 +516,13 @@ private function normalizePermissions(string $permissions): int return octdec(implode('', array_map($mapper, $parts))); } - private function listDirectoryContentsRecursive(string $directory): Generator + /** + * @param resource|Connection $connection + */ + private function listDirectoryContentsRecursive(string $directory, $connection): Generator { $location = $this->prefixer()->prefixPath($directory); - $listing = $this->ftpRawlist('-aln', $location); + $listing = $this->ftpRawlist('-aln', $location, $connection); /** @var StorageAttributes[] $listing */ $listing = $this->normalizeListing($listing, $directory); @@ -513,7 +533,7 @@ private function listDirectoryContentsRecursive(string $directory): Generator continue; } - $children = $this->listDirectoryContentsRecursive($item->path()); + $children = $this->listDirectoryContentsRecursive($item->path(), $connection); foreach ($children as $child) { yield $child; @@ -521,10 +541,9 @@ private function listDirectoryContentsRecursive(string $directory): Generator } } - private function ftpRawlist(string $options, string $path): array + private function ftpRawlist(string $options, string $path, Connection $connection): array { $path = rtrim($path, '/') . '/'; - $connection = $this->connection(); if ($this->isPureFtpdServer()) { $path = str_replace(' ', '\ ', $path); diff --git a/src/Ftp/FtpConnectionProvider.php b/src/Ftp/FtpConnectionProvider.php index ff13f17fe..94f9fd6c0 100644 --- a/src/Ftp/FtpConnectionProvider.php +++ b/src/Ftp/FtpConnectionProvider.php @@ -4,6 +4,7 @@ namespace League\Flysystem\Ftp; +use FTP\Connection; use const FTP_USEPASVADDRESS; use function error_clear_last; use function error_get_last; @@ -11,11 +12,9 @@ class FtpConnectionProvider implements ConnectionProvider { /** - * @return resource - * * @throws FtpConnectionException */ - public function createConnection(FtpConnectionOptions $options) + public function createConnection(FtpConnectionOptions $options): Connection { $connection = $this->createConnectionResource( $options->host(), @@ -37,10 +36,7 @@ public function createConnection(FtpConnectionOptions $options) return $connection; } - /** - * @return resource - */ - private function createConnectionResource(string $host, int $port, int $timeout, bool $ssl) + private function createConnectionResource(string $host, int $port, int $timeout, bool $ssl): Connection { error_clear_last(); $connection = $ssl ? @ftp_ssl_connect($host, $port, $timeout) : @ftp_connect($host, $port, $timeout); @@ -52,20 +48,14 @@ private function createConnectionResource(string $host, int $port, int $timeout, return $connection; } - /** - * @param resource $connection - */ - private function authenticate(FtpConnectionOptions $options, $connection): void + private function authenticate(FtpConnectionOptions $options, Connection $connection): void { if ( ! @ftp_login($connection, $options->username(), $options->password())) { throw new UnableToAuthenticate(); } } - /** - * @param resource $connection - */ - private function enableUtf8Mode(FtpConnectionOptions $options, $connection): void + private function enableUtf8Mode(FtpConnectionOptions $options, Connection $connection): void { if ( ! $options->utf8()) { return; @@ -80,10 +70,7 @@ private function enableUtf8Mode(FtpConnectionOptions $options, $connection): voi } } - /** - * @param resource $connection - */ - private function ignorePassiveAddress(FtpConnectionOptions $options, $connection): void + private function ignorePassiveAddress(FtpConnectionOptions $options, Connection $connection): void { $ignorePassiveAddress = $options->ignorePassiveAddress(); @@ -96,10 +83,7 @@ private function ignorePassiveAddress(FtpConnectionOptions $options, $connection } } - /** - * @param resource $connection - */ - private function makeConnectionPassive(FtpConnectionOptions $options, $connection): void + private function makeConnectionPassive(FtpConnectionOptions $options, Connection $connection): void { if ( ! @ftp_pasv($connection, $options->passive())) { throw new UnableToMakeConnectionPassive( diff --git a/src/Ftp/NoopCommandConnectivityChecker.php b/src/Ftp/NoopCommandConnectivityChecker.php index fe438223f..36e58bd96 100644 --- a/src/Ftp/NoopCommandConnectivityChecker.php +++ b/src/Ftp/NoopCommandConnectivityChecker.php @@ -4,17 +4,18 @@ namespace League\Flysystem\Ftp; +use FTP\Connection; use TypeError; use ValueError; class NoopCommandConnectivityChecker implements ConnectivityChecker { - public function isConnected($connection): bool + public function isConnected(Connection $connection): bool { // @codeCoverageIgnoreStart try { $response = @ftp_raw($connection, 'NOOP'); - } catch (TypeError | ValueError $typeError) { + } catch (TypeError | ValueError) { return false; } // @codeCoverageIgnoreEnd diff --git a/src/Ftp/RawListFtpConnectivityChecker.php b/src/Ftp/RawListFtpConnectivityChecker.php index 2fdeeaa28..df137606a 100644 --- a/src/Ftp/RawListFtpConnectivityChecker.php +++ b/src/Ftp/RawListFtpConnectivityChecker.php @@ -4,6 +4,7 @@ namespace League\Flysystem\Ftp; +use FTP\Connection; use ValueError; class RawListFtpConnectivityChecker implements ConnectivityChecker @@ -11,11 +12,11 @@ class RawListFtpConnectivityChecker implements ConnectivityChecker /** * @inheritDoc */ - public function isConnected($connection): bool + public function isConnected(Connection $connection): bool { try { return $connection !== false && @ftp_rawlist($connection, './') !== false; - } catch (ValueError $errror) { + } catch (ValueError) { return false; } } diff --git a/src/Ftp/StubConnectionProvider.php b/src/Ftp/StubConnectionProvider.php index f7c2ae075..8ba077cdd 100644 --- a/src/Ftp/StubConnectionProvider.php +++ b/src/Ftp/StubConnectionProvider.php @@ -3,6 +3,8 @@ namespace League\Flysystem\Ftp; +use FTP\Connection; + class StubConnectionProvider implements ConnectionProvider { public mixed $connection; @@ -11,7 +13,7 @@ public function __construct(private ConnectionProvider $provider) { } - public function createConnection(FtpConnectionOptions $options) + public function createConnection(FtpConnectionOptions $options): Connection { return $this->connection = $this->provider->createConnection($options); } diff --git a/src/Ftp/composer.json b/src/Ftp/composer.json index ad18e8943..6b7a022ad 100644 --- a/src/Ftp/composer.json +++ b/src/Ftp/composer.json @@ -10,7 +10,7 @@ } }, "require": { - "php": "^8.0.2", + "php": "^8.1", "ext-ftp": "*", "league/flysystem": "^4.0.0", "league/mime-type-detection": "^1.0.0" diff --git a/src/GoogleCloudStorage/GoogleCloudStorageAdapter.php b/src/GoogleCloudStorage/GoogleCloudStorageAdapter.php index b180f72b5..3347a7c74 100644 --- a/src/GoogleCloudStorage/GoogleCloudStorageAdapter.php +++ b/src/GoogleCloudStorage/GoogleCloudStorageAdapter.php @@ -300,6 +300,17 @@ public function storageObjectToStorageAttributes(StorageObject $object): Storage return new FileAttributes($path, $fileSize, null, $lastModified, $mimeType, $info); } + public function metadata(string $path, Config $config): StorageAttributes + { + $prefixedPath = $this->prefixer->prefixPath($path); + + try { + return $this->storageObjectToStorageAttributes($this->bucket->object($prefixedPath)); + } catch (Throwable $exception) { + throw UnableToRetrieveMetadata::metadata($path, $exception->getMessage(), $exception); + } + } + public function listContents(string $path, bool $deep): iterable { $prefixedPath = $this->prefixer->prefixPath($path); diff --git a/src/GoogleCloudStorage/GoogleCloudStorageAdapterTest.php b/src/GoogleCloudStorage/GoogleCloudStorageAdapterTest.php index 483bf5486..4b8ffea0a 100644 --- a/src/GoogleCloudStorage/GoogleCloudStorageAdapterTest.php +++ b/src/GoogleCloudStorage/GoogleCloudStorageAdapterTest.php @@ -28,7 +28,7 @@ class GoogleCloudStorageAdapterTest extends FilesystemAdapterTestCase public static function setUpBeforeClass(): void { - static::$adapterPrefix = 'frank-ci'; // . bin2hex(random_bytes(10)); + static::$adapterPrefix = 'frank-ci/' . bin2hex(random_bytes(10)); static::$prefixer = new PathPrefixer(static::$adapterPrefix); } diff --git a/src/GoogleCloudStorage/composer.json b/src/GoogleCloudStorage/composer.json index d79f87d6d..7379e1b12 100644 --- a/src/GoogleCloudStorage/composer.json +++ b/src/GoogleCloudStorage/composer.json @@ -10,7 +10,7 @@ } }, "require": { - "php": "^8.0.2", + "php": "^8.1", "google/cloud-storage": "^1.23", "league/flysystem": "^4.0.0", "league/mime-type-detection": "^1.0.0" diff --git a/src/GridFS/GridFSAdapter.php b/src/GridFS/GridFSAdapter.php index 83a103fac..382d45759 100644 --- a/src/GridFS/GridFSAdapter.php +++ b/src/GridFS/GridFSAdapter.php @@ -9,6 +9,7 @@ use League\Flysystem\FileAttributes; use League\Flysystem\FilesystemAdapter; use League\Flysystem\PathPrefixer; +use League\Flysystem\StorageAttributes; use League\Flysystem\UnableToCopyFile; use League\Flysystem\UnableToCreateDirectory; use League\Flysystem\UnableToDeleteDirectory; @@ -277,6 +278,21 @@ public function lastModified(string $path): FileAttributes return $this->mapFileAttributes($file); } + public function metadata(string $path, Config $config): StorageAttributes + { + $file = $this->findFile($path); + + if ($file !== null) { + return $this->mapFileAttributes($file); + } + + if ($this->directoryExists($path)) { + return new DirectoryAttributes($path); + } + + throw UnableToRetrieveMetadata::metadata($path, 'file does not exist'); + } + public function listContents(string $path, bool $deep): iterable { $path = $this->prefixer->prefixDirectoryPath($path); diff --git a/src/GridFS/GridFSAdapterTest.php b/src/GridFS/GridFSAdapterTest.php index cf5a97a0a..f595d6b5f 100644 --- a/src/GridFS/GridFSAdapterTest.php +++ b/src/GridFS/GridFSAdapterTest.php @@ -16,6 +16,7 @@ use MongoDB\Client; use MongoDB\Database; use function getenv; +use function in_array; /** * @group gridfs @@ -29,6 +30,15 @@ class GridFSAdapterTest extends TestCase */ private static $adapterPrefix = 'test-prefix'; + protected function setUp(): void + { + if (!in_array('mongodb', get_loaded_extensions())) { + $this->markTestSkipped('No MongoDB extension found.'); + } + + parent::setUp(); // TODO: Change the autogenerated stub + } + public static function tearDownAfterClass(): void { self::getDatabase()->drop(); diff --git a/src/GridFS/composer.json b/src/GridFS/composer.json index f07098866..a1fab6377 100644 --- a/src/GridFS/composer.json +++ b/src/GridFS/composer.json @@ -6,7 +6,7 @@ } }, "require": { - "php": "^8.0.2", + "php": "^8.1", "ext-mongodb": "^1.3", "league/flysystem": "^3.10.0", "mongodb/mongodb": "^1.2" diff --git a/src/InMemory/InMemoryFilesystemAdapter.php b/src/InMemory/InMemoryFilesystemAdapter.php index fb5469eba..9ae01dee7 100644 --- a/src/InMemory/InMemoryFilesystemAdapter.php +++ b/src/InMemory/InMemoryFilesystemAdapter.php @@ -8,6 +8,7 @@ use League\Flysystem\DirectoryAttributes; use League\Flysystem\FileAttributes; use League\Flysystem\FilesystemAdapter; +use League\Flysystem\StorageAttributes; use League\Flysystem\UnableToCopyFile; use League\Flysystem\UnableToMoveFile; use League\Flysystem\UnableToReadFile; @@ -17,8 +18,11 @@ use League\MimeTypeDetection\FinfoMimeTypeDetector; use League\MimeTypeDetection\MimeTypeDetector; +use function array_key_exists; use function array_keys; +use function ltrim; use function rtrim; +use function trim; class InMemoryFilesystemAdapter implements FilesystemAdapter { @@ -177,6 +181,28 @@ public function fileSize(string $path): FileAttributes return new FileAttributes($path, $this->files[$path]->fileSize()); } + public function metadata(string $path, Config $config): StorageAttributes + { + $location = $this->preparePath($path); + + if (array_key_exists($location, $this->files)) { + $file = $this->files[$location]; + + return new FileAttributes(ltrim($location, '/'), $file->fileSize(), $file->visibility(), $file->lastModified(), $file->mimeType()); + } + + $dirPath = rtrim($location, '/') . '/'; + $paths = array_keys($this->files); + + foreach ($paths as $storedPath) { + if (str_starts_with($storedPath, $location)) { + return new DirectoryAttributes(trim($dirPath, '/')); + } + } + + throw UnableToRetrieveMetadata::metadata($path, 'file does not exist'); + } + public function listContents(string $path, bool $deep): iterable { $prefix = rtrim($this->preparePath($path), '/') . '/'; diff --git a/src/InMemory/composer.json b/src/InMemory/composer.json index 828e01ba9..5eb0c61e3 100644 --- a/src/InMemory/composer.json +++ b/src/InMemory/composer.json @@ -11,7 +11,7 @@ } }, "require": { - "php": "^8.0.2", + "php": "^8.1", "ext-fileinfo": "*", "league/flysystem": "^4.0.0" }, diff --git a/src/Local/LocalFilesystemAdapter.php b/src/Local/LocalFilesystemAdapter.php index 5aed4ec3e..8d786dd03 100644 --- a/src/Local/LocalFilesystemAdapter.php +++ b/src/Local/LocalFilesystemAdapter.php @@ -9,6 +9,7 @@ use function sprintf; use function str_replace; use function substr; +use function var_dump; use const DIRECTORY_SEPARATOR; use const LOCK_EX; use DirectoryIterator; @@ -139,7 +140,7 @@ private function writeToFile(string $path, $contents, Config $config): void } } - public function metadata(string $path): StorageAttributes + public function metadata(string $path, Config $config): StorageAttributes { $location = $this->prefixer->prefixPath($path); @@ -224,7 +225,11 @@ public function listContents(string $path, bool $deep): iterable foreach ($iterator as $fileInfo) { try { - yield $this->mapFileInfo($fileInfo); + $item = $this->mapFileInfo($fileInfo); + + if ($item !== false) { + yield $item; + } } catch (Throwable $exception) { if (file_exists($fileInfo->getFilename())) { throw $exception; @@ -235,6 +240,7 @@ public function listContents(string $path, bool $deep): iterable private function mapFileInfo(SplFileInfo $fileInfo): StorageAttributes | false { $pathName = $fileInfo->getPathname(); + var_dump($fileInfo->isLink(), $this->linkHandling & self::SKIP_LINKS); if ($fileInfo->isLink()) { if ($this->linkHandling & self::SKIP_LINKS) { diff --git a/src/Local/LocalFilesystemAdapterTest.php b/src/Local/LocalFilesystemAdapterTest.php index 1893a9801..beb125947 100644 --- a/src/Local/LocalFilesystemAdapterTest.php +++ b/src/Local/LocalFilesystemAdapterTest.php @@ -111,21 +111,6 @@ public function writing_a_file(): void $this->assertEquals('contents', $contents); } - /** - * @test - */ - public function listing_metadata_for_a_file(): void - { - $now = \time(); - $this->givenWeHaveAnExistingFile('something.csv', ''); - - $metadata = $this->adapter()->metadata('something.csv'); - - $this->assertGreaterThanOrEqual($now, $metadata->lastModified()); - $this->assertFalse($metadata->isDir()); - $this->assertTrue($metadata->isFile()); - } - /** * @test */ diff --git a/src/Local/composer.json b/src/Local/composer.json index b59f6a914..9193b06fc 100644 --- a/src/Local/composer.json +++ b/src/Local/composer.json @@ -10,7 +10,7 @@ } }, "require": { - "php": "^8.0.2", + "php": "^8.1", "ext-fileinfo": "*", "league/flysystem": "^4.0.0", "league/mime-type-detection": "^1.0.0" diff --git a/src/MountManager.php b/src/MountManager.php index acab4a80a..05ba3020d 100644 --- a/src/MountManager.php +++ b/src/MountManager.php @@ -287,10 +287,6 @@ public function publicUrl(string $path, array $config = []): string /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($path); - if ( ! method_exists($filesystem, 'publicUrl')) { - throw new UnableToGeneratePublicUrl(sprintf('%s does not support generating public urls.', $filesystem::class), $path); - } - return $filesystem->publicUrl($path, $config); } @@ -299,10 +295,6 @@ public function temporaryUrl(string $path, DateTimeInterface $expiresAt, array $ /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($path); - if ( ! method_exists($filesystem, 'temporaryUrl')) { - throw new UnableToGenerateTemporaryUrl(sprintf('%s does not support generating public urls.', $filesystem::class), $path); - } - return $filesystem->temporaryUrl($path, $expiresAt, $this->config->extend($config)->toArray()); } @@ -311,13 +303,17 @@ public function checksum(string $path, array $config = []): string /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($path); - if ( ! method_exists($filesystem, 'checksum')) { - throw new UnableToProvideChecksum(sprintf('%s does not support providing checksums.', $filesystem::class), $path); - } - return $filesystem->checksum($path, $this->config->extend($config)->toArray()); } + public function metadata(string $path, array $config = []): StorageAttributes + { + /** @var FilesystemOperator $filesystem */ + [$filesystem, $path] = $this->determineFilesystemAndPath($path); + + return $filesystem->metadata($path, $this->config->extend($config)->toArray()); + } + private function mountFilesystems(array $filesystems): void { foreach ($filesystems as $key => $filesystem) { diff --git a/src/PathPrefixing/PathPrefixedAdapter.php b/src/PathPrefixing/PathPrefixedAdapter.php index 7409ea0e8..fbba50cd7 100644 --- a/src/PathPrefixing/PathPrefixedAdapter.php +++ b/src/PathPrefixing/PathPrefixedAdapter.php @@ -10,6 +10,7 @@ use League\Flysystem\FileAttributes; use League\Flysystem\FilesystemAdapter; use League\Flysystem\PathPrefixer; +use League\Flysystem\StorageAttributes; use League\Flysystem\UnableToCheckDirectoryExistence; use League\Flysystem\UnableToCheckFileExistence; use League\Flysystem\UnableToCopyFile; @@ -67,6 +68,13 @@ public function listContents(string $location, bool $deep): Generator } } + public function metadata(string $path, Config $config): StorageAttributes + { + $attributes = $this->adapter->metadata($this->prefix->prefixPath($path), $config); + + return $attributes->withPath($this->prefix->stripPrefix($attributes->path())); + } + public function fileExists(string $location): bool { try { diff --git a/src/PathPrefixing/composer.json b/src/PathPrefixing/composer.json index 9ef71d415..b80f2584a 100644 --- a/src/PathPrefixing/composer.json +++ b/src/PathPrefixing/composer.json @@ -10,7 +10,7 @@ } }, "require": { - "php": "^8.0.2", + "php": "^8.1", "league/flysystem": "^4.0.0" }, "license": "MIT", diff --git a/src/PhpseclibV2/.gitattributes b/src/PhpseclibV2/.gitattributes deleted file mode 100644 index 9e1465fb3..000000000 --- a/src/PhpseclibV2/.gitattributes +++ /dev/null @@ -1,8 +0,0 @@ -* text=auto - -.github export-ignore -.gitattributes export-ignore -.gitignore export-ignore -**/*Test.php export-ignore -**/*Stub.php export-ignore -README.md export-ignore diff --git a/src/PhpseclibV2/.github/workflows/close-subsplit-prs.yaml b/src/PhpseclibV2/.github/workflows/close-subsplit-prs.yaml deleted file mode 100644 index 99b1e91a5..000000000 --- a/src/PhpseclibV2/.github/workflows/close-subsplit-prs.yaml +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: Close sub-split PRs - -on: - push: - branches: - - 2.x - - 3.x - pull_request: - branches: - - 2.x - - 3.x - schedule: - - cron: '30 7 * * *' - -jobs: - close_subsplit_prs: - runs-on: ubuntu-latest - name: Close sub-split PRs - steps: - - uses: frankdejonge/action-close-subsplit-pr@0.1.0 - with: - close_pr: 'yes' - target_branch_match: '^(?!master).+$' - message: | - Hi :wave:, - - Thank you for contributing to Flysystem. Unfortunately, you've sent a PR to a read-only sub-split repository. - - All pull requests should be directed towards: https://github.com/thephpleague/flysystem diff --git a/src/PhpseclibV2/ConnectionProvider.php b/src/PhpseclibV2/ConnectionProvider.php deleted file mode 100644 index c9a3c942e..000000000 --- a/src/PhpseclibV2/ConnectionProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -succeedAfter = $succeedAfter; - } - - public function isConnected(SFTP $connection): bool - { - if ($this->numberOfTimesChecked >= $this->succeedAfter) { - return true; - } - - $this->numberOfTimesChecked++; - - return false; - } -} diff --git a/src/PhpseclibV2/README.md b/src/PhpseclibV2/README.md deleted file mode 100644 index 4c7c09118..000000000 --- a/src/PhpseclibV2/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# CAUTION: This package is deprecated since Flysystem 3.0 Instead, use the [Flysystem for SFTP v3](https://github.com/thephpleague/flysystem-sftp-v3) - -## Sub-split of Flysystem for SFTP using phpseclib2. - -> ⚠️ this is a sub-split, for pull requests and issues, visit: https://github.com/thephpleague/flysystem - -```bash -composer require league/flysystem-sftp -``` - -View the [documentation](https://flysystem.thephpleague.com/docs/adapter/sftp/). diff --git a/src/PhpseclibV2/SftpAdapter.php b/src/PhpseclibV2/SftpAdapter.php deleted file mode 100644 index f2e5d6b1f..000000000 --- a/src/PhpseclibV2/SftpAdapter.php +++ /dev/null @@ -1,359 +0,0 @@ -connectionProvider = $connectionProvider; - $this->prefixer = new PathPrefixer($root); - $this->visibilityConverter = $visibilityConverter ?? new PortableVisibilityConverter(); - $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector(); - } - - public function fileExists(string $path): bool - { - $location = $this->prefixer->prefixPath($path); - - try { - return $this->connectionProvider->provideConnection()->is_file($location); - } catch (Throwable $exception) { - throw UnableToCheckFileExistence::forLocation($path, $exception); - } - } - - public function directoryExists(string $path): bool - { - $location = $this->prefixer->prefixDirectoryPath($path); - - try { - return $this->connectionProvider->provideConnection()->is_dir($location); - } catch (Throwable $exception) { - throw UnableToCheckDirectoryExistence::forLocation($path, $exception); - } - } - - /** - * @param string $path - * @param string|resource $contents - * @param Config $config - * - * @throws FilesystemException - */ - private function upload(string $path, $contents, Config $config): void - { - $this->ensureParentDirectoryExists($path, $config); - $connection = $this->connectionProvider->provideConnection(); - $location = $this->prefixer->prefixPath($path); - - if ( ! $connection->put($location, $contents, SFTP::SOURCE_STRING)) { - throw UnableToWriteFile::atLocation($path, 'not able to write the file'); - } - - if ($visibility = $config->get(Config::OPTION_VISIBILITY)) { - $this->setVisibility($path, $visibility); - } - } - - private function ensureParentDirectoryExists(string $path, Config $config): void - { - $parentDirectory = dirname($path); - - if ($parentDirectory === '' || $parentDirectory === '.') { - return; - } - - /** @var string $visibility */ - $visibility = $config->get(Config::OPTION_DIRECTORY_VISIBILITY); - $this->makeDirectory($parentDirectory, $visibility); - } - - private function makeDirectory(string $directory, ?string $visibility): void - { - $location = $this->prefixer->prefixPath($directory); - $connection = $this->connectionProvider->provideConnection(); - - if ($connection->is_dir($location)) { - return; - } - - $mode = $visibility ? $this->visibilityConverter->forDirectory( - $visibility - ) : $this->visibilityConverter->defaultForDirectories(); - - if ( ! $connection->mkdir($location, $mode, true) && ! $connection->is_dir($location)) { - throw UnableToCreateDirectory::atLocation($directory); - } - } - - public function write(string $path, string $contents, Config $config): void - { - try { - $this->upload($path, $contents, $config); - } catch (UnableToWriteFile $exception) { - throw $exception; - } catch (Throwable $exception) { - throw UnableToWriteFile::atLocation($path, $exception->getMessage(), $exception); - } - } - - public function writeStream(string $path, $contents, Config $config): void - { - try { - $this->upload($path, $contents, $config); - } catch (UnableToWriteFile $exception) { - throw $exception; - } catch (Throwable $exception) { - throw UnableToWriteFile::atLocation($path, $exception->getMessage(), $exception); - } - } - - public function read(string $path): string - { - $location = $this->prefixer->prefixPath($path); - $connection = $this->connectionProvider->provideConnection(); - $contents = $connection->get($location); - - if ( ! is_string($contents)) { - throw UnableToReadFile::fromLocation($path); - } - - return $contents; - } - - public function readStream(string $path) - { - $location = $this->prefixer->prefixPath($path); - $connection = $this->connectionProvider->provideConnection(); - /** @var resource $readStream */ - $readStream = fopen('php://temp', 'w+'); - - if ( ! $connection->get($location, $readStream)) { - fclose($readStream); - throw UnableToReadFile::fromLocation($path); - } - - rewind($readStream); - - return $readStream; - } - - public function delete(string $path): void - { - $location = $this->prefixer->prefixPath($path); - $connection = $this->connectionProvider->provideConnection(); - $connection->delete($location); - } - - public function deleteDirectory(string $path): void - { - $location = rtrim($this->prefixer->prefixPath($path), '/') . '/'; - $connection = $this->connectionProvider->provideConnection(); - $connection->delete($location); - $connection->rmdir($location); - } - - public function createDirectory(string $path, Config $config): void - { - $this->makeDirectory($path, $config->get(Config::OPTION_DIRECTORY_VISIBILITY, $config->get(Config::OPTION_VISIBILITY))); - } - - public function setVisibility(string $path, string $visibility): void - { - $location = $this->prefixer->prefixPath($path); - $connection = $this->connectionProvider->provideConnection(); - $mode = $this->visibilityConverter->forFile($visibility); - - if ( ! $connection->chmod($mode, $location, false)) { - throw UnableToSetVisibility::atLocation($path); - } - } - - private function fetchFileMetadata(string $path, string $type): FileAttributes - { - $location = $this->prefixer->prefixPath($path); - $connection = $this->connectionProvider->provideConnection(); - $stat = $connection->stat($location); - - if ( ! is_array($stat)) { - throw UnableToRetrieveMetadata::create($path, $type); - } - - $attributes = $this->convertListingToAttributes($path, $stat); - - if ( ! $attributes instanceof FileAttributes) { - throw UnableToRetrieveMetadata::create($path, $type, 'path is not a file'); - } - - return $attributes; - } - - public function mimeType(string $path): FileAttributes - { - try { - $mimetype = $this->detectMimeTypeUsingPath - ? $this->mimeTypeDetector->detectMimeTypeFromPath($path) - : $this->mimeTypeDetector->detectMimeType($path, $this->read($path)); - } catch (Throwable $exception) { - throw UnableToRetrieveMetadata::mimeType($path, $exception->getMessage(), $exception); - } - - if ($mimetype === null) { - throw UnableToRetrieveMetadata::mimeType($path, 'Unknown.'); - } - - return new FileAttributes($path, null, null, null, $mimetype); - } - - public function lastModified(string $path): FileAttributes - { - return $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_LAST_MODIFIED); - } - - public function fileSize(string $path): FileAttributes - { - return $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_FILE_SIZE); - } - - public function visibility(string $path): FileAttributes - { - return $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_VISIBILITY); - } - - public function listContents(string $path, bool $deep): iterable - { - $connection = $this->connectionProvider->provideConnection(); - $location = $this->prefixer->prefixPath(rtrim($path, '/')) . '/'; - $listing = $connection->rawlist($location, false); - - if ($listing === false) { - return; - } - - foreach ($listing as $filename => $attributes) { - if ($filename === '.' || $filename === '..') { - continue; - } - - // Ensure numeric keys are strings. - $filename = (string) $filename; - $path = $this->prefixer->stripPrefix($location . ltrim($filename, '/')); - $attributes = $this->convertListingToAttributes($path, $attributes); - yield $attributes; - - if ($deep && $attributes->isDir()) { - foreach ($this->listContents($attributes->path(), true) as $child) { - yield $child; - } - } - } - } - - private function convertListingToAttributes(string $path, array $attributes): StorageAttributes - { - $permissions = $attributes['permissions'] & 0777; - $lastModified = $attributes['mtime'] ?? null; - - if (($attributes['type'] ?? null) === NET_SFTP_TYPE_DIRECTORY) { - return new DirectoryAttributes( - ltrim($path, '/'), - $this->visibilityConverter->inverseForDirectory($permissions), - $lastModified - ); - } - - return new FileAttributes( - $path, - $attributes['size'], - $this->visibilityConverter->inverseForFile($permissions), - $lastModified - ); - } - - public function move(string $source, string $destination, Config $config): void - { - $sourceLocation = $this->prefixer->prefixPath($source); - $destinationLocation = $this->prefixer->prefixPath($destination); - $connection = $this->connectionProvider->provideConnection(); - - try { - $this->ensureParentDirectoryExists($destination, $config); - } catch (Throwable $exception) { - throw UnableToMoveFile::fromLocationTo($source, $destination, $exception); - } - - if ( ! $connection->rename($sourceLocation, $destinationLocation)) { - throw UnableToMoveFile::fromLocationTo($source, $destination); - } - } - - public function copy(string $source, string $destination, Config $config): void - { - try { - $readStream = $this->readStream($source); - $visibility = $this->visibility($source)->visibility(); - $this->writeStream($destination, $readStream, new Config(compact(Config::OPTION_VISIBILITY))); - } catch (Throwable $exception) { - if (isset($readStream) && is_resource($readStream)) { - @fclose($readStream); - } - throw UnableToCopyFile::fromLocationTo($source, $destination, $exception); - } - } -} diff --git a/src/PhpseclibV2/SftpAdapterTest.php b/src/PhpseclibV2/SftpAdapterTest.php deleted file mode 100644 index 862a9e8b7..000000000 --- a/src/PhpseclibV2/SftpAdapterTest.php +++ /dev/null @@ -1,235 +0,0 @@ -provideConnection(); - $this->connection = $connection; - $this->connection->reset(); - } - - /** - * @test - */ - public function failing_to_create_a_directory(): void - { - $adapter = $this->adapterWithInvalidRoot(); - - $this->expectException(UnableToCreateDirectory::class); - - $adapter->createDirectory('not-gonna-happen', new Config()); - } - - /** - * @test - */ - public function failing_to_write_a_file(): void - { - $adapter = $this->adapterWithInvalidRoot(); - - $this->expectException(UnableToWriteFile::class); - - $adapter->write('not-gonna-happen', 'na-ah', new Config()); - } - - /** - * @test - */ - public function failing_to_read_a_file(): void - { - $adapter = $this->adapterWithInvalidRoot(); - - $this->expectException(UnableToReadFile::class); - - $adapter->read('not-gonna-happen'); - } - - /** - * @test - */ - public function failing_to_read_a_file_as_a_stream(): void - { - $adapter = $this->adapterWithInvalidRoot(); - - $this->expectException(UnableToReadFile::class); - - $adapter->readStream('not-gonna-happen'); - } - - /** - * @test - */ - public function failing_to_write_a_file_using_streams(): void - { - $adapter = $this->adapterWithInvalidRoot(); - $writeHandle = stream_with_contents('contents'); - - $this->expectException(UnableToWriteFile::class); - - try { - $adapter->writeStream('not-gonna-happen', $writeHandle, new Config()); - } finally { - fclose($writeHandle); - } - } - - /** - * @test - */ - public function detecting_mimetype(): void - { - $adapter = $this->adapter(); - $adapter->write('file.svg', (string) file_get_contents(__DIR__ . '/../AdapterTestUtilities/test_files/flysystem.svg'), new Config()); - - $mimeType = $adapter->mimeType('file.svg'); - - $this->assertStringStartsWith('image/svg+xml', $mimeType->mimeType()); - } - - /** - * @test - */ - public function failing_to_chmod_when_writing(): void - { - $this->connection->failOnChmod('/upload/path.txt'); - $adapter = $this->adapter(); - - $this->expectException(UnableToWriteFile::class); - - $adapter->write('path.txt', 'contents', new Config(['visibility' => 'public'])); - } - - /** - * @test - */ - public function failing_to_move_a_file_cause_the_parent_directory_cant_be_created(): void - { - $adapter = $this->adapterWithInvalidRoot(); - - $this->expectException(UnableToMoveFile::class); - - $adapter->move('path.txt', 'new-path.txt', new Config()); - } - - /** - * @test - */ - public function failing_to_copy_a_file(): void - { - $adapter = $this->adapterWithInvalidRoot(); - - $this->expectException(UnableToCopyFile::class); - - $adapter->copy('path.txt', 'new-path.txt', new Config()); - } - - /** - * @test - */ - public function failing_to_copy_a_file_because_writing_fails(): void - { - $this->givenWeHaveAnExistingFile('path.txt', 'contents'); - $adapter = $this->adapter(); - $this->connection->failOnPut('/upload/new-path.txt'); - - $this->expectException(UnableToCopyFile::class); - - $adapter->copy('path.txt', 'new-path.txt', new Config()); - } - - /** - * @test - */ - public function failing_to_chmod_when_writing_with_a_stream(): void - { - $writeStream = stream_with_contents('contents'); - $this->connection->failOnChmod('/upload/path.txt'); - $adapter = $this->adapter(); - - $this->expectException(UnableToWriteFile::class); - - try { - $adapter->writeStream('path.txt', $writeStream, new Config(['visibility' => 'public'])); - } finally { - @fclose($writeStream); - } - } - - /** - * @test - */ - public function list_contents_directory_does_not_exist(): void - { - $contents = $this->adapter()->listContents('/does_not_exist', false); - $this->assertCount(0, iterator_to_array($contents)); - } - - private static function connectionProvider(): ConnectionProvider - { - if ( ! static::$connectionProvider instanceof ConnectionProvider) { - static::$connectionProvider = new StubSftpConnectionProvider('localhost', 'foo', 'pass', 2222); - } - - return static::$connectionProvider; - } - - /** - * @return SftpAdapter - */ - private function adapterWithInvalidRoot(): SftpAdapter - { - $provider = static::connectionProvider(); - $adapter = new SftpAdapter($provider, '/invalid'); - - return $adapter; - } -} diff --git a/src/PhpseclibV2/SftpConnectionProvider.php b/src/PhpseclibV2/SftpConnectionProvider.php deleted file mode 100644 index 3d7df000d..000000000 --- a/src/PhpseclibV2/SftpConnectionProvider.php +++ /dev/null @@ -1,240 +0,0 @@ -host = $host; - $this->username = $username; - $this->password = $password; - $this->privateKey = $privateKey; - $this->passphrase = $passphrase; - $this->useAgent = $useAgent; - $this->port = $port; - $this->timeout = $timeout; - $this->hostFingerprint = $hostFingerprint; - $this->connectivityChecker = $connectivityChecker ?? new SimpleConnectivityChecker(); - $this->maxTries = $maxTries; - } - - public function provideConnection(): SFTP - { - $tries = 0; - start: - - $connection = $this->connection instanceof SFTP - ? $this->connection - : $this->setupConnection(); - - if ( ! $this->connectivityChecker->isConnected($connection)) { - $connection->disconnect(); - $this->connection = null; - - if ($tries < $this->maxTries) { - $tries++; - goto start; - } - - throw UnableToConnectToSftpHost::atHostname($this->host); - } - - return $this->connection = $connection; - } - - private function setupConnection(): SFTP - { - $connection = new SFTP($this->host, $this->port, $this->timeout); - $this->disableStatCache && $connection->disableStatCache(); - - try { - $this->checkFingerprint($connection); - $this->authenticate($connection); - } catch (Throwable $exception) { - $connection->disconnect(); - throw $exception; - } - - return $connection; - } - - private function checkFingerprint(SFTP $connection): void - { - if ( ! $this->hostFingerprint) { - return; - } - - $publicKey = $connection->getServerPublicHostKey(); - - if ($publicKey === false) { - throw UnableToEstablishAuthenticityOfHost::becauseTheAuthenticityCantBeEstablished($this->host); - } - - $fingerprint = $this->getFingerprintFromPublicKey($publicKey); - - if (0 !== strcasecmp($this->hostFingerprint, $fingerprint)) { - throw UnableToEstablishAuthenticityOfHost::becauseTheAuthenticityCantBeEstablished($this->host); - } - } - - private function getFingerprintFromPublicKey(string $publicKey): string - { - $content = explode(' ', $publicKey, 3); - - return implode(':', str_split(md5(base64_decode($content[1])), 2)); - } - - private function authenticate(SFTP $connection): void - { - if ($this->privateKey !== null) { - $this->authenticateWithPrivateKey($connection); - } elseif ($this->useAgent) { - $this->authenticateWithAgent($connection); - } elseif ( ! $connection->login($this->username, $this->password)) { - throw UnableToAuthenticate::withPassword(); - } - } - - public static function fromArray(array $options): SftpConnectionProvider - { - return new SftpConnectionProvider( - $options['host'], - $options['username'], - $options['password'] ?? null, - $options['privateKey'] ?? null, - $options['passphrase'] ?? null, - $options['port'] ?? 22, - $options['useAgent'] ?? false, - $options['timeout'] ?? 10, - $options['maxTries'] ?? 4, - $options['hostFingerprint'] ?? null, - $options['connectivityChecker'] ?? null - ); - } - - private function authenticateWithPrivateKey(SFTP $connection): void - { - $privateKey = $this->loadPrivateKey(); - - if ($connection->login($this->username, $privateKey)) { - return; - } - - if ($this->password !== null && $connection->login($this->username, $this->password)) { - return; - } - - throw UnableToAuthenticate::withPrivateKey(); - } - - private function loadPrivateKey(): RSA - { - if ("---" !== substr($this->privateKey, 0, 3) && is_file($this->privateKey)) { - $this->privateKey = file_get_contents($this->privateKey); - } - - $key = new RSA(); - - if ($this->passphrase !== null) { - $key->setPassword($this->passphrase); - } - - if ( ! $key->loadKey($this->privateKey)) { - throw new UnableToLoadPrivateKey(); - } - - return $key; - } - - private function authenticateWithAgent(SFTP $connection): void - { - $agent = new Agent(); - - if ( ! $connection->login($this->username, $agent)) { - throw UnableToAuthenticate::withSshAgent(); - } - } -} diff --git a/src/PhpseclibV2/SftpConnectionProviderTest.php b/src/PhpseclibV2/SftpConnectionProviderTest.php deleted file mode 100644 index eb71442ee..000000000 --- a/src/PhpseclibV2/SftpConnectionProviderTest.php +++ /dev/null @@ -1,247 +0,0 @@ -expectException(UnableToConnectToSftpHost::class); - - $provider = SftpConnectionProvider::fromArray( - [ - 'host' => 'localhost', - 'username' => 'foo', - 'password' => 'pass', - 'port' => 2222, - 'timeout' => 10, - 'connectivityChecker' => new FixatedConnectivityChecker(5) - ] - ); - - $provider->provideConnection(); - } - - /** - * @test - */ - public function trying_until_5_tries(): void - { - $provider = SftpConnectionProvider::fromArray([ - 'host' => 'localhost', - 'username' => 'foo', - 'password' => 'pass', - 'port' => 2222, - 'timeout' => 10, - 'connectivityChecker' => new FixatedConnectivityChecker(4) - ]); - $connection = $provider->provideConnection(); - $sameConnection = $provider->provideConnection(); - - $this->assertInstanceOf(SFTP::class, $connection); - $this->assertSame($connection, $sameConnection); - } - - /** - * @test - */ - public function authenticating_with_a_private_key(): void - { - $provider = SftpConnectionProvider::fromArray( - [ - 'host' => 'localhost', - 'username' => 'bar', - 'privateKey' => __DIR__ . '/../../test_files/sftp/id_rsa', - 'passphrase' => 'secret', - 'port' => 2222, - ] - ); - - $connection = $provider->provideConnection(); - $this->assertInstanceOf(SFTP::class, $connection); - } - - /** - * @test - */ - public function authenticating_with_an_invalid_private_key(): void - { - $provider = SftpConnectionProvider::fromArray( - [ - 'host' => 'localhost', - 'username' => 'bar', - 'privateKey' => __DIR__ . '/../../test_files/sftp/users.conf', - 'port' => 2222, - ] - ); - - $this->expectException(UnableToLoadPrivateKey::class); - - $provider->provideConnection(); - } - - /** - * @test - */ - public function authenticating_with_an_ssh_agent(): void - { - $provider = SftpConnectionProvider::fromArray( - [ - 'host' => 'localhost', - 'username' => 'bar', - 'useAgent' => true, - 'port' => 2222, - ] - ); - - $connection = $provider->provideConnection(); - $this->assertInstanceOf(SFTP::class, $connection); - } - - /** - * @test - */ - public function failing_to_authenticating_with_an_ssh_agent(): void - { - $this->expectException(UnableToAuthenticate::class); - - $provider = SftpConnectionProvider::fromArray( - [ - 'host' => 'localhost', - 'username' => 'foo', - 'useAgent' => true, - 'port' => 2222, - ] - ); - - $provider->provideConnection(); - } - - /** - * @test - */ - public function authenticating_with_a_private_key_and_falling_back_to_password(): void - { - $provider = SftpConnectionProvider::fromArray( - [ - 'host' => 'localhost', - 'username' => 'foo', - 'password' => 'pass', - 'privateKey' => __DIR__ . '/../../test_files/sftp/id_rsa', - 'passphrase' => 'secret', - 'port' => 2222, - ] - ); - - $connection = $provider->provideConnection(); - $this->assertInstanceOf(SFTP::class, $connection); - } - - /** - * @test - */ - public function not_being_able_to_authenticate_with_a_private_key(): void - { - $provider = SftpConnectionProvider::fromArray( - [ - 'host' => 'localhost', - 'username' => 'foo', - 'privateKey' => __DIR__ . '/../../test_files/sftp/unknown.key', - 'passphrase' => 'secret', - 'port' => 2222, - ] - ); - - $this->expectExceptionObject(UnableToAuthenticate::withPrivateKey()); - $provider->provideConnection(); - } - - /** - * @test - */ - public function verifying_a_fingerprint(): void - { - $key = file_get_contents(__DIR__ . '/../../test_files/sftp/ssh_host_rsa_key.pub'); - $fingerPrint = $this->computeFingerPrint($key); - - $provider = SftpConnectionProvider::fromArray( - [ - 'host' => 'localhost', - 'username' => 'foo', - 'password' => 'pass', - 'port' => 2222, - 'hostFingerprint' => $fingerPrint, - ] - ); - - $anotherConnection = $provider->provideConnection(); - $this->assertInstanceOf(SFTP::class, $anotherConnection); - } - - /** - * @test - */ - public function providing_an_invalid_fingerprint(): void - { - $this->expectException(UnableToEstablishAuthenticityOfHost::class); - - $provider = SftpConnectionProvider::fromArray( - [ - 'host' => 'localhost', - 'username' => 'foo', - 'password' => 'pass', - 'port' => 2222, - 'hostFingerprint' => 'invalid:fingerprint', - ] - ); - $provider->provideConnection(); - } - - /** - * @test - */ - public function providing_an_invalid_password(): void - { - $this->expectException(UnableToAuthenticate::class); - $provider = SftpConnectionProvider::fromArray( - [ - 'host' => 'localhost', - 'username' => 'foo', - 'password' => 'lol', - 'port' => 2222, - ] - ); - $provider->provideConnection(); - } - - private function computeFingerPrint(string $publicKey): string - { - $content = explode(' ', $publicKey, 3); - - return implode(':', str_split(md5(base64_decode($content[1])), 2)); - } -} diff --git a/src/PhpseclibV2/SftpStub.php b/src/PhpseclibV2/SftpStub.php deleted file mode 100644 index ab4dcde40..000000000 --- a/src/PhpseclibV2/SftpStub.php +++ /dev/null @@ -1,102 +0,0 @@ - - */ - private $tripWires = []; - - public function failOnChmod(string $filename): void - { - $key = $this->formatTripKey('chmod', $filename); - $this->tripWires[$key] = true; - } - - /** - * @param int $mode - * @param string $filename - * @param bool $recursive - * - * @return bool|mixed - */ - public function chmod($mode, $filename, $recursive = false) - { - $key = $this->formatTripKey('chmod', $filename); - $shouldTrip = $this->tripWires[$key] ?? false; - - if ($shouldTrip) { - unset($this->tripWires[$key]); - - return false; - } - - return parent::chmod($mode, $filename, $recursive); - } - - public function failOnPut(string $filename): void - { - $key = $this->formatTripKey('put', $filename); - $this->tripWires[$key] = true; - } - - /** - * @param string $remote_file - * @param resource|string $data - * @param int $mode - * @param int $start - * @param int $local_start - * @param null $progressCallback - * - * @return bool - */ - public function put( - $remote_file, - $data, - $mode = self::SOURCE_STRING, - $start = -1, - $local_start = -1, - $progressCallback = null - ) { - $key = $this->formatTripKey('put', $remote_file); - $shouldTrip = $this->tripWires[$key] ?? false; - - if ($shouldTrip) { - return false; - } - - return parent::put($remote_file, $data, $mode, $start, $local_start, $progressCallback); - } - - /** - * @param array $arguments - * - * @return string - */ - private function formatTripKey(...$arguments): string - { - $key = ''; - - foreach ($arguments as $argument) { - $key .= var_export($argument, true); - } - - return $key; - } - - public function reset(): void - { - $this->tripWires = []; - } -} diff --git a/src/PhpseclibV2/SimpleConnectivityChecker.php b/src/PhpseclibV2/SimpleConnectivityChecker.php deleted file mode 100644 index 424b165e9..000000000 --- a/src/PhpseclibV2/SimpleConnectivityChecker.php +++ /dev/null @@ -1,18 +0,0 @@ -isConnected(); - } -} diff --git a/src/PhpseclibV2/StubSftpConnectionProvider.php b/src/PhpseclibV2/StubSftpConnectionProvider.php deleted file mode 100644 index 8ee41e732..000000000 --- a/src/PhpseclibV2/StubSftpConnectionProvider.php +++ /dev/null @@ -1,62 +0,0 @@ -host = $host; - $this->username = $username; - $this->password = $password; - $this->port = $port; - } - - public function provideConnection(): SFTP - { - if ( ! $this->connection instanceof SFTP) { - $connection = new SftpStub($this->host, $this->port); - $connection->login($this->username, $this->password); - - $this->connection = $connection; - } - - return $this->connection; - } -} diff --git a/src/PhpseclibV2/UnableToAuthenticate.php b/src/PhpseclibV2/UnableToAuthenticate.php deleted file mode 100644 index 7d696b4de..000000000 --- a/src/PhpseclibV2/UnableToAuthenticate.php +++ /dev/null @@ -1,29 +0,0 @@ -fetchFileMetadata($path, FileAttributes::ATTRIBUTE_VISIBILITY); } + public function metadata(string $path, Config $config): StorageAttributes + { + $location = $this->prefixer->prefixPath($path); + $connection = $this->connectionProvider->provideConnection(); + $stat = $connection->stat($location); + + if ( ! is_array($stat)) { + throw UnableToRetrieveMetadata::metadata($path, 'no result returned'); + } + + return $this->convertListingToAttributes($path, $stat); + } + public function listContents(string $path, bool $deep): iterable { $connection = $this->connectionProvider->provideConnection(); diff --git a/src/PhpseclibV3/composer.json b/src/PhpseclibV3/composer.json index 96b24ad99..009bdc8c0 100644 --- a/src/PhpseclibV3/composer.json +++ b/src/PhpseclibV3/composer.json @@ -8,7 +8,7 @@ } }, "require": { - "php": "^8.0.2", + "php": "^8.1", "league/flysystem": "^4.0.0", "league/mime-type-detection": "^1.0.0", "phpseclib/phpseclib": "^3.0" diff --git a/src/ReadOnly/ReadOnlyFilesystemAdapter.php b/src/ReadOnly/ReadOnlyFilesystemAdapter.php index 2a6fc1517..0f0f28754 100644 --- a/src/ReadOnly/ReadOnlyFilesystemAdapter.php +++ b/src/ReadOnly/ReadOnlyFilesystemAdapter.php @@ -8,6 +8,7 @@ use League\Flysystem\Config; use League\Flysystem\DecoratedAdapter; use League\Flysystem\FilesystemAdapter; +use League\Flysystem\StorageAttributes; use League\Flysystem\UnableToCopyFile; use League\Flysystem\UnableToCreateDirectory; use League\Flysystem\UnableToDeleteDirectory; diff --git a/src/ReadOnly/composer.json b/src/ReadOnly/composer.json index a0cb82c17..66893f1a7 100644 --- a/src/ReadOnly/composer.json +++ b/src/ReadOnly/composer.json @@ -10,7 +10,7 @@ } }, "require": { - "php": "^8.0.2", + "php": "^8.1", "league/flysystem": "^4.0.0" }, "license": "MIT", diff --git a/src/StorageAttributes.php b/src/StorageAttributes.php index 6be6235bd..864573149 100644 --- a/src/StorageAttributes.php +++ b/src/StorageAttributes.php @@ -15,6 +15,7 @@ interface StorageAttributes extends JsonSerializable, ArrayAccess public const ATTRIBUTE_VISIBILITY = 'visibility'; public const ATTRIBUTE_LAST_MODIFIED = 'last_modified'; public const ATTRIBUTE_MIME_TYPE = 'mime_type'; + public const ATTRIBUTE_METADATA = 'metadata'; public const ATTRIBUTE_EXTRA_METADATA = 'extra_metadata'; public const TYPE_FILE = 'file'; diff --git a/src/UnableToRetrieveMetadata.php b/src/UnableToRetrieveMetadata.php index f7e3fde4c..e5f5f799f 100644 --- a/src/UnableToRetrieveMetadata.php +++ b/src/UnableToRetrieveMetadata.php @@ -44,6 +44,11 @@ public static function mimeType(string $location, string $reason = '', ?Throwabl return static::create($location, FileAttributes::ATTRIBUTE_MIME_TYPE, $reason, $previous); } + public static function metadata(string $location, string $reason = '', ?Throwable $previous = null): self + { + return static::create($location, FileAttributes::ATTRIBUTE_METADATA, $reason, $previous); + } + public static function create(string $location, string $type, string $reason = '', ?Throwable $previous = null): self { $e = new static("Unable to retrieve the $type for file at location: $location. {$reason}", 0, $previous); diff --git a/src/WebDAV/WebDAVAdapter.php b/src/WebDAV/WebDAVAdapter.php index 935972692..25acc859c 100644 --- a/src/WebDAV/WebDAVAdapter.php +++ b/src/WebDAV/WebDAVAdapter.php @@ -9,6 +9,7 @@ use League\Flysystem\FileAttributes; use League\Flysystem\FilesystemAdapter; use League\Flysystem\PathPrefixer; +use League\Flysystem\StorageAttributes; use League\Flysystem\UnableToCheckDirectoryExistence; use League\Flysystem\UnableToCheckFileExistence; use League\Flysystem\UnableToCopyFile; @@ -30,6 +31,7 @@ use function array_key_exists; use function array_shift; +use function count; use function dirname; use function explode; use function fclose; @@ -272,6 +274,34 @@ public function fileSize(string $path): FileAttributes return new FileAttributes($path, fileSize: $fileSize); } + public function metadata(string $path, Config $config): StorageAttributes + { + $location = $this->encodePath($this->prefixer->prefixPath($path)); + $response = $this->client->propFind($location, self::FIND_PROPERTIES); + + if (count($response) === 0) { + $location = $this->encodePath($this->prefixer->prefixDirectoryPath($path)); + $response = $this->client->propFind($location, self::FIND_PROPERTIES); + } + + if (count($response) === 0) { + throw UnableToRetrieveMetadata::metadata($path, 'no file/directory found'); + } + + $response = $this->normalizeObject($response); + + if ($this->propsIsDirectory($response)) { + return new DirectoryAttributes($path, lastModified: $response['last_modified'] ?? null); + } + + return new FileAttributes( + $path, + fileSize: $response['file_size'] ?? null, + lastModified: $response['last_modified'] ?? null, + mimeType: $response['mime_type'] ?? null, + ); + } + public function listContents(string $path, bool $deep): iterable { $location = $this->encodePath($this->prefixer->prefixDirectoryPath($path)); diff --git a/src/WebDAV/composer.json b/src/WebDAV/composer.json index 3dd79bb27..08f2326b0 100644 --- a/src/WebDAV/composer.json +++ b/src/WebDAV/composer.json @@ -8,7 +8,7 @@ } }, "require": { - "php": "^8.0.2", + "php": "^8.1", "league/flysystem": "^4.0.0", "sabre/dav": "^4.6.0" }, diff --git a/src/ZipArchive/ZipArchiveAdapter.php b/src/ZipArchive/ZipArchiveAdapter.php index 63f3411ed..9485ceb3d 100644 --- a/src/ZipArchive/ZipArchiveAdapter.php +++ b/src/ZipArchive/ZipArchiveAdapter.php @@ -10,6 +10,7 @@ use League\Flysystem\FileAttributes; use League\Flysystem\FilesystemAdapter; use League\Flysystem\PathPrefixer; +use League\Flysystem\StorageAttributes; use League\Flysystem\UnableToCopyFile; use League\Flysystem\UnableToCreateDirectory; use League\Flysystem\UnableToDeleteDirectory; @@ -281,6 +282,32 @@ public function fileSize(string $path): FileAttributes return new FileAttributes($path, $stats['size'], null, null); } + public function metadata(string $path, Config $config): StorageAttributes + { + $archive = $this->zipArchiveProvider->createZipArchive(); + $stats = $archive->statName($this->pathPrefixer->prefixPath($path)) + ?: $archive->statName($this->pathPrefixer->prefixDirectoryPath($path)); + + if ($stats === false) { + throw UnableToRetrieveMetadata::metadata($path, 'no file/directory found'); + } + + $itemPath = $stats['name']; + + return $this->isDirectoryPath($itemPath) + ? new DirectoryAttributes( + $this->pathPrefixer->stripDirectoryPrefix($itemPath), + null, + $stats['mtime'] + ) + : new FileAttributes( + $this->pathPrefixer->stripPrefix($itemPath), + $stats['size'], + null, + $stats['mtime'] + ); + } + public function listContents(string $path, bool $deep): iterable { $archive = $this->zipArchiveProvider->createZipArchive(); diff --git a/src/ZipArchive/composer.json b/src/ZipArchive/composer.json index 67e206ad1..3ef9ef86a 100644 --- a/src/ZipArchive/composer.json +++ b/src/ZipArchive/composer.json @@ -10,7 +10,7 @@ } }, "require": { - "php": "^8.0.2", + "php": "^8.1", "ext-zip": "*", "league/flysystem": "^4.0.0", "league/mime-type-detection": "^1.0.0"