From f716044b0be9fd103c5445a0620daea0eb8c34cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 15 Jan 2024 10:36:00 +0100 Subject: [PATCH 01/10] GridFS Adapter --- .github/workflows/quality-assurance.yml | 2 + composer.json | 1 + docker-compose.yml | 4 + .../FilesystemAdapterTestCase.php | 19 +- src/GridFS/.gitattributes | 8 + .../.github/workflows/close-subsplit-prs.yaml | 30 ++ src/GridFS/GridFSAdapter.php | 413 ++++++++++++++++++ src/GridFS/GridFSAdapterTest.php | 245 +++++++++++ src/GridFS/LICENSE | 19 + src/GridFS/README.md | 9 + src/GridFS/composer.json | 21 + 11 files changed, 770 insertions(+), 1 deletion(-) create mode 100644 src/GridFS/.gitattributes create mode 100644 src/GridFS/.github/workflows/close-subsplit-prs.yaml create mode 100644 src/GridFS/GridFSAdapter.php create mode 100644 src/GridFS/GridFSAdapterTest.php create mode 100644 src/GridFS/LICENSE create mode 100644 src/GridFS/README.md create mode 100644 src/GridFS/composer.json diff --git a/.github/workflows/quality-assurance.yml b/.github/workflows/quality-assurance.yml index 36c2eb1e0..5fececfc0 100644 --- a/.github/workflows/quality-assurance.yml +++ b/.github/workflows/quality-assurance.yml @@ -25,6 +25,7 @@ env: FLYSYSTEM_AWS_S3_KEY: '${{ secrets.FLYSYSTEM_AWS_S3_KEY }}' FLYSYSTEM_AWS_S3_SECRET: '${{ secrets.FLYSYSTEM_AWS_S3_SECRET }}' FLYSYSTEM_AWS_S3_BUCKET: '${{ secrets.FLYSYSTEM_AWS_S3_BUCKET }}' + MONGODB_URI: 'mongodb://127.0.0.1:27017/' FLYSYSTEM_TEST_DANGEROUS_THINGS: "yes" FLYSYSTEM_TEST_SFTP: "yes" @@ -70,6 +71,7 @@ jobs: - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} + extensions: mongodb coverage: pcov tools: composer:v2 - run: composer update --no-progress ${{ matrix.composer-flags }} diff --git a/composer.json b/composer.json index e67ab457e..c7f84f569 100644 --- a/composer.json +++ b/composer.json @@ -35,6 +35,7 @@ "google/cloud-storage": "^1.23", "async-aws/s3": "^1.5 || ^2.0", "async-aws/simple-s3": "^1.1 || ^2.0", + "mongodb/mongodb": "^1.2", "sabre/dav": "^4.6.0", "guzzlehttp/psr7": "^2.6" }, diff --git a/docker-compose.yml b/docker-compose.yml index 9a0463ac8..9de0d87bc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,10 @@ --- version: "3" services: + mongodb: + image: mongo:7 + ports: + - "27017:27017" sabredav: image: php:8.1-alpine3.15 restart: always diff --git a/src/AdapterTestUtilities/FilesystemAdapterTestCase.php b/src/AdapterTestUtilities/FilesystemAdapterTestCase.php index 80ac79260..69d0530a8 100644 --- a/src/AdapterTestUtilities/FilesystemAdapterTestCase.php +++ b/src/AdapterTestUtilities/FilesystemAdapterTestCase.php @@ -14,6 +14,7 @@ use League\Flysystem\FileAttributes; use League\Flysystem\FilesystemAdapter; use League\Flysystem\StorageAttributes; +use League\Flysystem\UnableToCopyFile; use League\Flysystem\UnableToMoveFile; use League\Flysystem\UnableToProvideChecksum; use League\Flysystem\UnableToReadFile; @@ -151,7 +152,9 @@ public function writing_a_file_with_a_stream(): void $adapter = $this->adapter(); $writeStream = stream_with_contents('contents'); - $adapter->writeStream('path.txt', $writeStream, new Config()); + $adapter->writeStream('path.txt', $writeStream, new Config([ + Config::OPTION_VISIBILITY => Visibility::PUBLIC, + ])); if (is_resource($writeStream)) { fclose($writeStream); @@ -592,10 +595,23 @@ public function copying_a_file(): void $this->assertTrue($adapter->fileExists('source.txt')); $this->assertTrue($adapter->fileExists('destination.txt')); $this->assertEquals(Visibility::PUBLIC, $adapter->visibility('destination.txt')->visibility()); + $this->assertEquals('text/plain', $adapter->mimeType('destination.txt')->mimeType()); $this->assertEquals('contents to be copied', $adapter->read('destination.txt')); }); } + /** + * @test + */ + public function copying_a_file_that_does_not_exist(): void + { + $this->expectException(UnableToCopyFile::class); + + $this->runScenario(function () { + $this->adapter()->copy('source.txt', 'destination.txt', new Config()); + }); + } + /** * @test */ @@ -640,6 +656,7 @@ public function moving_a_file(): void 'After moving, a file should be present at the new location.' ); $this->assertEquals(Visibility::PUBLIC, $adapter->visibility('destination.txt')->visibility()); + $this->assertEquals('text/plain', $adapter->mimeType('destination.txt')->mimeType()); $this->assertEquals('contents to be copied', $adapter->read('destination.txt')); }); } diff --git a/src/GridFS/.gitattributes b/src/GridFS/.gitattributes new file mode 100644 index 000000000..9e1465fb3 --- /dev/null +++ b/src/GridFS/.gitattributes @@ -0,0 +1,8 @@ +* 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/GridFS/.github/workflows/close-subsplit-prs.yaml b/src/GridFS/.github/workflows/close-subsplit-prs.yaml new file mode 100644 index 000000000..e9b702a96 --- /dev/null +++ b/src/GridFS/.github/workflows/close-subsplit-prs.yaml @@ -0,0 +1,30 @@ +--- +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/GridFS/GridFSAdapter.php b/src/GridFS/GridFSAdapter.php new file mode 100644 index 000000000..e8ed50d90 --- /dev/null +++ b/src/GridFS/GridFSAdapter.php @@ -0,0 +1,413 @@ +bucket = $bucket; + $this->prefixer = new PathPrefixer($prefix); + $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector(); + } + + public function fileExists(string $path): bool + { + $file = $this->findFile($path); + + return $file !== null; + } + + public function directoryExists(string $path): bool + { + // A directory exists if at least one file exists with a path starting with the directory name + $files = $this->listContents($path, true); + + foreach ($files as $file) { + return true; + } + + return false; + } + + public function write(string $path, string $contents, Config $config): void + { + if (str_ends_with($path, '/')) { + throw UnableToWriteFile::atLocation($path, 'file path cannot end with a slash'); + } + + $filename = $this->prefixer->prefixPath($path); + $options = [ + 'metadata' => $config->get('metadata', []), + ]; + if ($visibility = $config->get(Config::OPTION_VISIBILITY)) { + $options['metadata'][self::METADATA_VISIBILITY] = $visibility; + } + if (($mimeType = $config->get('mimetype')) || ($mimeType = $this->mimeTypeDetector->detectMimeType($path, $contents))) { + $options['metadata'][self::METADATA_MIMETYPE] = $mimeType; + } + + try { + $stream = $this->bucket->openUploadStream($filename, $options); + fwrite($stream, $contents); + fclose($stream); + } catch (Exception $exception) { + throw UnableToWriteFile::atLocation($path, $exception->getMessage(), $exception); + } + } + + public function writeStream(string $path, $contents, Config $config): void + { + if (str_ends_with($path, '/')) { + throw UnableToWriteFile::atLocation($path, 'file path cannot end with a slash'); + } + + $filename = $this->prefixer->prefixPath($path); + $options = []; + if ($visibility = $config->get(Config::OPTION_VISIBILITY)) { + $options['metadata'][self::METADATA_VISIBILITY] = $visibility; + } + if (($mimetype = $config->get('mimetype')) || ($mimetype = $this->mimeTypeDetector->detectMimeTypeFromPath($path))) { + $options['metadata'][self::METADATA_MIMETYPE] = $mimetype; + } + + try { + $this->bucket->uploadFromStream($filename, $contents, $options); + } catch (Exception $exception) { + throw UnableToWriteFile::atLocation($path, $exception->getMessage(), $exception); + } + } + + public function read(string $path): string + { + return stream_get_contents($this->readStream($path)); + } + + public function readStream(string $path) + { + if (str_ends_with($path, '/')) { + throw UnableToReadFile::fromLocation($path, 'file path cannot end with a slash'); + } + + $file = $this->findFile($path); + + if ($file === null) { + throw UnableToReadFile::fromLocation($path, 'file does not exist'); + } + + try { + return $this->bucket->openDownloadStream($file['_id']); + } catch (Exception $exception) { + throw UnableToReadFile::fromLocation($path, $exception->getMessage(), $exception); + } + } + + public function delete(string $path): void + { + if (str_ends_with($path, '/')) { + throw UnableToDeleteFile::atLocation($path, 'file path cannot end with a slash'); + } + + // Deleting a file that does not exist is no-op + while ($file = $this->findFile($path)) { + try { + $this->bucket->delete($file['_id']); + } catch (Exception $exception) { + throw UnableToDeleteFile::atLocation($path, $exception->getMessage(), $exception); + } + } + } + + public function deleteDirectory(string $path): void + { + foreach ($this->listContents($path, true) as $file) { + $this->bucket->delete($file->extraMetadata()['_id']); + } + } + + public function createDirectory(string $path, Config $config): void + { + $dirname = $this->prefixer->prefixDirectoryPath($path); + + $options = [ + 'metadata' => $config->get('metadata', []) + [self::METADATA_DIRECTORY => true], + ]; + + if ($visibility = $config->get(Config::OPTION_VISIBILITY)) { + $options['metadata'][self::METADATA_VISIBILITY] = $visibility; + } + + try { + $stream = $this->bucket->openUploadStream($dirname, $options); + fwrite($stream, ''); + fclose($stream); + } catch (Exception $exception) { + throw UnableToCreateDirectory::atLocation($path, $exception->getMessage(), $exception); + } + } + + public function setVisibility(string $path, string $visibility): void + { + $file = $this->findFile($path); + + if ($file === null) { + throw UnableToSetVisibility::atLocation($path, 'file does not exist'); + } + + try { + $this->bucket->getFilesCollection()->updateOne( + ['_id' => $file['_id']], + ['$set' => ['metadata.' . self::METADATA_VISIBILITY => $visibility]], + ); + } catch (Exception $exception) { + throw UnableToSetVisibility::atLocation($path, $exception->getMessage(), $exception); + } + } + + public function visibility(string $path): FileAttributes + { + $file = $this->findFile($path); + + if ($file === null) { + throw UnableToRetrieveMetadata::mimeType($path, 'file does not exist'); + } + + return new FileAttributes($path, null, $file['metadata'][self::METADATA_VISIBILITY] ?? null); + } + + public function mimeType(string $path): FileAttributes + { + if (str_ends_with($path, '/')) { + throw UnableToRetrieveMetadata::mimeType($path, 'file path cannot end with a slash'); + } + + $file = $this->findFile($path); + + if ($file === null) { + throw UnableToRetrieveMetadata::mimeType($path, 'file does not exist'); + } + + if ( ! isset($file['metadata'][self::METADATA_MIMETYPE])) { + throw UnableToRetrieveMetadata::mimeType($path, 'unknown'); + } + + return new FileAttributes($path, null, null, null, $file['metadata'][self::METADATA_MIMETYPE]); + } + + public function lastModified(string $path): FileAttributes + { + if (str_ends_with($path, '/')) { + throw UnableToRetrieveMetadata::lastModified($path, 'file path cannot end with a slash'); + } + + $file = $this->findFile($path); + + if ($file === null) { + throw UnableToRetrieveMetadata::lastModified($path, 'file does not exist'); + } + + return new FileAttributes($path, null, null, $file['uploadDate']->toDateTime()->getTimestamp()); + } + + public function fileSize(string $path): FileAttributes + { + if (str_ends_with($path, '/')) { + throw UnableToRetrieveMetadata::fileSize($path, 'file path cannot end with a slash'); + } + + $file = $this->findFile($path); + + if ($file === null) { + throw UnableToRetrieveMetadata::fileSize($path, 'file does not exist'); + } + + return new FileAttributes($path, $file['length']); + } + + public function listContents(string $path, bool $deep): iterable + { + $path = $this->prefixer->prefixDirectoryPath($path); + + $pathdeep = 0; + $pipeline = []; + if ($path !== '') { + $pathdeep = substr_count($path, '/'); + // Exclude files that do not start with the expected path + $pipeline[] = ['$match' => ['filename' => ['$regex' => new Regex('^' . preg_quote($path))]]]; + } + // Get the last revision of each file + $pipeline[] = ['$sort' => ['filename' => 1, 'uploadDate' => -1]]; + + if ($deep === false) { + $pipeline[] = ['$addFields' => ['splitpath' => ['$split' => ['$filename', '/']]]]; + $pipeline[] = ['$group' => [ + // The same name could be used as a filename and as part of the path of other files + '_id' => [ + 'basename' => ['$arrayElemAt' => ['$splitpath', $pathdeep]], + 'isDir' => ['$ne' => [['$size' => '$splitpath'], $pathdeep + 1]], + ], + // Get the metadata of the last revision of each file + 'file' => ['$first' => '$$ROOT'], + // The "lastModified" date is the date of the last uploaded file in the directory + 'uploadDate' => ['$max' => '$uploadDate'], + ]]; + + $files = $this->bucket->getFilesCollection()->aggregate( + $pipeline, + ['typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array']] + ); + + foreach ($files as $file) { + if ($file['_id']['isDir']) { + yield new DirectoryAttributes( + $this->prefixer->stripDirectoryPrefix($path . $file['_id']['basename']), + null, + $file['uploadDate']->toDateTime()->getTimestamp(), + ); + } else { + $file = $file['file']; + yield new FileAttributes( + $this->prefixer->stripPrefix($file['filename']), + $file['length'], + $file['metadata'][self::METADATA_VISIBILITY] ?? null, + $file['uploadDate']->toDateTime()->getTimestamp(), + $file['metadata'][self::METADATA_MIMETYPE] ?? null, + $file, + ); + } + } + } else { + // Get the metadata of the last revision of each file + $pipeline[] = ['$group' => [ + '_id' => '$filename', + 'file' => ['$first' => '$$ROOT'], + ]]; + + $files = $this->bucket->getFilesCollection()->aggregate( + $pipeline, + ['typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array']], + ); + + foreach ($files as $file) { + $file = $file['file']; + if (str_ends_with($file['filename'], '/')) { + // Empty files with a trailing slash are markers for directories, for Flysystem + yield new DirectoryAttributes( + $this->prefixer->stripDirectoryPrefix($file['filename']), + $file['metadata'][self::METADATA_VISIBILITY] ?? null, + $file['uploadDate']->toDateTime()->getTimestamp(), + $file, + ); + } else { + yield new FileAttributes( + $this->prefixer->stripPrefix($file['filename']), + $file['length'], + $file['metadata'][self::METADATA_VISIBILITY] ?? null, + $file['uploadDate']->toDateTime()->getTimestamp(), + $file['metadata'][self::METADATA_MIMETYPE] ?? null, + $file, + ); + } + } + } + } + + public function move(string $source, string $destination, Config $config): void + { + $file = $this->findFile($source); + + if ($file === null) { + throw UnableToMoveFile::because('file does not exist', $source, $destination); + } + + try { + $this->bucket->getFilesCollection()->updateMany( + ['filename' => $file['filename']], + ['$set' => ['filename' => $this->prefixer->prefixPath($destination)]], + ); + } catch (Exception $exception) { + throw UnableToMoveFile::fromLocationTo($source, $destination, $exception); + } + } + + public function copy(string $source, string $destination, Config $config): void + { + $file = $this->findFile($source); + + if ($file === null) { + throw UnableToCopyFile::fromLocationTo( + $source, + $destination, + ); + } + + $options = []; + if (($visibility = $config->get(Config::OPTION_VISIBILITY)) || $visibility = $file['metadata'][self::METADATA_VISIBILITY] ?? null) { + $options['metadata'][self::METADATA_VISIBILITY] = $visibility; + } + if (($mimetype = $config->get('mimetype')) || $mimetype = $file['metadata'][self::METADATA_MIMETYPE] ?? null) { + $options['metadata'][self::METADATA_MIMETYPE] = $mimetype; + } + + try { + $stream = $this->bucket->openDownloadStream($file['_id']); + $this->bucket->uploadFromStream($this->prefixer->prefixPath($destination), $stream, $options); + } catch (Exception $exception) { + throw UnableToCopyFile::fromLocationTo($source, $destination, $exception); + } + } + + /** + * @return array{_id:ObjectId, length:int, chunkSize:int, uploadDate:UTCDateTime, filename:string, metadata?:array{contentType?:string, flysystem_visibility?:string}}|null + */ + private function findFile(string $path): ?array + { + $filename = $this->prefixer->prefixPath($path); + $files = $this->bucket->find( + ['filename' => $filename], + [ + 'sort' => ['uploadDate' => -1], + 'limit' => 1, + 'typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array'], + ], + ); + + return $files->toArray()[0] ?? null; + } +} diff --git a/src/GridFS/GridFSAdapterTest.php b/src/GridFS/GridFSAdapterTest.php new file mode 100644 index 000000000..3ba508d42 --- /dev/null +++ b/src/GridFS/GridFSAdapterTest.php @@ -0,0 +1,245 @@ +drop(); + + parent::tearDownAfterClass(); + } + + /** + * @test + */ + public function fetching_last_modified_of_a_directory(): void + { + $this->expectException(UnableToRetrieveMetadata::class); + + $adapter = $this->adapter(); + + $this->runScenario(function () use ($adapter) { + $adapter->createDirectory('path', new Config()); + $adapter->lastModified('path/'); + }); + } + + /** + * @test + */ + public function fetching_mime_type_of_a_directory(): void + { + $this->expectException(UnableToRetrieveMetadata::class); + + $adapter = $this->adapter(); + + $this->runScenario(function () use ($adapter) { + $adapter->createDirectory('path', new Config()); + $adapter->mimeType('path/'); + }); + } + + /** + * @test + */ + public function reading_a_file_with_trailing_slash(): void + { + $this->expectException(UnableToReadFile::class); + $this->adapter()->read('foo/'); + } + + /** + * @test + */ + public function reading_a_file_stream_with_trailing_slash(): void + { + $this->expectException(UnableToReadFile::class); + $this->adapter()->readStream('foo/'); + } + + /** + * @test + */ + public function writing_a_file_with_trailing_slash(): void + { + $this->expectException(UnableToWriteFile::class); + $this->adapter()->write('foo/', 'contents', new Config()); + } + + /** + * @test + */ + public function writing_a_file_stream_with_trailing_slash(): void + { + $this->expectException(UnableToWriteFile::class); + $writeStream = stream_with_contents('contents'); + $this->adapter()->writeStream('foo/', $writeStream, new Config()); + } + + /** + * @test + */ + public function writing_a_file_with_a_invalid_stream(): void + { + $this->expectException(UnableToWriteFile::class); + // @phpstan-ignore argument.type + $this->adapter()->writeStream('file.txt', 'foo', new Config()); + } + + /** + * @test + */ + public function delete_a_file_with_trailing_slash(): void + { + $this->expectException(UnableToDeleteFile::class); + $this->adapter()->delete('foo/'); + } + + /** + * @test + */ + public function reading_last_revision(): void + { + $this->runScenario( + function () { + $this->givenWeHaveAnExistingFile('file.txt', 'version 1'); + $this->givenWeHaveAnExistingFile('file.txt', 'version 2'); + + $this->assertSame('version 2', $this->adapter()->read('file.txt')); + } + ); + } + + /** + * @testWith [false] + * [true] + * + * @test + */ + public function listing_contents_last_revision(bool $deep): void + { + $this->runScenario( + function () use ($deep) { + $this->givenWeHaveAnExistingFile('file.txt', 'version 1'); + $this->givenWeHaveAnExistingFile('file.txt', 'version 2'); + + $files = $this->adapter()->listContents('', $deep); + $files = iterator_to_array($files); + + $this->assertCount(1, $files); + $file = $files[0]; + $this->assertInstanceOf(FileAttributes::class, $file); + $this->assertSame('file.txt', $file->path()); + } + ); + } + + /** + * @test + */ + public function listing_contents_directory_with_multiple_files(): void + { + $this->runScenario( + function () { + $this->givenWeHaveAnExistingFile('some/file-1.txt'); + $this->givenWeHaveAnExistingFile('some/file-2.txt'); + $this->givenWeHaveAnExistingFile('some/other/file-1.txt'); + + $files = $this->adapter()->listContents('', false); + $files = iterator_to_array($files); + + $this->assertCount(1, $files); + $file = $files[0]; + $this->assertInstanceOf(DirectoryAttributes::class, $file); + $this->assertSame('some', $file->path()); + } + ); + } + + /** + * @test + */ + public function delete_all_revisions(): void + { + $this->runScenario( + function () { + $this->givenWeHaveAnExistingFile('file.txt', 'version 1'); + $this->givenWeHaveAnExistingFile('file.txt', 'version 2'); + $this->givenWeHaveAnExistingFile('file.txt', 'version 3'); + + $this->adapter()->delete('file.txt'); + + $this->assertFalse($this->adapter()->fileExists('file.txt'), 'File does not exist'); + } + ); + } + + /** + * @test + */ + public function move_all_revisions(): void + { + $this->runScenario( + function () { + $this->givenWeHaveAnExistingFile('file.txt', 'version 1'); + $this->givenWeHaveAnExistingFile('file.txt', 'version 2'); + $this->givenWeHaveAnExistingFile('file.txt', 'version 3'); + + $this->adapter()->move('file.txt', 'destination.txt', new Config()); + + $this->assertSame($this->adapter()->read('destination.txt'), 'version 3'); + $this->assertFalse($this->adapter()->fileExists('file.txt')); + + } + ); + } + + protected function tearDown(): void + { + self::getDatabase()->selectGridFSBucket()->drop(); + + parent::tearDown(); + } + + protected static function createFilesystemAdapter(): FilesystemAdapter + { + $bucket = self::getDatabase()->selectGridFSBucket(); + $prefix = getenv('FLYSYSTEM_MONGODB_PREFIX') ?: self::$adapterPrefix; + + return new GridFSAdapter($bucket, $prefix); + } + + private static function getDatabase(): Database + { + $uri = getenv('MONGODB_URI') ?: 'mongodb://127.0.0.1:27017/'; + + $client = new Client($uri); + + return $client->selectDatabase(getenv('MONGODB_DATABASE') ?: 'flysystem_tests'); + } +} diff --git a/src/GridFS/LICENSE b/src/GridFS/LICENSE new file mode 100644 index 000000000..fbb2ef58b --- /dev/null +++ b/src/GridFS/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024 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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/GridFS/README.md b/src/GridFS/README.md new file mode 100644 index 000000000..44ec30eab --- /dev/null +++ b/src/GridFS/README.md @@ -0,0 +1,9 @@ +## Sub-split for Flysystem's MongoDB GridFS Adapter + +> ⚠️ this is a sub-split, for pull requests and issues, visit: https://github.com/thephpleague/flysystem + +```bash +composer require league/flysystem-gridfs +``` + +View the [documentation](https://flysystem.thephpleague.com/docs/adapter/gridfs/). diff --git a/src/GridFS/composer.json b/src/GridFS/composer.json new file mode 100644 index 000000000..c7e64acc2 --- /dev/null +++ b/src/GridFS/composer.json @@ -0,0 +1,21 @@ +{ + "name": "league/flysystem-mongodb-gridfs", + "autoload": { + "psr-4": { + "League\\Flysystem\\GridFS\\": "" + } + }, + "require": { + "php": "^8.0.2", + "ext-mongodb": "^1.15", + "league/flysystem": "^3.10.0", + "mongodb/mongodb": "^1.2" + }, + "license": "MIT", + "authors": [ + { + "name": "Jérôme Tamarelle", + "email": "jerome.tamarelle@mongodb.com" + } + ] +} From cf3a3e9a3ad0ac88ac53f202a44cfaf37456b790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 20 May 2024 10:15:46 +0200 Subject: [PATCH 02/10] Always map all attrributes Fix ext-mongodb explicit dependency for exception Use generic type for file object --- composer.json | 1 + src/GridFS/GridFSAdapter.php | 101 +++++++++++++++++------------------ src/GridFS/composer.json | 2 +- 3 files changed, 52 insertions(+), 52 deletions(-) diff --git a/composer.json b/composer.json index c7f84f569..81a39c4ed 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "ext-zip": "*", "ext-fileinfo": "*", "ext-ftp": "*", + "ext-mongodb": "^1.3", "microsoft/azure-storage-blob": "^1.1", "phpunit/phpunit": "^9.5.11|^10.0", "phpstan/phpstan": "^1.10", diff --git a/src/GridFS/GridFSAdapter.php b/src/GridFS/GridFSAdapter.php index e8ed50d90..f90f89218 100644 --- a/src/GridFS/GridFSAdapter.php +++ b/src/GridFS/GridFSAdapter.php @@ -25,11 +25,15 @@ use MongoDB\Driver\Exception\Exception; use MongoDB\GridFS\Bucket; +/** + * @phpstan-type GridFile array{_id:ObjectId, length:int, chunkSize:int, uploadDate:UTCDateTime, filename:string, metadata?:array{contentType?:string, flysystem_visibility?:string}} + */ class GridFSAdapter implements FilesystemAdapter { private const METADATA_DIRECTORY = 'flysystem_directory'; private const METADATA_VISIBILITY = 'flysystem_visibility'; private const METADATA_MIMETYPE = 'contentType'; + private const TYPEMAP_ARRAY = ['typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array']]; private Bucket $bucket; @@ -116,7 +120,12 @@ public function writeStream(string $path, $contents, Config $config): void public function read(string $path): string { - return stream_get_contents($this->readStream($path)); + $stream = $this->readStream($path); + try { + return stream_get_contents($stream); + } finally { + fclose($stream); + } } public function readStream(string $path) @@ -208,7 +217,21 @@ public function visibility(string $path): FileAttributes throw UnableToRetrieveMetadata::mimeType($path, 'file does not exist'); } - return new FileAttributes($path, null, $file['metadata'][self::METADATA_VISIBILITY] ?? null); + return $this->mapFileAttributes($file); + } + + public function fileSize(string $path): FileAttributes + { + if (str_ends_with($path, '/')) { + throw UnableToRetrieveMetadata::fileSize($path, 'file path cannot end with a slash'); + } + + $file = $this->findFile($path); + if ($file === null) { + throw UnableToRetrieveMetadata::fileSize($path, 'file does not exist'); + } + + return $this->mapFileAttributes($file); } public function mimeType(string $path): FileAttributes @@ -237,27 +260,11 @@ public function lastModified(string $path): FileAttributes } $file = $this->findFile($path); - if ($file === null) { throw UnableToRetrieveMetadata::lastModified($path, 'file does not exist'); } - return new FileAttributes($path, null, null, $file['uploadDate']->toDateTime()->getTimestamp()); - } - - public function fileSize(string $path): FileAttributes - { - if (str_ends_with($path, '/')) { - throw UnableToRetrieveMetadata::fileSize($path, 'file path cannot end with a slash'); - } - - $file = $this->findFile($path); - - if ($file === null) { - throw UnableToRetrieveMetadata::fileSize($path, 'file does not exist'); - } - - return new FileAttributes($path, $file['length']); + return $this->mapFileAttributes($file); } public function listContents(string $path, bool $deep): iterable @@ -288,10 +295,7 @@ public function listContents(string $path, bool $deep): iterable 'uploadDate' => ['$max' => '$uploadDate'], ]]; - $files = $this->bucket->getFilesCollection()->aggregate( - $pipeline, - ['typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array']] - ); + $files = $this->bucket->getFilesCollection()->aggregate($pipeline, self::TYPEMAP_ARRAY); foreach ($files as $file) { if ($file['_id']['isDir']) { @@ -301,15 +305,7 @@ public function listContents(string $path, bool $deep): iterable $file['uploadDate']->toDateTime()->getTimestamp(), ); } else { - $file = $file['file']; - yield new FileAttributes( - $this->prefixer->stripPrefix($file['filename']), - $file['length'], - $file['metadata'][self::METADATA_VISIBILITY] ?? null, - $file['uploadDate']->toDateTime()->getTimestamp(), - $file['metadata'][self::METADATA_MIMETYPE] ?? null, - $file, - ); + yield $this->mapFileAttributes($file['file']); } } } else { @@ -319,15 +315,12 @@ public function listContents(string $path, bool $deep): iterable 'file' => ['$first' => '$$ROOT'], ]]; - $files = $this->bucket->getFilesCollection()->aggregate( - $pipeline, - ['typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array']], - ); + $files = $this->bucket->getFilesCollection()->aggregate($pipeline, self::TYPEMAP_ARRAY); foreach ($files as $file) { $file = $file['file']; if (str_ends_with($file['filename'], '/')) { - // Empty files with a trailing slash are markers for directories, for Flysystem + // Empty files with a trailing slash are markers for directories, only for Flysystem yield new DirectoryAttributes( $this->prefixer->stripDirectoryPrefix($file['filename']), $file['metadata'][self::METADATA_VISIBILITY] ?? null, @@ -335,14 +328,7 @@ public function listContents(string $path, bool $deep): iterable $file, ); } else { - yield new FileAttributes( - $this->prefixer->stripPrefix($file['filename']), - $file['length'], - $file['metadata'][self::METADATA_VISIBILITY] ?? null, - $file['uploadDate']->toDateTime()->getTimestamp(), - $file['metadata'][self::METADATA_MIMETYPE] ?? null, - $file, - ); + yield $this->mapFileAttributes($file); } } } @@ -394,20 +380,33 @@ public function copy(string $source, string $destination, Config $config): void } /** - * @return array{_id:ObjectId, length:int, chunkSize:int, uploadDate:UTCDateTime, filename:string, metadata?:array{contentType?:string, flysystem_visibility?:string}}|null + * Get the last revision of the file name. + * + * @return GridFile|null */ private function findFile(string $path): ?array { $filename = $this->prefixer->prefixPath($path); $files = $this->bucket->find( ['filename' => $filename], - [ - 'sort' => ['uploadDate' => -1], - 'limit' => 1, - 'typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array'], - ], + ['sort' => ['uploadDate' => -1], 'limit' => 1] + self::TYPEMAP_ARRAY, ); return $files->toArray()[0] ?? null; } + + /** + * @param GridFile $file + */ + private function mapFileAttributes(array $file): FileAttributes + { + return new FileAttributes( + $this->prefixer->stripPrefix($file['filename']), + $file['length'], + $file['metadata'][self::METADATA_VISIBILITY] ?? null, + $file['uploadDate']->toDateTime()->getTimestamp(), + $file['metadata'][self::METADATA_MIMETYPE] ?? null, + $file, + ); + } } diff --git a/src/GridFS/composer.json b/src/GridFS/composer.json index c7e64acc2..8e6d7dd4b 100644 --- a/src/GridFS/composer.json +++ b/src/GridFS/composer.json @@ -7,7 +7,7 @@ }, "require": { "php": "^8.0.2", - "ext-mongodb": "^1.15", + "ext-mongodb": "^1.3", "league/flysystem": "^3.10.0", "mongodb/mongodb": "^1.2" }, From c9a04362b30ea911dc1d7db6f2d3803c1bff88c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 20 May 2024 11:59:12 +0200 Subject: [PATCH 03/10] Factorize delete operation and ignore missing file ignore due to race-conditions --- src/GridFS/GridFSAdapter.php | 53 +++++++++++++++++++++++++------- src/GridFS/GridFSAdapterTest.php | 18 +++++++++++ 2 files changed, 60 insertions(+), 11 deletions(-) diff --git a/src/GridFS/GridFSAdapter.php b/src/GridFS/GridFSAdapter.php index f90f89218..3b539dd20 100644 --- a/src/GridFS/GridFSAdapter.php +++ b/src/GridFS/GridFSAdapter.php @@ -11,6 +11,7 @@ use League\Flysystem\PathPrefixer; use League\Flysystem\UnableToCopyFile; use League\Flysystem\UnableToCreateDirectory; +use League\Flysystem\UnableToDeleteDirectory; use League\Flysystem\UnableToDeleteFile; use League\Flysystem\UnableToMoveFile; use League\Flysystem\UnableToReadFile; @@ -24,6 +25,7 @@ use MongoDB\BSON\UTCDateTime; use MongoDB\Driver\Exception\Exception; use MongoDB\GridFS\Bucket; +use MongoDB\GridFS\Exception\FileNotFoundException; /** * @phpstan-type GridFile array{_id:ObjectId, length:int, chunkSize:int, uploadDate:UTCDateTime, filename:string, metadata?:array{contentType?:string, flysystem_visibility?:string}} @@ -33,7 +35,10 @@ class GridFSAdapter implements FilesystemAdapter private const METADATA_DIRECTORY = 'flysystem_directory'; private const METADATA_VISIBILITY = 'flysystem_visibility'; private const METADATA_MIMETYPE = 'contentType'; - private const TYPEMAP_ARRAY = ['typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array']]; + private const TYPEMAP_ARRAY = [ + 'typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array'], + 'codec' => null, + ]; private Bucket $bucket; @@ -147,26 +152,33 @@ public function readStream(string $path) } } + /** + * Delete all revisions of the file name, starting with the oldest, + * no-op if the file does not exist. + * + * @throws UnableToDeleteFile + */ public function delete(string $path): void { if (str_ends_with($path, '/')) { throw UnableToDeleteFile::atLocation($path, 'file path cannot end with a slash'); } - // Deleting a file that does not exist is no-op - while ($file = $this->findFile($path)) { - try { - $this->bucket->delete($file['_id']); - } catch (Exception $exception) { - throw UnableToDeleteFile::atLocation($path, $exception->getMessage(), $exception); - } + $filename = $this->prefixer->prefixPath($path); + try { + $this->findAndDelete(['filename' => $filename]); + } catch (Exception $exception) { + throw UnableToDeleteFile::atLocation($path, $exception->getMessage(), $exception); } } public function deleteDirectory(string $path): void { - foreach ($this->listContents($path, true) as $file) { - $this->bucket->delete($file->extraMetadata()['_id']); + $prefixedPath = $this->prefixer->prefixDirectoryPath($path); + try { + $this->findAndDelete(['filename' => new Regex('^' . preg_quote($prefixedPath))]); + } catch (Exception $exception) { + throw UnableToDeleteDirectory::atLocation($path, $exception->getMessage(), $exception); } } @@ -276,7 +288,7 @@ public function listContents(string $path, bool $deep): iterable if ($path !== '') { $pathdeep = substr_count($path, '/'); // Exclude files that do not start with the expected path - $pipeline[] = ['$match' => ['filename' => ['$regex' => new Regex('^' . preg_quote($path))]]]; + $pipeline[] = ['$match' => ['filename' => new Regex('^' . preg_quote($path))]]; } // Get the last revision of each file $pipeline[] = ['$sort' => ['filename' => 1, 'uploadDate' => -1]]; @@ -409,4 +421,23 @@ private function mapFileAttributes(array $file): FileAttributes $file, ); } + + /** + * @throws Exception + */ + private function findAndDelete(array $filter): void + { + $files = $this->bucket->find( + $filter, + ['sort' => ['uploadDate' => 1], 'projection' => ['_id' => 1]] + self::TYPEMAP_ARRAY, + ); + + foreach ($files as $file) { + try { + $this->bucket->delete($file['_id']); + } catch (FileNotFoundException) { + // Ignore error due to race condition + } + } + } } diff --git a/src/GridFS/GridFSAdapterTest.php b/src/GridFS/GridFSAdapterTest.php index 3ba508d42..147a9971c 100644 --- a/src/GridFS/GridFSAdapterTest.php +++ b/src/GridFS/GridFSAdapterTest.php @@ -19,6 +19,8 @@ /** * @group gridfs + * + * @method GridFSAdapter adapter() */ class GridFSAdapterTest extends TestCase { @@ -34,6 +36,22 @@ public static function tearDownAfterClass(): void parent::tearDownAfterClass(); } + /** + * @test + */ + public function fetching_contains_extra_metadata(): void + { + $adapter = $this->adapter(); + + $this->runScenario(function () use ($adapter) { + $this->givenWeHaveAnExistingFile('file.txt'); + $fileAttributes = $adapter->lastModified('file.txt'); + $extra = $fileAttributes->extraMetadata(); + $this->assertArrayHasKey('_id', $extra); + $this->assertArrayHasKey('filename', $extra); + }); + } + /** * @test */ From 9ec096c8e61aff487b8d4331c59d3735059c89c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 20 May 2024 17:19:13 +0200 Subject: [PATCH 04/10] Use openDownloadStreamByName --- src/GridFS/GridFSAdapter.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/GridFS/GridFSAdapter.php b/src/GridFS/GridFSAdapter.php index 3b539dd20..1031bb8c7 100644 --- a/src/GridFS/GridFSAdapter.php +++ b/src/GridFS/GridFSAdapter.php @@ -139,14 +139,12 @@ public function readStream(string $path) throw UnableToReadFile::fromLocation($path, 'file path cannot end with a slash'); } - $file = $this->findFile($path); - - if ($file === null) { - throw UnableToReadFile::fromLocation($path, 'file does not exist'); - } - try { - return $this->bucket->openDownloadStream($file['_id']); + $filename = $this->prefixer->prefixPath($path); + + return $this->bucket->openDownloadStreamByName($filename); + } catch (FileNotFoundException $exception) { + throw UnableToReadFile::fromLocation($path, 'file does not exist', $exception); } catch (Exception $exception) { throw UnableToReadFile::fromLocation($path, $exception->getMessage(), $exception); } From 86d94c5c260fe2772fc3236e25efd526f113fc89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 20 May 2024 17:37:55 +0200 Subject: [PATCH 05/10] Use mapFileAttributes in mimeType --- src/GridFS/GridFSAdapter.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/GridFS/GridFSAdapter.php b/src/GridFS/GridFSAdapter.php index 1031bb8c7..2409a4c5c 100644 --- a/src/GridFS/GridFSAdapter.php +++ b/src/GridFS/GridFSAdapter.php @@ -251,16 +251,16 @@ public function mimeType(string $path): FileAttributes } $file = $this->findFile($path); - if ($file === null) { throw UnableToRetrieveMetadata::mimeType($path, 'file does not exist'); } - if ( ! isset($file['metadata'][self::METADATA_MIMETYPE])) { + $attributes = $this->mapFileAttributes($file); + if ($attributes->mimeType() === null) { throw UnableToRetrieveMetadata::mimeType($path, 'unknown'); } - return new FileAttributes($path, null, null, null, $file['metadata'][self::METADATA_MIMETYPE]); + return $attributes; } public function lastModified(string $path): FileAttributes From f4a0e6c2218c90ddf1dda6318368a47de357c30e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 20 May 2024 17:43:45 +0200 Subject: [PATCH 06/10] Use file index in the specified order --- src/GridFS/GridFSAdapter.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/GridFS/GridFSAdapter.php b/src/GridFS/GridFSAdapter.php index 2409a4c5c..1035fdc2a 100644 --- a/src/GridFS/GridFSAdapter.php +++ b/src/GridFS/GridFSAdapter.php @@ -282,14 +282,13 @@ public function listContents(string $path, bool $deep): iterable $path = $this->prefixer->prefixDirectoryPath($path); $pathdeep = 0; - $pipeline = []; + // Get the last revision of each file, using the index on the files collection + $pipeline = [['$sort' => ['filename' => 1, 'uploadDate' => 1]]]; if ($path !== '') { $pathdeep = substr_count($path, '/'); // Exclude files that do not start with the expected path $pipeline[] = ['$match' => ['filename' => new Regex('^' . preg_quote($path))]]; } - // Get the last revision of each file - $pipeline[] = ['$sort' => ['filename' => 1, 'uploadDate' => -1]]; if ($deep === false) { $pipeline[] = ['$addFields' => ['splitpath' => ['$split' => ['$filename', '/']]]]; @@ -300,7 +299,7 @@ public function listContents(string $path, bool $deep): iterable 'isDir' => ['$ne' => [['$size' => '$splitpath'], $pathdeep + 1]], ], // Get the metadata of the last revision of each file - 'file' => ['$first' => '$$ROOT'], + 'file' => ['$last' => '$$ROOT'], // The "lastModified" date is the date of the last uploaded file in the directory 'uploadDate' => ['$max' => '$uploadDate'], ]]; From 39dcc17e2f80fde22157bb437bb9ab8e7ce91f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 21 May 2024 21:05:21 +0200 Subject: [PATCH 07/10] Update package name --- src/GridFS/composer.json | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/GridFS/composer.json b/src/GridFS/composer.json index 8e6d7dd4b..f07098866 100644 --- a/src/GridFS/composer.json +++ b/src/GridFS/composer.json @@ -1,5 +1,5 @@ { - "name": "league/flysystem-mongodb-gridfs", + "name": "league/flysystem-gridfs", "autoload": { "psr-4": { "League\\Flysystem\\GridFS\\": "" @@ -14,8 +14,12 @@ "license": "MIT", "authors": [ { - "name": "Jérôme Tamarelle", - "email": "jerome.tamarelle@mongodb.com" + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + }, + { + "name": "MongoDB PHP", + "email": "driver-php@mongodb.com" } ] } From 8fef05c7e480675589849e8393c534362a374688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 21 May 2024 22:24:14 +0200 Subject: [PATCH 08/10] Ensure files are created in a different millisecond in test to keep revisions order --- src/GridFS/GridFSAdapter.php | 14 ++++++-------- src/GridFS/GridFSAdapterTest.php | 6 +++++- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/GridFS/GridFSAdapter.php b/src/GridFS/GridFSAdapter.php index 1035fdc2a..cff68b188 100644 --- a/src/GridFS/GridFSAdapter.php +++ b/src/GridFS/GridFSAdapter.php @@ -345,17 +345,15 @@ public function listContents(string $path, bool $deep): iterable public function move(string $source, string $destination, Config $config): void { - $file = $this->findFile($source); - - if ($file === null) { - throw UnableToMoveFile::because('file does not exist', $source, $destination); - } - try { - $this->bucket->getFilesCollection()->updateMany( - ['filename' => $file['filename']], + $result = $this->bucket->getFilesCollection()->updateMany( + ['filename' => $this->prefixer->prefixPath($source)], ['$set' => ['filename' => $this->prefixer->prefixPath($destination)]], ); + + if ($result->getModifiedCount() === 0) { + throw UnableToMoveFile::because('file does not exist', $source, $destination); + } } catch (Exception $exception) { throw UnableToMoveFile::fromLocationTo($source, $destination, $exception); } diff --git a/src/GridFS/GridFSAdapterTest.php b/src/GridFS/GridFSAdapterTest.php index 147a9971c..fee6c566e 100644 --- a/src/GridFS/GridFSAdapterTest.php +++ b/src/GridFS/GridFSAdapterTest.php @@ -146,6 +146,7 @@ public function reading_last_revision(): void $this->runScenario( function () { $this->givenWeHaveAnExistingFile('file.txt', 'version 1'); + usleep(10); $this->givenWeHaveAnExistingFile('file.txt', 'version 2'); $this->assertSame('version 2', $this->adapter()->read('file.txt')); @@ -164,6 +165,7 @@ public function listing_contents_last_revision(bool $deep): void $this->runScenario( function () use ($deep) { $this->givenWeHaveAnExistingFile('file.txt', 'version 1'); + usleep(10); $this->givenWeHaveAnExistingFile('file.txt', 'version 2'); $files = $this->adapter()->listContents('', $deep); @@ -225,13 +227,15 @@ public function move_all_revisions(): void $this->runScenario( function () { $this->givenWeHaveAnExistingFile('file.txt', 'version 1'); + usleep(10); $this->givenWeHaveAnExistingFile('file.txt', 'version 2'); + usleep(10); $this->givenWeHaveAnExistingFile('file.txt', 'version 3'); $this->adapter()->move('file.txt', 'destination.txt', new Config()); - $this->assertSame($this->adapter()->read('destination.txt'), 'version 3'); $this->assertFalse($this->adapter()->fileExists('file.txt')); + $this->assertSame($this->adapter()->read('destination.txt'), 'version 3'); } ); From 635eb5fcaefa257b9030fe0c61d96903bee4d87d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 21 May 2024 23:00:39 +0200 Subject: [PATCH 09/10] Set readPreference in tests --- src/GridFS/GridFSAdapterTest.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/GridFS/GridFSAdapterTest.php b/src/GridFS/GridFSAdapterTest.php index fee6c566e..9e6baa322 100644 --- a/src/GridFS/GridFSAdapterTest.php +++ b/src/GridFS/GridFSAdapterTest.php @@ -15,6 +15,7 @@ use League\Flysystem\UnableToWriteFile; use MongoDB\Client; use MongoDB\Database; +use MongoDB\Driver\ReadPreference; use function getenv; /** @@ -146,7 +147,7 @@ public function reading_last_revision(): void $this->runScenario( function () { $this->givenWeHaveAnExistingFile('file.txt', 'version 1'); - usleep(10); + usleep(1000); $this->givenWeHaveAnExistingFile('file.txt', 'version 2'); $this->assertSame('version 2', $this->adapter()->read('file.txt')); @@ -165,7 +166,7 @@ public function listing_contents_last_revision(bool $deep): void $this->runScenario( function () use ($deep) { $this->givenWeHaveAnExistingFile('file.txt', 'version 1'); - usleep(10); + usleep(1000); $this->givenWeHaveAnExistingFile('file.txt', 'version 2'); $files = $this->adapter()->listContents('', $deep); @@ -209,7 +210,9 @@ public function delete_all_revisions(): void $this->runScenario( function () { $this->givenWeHaveAnExistingFile('file.txt', 'version 1'); + usleep(1000); $this->givenWeHaveAnExistingFile('file.txt', 'version 2'); + usleep(1000); $this->givenWeHaveAnExistingFile('file.txt', 'version 3'); $this->adapter()->delete('file.txt'); @@ -227,16 +230,15 @@ public function move_all_revisions(): void $this->runScenario( function () { $this->givenWeHaveAnExistingFile('file.txt', 'version 1'); - usleep(10); + usleep(1000); $this->givenWeHaveAnExistingFile('file.txt', 'version 2'); - usleep(10); + usleep(1000); $this->givenWeHaveAnExistingFile('file.txt', 'version 3'); $this->adapter()->move('file.txt', 'destination.txt', new Config()); $this->assertFalse($this->adapter()->fileExists('file.txt')); $this->assertSame($this->adapter()->read('destination.txt'), 'version 3'); - } ); } @@ -260,7 +262,9 @@ private static function getDatabase(): Database { $uri = getenv('MONGODB_URI') ?: 'mongodb://127.0.0.1:27017/'; - $client = new Client($uri); + $client = new Client($uri, [], [ + 'readPreference' => new ReadPreference(ReadPreference::PRIMARY), + ]); return $client->selectDatabase(getenv('MONGODB_DATABASE') ?: 'flysystem_tests'); } From 612b104db7d49158ca92d5d2a48eeaefe9edcd22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 22 May 2024 08:38:08 +0200 Subject: [PATCH 10/10] Remove read preference, as primary is the default --- src/GridFS/GridFSAdapterTest.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/GridFS/GridFSAdapterTest.php b/src/GridFS/GridFSAdapterTest.php index 9e6baa322..b84e2f9de 100644 --- a/src/GridFS/GridFSAdapterTest.php +++ b/src/GridFS/GridFSAdapterTest.php @@ -261,10 +261,7 @@ protected static function createFilesystemAdapter(): FilesystemAdapter private static function getDatabase(): Database { $uri = getenv('MONGODB_URI') ?: 'mongodb://127.0.0.1:27017/'; - - $client = new Client($uri, [], [ - 'readPreference' => new ReadPreference(ReadPreference::PRIMARY), - ]); + $client = new Client($uri); return $client->selectDatabase(getenv('MONGODB_DATABASE') ?: 'flysystem_tests'); }