├── .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 | [](https://github.com/coding-socks/laravel-upload-handler/actions?query=workflow%3A"run-tests")
6 | [](https://packagist.org/packages/coding-socks/laravel-upload-handler)
7 | [](https://packagist.org/packages/coding-socks/laravel-upload-handler)
8 | [](https://scrutinizer-ci.com/g/coding-socks/laravel-upload-handler/)
9 | [](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 |
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 |
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 |
16 |
17 |
18 | Your browser doesn't have HTML5 support.
19 |
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 |
--------------------------------------------------------------------------------