From 095e504a3432db02ca4f9df1c8d6f1bb7e4d0c85 Mon Sep 17 00:00:00 2001 From: Davi Senra Date: Wed, 27 Mar 2024 00:27:48 -0300 Subject: [PATCH] feat: adds flysystem and tests for file uploading --- composer.json | 6 +- composer.lock | 265 +++++++++++++++++- config/container.php | 44 ++- config/routes.php | 2 + src/Command/ExampleCommand.php | 2 +- src/Controller/FileUploadController.php | 38 +++ tests/Fixtures/text_file.txt | 3 + .../Controller/FileUploadControllerTest.php | 45 +++ 8 files changed, 397 insertions(+), 8 deletions(-) create mode 100644 src/Controller/FileUploadController.php create mode 100644 tests/Fixtures/text_file.txt create mode 100644 tests/Integration/Controller/FileUploadControllerTest.php diff --git a/composer.json b/composer.json index a468a90..d2ab635 100644 --- a/composer.json +++ b/composer.json @@ -30,13 +30,15 @@ "symfony/dotenv": "^7.0", "guzzlehttp/guzzle": "^7.8", "symfony/console": "^7.0", - "doctrine/migrations": "^3.7" + "doctrine/migrations": "^3.7", + "league/flysystem": "^3.0" }, "require-dev": { "symfony/var-dumper": "^7.0", "phpunit/phpunit": "^11.0", "phpstan/phpstan": "^1.10", "friendsofphp/php-cs-fixer": "^3.52", - "robiningelbrecht/phpunit-pretty-print": "^1.3" + "robiningelbrecht/phpunit-pretty-print": "^1.3", + "league/flysystem-memory": "^3.0" } } diff --git a/composer.lock b/composer.lock index 6aa305f..568a88f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "04ed903f728fec9b859d7ec037ba459e", + "content-hash": "a0d217369e3683c1775ce46a148e00d7", "packages": [ { "name": "doctrine/collections", @@ -1249,6 +1249,211 @@ }, "time": "2023-11-08T14:08:06+00:00" }, + { + "name": "league/flysystem", + "version": "3.26.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "072735c56cc0da00e10716dd90d5a7f7b40b36be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/072735c56cc0da00e10716dd90d5a7f7b40b36be", + "reference": "072735c56cc0da00e10716dd90d5a7f7b40b36be", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "microsoft/azure-storage-blob": "^1.1", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.26.0" + }, + "funding": [ + { + "url": "https://ecologi.com/frankdejonge", + "type": "custom" + }, + { + "url": "https://github.com/frankdejonge", + "type": "github" + } + ], + "time": "2024-03-25T11:49:53+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.25.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "61a6a90d6e999e4ddd9ce5adb356de0939060b92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/61a6a90d6e999e4ddd9ce5adb356de0939060b92", + "reference": "61a6a90d6e999e4ddd9ce5adb356de0939060b92", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.25.1" + }, + "funding": [ + { + "url": "https://ecologi.com/frankdejonge", + "type": "custom" + }, + { + "url": "https://github.com/frankdejonge", + "type": "github" + } + ], + "time": "2024-03-15T19:58:44+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.15.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301", + "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.15.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-01-28T23:22:08+00:00" + }, { "name": "monolog/monolog", "version": "3.5.0", @@ -3667,6 +3872,64 @@ ], "time": "2024-03-19T21:02:43+00:00" }, + { + "name": "league/flysystem-memory", + "version": "3.25.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-memory.git", + "reference": "0a0ea723243bd8dbc2be04e98ba59418148006ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-memory/zipball/0a0ea723243bd8dbc2be04e98ba59418148006ac", + "reference": "0a0ea723243bd8dbc2be04e98ba59418148006ac", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\InMemory\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "In-memory filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "memory" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-memory/tree/3.25.1" + }, + "funding": [ + { + "url": "https://ecologi.com/frankdejonge", + "type": "custom" + }, + { + "url": "https://github.com/frankdejonge", + "type": "github" + } + ], + "time": "2024-03-15T19:58:44+00:00" + }, { "name": "myclabs/deep-copy", "version": "1.11.1", diff --git a/config/container.php b/config/container.php index d0ac966..f61635d 100644 --- a/config/container.php +++ b/config/container.php @@ -13,13 +13,20 @@ use Doctrine\ORM\ORMSetup; use Doctrine\ORM\Tools\Console\ConsoleRunner; use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider; +use League\Flysystem\Filesystem; +use League\Flysystem\FilesystemAdapter; +use League\Flysystem\Local\LocalFilesystemAdapter; use Monolog\Handler\HandlerInterface; use Monolog\Handler\RotatingFileHandler; use Monolog\Handler\StreamHandler; use Monolog\Level; use Monolog\Logger; use Nyholm\Psr7\Factory\Psr17Factory; +use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ServerRequestFactoryInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Message\UploadedFileFactoryInterface; +use Psr\Http\Message\UriFactoryInterface; use Psr\Log\LoggerInterface; use Slim\App; use Slim\Factory\AppFactory; @@ -71,6 +78,26 @@ return $app; }, + ResponseFactoryInterface::class => function (Container $container) { + return $container->get(Psr17Factory::class); + }, + + ServerRequestFactoryInterface::class => function (Container $container) { + return $container->get(Psr17Factory::class); + }, + + StreamFactoryInterface::class => function (Container $container) { + return $container->get(Psr17Factory::class); + }, + + UploadedFileFactoryInterface::class => function (Container $container) { + return $container->get(Psr17Factory::class); + }, + + UriFactoryInterface::class => function (Container $container) { + return $container->get(Psr17Factory::class); + }, + Application::class => function (Container $container) { $cli = new Application(); $cli->setCatchExceptions(true); @@ -125,10 +152,6 @@ return $cli; }, - ServerRequestFactoryInterface::class => function (Container $container) { - return $container->get(Psr17Factory::class); - }, - LoggerInterface::class => function () { /** @var HandlerInterface[] $handlers */ $handlers = []; @@ -159,4 +182,17 @@ return new EntityManager($connection, $config); }, + Filesystem::class => function (Container $container) { + $adapter = $container->get(FilesystemAdapter::class); + + return new Filesystem($adapter); + }, + + FilesystemAdapter::class => function () { + $storagePath = __DIR__ . '/../var/'; + // TODO: return an adapter based on environment variable + + return new LocalFilesystemAdapter($storagePath); + }, + ]; diff --git a/config/routes.php b/config/routes.php index f222fff..c3a2275 100644 --- a/config/routes.php +++ b/config/routes.php @@ -2,12 +2,14 @@ declare(strict_types=1); +use App\Controller\FileUploadController; use App\Controller\HealthCheckController; use Nyholm\Psr7\Response; use Slim\App; return function (App $app) { $app->get('/healthcheck', HealthCheckController::class); + $app->post('/upload', FileUploadController::class); $app->options('/{routes:.+}', fn ($request, $response) => $response); diff --git a/src/Command/ExampleCommand.php b/src/Command/ExampleCommand.php index fb94d6d..f755eca 100644 --- a/src/Command/ExampleCommand.php +++ b/src/Command/ExampleCommand.php @@ -21,4 +21,4 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::SUCCESS; } -} \ No newline at end of file +} diff --git a/src/Controller/FileUploadController.php b/src/Controller/FileUploadController.php new file mode 100644 index 0000000..d99f1b9 --- /dev/null +++ b/src/Controller/FileUploadController.php @@ -0,0 +1,38 @@ +getUploadedFiles(); + $firstFile = array_pop($uploadedFiles); + + if (!$firstFile instanceof UploadedFile) { + return new Response(400); + } + + $this->filesystem->write( + location: sprintf('%s', $firstFile->getClientFilename()), + contents: $firstFile->getStream()->getContents() + ); + + return new Response( + status: 200, + headers: ['Content-Type' => 'application/json'], + body: json_encode(['status' => true], flags: JSON_THROW_ON_ERROR) + ); + } +} diff --git a/tests/Fixtures/text_file.txt b/tests/Fixtures/text_file.txt new file mode 100644 index 0000000..fab38ea --- /dev/null +++ b/tests/Fixtures/text_file.txt @@ -0,0 +1,3 @@ +Um dia de chuva é tão belo como um dia de sol. +Ambos existem, cada um como é. +Alberto Caeiro \ No newline at end of file diff --git a/tests/Integration/Controller/FileUploadControllerTest.php b/tests/Integration/Controller/FileUploadControllerTest.php new file mode 100644 index 0000000..22ce396 --- /dev/null +++ b/tests/Integration/Controller/FileUploadControllerTest.php @@ -0,0 +1,45 @@ +container->set(FilesystemAdapter::class, fn () => $fileSystemAdapter); + + $file = new Stream(fopen(__DIR__ . '/../../Fixtures/text_file.txt', 'r+')); + $request = $this + ->createFormRequest('POST', '/upload', []) + ->withHeader('Content-Type', 'multipart/form-data') + ->withUploadedFiles([ + 'file' => new UploadedFile( + streamOrFile: $file, + size: $file->getSize(), + errorStatus: 0, + clientFilename: 'text_file.txt', + clientMediaType: 'plain/text', + ), + ]); + + $response = $this->app->handle($request); + $responseData = $this->getJsonData($response); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertTrue($responseData['status']); + $this->assertTrue($fileSystemAdapter->fileExists('text_file.txt')); + } +}