diff --git a/.github/workflows/publish-subsplits.yml b/.github/workflows/publish-subsplits.yml index 2c6a4c201..5be15d684 100644 --- a/.github/workflows/publish-subsplits.yml +++ b/.github/workflows/publish-subsplits.yml @@ -3,13 +3,13 @@ name: Sub-Split Publishing on: push: branches: - - 2.x + - 3.x create: tags: - - '*' + - '3.*' delete: tags: - - '*' + - '3.*' jobs: publish_subsplits: @@ -33,7 +33,7 @@ jobs: key: '${{ runner.os }}-splitsh' - uses: frankdejonge/use-subsplit-publish@1.0.0-beta.3 with: - source-branch: '2.x' + source-branch: '3.x' config-path: './config.subsplit-publish.json' splitsh-path: './.splitsh/splitsh-lite' splitsh-version: 'v1.0.1' diff --git a/.github/workflows/quality-assurance.yml b/.github/workflows/quality-assurance.yml index d9d89ff04..c38ad604d 100644 --- a/.github/workflows/quality-assurance.yml +++ b/.github/workflows/quality-assurance.yml @@ -6,13 +6,14 @@ on: - src/**/*.php - .github/workflows/quality-assurance.yml branches: - - 2.x + - 3.x pull_request: paths: - src/**/*.php - .github/workflows/quality-assurance.yml branches: - 2.x + - 3.x env: FLYSYSTEM_AWS_S3_KEY: '${{ secrets.FLYSYSTEM_AWS_S3_KEY }}' @@ -29,23 +30,12 @@ jobs: strategy: fail-fast: false matrix: - php: [ '7.2', '7.3', '7.4' ] + php: [ '8.0', '8.1' ] composer-flags: [ '' ] experimental: [false] - upgrade-aws-sdk: [ 'no' ] phpunit-flags: [ '--coverage-text' ] include: - php: '8.0' - composer-flags: '' - experimental: false - phpunit-flags: '--no-coverage' - upgrade-aws-sdk: 'yes' - - php: '8.1' - composer-flags: '--ignore-platform-reqs' - experimental: true - phpunit-flags: '--no-coverage' - upgrade-aws-sdk: 'yes' - - php: '7.2' composer-flags: '--prefer-lowest' experimental: false phpunit-flags: '--no-coverage' @@ -63,8 +53,6 @@ jobs: coverage: pcov tools: composer:v2 - run: composer update --no-progress ${{ matrix.composer-flags }} - - run: composer require --dev --ignore-platform-reqs aws/aws-sdk-php:^3.147.3 - if: ${{ matrix.upgrade-aws-sdk == 'yes' }} - run: php test_files/wait_for_sftp.php - run: php test_files/wait_for_ftp.php 2121 - run: php test_files/wait_for_ftp.php 2122 diff --git a/.gitignore b/.gitignore index 6fc14647d..faaac7a00 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /.php-cs-fixer.php /.php-cs-fixer.cache /google-cloud-service-account.json +.idea \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 42abe853d..0836c3e8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,30 @@ -# Version 2.x Changelog +# Changelog + +## 3.0.1 - 2022-01-15 + +### Fixes + +* [ZipArchive] delete top-level directory too when deleting a directory +* [GoogleCloudStorage] Use listing to check for directory existence (consistency) +* [GoogleCloudStorage] Fixed bug where exceptions were not thrown +* [AwsS3V3] Allow passing options for controlling multi-upload options (#1396) +* [Local] Convert windows-style directory separator to unix-style (#1398) + +## 3.0.0 - 2022-01-13 + +### Added + +* FilesystemReader::has to check for directory or file existence +* FilesystemReader::directoryExists to check for directory existence +* FilesystemReader::fileExists to check for file existence +* FilesystemAdapter::directoryExists to check for directory existence +* FilesystemAdapter::fileExists to check for file existence + +## 2.4.0 - 2022-01-04 + +### Added + +- [SFTP V3] New adapter officially published ## 2.3.2 - 2021-11-28 diff --git a/LICENSE b/LICENSE index 7c1027d3e..1f0165218 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2013-2020 Frank de Jonge +Copyright (c) 2013-2022 Frank de Jonge Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/bin/check-versions.php b/bin/check-versions.php index 892ee0617..095108b26 100644 --- a/bin/check-versions.php +++ b/bin/check-versions.php @@ -8,7 +8,7 @@ * - All required dependencies of the extracted packages MUST be * present in the main composer.json's require(-dev) section. * - Dependency constraints of extracted packages may not exclude - * the constrains of the main package and visa versa. + * the constraints of the main package and visa versa. * - The provided target release argument must be satisfiable by * all of the extracted packages' core dependency constraint. */ @@ -21,7 +21,7 @@ use League\Flysystem\Local\LocalFilesystemAdapter; use League\Flysystem\StorageAttributes; -include_once __DIR__.'/tools.php'; +include_once __DIR__ . '/tools.php'; function constraint_has_conflict(string $mainConstraint, string $packageConstraint): bool @@ -53,7 +53,7 @@ function constraint_has_conflict(string $mainConstraint, string $packageConstrai write_line("🔎 Inspecting composer dependency incompatibilities."); $mainVersion = $argv[1]; -$filesystem = new Filesystem(new LocalFilesystemAdapter(__DIR__.'/../')); +$filesystem = new Filesystem(new LocalFilesystemAdapter(__DIR__ . '/../')); $mainComposer = $filesystem->read('composer.json'); /** @var string[] $otherComposers */ @@ -69,7 +69,7 @@ function constraint_has_conflict(string $mainConstraint, string $packageConstrai $information = json_decode($filesystem->read($composerFile), true); foreach ($information['require'] as $dependency => $constraint) { - if (strpos($dependency, 'ext-') === 0) { + if (str_starts_with($dependency, 'ext-') || $dependency === 'phpseclib/phpseclib') { continue; } @@ -79,6 +79,7 @@ function constraint_has_conflict(string $mainConstraint, string $packageConstrai } else { write_line("Composer file {$composerFile} allows league/flysystem:{$mainVersion} with {$constraint}"); } + continue; } diff --git a/bin/set-flysystem-version.php b/bin/set-flysystem-version.php index 7f25df7fc..1036e08b4 100644 --- a/bin/set-flysystem-version.php +++ b/bin/set-flysystem-version.php @@ -5,7 +5,7 @@ use League\Flysystem\Local\LocalFilesystemAdapter; use League\Flysystem\StorageAttributes; -include_once __DIR__.'/tools.php'; +include_once __DIR__ . '/tools.php'; if ( ! isset($argv[1])) { panic('No base version provided'); @@ -15,7 +15,7 @@ write_line("☝️ Setting all flysystem constraints to {$mainVersion}."); -$filesystem = new Filesystem(new LocalFilesystemAdapter(__DIR__.'/../')); +$filesystem = new Filesystem(new LocalFilesystemAdapter(__DIR__ . '/../')); /** @var string[] $otherComposers */ $composerFiles = $filesystem->listContents('src', true) diff --git a/bin/tools.php b/bin/tools.php index add416899..b114a30da 100644 --- a/bin/tools.php +++ b/bin/tools.php @@ -1,6 +1,6 @@ adapter->copy($source, $destination, $config); } + + public function directoryExists(string $path): bool + { + $this->throwStagedException(__METHOD__, $path); + + return $this->adapter->directoryExists($path); + } } diff --git a/src/AdapterTestUtilities/FilesystemAdapterTestCase.php b/src/AdapterTestUtilities/FilesystemAdapterTestCase.php index f3b1a2885..0bf4ebb2e 100644 --- a/src/AdapterTestUtilities/FilesystemAdapterTestCase.php +++ b/src/AdapterTestUtilities/FilesystemAdapterTestCase.php @@ -4,6 +4,8 @@ namespace League\Flysystem\AdapterTestUtilities; +use function is_resource; +use function iterator_to_array; use const PHP_EOL; use Generator; use League\Flysystem\Config; @@ -35,7 +37,7 @@ abstract class FilesystemAdapterTestCase extends TestCase /** * @var bool */ - private $isUsingCustomAdapter = false; + protected $isUsingCustomAdapter = false; public static function clearFilesystemAdapterCache(): void { @@ -77,8 +79,8 @@ protected function useAdapter(FilesystemAdapter $adapter): FilesystemAdapter */ public function cleanupAdapter(): void { - $this->clearStorage(); $this->clearCustomAdapter(); + $this->clearStorage(); } public function clearStorage(): void @@ -144,9 +146,11 @@ public function writing_a_file_with_a_stream(): void $writeStream = stream_with_contents('contents'); $adapter->writeStream('path.txt', $writeStream, new Config()); + if (is_resource($writeStream)) { fclose($writeStream); } + $fileExists = $adapter->fileExists('path.txt'); $this->assertTrue($fileExists); @@ -197,9 +201,11 @@ public function writing_a_file_with_an_empty_stream(): void $writeStream = stream_with_contents(''); $adapter->writeStream('path.txt', $writeStream, new Config()); + if (is_resource($writeStream)) { fclose($writeStream); } + $fileExists = $adapter->fileExists('path.txt'); $this->assertTrue($fileExists); @@ -302,6 +308,45 @@ public function listing_contents_shallow(): void }); } + /** + * @test + */ + public function checking_if_a_non_existing_directory_exists(): void + { + $this->runScenario(function () { + $adapter = $this->adapter(); + self::assertFalse($adapter->directoryExists('this-does-not-exist.php')); + }); + } + + /** + * @test + */ + public function checking_if_a_directory_exists_after_writing_a_file(): void + { + $this->runScenario(function () { + $adapter = $this->adapter(); + $this->givenWeHaveAnExistingFile('existing-directory/file.txt'); + self::assertTrue($adapter->directoryExists('existing-directory')); + }); + } + + /** + * @test + */ + public function checking_if_a_directory_exists_after_creating_it(): void + { + $this->runScenario(function () { + $adapter = $this->adapter(); + $adapter->createDirectory('explicitly-created-directory', new Config()); + self::assertTrue($adapter->directoryExists('explicitly-created-directory')); + $adapter->deleteDirectory('explicitly-created-directory'); + $l = iterator_to_array($adapter->listContents('/', false), false); + self::assertEquals([], $l); + self::assertFalse($adapter->directoryExists('explicitly-created-directory')); + }); + } + /** * @test */ @@ -686,17 +731,18 @@ public function creating_a_directory(): void $this->runScenario(function () { $adapter = $this->adapter(); - $adapter->createDirectory('path', new Config()); + $adapter->createDirectory('creating_a_directory/path', new Config()); // Creating a directory should be idempotent. - $adapter->createDirectory('path', new Config()); + $adapter->createDirectory('creating_a_directory/path', new Config()); - $contents = iterator_to_array($adapter->listContents('', false)); + $contents = iterator_to_array($adapter->listContents('creating_a_directory', false)); $this->assertCount(1, $contents, $this->formatIncorrectListingCount($contents)); /** @var DirectoryAttributes $directory */ $directory = $contents[0]; $this->assertInstanceOf(DirectoryAttributes::class, $directory); - $this->assertEquals('path', $directory->path()); + $this->assertEquals('creating_a_directory/path', $directory->path()); + $adapter->deleteDirectory('creating_a_directory/path'); }); } diff --git a/src/AdapterTestUtilities/composer.json b/src/AdapterTestUtilities/composer.json index b58d94dcd..872b988e6 100644 --- a/src/AdapterTestUtilities/composer.json +++ b/src/AdapterTestUtilities/composer.json @@ -13,8 +13,8 @@ ] }, "require": { - "php": "^7.2 || ^8.0", - "league/flysystem": "^2.0.0" + "php": "^8.0.2", + "league/flysystem": "^2.0.0 || ^3.0.0" }, "license": "MIT", "authors": [ diff --git a/src/AsyncAwsS3/AsyncAwsS3Adapter.php b/src/AsyncAwsS3/AsyncAwsS3Adapter.php index c9a293b92..d1abb5e72 100644 --- a/src/AsyncAwsS3/AsyncAwsS3Adapter.php +++ b/src/AsyncAwsS3/AsyncAwsS3Adapter.php @@ -19,6 +19,7 @@ use League\Flysystem\FilesystemAdapter; use League\Flysystem\PathPrefixer; use League\Flysystem\StorageAttributes; +use League\Flysystem\UnableToCheckDirectoryExistence; use League\Flysystem\UnableToCheckFileExistence; use League\Flysystem\UnableToCopyFile; use League\Flysystem\UnableToDeleteFile; @@ -31,6 +32,8 @@ use League\MimeTypeDetection\MimeTypeDetector; use Throwable; +use function trim; + class AsyncAwsS3Adapter implements FilesystemAdapter { /** @@ -254,6 +257,18 @@ public function fileSize(string $path): FileAttributes return $attributes; } + public function directoryExists(string $path): bool + { + try { + $prefix = $this->prefixer->prefixDirectoryPath($path); + $options = ['Bucket' => $this->bucket, 'Prefix' => $prefix, 'Delimiter' => '/']; + + return $this->client->listObjectsV2($options)->getKeyCount() > 0; + } catch (Throwable $exception) { + throw UnableToCheckDirectoryExistence::forLocation($path, $exception); + } + } + public function listContents(string $path, bool $deep): iterable { $prefix = trim($this->prefixer->prefixPath($path), '/'); diff --git a/src/AsyncAwsS3/AsyncAwsS3AdapterTest.php b/src/AsyncAwsS3/AsyncAwsS3AdapterTest.php index 72171c730..72f20145e 100644 --- a/src/AsyncAwsS3/AsyncAwsS3AdapterTest.php +++ b/src/AsyncAwsS3/AsyncAwsS3AdapterTest.php @@ -6,6 +6,7 @@ use AsyncAws\Core\Exception\Http\ClientException; +use AsyncAws\Core\Exception\Http\NetworkException; use AsyncAws\Core\Test\Http\SimpleMockedResponse; use AsyncAws\Core\Test\ResultMockFactory; @@ -18,6 +19,7 @@ use League\Flysystem\Config; use League\Flysystem\FileAttributes; use League\Flysystem\FilesystemAdapter; +use League\Flysystem\Ftp\UnableToConnectToFtpHost; use League\Flysystem\StorageAttributes; use League\Flysystem\UnableToCheckFileExistence; use League\Flysystem\UnableToDeleteFile; @@ -49,6 +51,12 @@ class AsyncAwsS3AdapterTest extends FilesystemAdapterTestCase */ private static $stubS3Client; + protected function setUp(): void + { + parent::setUp(); + $this->retryOnException(NetworkException::class); + } + public static function setUpBeforeClass(): void { static::$adapterPrefix = 'ci/' . bin2hex(random_bytes(10)); diff --git a/src/AsyncAwsS3/composer.json b/src/AsyncAwsS3/composer.json index 178f59907..bcc2c09c0 100644 --- a/src/AsyncAwsS3/composer.json +++ b/src/AsyncAwsS3/composer.json @@ -9,14 +9,17 @@ } }, "require": { - "php": "^7.2 || ^8.0", - "league/flysystem": "^2.0.0", + "php": "^8.0.2", + "league/flysystem": "^2.0.0 || ^3.0.0", "league/mime-type-detection": "^1.0.0", "async-aws/s3": "^1.5" }, "require-dev": { "async-aws/simple-s3": "^1.0" }, + "conflict": { + "symfony/http-client": "<5.2" + }, "license": "MIT", "authors": [ { diff --git a/src/AwsS3V3/AwsS3V3Adapter.php b/src/AwsS3V3/AwsS3V3Adapter.php index 15b7eb26d..f95986012 100644 --- a/src/AwsS3V3/AwsS3V3Adapter.php +++ b/src/AwsS3V3/AwsS3V3Adapter.php @@ -14,6 +14,7 @@ use League\Flysystem\FilesystemOperationFailed; use League\Flysystem\PathPrefixer; use League\Flysystem\StorageAttributes; +use League\Flysystem\UnableToCheckDirectoryExistence; use League\Flysystem\UnableToCheckFileExistence; use League\Flysystem\UnableToCopyFile; use League\Flysystem\UnableToDeleteFile; @@ -28,6 +29,8 @@ use Psr\Http\Message\StreamInterface; use Throwable; +use function trim; + class AwsS3V3Adapter implements FilesystemAdapter { /** @@ -57,6 +60,16 @@ class AwsS3V3Adapter implements FilesystemAdapter 'Tagging', 'WebsiteRedirectLocation', ]; + /** + * @var string[] + */ + public const MUP_AVAILABLE_OPTIONS = [ + 'before_upload', + 'concurrency', + 'mup_threshold', + 'params', + 'part_size', + ]; /** * @var string[] @@ -130,6 +143,19 @@ public function fileExists(string $path): bool } } + public function directoryExists(string $path): bool + { + try { + $prefix = $this->prefixer->prefixDirectoryPath($path); + $options = ['Bucket' => $this->bucket, 'Prefix' => $prefix, 'Delimiter' => '/']; + $command = $this->client->getCommand('ListObjects', $options); + + return $this->client->execute($command)->hasKey('Contents'); + } catch (Throwable $exception) { + throw UnableToCheckDirectoryExistence::forLocation($path, $exception); + } + } + public function write(string $path, string $contents, Config $config): void { $this->upload($path, $contents, $config); @@ -145,14 +171,14 @@ private function upload(string $path, $body, Config $config): void $key = $this->prefixer->prefixPath($path); $options = $this->createOptionsFromConfig($config); $acl = $options['ACL'] ?? $this->determineAcl($config); - $shouldDetermineMimetype = $body !== '' && ! array_key_exists('ContentType', $options); + $shouldDetermineMimetype = $body !== '' && ! array_key_exists('ContentType', $options['params']); if ($shouldDetermineMimetype && $mimeType = $this->mimeTypeDetector->detectMimeType($key, $body)) { - $options['ContentType'] = $mimeType; + $options['params']['ContentType'] = $mimeType; } try { - $this->client->upload($this->bucket, $key, $body, $acl, ['params' => $options]); + $this->client->upload($this->bucket, $key, $body, $acl, $options); } catch (Throwable $exception) { throw UnableToWriteFile::atLocation($path, '', $exception); } @@ -167,11 +193,19 @@ private function determineAcl(Config $config): string private function createOptionsFromConfig(Config $config): array { - $options = []; + $options = ['params' => []]; foreach (static::AVAILABLE_OPTIONS as $option) { $value = $config->get($option, '__NOT_SET__'); + if ($value !== '__NOT_SET__') { + $options['params'][$option] = $value; + } + } + + foreach (static::MUP_AVAILABLE_OPTIONS as $option) { + $value = $config->get($option, '__NOT_SET__'); + if ($value !== '__NOT_SET__') { $options[$option] = $value; } @@ -294,12 +328,7 @@ private function mapS3ObjectMetadata(array $metadata, string $path = null): Stor $lastModified = $dateTime instanceof DateTimeResult ? $dateTime->getTimeStamp() : null; return new FileAttributes( - $path, - $fileSize, - null, - $lastModified, - $mimetype, - $this->extractExtraMetadata($metadata) + $path, $fileSize, null, $lastModified, $mimetype, $this->extractExtraMetadata($metadata) ); } @@ -406,7 +435,7 @@ public function copy(string $source, string $destination, Config $config): void $this->bucket, $this->prefixer->prefixPath($destination), $this->visibility->visibilityToAcl($visibility), - $this->createOptionsFromConfig($config) + $this->createOptionsFromConfig($config)['params'] ); } catch (Throwable $exception) { throw UnableToCopyFile::fromLocationTo($source, $destination, $exception); diff --git a/src/AwsS3V3/composer.json b/src/AwsS3V3/composer.json index 01170ee0c..5869864dd 100644 --- a/src/AwsS3V3/composer.json +++ b/src/AwsS3V3/composer.json @@ -9,13 +9,14 @@ } }, "require": { - "php": "^7.2 || ^8.0", - "league/flysystem": "^2.0.0", + "php": "^8.0.2", + "league/flysystem": "^2.0.0 || ^3.0.0", "league/mime-type-detection": "^1.0.0", "aws/aws-sdk-php": "^3.132.4" }, "conflict": { - "guzzlehttp/ringphp": "<1.1.1" + "guzzlehttp/ringphp": "<1.1.1", + "guzzlehttp/guzzle": "<7.0" }, "license": "MIT", "authors": [ diff --git a/src/ExceptionInformationTest.php b/src/ExceptionInformationTest.php index eb747b4b4..a91cbb565 100644 --- a/src/ExceptionInformationTest.php +++ b/src/ExceptionInformationTest.php @@ -60,12 +60,30 @@ public function delete_file_exception_information(): void /** * @test */ - public function unable_to_check_file_existence(): void + public function unable_to_check_for_file_existence(): void { $exception = UnableToCheckFileExistence::forLocation('location'); $this->assertEquals(FilesystemOperationFailed::OPERATION_FILE_EXISTS, $exception->operation()); } + /** + * @test + */ + public function unable_to_check_for_existence(): void + { + $exception = UnableToCheckExistence::forLocation('location'); + $this->assertEquals(FilesystemOperationFailed::OPERATION_EXISTENCE_CHECK, $exception->operation()); + } + + /** + * @test + */ + public function unable_to_check_for_directory_existence(): void + { + $exception = UnableToCheckDirectoryExistence::forLocation('location'); + $this->assertEquals(FilesystemOperationFailed::OPERATION_DIRECTORY_EXISTS, $exception->operation()); + } + /** * @test */ diff --git a/src/Filesystem.php b/src/Filesystem.php index f66574d68..341fea0c2 100644 --- a/src/Filesystem.php +++ b/src/Filesystem.php @@ -36,6 +36,18 @@ public function fileExists(string $location): bool return $this->adapter->fileExists($this->pathNormalizer->normalizePath($location)); } + public function directoryExists(string $location): bool + { + return $this->adapter->directoryExists($this->pathNormalizer->normalizePath($location)); + } + + public function has(string $location): bool + { + $path = $this->pathNormalizer->normalizePath($location); + + return $this->adapter->fileExists($path) || $this->adapter->directoryExists($path); + } + public function write(string $location, string $contents, array $config = []): void { $this->adapter->write( diff --git a/src/FilesystemAdapter.php b/src/FilesystemAdapter.php index 6dcb51e40..714309f31 100644 --- a/src/FilesystemAdapter.php +++ b/src/FilesystemAdapter.php @@ -8,9 +8,16 @@ interface FilesystemAdapter { /** * @throws FilesystemException + * @throws UnableToCheckExistence */ public function fileExists(string $path): bool; + /** + * @throws FilesystemException + * @throws UnableToCheckExistence + */ + public function directoryExists(string $path): bool; + /** * @throws UnableToWriteFile * @throws FilesystemException diff --git a/src/FilesystemOperationFailed.php b/src/FilesystemOperationFailed.php index 1c0b6df76..1f61a6c4d 100644 --- a/src/FilesystemOperationFailed.php +++ b/src/FilesystemOperationFailed.php @@ -8,6 +8,8 @@ interface FilesystemOperationFailed extends FilesystemException { public const OPERATION_WRITE = 'WRITE'; public const OPERATION_UPDATE = 'UPDATE'; + public const OPERATION_EXISTENCE_CHECK = 'EXISTENCE_CHECK'; + public const OPERATION_DIRECTORY_EXISTS = 'DIRECTORY_EXISTS'; public const OPERATION_FILE_EXISTS = 'FILE_EXISTS'; public const OPERATION_CREATE_DIRECTORY = 'CREATE_DIRECTORY'; public const OPERATION_DELETE = 'DELETE'; diff --git a/src/FilesystemReader.php b/src/FilesystemReader.php index 63145d091..d08743f92 100644 --- a/src/FilesystemReader.php +++ b/src/FilesystemReader.php @@ -15,10 +15,22 @@ interface FilesystemReader /** * @throws FilesystemException - * @throws UnableToCheckFileExistence + * @throws UnableToCheckExistence */ public function fileExists(string $location): bool; + /** + * @throws FilesystemException + * @throws UnableToCheckExistence + */ + public function directoryExists(string $location): bool; + + /** + * @throws FilesystemException + * @throws UnableToCheckExistence + */ + public function has(string $location): bool; + /** * @throws UnableToReadFile * @throws FilesystemException diff --git a/src/FilesystemTest.php b/src/FilesystemTest.php index 2b79d7669..35bdfb73a 100644 --- a/src/FilesystemTest.php +++ b/src/FilesystemTest.php @@ -103,6 +103,20 @@ public function checking_if_files_exist(): void $this->assertFalse($otherFileExists); } + /** + * @test + */ + public function checking_if_directories_exist(): void + { + $this->filesystem->createDirectory('existing-directory'); + + $existingDirectory = $this->filesystem->directoryExists('existing-directory'); + $notExistingDirectory = $this->filesystem->directoryExists('not-existing-directory'); + + $this->assertTrue($existingDirectory); + $this->assertFalse($notExistingDirectory); + } + /** * @test */ diff --git a/src/Ftp/FtpAdapter.php b/src/Ftp/FtpAdapter.php index a32922a5e..976fa3841 100644 --- a/src/Ftp/FtpAdapter.php +++ b/src/Ftp/FtpAdapter.php @@ -10,6 +10,7 @@ use League\Flysystem\DirectoryAttributes; use League\Flysystem\FileAttributes; use League\Flysystem\FilesystemAdapter; +use League\Flysystem\FilesystemException; use League\Flysystem\PathPrefixer; use League\Flysystem\StorageAttributes; use League\Flysystem\UnableToCopyFile; @@ -622,4 +623,11 @@ private function hasFtpConnection(): bool { return $this->connection instanceof \FTP\Connection || is_resource($this->connection); } + + public function directoryExists(string $path): bool + { + $connection = $this->connection(); + + return @ftp_chdir($connection, $path) === true; + } } diff --git a/src/Ftp/composer.json b/src/Ftp/composer.json index d9d1b56ad..35339e241 100644 --- a/src/Ftp/composer.json +++ b/src/Ftp/composer.json @@ -10,9 +10,9 @@ } }, "require": { - "php": "^7.2 || ^8.0", + "php": "^8.0.2", "ext-ftp": "*", - "league/flysystem": "^2.0.0", + "league/flysystem": "^2.0.0 || ^3.0.0", "league/mime-type-detection": "^1.0.0" }, "license": "MIT", diff --git a/src/GoogleCloudStorage/GoogleCloudStorageAdapter.php b/src/GoogleCloudStorage/GoogleCloudStorageAdapter.php index 7bc6a3e60..0acf5211f 100644 --- a/src/GoogleCloudStorage/GoogleCloudStorageAdapter.php +++ b/src/GoogleCloudStorage/GoogleCloudStorageAdapter.php @@ -13,6 +13,8 @@ use League\Flysystem\FilesystemAdapter; use League\Flysystem\PathPrefixer; use League\Flysystem\StorageAttributes; +use League\Flysystem\UnableToCheckDirectoryExistence; +use League\Flysystem\UnableToCheckFileExistence; use League\Flysystem\UnableToCopyFile; use League\Flysystem\UnableToDeleteDirectory; use League\Flysystem\UnableToDeleteFile; @@ -24,6 +26,12 @@ use League\Flysystem\Visibility; use Throwable; +use function array_key_exists; +use function count; +use function rtrim; +use function sprintf; +use function strlen; + class GoogleCloudStorageAdapter implements FilesystemAdapter { /** @@ -46,8 +54,12 @@ class GoogleCloudStorageAdapter implements FilesystemAdapter */ private $defaultVisibility; - public function __construct(Bucket $bucket, string $prefix = '', VisibilityHandler $visibilityHandler = null, string $defaultVisibility = Visibility::PRIVATE) - { + public function __construct( + Bucket $bucket, + string $prefix = '', + VisibilityHandler $visibilityHandler = null, + string $defaultVisibility = Visibility::PRIVATE + ) { $this->bucket = $bucket; $this->prefixer = new PathPrefixer($prefix); $this->visibilityHandler = $visibilityHandler ?: new PortableVisibilityHandler(); @@ -58,7 +70,41 @@ public function fileExists(string $path): bool { $prefixedPath = $this->prefixer->prefixPath($path); - return $this->bucket->object($prefixedPath)->exists(); + try { + return $this->bucket->object($prefixedPath)->exists(); + } catch (Throwable $exception) { + throw UnableToCheckFileExistence::forLocation($path, $exception); + } + } + + public function directoryExists(string $path): bool + { + $prefixedPath = $this->prefixer->prefixPath($path); + $options = [ + 'delimiter' => '/', + 'includeTrailingDelimiter' => true, + ]; + + if (strlen($prefixedPath) > 0) { + $options = ['prefix' => rtrim($prefixedPath, '/') . '/']; + } + + try { + $objects = $this->bucket->objects($options); + } catch (Throwable $exception) { + throw UnableToCheckDirectoryExistence::forLocation($path, $exception); + } + + if (count($objects->prefixes()) > 0) { + return true; + } + + /** @var StorageObject $object */ + foreach ($objects as $object) { + return true; + } + + return false; } public function write(string $path, string $contents, Config $config): void @@ -109,9 +155,7 @@ public function readStream(string $path) $prefixedPath = $this->prefixer->prefixPath($path); try { - $stream = $this->bucket->object($prefixedPath) - ->downloadAsStream() - ->detach(); + $stream = $this->bucket->object($prefixedPath)->downloadAsStream()->detach(); } catch (Throwable $exception) { throw UnableToReadFile::fromLocation($path, '', $exception); } @@ -120,6 +164,7 @@ public function readStream(string $path) if ( ! is_resource($stream)) { throw UnableToReadFile::fromLocation($path, 'Downloaded object does not contain a file resource.'); } + // @codeCoverageIgnoreEnd return $stream; @@ -146,6 +191,10 @@ public function deleteDirectory(string $path): void foreach ($listing as $attributes) { $this->delete($attributes->path()); } + + if ($path !== '') { + $this->delete(rtrim($path, '/') . '/'); + } } catch (Throwable $exception) { throw UnableToDeleteDirectory::atLocation($path, '', $exception); } @@ -153,8 +202,11 @@ public function deleteDirectory(string $path): void public function createDirectory(string $path, Config $config): void { - $prefixedPath = rtrim($this->prefixer->prefixPath($path), '/') . '/'; - $this->bucket->upload('', ['name' => $prefixedPath]); + $prefixedPath = $this->prefixer->prefixDirectoryPath($path); + + if ($prefixedPath !== '') { + $this->bucket->upload('', ['name' => $prefixedPath]); + } } public function setVisibility(string $path, string $visibility): void diff --git a/src/GoogleCloudStorage/GoogleCloudStorageAdapterTest.php b/src/GoogleCloudStorage/GoogleCloudStorageAdapterTest.php index f86ff6777..cd563c70b 100644 --- a/src/GoogleCloudStorage/GoogleCloudStorageAdapterTest.php +++ b/src/GoogleCloudStorage/GoogleCloudStorageAdapterTest.php @@ -7,6 +7,7 @@ use League\Flysystem\AdapterTestUtilities\FilesystemAdapterTestCase; use League\Flysystem\Config; use League\Flysystem\FilesystemAdapter; +use League\Flysystem\PathPrefixer; use League\Flysystem\UnableToDeleteDirectory; use League\Flysystem\UnableToDeleteFile; use League\Flysystem\UnableToRetrieveMetadata; @@ -21,15 +22,23 @@ class GoogleCloudStorageAdapterTest extends FilesystemAdapterTestCase * @var string */ private static $adapterPrefix = 'ci'; - - /** - * @var StubBucket - */ - private static $bucket; + private static StubRiggedBucket $bucket; + private static PathPrefixer $prefixer; public static function setUpBeforeClass(): void { - static::$adapterPrefix = 'ci/' . bin2hex(random_bytes(10)); + static::$adapterPrefix = 'frank-ci'; // . bin2hex(random_bytes(10)); + static::$prefixer = new PathPrefixer(static::$adapterPrefix); + } + + public function prefixPath(string $path): string + { + return static::$prefixer->prefixPath($path); + } + + public function prefixDirectoryPath(string $path): string + { + return static::$prefixer->prefixDirectoryPath($path); } protected static function createFilesystemAdapter(): FilesystemAdapter @@ -44,12 +53,7 @@ protected static function createFilesystemAdapter(): FilesystemAdapter ]; $storageClient = new StubStorageClient($clientOptions); - $connection = $storageClient->connection(); - $projectId = $storageClient->projectId(); - - static::$bucket = $bucket = new StubBucket($connection, 'flysystem', [ - 'requesterProjectId' => $projectId, - ]); + static::$bucket = $bucket = $storageClient->bucket('flysystem'); return new GoogleCloudStorageAdapter($bucket, static::$adapterPrefix); } @@ -62,7 +66,7 @@ public function fetching_visibility_of_non_existing_file(): void $this->markTestSkipped(" Not relevant for this adapter since it's a missing ACL, which turns into a 404 which is the expected outcome - of a private visibility. 🤷‍♂️ + of a private visibility. ¯\_(ツ)_/¯ "); } @@ -89,7 +93,7 @@ public function listing_a_toplevel_directory(): void public function failing_to_write_a_file(): void { $adapter = $this->adapter(); - static::$bucket->failOnUpload(); + static::$bucket->failForUpload($this->prefixPath('something.txt')); $this->expectException(UnableToWriteFile::class); @@ -102,7 +106,7 @@ public function failing_to_write_a_file(): void public function failing_to_delete_a_file(): void { $adapter = $this->adapter(); - static::$bucket->withObject(static::$adapterPrefix . '/filename.txt')->failWhenDeleting(); + static::$bucket->failForObject($this->prefixPath('filename.txt')); $this->expectException(UnableToDeleteFile::class); @@ -116,7 +120,8 @@ public function failing_to_delete_a_directory(): void { $adapter = $this->adapter(); $this->givenWeHaveAnExistingFile('dir/filename.txt'); - static::$bucket->withObject(static::$adapterPrefix . '/dir/filename.txt')->failWhenDeleting(); + + static::$bucket->failForObject($this->prefixPath('dir/filename.txt')); $this->expectException(UnableToDeleteDirectory::class); @@ -129,7 +134,7 @@ public function failing_to_delete_a_directory(): void public function failing_to_retrieve_visibility(): void { $adapter = $this->adapter(); - static::$bucket->withObject(static::$adapterPrefix . '/filename.txt')->failWhenAccessingAcl(); + static::$bucket->failForObject($this->prefixPath('filename.txt')); $this->expectException(UnableToRetrieveMetadata::class); diff --git a/src/GoogleCloudStorage/StubBucket.php b/src/GoogleCloudStorage/StubBucket.php deleted file mode 100644 index bcd2d5d1c..000000000 --- a/src/GoogleCloudStorage/StubBucket.php +++ /dev/null @@ -1,64 +0,0 @@ - - */ - private $stubbedObjects; - - /** - * @var bool - */ - private $shouldFailOnUpload = false; - - public function __construct(ConnectionInterface $connection, $name, array $info = []) - { - parent::__construct($connection, $name, $info); - $this->theConnection = $connection; - } - - public function withObject(string $path): StubObject - { - return $this->stubbedObjects[$path] = new StubObject($this->theConnection, $path, '', parent::object($path)); - } - - public function object($name, array $options = []) - { - $object = $this->stubbedObjects[$name] ?? parent::object($name, $options); - unset($this->stubbedObjects[$name]); - - return $object; - } - - public function failOnUpload(): void - { - $this->shouldFailOnUpload = true; - } - - public function upload($data, array $options = []) - { - if ($this->shouldFailOnUpload) { - $this->shouldFailOnUpload = false; - throw new LogicException("Oh no!"); - } - - return parent::upload($data, $options); - } -} diff --git a/src/GoogleCloudStorage/StubObject.php b/src/GoogleCloudStorage/StubObject.php deleted file mode 100644 index 8b9781a3c..000000000 --- a/src/GoogleCloudStorage/StubObject.php +++ /dev/null @@ -1,73 +0,0 @@ -storageObject = $storageObject; - } - - public function failWhenAccessingAcl(): void - { - $this->shouldFailWhenAccessingAcl = true; - } - - public function acl() - { - if ($this->shouldFailWhenAccessingAcl) { - $this->shouldFailWhenAccessingAcl = false; - throw new LogicException("Something bad happened! Oh no!"); - } - - return $this->storageObject->acl(); - } - - public function failWhenDeleting(): void - { - $this->shouldFailWhenDeleting = true; - } - - /** - * @param array $options - */ - public function delete(array $options = []) - { - if ($this->shouldFailWhenDeleting) { - $this->shouldFailWhenDeleting = false; - throw new LogicException("Oh no!"); - } - - parent::delete($options); - } -} diff --git a/src/GoogleCloudStorage/StubRiggedBucket.php b/src/GoogleCloudStorage/StubRiggedBucket.php new file mode 100644 index 000000000..024e9454a --- /dev/null +++ b/src/GoogleCloudStorage/StubRiggedBucket.php @@ -0,0 +1,55 @@ +setupTrigger('object', $name, $throwable); + } + + public function failForUpload($name, ?Throwable $throwable = null): void + { + $this->setupTrigger('upload', $name, $throwable); + } + + public function object($name, array $options = []) + { + $this->pushTrigger('object', $name); + + return parent::object($name, $options); + } + + public function upload($data, array $options = []) + { + $this->pushTrigger('upload', $options['name'] ?? 'unknown-object-name'); + + return parent::upload($data, $options); + } + + private function setupTrigger(string $method, string $name, ?Throwable $throwable): void + { + $this->triggers[$method][$name] = $throwable ?: new LogicException('unknown error'); + } + + private function pushTrigger(string $method, string $name): void + { + $trigger = $this->triggers[$method][$name] ?? null; + + if ($trigger instanceof Throwable) { + unset($this->triggers[$method][$name]); + throw $trigger; + } + } +} diff --git a/src/GoogleCloudStorage/StubStorageClient.php b/src/GoogleCloudStorage/StubStorageClient.php index cc80dc8ec..1cad84b9f 100644 --- a/src/GoogleCloudStorage/StubStorageClient.php +++ b/src/GoogleCloudStorage/StubStorageClient.php @@ -4,23 +4,29 @@ namespace League\Flysystem\GoogleCloudStorage; -use Google\Cloud\Storage\Connection\ConnectionInterface; use Google\Cloud\Storage\StorageClient; class StubStorageClient extends StorageClient { + private ?StubRiggedBucket $riggedBucket = null; + + public function __construct(array $config = []) + { + parent::__construct($config); + } + /** * @var string|null */ protected $projectId; - public function connection(): ConnectionInterface - { - return $this->connection; - } - - public function projectId(): ?string + public function bucket($name, $userProject = false) { - return $this->projectId; + if ($name === 'flysystem' && ! $this->riggedBucket) { + $this->riggedBucket = new StubRiggedBucket($this->connection, 'flysystem', [ + 'requesterProjectId' => $this->projectId, + ]); + } + return $name === 'flysystem' ? $this->riggedBucket : parent::bucket($name, $userProject); } } diff --git a/src/GoogleCloudStorage/composer.json b/src/GoogleCloudStorage/composer.json index 686e8068a..cf47df51c 100644 --- a/src/GoogleCloudStorage/composer.json +++ b/src/GoogleCloudStorage/composer.json @@ -10,9 +10,9 @@ } }, "require": { - "php": "^7.2 || ^8.0", + "php": "^8.0.2", "google/cloud-storage": "^1.23", - "league/flysystem": "^2.0.0", + "league/flysystem": "^2.0.0 || ^3.0.0", "league/mime-type-detection": "^1.0.0" }, "license": "MIT", diff --git a/src/InMemory/InMemoryFilesystemAdapter.php b/src/InMemory/InMemoryFilesystemAdapter.php index 4c32bf167..d64c8d42c 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\FilesystemException; use League\Flysystem\UnableToCopyFile; use League\Flysystem\UnableToMoveFile; use League\Flysystem\UnableToReadFile; @@ -17,6 +18,10 @@ use League\MimeTypeDetection\FinfoMimeTypeDetector; use League\MimeTypeDetection\MimeTypeDetector; +use function array_keys; +use function rtrim; +use function strpos; + class InMemoryFilesystemAdapter implements FilesystemAdapter { const DUMMY_FILE_FOR_FORCED_LISTING_IN_FLYSYSTEM_TEST = '______DUMMY_FILE_FOR_FORCED_LISTING_IN_FLYSYSTEM_TEST'; @@ -107,6 +112,20 @@ public function createDirectory(string $path, Config $config): void $this->write($filePath, '', $config); } + public function directoryExists(string $path): bool + { + $prefix = $this->preparePath($path); + $prefix = rtrim($prefix, '/') . '/'; + + foreach (array_keys($this->files) as $path) { + if (strpos($path, $prefix) === 0) { + return true; + } + } + + return false; + } + public function setVisibility(string $path, string $visibility): void { $path = $this->preparePath($path); diff --git a/src/InMemory/composer.json b/src/InMemory/composer.json index 9bf7f732e..aba842930 100644 --- a/src/InMemory/composer.json +++ b/src/InMemory/composer.json @@ -11,9 +11,9 @@ } }, "require": { - "php": "^7.2 || ^8.0", + "php": "^8.0.2", "ext-fileinfo": "*", - "league/flysystem": "^2.0.0" + "league/flysystem": "^2.0.0 || ^3.0.0" }, "license": "MIT", "authors": [ diff --git a/src/Local/LocalFilesystemAdapter.php b/src/Local/LocalFilesystemAdapter.php index 83310f037..1b4ddaad8 100644 --- a/src/Local/LocalFilesystemAdapter.php +++ b/src/Local/LocalFilesystemAdapter.php @@ -213,7 +213,7 @@ public function listContents(string $path, bool $deep): iterable $permissions = octdec(substr(sprintf('%o', $fileInfo->getPerms()), -4)); $visibility = $isDirectory ? $this->visibility->inverseForDirectory($permissions) : $this->visibility->inverseForFile($permissions); - yield $isDirectory ? new DirectoryAttributes($path, $visibility, $lastModified) : new FileAttributes( + yield $isDirectory ? new DirectoryAttributes(str_replace('\\', '/', $path), $visibility, $lastModified) : new FileAttributes( str_replace('\\', '/', $path), $fileInfo->getSize(), $visibility, @@ -304,6 +304,13 @@ public function fileExists(string $location): bool return is_file($location); } + public function directoryExists(string $location): bool + { + $location = $this->prefixer->prefixPath($location); + + return is_dir($location); + } + public function createDirectory(string $path, Config $config): void { $location = $this->prefixer->prefixPath($path); diff --git a/src/Local/LocalFilesystemAdapterTest.php b/src/Local/LocalFilesystemAdapterTest.php index fc924d794..c9aa0050b 100644 --- a/src/Local/LocalFilesystemAdapterTest.php +++ b/src/Local/LocalFilesystemAdapterTest.php @@ -4,6 +4,8 @@ namespace League\Flysystem\Local; +use function strnatcasecmp; +use function usort; use const LOCK_EX; use League\Flysystem\AdapterTestUtilities\FilesystemAdapterTestCase; use League\Flysystem\Config; @@ -302,13 +304,17 @@ public function retrieving_visibility_while_listing_directory_contents(): void /** @var Traversable $contentListing */ $contentListing = $adapter->listContents('/', true); + $listing = iterator_to_array($contentListing); + usort($listing, function(StorageAttributes $a, StorageAttributes $b) { + return strnatcasecmp($a->path(), $b->path()); + }); /** * @var StorageAttributes $publicDirectoryAttributes * @var StorageAttributes $privateFileAttributes * @var StorageAttributes $privateDirectoryAttributes * @var StorageAttributes $publicFileAttributes */ - [$publicDirectoryAttributes, $privateFileAttributes, $privateDirectoryAttributes, $publicFileAttributes] = iterator_to_array($contentListing); + [$privateDirectoryAttributes, $publicFileAttributes, $publicDirectoryAttributes, $privateFileAttributes] = $listing; $this->assertEquals('public', $publicDirectoryAttributes->visibility()); $this->assertEquals('private', $privateFileAttributes->visibility()); diff --git a/src/MountManager.php b/src/MountManager.php index b5a777353..3dfd93e25 100644 --- a/src/MountManager.php +++ b/src/MountManager.php @@ -4,6 +4,8 @@ namespace League\Flysystem; +use Throwable; + use function sprintf; class MountManager implements FilesystemOperator @@ -30,11 +32,35 @@ public function fileExists(string $location): bool try { return $filesystem->fileExists($path); - } catch (UnableToCheckFileExistence $exception) { + } catch (Throwable $exception) { throw UnableToCheckFileExistence::forLocation($location, $exception); } } + public function has(string $location): bool + { + /** @var FilesystemOperator $filesystem */ + [$filesystem, $path] = $this->determineFilesystemAndPath($location); + + try { + return $filesystem->fileExists($path) || $filesystem->directoryExists($path); + } catch (Throwable $exception) { + throw UnableToCheckExistence::forLocation($location, $exception); + } + } + + public function directoryExists(string $location): bool + { + /** @var FilesystemOperator $filesystem */ + [$filesystem, $path] = $this->determineFilesystemAndPath($location); + + try { + return $filesystem->directoryExists($path); + } catch (Throwable $exception) { + throw UnableToCheckDirectoryExistence::forLocation($location, $exception); + } + } + public function read(string $location): string { /** @var FilesystemOperator $filesystem */ diff --git a/src/MountManagerTest.php b/src/MountManagerTest.php index 2a694a615..0469f6ed5 100644 --- a/src/MountManagerTest.php +++ b/src/MountManagerTest.php @@ -6,7 +6,9 @@ use League\Flysystem\InMemory\InMemoryFilesystemAdapter; use PHPUnit\Framework\TestCase; +use function fclose; use function is_resource; +use function stream_get_contents; use function tmpfile; /** @@ -162,6 +164,94 @@ public function reading_a_file_as_a_stream(): void $this->assertEquals('contents', $contents); } + /** + * @test + */ + public function checking_existence_for_an_existing_file(): void + { + $this->secondFilesystem->write('location.txt', 'contents'); + + $existence = $this->mountManager->fileExists('second://location.txt'); + + $this->assertTrue($existence); + } + + /** + * @test + */ + public function checking_existence_for_an_non_existing_file(): void + { + $existence = $this->mountManager->fileExists('second://location.txt'); + + $this->assertFalse($existence); + } + + /** + * @test + */ + public function checking_existence_for_an_non_existing_directory(): void + { + $existence = $this->mountManager->directoryExists('second://some-directory'); + + $this->assertFalse($existence); + } + + /** + * @test + */ + public function checking_existence_for_an_existing_directory(): void + { + $this->secondFilesystem->write('nested/location.txt', 'contents'); + + $existence = $this->mountManager->directoryExists('second://nested'); + + $this->assertTrue($existence); + } + + /** + * @test + */ + public function checking_existence_for_an_existing_file_using_has(): void + { + $this->secondFilesystem->write('location.txt', 'contents'); + + $existence = $this->mountManager->has('second://location.txt'); + + $this->assertTrue($existence); + } + + /** + * @test + */ + public function checking_existence_for_an_non_existing_file_using_has(): void + { + $existence = $this->mountManager->has('second://location.txt'); + + $this->assertFalse($existence); + } + + /** + * @test + */ + public function checking_existence_for_an_non_existing_directory_using_has(): void + { + $existence = $this->mountManager->has('second://some-directory'); + + $this->assertFalse($existence); + } + + /** + * @test + */ + public function checking_existence_for_an_existing_directory_using_has(): void + { + $this->secondFilesystem->write('nested/location.txt', 'contents'); + + $existence = $this->mountManager->has('second://nested'); + + $this->assertTrue($existence); + } + /** * @test */ diff --git a/src/PathPrefixer.php b/src/PathPrefixer.php index b675b8e5c..2a9cd45d0 100644 --- a/src/PathPrefixer.php +++ b/src/PathPrefixer.php @@ -51,7 +51,7 @@ public function prefixDirectoryPath(string $path): string { $prefixedPath = $this->prefixPath(rtrim($path, '\\/')); - if ((substr($prefixedPath, -1) === $this->separator) || $prefixedPath === '') { + if ($prefixedPath === '' || substr($prefixedPath, -1) === $this->separator) { return $prefixedPath; } diff --git a/src/PhpseclibV2/SftpAdapter.php b/src/PhpseclibV2/SftpAdapter.php index 45b56fc66..aa2cf8049 100644 --- a/src/PhpseclibV2/SftpAdapter.php +++ b/src/PhpseclibV2/SftpAdapter.php @@ -11,6 +11,8 @@ use League\Flysystem\FilesystemException; use League\Flysystem\PathPrefixer; use League\Flysystem\StorageAttributes; +use League\Flysystem\UnableToCheckDirectoryExistence; +use League\Flysystem\UnableToCheckFileExistence; use League\Flysystem\UnableToCopyFile; use League\Flysystem\UnableToCreateDirectory; use League\Flysystem\UnableToMoveFile; @@ -63,7 +65,22 @@ public function fileExists(string $path): bool { $location = $this->prefixer->prefixPath($path); - return $this->connectionProvider->provideConnection()->is_file($location); + 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); + } } /** diff --git a/src/PhpseclibV2/composer.json b/src/PhpseclibV2/composer.json index 71bd19d49..7f13e85ea 100644 --- a/src/PhpseclibV2/composer.json +++ b/src/PhpseclibV2/composer.json @@ -8,8 +8,8 @@ } }, "require": { - "php": "^7.2 || ^8.0", - "league/flysystem": "^2.0.0", + "php": "^8.0.2", + "league/flysystem": "^2.0.0 || ^3.0.0", "league/mime-type-detection": "^1.0.0", "phpseclib/phpseclib": "^2.0" }, diff --git a/src/PhpseclibV3/SftpAdapter.php b/src/PhpseclibV3/SftpAdapter.php index 64a13ab15..e48cbaf07 100644 --- a/src/PhpseclibV3/SftpAdapter.php +++ b/src/PhpseclibV3/SftpAdapter.php @@ -11,6 +11,8 @@ use League\Flysystem\FilesystemException; use League\Flysystem\PathPrefixer; use League\Flysystem\StorageAttributes; +use League\Flysystem\UnableToCheckDirectoryExistence; +use League\Flysystem\UnableToCheckFileExistence; use League\Flysystem\UnableToCopyFile; use League\Flysystem\UnableToCreateDirectory; use League\Flysystem\UnableToMoveFile; @@ -63,7 +65,22 @@ public function fileExists(string $path): bool { $location = $this->prefixer->prefixPath($path); - return $this->connectionProvider->provideConnection()->is_file($location); + 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); + } } /** diff --git a/src/PhpseclibV3/composer.json b/src/PhpseclibV3/composer.json index ef8fd0b11..fdcba2b69 100644 --- a/src/PhpseclibV3/composer.json +++ b/src/PhpseclibV3/composer.json @@ -8,8 +8,8 @@ } }, "require": { - "php": "^7.2 || ^8.0", - "league/flysystem": "^2.0.0", + "php": "^8.0.2", + "league/flysystem": "^2.0.0 || ^3.0.0", "league/mime-type-detection": "^1.0.0", "phpseclib/phpseclib": "^3.0" }, diff --git a/src/UnableToCheckDirectoryExistence.php b/src/UnableToCheckDirectoryExistence.php new file mode 100644 index 000000000..73ce858b9 --- /dev/null +++ b/src/UnableToCheckDirectoryExistence.php @@ -0,0 +1,13 @@ +zipArchiveProvider->createZipArchive(); + $location = $this->pathPrefixer->prefixDirectoryPath($path); + + return $archive->statName($location) !== false; + } + public function setVisibility(string $path, string $visibility): void { $archive = $this->zipArchiveProvider->createZipArchive(); diff --git a/src/ZipArchive/ZipArchiveAdapterTest.php b/src/ZipArchive/ZipArchiveAdapterTest.php index 4b64da78f..7bbd5c550 100644 --- a/src/ZipArchive/ZipArchiveAdapterTest.php +++ b/src/ZipArchive/ZipArchiveAdapterTest.php @@ -17,6 +17,8 @@ use League\Flysystem\UnableToWriteFile; use League\Flysystem\Visibility; +use function iterator_to_array; + /** * @group zip */ @@ -136,7 +138,7 @@ public function deleting_a_directory(): void $this->adapter()->deleteDirectory('one'); $items = iterator_to_array($this->adapter()->listContents('', true)); - $this->assertCount(4, $items); + $this->assertCount(3, $items); } /** @@ -227,6 +229,20 @@ public function failing_to_set_visibility_because_the_file_does_not_exist(): voi $this->adapter()->setVisibility('path.txt', Visibility::PUBLIC); } + /** + * @test + */ + public function deleting_a_directory_with_files_in_it(): void + { + $this->givenWeHaveAnExistingFile('nested/path-a.txt'); + $this->givenWeHaveAnExistingFile('nested/path-b.txt'); + + $this->adapter()->deleteDirectory('nested'); + $listing = iterator_to_array($this->adapter()->listContents('', true)); + + self::assertEquals([], $listing); + } + /** * @test */ diff --git a/src/ZipArchive/composer.json b/src/ZipArchive/composer.json index 486963771..a15cbf7e3 100644 --- a/src/ZipArchive/composer.json +++ b/src/ZipArchive/composer.json @@ -10,9 +10,9 @@ } }, "require": { - "php": "^7.2 || ^8.0", + "php": "^8.0.2", "ext-zip": "*", - "league/flysystem": "^2.0.0", + "league/flysystem": "^2.0.0 || ^3.0.0", "league/mime-type-detection": "^1.0.0" }, "license": "MIT",