From dac52c5f749801fd3dd1e3c1adbd93581033f78a Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Fri, 15 Jul 2022 12:10:54 -0400 Subject: [PATCH] [Live] add `UploadController` --- src/LiveComponent/composer.json | 1 + .../src/Controller/UploadController.php | 68 +++++++++ .../LiveComponentExtension.php | 8 ++ .../config/routing/live_component.xml | 6 + .../tests/Fixtures/files/image1.png | Bin 0 -> 8095 bytes .../tests/Fixtures/files/image2.png | Bin 0 -> 15991 bytes .../tests/Fixtures/files/text.txt | 1 + .../Controller/UploadControllerTest.php | 134 ++++++++++++++++++ 8 files changed, 218 insertions(+) create mode 100644 src/LiveComponent/src/Controller/UploadController.php create mode 100644 src/LiveComponent/tests/Fixtures/files/image1.png create mode 100644 src/LiveComponent/tests/Fixtures/files/image2.png create mode 100644 src/LiveComponent/tests/Fixtures/files/text.txt create mode 100644 src/LiveComponent/tests/Functional/Controller/UploadControllerTest.php diff --git a/src/LiveComponent/composer.json b/src/LiveComponent/composer.json index c65517c41c4..69e9ed662cd 100644 --- a/src/LiveComponent/composer.json +++ b/src/LiveComponent/composer.json @@ -27,6 +27,7 @@ }, "require": { "php": ">=8.0", + "symfony/mime": "^5.4|^6.0", "symfony/property-access": "^5.4|^6.0", "symfony/serializer": "^5.4|^6.0", "symfony/ux-twig-component": "^2.1" diff --git a/src/LiveComponent/src/Controller/UploadController.php b/src/LiveComponent/src/Controller/UploadController.php new file mode 100644 index 00000000000..85de29ec7db --- /dev/null +++ b/src/LiveComponent/src/Controller/UploadController.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\LiveComponent\Controller; + +use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; +use Symfony\Component\HttpFoundation\File\File; +use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +/** + * @author Kevin Bond + */ +final class UploadController +{ + private string $uploadDir; + + public function __construct(string $uploadDir) + { + $this->uploadDir = rtrim($uploadDir, '/'); + } + + public function uploadAction(Request $request): JsonResponse + { + $files = []; + + foreach ($request->files->all() as $file) { + if (!$file instanceof UploadedFile) { + continue; + } + + // TODO: use UUID? + $name = sprintf('%s.%s', uniqid('live-', true), strtolower($file->getClientOriginalExtension())); + + $file->move($this->uploadDir, $name); + + $files[$file->getClientOriginalName()] = $name; + } + + return new JsonResponse($files); + } + + public function previewAction(string $filename): BinaryFileResponse + { + try { + $file = new File("{$this->uploadDir}/{$filename}"); + } catch (FileNotFoundException) { + throw new NotFoundHttpException(sprintf('File "%s" not found.', $filename)); + } + + if (!str_starts_with((string) $file->getMimeType(), 'image/')) { + throw new NotFoundHttpException('Only images can be previewed.'); + } + + return new BinaryFileResponse($file); + } +} diff --git a/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php b/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php index 946886ca3b6..cf6cc797c0a 100644 --- a/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php +++ b/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php @@ -20,6 +20,7 @@ use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\ComponentValidator; use Symfony\UX\LiveComponent\ComponentValidatorInterface; +use Symfony\UX\LiveComponent\Controller\UploadController; use Symfony\UX\LiveComponent\EventListener\AddLiveAttributesSubscriber; use Symfony\UX\LiveComponent\EventListener\LiveComponentSubscriber; use Symfony\UX\LiveComponent\Form\Type\LiveCollectionType; @@ -108,5 +109,12 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) { ->addTag('form.type') ->setPublic(false) ; + + $container->setParameter('ux.live_component.upload_dir', '%kernel.project_dir%/var/live-tmp'); // TODO customizable? + + $container->register('ux.live_component.upload_controller', UploadController::class) + ->setArguments(['%ux.live_component.upload_dir%']) + ->setPublic(true) + ; } } diff --git a/src/LiveComponent/src/Resources/config/routing/live_component.xml b/src/LiveComponent/src/Resources/config/routing/live_component.xml index ac87d9a8c13..4973445663d 100644 --- a/src/LiveComponent/src/Resources/config/routing/live_component.xml +++ b/src/LiveComponent/src/Resources/config/routing/live_component.xml @@ -7,4 +7,10 @@ get + + + + + [\w\-\.]+ + diff --git a/src/LiveComponent/tests/Fixtures/files/image1.png b/src/LiveComponent/tests/Fixtures/files/image1.png new file mode 100644 index 0000000000000000000000000000000000000000..c8278fc1b655c2357451def7f3043600a660031a GIT binary patch literal 8095 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 U0)<&p{~NPwsp+dWDBDE+A1pMp4gdfE literal 0 HcmV?d00001 diff --git a/src/LiveComponent/tests/Fixtures/files/image2.png b/src/LiveComponent/tests/Fixtures/files/image2.png new file mode 100644 index 0000000000000000000000000000000000000000..c065647d5261f678e31176065787164cdf36728a GIT binary patch literal 15991 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 NI~f&7`CF6E{|}KMPMZJ# literal 0 HcmV?d00001 diff --git a/src/LiveComponent/tests/Fixtures/files/text.txt b/src/LiveComponent/tests/Fixtures/files/text.txt new file mode 100644 index 00000000000..2c3e89d43da --- /dev/null +++ b/src/LiveComponent/tests/Fixtures/files/text.txt @@ -0,0 +1 @@ +some text... diff --git a/src/LiveComponent/tests/Functional/Controller/UploadControllerTest.php b/src/LiveComponent/tests/Functional/Controller/UploadControllerTest.php new file mode 100644 index 00000000000..44059d673ce --- /dev/null +++ b/src/LiveComponent/tests/Functional/Controller/UploadControllerTest.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\LiveComponent\Tests\Functional\Controller; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpFoundation\File\UploadedFile; +use Zenstruck\Browser\Test\HasBrowser; + +/** + * @author Kevin Bond + */ +final class UploadControllerTest extends KernelTestCase +{ + use HasBrowser; + + private const FIXTURE_FILE_DIR = __DIR__.'/../../Fixtures/files'; + private const UPLOAD_FILE_DIR = __DIR__.'/../../../var/live-tmp'; + private const TEMP_DIR = __DIR__.'/../../../var/tmp'; + + /** + * @before + */ + public static function prepareTempDirs(): void + { + (new Filesystem())->remove(self::TEMP_DIR); + (new Filesystem())->remove(self::UPLOAD_FILE_DIR); + (new Filesystem())->mirror(self::FIXTURE_FILE_DIR, self::TEMP_DIR); + } + + public function testCanUploadASingleFile(): void + { + $json = $this->browser() + ->post('/live/upload', ['files' => [new UploadedFile(self::TEMP_DIR.'/image1.png', 'image1.png', test: true)]]) + ->assertSuccessful() + ->response() + ->assertJson() + ->json() + ; + + $this->assertIsArray($json); + $this->assertCount(1, $json); + $this->assertArrayHasKey('image1.png', $json); + $this->assertFileExists(self::UPLOAD_FILE_DIR.'/'.$json['image1.png']); + $this->assertFileDoesNotExist(self::TEMP_DIR.'/'.$json['image1.png']); + } + + public function testCanUploadMultipleFiles(): void + { + $json = $this->browser() + ->post('/live/upload', ['files' => [ + new UploadedFile(self::TEMP_DIR.'/image1.png', 'image1.png', test: true), + new UploadedFile(self::TEMP_DIR.'/image2.png', 'image2.png', test: true), + ]]) + ->assertSuccessful() + ->response() + ->assertJson() + ->json() + ; + + $this->assertIsArray($json); + $this->assertCount(2, $json); + $this->assertArrayHasKey('image1.png', $json); + $this->assertArrayHasKey('image2.png', $json); + $this->assertFileExists(self::UPLOAD_FILE_DIR.'/'.$json['image1.png']); + $this->assertFileDoesNotExist(self::TEMP_DIR.'/'.$json['image1.png']); + $this->assertFileExists(self::UPLOAD_FILE_DIR.'/'.$json['image2.png']); + $this->assertFileDoesNotExist(self::TEMP_DIR.'/'.$json['image2.png']); + } + + public function testUploadEndpointMustBePost(): void + { + $this->markTestIncomplete(); + } + + public function testUploadEndpointMustBeSigned(): void + { + $this->markTestIncomplete(); + } + + public function testUploadEndpointIsTemporary(): void + { + $this->markTestIncomplete(); + } + + public function testCanPreviewImages(): void + { + (new Filesystem())->copy(self::FIXTURE_FILE_DIR.'/image1.png', self::UPLOAD_FILE_DIR.'/image1.png'); + + $this->browser() + ->visit('/live/preview/image1.png') + ->assertSuccessful() + ->assertHeaderContains('Content-Type', 'image/png') + ->assertContains(file_get_contents(self::FIXTURE_FILE_DIR.'/image1.png')) + ; + } + + public function testCannotPreviewNonImages(): void + { + (new Filesystem())->copy(self::FIXTURE_FILE_DIR.'/text.txt', self::UPLOAD_FILE_DIR.'/text.txt'); + + $this->browser() + ->visit('/live/preview/text.txt') + ->assertStatus(404) + ; + } + + public function testMissingPreviewFileThrows404(): void + { + $this->browser() + ->visit('/live/preview/missing.png') + ->assertStatus(404) + ; + } + + public function testInvalidPreviewFilenameThrows404(): void + { + (new Filesystem())->mkdir(self::UPLOAD_FILE_DIR); + + $this->browser() + ->visit('/live/preview/../../tests/Fixtures/files/image1.png') + ->assertStatus(404) + ; + } +}