diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 78f830a..3b7bbfb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,4 +39,4 @@ jobs: composer update --${{ matrix.stability }} --prefer-dist --no-interaction - name: Execute tests - run: vendor/bin/phpunit + run: vendor/bin/phpunit --exclude-group disk-test diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e3a667..e421915 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to `laravel-filepond` will be documented in this file. +## 11.0.2 - 2024-07-30 + +- Fixed large file processing in third party storage 🐛. +- Docker development environment isolated 🐳. +- Filepond disk test cases added ✅. + ## 11.0.1 - 2024-07-10 - Fixed large file processing (out of memory exception) 🐛. diff --git a/README.md b/README.md index 1e80143..0709cc9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A straight forward backend support for Laravel application to work with [FilePond](https://pqina.nl/filepond/) file upload javascript library. This package keeps tracks of all the uploaded files and provides an easier interface for the developers to interact with them. It currently features - - Single and multiple file uploads. -- Chunk uploads with resume. +- Chunk uploads with resume support. - Third party storage support. - Global server side validation for temporary files. - Controller/Request level validation before moving the temporary files to permanent location. diff --git a/docker-compose.yml b/docker-compose.yml index ecca8c5..d5f079a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,10 @@ -version: '3' services: laravel-filepond: + image: laravel-filepond-development:11.x build: context: ./docker/8.2 dockerfile: Dockerfile - image: laravel-filepond-development - container_name: laravel-filepond-11 + container_name: laravel-filepond-11-dev stdin_open: true tty: true volumes: diff --git a/phpunit.xml b/phpunit.xml index 16f642e..0b70510 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -12,6 +12,11 @@ + + + + + diff --git a/src/Filepond.php b/src/Filepond.php index e9a3a06..9b37b5c 100644 --- a/src/Filepond.php +++ b/src/Filepond.php @@ -185,7 +185,11 @@ private function putFile(FilepondModel $filepond, string $path, string $disk, st $pathInfo = pathinfo($path); - Storage::disk($permanentDisk)->putFileAs($pathInfo['dirname'], new File(Storage::disk($this->getTempDisk())->path($filepond->filepath)), $pathInfo['filename'].'.'.$filepond->extension, $visibility); + Storage::disk($permanentDisk)->writeStream( + $pathInfo['dirname'].DIRECTORY_SEPARATOR.$pathInfo['filename'].'.'.$filepond->extension, + Storage::disk($this->getTempDisk())->readStream($filepond->filepath), + ['visibility' => $visibility], + ); return [ 'id' => $filepond->id, diff --git a/tests/Feature/FilepondFacadeTest.php b/tests/Feature/FilepondFacadeTest.php index e2dea7c..a20aec0 100644 --- a/tests/Feature/FilepondFacadeTest.php +++ b/tests/Feature/FilepondFacadeTest.php @@ -2,11 +2,8 @@ namespace RahulHaque\Filepond\Tests\Feature; -use Illuminate\Http\Request; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; -use Illuminate\Validation\Rule; -use Illuminate\Validation\ValidationException; use PHPUnit\Framework\Attributes\Test; use RahulHaque\Filepond\Facades\Filepond; use RahulHaque\Filepond\Tests\TestCase; @@ -14,173 +11,6 @@ class FilepondFacadeTest extends TestCase { - #[Test] - public function can_validate_null_filepond_file_upload() - { - Storage::disk(config('filepond.temp_disk', 'local'))->deleteDirectory(config('filepond.temp_folder', 'filepond/temp')); - - $request = new Request([ - 'avatar' => null, - ]); - - try { - $request->validate([ - 'avatar' => Rule::filepond('required|image|mimes:jpg|size:30'), - ]); - } catch (ValidationException $e) { - $this->assertEquals($e->errors(), ['avatar' => ['The avatar field is required.']]); - } - } - - #[Test] - public function can_validate_after_filepond_file_upload() - { - Storage::disk(config('filepond.temp_disk', 'local'))->deleteDirectory(config('filepond.temp_folder', 'filepond/temp')); - - $user = User::factory()->create(); - - $response = $this - ->actingAs($user) - ->post(route('filepond-process'), [ - 'avatar' => UploadedFile::fake()->image('avatar.png', 1024, 1024), - ], [ - 'Content-Type' => 'multipart/form-data', - 'accept' => 'application/json', - ]); - - $request = new Request([ - 'avatar' => $response->content(), - ]); - - try { - $request->validate([ - 'avatar' => Rule::filepond('required|image|mimes:jpg|size:30'), - ]); - } catch (ValidationException $e) { - $this->assertEquals($e->errors(), [ - 'avatar' => [ - 'The avatar field must be a file of type: jpg.', - 'The avatar field must be 30 kilobytes.', - ], - ]); - } - } - - #[Test] - public function can_validate_after_multiple_filepond_file_upload() - { - Storage::disk(config('filepond.temp_disk', 'local'))->deleteDirectory(config('filepond.temp_folder', 'filepond/temp')); - - $user = User::factory()->create(); - - $responses = []; - - // Create 5 temporary file uploads - for ($i = 1; $i <= 5; $i++) { - $response = $this->actingAs($user) - ->post(route('filepond-process'), [ - 'gallery' => UploadedFile::fake()->image('gallery-'.$i.'.png', 1024, 1024), - ], [ - 'Content-Type' => 'multipart/form-data', - 'accept' => 'application/json', - ]); - - $responses[] = $response->content(); - } - - $request = new Request([ - 'gallery' => $responses, - ]); - - try { - $request->validate([ - 'gallery.*' => Rule::filepond('required|image|mimes:jpg|size:30'), - ]); - } catch (ValidationException $e) { - $this->assertEquals($e->errors(), [ - 'gallery.0' => [ - 'The gallery.0 field must be a file of type: jpg.', - 'The gallery.0 field must be 30 kilobytes.', - ], - 'gallery.1' => [ - 'The gallery.1 field must be a file of type: jpg.', - 'The gallery.1 field must be 30 kilobytes.', - ], - 'gallery.2' => [ - 'The gallery.2 field must be a file of type: jpg.', - 'The gallery.2 field must be 30 kilobytes.', - ], - 'gallery.3' => [ - 'The gallery.3 field must be a file of type: jpg.', - 'The gallery.3 field must be 30 kilobytes.', - ], - 'gallery.4' => [ - 'The gallery.4 field must be a file of type: jpg.', - 'The gallery.4 field must be 30 kilobytes.', - ], - ]); - } - } - - #[Test] - public function can_validate_after_nested_multiple_filepond_file_upload() - { - Storage::disk(config('filepond.temp_disk', 'local'))->deleteDirectory(config('filepond.temp_folder', 'filepond/temp')); - - $user = User::factory()->create(); - - $responses = []; - - // Create 5 temporary file uploads - for ($i = 1; $i <= 5; $i++) { - $response = $this->actingAs($user) - ->post(route('filepond-process'), [ - 'galleries' => UploadedFile::fake()->image('gallery-'.$i.'.png', 1024, 1024), - ], [ - 'Content-Type' => 'multipart/form-data', - 'accept' => 'application/json', - ]); - - $responses[] = [ - 'title' => fake()->name(), - 'image' => $response->content(), - ]; - } - - $request = new Request([ - 'galleries' => $responses, - ]); - - try { - $request->validate([ - 'galleries.*.image' => Rule::filepond('required|image|mimes:jpg|size:30'), - ]); - } catch (ValidationException $e) { - $this->assertEquals($e->errors(), [ - 'galleries.0.image' => [ - 'The galleries.0.image field must be a file of type: jpg.', - 'The galleries.0.image field must be 30 kilobytes.', - ], - 'galleries.1.image' => [ - 'The galleries.1.image field must be a file of type: jpg.', - 'The galleries.1.image field must be 30 kilobytes.', - ], - 'galleries.2.image' => [ - 'The galleries.2.image field must be a file of type: jpg.', - 'The galleries.2.image field must be 30 kilobytes.', - ], - 'galleries.3.image' => [ - 'The galleries.3.image field must be a file of type: jpg.', - 'The galleries.3.image field must be 30 kilobytes.', - ], - 'galleries.4.image' => [ - 'The galleries.4.image field must be a file of type: jpg.', - 'The galleries.4.image field must be 30 kilobytes.', - ], - ]); - } - } - #[Test] public function can_get_temporary_file_after_filepond_file_upload() { diff --git a/tests/Feature/FilepondValidationTest.php b/tests/Feature/FilepondValidationTest.php new file mode 100644 index 0000000..66dfdec --- /dev/null +++ b/tests/Feature/FilepondValidationTest.php @@ -0,0 +1,182 @@ +deleteDirectory(config('filepond.temp_folder', 'filepond/temp')); + + $request = new Request([ + 'avatar' => null, + ]); + + try { + $request->validate([ + 'avatar' => Rule::filepond('required|image|mimes:jpg|size:30'), + ]); + } catch (ValidationException $e) { + $this->assertEquals($e->errors(), ['avatar' => ['The avatar field is required.']]); + } + } + + #[Test] + public function can_validate_after_filepond_file_upload() + { + Storage::disk(config('filepond.temp_disk', 'local'))->deleteDirectory(config('filepond.temp_folder', 'filepond/temp')); + + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->post(route('filepond-process'), [ + 'avatar' => UploadedFile::fake()->image('avatar.png', 1024, 1024), + ], [ + 'Content-Type' => 'multipart/form-data', + 'accept' => 'application/json', + ]); + + $request = new Request([ + 'avatar' => $response->content(), + ]); + + try { + $request->validate([ + 'avatar' => Rule::filepond('required|image|mimes:jpg|size:30'), + ]); + } catch (ValidationException $e) { + $this->assertEquals($e->errors(), [ + 'avatar' => [ + 'The avatar field must be a file of type: jpg.', + 'The avatar field must be 30 kilobytes.', + ], + ]); + } + } + + #[Test] + public function can_validate_after_multiple_filepond_file_upload() + { + Storage::disk(config('filepond.temp_disk', 'local'))->deleteDirectory(config('filepond.temp_folder', 'filepond/temp')); + + $user = User::factory()->create(); + + $responses = []; + + // Create 5 temporary file uploads + for ($i = 1; $i <= 5; $i++) { + $response = $this->actingAs($user) + ->post(route('filepond-process'), [ + 'gallery' => UploadedFile::fake()->image('gallery-'.$i.'.png', 1024, 1024), + ], [ + 'Content-Type' => 'multipart/form-data', + 'accept' => 'application/json', + ]); + + $responses[] = $response->content(); + } + + $request = new Request([ + 'gallery' => $responses, + ]); + + try { + $request->validate([ + 'gallery.*' => Rule::filepond('required|image|mimes:jpg|size:30'), + ]); + } catch (ValidationException $e) { + $this->assertEquals($e->errors(), [ + 'gallery.0' => [ + 'The gallery.0 field must be a file of type: jpg.', + 'The gallery.0 field must be 30 kilobytes.', + ], + 'gallery.1' => [ + 'The gallery.1 field must be a file of type: jpg.', + 'The gallery.1 field must be 30 kilobytes.', + ], + 'gallery.2' => [ + 'The gallery.2 field must be a file of type: jpg.', + 'The gallery.2 field must be 30 kilobytes.', + ], + 'gallery.3' => [ + 'The gallery.3 field must be a file of type: jpg.', + 'The gallery.3 field must be 30 kilobytes.', + ], + 'gallery.4' => [ + 'The gallery.4 field must be a file of type: jpg.', + 'The gallery.4 field must be 30 kilobytes.', + ], + ]); + } + } + + #[Test] + public function can_validate_after_nested_multiple_filepond_file_upload() + { + Storage::disk(config('filepond.temp_disk', 'local'))->deleteDirectory(config('filepond.temp_folder', 'filepond/temp')); + + $user = User::factory()->create(); + + $responses = []; + + // Create 5 temporary file uploads + for ($i = 1; $i <= 5; $i++) { + $response = $this->actingAs($user) + ->post(route('filepond-process'), [ + 'galleries' => UploadedFile::fake()->image('gallery-'.$i.'.png', 1024, 1024), + ], [ + 'Content-Type' => 'multipart/form-data', + 'accept' => 'application/json', + ]); + + $responses[] = [ + 'title' => fake()->name(), + 'image' => $response->content(), + ]; + } + + $request = new Request([ + 'galleries' => $responses, + ]); + + try { + $request->validate([ + 'galleries.*.image' => Rule::filepond('required|image|mimes:jpg|size:30'), + ]); + } catch (ValidationException $e) { + $this->assertEquals($e->errors(), [ + 'galleries.0.image' => [ + 'The galleries.0.image field must be a file of type: jpg.', + 'The galleries.0.image field must be 30 kilobytes.', + ], + 'galleries.1.image' => [ + 'The galleries.1.image field must be a file of type: jpg.', + 'The galleries.1.image field must be 30 kilobytes.', + ], + 'galleries.2.image' => [ + 'The galleries.2.image field must be a file of type: jpg.', + 'The galleries.2.image field must be 30 kilobytes.', + ], + 'galleries.3.image' => [ + 'The galleries.3.image field must be a file of type: jpg.', + 'The galleries.3.image field must be 30 kilobytes.', + ], + 'galleries.4.image' => [ + 'The galleries.4.image field must be a file of type: jpg.', + 'The galleries.4.image field must be 30 kilobytes.', + ], + ]); + } + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 8100b69..1b3702d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -30,7 +30,7 @@ protected function defineEnvironment($app) include_once __DIR__.'/database/migrations/create_users_table.php.stub'; include_once __DIR__.'/../database/migrations/create_fileponds_table.php.stub'; - (new \CreateUsersTable())->up(); - (new \CreateFilepondsTable())->up(); + (new \CreateUsersTable)->up(); + (new \CreateFilepondsTable)->up(); } } diff --git a/tests/Unit/FilepondDiskTest.php b/tests/Unit/FilepondDiskTest.php new file mode 100644 index 0000000..34c624f --- /dev/null +++ b/tests/Unit/FilepondDiskTest.php @@ -0,0 +1,130 @@ +deleteDirectory(config('filepond.temp_folder', 'filepond/temp')); + Storage::disk(config('filepond.disk', 'public'))->deleteDirectory('moved'); + + $user = User::factory()->create(); + + $uploadedFile = UploadedFile::fake()->image('avatar.png', 1024, 1024); + + $response = $this + ->actingAs($user) + ->post(route('filepond-process'), [ + 'avatar' => $uploadedFile, + ], [ + 'Content-Type' => 'multipart/form-data', + 'accept' => 'application/json', + ]); + + $fileInfo = Filepond::field($response->content())->moveTo($pathToMove); + + Storage::disk(config('filepond.disk', 'public'))->assertExists($fileInfo['location']); + } + + #[Test] + #[Group('disk-test')] + public function can_move_file_external_to_external() + { + Config::set('filepond.temp_disk', 's3'); + Config::set('filepond.disk', 's3'); + + $pathToMove = 'moved/avatar'; + + Storage::disk(config('filepond.temp_disk', 'local'))->deleteDirectory(config('filepond.temp_folder', 'filepond/temp')); + Storage::disk(config('filepond.disk', 'public'))->deleteDirectory('moved'); + + $user = User::factory()->create(); + + $uploadedFile = UploadedFile::fake()->image('avatar.png', 1024, 1024); + + $response = $this + ->actingAs($user) + ->post(route('filepond-process'), [ + 'avatar' => $uploadedFile, + ], [ + 'Content-Type' => 'multipart/form-data', + 'accept' => 'application/json', + ]); + + $fileInfo = Filepond::field($response->content())->moveTo($pathToMove); + + Storage::disk(config('filepond.disk', 'public'))->assertExists($fileInfo['location']); + } + + #[Test] + #[Group('disk-test')] + public function can_move_file_local_to_external() + { + Config::set('filepond.disk', 's3'); + + $pathToMove = 'moved/avatar'; + + Storage::disk(config('filepond.temp_disk', 'local'))->deleteDirectory(config('filepond.temp_folder', 'filepond/temp')); + Storage::disk(config('filepond.disk', 'public'))->deleteDirectory('moved'); + + $user = User::factory()->create(); + + $uploadedFile = UploadedFile::fake()->image('avatar.png', 1024, 1024); + + $response = $this + ->actingAs($user) + ->post(route('filepond-process'), [ + 'avatar' => $uploadedFile, + ], [ + 'Content-Type' => 'multipart/form-data', + 'accept' => 'application/json', + ]); + + $fileInfo = Filepond::field($response->content())->moveTo($pathToMove); + + Storage::disk(config('filepond.disk', 'public'))->assertExists($fileInfo['location']); + } + + #[Test] + #[Group('disk-test')] + public function can_move_file_external_to_local() + { + Config::set('filepond.temp_disk', 's3'); + + $pathToMove = 'moved/avatar'; + + Storage::disk(config('filepond.temp_disk', 'local'))->deleteDirectory(config('filepond.temp_folder', 'filepond/temp')); + Storage::disk(config('filepond.disk', 'public'))->deleteDirectory('moved'); + + $user = User::factory()->create(); + + $uploadedFile = UploadedFile::fake()->image('avatar.png', 1024, 1024); + + $response = $this + ->actingAs($user) + ->post(route('filepond-process'), [ + 'avatar' => $uploadedFile, + ], [ + 'Content-Type' => 'multipart/form-data', + 'accept' => 'application/json', + ]); + + $fileInfo = Filepond::field($response->content())->moveTo($pathToMove); + + Storage::disk(config('filepond.disk', 'public'))->assertExists($fileInfo['location']); + } +}