├── .styleci.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── composer.lock ├── config └── filepond.php ├── database └── migrations │ └── create_fileponds_table.php.stub ├── docker-compose.yml ├── docker └── 8.2 │ └── Dockerfile ├── pint.json ├── routes └── web.php └── src ├── AbstractFilepond.php ├── Console └── FilepondClear.php ├── Exceptions └── InvalidChunkException.php ├── Facades └── Filepond.php ├── Filepond.php ├── FilepondServiceProvider.php ├── Http └── Controllers │ └── FilepondController.php ├── Models └── Filepond.php ├── Rules └── FilepondRule.php ├── Services └── FilepondService.php └── Traits └── HasFilepond.php /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | disabled: 4 | - single_class_element_per_statement 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-filepond` will be documented in this file. 4 | 5 | ## 12.1.1 - 2025-05-08 6 | 7 | - Fixed overriding the disk default visibility #75. 🐛 8 | 9 | ## 12.1.0 - 2025-03-19 10 | 11 | - Mimetype added in fileinfo response. ✨ 12 | 13 | ## 12.0.0 - 2025-02-27 14 | 15 | - Laravel 12 support added. ✨ 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Rahul Haque 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel FilePond Backend 2 | 3 | [](https://packagist.org/packages/rahulhaque/laravel-filepond) 4 | [](https://packagist.org/packages/rahulhaque/laravel-filepond) 5 | 6 | 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 - 7 | 8 | - Single and multiple file uploads. 9 | - Chunk uploads with resume support. 10 | - Third party storage support. 11 | - Global server side validation for temporary files. 12 | - Controller/Request level validation before moving the temporary files to permanent location. 13 | - Scheduled artisan command to clean up temporary files and folders after they have expired. 14 | - Can handle filepond's `process`, `patch`, `head`, `revert` and `restore` endpoints. 15 | - Can handle large files efficiently. 16 | 17 | Support the development with a :star: to let others know it worked for you. 18 | 19 | [](https://ko-fi.com/W7W2I1JIV) 20 | 21 | **Demo Projects** 22 | 23 | - [Laravel-filepond-vue-inertia-example](https://github.com/rahulhaque/laravel-filepond-vue-inertia-example) 24 | 25 | **Video Tutorials:** 26 | 27 | - Thanks [ludoguenet](https://github.com/ludoguenet) for featuring my package in - [Créer un système de Drag'n Drop avec Laravel Filepond](https://www.youtube.com/watch?v=IQ3fEseDck8) (in French). 28 | 29 | ## Documentation 30 | 31 | See the corresponding branch for the documentation. 32 | 33 | |Version|Branch| 34 | |:-:|:-:| 35 | |Laravel 12|[12.x branch](../../tree/12.x/README.md)| 36 | |Laravel 11|[11.x branch](../../tree/11.x/README.md)| 37 | |Laravel 10|[10.x branch](../../tree/10.x/README.md)| 38 | |Laravel 9|[9.x branch](../../tree/9.x/README.md)| 39 | |Laravel 8|[8.x branch](../../tree/8.x/README.md)| 40 | |Laravel 7|[7.x branch](../../tree/7.x/README.md)| 41 | 42 | ## Installation 43 | 44 | Laravel 12 users install with. 45 | 46 | ```bash 47 | composer require rahulhaque/laravel-filepond:"^12.0" 48 | ``` 49 | 50 | Publish the configuration and migration files. 51 | 52 | ```bash 53 | php artisan vendor:publish --provider="RahulHaque\Filepond\FilepondServiceProvider" 54 | ``` 55 | 56 | Run the migration. 57 | 58 | ```bash 59 | php artisan migrate 60 | ``` 61 | 62 | ## Quickstart 63 | 64 | Before we begin, first install and integrate the [FilePond](https://pqina.nl/filepond/docs/) library in your project any way you prefer. 65 | 66 | Let's assume we are updating a user avatar and his/her gallery like the form below. 67 | 68 | ```html 69 |
81 | 82 | 97 | ``` 98 | 99 | Now selecting a file with FilePond input field will upload the file in the temporary directory immediately and append the hidden input in the form. Submit the form to process the uploaded file like below in your controller. 100 | 101 | In `UserAvatarController.php` get and process the submitted file by calling the `moveTo()` method from the `Filepond` facade which will return the moved file information as well as delete the file from the temporary storage. 102 | 103 | ```php 104 | use Illuminate\Http\Request; 105 | use Illuminate\Routing\Controller; 106 | use Illuminate\Validation\Rule; 107 | use RahulHaque\Filepond\Facades\Filepond; 108 | 109 | class UserAvatarController extends Controller 110 | { 111 | /** 112 | * Update the avatar for the user. 113 | * 114 | * @param \Illuminate\Http\Request $request 115 | * @return \Illuminate\Http\Response 116 | */ 117 | public function update(Request $request) 118 | { 119 | // Single and multiple file validation 120 | $this->validate($request, [ 121 | 'avatar' => Rule::filepond([ 122 | 'required', 123 | 'image', 124 | 'max:2000' 125 | ]), 126 | 'gallery.*' => Rule::filepond([ 127 | 'required', 128 | 'image', 129 | 'max:2000' 130 | ]) 131 | ]); 132 | 133 | // Set filename 134 | $avatarName = 'avatar-' . auth()->id(); 135 | 136 | // Move the file to permanent storage 137 | // Automatic file extension set 138 | $fileInfo = Filepond::field($request->avatar) 139 | ->moveTo('avatars/' . $avatarName); 140 | 141 | // dd($fileInfo); 142 | // [ 143 | // "id" => 1, 144 | // "dirname" => "avatars", 145 | // "basename" => "avatar-1.png", 146 | // "extension" => "png", 147 | // "mimetype" => "image/png", 148 | // "filename" => "avatar-1", 149 | // "location" => "avatars/avatar-1.png", 150 | // "url" => "http://localhost/storage/avatars/avatar-1.png", 151 | // ]; 152 | 153 | $galleryName = 'gallery-' . auth()->id(); 154 | 155 | $fileInfos = Filepond::field($request->gallery) 156 | ->moveTo('galleries/' . $galleryName); 157 | 158 | // dd($fileInfos); 159 | // [ 160 | // [ 161 | // "id" => 1, 162 | // "dirname" => "galleries", 163 | // "basename" => "gallery-1-1.png", 164 | // "extension" => "png", 165 | // "mimetype" => "image/png", 166 | // "filename" => "gallery-1-1", 167 | // "location" => "galleries/gallery-1-1.png", 168 | // "url" => "http://localhost/storage/galleries/gallery-1-1.png", 169 | // ], 170 | // [ 171 | // "id" => 2, 172 | // "dirname" => "galleries", 173 | // "basename" => "gallery-1-2.png", 174 | // "extension" => "png", 175 | // "mimetype" => "image/png", 176 | // "filename" => "gallery-1-2", 177 | // "location" => "galleries/gallery-1-2.png", 178 | // "url" => "http://localhost/storage/galleries/gallery-1-2.png", 179 | // ], 180 | // [ 181 | // "id" => 3, 182 | // "dirname" => "galleries", 183 | // "basename" => "gallery-1-3.png", 184 | // "extension" => "png", 185 | // "mimetype" => "image/png", 186 | // "filename" => "gallery-1-3", 187 | // "location" => "galleries/gallery-1-3.png", 188 | // "url" => "http://localhost/storage/galleries/gallery-1-3.png", 189 | // ], 190 | // ] 191 | } 192 | } 193 | ``` 194 | 195 | This is the quickest way to get started. This package has already implemented all the classes and controllers for you. Next we will discuss about all the nitty gritty stuffs available. 196 | 197 | > [!IMPORTANT] 198 | > If you have Laravel debugbar installed, make sure to add `filepond*` in the `except` array of the `./config/debugbar.php` to ignore appending debugbar information. 199 | 200 | ## Configuration 201 | 202 | First have a look at the `./config/filepond.php` to know about all the options available out of the box. Some important ones mentioned below. 203 | 204 | #### Permanent Storage 205 | 206 | This package uses Laravel's public filesystem driver for permanent file storage by default. Change the `disk` option to anything you prefer for permanent storage. Hold up! But I am using different disks for different uploads? Don't worry. You will be able to change the disk name on the fly with [copyTo()](https://github.com/rahulhaque/laravel-filepond#copyto) and [moveTo()](https://github.com/rahulhaque/laravel-filepond#moveto) methods. 207 | 208 | #### Temporary Storage 209 | 210 | This package uses Laravel's `local` filesystem driver for temporary file storage by default. Change the `temp_disk` and `temp_folder` name to points towards directory for temporary file storage. 211 | 212 | > [!NOTE] 213 | > Setting temporary file storage to third party will upload the files directly to cloud. On the other hand, you will lose the ability to use controller level validation because the files will not be available in your application server. 214 | 215 | #### Validation Rules 216 | 217 | Default global server side validation rules can be changed by modifying `validation_rules` array in `./config/filepond.php`. These rules will be applicable to all file uploads by FilePond's `/process` endpoint. 218 | 219 | There is also a custom validation rule `Rule::filepond($rules)` is available to validate temporary files before moving them to desired location. Use it with your `FormRequest` class or directly in the controller, whichever you prefer. 220 | 221 | #### Middleware 222 | 223 | By default, all filepond's routes are protected by `web` and `auth` middleware. Change it if required. 224 | 225 | #### Soft Delete 226 | 227 | By default `soft_delete` is set to `true` to keep track of all the files uploaded by the users. Set it to false if you want to delete the files with delete request. 228 | 229 | ## Commands (Cleanup) 230 | 231 | This package includes a `php artisan filepond:clear` command to clean up the expired files from the temporary storage. File `expiration` minute can be set in the config file, default is 30 minutes. Add this command to your scheduled command list to run daily. Know more about task scheduling here - [Scheduling Artisan Commands](https://laravel.com/docs/8.x/scheduling#scheduling-artisan-commands) 232 | 233 | This command takes a `--all` option which will truncate the `Filepond` model and delete everything inside the temporary storage regardless they are expired or not. This is useful when you lost track of your uploaded files and want to start clean. 234 | 235 | > [!NOTE] 236 | > If you see your files are not deleted even after everything is set up correctly, then its probably directory permission issue. Try setting the permission of filepond's temporary directory to 775 with `sudo chmod -R 775 ./storage/app/filepond/`. And run `php artisan filepond:clear --all` for a clean start (optional). For third party storage like - amazon s3, make sure you have the correct policy set. 237 | 238 | ### Methods 239 | 240 | #### field() 241 | 242 | `Filepond::field($field, $checkOwnership)` is a required method which tells the library which FilePond form field to work with. The optional second parameter is to check file ownership of the field. It is useful when an unauthenticated user uploads a file and later tries to retrieve it after authentication. Chain the rest of the methods as required. 243 | 244 | #### Rule::filepond($rules) 245 | 246 | Use `Rule::filepond($rules)` inside Request class or directly in controller or in custom Validator to validate your filepond fields. See the example. 247 | 248 | > [!NOTE] 249 | > This method will not work when third party storage is set as your temporary storage. The files are uploaded directly to your third party storage and not available locally for any further modification. Calling this method in such condition will throw error that the file is not found. 250 | 251 | #### copyTo() 252 | 253 | Calling the `Filepond::field()->copyTo($pathWithFilename)` method will copy the file from the temporary storage to the path provided along with the filename. It will set the file extension **automatically**. By default the files will be copied to directory relative to config's `disk` option. You can also pass a disk name as **second parameter** if you want to override that. This method will return the copied file info along with `Filepond` model id. For multiple file upload, it will return an array of copied files info. Also note that multiple files will be copied with **trailing incremental** values like `$filename-{$i}`. 254 | 255 | #### moveTo() 256 | 257 | Calling the `Filepond::field()->moveTo($pathWithFilename)` method works the same way as `copyTo()` method. By default the files will be moved to directory relative to config's `disk` option. You can also pass a disk name as **second parameter** if you want to override that. One thing it does extra for you is delete the temporary file after copying, respecting the value of config's `soft_delete` option for `Filepond` model. 258 | 259 | #### delete() 260 | 261 | Calling the `Filepond::field()->delete()` method will delete the temporary file respecting the soft delete configuration for `Filepond` model. This method is useful when you're manually handling the file processing using `getFile()` method. 262 | 263 | ### APIs 264 | 265 | If you need more granular approach and know the ins and outs of this package, you may use the below APIs to get the underneath file object and file model to interact with them further. 266 | 267 | #### getFile() 268 | 269 | `Filepond::field()->getFile()` method returns the file object same as the Laravel's `$request->file()` object. For multiple uploads, it will return an array of uploaded file objects. You can then process the file manually any way you want. 270 | 271 | Processing the file object manually will not update the associated `Filepond` model which is used to keep track of the uploaded files. However the expired files will be cleaned up as usual by the scheduled command. It is recommended that you either call the [delete()](#delete) method or update the underlying model by calling [getModel()](#getModel) method after the processing is done. 272 | 273 | > [!NOTE] 274 | > This method is not available when third party storage is set as your temporary storage. The files are uploaded directly to your third party storage and not available locally for any further modification. Calling this method in such condition will throw error that the file is not found. 275 | 276 | #### getModel() 277 | 278 | `Filepond::field()->getModel()` method returns the underlying Laravel `Filepond` model for the given field. This is useful when you have added some custom fields to update in the published migration file for your need. 279 | 280 | #### getDataURL() 281 | 282 | `Filepond::field()->getDataURL()` method returns the Data URL of uploaded file for the given field just like [HTTP Data URLs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs). This is useful when you need to store the raw content along with encryption, such as - user signature. 283 | 284 | ### Traits 285 | 286 | There is a `HasFilepond` trait available to get the temporary files uploaded by the users. 287 | 288 | ```php 289 | namespace App\Models; 290 | 291 | use Illuminate\Foundation\Auth\User as Authenticatable; 292 | use RahulHaque\Filepond\Traits\HasFilepond; 293 | 294 | class User extends Authenticatable 295 | { 296 | use HasFilepond; 297 | } 298 | ``` 299 | 300 | Now you can get all the files info uploaded by a single user like this. 301 | 302 | ```php 303 | User::find(1)->fileponds; 304 | ``` 305 | 306 | ## Development 307 | 308 | First clone the repo and `cd` into the directory. Build development environment with docker. 309 | 310 | ```bash 311 | # Build the development image 312 | docker compose build 313 | 314 | # Run the development container from image 315 | docker compose up -d 316 | 317 | # Drop to development shell 318 | docker compose exec laravel-filepond-12 bash 319 | 320 | # Install dependencies 321 | composer install 322 | 323 | # Run tests 324 | composer test 325 | ``` 326 | 327 | To free up space after development. 328 | 329 | ```bash 330 | # To stop the container for later use 331 | docker compose stop 332 | 333 | # Stop and remove the container 334 | docker compose down -v 335 | 336 | # Also remove the development image if necessary 337 | docker image rm laravel-filepond-12-development 338 | ``` 339 | 340 | ## Testing 341 | 342 | ```bash 343 | composer test 344 | ``` 345 | 346 | ## Changelog 347 | 348 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 349 | 350 | ## Contributing 351 | 352 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 353 | 354 | ### Security 355 | 356 | If you discover any security related issues, please email rahulhaque07@gmail.com instead of using the issue tracker. 357 | 358 | ## Credits 359 | 360 | - [Rahul Haque](https://github.com/rahulhaque) 361 | - [All Contributors](../../contributors) 362 | 363 | ## License 364 | 365 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 366 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rahulhaque/laravel-filepond", 3 | "description": "Use FilePond the Laravel way", 4 | "keywords": [ 5 | "filepond-laravel", 6 | "laravel-filepond", 7 | "filepond" 8 | ], 9 | "homepage": "https://github.com/rahulhaque/laravel-filepond", 10 | "license": "MIT", 11 | "type": "library", 12 | "authors": [ 13 | { 14 | "name": "Rahul Haque", 15 | "email": "rahulhaque07@gmail.com", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.2", 21 | "laravel/framework": "^12.0" 22 | }, 23 | "require-dev": { 24 | "fakerphp/faker": "^1.23", 25 | "laravel/pail": "^1.2.2", 26 | "laravel/pint": "^1.13", 27 | "laravel/sail": "^1.41", 28 | "league/flysystem-aws-s3-v3": "^3.0", 29 | "mockery/mockery": "^1.6", 30 | "nunomaduro/collision": "^8.6", 31 | "orchestra/testbench": "^10.0", 32 | "phpunit/phpunit": "^11.5.3" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "RahulHaque\\Filepond\\": "src" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "RahulHaque\\Filepond\\Tests\\": "tests", 42 | "Workbench\\App\\": "workbench/app/", 43 | "Workbench\\Database\\Factories\\": "workbench/database/factories/", 44 | "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" 45 | } 46 | }, 47 | "scripts": { 48 | "test": "vendor/bin/phpunit", 49 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage", 50 | "post-autoload-dump": [ 51 | "@clear", 52 | "@prepare" 53 | ], 54 | "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", 55 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 56 | "build": "@php vendor/bin/testbench workbench:build --ansi", 57 | "serve": [ 58 | "Composer\\Config::disableProcessTimeout", 59 | "@build", 60 | "@php vendor/bin/testbench serve" 61 | ], 62 | "lint": [ 63 | "@php vendor/bin/pint", 64 | "@php vendor/bin/phpstan analyse" 65 | ] 66 | }, 67 | "config": { 68 | "sort-packages": true 69 | }, 70 | "extra": { 71 | "laravel": { 72 | "providers": [ 73 | "RahulHaque\\Filepond\\FilepondServiceProvider" 74 | ], 75 | "aliases": { 76 | "Filepond": "RahulHaque\\Filepond\\Facades\\Filepond" 77 | } 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /config/filepond.php: -------------------------------------------------------------------------------- 1 | env('FILEPOND_DISK', 'public'), 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | FilePond Temporary Disk 19 | |-------------------------------------------------------------------------- 20 | | 21 | | Set the FilePond temporary disk and folder name to be used for temporary 22 | | storage. This disk will be used for temporary file storage and cleared 23 | | upon running the "artisan filepond:clear" command. It is recommended to 24 | | use local disk for temporary storage when you want to take advantage of 25 | | controller level validation. File validation from third party storage is 26 | | not yet supported. However, global 'validation_rules' defined in this 27 | | config will work fine. 28 | | 29 | */ 30 | 'temp_disk' => 'local', 31 | 'temp_folder' => 'filepond/temp', 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | FilePond Routes Middleware 36 | |-------------------------------------------------------------------------- 37 | | 38 | | Default middleware for FilePond routes. 39 | | 40 | */ 41 | 'middleware' => [ 42 | 'web', 'auth', 43 | ], 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Soft Delete FilePond Model 48 | |-------------------------------------------------------------------------- 49 | | 50 | | Determine whether to enable or disable soft delete in FilePond model. 51 | | 52 | */ 53 | 'soft_delete' => true, 54 | 55 | /* 56 | |-------------------------------------------------------------------------- 57 | | File Delete After (Minutes) 58 | |-------------------------------------------------------------------------- 59 | | 60 | | Set the minutes after which the FilePond temporary storage files will be 61 | | deleted while running 'artisan filepond:clear' command. 62 | | 63 | */ 64 | 'expiration' => 30, 65 | 66 | /* 67 | |-------------------------------------------------------------------------- 68 | | FilePond Controller 69 | |-------------------------------------------------------------------------- 70 | | 71 | | FilePond controller determines how the requests from FilePond library is 72 | | processed. 73 | | 74 | */ 75 | 'controller' => RahulHaque\Filepond\Http\Controllers\FilepondController::class, 76 | 77 | /* 78 | |-------------------------------------------------------------------------- 79 | | FilePond Model 80 | |-------------------------------------------------------------------------- 81 | | 82 | | Set the filepond model to be used by the package. Make sure you extend 83 | | the custom model with "RahulHaque\Filepond\Models\Filepond" model. 84 | | 85 | */ 86 | 'model' => RahulHaque\Filepond\Models\Filepond::class, 87 | 88 | /* 89 | |-------------------------------------------------------------------------- 90 | | Global Validation Rules 91 | |-------------------------------------------------------------------------- 92 | | 93 | | Set the default validation for filepond's ./process route. In other words 94 | | temporary file upload validation. 95 | | 96 | */ 97 | 'validation_rules' => [ 98 | 'required', 99 | 'file', 100 | 'max:5000', 101 | ], 102 | 103 | /* 104 | |-------------------------------------------------------------------------- 105 | | FilePond Server Paths 106 | |-------------------------------------------------------------------------- 107 | | 108 | | Configure url for each of the FilePond actions. 109 | | See details - https://pqina.nl/filepond/docs/patterns/api/server/ 110 | | 111 | */ 112 | 'server' => [ 113 | 'url' => env('FILEPOND_URL', '/filepond'), 114 | ], 115 | ]; 116 | -------------------------------------------------------------------------------- /database/migrations/create_fileponds_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('filename'); 19 | $table->string('filepath'); 20 | $table->string('extension', 100); 21 | $table->string('mimetypes', 100); 22 | $table->string('disk', 100); 23 | $table->unsignedBigInteger('created_by')->nullable(); 24 | $table->dateTime('expires_at')->nullable(); 25 | $table->softDeletes(); 26 | $table->timestamps(); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('fileponds'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | laravel-filepond-12: 3 | image: laravel-filepond-12-development:12.x 4 | build: 5 | context: ./docker/8.2 6 | dockerfile: Dockerfile 7 | container_name: laravel-filepond-12-dev 8 | stdin_open: true 9 | tty: true 10 | volumes: 11 | - '.:/code' 12 | network_mode: host 13 | -------------------------------------------------------------------------------- /docker/8.2/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | WORKDIR /code 4 | 5 | RUN ln -snf /usr/share/zoneinfo/UTC /etc/localtime && echo UTC > /etc/timezone 6 | 7 | RUN apt update && apt install -y gnupg ca-certificates sqlite3 8 | 9 | RUN mkdir -p ~/.gnupg \ 10 | && chmod 600 ~/.gnupg \ 11 | && echo "disable-ipv6" >> ~/.gnupg/dirmngr.conf \ 12 | && echo "keyserver hkp://keyserver.ubuntu.com:80" >> ~/.gnupg/dirmngr.conf \ 13 | && gpg --recv-key 0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c \ 14 | && gpg --export 0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c > /usr/share/keyrings/ppa_ondrej_php.gpg \ 15 | && echo "deb [signed-by=/usr/share/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu jammy main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \ 16 | && apt update 17 | 18 | RUN apt install -y php8.2-fpm php8.2-cli \ 19 | php8.2-sqlite3 php8.2-gd php8.2-curl \ 20 | php8.2-imap php8.2-mbstring \ 21 | php8.2-xml php8.2-zip php8.2-bcmath php8.2-soap \ 22 | php8.2-intl php8.2-readline \ 23 | php8.2-ldap \ 24 | php8.2-msgpack php8.2-igbinary \ 25 | && php -r "readfile('https://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer 26 | 27 | RUN apt-get -y autoremove \ 28 | && apt-get clean \ 29 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 30 | 31 | ENTRYPOINT ["bash"] -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "rules": { 4 | "array_push": true, 5 | "backtick_to_shell_exec": true, 6 | "date_time_immutable": true, 7 | "declare_strict_types": true, 8 | "lowercase_keywords": true, 9 | "lowercase_static_reference": true, 10 | "final_class": false, 11 | "final_internal_class": false, 12 | "final_public_method_for_abstract_class": false, 13 | "fully_qualified_strict_types": true, 14 | "global_namespace_import": { 15 | "import_classes": true, 16 | "import_constants": true, 17 | "import_functions": true 18 | }, 19 | "mb_str_functions": true, 20 | "modernize_types_casting": true, 21 | "new_with_parentheses": false, 22 | "no_superfluous_elseif": true, 23 | "no_useless_else": true, 24 | "no_multiple_statements_per_line": true, 25 | "ordered_class_elements": { 26 | "order": [ 27 | "use_trait", 28 | "case", 29 | "constant", 30 | "constant_public", 31 | "constant_protected", 32 | "constant_private", 33 | "property_public", 34 | "property_protected", 35 | "property_private", 36 | "construct", 37 | "destruct", 38 | "magic", 39 | "phpunit", 40 | "method_abstract", 41 | "method_public_static", 42 | "method_public", 43 | "method_protected_static", 44 | "method_protected", 45 | "method_private_static", 46 | "method_private" 47 | ], 48 | "sort_algorithm": "none" 49 | }, 50 | "ordered_interfaces": true, 51 | "ordered_traits": true, 52 | "protected_to_private": false, 53 | "self_accessor": true, 54 | "self_static_accessor": true, 55 | "strict_comparison": true, 56 | "visibility_required": true 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | config('filepond.middleware', ['web', 'auth'])], function () { 9 | Route::post(config('filepond.server.url', '/filepond'), [config('filepond.controller', FilepondController::class), 'process'])->name('filepond-process'); 10 | Route::patch(config('filepond.server.url', '/filepond'), [config('filepond.controller', FilepondController::class), 'patch'])->name('filepond-patch'); 11 | Route::get(config('filepond.server.url', '/filepond'), [config('filepond.controller', FilepondController::class), 'head'])->name('filepond-head'); 12 | Route::delete(config('filepond.server.url', '/filepond'), [config('filepond.controller', FilepondController::class), 'revert'])->name('filepond-revert'); 13 | }); 14 | -------------------------------------------------------------------------------- /src/AbstractFilepond.php: -------------------------------------------------------------------------------- 1 | tempDisk; 35 | } 36 | 37 | /** 38 | * @return $this 39 | */ 40 | public function setTempDisk(string $tempDisk) 41 | { 42 | $this->tempDisk = $tempDisk; 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * Decrypt the FilePond field value data 49 | * 50 | * @return array 51 | */ 52 | protected function getFieldValue() 53 | { 54 | return $this->fieldValue; 55 | } 56 | 57 | /** 58 | * Set the FilePond field value data 59 | * 60 | * @return $this 61 | */ 62 | protected function setFieldValue(string|array|null $fieldValue) 63 | { 64 | if (! $fieldValue) { 65 | $this->fieldValue = null; 66 | 67 | return $this; 68 | } 69 | 70 | $this->isMultipleUpload = is_array($fieldValue); 71 | 72 | if ($this->getIsMultipleUpload()) { 73 | if (! $fieldValue[0]) { 74 | $this->fieldValue = null; 75 | 76 | return $this; 77 | } 78 | 79 | $this->fieldValue = array_map(function ($input) { 80 | return $this->decrypt($input); 81 | }, $fieldValue); 82 | 83 | return $this; 84 | } 85 | 86 | $this->fieldValue = $this->decrypt($fieldValue); 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * @return bool 93 | */ 94 | protected function getIsMultipleUpload() 95 | { 96 | return $this->isMultipleUpload; 97 | } 98 | 99 | /** 100 | * Get the filepond database model for the FilePond field 101 | * 102 | * @return mixed 103 | */ 104 | protected function getFieldModel() 105 | { 106 | return $this->fieldModel; 107 | } 108 | 109 | /** 110 | * Set the FilePond model from the field 111 | * 112 | * @return $this 113 | */ 114 | protected function setFieldModel(string $model) 115 | { 116 | if (! $this->getFieldValue()) { 117 | $this->fieldModel = null; 118 | 119 | return $this; 120 | } 121 | 122 | if ($this->getIsMultipleUpload()) { 123 | $this->fieldModel = $model::when($this->isOwnershipAware, function ($query) { 124 | $query->owned(); 125 | }) 126 | ->whereIn('id', (new Collection($this->getFieldValue()))->pluck('id')) 127 | ->get(); 128 | 129 | return $this; 130 | } 131 | 132 | $input = $this->getFieldValue(); 133 | $this->fieldModel = $model::when($this->isOwnershipAware, function ($query) { 134 | $query->owned(); 135 | }) 136 | ->where('id', $input['id']) 137 | ->first(); 138 | 139 | return $this; 140 | } 141 | 142 | /** 143 | * Get the soft delete from filepond config 144 | * 145 | * @return bool 146 | */ 147 | protected function getIsSoftDeletable() 148 | { 149 | return $this->isSoftDeletable; 150 | } 151 | 152 | /** 153 | * Set the soft delete value from filepond config 154 | * 155 | * @return $this 156 | */ 157 | protected function setIsSoftDeletable(bool $isSoftDeletable) 158 | { 159 | $this->isSoftDeletable = $isSoftDeletable; 160 | 161 | return $this; 162 | } 163 | 164 | /** 165 | * Get the ownership check value for filepond model 166 | * 167 | * @return bool 168 | */ 169 | protected function getIsOwnershipAware() 170 | { 171 | return $this->isOwnershipAware; 172 | } 173 | 174 | /** 175 | * Set the ownership check value for filepond model 176 | * 177 | * @return $this 178 | */ 179 | protected function setIsOwnershipAware(bool $isOwnershipAware) 180 | { 181 | $this->isOwnershipAware = $isOwnershipAware; 182 | 183 | return $this; 184 | } 185 | 186 | /** 187 | * Decrypt the FilePond field value data 188 | * 189 | * @return mixed 190 | */ 191 | protected function decrypt(string $data) 192 | { 193 | return Crypt::decrypt($data, true); 194 | } 195 | 196 | /** 197 | * Create file object from filepond model 198 | * 199 | * @return UploadedFile 200 | */ 201 | protected function createFileObject(Filepond $filepond) 202 | { 203 | return new UploadedFile( 204 | Storage::disk($this->tempDisk)->path($filepond->filepath), 205 | $filepond->filename, 206 | $filepond->mimetypes, 207 | UPLOAD_ERR_OK, 208 | true 209 | ); 210 | } 211 | 212 | /** 213 | * Create Data URL from filepond model 214 | * More at - https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs 215 | * 216 | * @return string 217 | * 218 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 219 | */ 220 | protected function createDataUrl(Filepond $filepond) 221 | { 222 | return 'data:'.$filepond->mimetypes.';base64,'.base64_encode(Storage::disk($this->tempDisk)->get($filepond->filepath)); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/Console/FilepondClear.php: -------------------------------------------------------------------------------- 1 | option('all')) { 49 | if ($this->confirm('Are you sure?', true)) { 50 | $model::truncate(); 51 | $this->info('Fileponds table truncated.'); 52 | Storage::disk($tempDisk)->deleteDirectory($tempFolder); 53 | $this->info('Temporary files and folders deleted.'); 54 | 55 | return 0; 56 | } 57 | $this->info('Operation cancelled.'); 58 | 59 | return 0; 60 | } 61 | 62 | $expiredFiles = $model::withTrashed()->where('expires_at', '<=', now())->select(['id', 'filepath']); 63 | $this->info('Total expired files and folders: '.$expiredFiles->count()); 64 | if ($expiredFiles->count() > 0) { 65 | foreach ($expiredFiles->get() as $expiredFile) { 66 | Storage::disk($tempDisk)->delete($expiredFile->filepath); 67 | Storage::disk($tempDisk)->deleteDirectory($tempFolder.'/'.$expiredFile->id); 68 | } 69 | $this->info('Temporary files and folders deleted.'); 70 | $expiredFiles->forceDelete(); 71 | } 72 | 73 | return 0; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidChunkException.php: -------------------------------------------------------------------------------- 1 | setFieldValue($field) 20 | ->setTempDisk(config('filepond.temp_disk', 'local')) 21 | ->setIsSoftDeletable(config('filepond.soft_delete', true)) 22 | ->setIsOwnershipAware($checkOwnership) 23 | ->setFieldModel(config('filepond.model', FilepondModel::class)); 24 | 25 | return $this; 26 | } 27 | 28 | /** 29 | * Return file object from the field 30 | * 31 | * @return array|\Illuminate\Http\UploadedFile 32 | */ 33 | public function getFile() 34 | { 35 | if (! $this->getFieldValue()) { 36 | return null; 37 | } 38 | 39 | if ($this->getIsMultipleUpload()) { 40 | return $this->getFieldModel()->map(function ($filepond) { 41 | return $this->createFileObject($filepond); 42 | })->toArray(); 43 | } 44 | 45 | return $this->createFileObject($this->getFieldModel()); 46 | } 47 | 48 | /** 49 | * Get the filepond file as Data URL string 50 | * More at - https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs 51 | * 52 | * @return array|string 53 | * 54 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 55 | */ 56 | public function getDataURL() 57 | { 58 | if (! $this->getFieldValue()) { 59 | return null; 60 | } 61 | 62 | if ($this->getIsMultipleUpload()) { 63 | return $this->getFieldModel()->map(function ($filepond) { 64 | return $this->createDataUrl($filepond); 65 | })->toArray(); 66 | } 67 | 68 | return $this->createDataUrl($this->getFieldModel()); 69 | } 70 | 71 | /** 72 | * Get the filepond database model for the FilePond field 73 | * 74 | * @return mixed 75 | */ 76 | public function getModel() 77 | { 78 | return $this->getFieldModel(); 79 | } 80 | 81 | /** 82 | * Copy the FilePond files to destination 83 | * 84 | * @return array 85 | * 86 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 87 | */ 88 | public function copyTo(string $path, string $disk = '', string $visibility = '') 89 | { 90 | if (! $this->getFieldValue()) { 91 | return null; 92 | } 93 | 94 | if ($this->getIsMultipleUpload()) { 95 | $response = []; 96 | $fileponds = $this->getFieldModel(); 97 | foreach ($fileponds as $index => $filepond) { 98 | $to = $path.'-'.($index + 1); 99 | $response[] = $this->putFile($filepond, $to, $disk, $visibility); 100 | } 101 | 102 | return $response; 103 | } 104 | 105 | $filepond = $this->getFieldModel(); 106 | 107 | return $this->putFile($filepond, $path, $disk, $visibility); 108 | } 109 | 110 | /** 111 | * Copy the FilePond files to destination and delete 112 | * 113 | * @return array 114 | * 115 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 116 | */ 117 | public function moveTo(string $path, string $disk = '', string $visibility = '') 118 | { 119 | if (! $this->getFieldValue()) { 120 | return null; 121 | } 122 | 123 | if ($this->getIsMultipleUpload()) { 124 | $response = []; 125 | $fileponds = $this->getFieldModel(); 126 | foreach ($fileponds as $index => $filepond) { 127 | $to = $path.'-'.($index + 1); 128 | $response[] = $this->putFile($filepond, $to, $disk, $visibility); 129 | } 130 | $this->delete(); 131 | 132 | return $response; 133 | } 134 | 135 | $filepond = $this->getFieldModel(); 136 | $response = $this->putFile($filepond, $path, $disk, $visibility); 137 | $this->delete(); 138 | 139 | return $response; 140 | } 141 | 142 | /** 143 | * Delete files related to FilePond field 144 | * 145 | * @return void 146 | */ 147 | public function delete() 148 | { 149 | if (! $this->getFieldValue()) { 150 | return null; 151 | } 152 | 153 | if ($this->getIsMultipleUpload()) { 154 | $fileponds = $this->getFieldModel(); 155 | foreach ($fileponds as $filepond) { 156 | if ($this->getIsSoftDeletable()) { 157 | $filepond->delete(); 158 | } else { 159 | Storage::disk($this->getTempDisk())->delete($filepond->filepath); 160 | $filepond->forceDelete(); 161 | } 162 | } 163 | 164 | return; 165 | } 166 | 167 | $filepond = $this->getFieldModel(); 168 | if ($this->getIsSoftDeletable()) { 169 | $filepond->delete(); 170 | } else { 171 | Storage::disk($this->getTempDisk())->delete($filepond->filepath); 172 | $filepond->forceDelete(); 173 | } 174 | } 175 | 176 | /** 177 | * Put the file in permanent storage and return response 178 | * 179 | * @return array 180 | * 181 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 182 | */ 183 | private function putFile(FilepondModel $filepond, string $path, string $disk, string $visibility) 184 | { 185 | $permanentDisk = $disk === '' ? $filepond->disk : $disk; 186 | 187 | $pathInfo = pathinfo($path); 188 | 189 | Storage::disk($permanentDisk)->writeStream( 190 | $pathInfo['dirname'].DIRECTORY_SEPARATOR.$pathInfo['filename'].'.'.$filepond->extension, 191 | Storage::disk($this->getTempDisk())->readStream($filepond->filepath), 192 | $visibility !== '' ? ['visibility' => $visibility] : [], 193 | ); 194 | 195 | return [ 196 | 'id' => $filepond->id, 197 | 'dirname' => dirname($path.'.'.$filepond->extension), 198 | 'basename' => basename($path.'.'.$filepond->extension), 199 | 'extension' => $filepond->extension, 200 | 'mimetype' => $filepond->mimetypes, 201 | 'filename' => basename($path.'.'.$filepond->extension, '.'.$filepond->extension), 202 | 'location' => $path.'.'.$filepond->extension, 203 | 'url' => Storage::disk($permanentDisk)->url($path.'.'.$filepond->extension), 204 | ]; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/FilepondServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadRoutesFrom(__DIR__.'/../routes/web.php'); 20 | 21 | Rule::macro('filepond', function ($args) { 22 | return new FilepondRule($args); 23 | }); 24 | 25 | if ($this->app->runningInConsole()) { 26 | $this->publishes([ 27 | __DIR__.'/../config/filepond.php' => base_path('config/filepond.php'), 28 | ], 'filepond-config'); 29 | 30 | if (! class_exists('CreateFilepondsTable')) { 31 | $this->publishes([ 32 | __DIR__.'/../database/migrations/create_fileponds_table.php.stub' => database_path('migrations/'.date('Y_m_d_His', time()).'_create_fileponds_table.php'), 33 | ], 'filepond-migrations'); 34 | } 35 | 36 | $this->commands([ 37 | FilepondClear::class, 38 | ]); 39 | } 40 | } 41 | 42 | /** 43 | * Register the application services. 44 | */ 45 | public function register() 46 | { 47 | $this->mergeConfigFrom(__DIR__.'/../config/filepond.php', 'filepond'); 48 | 49 | $this->app->singleton('filepond', function () { 50 | return new Filepond; 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Http/Controllers/FilepondController.php: -------------------------------------------------------------------------------- 1 | hasHeader('upload-length')) { 24 | return Response::make($service->initChunk(), 200, ['content-type' => 'text/plain']); 25 | } 26 | 27 | $validator = $service->validator($request, config('filepond.validation_rules', [])); 28 | 29 | if ($validator->fails()) { 30 | return Response::make($validator->errors(), 422); 31 | } 32 | 33 | return Response::make($service->store($request), 200, ['content-type' => 'text/plain']); 34 | } 35 | 36 | /** 37 | * FilePond ./patch route logic. 38 | * 39 | * @return \Illuminate\Http\Response 40 | * 41 | * @throws Throwable 42 | */ 43 | public function patch(Request $request, FilepondService $service) 44 | { 45 | return Response::make('Ok', 200)->withHeaders(['Upload-Offset' => $service->chunk($request)]); 46 | } 47 | 48 | /** 49 | * FilePond ./head, ./restore route logic. 50 | * 51 | * @return \Illuminate\Http\Response 52 | * 53 | * @throws Throwable 54 | */ 55 | public function head(Request $request, FilepondService $service) 56 | { 57 | // If request has patch key, then its a head request 58 | if ($request->has('patch')) { 59 | return Response::make('Ok', 200)->withHeaders(['Upload-Offset' => $service->offset($request->patch)]); 60 | } 61 | 62 | // If request has restore key, then its a restore request 63 | if ($request->has('restore')) { 64 | [$filepond, $content] = $service->restore($request->restore); 65 | 66 | return Response::make($content, 200)->withHeaders([ 67 | 'Access-Control-Expose-Headers' => 'Content-Disposition', 68 | 'Content-Type' => $filepond->mimetypes, 69 | 'Content-Disposition' => 'inline; filename="'.$filepond->filename.'"', 70 | ]); 71 | } 72 | 73 | return Response::make('Feature not implemented yet.', 406); 74 | } 75 | 76 | /** 77 | * FilePond ./revert route logic. 78 | * 79 | * @return \Illuminate\Http\Response 80 | */ 81 | public function revert(Request $request, FilepondService $service) 82 | { 83 | $filepond = $service->retrieve($request->getContent()); 84 | 85 | $service->delete($filepond); 86 | 87 | return Response::make('Ok', 200, ['content-type' => 'text/plain']); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Models/Filepond.php: -------------------------------------------------------------------------------- 1 | when(auth()->check(), function ($query) { 22 | $query->where($this->getTable().'.created_by', auth()->id()); 23 | }); 24 | } 25 | 26 | public function creator() 27 | { 28 | return $this->belongsTo(config('auth.providers.users.model'), 'created_by'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Rules/FilepondRule.php: -------------------------------------------------------------------------------- 1 | rules = $rules; 31 | } 32 | 33 | /** 34 | * Set the performing validator. 35 | * 36 | * @param \Illuminate\Contracts\Validation\Validator $validator 37 | * @return $this 38 | */ 39 | public function setValidator($validator) 40 | { 41 | $this->validator = $validator; 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * Set the data under validation. 48 | * 49 | * @param array $data 50 | * @return $this 51 | */ 52 | public function setData($data) 53 | { 54 | $this->data = $data; 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * Determine if the validation rule passes. 61 | * 62 | * @param string $attribute 63 | * @param mixed $value 64 | * @return bool 65 | */ 66 | public function passes($attribute, $value) 67 | { 68 | $file = Filepond::field($value)->getFile(); 69 | 70 | data_set($this->data, $attribute, $file); 71 | 72 | $validator = Validator::make( 73 | $this->data, 74 | [$attribute => $this->rules], 75 | $this->validator->customMessages, 76 | $this->validator->customAttributes 77 | ); 78 | 79 | $this->messages = $validator->errors()->all(); 80 | 81 | return ! $validator->fails(); 82 | } 83 | 84 | /** 85 | * Get the validation error message. 86 | * 87 | * @return array 88 | */ 89 | public function message() 90 | { 91 | return $this->messages; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Services/FilepondService.php: -------------------------------------------------------------------------------- 1 | disk = config('filepond.disk', 'public'); 29 | $this->tempDisk = config('filepond.temp_disk', 'local'); 30 | $this->tempFolder = config('filepond.temp_folder', 'filepond/temp'); 31 | $this->model = config('filepond.model', Filepond::class); 32 | } 33 | 34 | /** 35 | * Validate the filepond file 36 | * 37 | * @return \Illuminate\Contracts\Validation\Validator 38 | */ 39 | public function validator(Request $request, array $rules) 40 | { 41 | $field = array_key_first(Arr::dot($request->all())); 42 | 43 | return Validator::make($request->all(), [$field => $rules]); 44 | } 45 | 46 | /** 47 | * Store the uploaded file in the fileponds table 48 | * 49 | * @return string 50 | */ 51 | public function store(Request $request) 52 | { 53 | $file = $this->getUploadedFile($request); 54 | 55 | $filepond = $this->model::create([ 56 | 'filepath' => $file->store($this->tempFolder, $this->tempDisk), 57 | 'filename' => $file->getClientOriginalName(), 58 | 'extension' => $file->getClientOriginalExtension(), 59 | 'mimetypes' => $file->getClientMimeType(), 60 | 'disk' => $this->disk, 61 | 'created_by' => auth()->id(), 62 | 'expires_at' => now()->addMinutes(config('filepond.expiration', 30)), 63 | ]); 64 | 65 | return Crypt::encrypt(['id' => $filepond->id]); 66 | } 67 | 68 | /** 69 | * Retrieve the filepond file from encrypted text 70 | * 71 | * @return mixed 72 | */ 73 | public function retrieve(string $content) 74 | { 75 | $input = Crypt::decrypt($content); 76 | 77 | return $this->model::where('id', $input['id'])->firstOrFail(); 78 | } 79 | 80 | /** 81 | * Initialize and make a slot for chunk upload 82 | * 83 | * @return string 84 | */ 85 | public function initChunk() 86 | { 87 | $filepond = $this->model::create([ 88 | 'filepath' => '', 89 | 'filename' => '', 90 | 'extension' => '', 91 | 'mimetypes' => '', 92 | 'disk' => $this->disk, 93 | 'created_by' => auth()->id(), 94 | 'expires_at' => now()->addMinutes(config('filepond.expiration', 30)), 95 | ]); 96 | 97 | Storage::disk($this->tempDisk)->makeDirectory($this->tempFolder.'/'.$filepond->id); 98 | 99 | return Crypt::encrypt(['id' => $filepond->id]); 100 | } 101 | 102 | /** 103 | * Merge chunks 104 | * 105 | * @return string 106 | * 107 | * @throws Throwable 108 | */ 109 | public function chunk(Request $request) 110 | { 111 | $id = Crypt::decrypt($request->patch)['id']; 112 | $dir = Storage::disk($this->tempDisk)->path($this->tempFolder.'/'.$id.'/'); 113 | 114 | $contentLength = $request->header('Content-Length'); 115 | $uploadLength = $request->header('Upload-Length'); 116 | $uploadName = $request->header('Upload-Name'); 117 | $uploadOffset = $request->header('Upload-Offset'); 118 | 119 | $chunkSize = file_put_contents($dir.$uploadOffset, $request->getContent()); 120 | 121 | if ($chunkSize === false || $chunkSize === 0 || (int) $contentLength !== $chunkSize) { 122 | unlink($dir.$uploadOffset); // Remove invalid chunk to retry 123 | throw new InvalidChunkException; 124 | } 125 | 126 | $size = 0; 127 | $chunks = glob($dir.'*'); 128 | foreach ($chunks as $chunk) { 129 | $size += filesize($chunk); 130 | } 131 | 132 | if ((int) $uploadLength === $size) { 133 | $file = fopen($dir.$uploadName, 'w'); 134 | foreach ($chunks as $chunk) { 135 | $uploadOffset = basename($chunk); 136 | 137 | $chunkFile = fopen($chunk, 'r'); 138 | $chunkContent = fread($chunkFile, filesize($chunk)); 139 | fclose($chunkFile); 140 | 141 | fseek($file, (int) $uploadOffset); 142 | fwrite($file, $chunkContent); 143 | 144 | unlink($chunk); 145 | } 146 | fclose($file); 147 | 148 | $filepond = $this->retrieve($request->patch); 149 | $filepond->update([ 150 | 'filepath' => $this->tempFolder.'/'.$id.'/'.$uploadName, 151 | 'filename' => $uploadName, 152 | 'extension' => pathinfo($uploadName, PATHINFO_EXTENSION), 153 | 'mimetypes' => Storage::disk($this->tempDisk)->mimeType($this->tempFolder.'/'.$id.'/'.$uploadName), 154 | 'disk' => $this->disk, 155 | 'created_by' => auth()->id(), 156 | 'expires_at' => now()->addMinutes(config('filepond.expiration', 30)), 157 | ]); 158 | } 159 | 160 | return $size; 161 | } 162 | 163 | /** 164 | * Get the offset of the last uploaded chunk for resume 165 | * 166 | * @return false|int 167 | */ 168 | public function offset(string $content) 169 | { 170 | $filepond = $this->retrieve($content); 171 | 172 | $dir = Storage::disk($this->tempDisk)->path($this->tempFolder.'/'.$filepond->id.'/'); 173 | $size = 0; 174 | $chunks = glob($dir.'*'); 175 | foreach ($chunks as $chunk) { 176 | $size += filesize($chunk); 177 | } 178 | 179 | return $size; 180 | } 181 | 182 | /** 183 | * Retrieve the filepond file model and content 184 | * 185 | * @return mixed 186 | */ 187 | public function restore(string $content) 188 | { 189 | $filepond = $this->retrieve($content); 190 | 191 | return [$filepond, Storage::disk($this->tempDisk)->get($filepond->filepath)]; 192 | } 193 | 194 | /** 195 | * Delete the filepond file and record respecting soft delete 196 | * 197 | * @return bool|null 198 | */ 199 | public function delete(Filepond $filepond) 200 | { 201 | if (config('filepond.soft_delete', true)) { 202 | return $filepond->delete(); 203 | } 204 | 205 | Storage::disk($this->tempDisk)->delete($filepond->filepath); 206 | Storage::disk($this->tempDisk)->deleteDirectory($this->tempFolder.'/'.$filepond->id); 207 | 208 | return $filepond->forceDelete(); 209 | } 210 | 211 | /** 212 | * Get the file from request 213 | * 214 | * @return mixed 215 | */ 216 | protected function getUploadedFile(Request $request) 217 | { 218 | $field = array_key_first(Arr::dot($request->all())); 219 | 220 | return $request->file($field); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/Traits/HasFilepond.php: -------------------------------------------------------------------------------- 1 | hasMany(config('filepond.model', Filepond::class), 'created_by'); 19 | } 20 | } 21 | --------------------------------------------------------------------------------