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..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", @@ -35,6 +36,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..cff68b188 --- /dev/null +++ b/src/GridFS/GridFSAdapter.php @@ -0,0 +1,438 @@ + ['root' => 'array', 'document' => 'array', 'array' => 'array'], + 'codec' => null, + ]; + + private Bucket $bucket; + + private PathPrefixer $prefixer; + + private MimeTypeDetector $mimeTypeDetector; + + public function __construct( + Bucket $bucket, + string $prefix = '', + ?MimeTypeDetector $mimeTypeDetector = null, + ) { + $this->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 + { + $stream = $this->readStream($path); + try { + return stream_get_contents($stream); + } finally { + fclose($stream); + } + } + + public function readStream(string $path) + { + if (str_ends_with($path, '/')) { + throw UnableToReadFile::fromLocation($path, 'file path cannot end with a slash'); + } + + try { + $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); + } + } + + /** + * 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'); + } + + $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 + { + $prefixedPath = $this->prefixer->prefixDirectoryPath($path); + try { + $this->findAndDelete(['filename' => new Regex('^' . preg_quote($prefixedPath))]); + } catch (Exception $exception) { + throw UnableToDeleteDirectory::atLocation($path, $exception->getMessage(), $exception); + } + } + + 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 $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 + { + 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'); + } + + $attributes = $this->mapFileAttributes($file); + if ($attributes->mimeType() === null) { + throw UnableToRetrieveMetadata::mimeType($path, 'unknown'); + } + + return $attributes; + } + + 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 $this->mapFileAttributes($file); + } + + public function listContents(string $path, bool $deep): iterable + { + $path = $this->prefixer->prefixDirectoryPath($path); + + $pathdeep = 0; + // 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))]]; + } + + 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' => ['$last' => '$$ROOT'], + // The "lastModified" date is the date of the last uploaded file in the directory + 'uploadDate' => ['$max' => '$uploadDate'], + ]]; + + $files = $this->bucket->getFilesCollection()->aggregate($pipeline, self::TYPEMAP_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 { + yield $this->mapFileAttributes($file['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, 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, only for Flysystem + yield new DirectoryAttributes( + $this->prefixer->stripDirectoryPrefix($file['filename']), + $file['metadata'][self::METADATA_VISIBILITY] ?? null, + $file['uploadDate']->toDateTime()->getTimestamp(), + $file, + ); + } else { + yield $this->mapFileAttributes($file); + } + } + } + } + + public function move(string $source, string $destination, Config $config): void + { + try { + $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); + } + } + + 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); + } + } + + /** + * 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] + 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, + ); + } + + /** + * @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 new file mode 100644 index 000000000..b84e2f9de --- /dev/null +++ b/src/GridFS/GridFSAdapterTest.php @@ -0,0 +1,268 @@ +drop(); + + 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 + */ + 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'); + usleep(1000); + $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'); + usleep(1000); + $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'); + usleep(1000); + $this->givenWeHaveAnExistingFile('file.txt', 'version 2'); + usleep(1000); + $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'); + usleep(1000); + $this->givenWeHaveAnExistingFile('file.txt', 'version 2'); + 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'); + } + ); + } + + 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..f07098866 --- /dev/null +++ b/src/GridFS/composer.json @@ -0,0 +1,25 @@ +{ + "name": "league/flysystem-gridfs", + "autoload": { + "psr-4": { + "League\\Flysystem\\GridFS\\": "" + } + }, + "require": { + "php": "^8.0.2", + "ext-mongodb": "^1.3", + "league/flysystem": "^3.10.0", + "mongodb/mongodb": "^1.2" + }, + "license": "MIT", + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + }, + { + "name": "MongoDB PHP", + "email": "driver-php@mongodb.com" + } + ] +}