├── .editorconfig ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .scrutinizer.yml ├── .styleci.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── config └── upload-handler.php ├── examples ├── blueimp.blade.php ├── dropzone.blade.php ├── flow-js.blade.php ├── ng-file-upload.blade.php ├── plupload.blade.php ├── resumable-js.blade.php └── simple-uploader-js.blade.php ├── phpunit.xml.dist ├── phpunit.xml.dist.bak ├── src ├── Driver │ ├── BaseHandler.php │ ├── BlueimpHandler.php │ ├── DropzoneHandler.php │ ├── FlowJsHandler.php │ ├── MonolithHandler.php │ ├── NgFileHandler.php │ ├── PluploadHandler.php │ ├── ResumableJsHandler.php │ └── SimpleUploaderJsHandler.php ├── Event │ └── FileUploaded.php ├── Exception │ ├── ChecksumMismatchHttpException.php │ ├── InternalServerErrorHttpException.php │ └── RequestEntityTooLargeHttpException.php ├── Helper │ └── ChunkHelpers.php ├── Identifier │ ├── AuthIdentifier.php │ ├── Identifier.php │ ├── NopIdentifier.php │ └── SessionIdentifier.php ├── IdentityManager.php ├── Range │ ├── ContentRange.php │ ├── DropzoneRange.php │ ├── NgFileUploadRange.php │ ├── PluploadRange.php │ ├── Range.php │ ├── RequestBodyRange.php │ └── ResumableJsRange.php ├── Response │ └── PercentageJsonResponse.php ├── StorageConfig.php ├── UploadHandler.php ├── UploadHandlerServiceProvider.php └── UploadManager.php └── tests ├── Driver ├── BlueimpHandlerTest.php ├── DropzoneHandlerTest.php ├── FlowJsHandlerTest.php ├── MonolithHandlerTest.php ├── NgFileHandlerTest.php ├── PluploadHandlerTest.php ├── ResumableJsHandlerTest.php └── SimpleUploaderJsHandlerTest.php ├── Identifier ├── AuthIdentifierTest.php ├── NopIdentifierTest.php └── SessionIdentifierTest.php ├── IdentityManagerTest.php ├── Range ├── ContentRangeTest.php ├── DropzoneRangeTest.php ├── NgFileUploadRangeTest.php ├── PluploadRangeTest.php └── ResumableJsRangeTest.php ├── Response └── PercentageJsonResponseTest.php ├── TestCase.php └── UploadManagerTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | pull_request: 8 | branches: 9 | - 'main' 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | php: [ 8.0, 8.1, 8.2, 8.3, 8.4 ] 19 | laravel: [ '^9.0', '^10.0', '^11.0', '^12.0' ] 20 | dependency-version: [prefer-lowest, prefer-stable] 21 | exclude: 22 | - laravel: '^9.0' 23 | php: 8.3 24 | - laravel: '^10.0' 25 | php: 8.0 26 | - laravel: '^11.0' 27 | php: 8.0 28 | - laravel: '^11.0' 29 | php: 8.1 30 | - laravel: '^12.0' 31 | php: 8.0 32 | - laravel: '^12.0' 33 | php: 8.1 34 | 35 | name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.dependency-version }} 36 | 37 | steps: 38 | - name: Update apt 39 | run: sudo apt-get update --fix-missing 40 | 41 | - name: Checkout code 42 | uses: actions/checkout@v4 43 | 44 | - name: Cache dependencies 45 | uses: actions/cache@v4 46 | with: 47 | path: ~/.composer/cache/files 48 | key: laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 49 | 50 | - name: Setup PHP 51 | uses: shivammathur/setup-php@v2 52 | with: 53 | php-version: ${{ matrix.php }} 54 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick 55 | coverage: xdebug 56 | 57 | - name: Validate composer.json 58 | run: composer validate 59 | 60 | - name: Install dependencies 61 | run: | 62 | composer require "illuminate/support:${{ matrix.laravel }}" "illuminate/http:${{ matrix.laravel }}" --no-interaction --no-update 63 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest 64 | 65 | - name: Execute tests 66 | run: vendor/bin/phpunit 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | .DS_Store 3 | Thumbs.db 4 | /.idea 5 | /.vscode 6 | .phpunit.result.cache 7 | composer.lock 8 | /build 9 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | paths: 3 | - 'src/' 4 | excluded_paths: 5 | - 'tests/' 6 | dependency_paths: 7 | - 'vendor/' 8 | 9 | checks: 10 | php: true 11 | 12 | build: 13 | nodes: 14 | coverage: 15 | environment: 16 | php: 17 | ini: 18 | "xdebug.mode": coverage 19 | tests: 20 | override: 21 | - command: ./vendor/bin/phpunit --coverage-clover=build/coverage.clover 22 | coverage: 23 | file: build/coverage.clover 24 | format: clover 25 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr2 2 | 3 | enabled: 4 | - alpha_ordered_imports 5 | - concat_with_spaces 6 | - no_trailing_comma_in_list_call 7 | - no_trailing_comma_in_singleline_array 8 | - no_unused_imports 9 | - no_useless_else 10 | - no_useless_return 11 | - no_whitespace_before_comma_in_array 12 | - short_array_syntax 13 | - trailing_comma_in_multiline_array 14 | 15 | risky: true 16 | 17 | finder: 18 | exclude: 19 | - vendor 20 | name: "*.php" 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | We are very grateful that you have thought about contributing to this project, we actually have made some guidelines here to make it easier for everyone to start contributing and making this project a real better one. 3 | 4 | ## Code Style 5 | - This project follows the PSR-2 code style, please refer to the PHP-FIG's article on PSR-2 for more information. 6 | - This project uses **PHPDoc** for DocBlocking. 7 | 8 | ## Autoloading 9 | - This project follows the PSR-4 autoloading standards, please refer to the PHP-FIG's article on PSR-4 for more information. 10 | 11 | ## Pull Request Best Practices 12 | - Create an issue: It is considered a best practice to start a new issue containing the subject of the suggested change, either it is a bug fix, an optimization or a new feature. 13 | Sometimes there things that you might not have noticed or not clear enough to you, so we strongly recommend you to start a new issue before risking to waste your time on something that have been added already. 14 | - Create a fork of this project: This will make your own. 15 | - Create a new branch for the code you want to contribute (e.g. `new-feature`) 16 | - Make sure all the existing tests pass, and add tests for the new code if relevant. 17 | - Push your code to your feature branch. 18 | - Make a new pull request: As we have already suggested in the first point of this guide, it is strongly recommended to create a new issue in all cases, if you have followed the mentioned advice, you can include the issue number in the PR message. 19 | 20 | ## Licensing 21 | All contributions to this project are licensed under the MIT License. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Coding Socks 4 | Copyright (c) 2019 LaraCrafts 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Upload Handler 2 | 3 | Upload Handler Package For Laravel 4 | 5 | [![Github Actions Badge](https://github.com/coding-socks/laravel-upload-handler/workflows/run-tests/badge.svg)](https://github.com/coding-socks/laravel-upload-handler/actions?query=workflow%3A"run-tests") 6 | [![Downloads Badge](https://poser.pugx.org/coding-socks/laravel-upload-handler/downloads)](https://packagist.org/packages/coding-socks/laravel-upload-handler) 7 | [![Version Badge](https://poser.pugx.org/coding-socks/laravel-upload-handler/version)](https://packagist.org/packages/coding-socks/laravel-upload-handler) 8 | [![Coverage Badge](https://scrutinizer-ci.com/g/coding-socks/laravel-upload-handler/badges/coverage.png?b=main)](https://scrutinizer-ci.com/g/coding-socks/laravel-upload-handler/) 9 | [![License Badge](https://poser.pugx.org/coding-socks/laravel-upload-handler/license)](https://packagist.org/packages/coding-socks/laravel-upload-handler) 10 | 11 | This package helps integrate a Laravel application with chunk uploader libraries eg. 12 | [DropzoneJS](https://www.dropzonejs.com/) and 13 | [jQuery-File-Upload from blueimp](https://blueimp.github.io/jQuery-File-Upload/). 14 | 15 | Uploading a large file in chunks can help reduce risks. 16 | 17 | - PHP from 5.3.4 limits the number of concurrent uploads and by uploading a file in one request can limit the 18 | availability of a service. ([max_file_uploads][php-max-file-uploads]) 19 | - For security reasons many systems limit the payload size, and the uploadable file size. PHP is not an exception. 20 | ([upload_max_filesize][php-upload-max-filesize]) 21 | - It can be useful to check the meta information of a file and decline an upload upfront, so the user does not have to 22 | wait for minutes or seconds to upload a large file and then receive an error message for an invalid the file type 23 | or mime type. 24 | - Can include resume functionality which means an upload can be continued after a reconnection. 25 | 26 | However, there is not a single RFC about chunked uploads and this caused many implementations. The most mature 27 | project at the moment is [tus](https://tus.io/). 28 | 29 | Similar projects: 30 | 31 | - Multiple library support: 32 | [1up-lab/OneupUploaderBundle](https://github.com/1up-lab/OneupUploaderBundle), 33 | [pionl/laravel-chunk-upload](https://github.com/pionl/laravel-chunk-upload) 34 | - Single library support: 35 | [ankitpokhrel/tus-php](https://github.com/ankitpokhrel/tus-php), 36 | [flowjs/flow-php-server](https://github.com/flowjs/flow-php-server), 37 | [jildertmiedema/laravel-plupload](https://github.com/jildertmiedema/laravel-plupload), 38 | [OneOffTech/laravel-tus-upload](https://github.com/OneOffTech/laravel-tus-upload) 39 | 40 | ## Table of contents 41 | 42 | - [Installation](#installation) 43 | - [Requirements](#requirements) 44 | - [Usage](#usage) 45 | - [Events](#events) 46 | - [Changing the driver](#changing-the-driver) 47 | - [Adding your own drivers](#adding-your-own-drivers) 48 | - [Drivers](#drivers) 49 | - [Monolith](#monolith-driver) 50 | - [Blueimp](#blueimp-driver) 51 | - [DropzoneJS](#dropzonejs-driver) 52 | - [Flow.js](#flowjs-driver) 53 | - [ng-file-upload](#ng-file-upload-driver) 54 | - [Plupload](#plupload-driver) 55 | - [Resumable.js](#resumablejs-driver) 56 | - [simple-uploader.js](#simple-uploaderjs-driver) 57 | - [Identifiers](#identifiers) 58 | - [Session identifier](#session-identifier) 59 | - [Auth identifier](#auth-identifier) 60 | - [NOP identifier](#nop-identifier) 61 | - [Contribution](#contribution) 62 | - [License](#license) 63 | 64 | ## Installation 65 | 66 | You can easily install this package using Composer, by running the following command: 67 | 68 | ```bash 69 | composer require coding-socks/laravel-upload-handler 70 | ``` 71 | 72 | ### Requirements 73 | 74 | This package has the following requirements: 75 | 76 | - PHP `^7.3` 77 | - Laravel `^6.10 || ^7.0 || ^8.0` 78 | 79 | [Caret Version Range (^)](https://getcomposer.org/doc/articles/versions.md#caret-version-range-) 80 | 81 | ## Usage 82 | 83 | 1. Register a route 84 | ```php 85 | Route::any('/my-route', 'MyController@myFunction'); 86 | ``` 87 | 2. Retrieve the upload handler. (The chunk upload handler can be retrieved from the container in two ways.) 88 | - Using dependency injection 89 | ```php 90 | use Illuminate\Http\Request; 91 | use CodingSocks\UploadHandler\UploadHandler; 92 | 93 | class MyController extends Controller 94 | { 95 | public function myFunction(Request $request, UploadHandler $handler) 96 | { 97 | return $handler->handle($request); 98 | } 99 | } 100 | ``` 101 | - Resolving from the app container 102 | ```php 103 | use Illuminate\Http\Request; 104 | use CodingSocks\UploadHandler\UploadHandler; 105 | 106 | class MyController extends Controller 107 | { 108 | public function myFunction(Request $request) 109 | { 110 | $handler = app()->make(UploadHandler::class); 111 | return $handler->handle($request); 112 | } 113 | } 114 | ``` 115 | 116 | The handler exposes the following methods: 117 | 118 | Method | Description 119 | ---------------|-------------------------- 120 | `handle` | Handle the given request 121 | 122 | "Handle" is quite vague but there is a reason for that. This library tries to provide more functionality than just 123 | saving the uploaded chunks. It is also adds functionality for resumable uploads which depending on the client side 124 | library can differ very much. 125 | 126 | ### Events 127 | 128 | Once a file upload finished a `\CodingSocks\UploadHandler\Event\FileUploaded` is triggered. This event contains 129 | the disk and the path of the uploaded file. 130 | 131 | - [Laravel 7.x - Defining Listeners](https://laravel.com/docs/6.x/events#defining-listeners) 132 | - [Laravel 7.x - Defining Listeners](https://laravel.com/docs/7.x/events#defining-listeners) 133 | - [Laravel 8.x - Defining Listeners](https://laravel.com/docs/8.x/events#defining-listeners) 134 | 135 | You can also add a `Closure` as the second parameter of the `handle` method to add an inline listener. The listener 136 | is called with the disk and the path of the uploaded file. 137 | 138 | ```php 139 | $handler->handle($request, function ($disk, $path) { 140 | // Triggered when upload is finished 141 | }); 142 | ``` 143 | 144 | ### Changing the driver 145 | 146 | You can change the default driver by setting an `UPLOAD_DRIVER` environment variable or publishing the 147 | config file and changing it directly. 148 | 149 | ### Adding your own drivers 150 | 151 | Much like Laravel's core components, you can add your own drivers for this package. You can do this by adding the 152 | following code to a service provider. 153 | 154 | ```php 155 | app()->make(UploadManager::class)->extend('my_driver', function () { 156 | return new MyCustomUploadDriver(); 157 | }); 158 | ``` 159 | 160 | If you are adding a driver you need to extend the `\CodingSocks\UploadHandler\Driver\BaseHandler` abstract class, for 161 | which you can use the shipped drivers (e.g. `\CodingSocks\UploadHandler\Driver\BlueimpUploadDriver`) as an example as to 162 | how. 163 | 164 | If you wrote a custom driver that others might find useful, please consider adding it to the package via a pull request. 165 | 166 | ## Drivers 167 | 168 | Below is a list of available drivers along with their individual specs: 169 | 170 | Service | Driver name | Chunk upload | Resumable 171 | -------------------------------------------------|----------------------|--------------|----------- 172 | [Monolith](#monolith-driver) | `monolith` | no | no 173 | [Blueimp](#blueimp-driver) | `blueimp` | yes | yes 174 | [DropzoneJS](#dropzonejs-driver) | `dropzone` | yes | no 175 | [Flow.js](#flowjs-driver) | `flow-js` | yes | yes 176 | [ng-file-upload](#ng-file-upload-driver) | `ng-file-upload` | yes | no 177 | [Plupload](#plupload-driver) | `plupload` | yes | no 178 | [Resumable.js](#resumablejs-driver) | `resumable-js` | yes | yes 179 | [simple-uploader.js](#simple-uploaderjs-driver) | `simple-uploader-js` | yes | yes 180 | 181 | ### Monolith driver 182 | 183 | This driver is a fallback driver as it can handle normal file request. Save and delete capabilities are also added. 184 | 185 | ### Blueimp driver 186 | 187 | [website](https://blueimp.github.io/jQuery-File-Upload/) 188 | 189 | This driver handles requests made by the Blueimp jQuery File Upload client library. 190 | 191 | ### DropzoneJS driver 192 | 193 | [website](https://www.dropzonejs.com/) 194 | 195 | This driver handles requests made by the DropzoneJS client library. 196 | 197 | ### Flow.js driver 198 | 199 | [website](https://github.com/flowjs/flow.js) 200 | 201 | This driver handles requests made by the Flow.js client library. 202 | 203 | Because of [Issue #44](https://github.com/coding-socks/laravel-upload-handler/issues/44) you must use `forceChunkSize` 204 | option. 205 | 206 | ### ng-file-upload driver 207 | 208 | [website](https://github.com/danialfarid/ng-file-upload) 209 | 210 | This driver handles requests made by the ng-file-upload client library. 211 | 212 | ### Plupload driver 213 | 214 | [website](https://github.com/moxiecode/plupload) 215 | 216 | This driver handles requests made by the Plupload client library. 217 | 218 | ### Resumable.js driver 219 | 220 | [website](http://resumablejs.com/) 221 | 222 | This driver handles requests made by the Resumable.js client library. 223 | 224 | Because of [Issue #44](https://github.com/coding-socks/laravel-upload-handler/issues/44) you must use `forceChunkSize` 225 | option. 226 | 227 | ### simple-uploader.js driver 228 | 229 | [website](https://github.com/simple-uploader/Uploader) 230 | 231 | This driver handles requests made by the simple-uploader.js client library. 232 | 233 | Because of [Issue #44](https://github.com/coding-socks/laravel-upload-handler/issues/44) you must use `forceChunkSize` 234 | option. 235 | 236 | ## Identifiers 237 | 238 | In some cases an identifier is needed for the uploaded file when the client side library does not provide one. 239 | This identifier is important for resumable uploads as the library has to be able to check the status of the given 240 | file for a specific client. Without the identifier collisions can happen. 241 | 242 | Service | Driver name 243 | ------------------------------------------|------------- 244 | [Session identifier](#session-identifier) | `session` 245 | [Auth identifier](#auth-identifier) | `auth` 246 | [NOP identifier](#nop-identifier) | `nop` 247 | 248 | ### Session identifier 249 | 250 | This identifier uses the client session and the original file name to create an identifier for the upload file. 251 | 252 | ### Auth identifier 253 | 254 | This identifier uses the id of the authenticated user and the original file name to create an identifier for the upload file. 255 | 256 | It will throw `UnauthorizedException` when the user is unauthorized. However, it is still recommended using the `auth` middleware. 257 | 258 | ### NOP identifier 259 | 260 | This identifier uses the original file name to create an identifier for the upload file. This does not abstract the file 261 | identifier which can be useful for testing. 262 | 263 | ## Contribution 264 | 265 | All contributions are welcomed for this project, please refer to the [CONTRIBUTING.md][contributing] file for more 266 | information about contribution guidelines. 267 | 268 | ## License 269 | 270 | This product is licensed under the MIT license, please refer to the [License file][license] for more information. 271 | 272 | [contributing]: CONTRIBUTING.md 273 | [license]: LICENSE 274 | [php-max-file-uploads]: https://www.php.net/manual/en/ini.core.php#ini.max-file-uploads 275 | [php-upload-max-filesize]: https://www.php.net/manual/en/ini.core.php#ini.upload-max-filesize 276 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coding-socks/laravel-upload-handler", 3 | "description": "This package helps integrate a Laravel application with chunk uploader libraries eg. DropzoneJS and Resumable.js", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "László Görög", 9 | "homepage": "https://github.com/nerg4l" 10 | }, 11 | { 12 | "name": "Choraimy Kroonstuiver", 13 | "homepage": "https://github.com/axlon" 14 | } 15 | ], 16 | "require": { 17 | "php": "^8.0", 18 | "illuminate/support": "^9.0 || ^10.0 || ^11.0 || ^12.0", 19 | "illuminate/http": "^9.0 || ^10.0 || ^11.0 || ^12.0" 20 | }, 21 | "require-dev": { 22 | "orchestra/testbench": "^7.3 || ^8.0 || ^9.0 || ^10.0", 23 | "phpunit/phpunit": "^9.5 || ^10.5 || ^11.5" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "CodingSocks\\UploadHandler\\": "src/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "CodingSocks\\UploadHandler\\Tests\\": "tests/" 33 | } 34 | }, 35 | "config": { 36 | "sort-packages": true 37 | }, 38 | "extra": { 39 | "branch-alias": { 40 | "dev-main": "1.0.x-dev" 41 | }, 42 | "laravel": { 43 | "providers": [ 44 | "CodingSocks\\UploadHandler\\UploadHandlerServiceProvider" 45 | ] 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /config/upload-handler.php: -------------------------------------------------------------------------------- 1 | env('UPLOAD_HANDLER', 'monolith'), 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Client Identifier 25 | |-------------------------------------------------------------------------- 26 | | 27 | | The module can support several identifiers to identify a client. You may 28 | | specify which one you're using throughout your application here. By 29 | | default, the module is setup for session identity. 30 | | 31 | | Supported: "auth", "nop", "session" 32 | | 33 | */ 34 | 35 | 'identifier' => 'session', 36 | 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | Cleanup 40 | |-------------------------------------------------------------------------- 41 | | 42 | | Here you may enable or disable the deletion of chunks after merging 43 | | them. 44 | | 45 | */ 46 | 47 | 'sweep' => true, 48 | 49 | /* 50 | |-------------------------------------------------------------------------- 51 | | Upload Disk 52 | |-------------------------------------------------------------------------- 53 | | 54 | | Here you may configure the target disk for chunk and merged files. 55 | | 56 | */ 57 | 58 | 'disk' => 'local', 59 | 60 | /* 61 | |-------------------------------------------------------------------------- 62 | | Upload Disk 63 | |-------------------------------------------------------------------------- 64 | | 65 | | Here you may configure the target directory for chunk and merged files. 66 | | 67 | */ 68 | 69 | 'directories' => [ 70 | 71 | 'chunk' => 'chunks', 72 | 73 | 'merged' => 'merged', 74 | 75 | ], 76 | 77 | /* 78 | |-------------------------------------------------------------------------- 79 | | Monolith Options 80 | |-------------------------------------------------------------------------- 81 | | 82 | | Here you may configure the options for the monolith driver. 83 | | 84 | */ 85 | 86 | 'monolith' => [ 87 | 88 | 'param' => 'file', 89 | 90 | ], 91 | 92 | /* 93 | |-------------------------------------------------------------------------- 94 | | Blueimp Options 95 | |-------------------------------------------------------------------------- 96 | | 97 | | Here you may configure the options for the blueimp driver. 98 | | 99 | */ 100 | 101 | 'blueimp' => [ 102 | 103 | 'param' => 'file', 104 | 105 | ], 106 | 107 | /* 108 | |-------------------------------------------------------------------------- 109 | | Dropzone Options 110 | |-------------------------------------------------------------------------- 111 | | 112 | | Here you may configure the options for the Dropzone driver. 113 | | 114 | */ 115 | 116 | 'dropzone' => [ 117 | 118 | 'param' => 'file', 119 | 120 | ], 121 | 122 | /* 123 | |-------------------------------------------------------------------------- 124 | | Flow.js Options 125 | |-------------------------------------------------------------------------- 126 | | 127 | | Here you may configure the options for the Flow.js driver. 128 | | 129 | */ 130 | 131 | 'flow-js' => [ 132 | 133 | // The name of the multipart request parameter to use for the file chunk 134 | 'param' => 'file', 135 | 136 | // HTTP method for chunk test request. 137 | 'test-method' => Illuminate\Http\Request::METHOD_GET, 138 | // HTTP method to use when sending chunks to the server (POST, PUT, PATCH). 139 | 'upload-method' => Illuminate\Http\Request::METHOD_POST, 140 | 141 | ], 142 | 143 | /* 144 | |-------------------------------------------------------------------------- 145 | | Resumable.js Options 146 | |-------------------------------------------------------------------------- 147 | | 148 | | Here you may configure the options for the Resumable.js driver. 149 | | 150 | */ 151 | 152 | 'resumable-js' => [ 153 | 154 | // The name of the multipart request parameter to use for the file chunk 155 | 'param' => 'file', 156 | 157 | // HTTP method for chunk test request. 158 | 'test-method' => Illuminate\Http\Request::METHOD_GET, 159 | // HTTP method to use when sending chunks to the server (POST, PUT, PATCH). 160 | 'upload-method' => Illuminate\Http\Request::METHOD_POST, 161 | 162 | // Extra prefix added before the name of each parameter included in the multipart POST or in the test GET. 163 | 'parameter-namespace' => '', 164 | 165 | 'parameter-names' => [ 166 | // The name of the chunk index (base-1) in the current upload POST parameter to use for the file chunk. 167 | 'chunk-number' => 'resumableChunkNumber', 168 | // The name of the total number of chunks POST parameter to use for the file chunk. 169 | 'total-chunks' => 'resumableTotalChunks', 170 | // The name of the general chunk size POST parameter to use for the file chunk. 171 | 'chunk-size' => 'resumableChunkSize', 172 | // The name of the total file size number POST parameter to use for the file chunk. 173 | 'total-size' => 'resumableTotalSize', 174 | // The name of the unique identifier POST parameter to use for the file chunk. 175 | 'identifier' => 'resumableIdentifier', 176 | // The name of the original file name POST parameter to use for the file chunk. 177 | 'file-name' => 'resumableFilename', 178 | // The name of the file's relative path POST parameter to use for the file chunk. 179 | 'relative-path' => 'resumableRelativePath', 180 | // The name of the current chunk size POST parameter to use for the file chunk. 181 | 'current-chunk-size' => 'resumableCurrentChunkSize', 182 | // The name of the file type POST parameter to use for the file chunk. 183 | 'type' => 'resumableType', 184 | ], 185 | 186 | ], 187 | 188 | /* 189 | |-------------------------------------------------------------------------- 190 | | simple-uploader.js Options 191 | |-------------------------------------------------------------------------- 192 | | 193 | | Here you may configure the options for the simple-uploader.js driver. 194 | | 195 | */ 196 | 197 | 'simple-uploader-js' => [ 198 | 199 | // The name of the multipart request parameter to use for the file chunk 200 | 'param' => 'file', 201 | 202 | // HTTP method for chunk test request. 203 | 'test-method' => Illuminate\Http\Request::METHOD_GET, 204 | // HTTP method to use when sending chunks to the server (POST, PUT, PATCH). 205 | 'upload-method' => Illuminate\Http\Request::METHOD_POST, 206 | 207 | ], 208 | 209 | ]; 210 | -------------------------------------------------------------------------------- /examples/blueimp.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Blueimp 8 | 9 | 10 |

Blueimp

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/dropzone.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | DropzoneJS 8 | 9 | 10 | 11 | 12 |

DropzoneJS

13 | 14 |
15 | @csrf 16 | 17 | 18 | 19 |
20 | 21 | 22 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/flow-js.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Flow.js 8 | 9 | 10 |

Flow.js

11 | 12 | 13 | 14 | 15 | 16 | 17 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /examples/ng-file-upload.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ng-file-upload 8 | 9 | 10 |

ng-file-upload

11 | 12 |
13 |
14 | 15 |
16 | 17 | 18 | Upload Successful 19 |
20 |
21 | 22 | 23 | 24 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/plupload.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Plupload 8 | 9 | 10 |

Plupload

11 | 12 |
13 | [Select files] 14 | [Upload files] 15 |
16 | 17 | 20 | 21 | 22 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /examples/resumable-js.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Resumable.js 8 | 9 | 10 |

Resumable.js

11 | 12 | 13 | 14 | 15 | 16 | 17 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /examples/simple-uploader-js.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | simple-uploader.js 8 | 9 | 10 |

simple-uploader.js

11 | 12 | 13 | 14 | 15 | 16 | 17 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | tests 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | src/ 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /phpunit.xml.dist.bak: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | src/ 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | tests 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Driver/BaseHandler.php: -------------------------------------------------------------------------------- 1 | isMethod($method)) { 39 | return true; 40 | } 41 | } 42 | 43 | return false; 44 | } 45 | 46 | /** 47 | * Dispatch a {@link \CodingSocks\UploadHandler\Event\FileUploaded} event. 48 | * Also call the given {@link \Closure} if not null. 49 | * 50 | * @param $disk 51 | * @param $path 52 | * @param \Closure|null $fileUploaded 53 | */ 54 | protected function triggerFileUploadedEvent($disk, $path, Closure $fileUploaded = null): void 55 | { 56 | if ($fileUploaded !== null) { 57 | $fileUploaded($disk, $path); 58 | } 59 | 60 | event(new FileUploaded($disk, $path)); 61 | } 62 | 63 | /** 64 | * Validate an uploaded file. An exception is thrown when it is invalid. 65 | * 66 | * @param \Illuminate\Http\UploadedFile|array|null $file 67 | * 68 | * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException when given file is null. 69 | * @throws \CodingSocks\UploadHandler\Exception\InternalServerErrorHttpException when given file is invalid. 70 | */ 71 | protected function validateUploadedFile($file): void 72 | { 73 | if (null === $file) { 74 | throw new BadRequestHttpException('File not found in request body'); 75 | } 76 | 77 | if (is_array($file)) { 78 | throw new UnprocessableEntityHttpException('File parameter cannot be an array'); 79 | } 80 | 81 | if (! $file->isValid()) { 82 | throw new InternalServerErrorHttpException($file->getErrorMessage()); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Driver/BlueimpHandler.php: -------------------------------------------------------------------------------- 1 | fileParam = $config['param']; 43 | $this->identifier = $identifier; 44 | } 45 | 46 | /** 47 | * {@inheritDoc} 48 | */ 49 | public function handle(Request $request, StorageConfig $config, Closure $fileUploaded = null): Response 50 | { 51 | if ($this->isRequestMethodIn($request, [Request::METHOD_HEAD, Request::METHOD_OPTIONS])) { 52 | return $this->info(); 53 | } 54 | 55 | if ($this->isRequestMethodIn($request, [Request::METHOD_GET])) { 56 | return $this->download($request, $config); 57 | } 58 | 59 | if ($this->isRequestMethodIn($request, [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH])) { 60 | return $this->save($request, $config, $fileUploaded); 61 | } 62 | 63 | throw new MethodNotAllowedHttpException([ 64 | Request::METHOD_HEAD, 65 | Request::METHOD_OPTIONS, 66 | Request::METHOD_GET, 67 | Request::METHOD_POST, 68 | Request::METHOD_PUT, 69 | Request::METHOD_PATCH, 70 | ]); 71 | } 72 | 73 | /** 74 | * @return \Symfony\Component\HttpFoundation\Response 75 | */ 76 | public function info(): Response 77 | { 78 | return new JsonResponse([], Response::HTTP_OK, [ 79 | 'Pragma' => 'no-cache', 80 | 'Cache-Control' => 'no-store, no-cache, must-revalidate', 81 | 'Content-Disposition' => 'inline; filename="files.json"', 82 | 'X-Content-Type-Options' => 'nosniff', 83 | 'Vary' => 'Accept', 84 | ]); 85 | } 86 | 87 | /** 88 | * @param \Illuminate\Http\Request $request 89 | * @param \CodingSocks\UploadHandler\StorageConfig $config 90 | * 91 | * @return \Symfony\Component\HttpFoundation\Response 92 | */ 93 | public function download(Request $request, StorageConfig $config): Response 94 | { 95 | $request->validate([ 96 | $this->fileParam => 'required', 97 | 'totalSize' => 'required', 98 | ]); 99 | 100 | $originalFilename = $request->query($this->fileParam); 101 | $totalSize = $request->query('totalSize'); 102 | $uid = $this->identifier->generateFileIdentifier($totalSize, $originalFilename); 103 | 104 | if (!$this->chunkExists($config, $uid)) { 105 | return new JsonResponse([ 106 | 'file' => null, 107 | ]); 108 | } 109 | 110 | $chunk = Arr::last($this->chunks($config, $uid)); 111 | $size = explode('-', basename($chunk))[1] + 1; 112 | 113 | return new JsonResponse([ 114 | 'file' => [ 115 | 'name' => $originalFilename, 116 | 'size' => $size, 117 | ], 118 | ]); 119 | } 120 | 121 | /** 122 | * @param \Illuminate\Http\Request $request 123 | * @param \CodingSocks\UploadHandler\StorageConfig $config 124 | * @param \Closure|null $fileUploaded 125 | * 126 | * @return \Symfony\Component\HttpFoundation\Response 127 | */ 128 | public function save(Request $request, StorageConfig $config, Closure $fileUploaded = null): Response 129 | { 130 | $file = $request->file($this->fileParam); 131 | 132 | if (null === $file) { 133 | $file = Arr::first($request->file(Str::plural($this->fileParam), [])); 134 | } 135 | 136 | $this->validateUploadedFile($file); 137 | 138 | try { 139 | $range = new ContentRange($request->headers); 140 | } catch (InvalidArgumentException $e) { 141 | throw new BadRequestHttpException($e->getMessage(), $e); 142 | } 143 | 144 | $uid = $this->identifier->generateFileIdentifier($range->getTotal(), $file->getClientOriginalName()); 145 | 146 | $chunks = $this->storeChunk($config, $range, $file, $uid); 147 | 148 | if (!$range->isLast()) { 149 | return new PercentageJsonResponse($range->getPercentage()); 150 | } 151 | 152 | $targetFilename = $file->hashName(); 153 | 154 | $path = $this->mergeChunks($config, $chunks, $targetFilename); 155 | 156 | if ($config->sweep()) { 157 | $this->deleteChunkDirectory($config, $uid); 158 | } 159 | 160 | $this->triggerFileUploadedEvent($config->getDisk(), $path, $fileUploaded); 161 | 162 | return new PercentageJsonResponse(100); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Driver/DropzoneHandler.php: -------------------------------------------------------------------------------- 1 | fileParam = $config['param']; 34 | } 35 | 36 | /** 37 | * {@inheritDoc} 38 | */ 39 | public function handle(Request $request, StorageConfig $config, Closure $fileUploaded = null): Response 40 | { 41 | if ($this->isRequestMethodIn($request, [Request::METHOD_POST])) { 42 | return $this->save($request, $config, $fileUploaded); 43 | } 44 | 45 | throw new MethodNotAllowedHttpException([ 46 | Request::METHOD_POST, 47 | ]); 48 | } 49 | 50 | /** 51 | * @param \Illuminate\Http\Request $request 52 | * @param \CodingSocks\UploadHandler\StorageConfig $config 53 | * @param \Closure|null $fileUploaded 54 | * 55 | * @return \Symfony\Component\HttpFoundation\Response 56 | */ 57 | public function save(Request $request, StorageConfig $config, Closure $fileUploaded = null): Response 58 | { 59 | $file = $request->file($this->fileParam); 60 | 61 | $this->validateUploadedFile($file); 62 | 63 | if ($this->isMonolithRequest($request)) { 64 | return $this->saveMonolith($file, $config, $fileUploaded); 65 | } 66 | 67 | $this->validateChunkRequest($request); 68 | 69 | return $this->saveChunk($file, $request, $config, $fileUploaded); 70 | } 71 | 72 | /** 73 | * @param \Illuminate\Http\Request $request 74 | * 75 | * @return bool 76 | */ 77 | private function isMonolithRequest(Request $request): bool 78 | { 79 | return $request->post('dzuuid') === null 80 | && $request->post('dzchunkindex') === null 81 | && $request->post('dztotalfilesize') === null 82 | && $request->post('dzchunksize') === null 83 | && $request->post('dztotalchunkcount') === null 84 | && $request->post('dzchunkbyteoffset') === null; 85 | } 86 | 87 | /** 88 | * @param \Illuminate\Http\Request $request 89 | */ 90 | private function validateChunkRequest(Request $request): void 91 | { 92 | $request->validate([ 93 | 'dzuuid' => 'required', 94 | 'dzchunkindex' => 'required', 95 | 'dztotalfilesize' => 'required', 96 | 'dzchunksize' => 'required', 97 | 'dztotalchunkcount' => 'required', 98 | 'dzchunkbyteoffset' => 'required', 99 | ]); 100 | } 101 | 102 | /** 103 | * @param \Illuminate\Http\UploadedFile $file 104 | * @param \CodingSocks\UploadHandler\StorageConfig $config 105 | * @param \Closure|null $fileUploaded 106 | * 107 | * @return \Symfony\Component\HttpFoundation\Response 108 | */ 109 | private function saveMonolith(UploadedFile $file, StorageConfig $config, Closure $fileUploaded = null): Response 110 | { 111 | $path = $file->store($config->getMergedDirectory(), [ 112 | 'disk' => $config->getDisk(), 113 | ]); 114 | 115 | $this->triggerFileUploadedEvent($config->getDisk(), $path, $fileUploaded); 116 | 117 | return new PercentageJsonResponse(100); 118 | } 119 | 120 | /** 121 | * @param \Illuminate\Http\UploadedFile $file 122 | * @param \Illuminate\Http\Request $request 123 | * @param \CodingSocks\UploadHandler\StorageConfig $config 124 | * @param \Closure|null $fileUploaded 125 | * 126 | * @return \Symfony\Component\HttpFoundation\Response 127 | */ 128 | private function saveChunk(UploadedFile $file, Request $request, StorageConfig $config, Closure $fileUploaded = null): Response 129 | { 130 | try { 131 | $range = new DropzoneRange( 132 | $request, 133 | 'dzchunkindex', 134 | 'dztotalchunkcount', 135 | 'dzchunksize', 136 | 'dztotalfilesize' 137 | ); 138 | } catch (InvalidArgumentException $e) { 139 | throw new BadRequestHttpException($e->getMessage(), $e); 140 | } 141 | 142 | $uid = $request->post('dzuuid'); 143 | 144 | $chunks = $this->storeChunk($config, $range, $file, $uid); 145 | 146 | if (!$range->isFinished($chunks)) { 147 | return new PercentageJsonResponse($range->getPercentage($chunks)); 148 | } 149 | 150 | $targetFilename = $file->hashName(); 151 | 152 | $path = $this->mergeChunks($config, $chunks, $targetFilename); 153 | 154 | if ($config->sweep()) { 155 | $this->deleteChunkDirectory($config, $uid); 156 | } 157 | 158 | $this->triggerFileUploadedEvent($config->getDisk(), $path, $fileUploaded); 159 | 160 | return new PercentageJsonResponse(100); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/Driver/FlowJsHandler.php: -------------------------------------------------------------------------------- 1 | 'flowChunkNumber', 15 | // The name of the total number of chunks POST parameter to use for the file chunk. 16 | 'total-chunks' => 'flowTotalChunks', 17 | // The name of the general chunk size POST parameter to use for the file chunk. 18 | 'chunk-size' => 'flowChunkSize', 19 | // The name of the total file size number POST parameter to use for the file chunk. 20 | 'total-size' => 'flowTotalSize', 21 | // The name of the unique identifier POST parameter to use for the file chunk. 22 | 'identifier' => 'flowIdentifier', 23 | // The name of the original file name POST parameter to use for the file chunk. 24 | 'file-name' => 'flowFilename', 25 | // The name of the file's relative path POST parameter to use for the file chunk. 26 | 'relative-path' => 'flowRelativePath', 27 | // The name of the current chunk size POST parameter to use for the file chunk. 28 | 'current-chunk-size' => 'flowCurrentChunkSize', 29 | ]; 30 | parent::__construct($config, $identifier); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Driver/MonolithHandler.php: -------------------------------------------------------------------------------- 1 | fileParam = $config['param']; 27 | } 28 | 29 | /** 30 | * {@inheritDoc} 31 | */ 32 | public function handle(Request $request, StorageConfig $config, Closure $fileUploaded = null): Response 33 | { 34 | if ($request->isMethod(Request::METHOD_POST)) { 35 | return $this->save($request, $config, $fileUploaded); 36 | } 37 | 38 | throw new MethodNotAllowedHttpException([ 39 | Request::METHOD_POST, 40 | ]); 41 | } 42 | 43 | /** 44 | * @param \Illuminate\Http\Request $request 45 | * @param \CodingSocks\UploadHandler\StorageConfig $config 46 | * @param \Closure|null $fileUploaded 47 | * 48 | * @return \Symfony\Component\HttpFoundation\Response 49 | */ 50 | public function save(Request $request, StorageConfig $config, Closure $fileUploaded = null): Response 51 | { 52 | $file = $request->file($this->fileParam); 53 | 54 | $this->validateUploadedFile($file); 55 | 56 | $path = $file->store($config->getMergedDirectory(), [ 57 | 'disk' => $config->getDisk(), 58 | ]); 59 | 60 | $this->triggerFileUploadedEvent($config->getDisk(), $path, $fileUploaded); 61 | 62 | return new PercentageJsonResponse(100); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Driver/NgFileHandler.php: -------------------------------------------------------------------------------- 1 | identifier = $identifier; 37 | } 38 | 39 | /** 40 | * @inheritDoc 41 | */ 42 | public function handle(Request $request, StorageConfig $config, Closure $fileUploaded = null): Response 43 | { 44 | if ($this->isRequestMethodIn($request, [Request::METHOD_GET])) { 45 | return $this->resume($request, $config); 46 | } 47 | 48 | if ($this->isRequestMethodIn($request, [Request::METHOD_POST])) { 49 | return $this->save($request, $config, $fileUploaded); 50 | } 51 | 52 | throw new MethodNotAllowedHttpException([ 53 | Request::METHOD_GET, 54 | Request::METHOD_POST, 55 | ]); 56 | } 57 | 58 | private function resume(Request $request, StorageConfig $config): Response 59 | { 60 | $request->validate([ 61 | 'file' => 'required', 62 | 'totalSize' => 'required', 63 | ]); 64 | 65 | $originalFilename = $request->get('file'); 66 | $totalSize = $request->get('totalSize'); 67 | $uid = $this->identifier->generateFileIdentifier($totalSize, $originalFilename); 68 | 69 | if (!$this->chunkExists($config, $uid)) { 70 | return new JsonResponse([ 71 | 'file' => $originalFilename, 72 | 'size' => 0, 73 | ]); 74 | } 75 | 76 | $chunk = Arr::last($this->chunks($config, $uid)); 77 | $size = explode('-', basename($chunk))[1] + 1; 78 | 79 | return new JsonResponse([ 80 | 'file' => $originalFilename, 81 | 'size' => $size, 82 | ]); 83 | } 84 | 85 | private function save(Request $request, StorageConfig $config, Closure $fileUploaded = null): Response 86 | { 87 | $file = $request->file('file'); 88 | 89 | $this->validateUploadedFile($file); 90 | 91 | if ($this->isMonolithRequest($request)) { 92 | return $this->saveMonolith($file, $config, $fileUploaded); 93 | } 94 | 95 | $this->validateChunkRequest($request); 96 | 97 | return $this->saveChunk($file, $request, $config, $fileUploaded); 98 | } 99 | 100 | private function isMonolithRequest(Request $request) 101 | { 102 | return empty($request->post()); 103 | } 104 | 105 | /** 106 | * @param \Illuminate\Http\Request $request 107 | */ 108 | private function validateChunkRequest(Request $request): void 109 | { 110 | $request->validate([ 111 | '_chunkNumber' => 'required|numeric', 112 | '_chunkSize' => 'required|numeric', 113 | '_totalSize' => 'required|numeric', 114 | '_currentChunkSize' => 'required|numeric', 115 | ]); 116 | } 117 | 118 | /** 119 | * @param \Illuminate\Http\UploadedFile $file 120 | * @param \CodingSocks\UploadHandler\StorageConfig $config 121 | * @param \Closure|null $fileUploaded 122 | * 123 | * @return \Symfony\Component\HttpFoundation\Response 124 | */ 125 | private function saveMonolith(UploadedFile $file, StorageConfig $config, Closure $fileUploaded = null): Response 126 | { 127 | $path = $file->store($config->getMergedDirectory(), [ 128 | 'disk' => $config->getDisk(), 129 | ]); 130 | 131 | $this->triggerFileUploadedEvent($config->getDisk(), $path, $fileUploaded); 132 | 133 | return new PercentageJsonResponse(100); 134 | } 135 | 136 | /** 137 | * @param \Illuminate\Http\UploadedFile $file 138 | * @param \Illuminate\Http\Request $request 139 | * @param \CodingSocks\UploadHandler\StorageConfig $config 140 | * @param \Closure|null $fileUploaded 141 | * 142 | * @return \Symfony\Component\HttpFoundation\Response 143 | */ 144 | private function saveChunk(UploadedFile $file, Request $request, StorageConfig $config, Closure $fileUploaded = null): Response 145 | { 146 | try { 147 | $range = new NgFileUploadRange($request); 148 | } catch (InvalidArgumentException $e) { 149 | throw new BadRequestHttpException($e->getMessage(), $e); 150 | } 151 | 152 | $originalFilename = $file->getClientOriginalName(); 153 | $totalSize = $request->get('_totalSize'); 154 | $uid = $this->identifier->generateFileIdentifier($totalSize, $originalFilename); 155 | 156 | $chunks = $this->storeChunk($config, $range, $file, $uid); 157 | 158 | if (!$range->isLast()) { 159 | return new PercentageJsonResponse($range->getPercentage()); 160 | } 161 | 162 | $targetFilename = $file->hashName(); 163 | 164 | $path = $this->mergeChunks($config, $chunks, $targetFilename); 165 | 166 | if ($config->sweep()) { 167 | $this->deleteChunkDirectory($config, $uid); 168 | } 169 | 170 | $this->triggerFileUploadedEvent($config->getDisk(), $path, $fileUploaded); 171 | 172 | return new PercentageJsonResponse(100); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/Driver/PluploadHandler.php: -------------------------------------------------------------------------------- 1 | identifier = $identifier; 35 | } 36 | 37 | 38 | /** 39 | * @inheritDoc 40 | */ 41 | public function handle(Request $request, StorageConfig $config, Closure $fileUploaded = null): Response 42 | { 43 | if ($this->isRequestMethodIn($request, [Request::METHOD_POST])) { 44 | return $this->save($request, $config, $fileUploaded); 45 | } 46 | 47 | throw new MethodNotAllowedHttpException([ 48 | Request::METHOD_POST, 49 | ]); 50 | } 51 | 52 | /** 53 | * @param \Illuminate\Http\Request $request 54 | * @param \CodingSocks\UploadHandler\StorageConfig $config 55 | * @param \Closure|null $fileUploaded 56 | * 57 | * @return mixed 58 | */ 59 | private function save(Request $request, StorageConfig $config, ?Closure $fileUploaded) 60 | { 61 | $file = $request->file('file'); 62 | 63 | $this->validateUploadedFile($file); 64 | 65 | $this->validateChunkRequest($request); 66 | 67 | return $this->saveChunk($file, $request, $config, $fileUploaded); 68 | } 69 | 70 | /** 71 | * @param \Illuminate\Http\Request $request 72 | */ 73 | private function validateChunkRequest(Request $request): void 74 | { 75 | $request->validate([ 76 | 'name' => 'required', 77 | 'chunk' => 'required|integer', 78 | 'chunks' => 'required|integer', 79 | ]); 80 | } 81 | 82 | /** 83 | * @param \Illuminate\Http\UploadedFile $file 84 | * @param \Illuminate\Http\Request $request 85 | * @param \CodingSocks\UploadHandler\StorageConfig $config 86 | * @param \Closure|null $fileUploaded 87 | * 88 | * @return \Symfony\Component\HttpFoundation\Response 89 | */ 90 | private function saveChunk(UploadedFile $file, Request $request, StorageConfig $config, Closure $fileUploaded = null): Response 91 | { 92 | try { 93 | $range = new PluploadRange($request); 94 | } catch (InvalidArgumentException $e) { 95 | throw new BadRequestHttpException($e->getMessage(), $e); 96 | } 97 | 98 | $uid = $this->identifier->generateFileIdentifier($range->getTotal(), $file->getClientOriginalName()); 99 | 100 | $chunks = $this->storeChunk($config, $range, $file, $uid); 101 | 102 | if (!$range->isLast()) { 103 | return new PercentageJsonResponse($range->getPercentage()); 104 | } 105 | 106 | $targetFilename = $file->hashName(); 107 | 108 | $path = $this->mergeChunks($config, $chunks, $targetFilename); 109 | 110 | if ($config->sweep()) { 111 | $this->deleteChunkDirectory($config, $uid); 112 | } 113 | 114 | $this->triggerFileUploadedEvent($config->getDisk(), $path, $fileUploaded); 115 | 116 | return new PercentageJsonResponse($range->getPercentage()); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Driver/ResumableJsHandler.php: -------------------------------------------------------------------------------- 1 | fileParam = $config['param']; 62 | $this->identifier = $identifier; 63 | 64 | $this->uploadMethod = $config['upload-method']; 65 | $this->testMethod = $config['test-method']; 66 | 67 | $this->parameterNamespace = $config['parameter-namespace']; 68 | $this->parameterNames = $config['parameter-names']; 69 | } 70 | 71 | /** 72 | * @inheritDoc 73 | */ 74 | public function handle(Request $request, StorageConfig $config, Closure $fileUploaded = null): Response 75 | { 76 | if ($this->isRequestMethodIn($request, [$this->testMethod])) { 77 | return $this->resume($request, $config); 78 | } 79 | 80 | if ($this->isRequestMethodIn($request, [$this->uploadMethod])) { 81 | return $this->save($request, $config, $fileUploaded); 82 | } 83 | 84 | throw new MethodNotAllowedHttpException([ 85 | $this->uploadMethod, 86 | $this->testMethod, 87 | ]); 88 | } 89 | 90 | /** 91 | * @param \Illuminate\Http\Request $request 92 | * @param \CodingSocks\UploadHandler\StorageConfig $config 93 | * 94 | * @return \Symfony\Component\HttpFoundation\Response 95 | */ 96 | public function resume(Request $request, StorageConfig $config): Response 97 | { 98 | $this->validateChunkRequest($request); 99 | 100 | try { 101 | $range = new ResumableJsRange( 102 | $request->query, 103 | $this->buildParameterName('chunk-number'), 104 | $this->buildParameterName('total-chunks'), 105 | $this->buildParameterName('chunk-size'), 106 | $this->buildParameterName('total-size') 107 | ); 108 | } catch (InvalidArgumentException $e) { 109 | throw new BadRequestHttpException($e->getMessage(), $e); 110 | } 111 | 112 | $uid = $this->identifier->generateIdentifier($request->query($this->buildParameterName('identifier'))); 113 | $chunkname = $this->buildChunkname($range); 114 | 115 | if (! $this->chunkExists($config, $uid, $chunkname)) { 116 | return new Response('', Response::HTTP_NO_CONTENT); 117 | } 118 | 119 | return new JsonResponse(['OK']); 120 | } 121 | 122 | /** 123 | * @param \Illuminate\Http\Request $request 124 | * @param \CodingSocks\UploadHandler\StorageConfig $config 125 | * @param \Closure|null $fileUploaded 126 | * 127 | * @return \Symfony\Component\HttpFoundation\Response 128 | */ 129 | public function save(Request $request, StorageConfig $config, Closure $fileUploaded = null): Response 130 | { 131 | $file = $request->file($this->fileParam); 132 | 133 | $this->validateUploadedFile($file); 134 | 135 | $this->validateChunkRequest($request); 136 | 137 | return $this->saveChunk($file, $request, $config, $fileUploaded); 138 | } 139 | 140 | /** 141 | * @param \Illuminate\Http\Request $request 142 | */ 143 | private function validateChunkRequest(Request $request): void 144 | { 145 | $validation = []; 146 | 147 | foreach ($this->parameterNames as $key => $_) { 148 | $validation[$this->buildParameterName($key)] = 'required'; 149 | } 150 | 151 | $request->validate($validation); 152 | } 153 | 154 | /** 155 | * @param \Illuminate\Http\UploadedFile $file 156 | * @param \Illuminate\Http\Request $request 157 | * @param \CodingSocks\UploadHandler\StorageConfig $config 158 | * @param \Closure|null $fileUploaded 159 | * 160 | * @return \Symfony\Component\HttpFoundation\Response 161 | */ 162 | private function saveChunk(UploadedFile $file, Request $request, StorageConfig $config, Closure $fileUploaded = null): Response 163 | { 164 | try { 165 | $range = new ResumableJsRange( 166 | $request, 167 | $this->buildParameterName('chunk-number'), 168 | $this->buildParameterName('total-chunks'), 169 | $this->buildParameterName('chunk-size'), 170 | $this->buildParameterName('total-size') 171 | ); 172 | } catch (InvalidArgumentException $e) { 173 | throw new BadRequestHttpException($e->getMessage(), $e); 174 | } 175 | 176 | $weakId = $request->post($this->buildParameterName('identifier')); 177 | $uid = $this->identifier->generateIdentifier($weakId); 178 | 179 | $chunks = $this->storeChunk($config, $range, $file, $uid); 180 | 181 | if (!$range->isFinished($chunks)) { 182 | return new PercentageJsonResponse($range->getPercentage($chunks)); 183 | } 184 | 185 | $targetFilename = $file->hashName(); 186 | 187 | $path = $this->mergeChunks($config, $chunks, $targetFilename); 188 | 189 | if ($config->sweep()) { 190 | $this->deleteChunkDirectory($config, $uid); 191 | } 192 | 193 | $this->triggerFileUploadedEvent($config->getDisk(), $path, $fileUploaded); 194 | 195 | return new PercentageJsonResponse(100); 196 | } 197 | 198 | /** 199 | * @param $key string 200 | * 201 | * @return string 202 | */ 203 | private function buildParameterName(string $key): string 204 | { 205 | if (! array_key_exists($key, $this->parameterNames)) { 206 | throw new InvalidArgumentException(sprintf('`%s` is an invalid key for parameter name', $key)); 207 | } 208 | 209 | return $this->parameterNamespace . $this->parameterNames[$key]; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/Driver/SimpleUploaderJsHandler.php: -------------------------------------------------------------------------------- 1 | 'chunkNumber', 15 | // The name of the total number of chunks POST parameter to use for the file chunk. 16 | 'total-chunks' => 'totalChunks', 17 | // The name of the general chunk size POST parameter to use for the file chunk. 18 | 'chunk-size' => 'chunkSize', 19 | // The name of the total file size number POST parameter to use for the file chunk. 20 | 'total-size' => 'totalSize', 21 | // The name of the unique identifier POST parameter to use for the file chunk. 22 | 'identifier' => 'identifier', 23 | // The name of the original file name POST parameter to use for the file chunk. 24 | 'file-name' => 'filename', 25 | // The name of the file's relative path POST parameter to use for the file chunk. 26 | 'relative-path' => 'relativePath', 27 | // The name of the current chunk size POST parameter to use for the file chunk. 28 | 'current-chunk-size' => 'currentChunkSize', 29 | ]; 30 | parent::__construct($config, $identifier); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Event/FileUploaded.php: -------------------------------------------------------------------------------- 1 | disk = $disk; 26 | $this->file = $file; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Exception/ChecksumMismatchHttpException.php: -------------------------------------------------------------------------------- 1 | getDisk()); 31 | 32 | $mergedDirectory = $config->getMergedDirectory(); 33 | $disk->makeDirectory($mergedDirectory); 34 | $targetPath = $mergedDirectory . '/' . $targetFilename; 35 | 36 | $chunk = array_shift($chunks); 37 | $disk->copy($chunk, $targetPath); 38 | $mergedFile = new File($disk->path($targetPath)); 39 | $mergedFileInfo = $mergedFile->openFile('ab+'); 40 | 41 | foreach ($chunks as $chunk) { 42 | $chunkFileInfo = (new File($disk->path($chunk)))->openFile('rb'); 43 | 44 | while (! $chunkFileInfo->eof()) { 45 | $mergedFileInfo->fwrite($chunkFileInfo->fread($this->bufferSize)); 46 | } 47 | } 48 | 49 | return $this->correctMergedExt($disk, $mergedDirectory, $targetFilename); 50 | } 51 | 52 | /** 53 | * Delete a directory with the given name from the chunk directory. 54 | * 55 | * @param \CodingSocks\UploadHandler\StorageConfig $config 56 | * @param string $uid 57 | */ 58 | public function deleteChunkDirectory(StorageConfig $config, string $uid): void 59 | { 60 | $directory = $config->getChunkDirectory() . '/' . $uid; 61 | Storage::disk($config->getDisk())->deleteDirectory($directory); 62 | } 63 | 64 | /** 65 | * Persist an uploaded chunk in a directory with the given name in the chunk directory. 66 | * 67 | * @param \CodingSocks\UploadHandler\StorageConfig $config 68 | * @param \CodingSocks\UploadHandler\Range\Range $range 69 | * @param \Illuminate\Http\UploadedFile $file 70 | * @param string $uid 71 | * 72 | * @return array 73 | */ 74 | public function storeChunk(StorageConfig $config, Range $range, UploadedFile $file, string $uid): array 75 | { 76 | $chunkname = $this->buildChunkname($range); 77 | 78 | $directory = $config->getChunkDirectory() . '/' . $uid; 79 | $file->storeAs($directory, $chunkname, [ 80 | 'disk' => $config->getDisk(), 81 | ]); 82 | 83 | return Storage::disk($config->getDisk())->files($directory); 84 | } 85 | 86 | /** 87 | * List all chunks from a directory with the given name. 88 | * 89 | * @param \CodingSocks\UploadHandler\StorageConfig $config 90 | * @param string $uid 91 | * 92 | * @return array 93 | */ 94 | public function chunks(StorageConfig $config, string $uid): array 95 | { 96 | $directory = $config->getChunkDirectory() . '/' . $uid; 97 | return Storage::disk($config->getDisk())->files($directory); 98 | } 99 | 100 | /** 101 | * Create a chunkname which contains range details. 102 | * 103 | * @param \CodingSocks\UploadHandler\Range\Range $range 104 | * 105 | * @return string 106 | */ 107 | public function buildChunkname(Range $range): string 108 | { 109 | $len = strlen($range->getTotal()); 110 | return implode('-', [ 111 | str_pad($range->getStart(), $len, '0', STR_PAD_LEFT), 112 | str_pad($range->getEnd(), $len, '0', STR_PAD_LEFT), 113 | ]); 114 | } 115 | 116 | /** 117 | * Check if a chunk exists. 118 | * 119 | * When chunkname is given it checks the exact chunk. Otherwise only the folder has to exists. 120 | * 121 | * @param \CodingSocks\UploadHandler\StorageConfig $config 122 | * @param string $uid 123 | * @param string|null $chunkname 124 | * 125 | * @return bool 126 | */ 127 | public function chunkExists(StorageConfig $config, string $uid, string $chunkname = null): bool 128 | { 129 | $directory = $config->getChunkDirectory() . '/' . $uid; 130 | $disk = Storage::disk($config->getDisk()); 131 | 132 | if (!$disk->exists($directory)) { 133 | return false; 134 | } 135 | 136 | return $chunkname === null || $disk->exists($directory . '/' . $chunkname); 137 | } 138 | 139 | /** 140 | * @param \Illuminate\Contracts\Filesystem\Filesystem $disk 141 | * @param string $mergedDirectory 142 | * @param string $targetFilename 143 | * 144 | * @return string 145 | */ 146 | private function correctMergedExt(Filesystem $disk, string $mergedDirectory, string $targetFilename): string 147 | { 148 | $targetPath = $mergedDirectory . '/' . $targetFilename; 149 | $ext = pathinfo($targetFilename, PATHINFO_EXTENSION); 150 | if ($ext === 'bin') { 151 | $var = $disk->path($targetPath); 152 | $uploadedFile = new UploadedFile($var, $targetFilename); 153 | $filename = pathinfo($targetFilename, PATHINFO_FILENAME); 154 | $fixedTargetPath = $mergedDirectory . '/' . $filename . '.' . $uploadedFile->guessExtension(); 155 | $disk->move($targetPath, $fixedTargetPath); 156 | $targetPath = $fixedTargetPath; 157 | } 158 | return $targetPath; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Identifier/AuthIdentifier.php: -------------------------------------------------------------------------------- 1 | generateIdentifier($data); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Identifier/NopIdentifier.php: -------------------------------------------------------------------------------- 1 | container['config']['upload-handler.identifier']; 35 | } 36 | 37 | /** 38 | * Set the default mail driver name. 39 | * 40 | * @param string $name 41 | * 42 | * @return void 43 | */ 44 | public function setDefaultDriver($name) 45 | { 46 | $this->container['config']['upload-handler.identifier'] = $name; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Range/ContentRange.php: -------------------------------------------------------------------------------- 1 | header('content-range'); 36 | } elseif ($contentRange instanceof HeaderBag) { 37 | $contentRange = $contentRange->get('content-range'); 38 | } 39 | 40 | if (preg_match('#bytes (\d+)-(\d+)/(\d+)#', $contentRange, $matches) !== 1) { 41 | throw new InvalidArgumentException('Content Range header is missing or invalid'); 42 | } 43 | 44 | $this->start = $this->numericValue($matches[1]); 45 | $this->end = $this->numericValue($matches[2]); 46 | $this->total = $this->numericValue($matches[3]); 47 | 48 | if ($this->end < $this->start) { 49 | throw new InvalidArgumentException('Range end must be greater than or equal to range start'); 50 | } 51 | if ($this->total <= $this->end) { 52 | throw new InvalidArgumentException('Size must be greater than range end'); 53 | } 54 | } 55 | 56 | /** 57 | * Converts the string value to float - throws exception if float value is exceeded. 58 | * 59 | * @param string $value 60 | * 61 | * @return float 62 | * @throws \Symfony\Component\HttpKernel\Exception\HttpException 63 | */ 64 | protected function numericValue($value): float 65 | { 66 | $floatVal = floatval($value); 67 | 68 | if ($floatVal === INF) { 69 | throw new RequestEntityTooLargeHttpException('The content range value is too large'); 70 | } 71 | 72 | return $floatVal; 73 | } 74 | 75 | /** 76 | * {@inheritDoc} 77 | */ 78 | public function getStart(): float 79 | { 80 | return $this->start; 81 | } 82 | 83 | /** 84 | * {@inheritDoc} 85 | */ 86 | public function getEnd(): float 87 | { 88 | return $this->end; 89 | } 90 | 91 | /** 92 | * {@inheritDoc} 93 | */ 94 | public function getTotal(): float 95 | { 96 | return $this->total; 97 | } 98 | 99 | /** 100 | * {@inheritDoc} 101 | */ 102 | public function isFirst(): bool 103 | { 104 | return $this->start === 0.0; 105 | } 106 | 107 | /** 108 | * {@inheritDoc} 109 | */ 110 | public function isLast(): bool 111 | { 112 | return $this->end >= ($this->total - 1); 113 | } 114 | 115 | /** 116 | * @return float 117 | */ 118 | public function getPercentage(): float 119 | { 120 | return floor(($this->end + 1) / $this->total * 100); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Range/DropzoneRange.php: -------------------------------------------------------------------------------- 1 | index < 0) { 16 | throw new InvalidArgumentException(sprintf('`%s` must be greater than or equal to zero', $indexKey)); 17 | } 18 | if ($this->index >= $this->numberOfChunks) { 19 | throw new InvalidArgumentException(sprintf('`%s` must be smaller than `%s`', $indexKey, $numberOfChunksKey)); 20 | } 21 | } 22 | 23 | /** 24 | * @param string $indexKey 25 | * @param string $numberOfChunksKey 26 | * @param string $chunkSizeKey 27 | * @param string $totalSizeKey 28 | */ 29 | protected function validateTotalSize(string $indexKey, string $numberOfChunksKey, string $chunkSizeKey, string $totalSizeKey): void 30 | { 31 | if ($this->totalSize < 1) { 32 | throw new InvalidArgumentException(sprintf('`%s` must be greater than zero', $totalSizeKey)); 33 | } elseif ($this->totalSize <= $this->index * $this->chunkSize) { 34 | throw new InvalidArgumentException( 35 | sprintf('`%s` must be greater than the multiple of `%s` and `%s`', $totalSizeKey, $chunkSizeKey, $indexKey) 36 | ); 37 | } elseif ($this->totalSize > $this->numberOfChunks * $this->chunkSize) { 38 | throw new InvalidArgumentException( 39 | sprintf('`%s` must be smaller than or equal to the multiple of `%s` and `%s`', $totalSizeKey, $chunkSizeKey, $numberOfChunksKey) 40 | ); 41 | } 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | public function getStart(): float 48 | { 49 | return $this->index * $this->chunkSize; 50 | } 51 | 52 | /** 53 | * {@inheritDoc} 54 | */ 55 | public function getEnd(): float 56 | { 57 | $end = (($this->index + 1) * $this->chunkSize) - 1; 58 | 59 | $sizeIndex = $this->totalSize - 1; 60 | if ($end > ($sizeIndex)) { 61 | return $sizeIndex; 62 | } 63 | 64 | return $end; 65 | } 66 | 67 | /** 68 | * {@inheritDoc} 69 | */ 70 | public function isFirst(): bool 71 | { 72 | return $this->index === 0; 73 | } 74 | 75 | /** 76 | * {@inheritDoc} 77 | */ 78 | public function isLast(): bool 79 | { 80 | return $this->index === ($this->numberOfChunks - 1); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Range/NgFileUploadRange.php: -------------------------------------------------------------------------------- 1 | chunkNumber = (int) $request->get(self::$CHUNK_NUMBER_PARAMETER_NAME); 30 | $this->chunkSize = (int) $request->get(self::$CHUNK_SIZE_PARAMETER_NAME); 31 | $this->currentChunkSize = (int) $request->get(self::$CURRENT_CHUNK_SIZE_PARAMETER_NAME); 32 | $this->totalSize = (double) $request->get(self::$TOTAL_SIZE_PARAMETER_NAME); 33 | 34 | if ($this->chunkNumber < 0) { 35 | throw new InvalidArgumentException(sprintf('`%s` must be greater than or equal to zero', self::$CHUNK_NUMBER_PARAMETER_NAME)); 36 | } 37 | if ($this->chunkSize < 1) { 38 | throw new InvalidArgumentException(sprintf('`%s` must be greater than zero', self::$CHUNK_SIZE_PARAMETER_NAME)); 39 | } 40 | if ($this->currentChunkSize < 1) { 41 | throw new InvalidArgumentException(sprintf('`%s` must be greater than zero', self::$CURRENT_CHUNK_SIZE_PARAMETER_NAME)); 42 | } 43 | if ($this->totalSize < 1) { 44 | throw new InvalidArgumentException(sprintf('`%s` must be greater than zero', self::$TOTAL_SIZE_PARAMETER_NAME)); 45 | } 46 | } 47 | 48 | /** 49 | * {@inheritDoc} 50 | */ 51 | public function getStart(): float 52 | { 53 | return $this->chunkNumber * $this->chunkSize; 54 | } 55 | 56 | /** 57 | * {@inheritDoc} 58 | */ 59 | public function getEnd(): float 60 | { 61 | return $this->getStart() + $this->currentChunkSize - 1; 62 | } 63 | 64 | /** 65 | * {@inheritDoc} 66 | */ 67 | public function getTotal(): float 68 | { 69 | return $this->totalSize; 70 | } 71 | 72 | /** 73 | * {@inheritDoc} 74 | */ 75 | public function isFirst(): bool 76 | { 77 | return $this->chunkNumber === 0; 78 | } 79 | 80 | /** 81 | * {@inheritDoc} 82 | */ 83 | public function isLast(): bool 84 | { 85 | return $this->getEnd() === ($this->getTotal() - 1); 86 | } 87 | 88 | /** 89 | * @return float 90 | */ 91 | public function getPercentage(): float 92 | { 93 | return floor(($this->getEnd() + 1) / $this->getTotal() * 100); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Range/PluploadRange.php: -------------------------------------------------------------------------------- 1 | request; 32 | } 33 | 34 | $this->current = (float) $request->get(self::$CHUNK_NUMBER_PARAMETER_NAME); 35 | $this->total = (float) $request->get(self::$TOTAL_CHUNK_NUMBER_PARAMETER_NAME); 36 | 37 | if ($this->current < 0) { 38 | throw new InvalidArgumentException( 39 | sprintf('`%s` must be greater than or equal to zero', self::$CHUNK_NUMBER_PARAMETER_NAME) 40 | ); 41 | } 42 | if ($this->total < 1) { 43 | throw new InvalidArgumentException( 44 | sprintf('`%s` must be greater than zero', self::$TOTAL_CHUNK_NUMBER_PARAMETER_NAME) 45 | ); 46 | } 47 | if ($this->current >= $this->total) { 48 | throw new InvalidArgumentException( 49 | sprintf('`%s` must be less than `%s`', self::$CHUNK_NUMBER_PARAMETER_NAME, self::$TOTAL_CHUNK_NUMBER_PARAMETER_NAME) 50 | ); 51 | } 52 | } 53 | 54 | /** 55 | * {@inheritDoc} 56 | */ 57 | public function getStart(): float 58 | { 59 | return $this->current; 60 | } 61 | 62 | /** 63 | * {@inheritDoc} 64 | */ 65 | public function getEnd(): float 66 | { 67 | return $this->current + 1; 68 | } 69 | 70 | /** 71 | * {@inheritDoc} 72 | */ 73 | public function getTotal(): float 74 | { 75 | return $this->total; 76 | } 77 | 78 | /** 79 | * {@inheritDoc} 80 | */ 81 | public function isFirst(): bool 82 | { 83 | return $this->current === 0.0; 84 | } 85 | 86 | /** 87 | * {@inheritDoc} 88 | */ 89 | public function isLast(): bool 90 | { 91 | return $this->current >= ($this->total - 1); 92 | } 93 | 94 | /** 95 | * @return float 96 | */ 97 | public function getPercentage(): float 98 | { 99 | return floor(($this->current + 1) / $this->total * 100); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Range/Range.php: -------------------------------------------------------------------------------- 1 | request; 31 | } 32 | 33 | $this->index = (int) $request->get($indexKey); 34 | $this->numberOfChunks = (int) $request->get($numberOfChunksKey); 35 | $this->chunkSize = (int) $request->get($chunkSizeKey); 36 | // Must be double (which is an alias for float) for 32 bit systems 37 | $this->totalSize = (double) $request->get($totalSizeKey); 38 | 39 | $this->validateNumberOfChunks($numberOfChunksKey); 40 | $this->validateIndexKey($indexKey, $numberOfChunksKey); 41 | $this->validateChunkSize($chunkSizeKey); 42 | $this->validateTotalSize($indexKey, $numberOfChunksKey, $chunkSizeKey, $totalSizeKey); 43 | } 44 | 45 | /** 46 | * @param string $numberOfChunksKey 47 | */ 48 | protected function validateNumberOfChunks(string $numberOfChunksKey): void 49 | { 50 | if ($this->numberOfChunks <= 0) { 51 | throw new InvalidArgumentException(sprintf('`%s` must be greater than zero', $numberOfChunksKey)); 52 | } 53 | } 54 | 55 | /** 56 | * @param string $indexKey 57 | * @param string $numberOfChunksKey 58 | */ 59 | abstract protected function validateIndexKey(string $indexKey, string $numberOfChunksKey): void; 60 | 61 | /** 62 | * @param string $chunkSizeKey 63 | */ 64 | protected function validateChunkSize(string $chunkSizeKey): void 65 | { 66 | if ($this->chunkSize < 1) { 67 | throw new InvalidArgumentException(sprintf('`%s` must be greater than zero', $chunkSizeKey)); 68 | } 69 | } 70 | 71 | /** 72 | * @param string $indexKey 73 | * @param string $numberOfChunksKey 74 | * @param string $chunkSizeKey 75 | * @param string $totalSizeKey 76 | */ 77 | abstract protected function validateTotalSize(string $indexKey, string $numberOfChunksKey, string $chunkSizeKey, string $totalSizeKey): void; 78 | 79 | /** 80 | * {@inheritDoc} 81 | */ 82 | abstract public function getStart(): float; 83 | 84 | /** 85 | * {@inheritDoc} 86 | */ 87 | abstract public function getEnd(): float; 88 | 89 | /** 90 | * {@inheritDoc} 91 | */ 92 | public function getTotal(): float 93 | { 94 | return $this->totalSize; 95 | } 96 | 97 | /** 98 | * {@inheritDoc} 99 | */ 100 | abstract public function isFirst(): bool; 101 | 102 | /** 103 | * {@inheritDoc} 104 | */ 105 | abstract public function isLast(): bool; 106 | 107 | /** 108 | * @param $uploadedChunks 109 | * 110 | * @return float 111 | */ 112 | public function getPercentage($uploadedChunks): float 113 | { 114 | return floor(count($uploadedChunks) / $this->numberOfChunks * 100); 115 | } 116 | 117 | /** 118 | * @param $uploadedChunks 119 | * 120 | * @return bool 121 | */ 122 | public function isFinished($uploadedChunks): bool 123 | { 124 | return $this->numberOfChunks === count($uploadedChunks); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Range/ResumableJsRange.php: -------------------------------------------------------------------------------- 1 | numberOfChunks <= 0) { 15 | throw new InvalidArgumentException(sprintf('`%s` must be greater than zero', $numberOfChunksKey)); 16 | } 17 | } 18 | 19 | /** 20 | * @param string $indexKey 21 | * @param string $numberOfChunksKey 22 | */ 23 | protected function validateIndexKey(string $indexKey, string $numberOfChunksKey): void 24 | { 25 | if ($this->index < 1) { 26 | throw new InvalidArgumentException(sprintf('`%s` must be greater than zero', $indexKey)); 27 | } 28 | if ($this->index > $this->numberOfChunks) { 29 | throw new InvalidArgumentException(sprintf('`%s` must be smaller than or equal to `%s`', $indexKey, $numberOfChunksKey)); 30 | } 31 | } 32 | 33 | /** 34 | * @param string $indexKey 35 | * @param string $numberOfChunksKey 36 | * @param string $chunkSizeKey 37 | * @param string $totalSizeKey 38 | */ 39 | protected function validateTotalSize(string $indexKey, string $numberOfChunksKey, string $chunkSizeKey, string $totalSizeKey): void 40 | { 41 | if ($this->totalSize < 1) { 42 | throw new InvalidArgumentException(sprintf('`%s` must be greater than zero', $totalSizeKey)); 43 | } elseif ($this->totalSize <= ($this->index - 1) * $this->chunkSize) { 44 | throw new InvalidArgumentException( 45 | sprintf('`%s` must be greater than or equal to the multiple of `%s` and `%s`', $totalSizeKey, $chunkSizeKey, $indexKey) 46 | ); 47 | } elseif ($this->totalSize > $this->numberOfChunks * $this->chunkSize) { 48 | throw new InvalidArgumentException( 49 | sprintf('`%s` must be smaller than or equal to the multiple of `%s` and `%s`', $totalSizeKey, $chunkSizeKey, $numberOfChunksKey) 50 | ); 51 | } 52 | } 53 | 54 | /** 55 | * {@inheritDoc} 56 | */ 57 | public function getStart(): float 58 | { 59 | return ($this->index - 1) * $this->chunkSize; 60 | } 61 | 62 | /** 63 | * {@inheritDoc} 64 | */ 65 | public function getEnd(): float 66 | { 67 | $end = ($this->index * $this->chunkSize) - 1; 68 | 69 | $sizeIndex = $this->totalSize - 1; 70 | if ($end > ($sizeIndex)) { 71 | return $sizeIndex; 72 | } 73 | 74 | return $end; 75 | } 76 | 77 | /** 78 | * {@inheritDoc} 79 | */ 80 | public function isFirst(): bool 81 | { 82 | return $this->index === 1; 83 | } 84 | 85 | /** 86 | * {@inheritDoc} 87 | */ 88 | public function isLast(): bool 89 | { 90 | return $this->index === $this->numberOfChunks; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Response/PercentageJsonResponse.php: -------------------------------------------------------------------------------- 1 | $percentage, 23 | ]); 24 | 25 | $this->percentage = $percentage; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/StorageConfig.php: -------------------------------------------------------------------------------- 1 | config = $config; 19 | } 20 | 21 | public function getDisk(): string 22 | { 23 | return $this->config['disk']; 24 | } 25 | 26 | public function getChunkDirectory(): string 27 | { 28 | return $this->config['directories']['chunk']; 29 | } 30 | 31 | public function getMergedDirectory(): string 32 | { 33 | return $this->config['directories']['merged']; 34 | } 35 | 36 | public function sweep(): bool 37 | { 38 | return $this->config['sweep']; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/UploadHandler.php: -------------------------------------------------------------------------------- 1 | driver = $driver; 34 | $this->config = $config; 35 | } 36 | 37 | /** 38 | * Save an uploaded file to the target directory. 39 | * 40 | * @param \Illuminate\Http\Request $request 41 | * @param \Closure $fileUploaded 42 | * 43 | * @return \Illuminate\Http\Response|\Symfony\Component\HttpFoundation\Response 44 | */ 45 | public function handle(Request $request, Closure $fileUploaded = null): Response 46 | { 47 | return $this->driver->handle($request, $this->config, $fileUploaded); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/UploadHandlerServiceProvider.php: -------------------------------------------------------------------------------- 1 | setupConfig(); 17 | } 18 | 19 | /** 20 | * Setup the config. 21 | * 22 | * @return void 23 | */ 24 | protected function setupConfig() 25 | { 26 | $source = realpath(__DIR__ . '/../config/upload-handler.php'); 27 | $this->publishes([$source => config_path('upload-handler.php')]); 28 | 29 | $this->mergeConfigFrom($source, 'upload-handler'); 30 | } 31 | 32 | /** 33 | * Register any package services. 34 | * 35 | * @return void 36 | */ 37 | public function register() 38 | { 39 | $this->registerUploadHandler(); 40 | } 41 | 42 | /** 43 | * Register the Upload Handler instance. 44 | * 45 | * @return void 46 | */ 47 | protected function registerUploadHandler() 48 | { 49 | $this->registerUploadManager(); 50 | $this->registerIdentityManager(); 51 | 52 | $this->app->singleton(UploadHandler::class, function () { 53 | /** @var \Illuminate\Support\Manager $uploadManager */ 54 | $uploadManager = $this->app['upload-handler.upload-manager']; 55 | 56 | $storageConfig = new StorageConfig($this->app->make('config')->get('upload-handler')); 57 | 58 | return new UploadHandler($uploadManager->driver(), $storageConfig); 59 | }); 60 | } 61 | 62 | /** 63 | * Register the Upload Manager instance. 64 | * 65 | * @return void 66 | */ 67 | protected function registerUploadManager() 68 | { 69 | $this->app->singleton('upload-handler.upload-manager', function () { 70 | return new UploadManager($this->app); 71 | }); 72 | } 73 | 74 | /** 75 | * Register the Upload Manager instance. 76 | * 77 | * @return void 78 | */ 79 | protected function registerIdentityManager() 80 | { 81 | $this->app->singleton('upload-handler.identity-manager', function () { 82 | return new IdentityManager($this->app); 83 | }); 84 | } 85 | 86 | /** 87 | * Get the services provided by the provider. 88 | * 89 | * @return array 90 | */ 91 | public function provides() 92 | { 93 | return [ 94 | UploadHandler::class, 95 | 'upload-handler.upload-manager', 96 | ]; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/UploadManager.php: -------------------------------------------------------------------------------- 1 | container['config']['upload-handler.monolith']); 20 | } 21 | 22 | public function createBlueimpDriver() 23 | { 24 | /** @var \Illuminate\Support\Manager $identityManager */ 25 | $identityManager = $this->container['upload-handler.identity-manager']; 26 | 27 | return new BlueimpHandler($this->container['config']['upload-handler.blueimp'], $identityManager->driver()); 28 | } 29 | 30 | public function createDropzoneDriver() 31 | { 32 | return new DropzoneHandler($this->container['config']['upload-handler.dropzone']); 33 | } 34 | 35 | public function createFlowJsDriver() 36 | { 37 | return new FlowJsHandler($this->container['config']['upload-handler.resumable-js'], $this->identityManager()->driver()); 38 | } 39 | 40 | public function createNgFileUploadDriver() 41 | { 42 | return new NgFileHandler($this->identityManager()->driver()); 43 | } 44 | 45 | public function createPluploadDriver() 46 | { 47 | return new PluploadHandler($this->identityManager()->driver()); 48 | } 49 | 50 | public function createResumableJsDriver() 51 | { 52 | return new ResumableJsHandler($this->container['config']['upload-handler.resumable-js'], $this->identityManager()->driver()); 53 | } 54 | 55 | public function createSimpleUploaderJsDriver() 56 | { 57 | return new SimpleUploaderJsHandler($this->container['config']['upload-handler.simple-uploader-js'], $this->identityManager()->driver()); 58 | } 59 | 60 | /** 61 | * @return \Illuminate\Support\Manager 62 | */ 63 | protected function identityManager() 64 | { 65 | return $this->container['upload-handler.identity-manager']; 66 | } 67 | 68 | /** 69 | * Get the default driver name. 70 | * 71 | * @return string 72 | */ 73 | public function getDefaultDriver() 74 | { 75 | return $this->container['config']['upload-handler.handler']; 76 | } 77 | 78 | /** 79 | * Set the default mail driver name. 80 | * 81 | * @param string $name 82 | * 83 | * @return void 84 | */ 85 | public function setDefaultDriver($name) 86 | { 87 | $this->container['config']['upload-handler.handler'] = $name; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/Driver/BlueimpHandlerTest.php: -------------------------------------------------------------------------------- 1 | set('upload-handler.identifier', 'nop'); 30 | config()->set('upload-handler.handler', 'blueimp'); 31 | config()->set('upload-handler.sweep', false); 32 | $this->handler = app()->make(UploadHandler::class); 33 | 34 | Storage::fake('local'); 35 | Event::fake(); 36 | } 37 | 38 | public function testDriverInstance() 39 | { 40 | $manager = app()->make('upload-handler.upload-manager'); 41 | 42 | $this->assertInstanceOf(BlueimpHandler::class, $manager->driver()); 43 | } 44 | 45 | public static function notAllowedRequestMethods() 46 | { 47 | return [ 48 | 'DELETE' => [Request::METHOD_DELETE], 49 | 'PURGE' => [Request::METHOD_PURGE], 50 | 'TRACE' => [Request::METHOD_TRACE], 51 | 'CONNECT' => [Request::METHOD_CONNECT], 52 | ]; 53 | } 54 | 55 | /** 56 | * @dataProvider notAllowedRequestMethods 57 | */ 58 | public function testMethodNotAllowed($requestMethod) 59 | { 60 | $request = Request::create('', $requestMethod); 61 | 62 | $this->expectException(MethodNotAllowedHttpException::class); 63 | 64 | TestResponse::fromBaseResponse($this->handler->handle($request)); 65 | } 66 | 67 | public function testInfo() 68 | { 69 | $request = Request::create('', Request::METHOD_HEAD); 70 | 71 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 72 | $response->assertSuccessful(); 73 | 74 | $response->assertHeader('Pragma', 'no-cache'); 75 | $response->assertHeader('Cache-Control', 'must-revalidate, no-cache, no-store, private'); 76 | $response->assertHeader('Content-Disposition', 'inline; filename="files.json"'); 77 | $response->assertHeader('X-Content-Type-Options', 'nosniff'); 78 | $response->assertHeader('Vary', 'Accept'); 79 | } 80 | 81 | public function testResume() 82 | { 83 | $this->createFakeLocalFile('chunks/200_test.txt', '000-099'); 84 | 85 | $request = Request::create('', Request::METHOD_GET, [ 86 | 'file' => 'test.txt', 87 | 'totalSize' => '200', 88 | ]); 89 | 90 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 91 | $response->assertSuccessful(); 92 | 93 | $response->assertJson([ 94 | 'file' => [ 95 | 'name' => 'test.txt', 96 | 'size' => 100, 97 | ], 98 | ]); 99 | } 100 | 101 | public function testUploadWhenFileParameterIsEmpty() 102 | { 103 | $request = Request::create('', Request::METHOD_POST); 104 | 105 | $this->expectException(BadRequestHttpException::class); 106 | 107 | $this->handler->handle($request); 108 | } 109 | 110 | public function testUploadWhenFileParameterIsInvalid() 111 | { 112 | $file = new UploadedFile('', '', null, \UPLOAD_ERR_INI_SIZE); 113 | 114 | $request = Request::create('', Request::METHOD_POST, [], [], [ 115 | 'file' => $file, 116 | ]); 117 | 118 | $this->expectException(InternalServerErrorHttpException::class); 119 | 120 | $this->handler->handle($request); 121 | } 122 | 123 | public function testUploadFirstChunk() 124 | { 125 | $file = UploadedFile::fake()->create('test.txt', 100); 126 | $request = Request::create('', Request::METHOD_POST, [], [], [ 127 | 'file' => $file, 128 | ], [ 129 | 'HTTP_CONTENT_RANGE' => 'bytes 0-99/200', 130 | ]); 131 | 132 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 133 | $response->assertSuccessful(); 134 | $response->assertJson(['done' => 50]); 135 | 136 | Storage::disk('local')->assertExists('chunks/200_test.txt/000-099'); 137 | 138 | Event::assertNotDispatched(FileUploaded::class, function ($event) use ($file) { 139 | return $event->file = $file->hashName('merged'); 140 | }); 141 | } 142 | 143 | public function testUploadFirstChunkWithCallback() 144 | { 145 | $file = UploadedFile::fake()->create('test.txt', 100); 146 | $request = Request::create('', Request::METHOD_POST, [], [], [ 147 | 'file' => $file, 148 | ], [ 149 | 'HTTP_CONTENT_RANGE' => 'bytes 0-99/200', 150 | ]); 151 | 152 | $callback = $this->createClosureMock($this->never()); 153 | 154 | $this->handler->handle($request, $callback); 155 | 156 | Event::assertNotDispatched(FileUploaded::class, function ($event) use ($file) { 157 | return $event->file = $file->hashName('merged'); 158 | }); 159 | } 160 | 161 | public function testUploadLastChunk() 162 | { 163 | $this->createFakeLocalFile('chunks/200_test.txt', '000'); 164 | 165 | $file = UploadedFile::fake()->create('test.txt', 100); 166 | $request = Request::create('', Request::METHOD_POST, [], [], [ 167 | 'file' => $file, 168 | ], [ 169 | 'HTTP_CONTENT_RANGE' => 'bytes 100-199/200', 170 | ]); 171 | 172 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 173 | $response->assertSuccessful(); 174 | $response->assertJson(['done' => 100]); 175 | 176 | Storage::disk('local')->assertExists('chunks/200_test.txt/100-199'); 177 | Storage::disk('local')->assertExists($file->hashName('merged')); 178 | 179 | Event::assertDispatched(FileUploaded::class, function ($event) use ($file) { 180 | return $event->file = $file->hashName('merged'); 181 | }); 182 | } 183 | 184 | public function testUploadLastChunkWithCallback() 185 | { 186 | $this->createFakeLocalFile('chunks/200_test.txt', '000'); 187 | 188 | $file = UploadedFile::fake()->create('test.txt', 100); 189 | $request = Request::create('', Request::METHOD_POST, [], [], [ 190 | 'file' => $file, 191 | ], [ 192 | 'HTTP_CONTENT_RANGE' => 'bytes 100-199/200', 193 | ]); 194 | 195 | $callback = $this->createClosureMock( 196 | $this->once(), 197 | 'local', 198 | $file->hashName('merged') 199 | ); 200 | 201 | $this->handler->handle($request, $callback); 202 | 203 | Event::assertDispatched(FileUploaded::class, function ($event) use ($file) { 204 | return $event->file = $file->hashName('merged'); 205 | }); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /tests/Driver/DropzoneHandlerTest.php: -------------------------------------------------------------------------------- 1 | set('upload-handler.handler', 'dropzone'); 31 | config()->set('upload-handler.sweep', false); 32 | $this->handler = app()->make(UploadHandler::class); 33 | 34 | Storage::fake('local'); 35 | Event::fake(); 36 | } 37 | 38 | public function testDriverInstance() 39 | { 40 | $manager = app()->make('upload-handler.upload-manager'); 41 | 42 | $this->assertInstanceOf(DropzoneHandler::class, $manager->driver()); 43 | } 44 | 45 | public static function notAllowedRequestMethods() 46 | { 47 | return [ 48 | 'HEAD' => [Request::METHOD_HEAD], 49 | 'GET' => [Request::METHOD_GET], 50 | 'PUT' => [Request::METHOD_PUT], 51 | 'PATCH' => [Request::METHOD_PATCH], 52 | 'DELETE' => [Request::METHOD_DELETE], 53 | 'PURGE' => [Request::METHOD_PURGE], 54 | 'OPTIONS' => [Request::METHOD_OPTIONS], 55 | 'TRACE' => [Request::METHOD_TRACE], 56 | 'CONNECT' => [Request::METHOD_CONNECT], 57 | ]; 58 | } 59 | 60 | /** 61 | * @dataProvider notAllowedRequestMethods 62 | */ 63 | public function testMethodNotAllowed($requestMethod) 64 | { 65 | $request = Request::create('', $requestMethod); 66 | 67 | $this->expectException(MethodNotAllowedHttpException::class); 68 | 69 | TestResponse::fromBaseResponse($this->handler->handle($request)); 70 | } 71 | 72 | public function testUploadWhenFileParameterIsEmpty() 73 | { 74 | $request = Request::create('', Request::METHOD_POST); 75 | 76 | $this->expectException(BadRequestHttpException::class); 77 | 78 | $this->handler->handle($request); 79 | } 80 | 81 | public function testUploadWhenFileParameterIsInvalid() 82 | { 83 | $file = new UploadedFile('', '', null, \UPLOAD_ERR_INI_SIZE); 84 | 85 | $request = Request::create('', Request::METHOD_POST, [], [], [ 86 | 'file' => $file, 87 | ]); 88 | 89 | $this->expectException(InternalServerErrorHttpException::class); 90 | 91 | $this->handler->handle($request); 92 | } 93 | 94 | public function testUploadMonolith() 95 | { 96 | $file = UploadedFile::fake()->create('test.txt', 100); 97 | $request = Request::create('', Request::METHOD_POST, [], [], [ 98 | 'file' => $file, 99 | ]); 100 | 101 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 102 | $response->assertSuccessful(); 103 | $response->assertJson(['done' => 100]); 104 | 105 | Storage::disk('local')->assertExists($file->hashName('merged')); 106 | 107 | Event::assertDispatched(FileUploaded::class, function ($event) use ($file) { 108 | return $event->file = $file->hashName('merged'); 109 | }); 110 | } 111 | 112 | public function testUploadMonolithWithCallback() 113 | { 114 | $file = UploadedFile::fake()->create('test.txt', 100); 115 | $request = Request::create('', Request::METHOD_POST, [], [], [ 116 | 'file' => $file, 117 | ]); 118 | 119 | $callback = $this->createClosureMock( 120 | $this->once(), 121 | 'local', 122 | $file->hashName('merged') 123 | ); 124 | 125 | $this->handler->handle($request, $callback); 126 | 127 | Event::assertDispatched(FileUploaded::class, function ($event) use ($file) { 128 | return $event->file = $file->hashName('merged'); 129 | }); 130 | } 131 | 132 | public static function excludedPostParameterProvider() 133 | { 134 | return [ 135 | 'dzuuid' => ['dzuuid'], 136 | 'dzchunkindex' => ['dzchunkindex'], 137 | 'dztotalfilesize' => ['dztotalfilesize'], 138 | 'dzchunksize' => ['dzchunksize'], 139 | 'dztotalchunkcount' => ['dztotalchunkcount'], 140 | 'dzchunkbyteoffset' => ['dzchunkbyteoffset'], 141 | ]; 142 | } 143 | 144 | /** 145 | * @dataProvider excludedPostParameterProvider 146 | */ 147 | public function testPostParameterValidation($exclude) 148 | { 149 | $arr = [ 150 | 'dzuuid' => '2494cefe4d234bd331aeb4514fe97d810efba29b', 151 | 'dzchunkindex' => 0, 152 | 'dztotalfilesize' => 200, 153 | 'dzchunksize' => 100, 154 | 'dztotalchunkcount' => 2, 155 | 'dzchunkbyteoffset' => 100, 156 | ]; 157 | 158 | unset($arr[$exclude]); 159 | 160 | $request = Request::create('', Request::METHOD_POST, $arr, [], [ 161 | 'file' => UploadedFile::fake()->create('test.txt', 100), 162 | ]); 163 | 164 | $this->expectException(ValidationException::class); 165 | 166 | $this->handler->handle($request); 167 | } 168 | 169 | public function testUploadFirstChunk() 170 | { 171 | $file = UploadedFile::fake()->create('test.txt', 100); 172 | $request = Request::create('', Request::METHOD_POST, [ 173 | 'dzuuid' => '2494cefe4d234bd331aeb4514fe97d810efba29b', 174 | 'dzchunkindex' => 0, 175 | 'dztotalfilesize' => 200, 176 | 'dzchunksize' => 100, 177 | 'dztotalchunkcount' => 2, 178 | 'dzchunkbyteoffset' => 100, 179 | ], [], [ 180 | 'file' => $file, 181 | ]); 182 | 183 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 184 | $response->assertSuccessful(); 185 | $response->assertJson(['done' => 50]); 186 | 187 | Storage::disk('local')->assertExists('chunks/2494cefe4d234bd331aeb4514fe97d810efba29b/000-099'); 188 | 189 | Event::assertNotDispatched(FileUploaded::class, function ($event) use ($file) { 190 | return $event->file = $file->hashName('merged'); 191 | }); 192 | } 193 | 194 | public function testUploadFirstChunkWithCallback() 195 | { 196 | $file = UploadedFile::fake()->create('test.txt', 100); 197 | $request = Request::create('', Request::METHOD_POST, [ 198 | 'dzuuid' => '2494cefe4d234bd331aeb4514fe97d810efba29b', 199 | 'dzchunkindex' => 0, 200 | 'dztotalfilesize' => 200, 201 | 'dzchunksize' => 100, 202 | 'dztotalchunkcount' => 2, 203 | 'dzchunkbyteoffset' => 100, 204 | ], [], [ 205 | 'file' => $file, 206 | ]); 207 | 208 | $callback = $this->createClosureMock($this->never()); 209 | 210 | $this->handler->handle($request, $callback); 211 | 212 | Event::assertNotDispatched(FileUploaded::class, function ($event) use ($file) { 213 | return $event->file = $file->hashName('merged'); 214 | }); 215 | } 216 | 217 | public function testUploadLastChunk() 218 | { 219 | $this->createFakeLocalFile('chunks/2494cefe4d234bd331aeb4514fe97d810efba29b', '000'); 220 | 221 | $file = UploadedFile::fake()->create('test.txt', 100); 222 | $request = Request::create('', Request::METHOD_POST, [ 223 | 'dzuuid' => '2494cefe4d234bd331aeb4514fe97d810efba29b', 224 | 'dzchunkindex' => 1, 225 | 'dztotalfilesize' => 200, 226 | 'dzchunksize' => 100, 227 | 'dztotalchunkcount' => 2, 228 | 'dzchunkbyteoffset' => 100, 229 | ], [], [ 230 | 'file' => $file, 231 | ]); 232 | 233 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 234 | $response->assertSuccessful(); 235 | $response->assertJson(['done' => 100]); 236 | 237 | Storage::disk('local')->assertExists('chunks/2494cefe4d234bd331aeb4514fe97d810efba29b/100-199'); 238 | Storage::disk('local')->assertExists($file->hashName('merged')); 239 | 240 | Event::assertDispatched(FileUploaded::class, function ($event) use ($file) { 241 | return $event->file = $file->hashName('merged'); 242 | }); 243 | } 244 | 245 | public function testUploadLastChunkWithCallback() 246 | { 247 | $this->createFakeLocalFile('chunks/2494cefe4d234bd331aeb4514fe97d810efba29b', '000'); 248 | 249 | $file = UploadedFile::fake()->create('test.txt', 100); 250 | $request = Request::create('', Request::METHOD_POST, [ 251 | 'dzuuid' => '2494cefe4d234bd331aeb4514fe97d810efba29b', 252 | 'dzchunkindex' => 1, 253 | 'dztotalfilesize' => 200, 254 | 'dzchunksize' => 100, 255 | 'dztotalchunkcount' => 2, 256 | 'dzchunkbyteoffset' => 100, 257 | ], [], [ 258 | 'file' => $file, 259 | ]); 260 | 261 | $callback = $this->createClosureMock( 262 | $this->once(), 263 | 'local', 264 | $file->hashName('merged') 265 | ); 266 | 267 | $this->handler->handle($request, $callback); 268 | 269 | Event::assertDispatched(FileUploaded::class, function ($event) use ($file) { 270 | return $event->file = $file->hashName('merged'); 271 | }); 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /tests/Driver/FlowJsHandlerTest.php: -------------------------------------------------------------------------------- 1 | set('upload-handler.identifier', 'nop'); 32 | config()->set('upload-handler.handler', 'flow-js'); 33 | config()->set('upload-handler.sweep', false); 34 | $this->handler = app()->make(UploadHandler::class); 35 | 36 | Storage::fake('local'); 37 | Event::fake(); 38 | } 39 | 40 | public function testDriverInstance() 41 | { 42 | $manager = app()->make('upload-handler.upload-manager'); 43 | 44 | $this->assertInstanceOf(FlowJsHandler::class, $manager->driver()); 45 | } 46 | 47 | public static function notAllowedRequestMethods() 48 | { 49 | return [ 50 | 'HEAD' => [Request::METHOD_HEAD], 51 | 'PUT' => [Request::METHOD_PUT], 52 | 'PATCH' => [Request::METHOD_PATCH], 53 | 'DELETE' => [Request::METHOD_DELETE], 54 | 'PURGE' => [Request::METHOD_PURGE], 55 | 'OPTIONS' => [Request::METHOD_OPTIONS], 56 | 'TRACE' => [Request::METHOD_TRACE], 57 | 'CONNECT' => [Request::METHOD_CONNECT], 58 | ]; 59 | } 60 | 61 | /** 62 | * @dataProvider notAllowedRequestMethods 63 | */ 64 | public function testMethodNotAllowed($requestMethod) 65 | { 66 | $request = Request::create('', $requestMethod); 67 | 68 | $this->expectException(MethodNotAllowedHttpException::class); 69 | 70 | TestResponse::fromBaseResponse($this->handler->handle($request)); 71 | } 72 | 73 | public function testResumeWhenChunkDoesNotExists() 74 | { 75 | $this->createFakeLocalFile('chunks/200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', '000-099'); 76 | 77 | $request = Request::create('', Request::METHOD_GET, [ 78 | 'flowChunkNumber' => 2, 79 | 'flowTotalChunks' => 2, 80 | 'flowChunkSize' => 100, 81 | 'flowTotalSize' => 200, 82 | 'flowIdentifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', 83 | 'flowFilename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 84 | 'flowRelativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 85 | 'flowCurrentChunkSize' => 100, 86 | ]); 87 | 88 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 89 | $response->assertStatus(Response::HTTP_NO_CONTENT); 90 | } 91 | 92 | public function testResume() 93 | { 94 | $this->createFakeLocalFile('chunks/200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', '000-099'); 95 | 96 | $request = Request::create('', Request::METHOD_GET, [ 97 | 'flowChunkNumber' => 1, 98 | 'flowTotalChunks' => 2, 99 | 'flowChunkSize' => 100, 100 | 'flowTotalSize' => 200, 101 | 'flowIdentifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', 102 | 'flowFilename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 103 | 'flowRelativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 104 | 'flowCurrentChunkSize' => 100, 105 | ]); 106 | 107 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 108 | $response->assertSuccessful(); 109 | } 110 | 111 | public function testUploadWhenFileParameterIsEmpty() 112 | { 113 | $request = Request::create('', Request::METHOD_POST); 114 | 115 | $this->expectException(BadRequestHttpException::class); 116 | 117 | $this->handler->handle($request); 118 | } 119 | 120 | public function testUploadWhenFileParameterIsInvalid() 121 | { 122 | $file = new UploadedFile('', '', null, \UPLOAD_ERR_INI_SIZE); 123 | 124 | $request = Request::create('', Request::METHOD_POST, [], [], [ 125 | 'file' => $file, 126 | ]); 127 | 128 | $this->expectException(InternalServerErrorHttpException::class); 129 | 130 | $this->handler->handle($request); 131 | } 132 | 133 | public static function excludedPostParameterProvider() 134 | { 135 | return [ 136 | 'flowChunkNumber' => ['flowChunkNumber'], 137 | 'flowTotalChunks' => ['flowTotalChunks'], 138 | 'flowChunkSize' => ['flowChunkSize'], 139 | 'flowTotalSize' => ['flowTotalSize'], 140 | 'flowIdentifier' => ['flowIdentifier'], 141 | 'flowFilename' => ['flowFilename'], 142 | 'flowRelativePath' => ['flowRelativePath'], 143 | 'flowCurrentChunkSize' => ['flowCurrentChunkSize'], 144 | ]; 145 | } 146 | 147 | /** 148 | * @dataProvider excludedPostParameterProvider 149 | */ 150 | public function testPostParameterValidation($exclude) 151 | { 152 | $arr = [ 153 | 'flowChunkNumber' => 1, 154 | 'flowTotalChunks' => 2, 155 | 'flowChunkSize' => 100, 156 | 'flowTotalSize' => 200, 157 | 'flowIdentifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', 158 | 'flowFilename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 159 | 'flowRelativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 160 | 'flowCurrentChunkSize' => 100, 161 | ]; 162 | 163 | unset($arr[$exclude]); 164 | 165 | $request = Request::create('', Request::METHOD_POST, $arr, [], [ 166 | 'file' => UploadedFile::fake() 167 | ->create('test.txt', 100), 168 | ]); 169 | 170 | $this->expectException(ValidationException::class); 171 | 172 | $this->handler->handle($request); 173 | } 174 | 175 | public function testUploadFirstChunk() 176 | { 177 | $file = UploadedFile::fake()->create('test.txt', 100); 178 | $request = Request::create('', Request::METHOD_POST, [ 179 | 'flowChunkNumber' => 1, 180 | 'flowTotalChunks' => 2, 181 | 'flowChunkSize' => 100, 182 | 'flowTotalSize' => 200, 183 | 'flowIdentifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', 184 | 'flowFilename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 185 | 'flowRelativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 186 | 'flowCurrentChunkSize' => 100, 187 | ], [], [ 188 | 'file' => $file, 189 | ]); 190 | 191 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 192 | $response->assertSuccessful(); 193 | $response->assertJson(['done' => 50]); 194 | 195 | Storage::disk('local')->assertExists('chunks/200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt/000-099'); 196 | 197 | Event::assertNotDispatched(FileUploaded::class, function ($event) use ($file) { 198 | return $event->file = $file->hashName('merged'); 199 | }); 200 | } 201 | 202 | public function testUploadFirstChunkWithCallback() 203 | { 204 | $file = UploadedFile::fake()->create('test.txt', 100); 205 | $request = Request::create('', Request::METHOD_POST, [ 206 | 'flowChunkNumber' => 1, 207 | 'flowTotalChunks' => 2, 208 | 'flowChunkSize' => 100, 209 | 'flowTotalSize' => 200, 210 | 'flowIdentifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', 211 | 'flowFilename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 212 | 'flowRelativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 213 | 'flowCurrentChunkSize' => 100, 214 | ], [], [ 215 | 'file' => $file, 216 | ]); 217 | 218 | $callback = $this->createClosureMock($this->never()); 219 | 220 | $this->handler->handle($request, $callback); 221 | 222 | Event::assertNotDispatched(FileUploaded::class, function ($event) use ($file) { 223 | return $event->file = $file->hashName('merged'); 224 | }); 225 | } 226 | 227 | public function testUploadLastChunk() 228 | { 229 | $this->createFakeLocalFile('chunks/200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', '000-099'); 230 | 231 | $file = UploadedFile::fake()->create('test.txt', 100); 232 | $request = Request::create('', Request::METHOD_POST, [ 233 | 'flowChunkNumber' => 2, 234 | 'flowTotalChunks' => 2, 235 | 'flowChunkSize' => 100, 236 | 'flowTotalSize' => 200, 237 | 'flowIdentifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', 238 | 'flowFilename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 239 | 'flowRelativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 240 | 'flowCurrentChunkSize' => 100, 241 | ], [], [ 242 | 'file' => $file, 243 | ]); 244 | 245 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 246 | $response->assertSuccessful(); 247 | $response->assertJson(['done' => 100]); 248 | 249 | Storage::disk('local')->assertExists('chunks/200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt/100-199'); 250 | Storage::disk('local')->assertExists($file->hashName('merged')); 251 | 252 | Event::assertDispatched(FileUploaded::class, function ($event) use ($file) { 253 | return $event->file = $file->hashName('merged'); 254 | }); 255 | } 256 | 257 | public function testUploadLastChunkWithCallback() 258 | { 259 | $this->createFakeLocalFile('chunks/200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', '000-099'); 260 | 261 | $file = UploadedFile::fake()->create('test.txt', 100); 262 | $request = Request::create('', Request::METHOD_POST, [ 263 | 'flowChunkNumber' => 2, 264 | 'flowTotalChunks' => 2, 265 | 'flowChunkSize' => 100, 266 | 'flowTotalSize' => 200, 267 | 'flowIdentifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', 268 | 'flowFilename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 269 | 'flowRelativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 270 | 'flowCurrentChunkSize' => 100, 271 | ], [], [ 272 | 'file' => $file, 273 | ]); 274 | 275 | $callback = $this->createClosureMock( 276 | $this->once(), 277 | 'local', 278 | $file->hashName('merged') 279 | ); 280 | 281 | $this->handler->handle($request, $callback); 282 | 283 | Event::assertDispatched(FileUploaded::class, function ($event) use ($file) { 284 | return $event->file = $file->hashName('merged'); 285 | }); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /tests/Driver/MonolithHandlerTest.php: -------------------------------------------------------------------------------- 1 | set('upload-handler.handler', 'monolith'); 30 | $this->handler = app()->make(UploadHandler::class); 31 | 32 | Storage::fake('local'); 33 | Event::fake(); 34 | } 35 | 36 | public function testDriverInstance() 37 | { 38 | $manager = app()->make('upload-handler.upload-manager'); 39 | 40 | $this->assertInstanceOf(MonolithHandler::class, $manager->driver()); 41 | } 42 | 43 | public static function notAllowedRequestMethods() 44 | { 45 | return [ 46 | 'HEAD' => [Request::METHOD_HEAD], 47 | 'GET' => [Request::METHOD_GET], 48 | 'PUT' => [Request::METHOD_PUT], 49 | 'PATCH' => [Request::METHOD_PATCH], 50 | 'DELETE' => [Request::METHOD_DELETE], 51 | 'PURGE' => [Request::METHOD_PURGE], 52 | 'OPTIONS' => [Request::METHOD_OPTIONS], 53 | 'TRACE' => [Request::METHOD_TRACE], 54 | 'CONNECT' => [Request::METHOD_CONNECT], 55 | ]; 56 | } 57 | 58 | /** 59 | * @dataProvider notAllowedRequestMethods 60 | */ 61 | public function testMethodNotAllowed($requestMethod) 62 | { 63 | $request = Request::create('', $requestMethod); 64 | 65 | $this->expectException(MethodNotAllowedHttpException::class); 66 | 67 | TestResponse::fromBaseResponse($this->handler->handle($request)); 68 | } 69 | 70 | public function testUploadWhenFileParameterIsEmpty() 71 | { 72 | $request = Request::create('', Request::METHOD_POST); 73 | 74 | $this->expectException(BadRequestHttpException::class); 75 | 76 | $this->handler->handle($request); 77 | } 78 | 79 | public function testUploadWhenFileParameterIsInvalid() 80 | { 81 | $file = new UploadedFile('', '', null, \UPLOAD_ERR_INI_SIZE); 82 | 83 | $request = Request::create('', Request::METHOD_POST, [], [], [ 84 | 'file' => $file, 85 | ]); 86 | 87 | $this->expectException(InternalServerErrorHttpException::class); 88 | 89 | $this->handler->handle($request); 90 | } 91 | 92 | public function testUpload() 93 | { 94 | $file = UploadedFile::fake()->create('test.txt', 20); 95 | $request = Request::create('', Request::METHOD_POST, [], [], [ 96 | 'file' => $file, 97 | ]); 98 | 99 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 100 | $response->assertSuccessful(); 101 | 102 | Storage::disk('local')->assertExists($file->hashName('merged')); 103 | 104 | Event::assertDispatched(FileUploaded::class, function ($event) use ($file) { 105 | return $event->file = $file->hashName('merged'); 106 | }); 107 | } 108 | 109 | public function testUploadWithCallback() 110 | { 111 | $file = UploadedFile::fake()->create('test.txt', 20); 112 | $request = Request::create('', Request::METHOD_POST, [], [], [ 113 | 'file' => $file, 114 | ]); 115 | 116 | $callback = $this->createClosureMock( 117 | $this->once(), 118 | 'local', 119 | $file->hashName('merged') 120 | ); 121 | 122 | $this->handler->handle($request, $callback); 123 | 124 | Event::assertDispatched(FileUploaded::class, function ($event) use ($file) { 125 | return $event->file = $file->hashName('merged'); 126 | }); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/Driver/NgFileHandlerTest.php: -------------------------------------------------------------------------------- 1 | set('upload-handler.identifier', 'nop'); 31 | config()->set('upload-handler.handler', 'ng-file-upload'); 32 | config()->set('upload-handler.sweep', false); 33 | $this->handler = app()->make(UploadHandler::class); 34 | 35 | Storage::fake('local'); 36 | Event::fake(); 37 | } 38 | 39 | public function testDriverInstance() 40 | { 41 | $manager = app()->make('upload-handler.upload-manager'); 42 | 43 | $this->assertInstanceOf(NgFileHandler::class, $manager->driver()); 44 | } 45 | 46 | public static function notAllowedRequestMethods() 47 | { 48 | return [ 49 | 'HEAD' => [Request::METHOD_HEAD], 50 | 'PUT' => [Request::METHOD_PUT], 51 | 'PATCH' => [Request::METHOD_PATCH], 52 | 'DELETE' => [Request::METHOD_DELETE], 53 | 'PURGE' => [Request::METHOD_PURGE], 54 | 'OPTIONS' => [Request::METHOD_OPTIONS], 55 | 'TRACE' => [Request::METHOD_TRACE], 56 | 'CONNECT' => [Request::METHOD_CONNECT], 57 | ]; 58 | } 59 | 60 | /** 61 | * @dataProvider notAllowedRequestMethods 62 | */ 63 | public function testMethodNotAllowed($requestMethod) 64 | { 65 | $request = Request::create('', $requestMethod); 66 | 67 | $this->expectException(MethodNotAllowedHttpException::class); 68 | 69 | TestResponse::fromBaseResponse($this->handler->handle($request)); 70 | } 71 | 72 | public function testResumeWhenChunkDoesNotExists() 73 | { 74 | $request = Request::create('', Request::METHOD_GET, [ 75 | 'file' => 'test.txt', 76 | 'totalSize' => '200', 77 | ]); 78 | 79 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 80 | $response->assertSuccessful(); 81 | $response->assertJson(['size' => 0]); 82 | } 83 | 84 | public function testResume() 85 | { 86 | $this->createFakeLocalFile('chunks/200_test.txt', '000-099'); 87 | 88 | $request = Request::create('', Request::METHOD_GET, [ 89 | 'file' => 'test.txt', 90 | 'totalSize' => '200', 91 | ]); 92 | 93 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 94 | $response->assertSuccessful(); 95 | $response->assertJson(['size' => 100]); 96 | } 97 | 98 | public function testUploadWhenFileParameterIsEmpty() 99 | { 100 | $request = Request::create('', Request::METHOD_POST); 101 | 102 | $this->expectException(BadRequestHttpException::class); 103 | 104 | $this->handler->handle($request); 105 | } 106 | 107 | public function testUploadWhenFileParameterIsInvalid() 108 | { 109 | $file = new UploadedFile('', '', null, \UPLOAD_ERR_INI_SIZE); 110 | 111 | $request = Request::create('', Request::METHOD_POST, [], [], [ 112 | 'file' => $file, 113 | ]); 114 | 115 | $this->expectException(InternalServerErrorHttpException::class); 116 | 117 | $this->handler->handle($request); 118 | } 119 | 120 | public function testUploadMonolith() 121 | { 122 | $file = UploadedFile::fake()->create('test.txt', 100); 123 | $request = Request::create('', Request::METHOD_POST, [], [], [ 124 | 'file' => $file, 125 | ]); 126 | 127 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 128 | $response->assertSuccessful(); 129 | $response->assertJson(['done' => 100]); 130 | 131 | Storage::disk('local')->assertExists($file->hashName('merged')); 132 | 133 | Event::assertDispatched(FileUploaded::class, function ($event) use ($file) { 134 | return $event->file = $file->hashName('merged'); 135 | }); 136 | } 137 | 138 | public function testUploadMonolithWithCallback() 139 | { 140 | $file = UploadedFile::fake()->create('test.txt', 100); 141 | $request = Request::create('', Request::METHOD_POST, [], [], [ 142 | 'file' => $file, 143 | ]); 144 | 145 | $callback = $this->createClosureMock( 146 | $this->once(), 147 | 'local', 148 | $file->hashName('merged') 149 | ); 150 | 151 | $this->handler->handle($request, $callback); 152 | 153 | Event::assertDispatched(FileUploaded::class, function ($event) use ($file) { 154 | return $event->file = $file->hashName('merged'); 155 | }); 156 | } 157 | 158 | public static function excludedPostParameterProvider() 159 | { 160 | return [ 161 | '_chunkNumber' => ['_chunkNumber'], 162 | '_chunkSize' => ['_chunkSize'], 163 | '_totalSize' => ['_totalSize'], 164 | '_currentChunkSize' => ['_currentChunkSize'], 165 | ]; 166 | } 167 | 168 | /** 169 | * @dataProvider excludedPostParameterProvider 170 | */ 171 | public function testPostParameterValidation($exclude) 172 | { 173 | $arr = [ 174 | '_chunkNumber' => 1, 175 | '_chunkSize' => 100, 176 | '_totalSize' => 200, 177 | '_currentChunkSize' => 100, 178 | ]; 179 | 180 | unset($arr[$exclude]); 181 | 182 | $request = Request::create('', Request::METHOD_POST, $arr, [], [ 183 | 'file' => UploadedFile::fake() 184 | ->create('test.txt', 100), 185 | ]); 186 | 187 | $this->expectException(ValidationException::class); 188 | 189 | $this->handler->handle($request); 190 | } 191 | 192 | public function testUploadFirstChunk() 193 | { 194 | $file = UploadedFile::fake()->create('test.txt', 100); 195 | $request = Request::create('', Request::METHOD_POST, [ 196 | '_chunkNumber' => 0, 197 | '_chunkSize' => 100, 198 | '_totalSize' => 200, 199 | '_currentChunkSize' => 100, 200 | ], [], [ 201 | 'file' => $file, 202 | ]); 203 | 204 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 205 | $response->assertSuccessful(); 206 | $response->assertJson(['done' => 50]); 207 | 208 | Storage::disk('local')->assertExists('chunks/200_test.txt/000-099'); 209 | 210 | Event::assertNotDispatched(FileUploaded::class, function ($event) use ($file) { 211 | return $event->file = $file->hashName('merged'); 212 | }); 213 | } 214 | 215 | public function testUploadFirstChunkWithCallback() 216 | { 217 | $file = UploadedFile::fake()->create('test.txt', 100); 218 | $request = Request::create('', Request::METHOD_POST, [ 219 | '_chunkNumber' => 0, 220 | '_chunkSize' => 100, 221 | '_totalSize' => 200, 222 | '_currentChunkSize' => 100, 223 | ], [], [ 224 | 'file' => $file, 225 | ]); 226 | 227 | $callback = $this->createClosureMock($this->never()); 228 | 229 | $this->handler->handle($request, $callback); 230 | 231 | Event::assertNotDispatched(FileUploaded::class, function ($event) use ($file) { 232 | return $event->file = $file->hashName('merged'); 233 | }); 234 | } 235 | 236 | public function testUploadLastChunk() 237 | { 238 | $this->createFakeLocalFile('chunks/200_test.txt', '000-099'); 239 | 240 | $file = UploadedFile::fake()->create('test.txt', 100); 241 | $request = Request::create('', Request::METHOD_POST, [ 242 | '_chunkNumber' => 1, 243 | '_chunkSize' => 100, 244 | '_totalSize' => 200, 245 | '_currentChunkSize' => 100, 246 | ], [], [ 247 | 'file' => $file, 248 | ]); 249 | 250 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 251 | $response->assertSuccessful(); 252 | $response->assertJson(['done' => 100]); 253 | 254 | Storage::disk('local')->assertExists('chunks/200_test.txt/100-199'); 255 | Storage::disk('local')->assertExists($file->hashName('merged')); 256 | 257 | Event::assertDispatched(FileUploaded::class, function ($event) use ($file) { 258 | return $event->file = $file->hashName('merged'); 259 | }); 260 | } 261 | 262 | public function testUploadLastChunkWithCallback() 263 | { 264 | $this->createFakeLocalFile('chunks/200_test.txt', '000-099'); 265 | 266 | $file = UploadedFile::fake()->create('test.txt', 100); 267 | $request = Request::create('', Request::METHOD_POST, [ 268 | '_chunkNumber' => 1, 269 | '_chunkSize' => 100, 270 | '_totalSize' => 200, 271 | '_currentChunkSize' => 100, 272 | ], [], [ 273 | 'file' => $file, 274 | ]); 275 | 276 | $callback = $this->createClosureMock( 277 | $this->once(), 278 | 'local', 279 | $file->hashName('merged') 280 | ); 281 | 282 | $this->handler->handle($request, $callback); 283 | 284 | Event::assertDispatched(FileUploaded::class, function ($event) use ($file) { 285 | return $event->file = $file->hashName('merged'); 286 | }); 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /tests/Driver/PluploadHandlerTest.php: -------------------------------------------------------------------------------- 1 | set('upload-handler.identifier', 'nop'); 31 | config()->set('upload-handler.handler', 'plupload'); 32 | config()->set('upload-handler.sweep', false); 33 | $this->handler = app()->make(UploadHandler::class); 34 | 35 | Storage::fake('local'); 36 | Event::fake(); 37 | } 38 | 39 | public function testDriverInstance() 40 | { 41 | $manager = app()->make('upload-handler.upload-manager'); 42 | 43 | $this->assertInstanceOf(PluploadHandler::class, $manager->driver()); 44 | } 45 | 46 | public static function notAllowedRequestMethods() 47 | { 48 | return [ 49 | 'GET' => [Request::METHOD_GET], 50 | 'HEAD' => [Request::METHOD_HEAD], 51 | 'PUT' => [Request::METHOD_PUT], 52 | 'PATCH' => [Request::METHOD_PATCH], 53 | 'DELETE' => [Request::METHOD_DELETE], 54 | 'PURGE' => [Request::METHOD_PURGE], 55 | 'OPTIONS' => [Request::METHOD_OPTIONS], 56 | 'TRACE' => [Request::METHOD_TRACE], 57 | 'CONNECT' => [Request::METHOD_CONNECT], 58 | ]; 59 | } 60 | 61 | /** 62 | * @dataProvider notAllowedRequestMethods 63 | */ 64 | public function testMethodNotAllowed($requestMethod) 65 | { 66 | $request = Request::create('', $requestMethod); 67 | 68 | $this->expectException(MethodNotAllowedHttpException::class); 69 | 70 | TestResponse::fromBaseResponse($this->handler->handle($request)); 71 | } 72 | 73 | public function testUploadWhenFileParameterIsEmpty() 74 | { 75 | $request = Request::create('', Request::METHOD_POST); 76 | 77 | $this->expectException(BadRequestHttpException::class); 78 | 79 | $this->handler->handle($request); 80 | } 81 | 82 | public function testUploadWhenFileParameterIsInvalid() 83 | { 84 | $file = new UploadedFile('', '', null, \UPLOAD_ERR_INI_SIZE); 85 | 86 | $request = Request::create('', Request::METHOD_POST, [], [], [ 87 | 'file' => $file, 88 | ]); 89 | 90 | $this->expectException(InternalServerErrorHttpException::class); 91 | 92 | $this->handler->handle($request); 93 | } 94 | 95 | public static function excludedPostParameterProvider() 96 | { 97 | return [ 98 | 'name' => ['name'], 99 | 'chunk' => ['chunk'], 100 | 'chunks' => ['chunks'], 101 | ]; 102 | } 103 | 104 | /** 105 | * @dataProvider excludedPostParameterProvider 106 | */ 107 | public function testPostParameterValidation($exclude) 108 | { 109 | $arr = [ 110 | 'name' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 111 | 'chunk' => 1, 112 | 'chunks' => 2, 113 | ]; 114 | 115 | unset($arr[$exclude]); 116 | 117 | $request = Request::create('', Request::METHOD_POST, $arr, [], [ 118 | 'file' => UploadedFile::fake() 119 | ->create('test.txt', 100), 120 | ]); 121 | 122 | $this->expectException(ValidationException::class); 123 | 124 | $this->handler->handle($request); 125 | } 126 | 127 | public function testUploadFirstChunk() 128 | { 129 | $file = UploadedFile::fake()->create('test.txt', 100); 130 | $request = Request::create('', Request::METHOD_POST, [ 131 | 'name' => 'test.txt', 132 | 'chunk' => '0', 133 | 'chunks' => '2', 134 | ], [], [ 135 | 'file' => $file, 136 | ]); 137 | 138 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 139 | $response->assertSuccessful(); 140 | $response->assertJson(['done' => 50]); 141 | 142 | Storage::disk('local')->assertExists('chunks/2_test.txt/0-1'); 143 | 144 | Event::assertNotDispatched(FileUploaded::class, function ($event) use ($file) { 145 | return $event->file = $file->hashName('merged'); 146 | }); 147 | } 148 | 149 | public function testUploadFirstChunkWithCallback() 150 | { 151 | $file = UploadedFile::fake()->create('test.txt', 100); 152 | $request = Request::create('', Request::METHOD_POST, [ 153 | 'name' => 'test.txt', 154 | 'chunk' => '0', 155 | 'chunks' => '2', 156 | ], [], [ 157 | 'file' => $file, 158 | ]); 159 | 160 | $callback = $this->createClosureMock($this->never()); 161 | 162 | $this->handler->handle($request, $callback); 163 | 164 | Event::assertNotDispatched(FileUploaded::class, function ($event) use ($file) { 165 | return $event->file = $file->hashName('merged'); 166 | }); 167 | } 168 | 169 | public function testUploadLastChunk() 170 | { 171 | $this->createFakeLocalFile('chunks/2_test.txt', '0-1'); 172 | 173 | $file = UploadedFile::fake()->create('test.txt', 100); 174 | $request = Request::create('', Request::METHOD_POST, [ 175 | 'name' => 'test.txt', 176 | 'chunk' => '1', 177 | 'chunks' => '2', 178 | ], [], [ 179 | 'file' => $file, 180 | ]); 181 | 182 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 183 | $response->assertSuccessful(); 184 | $response->assertJson(['done' => 100]); 185 | 186 | Storage::disk('local')->assertExists('chunks/2_test.txt/1-2'); 187 | Storage::disk('local')->assertExists($file->hashName('merged')); 188 | 189 | Event::assertDispatched(FileUploaded::class, function ($event) use ($file) { 190 | return $event->file = $file->hashName('merged'); 191 | }); 192 | } 193 | 194 | public function testUploadLastChunkWithCallback() 195 | { 196 | $this->createFakeLocalFile('chunks/2_test.txt', '0-1'); 197 | 198 | $file = UploadedFile::fake()->create('test.txt', 100); 199 | $request = Request::create('', Request::METHOD_POST, [ 200 | 'name' => 'test.txt', 201 | 'chunk' => '1', 202 | 'chunks' => '2', 203 | ], [], [ 204 | 'file' => $file, 205 | ]); 206 | 207 | $callback = $this->createClosureMock( 208 | $this->once(), 209 | 'local', 210 | $file->hashName('merged') 211 | ); 212 | 213 | $this->handler->handle($request, $callback); 214 | 215 | Event::assertDispatched(FileUploaded::class, function ($event) use ($file) { 216 | return $event->file = $file->hashName('merged'); 217 | }); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /tests/Driver/SimpleUploaderJsHandlerTest.php: -------------------------------------------------------------------------------- 1 | set('upload-handler.identifier', 'nop'); 32 | config()->set('upload-handler.handler', 'simple-uploader-js'); 33 | config()->set('upload-handler.sweep', false); 34 | $this->handler = app()->make(UploadHandler::class); 35 | 36 | Storage::fake('local'); 37 | Event::fake(); 38 | } 39 | 40 | public function testDriverInstance() 41 | { 42 | $manager = app()->make('upload-handler.upload-manager'); 43 | 44 | $this->assertInstanceOf(SimpleUploaderJsHandler::class, $manager->driver()); 45 | } 46 | 47 | public static function notAllowedRequestMethods() 48 | { 49 | return [ 50 | 'HEAD' => [Request::METHOD_HEAD], 51 | 'PUT' => [Request::METHOD_PUT], 52 | 'PATCH' => [Request::METHOD_PATCH], 53 | 'DELETE' => [Request::METHOD_DELETE], 54 | 'PURGE' => [Request::METHOD_PURGE], 55 | 'OPTIONS' => [Request::METHOD_OPTIONS], 56 | 'TRACE' => [Request::METHOD_TRACE], 57 | 'CONNECT' => [Request::METHOD_CONNECT], 58 | ]; 59 | } 60 | 61 | /** 62 | * @dataProvider notAllowedRequestMethods 63 | */ 64 | public function testMethodNotAllowed($requestMethod) 65 | { 66 | $request = Request::create('', $requestMethod); 67 | 68 | $this->expectException(MethodNotAllowedHttpException::class); 69 | 70 | TestResponse::fromBaseResponse($this->handler->handle($request)); 71 | } 72 | 73 | public function testResumeWhenChunkDoesNotExists() 74 | { 75 | $this->createFakeLocalFile('chunks/200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', '000-099'); 76 | 77 | $request = Request::create('', Request::METHOD_GET, [ 78 | 'chunkNumber' => 2, 79 | 'totalChunks' => 2, 80 | 'chunkSize' => 100, 81 | 'totalSize' => 200, 82 | 'identifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', 83 | 'filename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 84 | 'relativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 85 | 'currentChunkSize' => 100, 86 | ]); 87 | 88 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 89 | $response->assertStatus(Response::HTTP_NO_CONTENT); 90 | } 91 | 92 | public function testResume() 93 | { 94 | $this->createFakeLocalFile('chunks/200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', '000-099'); 95 | 96 | $request = Request::create('', Request::METHOD_GET, [ 97 | 'chunkNumber' => 1, 98 | 'totalChunks' => 2, 99 | 'chunkSize' => 100, 100 | 'totalSize' => 200, 101 | 'identifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', 102 | 'filename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 103 | 'relativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 104 | 'currentChunkSize' => 100, 105 | ]); 106 | 107 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 108 | $response->assertSuccessful(); 109 | } 110 | 111 | public function testUploadWhenFileParameterIsEmpty() 112 | { 113 | $request = Request::create('', Request::METHOD_POST); 114 | 115 | $this->expectException(BadRequestHttpException::class); 116 | 117 | $this->handler->handle($request); 118 | } 119 | 120 | public function testUploadWhenFileParameterIsInvalid() 121 | { 122 | $file = new UploadedFile('', '', null, \UPLOAD_ERR_INI_SIZE); 123 | 124 | $request = Request::create('', Request::METHOD_POST, [], [], [ 125 | 'file' => $file, 126 | ]); 127 | 128 | $this->expectException(InternalServerErrorHttpException::class); 129 | 130 | $this->handler->handle($request); 131 | } 132 | 133 | public static function excludedPostParameterProvider() 134 | { 135 | return [ 136 | 'chunkNumber' => ['chunkNumber'], 137 | 'totalChunks' => ['totalChunks'], 138 | 'chunkSize' => ['chunkSize'], 139 | 'totalSize' => ['totalSize'], 140 | 'identifier' => ['identifier'], 141 | 'filename' => ['filename'], 142 | 'relativePath' => ['relativePath'], 143 | 'currentChunkSize' => ['currentChunkSize'], 144 | ]; 145 | } 146 | 147 | /** 148 | * @dataProvider excludedPostParameterProvider 149 | */ 150 | public function testPostParameterValidation($exclude) 151 | { 152 | $arr = [ 153 | 'chunkNumber' => 1, 154 | 'totalChunks' => 2, 155 | 'chunkSize' => 100, 156 | 'totalSize' => 200, 157 | 'identifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', 158 | 'filename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 159 | 'relativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 160 | 'currentChunkSize' => 100, 161 | ]; 162 | 163 | unset($arr[$exclude]); 164 | 165 | $request = Request::create('', Request::METHOD_POST, $arr, [], [ 166 | 'file' => UploadedFile::fake() 167 | ->create('test.txt', 100), 168 | ]); 169 | 170 | $this->expectException(ValidationException::class); 171 | 172 | $this->handler->handle($request); 173 | } 174 | 175 | public function testUploadFirstChunk() 176 | { 177 | $file = UploadedFile::fake()->create('test.txt', 100); 178 | $request = Request::create('', Request::METHOD_POST, [ 179 | 'chunkNumber' => 1, 180 | 'totalChunks' => 2, 181 | 'chunkSize' => 100, 182 | 'totalSize' => 200, 183 | 'identifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', 184 | 'filename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 185 | 'relativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 186 | 'currentChunkSize' => 100, 187 | ], [], [ 188 | 'file' => $file, 189 | ]); 190 | 191 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 192 | $response->assertSuccessful(); 193 | $response->assertJson(['done' => 50]); 194 | 195 | Storage::disk('local')->assertExists('chunks/200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt/000-099'); 196 | 197 | Event::assertNotDispatched(FileUploaded::class, function ($event) use ($file) { 198 | return $event->file = $file->hashName('merged'); 199 | }); 200 | } 201 | 202 | public function testUploadFirstChunkWithCallback() 203 | { 204 | $file = UploadedFile::fake()->create('test.txt', 100); 205 | $request = Request::create('', Request::METHOD_POST, [ 206 | 'chunkNumber' => 1, 207 | 'totalChunks' => 2, 208 | 'chunkSize' => 100, 209 | 'totalSize' => 200, 210 | 'identifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', 211 | 'filename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 212 | 'relativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 213 | 'currentChunkSize' => 100, 214 | ], [], [ 215 | 'file' => $file, 216 | ]); 217 | 218 | $callback = $this->createClosureMock($this->never()); 219 | 220 | $this->handler->handle($request, $callback); 221 | 222 | Event::assertNotDispatched(FileUploaded::class, function ($event) use ($file) { 223 | return $event->file = $file->hashName('merged'); 224 | }); 225 | } 226 | 227 | public function testUploadLastChunk() 228 | { 229 | $this->createFakeLocalFile('chunks/200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', '000-099'); 230 | 231 | $file = UploadedFile::fake()->create('test.txt', 100); 232 | $request = Request::create('', Request::METHOD_POST, [ 233 | 'chunkNumber' => 2, 234 | 'totalChunks' => 2, 235 | 'chunkSize' => 100, 236 | 'totalSize' => 200, 237 | 'identifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', 238 | 'filename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 239 | 'relativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 240 | 'currentChunkSize' => 100, 241 | ], [], [ 242 | 'file' => $file, 243 | ]); 244 | 245 | $response = TestResponse::fromBaseResponse($this->handler->handle($request)); 246 | $response->assertSuccessful(); 247 | $response->assertJson(['done' => 100]); 248 | 249 | Storage::disk('local')->assertExists('chunks/200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt/100-199'); 250 | Storage::disk('local')->assertExists($file->hashName('merged')); 251 | 252 | Event::assertDispatched(FileUploaded::class, function ($event) use ($file) { 253 | return $event->file = $file->hashName('merged'); 254 | }); 255 | } 256 | 257 | public function testUploadLastChunkWithCallback() 258 | { 259 | $this->createFakeLocalFile('chunks/200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', '000-099'); 260 | 261 | $file = UploadedFile::fake()->create('test.txt', 100); 262 | $request = Request::create('', Request::METHOD_POST, [ 263 | 'chunkNumber' => 2, 264 | 'totalChunks' => 2, 265 | 'chunkSize' => 100, 266 | 'totalSize' => 200, 267 | 'identifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', 268 | 'filename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 269 | 'relativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', 270 | 'currentChunkSize' => 100, 271 | ], [], [ 272 | 'file' => $file, 273 | ]); 274 | 275 | $callback = $this->createClosureMock( 276 | $this->once(), 277 | 'local', 278 | $file->hashName('merged') 279 | ); 280 | 281 | $this->handler->handle($request, $callback); 282 | 283 | Event::assertDispatched(FileUploaded::class, function ($event) use ($file) { 284 | return $event->file = $file->hashName('merged'); 285 | }); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /tests/Identifier/AuthIdentifierTest.php: -------------------------------------------------------------------------------- 1 | andReturn(100); 23 | 24 | $this->identifier = new AuthIdentifier(); 25 | } 26 | 27 | protected function tearDown(): void 28 | { 29 | parent::tearDown(); 30 | 31 | Auth::clearResolvedInstances(); 32 | } 33 | 34 | public function testGenerateIdentifierThrowsUnauthorizedException() 35 | { 36 | Auth::shouldReceive('check') 37 | ->andReturn(false); 38 | 39 | $this->expectException(UnauthorizedException::class); 40 | $this->identifier->generateIdentifier('any_string'); 41 | } 42 | 43 | public function testGenerateIdentifier() 44 | { 45 | Auth::shouldReceive('check') 46 | ->andReturn(true); 47 | 48 | $identifier = $this->identifier->generateIdentifier('any_string'); 49 | $this->assertEquals('2b2ea43a7652e1f7925c588b9ae7a31f09be3bf9', $identifier); 50 | } 51 | 52 | public function testUploadedFileIdentifierName() 53 | { 54 | Auth::shouldReceive('check') 55 | ->andReturn(true); 56 | 57 | $identifier = $this->identifier->generateFileIdentifier(200, 'any_filename.ext'); 58 | $this->assertEquals('4317e3d56e27deda5bd84dd35830bff799736257', $identifier); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Identifier/NopIdentifierTest.php: -------------------------------------------------------------------------------- 1 | identifier = new NopIdentifier(); 20 | } 21 | 22 | public function testGenerateIdentifier() 23 | { 24 | $identifier = $this->identifier->generateIdentifier('any_string'); 25 | $this->assertEquals('any_string', $identifier); 26 | } 27 | 28 | public function testUploadedFileIdentifierName() 29 | { 30 | $identifier = $this->identifier->generateFileIdentifier(200, 'any_filename.ext'); 31 | $this->assertEquals('200_any_filename.ext', $identifier); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Identifier/SessionIdentifierTest.php: -------------------------------------------------------------------------------- 1 | andReturn('frgYt7cPmNGtORpRCo4xvFIrWklzFqc2mnO6EE6b'); 22 | 23 | $this->identifier = new SessionIdentifier(); 24 | } 25 | 26 | public function testGenerateIdentifier() 27 | { 28 | $identifier = $this->identifier->generateIdentifier('any_string'); 29 | $this->assertEquals('b41d07049729f460973494395f9bf8fe23834d48', $identifier); 30 | } 31 | 32 | public function testUploadedFileIdentifierName() 33 | { 34 | $identifier = $this->identifier->generateFileIdentifier(200, 'any_filename.ext'); 35 | $this->assertEquals('ec1669bf4dee72e6dd30b94d2d29413601f1b69b', $identifier); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/IdentityManagerTest.php: -------------------------------------------------------------------------------- 1 | manager = new IdentityManager($this->app); 25 | } 26 | 27 | public static function availableDrivers() 28 | { 29 | return [ 30 | 'auth' => ['auth', AuthIdentifier::class], 31 | 'nop' => ['nop', NopIdentifier::class], 32 | 'session' => ['session', SessionIdentifier::class], 33 | ]; 34 | } 35 | 36 | /** 37 | * @dataProvider availableDrivers 38 | */ 39 | public function testDriverCreation($driverName, $expectedInstanceOf) 40 | { 41 | $driver = $this->manager->driver($driverName); 42 | $this->assertInstanceOf($expectedInstanceOf, $driver); 43 | } 44 | 45 | public function testDefaultDriver() 46 | { 47 | $driver = $this->manager->driver(); 48 | $this->assertInstanceOf(SessionIdentifier::class, $driver); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/Range/ContentRangeTest.php: -------------------------------------------------------------------------------- 1 | [null, 'Content Range header is missing or invalid'], 18 | 'Empty string' => ['', 'Content Range header is missing or invalid'], 19 | 'Invalid string' => ['invalid string', 'Content Range header is missing or invalid'], 20 | 'End greater than start' => ['bytes 40-39/200', 'Range end must be greater than or equal to range start'], 21 | 'Total equal to end' => ['bytes 40-49/49', 'Size must be greater than range end'], 22 | 'Total greater than end' => ['bytes 40-49/48', 'Size must be greater than range end'], 23 | ]; 24 | } 25 | 26 | /** 27 | * @dataProvider invalidArgumentProvider 28 | * 29 | * @param $contentRange 30 | * @param $expectedExceptionMessage 31 | */ 32 | public function testArgumentValidation($contentRange, $expectedExceptionMessage) 33 | { 34 | $this->expectException(InvalidArgumentException::class); 35 | $this->expectExceptionMessage($expectedExceptionMessage); 36 | 37 | new ContentRange($contentRange); 38 | } 39 | 40 | public function testRequestEntityTooLargeHttpException() 41 | { 42 | $this->expectException(RequestEntityTooLargeHttpException::class); 43 | $this->expectExceptionMessage('The content range value is too large'); 44 | 45 | new ContentRange(sprintf('bytes 40-49/%s', str_repeat('9', 350))); 46 | } 47 | 48 | public function testIsFirst() 49 | { 50 | $range = new ContentRange('bytes 0-9/200'); 51 | $this->assertTrue($range->isFirst()); 52 | } 53 | 54 | public function testIsLast() 55 | { 56 | $range = new ContentRange('bytes 190-199/200'); 57 | $this->assertTrue($range->isLast()); 58 | } 59 | 60 | public function testIsFirstAndIsLast() 61 | { 62 | $range = new ContentRange('bytes 0-9/10'); 63 | $this->assertTrue($range->isFirst()); 64 | $this->assertTrue($range->isLast()); 65 | } 66 | 67 | public function testGetTotal() 68 | { 69 | $range = new ContentRange('bytes 40-49/200'); 70 | $this->assertEquals(200, $range->getTotal()); 71 | } 72 | 73 | public function testGetStart() 74 | { 75 | $range = new ContentRange('bytes 40-49/200'); 76 | $this->assertEquals(40, $range->getStart()); 77 | } 78 | 79 | public function testGetEnd() 80 | { 81 | $range = new ContentRange('bytes 40-49/200'); 82 | $this->assertEquals(49, $range->getEnd()); 83 | } 84 | 85 | public function testGetPercentage() 86 | { 87 | $range = new ContentRange('bytes 40-49/200'); 88 | $this->assertEquals(25, $range->getPercentage()); 89 | } 90 | 91 | public function testCreateFromHeaderBag() 92 | { 93 | $range = new ContentRange(new HeaderBag([ 94 | 'Content-Range' => 'bytes 40-49/200', 95 | ])); 96 | 97 | $this->assertEquals(200, $range->getTotal()); 98 | $this->assertEquals(40, $range->getStart()); 99 | $this->assertEquals(49, $range->getEnd()); 100 | $this->assertEquals(25, $range->getPercentage()); 101 | } 102 | 103 | public function testCreateFromRequest() 104 | { 105 | $request = new Request(); 106 | $request->headers->set('content-range', 'bytes 40-49/200'); 107 | 108 | $range = new ContentRange($request); 109 | 110 | $this->assertEquals(200, $range->getTotal()); 111 | $this->assertEquals(40, $range->getStart()); 112 | $this->assertEquals(49, $range->getEnd()); 113 | $this->assertEquals(25, $range->getPercentage()); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tests/Range/DropzoneRangeTest.php: -------------------------------------------------------------------------------- 1 | [4, 0, 20, 190, '`numberOfChunks` must be greater than zero'], 17 | 'Number of chunks size smaller than zero' => [4, -1, 20, 190, '`numberOfChunks` must be greater than zero'], 18 | 'Index smaller than zero' => [-1, 10, 20, 190, '`index` must be greater than or equal to zero'], 19 | 'Index equal to the number of chunks' => [10, 10, 20, 190, '`index` must be smaller than `numberOfChunks`'], 20 | 'Index greater than the number of chunks' => [14, 10, 20, 190, '`index` must be smaller than `numberOfChunks`'], 21 | 'Chunk size equal to zero' => [4, 10, 0, 190, '`chunkSize` must be greater than zero'], 22 | 'Chunk size smaller than zero' => [4, 10, -1, 190, '`chunkSize` must be greater than zero'], 23 | 'Total size equal to zero' => [4, 10, 20, 0, '`totalSize` must be greater than zero'], 24 | 'Total size smaller than zero' => [4, 10, 20, -1, '`totalSize` must be greater than zero'], 25 | 'Total size too small' => [4, 10, 20, 80, '`totalSize` must be greater than the multiple of `chunkSize` and `index`'], 26 | 'Total size too big' => [4, 10, 20, 201, '`totalSize` must be smaller than or equal to the multiple of `chunkSize` and `numberOfChunks`'], 27 | ]; 28 | } 29 | 30 | /** 31 | * @dataProvider invalidArgumentProvider 32 | * 33 | * @param $index 34 | * @param $numberOfChunks 35 | * @param $chunkSize 36 | * @param $totalSize 37 | * @param $expectedExceptionMessage 38 | */ 39 | public function testArgumentValidation($index, $numberOfChunks, $chunkSize, $totalSize, $expectedExceptionMessage) 40 | { 41 | $this->expectException(InvalidArgumentException::class); 42 | $this->expectExceptionMessage($expectedExceptionMessage); 43 | 44 | $this->createRequestBodyRange($index, $numberOfChunks, $chunkSize, $totalSize); 45 | } 46 | 47 | public function testIsFirst() 48 | { 49 | $range = $this->createRequestBodyRange(0, 2, 1, 2); 50 | $this->assertTrue($range->isFirst()); 51 | 52 | $range = $this->createRequestBodyRange(1, 2, 1, 2); 53 | $this->assertFalse($range->isFirst()); 54 | } 55 | 56 | public function testIsLast() 57 | { 58 | $range = $this->createRequestBodyRange(1, 2, 1, 2); 59 | $this->assertTrue($range->isLast()); 60 | 61 | $range = $this->createRequestBodyRange(0, 2, 1, 2); 62 | $this->assertFalse($range->isLast()); 63 | } 64 | 65 | public function testIsFirstAndIsLast() 66 | { 67 | $range = $this->createRequestBodyRange(0, 1, 1, 1); 68 | $this->assertTrue($range->isLast()); 69 | $this->assertTrue($range->isLast()); 70 | } 71 | 72 | public function testGetTotal() 73 | { 74 | $range = $this->createRequestBodyRange(4, 10, 20, 190); 75 | $this->assertEquals(190, $range->getTotal()); 76 | } 77 | 78 | public function testGetStart() 79 | { 80 | $range = $this->createRequestBodyRange(4, 10, 20, 190); 81 | $this->assertEquals(80, $range->getStart()); 82 | } 83 | 84 | public function testGetEnd() 85 | { 86 | $range = $this->createRequestBodyRange(4, 10, 20, 190); 87 | $this->assertEquals(99, $range->getEnd()); 88 | 89 | $range = $this->createRequestBodyRange(9, 10, 20, 190); 90 | $this->assertEquals(189, $range->getEnd()); 91 | } 92 | 93 | public function testGetPercentage() 94 | { 95 | $range = $this->createRequestBodyRange(4, 10, 20, 190); 96 | $this->assertEquals(100, $range->getPercentage(range(0, 9))); 97 | 98 | $range = $this->createRequestBodyRange(4, 10, 20, 190); 99 | $this->assertEquals(90, $range->getPercentage(range(0, 8))); 100 | } 101 | 102 | public function testIsFinished() 103 | { 104 | $range = $this->createRequestBodyRange(4, 10, 20, 190); 105 | $this->assertTrue($range->isFinished(range(0, 9))); 106 | 107 | $range = $this->createRequestBodyRange(4, 10, 20, 190); 108 | $this->assertFalse($range->isFinished(range(0, 8))); 109 | } 110 | 111 | public function testCreateFromRequest() 112 | { 113 | $request = new Request([], [ 114 | 'index' => 4, 115 | 'numberOfChunks' => 10, 116 | 'chunkSize' => 20, 117 | 'totalSize' => 190, 118 | ]); 119 | 120 | $range = new DropzoneRange($request, 'index', 'numberOfChunks', 'chunkSize', 'totalSize'); 121 | 122 | $this->assertEquals(80, $range->getStart()); 123 | $this->assertEquals(99, $range->getEnd()); 124 | $this->assertEquals(190, $range->getTotal()); 125 | } 126 | 127 | /** 128 | * @param int $index 129 | * @param int $numberOfChunks 130 | * @param int $chunkSize 131 | * @param float $totalSize 132 | * 133 | * @return \CodingSocks\UploadHandler\Range\DropzoneRange 134 | */ 135 | private function createRequestBodyRange(int $index, int $numberOfChunks, int $chunkSize, float $totalSize) 136 | { 137 | $request = new ParameterBag([ 138 | 'index' => (string) $index, 139 | 'numberOfChunks' => (string) $numberOfChunks, 140 | 'chunkSize' => (string) $chunkSize, 141 | 'totalSize' => (string) $totalSize, 142 | ]); 143 | 144 | return new DropzoneRange($request, 'index', 'numberOfChunks', 'chunkSize', 'totalSize'); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /tests/Range/NgFileUploadRangeTest.php: -------------------------------------------------------------------------------- 1 | [-1, 10, 10, 100, '`_chunkNumber` must be greater than or equal to zero'], 17 | 'Chunk size less than one' => [0, 0, 10, 100, '`_chunkSize` must be greater than zero'], 18 | 'Current chunk size less than one' => [0, 10, 0, 100, '`_currentChunkSize` must be greater than zero'], 19 | 'Total size less than one' => [0, 10, 10, 0, '`_totalSize` must be greater than zero'], 20 | ]; 21 | } 22 | 23 | /** 24 | * @dataProvider invalidArgumentProvider 25 | * 26 | * @param $chunkNumber 27 | * @param $chunkSize 28 | * @param $currentChunkSize 29 | * @param $totalSize 30 | * @param $expectedExceptionMessage 31 | */ 32 | public function testArgumentValidation($chunkNumber, $chunkSize, $currentChunkSize, $totalSize, $expectedExceptionMessage) 33 | { 34 | $this->expectException(InvalidArgumentException::class); 35 | $this->expectExceptionMessage($expectedExceptionMessage); 36 | 37 | $this->createRequestBodyRange($chunkNumber, $chunkSize, $currentChunkSize, $totalSize); 38 | } 39 | 40 | public function testIsFirst() 41 | { 42 | $range = $this->createRequestBodyRange(0, 10, 10, 30); 43 | $this->assertTrue($range->isFirst()); 44 | 45 | $range = $this->createRequestBodyRange(1, 10, 10, 30); 46 | $this->assertFalse($range->isFirst()); 47 | } 48 | 49 | public function testIsLast() 50 | { 51 | $range = $this->createRequestBodyRange(2, 10, 10, 30); 52 | $this->assertTrue($range->isLast()); 53 | 54 | $range = $this->createRequestBodyRange(1, 10, 10, 30); 55 | $this->assertFalse($range->isLast()); 56 | } 57 | 58 | public function testIsFirstAndIsLast() 59 | { 60 | $range = $this->createRequestBodyRange(0, 10, 10, 10); 61 | $this->assertTrue($range->isLast()); 62 | $this->assertTrue($range->isLast()); 63 | } 64 | 65 | public function testGetTotal() 66 | { 67 | $range = $this->createRequestBodyRange(4, 10, 10, 190); 68 | $this->assertEquals(190, $range->getTotal()); 69 | } 70 | 71 | public function testGetStart() 72 | { 73 | $range = $this->createRequestBodyRange(4, 10, 10, 190); 74 | $this->assertEquals(40, $range->getStart()); 75 | } 76 | 77 | public function testGetEnd() 78 | { 79 | $range = $this->createRequestBodyRange(4, 10, 10, 190); 80 | $this->assertEquals(49, $range->getEnd()); 81 | } 82 | 83 | public function testGetPercentage() 84 | { 85 | $range = $this->createRequestBodyRange(4, 10, 10, 100); 86 | $this->assertEquals(50, $range->getPercentage()); 87 | 88 | $range = $this->createRequestBodyRange(9, 10, 10, 100); 89 | $this->assertEquals(100, $range->getPercentage()); 90 | } 91 | 92 | public function testCreateFromRequest() 93 | { 94 | $request = new Request([], [ 95 | '_chunkNumber' => (string) 5, 96 | '_chunkSize' => (string) 10, 97 | '_currentChunkSize' => (string) 10, 98 | '_totalSize' => (string) 100, 99 | ]); 100 | 101 | $range = new NgFileUploadRange($request); 102 | 103 | $this->assertEquals(50, $range->getStart()); 104 | $this->assertEquals(59, $range->getEnd()); 105 | $this->assertEquals(100, $range->getTotal()); 106 | } 107 | 108 | /** 109 | * @param int $chunkNumber 110 | * @param int $chunkSize 111 | * @param int $currentChunkSize 112 | * @param float $totalSize 113 | * 114 | * @return \CodingSocks\UploadHandler\Range\NgFileUploadRange 115 | */ 116 | private function createRequestBodyRange(int $chunkNumber, int $chunkSize, int $currentChunkSize, float $totalSize) 117 | { 118 | $request = new ParameterBag([ 119 | '_chunkNumber' => (string) $chunkNumber, 120 | '_chunkSize' => (string) $chunkSize, 121 | '_currentChunkSize' => (string) $currentChunkSize, 122 | '_totalSize' => (string) $totalSize, 123 | ]); 124 | 125 | return new NgFileUploadRange($request); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /tests/Range/PluploadRangeTest.php: -------------------------------------------------------------------------------- 1 | [-1, 10, '`chunk` must be greater than or equal to zero'], 17 | 'Number of chunks size smaller than zero' => [0, 0, '`chunks` must be greater than zero'], 18 | 'Index smaller than zero' => [10, 10, '`chunk` must be less than `chunks`'], 19 | ]; 20 | } 21 | 22 | /** 23 | * @dataProvider invalidArgumentProvider 24 | * 25 | * @param $chunk 26 | * @param $chunks 27 | * @param $expectedExceptionMessage 28 | */ 29 | public function testArgumentValidation($chunk, $chunks, $expectedExceptionMessage) 30 | { 31 | $this->expectException(InvalidArgumentException::class); 32 | $this->expectExceptionMessage($expectedExceptionMessage); 33 | 34 | $this->createRequestBodyRange($chunk, $chunks); 35 | } 36 | 37 | public function testIsFirst() 38 | { 39 | $range = $this->createRequestBodyRange(0, 20); 40 | $this->assertTrue($range->isFirst()); 41 | } 42 | 43 | public function testIsLast() 44 | { 45 | $range = $this->createRequestBodyRange(19, 20); 46 | $this->assertTrue($range->isLast()); 47 | } 48 | 49 | public function testIsFirstAndIsLast() 50 | { 51 | $range = $this->createRequestBodyRange(0, 1); 52 | $this->assertTrue($range->isFirst()); 53 | $this->assertTrue($range->isLast()); 54 | } 55 | 56 | public function testGetTotal() 57 | { 58 | $range = $this->createRequestBodyRange(4, 20); 59 | $this->assertEquals(20, $range->getTotal()); 60 | } 61 | 62 | public function testGetStart() 63 | { 64 | $range = $this->createRequestBodyRange(4, 20); 65 | $this->assertEquals(4, $range->getStart()); 66 | } 67 | 68 | public function testGetEnd() 69 | { 70 | $range = $this->createRequestBodyRange(4, 20); 71 | $this->assertEquals(5, $range->getEnd()); 72 | } 73 | 74 | public function testGetPercentage() 75 | { 76 | $range = $this->createRequestBodyRange(4, 20); 77 | $this->assertEquals(25, $range->getPercentage()); 78 | } 79 | 80 | public function testCreateFromRequest() 81 | { 82 | $request = new Request([], [ 83 | 'chunk' => 4, 84 | 'chunks' => 10, 85 | ]); 86 | 87 | $range = new PluploadRange($request); 88 | 89 | $this->assertEquals(4, $range->getStart()); 90 | $this->assertEquals(5, $range->getEnd()); 91 | $this->assertEquals(10, $range->getTotal()); 92 | } 93 | 94 | /** 95 | * @param float $chunk 96 | * @param float $chunks 97 | * 98 | * @return \CodingSocks\UploadHandler\Range\PluploadRange 99 | */ 100 | private function createRequestBodyRange(float $chunk, float $chunks) 101 | { 102 | $request = new ParameterBag([ 103 | 'chunk' => (string) $chunk, 104 | 'chunks' => (string) $chunks, 105 | ]); 106 | 107 | return new PluploadRange($request); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/Range/ResumableJsRangeTest.php: -------------------------------------------------------------------------------- 1 | [5, 0, 20, 190, '`numberOfChunks` must be greater than zero'], 17 | 'Number of chunks size smaller than zero' => [5, -1, 20, 190, '`numberOfChunks` must be greater than zero'], 18 | 'Index smaller than zero' => [0, 10, 20, 190, '`index` must be greater than'], 19 | 'Index greater than the number of chunks' => [15, 10, 20, 190, '`index` must be smaller than or equal to `numberOfChunks`'], 20 | 'Chunk size equal to zero' => [5, 10, 0, 190, '`chunkSize` must be greater than zero'], 21 | 'Chunk size smaller than zero' => [5, 10, -1, 190, '`chunkSize` must be greater than zero'], 22 | 'Total size equal to zero' => [5, 10, 20, 0, '`totalSize` must be greater than zero'], 23 | 'Total size smaller than zero' => [5, 10, 20, -1, '`totalSize` must be greater than zero'], 24 | 'Total size too small' => [5, 10, 20, 80, '`totalSize` must be greater than or equal to the multiple of `chunkSize` and `index`'], 25 | 'Total size too big' => [5, 10, 20, 201, '`totalSize` must be smaller than or equal to the multiple of `chunkSize` and `numberOfChunks`'], 26 | ]; 27 | } 28 | 29 | /** 30 | * @dataProvider invalidArgumentProvider 31 | * 32 | * @param $index 33 | * @param $numberOfChunks 34 | * @param $chunkSize 35 | * @param $totalSize 36 | * @param $expectedExceptionMessage 37 | */ 38 | public function testArgumentValidation($index, $numberOfChunks, $chunkSize, $totalSize, $expectedExceptionMessage) 39 | { 40 | $this->expectException(InvalidArgumentException::class); 41 | $this->expectExceptionMessage($expectedExceptionMessage); 42 | 43 | $this->createRequestBodyRange($index, $numberOfChunks, $chunkSize, $totalSize); 44 | } 45 | 46 | public function testIsFirst() 47 | { 48 | $range = $this->createRequestBodyRange(1, 2, 1, 2); 49 | $this->assertTrue($range->isFirst()); 50 | 51 | $range = $this->createRequestBodyRange(2, 2, 1, 2); 52 | $this->assertFalse($range->isFirst()); 53 | } 54 | 55 | public function testIsLast() 56 | { 57 | $range = $this->createRequestBodyRange(2, 2, 1, 2); 58 | $this->assertTrue($range->isLast()); 59 | 60 | $range = $this->createRequestBodyRange(1, 2, 1, 2); 61 | $this->assertFalse($range->isLast()); 62 | } 63 | 64 | public function testIsFirstAndIsLast() 65 | { 66 | $range = $this->createRequestBodyRange(1, 1, 1, 1); 67 | $this->assertTrue($range->isLast()); 68 | $this->assertTrue($range->isLast()); 69 | } 70 | 71 | public function testGetTotal() 72 | { 73 | $range = $this->createRequestBodyRange(4, 10, 20, 190); 74 | $this->assertEquals(190, $range->getTotal()); 75 | } 76 | 77 | public function testGetStart() 78 | { 79 | $range = $this->createRequestBodyRange(5, 10, 20, 190); 80 | $this->assertEquals(80, $range->getStart()); 81 | } 82 | 83 | public function testGetEnd() 84 | { 85 | $range = $this->createRequestBodyRange(5, 10, 20, 190); 86 | $this->assertEquals(99, $range->getEnd()); 87 | 88 | $range = $this->createRequestBodyRange(10, 10, 20, 190); 89 | $this->assertEquals(189, $range->getEnd()); 90 | } 91 | 92 | public function testGetPercentage() 93 | { 94 | $range = $this->createRequestBodyRange(4, 10, 20, 190); 95 | $this->assertEquals(100, $range->getPercentage(range(0, 9))); 96 | 97 | $range = $this->createRequestBodyRange(4, 10, 20, 190); 98 | $this->assertEquals(90, $range->getPercentage(range(0, 8))); 99 | } 100 | 101 | public function testIsFinished() 102 | { 103 | $range = $this->createRequestBodyRange(4, 10, 20, 190); 104 | $this->assertTrue($range->isFinished(range(0, 9))); 105 | 106 | $range = $this->createRequestBodyRange(4, 10, 20, 190); 107 | $this->assertFalse($range->isFinished(range(0, 8))); 108 | } 109 | 110 | public function testCreateFromRequest() 111 | { 112 | $request = new Request([], [ 113 | 'index' => 5, 114 | 'numberOfChunks' => 10, 115 | 'chunkSize' => 20, 116 | 'totalSize' => 190, 117 | ]); 118 | 119 | $range = new ResumableJsRange($request, 'index', 'numberOfChunks', 'chunkSize', 'totalSize'); 120 | 121 | $this->assertEquals(80, $range->getStart()); 122 | $this->assertEquals(99, $range->getEnd()); 123 | $this->assertEquals(190, $range->getTotal()); 124 | } 125 | 126 | /** 127 | * @param int $index 128 | * @param int $numberOfChunks 129 | * @param int $chunkSize 130 | * @param float $totalSize 131 | * 132 | * @return \CodingSocks\UploadHandler\Range\ResumableJsRange 133 | */ 134 | private function createRequestBodyRange(int $index, int $numberOfChunks, int $chunkSize, float $totalSize) 135 | { 136 | $request = new ParameterBag([ 137 | 'index' => (string) $index, 138 | 'numberOfChunks' => (string) $numberOfChunks, 139 | 'chunkSize' => (string) $chunkSize, 140 | 'totalSize' => (string) $totalSize, 141 | ]); 142 | 143 | return new ResumableJsRange($request, 'index', 'numberOfChunks', 'chunkSize', 'totalSize'); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /tests/Response/PercentageJsonResponseTest.php: -------------------------------------------------------------------------------- 1 | 21]], 19 | [50, ['done' => 50]], 20 | [73, ['done' => 73]], 21 | [100, ['done' => 100]], 22 | ]; 23 | } 24 | 25 | /** 26 | * @dataProvider percentageProvider 27 | * 28 | * @param int $percentage 29 | * @param array $expectedContent 30 | */ 31 | public function testContent(int $percentage, array $expectedContent) 32 | { 33 | if (class_exists('\Illuminate\Testing\TestResponse')) { 34 | $response = TestResponse::fromBaseResponse(new PercentageJsonResponse($percentage)); 35 | } else { 36 | $response = TestResponse::fromBaseResponse(new PercentageJsonResponse($percentage)); 37 | } 38 | 39 | $response->assertSuccessful(); 40 | $response->assertStatus(Response::HTTP_OK); 41 | $response->assertExactJson($expectedContent); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | create($name); 27 | $file->storeAs($path, $name, [ 28 | 'disk' => 'local', 29 | ]); 30 | } 31 | 32 | /** 33 | * https://github.com/sebastianbergmann/phpunit-mock-objects/issues/257 34 | * 35 | * @param $expects 36 | * @param mixed ...$arguments 37 | * 38 | * @return \Closure 39 | */ 40 | protected function createClosureMock($expects, ...$arguments) 41 | { 42 | /** @var \Closure|\PHPUnit\Framework\MockObject\MockObject $callback */ 43 | $callback = $this->getMockBuilder(\stdClass::class) 44 | ->addMethods(['__invoke']) 45 | ->getMock(); 46 | $callback->expects($expects) 47 | ->method('__invoke') 48 | ->with(...$arguments); 49 | 50 | return function () use ($callback) { 51 | return $callback(...func_get_args()); 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/UploadManagerTest.php: -------------------------------------------------------------------------------- 1 | manager = new UploadManager($this->app); 30 | } 31 | 32 | public static function availableDrivers() 33 | { 34 | return [ 35 | 'monolith' => ['monolith', MonolithHandler::class], 36 | 'blueimp' => ['blueimp', BlueimpHandler::class], 37 | 'dropzone' => ['dropzone', DropzoneHandler::class], 38 | 'flow-js' => ['flow-js', FlowJsHandler::class], 39 | 'ng-file-upload' => ['ng-file-upload', NgFileHandler::class], 40 | 'plupload' => ['plupload', PluploadHandler::class], 41 | 'resumable-js' => ['resumable-js', ResumableJsHandler::class], 42 | 'simple-uploader-js' => ['simple-uploader-js', SimpleUploaderJsHandler::class], 43 | ]; 44 | } 45 | 46 | /** 47 | * @dataProvider availableDrivers 48 | * 49 | * @param $driverName 50 | * @param $expectedInstanceOf 51 | */ 52 | public function testDriverCreation($driverName, $expectedInstanceOf) 53 | { 54 | $driver = $this->manager->driver($driverName); 55 | $this->assertInstanceOf($expectedInstanceOf, $driver); 56 | } 57 | 58 | public function testDefaultDriver() 59 | { 60 | $driver = $this->manager->driver(); 61 | $this->assertInstanceOf(MonolithHandler::class, $driver); 62 | } 63 | } 64 | --------------------------------------------------------------------------------