├── CONTRIBUTING.md ├── LICENSE.md ├── composer.json ├── config └── chunk-upload.php ├── readme.md └── src ├── ChunkFile.php ├── Commands └── ClearChunksCommand.php ├── Config ├── AbstractConfig.php └── FileConfig.php ├── Exceptions ├── ChunkInvalidValueException.php ├── ChunkSaveException.php ├── ContentRangeValueToLargeException.php ├── MissingChunkFilesException.php ├── UploadFailedException.php └── UploadMissingFileException.php ├── FileMerger.php ├── Handler ├── AbstractHandler.php ├── ChunksInRequestSimpleUploadHandler.php ├── ChunksInRequestUploadHandler.php ├── ContentRangeUploadHandler.php ├── DropZoneUploadHandler.php ├── HandlerFactory.php ├── NgFileUploadHandler.php ├── ResumableJSUploadHandler.php ├── SingleUploadHandler.php └── Traits │ └── HandleParallelUploadTrait.php ├── Providers └── ChunkUploadServiceProvider.php ├── Receiver └── FileReceiver.php ├── Save ├── AbstractSave.php ├── ChunkSave.php ├── ParallelSave.php └── SingleSave.php └── Storage └── ChunkStorage.php /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | - [Pull Requests](#pull-requests) 4 | - [Adding new library](#adding-new-library) 5 | - [Your additions to your code base](#your-additions-to-your-code-base) 6 | 7 | We welcome contributions to our project. If you want to add a new provider, follow these steps: 8 | 9 | 1. **Fork the Project:** Begin by forking the project to your own GitHub account. 10 | 2. **Create a Feature Branch:** Create a new branch for your bug fix or feature implementation. Ensure your code is well-commented. 11 | 3. **Commit and Push Changes:** Make your changes, including any necessary tests, and commit them to your branch. Then, push your changes to your forked repository. 12 | 4. **Submit a Pull Request:** Once your changes are ready, submit a pull request to merge them into the main project's `master` branch. 13 | 5. **Test Your Code:** Before submitting your pull request, ensure that your code works properly by testing it in the [laravel-chunk-upload-example](https://github.com/pionl/laravel-chunk-upload-example) project. 14 | 6. **Debugging Assistance:** If you encounter any issues, consider using XDEBUG for debugging purposes. 15 | 16 | ## Pull Requests 17 | 18 | When submitting pull requests, please adhere to the following guidelines: 19 | 20 | - **Coding Standards:** Follow the [PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md). You can easily apply these conventions using `composer run lint:fix`. 21 | 22 | - **Release Cycle:** Consider our release cycle, aiming to follow [SemVer v2.0.0](http://semver.org/). 23 | 24 | - **Document Changes:** Document any changes in behavior thoroughly, ensuring that the `README.md` and other relevant documentation are updated accordingly. 25 | 26 | - **Feature Branches:** Create feature branches for your pull requests rather than requesting to pull from your master branch directly. 27 | 28 | - **Single Feature per Pull Request:** Submit one pull request per feature. If you're implementing multiple features, send separate pull requests for each. 29 | 30 | Before submitting your pull request: 31 | 32 | 1. **Rebase Changes:** Rebase your changes on the master branch to ensure a clean commit history. 33 | 2. **Lint Project:** Check for any coding standard violations using `composer run lint`. 34 | 3. **Run Tests:** Ensure that all tests pass by running `composer run test`. 35 | 4. **Write Tests (Recommended):** If possible, write tests to cover your code changes. 36 | 5. **Rebase Commits (Optional):** Consider rebasing your commits to keep them concise and relevant. 37 | 38 | Thank you for your contributions! 39 | 40 | # Adding new library 41 | 42 | The `AbstractHandler` class provides a foundation for implementing custom detection of chunk mode and file naming. While it's stored in the Handler namespace by default, you can place your handler in any namespace and pass the class to the `FileReceiver` as a parameter. 43 | 44 | ### You Must Implement: 45 | 46 | - `getChunkFileName()`: Returns the chunk file name for storing the temporary file. 47 | - `isFirstChunk()`: Checks if the request contains the first chunk. 48 | - `isLastChunk()`: Checks if the current request contains the last chunk. 49 | - `isChunkedUpload()`: Checks if the current request is a chunked upload. 50 | - `getPercentageDone()`: Calculates the current upload percentage. 51 | 52 | ### Automatic Detection 53 | 54 | To enable your own detection, simply override the `canBeUsedForRequest` method: 55 | 56 | ```php 57 | public static function canBeUsedForRequest(Request $request) 58 | { 59 | return true; 60 | } 61 | ``` 62 | 63 | # Your additions to your code base 64 | 65 | If you wish to contribute without forking, follow these steps: 66 | 67 | 1. **Edit HandlerFactory:** Add your handler to the `$handlers` array. 68 | 69 | 2. **Runtime Usage:** Call `HandlerFactory::register($name)` at runtime to register your custom Handler and utilize it. 70 | 71 | Feel free to contribute and thank you for your support! 72 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Martin Kluska 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in all 13 | > copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | > SOFTWARE. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pion/laravel-chunk-upload", 3 | "description": "Service for chunked upload with several js providers", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Martin Kluska", 8 | "email": "martin@kluska.cz" 9 | } 10 | ], 11 | "scripts": { 12 | "lint:fix": "./vendor/bin/php-cs-fixer fix --config=.php_cs --using-cache false", 13 | "lint:check": "./vendor/bin/phplint", 14 | "lint": "composer run-script lint:fix && composer run-script lint:check", 15 | "test": "./vendor/bin/phpunit" 16 | }, 17 | "require": { 18 | "illuminate/http": "5.2 - 5.8 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0", 19 | "illuminate/console": "5.2 - 5.8 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0", 20 | "illuminate/support": "5.2 - 5.8 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0", 21 | "illuminate/filesystem": "5.2 - 5.8 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "5.7 | 6.0 | 7.0 | 7.5 | 8.4 | ^8.5 | ^9.3 | ^10.0 | ^11.0", 25 | "mockery/mockery": "^1.1.0 | ^1.3.0 | ^1.6.0", 26 | "friendsofphp/php-cs-fixer": "^2.16.0 | ^3.52.0", 27 | "overtrue/phplint": "^1.1 | ^2.0 | ^9.1" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Pion\\Laravel\\ChunkUpload\\": "src/" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Tests\\": "tests/" 37 | } 38 | }, 39 | "extra":{ 40 | "laravel":{ 41 | "providers":[ 42 | "Pion\\Laravel\\ChunkUpload\\Providers\\ChunkUploadServiceProvider" 43 | ] 44 | } 45 | }, 46 | "minimum-stability": "stable" 47 | } 48 | -------------------------------------------------------------------------------- /config/chunk-upload.php: -------------------------------------------------------------------------------- 1 | [ 11 | /* 12 | * Returns the folder name of the chunks. The location is in storage/app/{folder_name} 13 | */ 14 | 'chunks' => 'chunks', 15 | 'disk' => 'local', 16 | ], 17 | 'clear' => [ 18 | /* 19 | * How old chunks we should delete 20 | */ 21 | 'timestamp' => '-3 HOURS', 22 | 'schedule' => [ 23 | 'enabled' => true, 24 | 'cron' => '25 * * * *', // run every hour on the 25th minute 25 | ], 26 | ], 27 | 'chunk' => [ 28 | // setup for the chunk naming setup to ensure same name upload at same time 29 | 'name' => [ 30 | 'use' => [ 31 | 'session' => true, // should the chunk name use the session id? The uploader must send cookie!, 32 | 'browser' => false, // instead of session we can use the ip and browser? 33 | ], 34 | ], 35 | ], 36 | 'handlers' => [ 37 | // A list of handlers/providers that will be appended to existing list of handlers 38 | 'custom' => [], 39 | // Overrides the list of handlers - use only what you really want 40 | 'override' => [ 41 | // \Pion\Laravel\ChunkUpload\Handler\DropZoneUploadHandler::class 42 | ], 43 | ], 44 | ]; 45 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Laravel Chunk Upload 2 | 3 | [![Total Downloads](https://poser.pugx.org/pion/laravel-chunk-upload/downloads?format=flat)](https://packagist.org/packages/pion/laravel-chunk-upload) 4 | [![Build Status](https://github.com/pionl/laravel-chunk-upload/workflows/build/badge.svg)](https://github.com/pionl/laravel-chunk-upload/actions) 5 | [![Latest Stable Version](https://poser.pugx.org/pion/laravel-chunk-upload/v/stable?format=flat)](https://packagist.org/packages/pion/laravel-chunk-upload) 6 | [![License](https://poser.pugx.org/pion/laravel-chunk-upload/license)](https://packagist.org/packages/pion/laravel-chunk-upload) 7 | 8 | ## Introduction 9 | 10 | Laravel Chunk Upload simplifies chunked uploads with support for multiple JavaScript libraries atop Laravel's file upload system, designed with a minimal memory footprint. Features include cross-domain request support, automatic cleaning, and intuitive usage. 11 | 12 | For example repository with **integration tests**, visit [laravel-chunk-upload-example](https://github.com/pionl/laravel-chunk-upload-example). 13 | 14 | Before contributing, familiarize yourself with the guidelines outlined in CONTRIBUTION.md. 15 | 16 | ## Installation 17 | 18 | **1. Install via Composer** 19 | 20 | ```bash 21 | composer require pion/laravel-chunk-upload 22 | ``` 23 | 24 | **2. Publish the Configuration (Optional)** 25 | 26 | ```bash 27 | php artisan vendor:publish --provider="Pion\Laravel\ChunkUpload\Providers\ChunkUploadServiceProvider" 28 | ``` 29 | 30 | ## Usage 31 | 32 | The setup involves three steps: 33 | 34 | 1. Integrate your controller to handle file uploads. [Instructions](https://github.com/pionl/laravel-chunk-upload/wiki/controller) 35 | 2. Define a route for the controller. [Instructions](https://github.com/pionl/laravel-chunk-upload/wiki/routing) 36 | 3. Select your preferred frontend provider (multiple providers are supported in a single controller). 37 | 38 | | Library | Wiki | Single & Chunk Upload | Simultaneous Uploads | Included in [Example Project](https://github.com/pionl/laravel-chunk-upload-example) | Author | 39 | |---------|------|-----------------------|----------------------|--------------------------------------------------|--------| 40 | | [resumable.js](https://github.com/23/resumable.js) | [Wiki](https://github.com/pionl/laravel-chunk-upload/wiki/resumable-js) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | [@pionl](https://github.com/pionl) | 41 | | [DropZone](https://github.com/dropzone/dropzone) | [Wiki](https://github.com/pionl/laravel-chunk-upload/wiki/dropzone) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | [@pionl](https://github.com/pionl) | 42 | | [jQuery-File-Upload](https://github.com/blueimp/jQuery-File-Upload) | [Wiki](https://github.com/pionl/laravel-chunk-upload/wiki/jquery-file-upload) | :heavy_check_mark: | :heavy_multiplication_x: | :heavy_check_mark: | [@pionl](https://github.com/pionl) | 43 | | [Plupload](https://github.com/moxiecode/plupload) | [Wiki](https://github.com/pionl/laravel-chunk-upload/wiki/plupload) | :heavy_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | [@pionl](https://github.com/pionl) | 44 | | [simple uploader](https://github.com/simple-uploader) | :heavy_multiplication_x: | :heavy_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | [@dyktek](https://github.com/dyktek) | 45 | | [ng-file-upload](https://github.com/danialfarid/ng-file-upload) | [Wiki](https://github.com/pionl/laravel-chunk-upload/wiki/ng-file-upload) | :heavy_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | [@L3o-pold](https://github.com/L3o-pold) | 46 | 47 | **Simultaneous Uploads:** The library must send the last chunk as the final one to ensure correct merging. 48 | 49 | **Custom Disk:** Currently, it's recommended to use the basic storage setup (not linking the public folder). If you have time to verify its functionality, please PR the changes! 50 | 51 | For detailed information and tips, refer to the [Wiki](https://github.com/pionl/laravel-chunk-upload/wiki) or explore a working example in a separate repository with [example](https://github.com/pionl/laravel-chunk-upload-example). 52 | 53 | ## Changelog 54 | 55 | View the changelog in [releases](https://github.com/pionl/laravel-chunk-upload/releases). 56 | 57 | ## Contribution or Extension 58 | 59 | Review the contribution guidelines before submitting your PRs (and utilize the example repository for running integration tests). 60 | 61 | Refer to [CONTRIBUTING.md](CONTRIBUTING.md) for contribution instructions. All contributions are welcome. 62 | 63 | ## Compatibility 64 | 65 | Though not tested via automation scripts, Laravel 5/6 should still be supported. 66 | 67 | | Version | PHP | 68 | |---------|---------------| 69 | | 12.* | 8.2,8.3,8.4 | 70 | | 11.* | 8.2,8.3,8.4 | 71 | | 10.* | 8.1, 8.2 | 72 | | 9.* | 8.0, 8.1 | 73 | | 8.* | 7.4, 8.0, 8.1 | 74 | | 7.* | 7.4 | 75 | 76 | ### New versions 77 | 78 | If there is a new Laravel version and there is no a offical release in our package you can create a PR. Then any one can use the PR until offical release is made. Check the PR and and update composer.json with the repository. 79 | 80 | ``` 81 | "repositories": [ 82 | { 83 | "type": "vcs", 84 | "url": "https://github.com/{!ENTER_USER_NAME!}/laravel-chunk-upload" 85 | } 86 | ] 87 | ``` 88 | 89 | Here is [exmplanation article](https://putyourlightson.com/articles/requiring-a-forked-repo-with-composer) 90 | 91 | ## Copyright and License 92 | 93 | [laravel-chunk-upload](https://github.com/pionl/laravel-chunk-upload) was authored by [Martin Kluska](http://kluska.cz) and is released under the [MIT License](LICENSE.md). 94 | 95 | Copyright (c) 2017 and beyond Martin Kluska and all contributors (Thank you ❤️) 96 | -------------------------------------------------------------------------------- /src/ChunkFile.php: -------------------------------------------------------------------------------- 1 | path = $path; 39 | $this->modifiedTime = $modifiedTime; 40 | $this->storage = $storage; 41 | } 42 | 43 | /** 44 | * @return string relative to the disk 45 | */ 46 | public function getPath() 47 | { 48 | return $this->path; 49 | } 50 | 51 | public function getAbsolutePath() 52 | { 53 | $pathPrefix = $this->storage->getDiskPathPrefix(); 54 | 55 | return $pathPrefix.'/'.$this->path; 56 | } 57 | 58 | /** 59 | * @return int 60 | */ 61 | public function getModifiedTime() 62 | { 63 | return $this->modifiedTime; 64 | } 65 | 66 | /** 67 | * Moves the chunk file to given relative path (within the disk). 68 | * 69 | * @param string $pathTo 70 | * 71 | * @return bool 72 | */ 73 | public function move($pathTo) 74 | { 75 | return $this->storage->disk()->move($this->path, $pathTo); 76 | } 77 | 78 | /** 79 | * Deletes the chunk file. 80 | * 81 | * @return bool 82 | */ 83 | public function delete() 84 | { 85 | return $this->storage->disk()->delete($this->path); 86 | } 87 | 88 | /** 89 | * The __toString method allows a class to decide how it will react when it is converted to a string. 90 | * 91 | * @return string 92 | * 93 | * @see http://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.tostring 94 | */ 95 | public function __toString() 96 | { 97 | return sprintf('ChunkFile %s uploaded at %s', $this->getPath(), date('Y-m-d H:i:s', $this->getModifiedTime())); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Commands/ClearChunksCommand.php: -------------------------------------------------------------------------------- 1 | oldChunkFiles(); 38 | 39 | if ($oldFiles->isEmpty()) { 40 | $this->warn('Chunks: no old files'); 41 | 42 | return; 43 | } 44 | 45 | $this->info(sprintf('Found %d chunk files', $oldFiles->count()), $verbouse); 46 | $deleted = 0; 47 | 48 | /** @var ChunkFile $file */ 49 | foreach ($oldFiles as $file) { 50 | // debug the file info 51 | $this->comment('> '.$file, $verbouse); 52 | 53 | // delete the file 54 | if ($file->delete()) { 55 | ++$deleted; 56 | } else { 57 | $this->error('> chunk not deleted: '.$file); 58 | } 59 | } 60 | 61 | $this->info('Chunks: cleared '.$deleted.' '.Str::plural('file', $deleted)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Config/AbstractConfig.php: -------------------------------------------------------------------------------- 1 | 51 | */ 52 | abstract public function scheduleConfig(); 53 | 54 | /** 55 | * Should the chunk name add a session? 56 | * 57 | * @return bool 58 | */ 59 | abstract public function chunkUseSessionForName(); 60 | 61 | /** 62 | * Should the chunk name add a ip address? 63 | * 64 | * @return bool 65 | */ 66 | abstract public function chunkUseBrowserInfoForName(); 67 | } 68 | -------------------------------------------------------------------------------- /src/Config/FileConfig.php: -------------------------------------------------------------------------------- 1 | get('handlers', []); 25 | } 26 | 27 | /** 28 | * Returns the disk name to use for the chunk storage. 29 | * 30 | * @return string 31 | */ 32 | public function chunksDiskName() 33 | { 34 | return $this->get('storage.disk'); 35 | } 36 | 37 | /** 38 | * The storage path for the chunks. 39 | * 40 | * @return string the full path to the storage 41 | * 42 | * @see FileConfig::get() 43 | */ 44 | public function chunksStorageDirectory() 45 | { 46 | return $this->get('storage.chunks'); 47 | } 48 | 49 | /** 50 | * Returns the time stamp string for clear command. 51 | * 52 | * @return string 53 | * 54 | * @see FileConfig::get() 55 | */ 56 | public function clearTimestampString() 57 | { 58 | return $this->get('clear.timestamp'); 59 | } 60 | 61 | /** 62 | * Returns the shedule config array. 63 | * 64 | * @return array 65 | */ 66 | public function scheduleConfig() 67 | { 68 | return $this->get('clear.schedule'); 69 | } 70 | 71 | /** 72 | * Should the chunk name add a session? 73 | * 74 | * @return bool 75 | */ 76 | public function chunkUseSessionForName() 77 | { 78 | return $this->get('chunk.name.use.session', true); 79 | } 80 | 81 | /** 82 | * Should the chunk name add a ip address? 83 | * 84 | * @return bool 85 | */ 86 | public function chunkUseBrowserInfoForName() 87 | { 88 | return $this->get('chunk.name.use.browser', false); 89 | } 90 | 91 | /** 92 | * Returns a chunks config value. 93 | * 94 | * @param string $key the config name is prepended to the key value 95 | * @param mixed|null $default 96 | * 97 | * @return mixed 98 | * 99 | * @see \Config::get() 100 | */ 101 | public function get($key, $default = null) 102 | { 103 | return config(self::FILE_NAME.'.'.$key, $default); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Exceptions/ChunkInvalidValueException.php: -------------------------------------------------------------------------------- 1 | destinationFile = @fopen($targetFile, 'ab')) { 25 | throw new ChunkSaveException('Failed to open output stream.', 102); 26 | } 27 | } 28 | 29 | /** 30 | * Appends given file. 31 | * 32 | * @param string $sourceFilePath 33 | * 34 | * @return $this 35 | * 36 | * @throws ChunkSaveException 37 | */ 38 | public function appendFile($sourceFilePath) 39 | { 40 | // open the new uploaded chunk 41 | if (!$in = @fopen($sourceFilePath, 'rb')) { 42 | @fclose($this->destinationFile); 43 | throw new ChunkSaveException('Failed to open input stream', 101); 44 | } 45 | 46 | // read and write in buffs 47 | while ($buff = fread($in, 4096)) { 48 | fwrite($this->destinationFile, $buff); 49 | } 50 | 51 | @fclose($in); 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * Closes the connection to the file. 58 | */ 59 | public function close() 60 | { 61 | @fclose($this->destinationFile); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Handler/AbstractHandler.php: -------------------------------------------------------------------------------- 1 | request = $request; 42 | $this->file = $file; 43 | $this->config = $config; 44 | } 45 | 46 | /** 47 | * Checks if the current abstract handler can be used via HandlerFactory. 48 | * 49 | * @param Request $request 50 | * 51 | * @return bool 52 | */ 53 | public static function canBeUsedForRequest(Request $request) 54 | { 55 | return false; 56 | } 57 | 58 | /** 59 | * Checks the current setup if session driver was booted - if not, it will generate random hash. 60 | * 61 | * @return bool 62 | */ 63 | public static function canUseSession() 64 | { 65 | // Get the session driver and check if it was started - fully inited by laravel 66 | $session = session(); 67 | $driver = $session->getDefaultDriver(); 68 | $drivers = $session->getDrivers(); 69 | 70 | // Check if the driver is valid and started - allow using session 71 | if (isset($drivers[$driver]) && true === $drivers[$driver]->isStarted()) { 72 | return true; 73 | } 74 | 75 | return false; 76 | } 77 | 78 | /** 79 | * Builds the chunk file name per session and the original name. You can 80 | * provide custom additional name at the end of the generated file name. All chunk 81 | * files has .part extension. 82 | * 83 | * @param string|null $additionalName Make the name more unique (example: use id from request) 84 | * @param string|null $currentChunkIndex Add the chunk index for parallel upload 85 | * 86 | * @return string 87 | * 88 | * @see UploadedFile::getClientOriginalName() 89 | * @see Session::getId() 90 | */ 91 | public function createChunkFileName($additionalName = null, $currentChunkIndex = null) 92 | { 93 | // prepare basic name structure 94 | $array = [ 95 | $this->file->getClientOriginalName(), 96 | ]; 97 | 98 | // ensure that the chunk name is for unique for the client session 99 | $useSession = $this->config->chunkUseSessionForName(); 100 | $useBrowser = $this->config->chunkUseBrowserInfoForName(); 101 | if ($useSession && false === static::canUseSession()) { 102 | $useBrowser = true; 103 | $useSession = false; 104 | } 105 | 106 | // the session needs more config on the provider 107 | if ($useSession) { 108 | $array[] = Session::getId(); 109 | } 110 | 111 | // can work without any additional setup 112 | if ($useBrowser) { 113 | $array[] = md5($this->request->ip().$this->request->header('User-Agent', 'no-browser')); 114 | } 115 | 116 | // Add additional name for more unique chunk name 117 | if (!is_null($additionalName)) { 118 | $array[] = $additionalName; 119 | } 120 | 121 | // Build the final name - parts separated by dot 122 | $namesSeparatedByDot = [ 123 | implode('-', $array), 124 | ]; 125 | 126 | // Add the chunk index for parallel upload 127 | if (null !== $currentChunkIndex) { 128 | $namesSeparatedByDot[] = $currentChunkIndex; 129 | } 130 | 131 | // Add extension 132 | $namesSeparatedByDot[] = ChunkStorage::CHUNK_EXTENSION; 133 | 134 | // build name 135 | return implode('.', $namesSeparatedByDot); 136 | } 137 | 138 | /** 139 | * Creates save instance and starts saving the uploaded file. 140 | * 141 | * @param ChunkStorage $chunkStorage the chunk storage 142 | * 143 | * @return AbstractSave 144 | */ 145 | abstract public function startSaving($chunkStorage); 146 | 147 | /** 148 | * Returns the chunk file name for a storing the tmp file. 149 | * 150 | * @return string 151 | */ 152 | abstract public function getChunkFileName(); 153 | 154 | /** 155 | * Checks if the request has first chunk. 156 | * 157 | * @return bool 158 | */ 159 | abstract public function isFirstChunk(); 160 | 161 | /** 162 | * Checks if the current request has the last chunk. 163 | * 164 | * @return bool 165 | */ 166 | abstract public function isLastChunk(); 167 | 168 | /** 169 | * Checks if the current request is chunked upload. 170 | * 171 | * @return bool 172 | */ 173 | abstract public function isChunkedUpload(); 174 | 175 | /** 176 | * Returns the percentage of the upload file. 177 | * 178 | * @return int 179 | */ 180 | abstract public function getPercentageDone(); 181 | } 182 | -------------------------------------------------------------------------------- /src/Handler/ChunksInRequestSimpleUploadHandler.php: -------------------------------------------------------------------------------- 1 | get(static::KEY_CHUNK_NUMBER)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Handler/ChunksInRequestUploadHandler.php: -------------------------------------------------------------------------------- 1 | currentChunk = $this->getCurrentChunkFromRequest($request); 61 | $this->chunksTotal = $this->getTotalChunksFromRequest($request); 62 | } 63 | 64 | /** 65 | * Checks if the current abstract handler can be used via HandlerFactory. 66 | * 67 | * @param Request $request 68 | * 69 | * @return bool 70 | */ 71 | public static function canBeUsedForRequest(Request $request) 72 | { 73 | return $request->has(static::KEY_CHUNK_NUMBER) && $request->has(static::KEY_ALL_CHUNKS); 74 | } 75 | 76 | /** 77 | * Returns the chunk save instance for saving. 78 | * 79 | * @param ChunkStorage $chunkStorage the chunk storage 80 | * 81 | * @return ChunkSave 82 | * 83 | * @throws ChunkSaveException 84 | */ 85 | public function startSaving($chunkStorage) 86 | { 87 | return new ChunkSave($this->file, $this, $chunkStorage, $this->config); 88 | } 89 | 90 | /** 91 | * Returns current chunk from the request. 92 | * 93 | * @param Request $request 94 | * 95 | * @return int 96 | */ 97 | protected function getCurrentChunkFromRequest(Request $request) 98 | { 99 | // the chunk is indexed from zero (for 5 chunks: 0,1,2,3,4) 100 | return intval($request->get(static::KEY_CHUNK_NUMBER)) + 1; 101 | } 102 | 103 | /** 104 | * Returns current chunk from the request. 105 | * 106 | * @param Request $request 107 | * 108 | * @return int 109 | */ 110 | protected function getTotalChunksFromRequest(Request $request) 111 | { 112 | return intval($request->get(static::KEY_ALL_CHUNKS)); 113 | } 114 | 115 | /** 116 | * Returns the first chunk. 117 | * 118 | * @return bool 119 | */ 120 | public function isFirstChunk() 121 | { 122 | return 1 == $this->currentChunk; 123 | } 124 | 125 | /** 126 | * Checks if the chunk is last. 127 | * 128 | * @return int 129 | */ 130 | public function isLastChunk() 131 | { 132 | // the bytes starts from zero, remove 1 byte from total 133 | return $this->currentChunk == $this->chunksTotal; 134 | } 135 | 136 | /** 137 | * Returns the current chunk index. 138 | * 139 | * @return bool 140 | */ 141 | public function isChunkedUpload() 142 | { 143 | return $this->chunksTotal > 1; 144 | } 145 | 146 | /** 147 | * Returns the chunk file name. Uses the original client name and the total bytes. 148 | * 149 | * @return string returns the original name with the part extension 150 | * 151 | * @see createChunkFileName() 152 | */ 153 | public function getChunkFileName() 154 | { 155 | return $this->createChunkFileName($this->chunksTotal); 156 | } 157 | 158 | /** 159 | * @return int 160 | */ 161 | public function getTotalChunks() 162 | { 163 | return $this->chunksTotal; 164 | } 165 | 166 | /** 167 | * @return int 168 | */ 169 | public function getCurrentChunk() 170 | { 171 | return $this->currentChunk; 172 | } 173 | 174 | /** 175 | * Returns the percentage of the uploaded file. 176 | * 177 | * @return int 178 | */ 179 | public function getPercentageDone() 180 | { 181 | return ceil($this->currentChunk / $this->chunksTotal * 100); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/Handler/ContentRangeUploadHandler.php: -------------------------------------------------------------------------------- 1 | request->header(self::CONTENT_RANGE_INDEX, ''); 71 | 72 | $this->tryToParseContentRange($contentRange); 73 | } 74 | 75 | /** 76 | * Checks if the current abstract handler can be used via HandlerFactory. 77 | * 78 | * @param Request $request 79 | * 80 | * @return bool 81 | * 82 | * @throws ContentRangeValueToLargeException 83 | */ 84 | public static function canBeUsedForRequest(Request $request) 85 | { 86 | return (new static($request, null, null))->isChunkedUpload(); 87 | } 88 | 89 | /** 90 | * Returns the chunk save instance for saving. 91 | * 92 | * @param ChunkStorage $chunkStorage the chunk storage 93 | * 94 | * @return ChunkSave 95 | * 96 | * @throws ChunkSaveException 97 | */ 98 | public function startSaving($chunkStorage) 99 | { 100 | return new ChunkSave($this->file, $this, $chunkStorage, $this->config); 101 | } 102 | 103 | /** 104 | * Tries to parse the content range from the string. 105 | * 106 | * @param string $contentRange 107 | * 108 | * @throws ContentRangeValueToLargeException 109 | */ 110 | protected function tryToParseContentRange($contentRange) 111 | { 112 | // try to get the content range 113 | if (preg_match("/bytes ([\d]+)-([\d]+)\/([\d]+)/", $contentRange, $matches)) { 114 | $this->chunkedUpload = true; 115 | 116 | // write the bytes values 117 | $this->bytesStart = $this->convertToNumericValue($matches[1]); 118 | $this->bytesEnd = $this->convertToNumericValue($matches[2]); 119 | $this->bytesTotal = $this->convertToNumericValue($matches[3]); 120 | } 121 | } 122 | 123 | /** 124 | * Converts the string value to float - throws exception if float value is exceeded. 125 | * 126 | * @param string $value 127 | * 128 | * @return float 129 | * 130 | * @throws ContentRangeValueToLargeException 131 | */ 132 | protected function convertToNumericValue($value) 133 | { 134 | $floatVal = floatval($value); 135 | 136 | if (INF === $floatVal) { 137 | throw new ContentRangeValueToLargeException(); 138 | } 139 | 140 | return $floatVal; 141 | } 142 | 143 | /** 144 | * Returns the first chunk. 145 | * 146 | * @return bool 147 | */ 148 | public function isFirstChunk() 149 | { 150 | return 0 == $this->bytesStart; 151 | } 152 | 153 | /** 154 | * Returns the chunks count. 155 | * 156 | * @return int 157 | */ 158 | public function isLastChunk() 159 | { 160 | // the bytes starts from zero, remove 1 byte from total 161 | return $this->bytesEnd >= ($this->bytesTotal - 1); 162 | } 163 | 164 | /** 165 | * Returns the current chunk index. 166 | * 167 | * @return bool 168 | */ 169 | public function isChunkedUpload() 170 | { 171 | return $this->chunkedUpload; 172 | } 173 | 174 | /** 175 | * @return int returns the starting bytes for current request 176 | */ 177 | public function getBytesStart() 178 | { 179 | return $this->bytesStart; 180 | } 181 | 182 | /** 183 | * @return int returns the ending bytes for current request 184 | */ 185 | public function getBytesEnd() 186 | { 187 | return $this->bytesEnd; 188 | } 189 | 190 | /** 191 | * @return int returns the total bytes for the file 192 | */ 193 | public function getBytesTotal() 194 | { 195 | return $this->bytesTotal; 196 | } 197 | 198 | /** 199 | * Returns the chunk file name. Uses the original client name and the total bytes. 200 | * 201 | * @return string returns the original name with the part extension 202 | * 203 | * @see createChunkFileName() 204 | */ 205 | public function getChunkFileName() 206 | { 207 | return $this->createChunkFileName($this->bytesTotal); 208 | } 209 | 210 | /** 211 | * @return int 212 | */ 213 | public function getPercentageDone() 214 | { 215 | // Check that we have received total bytes 216 | if (0 == $this->getBytesTotal()) { 217 | return 0; 218 | } 219 | 220 | return ceil($this->getBytesEnd() / $this->getBytesTotal() * 100); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/Handler/DropZoneUploadHandler.php: -------------------------------------------------------------------------------- 1 | fileUuid = $request->get(self::CHUNK_UUID_INDEX); 39 | } 40 | 41 | /** 42 | * Builds the chunk file name from file uuid and current chunk. 43 | * 44 | * @return string 45 | */ 46 | public function getChunkFileName() 47 | { 48 | return $this->createChunkFileName($this->fileUuid, $this->getCurrentChunk()); 49 | } 50 | 51 | /** 52 | * Returns current chunk from the request. 53 | * 54 | * @param Request $request 55 | * 56 | * @return int 57 | */ 58 | protected function getCurrentChunkFromRequest(Request $request) 59 | { 60 | return intval($request->get(self::CHUNK_INDEX, 0)) + 1; 61 | } 62 | 63 | /** 64 | * Returns current chunk from the request. 65 | * 66 | * @param Request $request 67 | * 68 | * @return int 69 | */ 70 | protected function getTotalChunksFromRequest(Request $request) 71 | { 72 | return intval($request->get(self::CHUNK_TOTAL_INDEX, 1)); 73 | } 74 | 75 | /** 76 | * Checks if the current abstract handler can be used via HandlerFactory. 77 | * 78 | * @param Request $request 79 | * 80 | * @return bool 81 | */ 82 | public static function canBeUsedForRequest(Request $request) 83 | { 84 | return $request->has(self::CHUNK_UUID_INDEX) && $request->has(self::CHUNK_TOTAL_INDEX) && 85 | $request->has(self::CHUNK_INDEX); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Handler/HandlerFactory.php: -------------------------------------------------------------------------------- 1 | has(static::KEY_CHUNK_NUMBER) 57 | && $request->has(static::KEY_TOTAL_SIZE) 58 | && $request->has(static::KEY_CHUNK_SIZE) 59 | && $request->has(static::KEY_CHUNK_CURRENT_SIZE); 60 | 61 | return $hasChunkParams && self::checkChunkParams($request); 62 | } 63 | 64 | /** 65 | * @return int 66 | */ 67 | public function getPercentageDone() 68 | { 69 | // Check that we have received total chunks 70 | if (!$this->chunksTotal) { 71 | return 0; 72 | } 73 | 74 | return intval(parent::getPercentageDone()); 75 | } 76 | 77 | /** 78 | * @param Request $request 79 | * 80 | * @return bool 81 | * 82 | * @throws ChunkInvalidValueException 83 | */ 84 | protected static function checkChunkParams($request) 85 | { 86 | $isInteger = ctype_digit($request->input(static::KEY_CHUNK_NUMBER)) 87 | && ctype_digit($request->input(static::KEY_TOTAL_SIZE)) 88 | && ctype_digit($request->input(static::KEY_CHUNK_SIZE)) 89 | && ctype_digit($request->input(static::KEY_CHUNK_CURRENT_SIZE)); 90 | 91 | if ($request->get(static::KEY_CHUNK_SIZE) < $request->get(static::KEY_CHUNK_CURRENT_SIZE)) { 92 | throw new ChunkInvalidValueException(); 93 | } 94 | 95 | if ($request->get(static::KEY_CHUNK_NUMBER) < 0) { 96 | throw new ChunkInvalidValueException(); 97 | } 98 | 99 | if ($request->get(static::KEY_TOTAL_SIZE) < 0) { 100 | throw new ChunkInvalidValueException(); 101 | } 102 | 103 | return $isInteger; 104 | } 105 | 106 | /** 107 | * Returns current chunk from the request. 108 | * 109 | * @param Request $request 110 | * 111 | * @return int 112 | */ 113 | protected function getTotalChunksFromRequest(Request $request) 114 | { 115 | if (!$request->get(static::KEY_CHUNK_SIZE)) { 116 | return 0; 117 | } 118 | 119 | return intval( 120 | ceil($request->get(static::KEY_TOTAL_SIZE) / $request->get(static::KEY_CHUNK_SIZE)) 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Handler/ResumableJSUploadHandler.php: -------------------------------------------------------------------------------- 1 | fileUuid = $request->get(self::CHUNK_UUID_INDEX); 37 | } 38 | 39 | /** 40 | * Append the resumable file - uuid and pass the current chunk index for parallel upload. 41 | * 42 | * @return string 43 | */ 44 | public function getChunkFileName() 45 | { 46 | return $this->createChunkFileName(substr($this->fileUuid,0,40), $this->getCurrentChunk()); 47 | } 48 | 49 | /** 50 | * Returns current chunk from the request. 51 | * 52 | * @param Request $request 53 | * 54 | * @return int 55 | */ 56 | protected function getCurrentChunkFromRequest(Request $request) 57 | { 58 | return $request->get(self::CHUNK_NUMBER_INDEX); 59 | } 60 | 61 | /** 62 | * Returns current chunk from the request. 63 | * 64 | * @param Request $request 65 | * 66 | * @return int 67 | */ 68 | protected function getTotalChunksFromRequest(Request $request) 69 | { 70 | return $request->get(self::TOTAL_CHUNKS_INDEX); 71 | } 72 | 73 | /** 74 | * Checks if the current abstract handler can be used via HandlerFactory. 75 | * 76 | * @param Request $request 77 | * 78 | * @return bool 79 | */ 80 | public static function canBeUsedForRequest(Request $request) 81 | { 82 | return $request->has(self::CHUNK_NUMBER_INDEX) && $request->has(self::TOTAL_CHUNKS_INDEX) && 83 | $request->has(self::CHUNK_UUID_INDEX); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Handler/SingleUploadHandler.php: -------------------------------------------------------------------------------- 1 | file, $this, $this->config); 27 | } 28 | 29 | /** 30 | * Returns the chunk file name for a storing the tmp file. 31 | * 32 | * @return string 33 | */ 34 | public function getChunkFileName() 35 | { 36 | return null; // never used 37 | } 38 | 39 | /** 40 | * Checks if the request has first chunk. 41 | * 42 | * @return bool 43 | */ 44 | public function isFirstChunk() 45 | { 46 | return true; 47 | } 48 | 49 | /** 50 | * Checks if the current request has the last chunk. 51 | * 52 | * @return bool 53 | */ 54 | public function isLastChunk() 55 | { 56 | return true; 57 | } 58 | 59 | /** 60 | * Checks if the current request is chunked upload. 61 | * 62 | * @return bool 63 | */ 64 | public function isChunkedUpload() 65 | { 66 | return false; // force the `SingleSave` instance 67 | } 68 | 69 | /** 70 | * Returns the percentage of the upload file. 71 | * 72 | * @return int 73 | */ 74 | public function getPercentageDone() 75 | { 76 | return 100; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Handler/Traits/HandleParallelUploadTrait.php: -------------------------------------------------------------------------------- 1 | file, 32 | $this, 33 | $chunkStorage, 34 | $this->config 35 | ); 36 | } 37 | 38 | public function getPercentageDone() 39 | { 40 | return $this->percentageDone; 41 | } 42 | 43 | /** 44 | * Sets percentegage done - should be calculated from chunks count. 45 | * 46 | * @param int $percentageDone 47 | * 48 | * @return HandleParallelUploadTrait 49 | */ 50 | public function setPercentageDone(int $percentageDone) 51 | { 52 | $this->percentageDone = $percentageDone; 53 | 54 | return $this; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Providers/ChunkUploadServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->make(AbstractConfig::class); 26 | $scheduleConfig = $config->scheduleConfig(); 27 | 28 | // Run only if schedule is enabled 29 | if (true === Arr::get($scheduleConfig, 'enabled', false)) { 30 | // Wait until the app is fully booted 31 | $this->app->booted(function () use ($scheduleConfig) { 32 | // Get the scheduler instance 33 | /** @var Schedule $schedule */ 34 | $schedule = $this->app->make(Schedule::class); 35 | 36 | // Register the clear chunks with custom schedule 37 | $schedule->command('uploads:clear') 38 | ->cron(Arr::get($scheduleConfig, 'cron', '* * * * *')); 39 | }); 40 | } 41 | 42 | $this->registerHandlers($config->handlers()); 43 | } 44 | 45 | /** 46 | * Register the package requirements. 47 | * 48 | * @see ChunkUploadServiceProvider::registerConfig() 49 | */ 50 | public function register() 51 | { 52 | // Register the commands 53 | $this->commands([ 54 | ClearChunksCommand::class, 55 | ]); 56 | 57 | // Register the config 58 | $this->registerConfig(); 59 | 60 | // Register the config via abstract instance 61 | $this->app->singleton(AbstractConfig::class, function () { 62 | return new FileConfig(); 63 | }); 64 | 65 | // Register the config via abstract instance 66 | $this->app->singleton(ChunkStorage::class, function ($app) { 67 | /** @var AbstractConfig $config */ 68 | $config = $app->make(AbstractConfig::class); 69 | 70 | // Build the chunk storage 71 | return new ChunkStorage($this->disk($config->chunksDiskName()), $config); 72 | }); 73 | 74 | /* 75 | * Bind a FileReceiver for dependency and use only the first object 76 | */ 77 | $this->app->bind(FileReceiver::class, function ($app) { 78 | /** @var Request $request */ 79 | $request = $app->make('request'); 80 | 81 | // Get the first file object - must be converted instances of UploadedFile 82 | $file = Arr::first($request->allFiles()); 83 | 84 | // Build the file receiver 85 | return new FileReceiver($file, $request, HandlerFactory::classFromRequest($request)); 86 | }); 87 | } 88 | 89 | /** 90 | * Returns disk name. 91 | * 92 | * @param string $diskName 93 | * 94 | * @return \Illuminate\Contracts\Filesystem\Filesystem 95 | */ 96 | protected function disk($diskName) 97 | { 98 | return Storage::disk($diskName); 99 | } 100 | 101 | /** 102 | * Publishes and mergers the config. Uses the FileConfig. Registers custom handlers. 103 | * 104 | * @see FileConfig 105 | * @see ServiceProvider::publishes 106 | * @see ServiceProvider::mergeConfigFrom 107 | * 108 | * @return $this 109 | */ 110 | protected function registerConfig() 111 | { 112 | // Config options 113 | $configIndex = FileConfig::FILE_NAME; 114 | $configFileName = FileConfig::FILE_NAME.'.php'; 115 | $configPath = __DIR__.'/../../config/'.$configFileName; 116 | 117 | // Publish the config 118 | $this->publishes([ 119 | $configPath => config_path($configFileName), 120 | ]); 121 | 122 | // Merge the default config to prevent any crash or unfilled configs 123 | $this->mergeConfigFrom( 124 | $configPath, 125 | $configIndex 126 | ); 127 | 128 | return $this; 129 | } 130 | 131 | /** 132 | * Registers handlers from config. 133 | * 134 | * @param array $handlersConfig 135 | * 136 | * @return $this 137 | */ 138 | protected function registerHandlers(array $handlersConfig) 139 | { 140 | $overrideHandlers = Arr::get($handlersConfig, 'override', []); 141 | if (count($overrideHandlers) > 0) { 142 | HandlerFactory::setHandlers($overrideHandlers); 143 | 144 | return $this; 145 | } 146 | 147 | foreach (Arr::get($handlersConfig, 'custom', []) as $handler) { 148 | HandlerFactory::register($handler); 149 | } 150 | 151 | return $this; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Receiver/FileReceiver.php: -------------------------------------------------------------------------------- 1 | request = $request; 62 | $this->file = is_object($fileIndexOrFile) ? $fileIndexOrFile : $request->file($fileIndexOrFile); 63 | $this->chunkStorage = is_null($chunkStorage) ? ChunkStorage::storage() : $chunkStorage; 64 | $this->config = is_null($config) ? AbstractConfig::config() : $config; 65 | 66 | if ($this->isUploaded()) { 67 | if (!$this->file->isValid()) { 68 | throw new UploadFailedException($this->file->getErrorMessage()); 69 | } 70 | 71 | $this->handler = new $handlerClass($this->request, $this->file, $this->config); 72 | } 73 | } 74 | 75 | /** 76 | * Checks if the file was uploaded. 77 | * 78 | * @return bool 79 | */ 80 | public function isUploaded() 81 | { 82 | return is_object($this->file) && UPLOAD_ERR_NO_FILE !== $this->file->getError(); 83 | } 84 | 85 | /** 86 | * Tries to handle the upload request. If the file is not uploaded, returns false. If the file 87 | * is present in the request, it will create the save object. 88 | * 89 | * If the file in the request is chunk, it will create the `ChunkSave` object, otherwise creates the `SingleSave` 90 | * which doesn't nothing at this moment. 91 | * 92 | * @return bool|AbstractSave 93 | */ 94 | public function receive() 95 | { 96 | if (false === is_object($this->handler)) { 97 | return false; 98 | } 99 | 100 | return $this->handler->startSaving($this->chunkStorage, $this->config); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Save/AbstractSave.php: -------------------------------------------------------------------------------- 1 | file = $file; 53 | $this->handler = $handler; 54 | $this->config = $config; 55 | } 56 | 57 | /** 58 | * Checks if the file upload is finished. 59 | * 60 | * @return bool 61 | */ 62 | public function isFinished() 63 | { 64 | return $this->isValid(); 65 | } 66 | 67 | /** 68 | * Checks if the upload is valid. 69 | * 70 | * @return bool 71 | */ 72 | public function isValid() 73 | { 74 | return $this->file->isValid(); 75 | } 76 | 77 | /** 78 | * Returns the error message. 79 | * 80 | * @return string 81 | */ 82 | public function getErrorMessage() 83 | { 84 | return $this->file->getErrorMessage(); 85 | } 86 | 87 | /** 88 | * @return UploadedFile 89 | */ 90 | public function getFile() 91 | { 92 | return $this->file; 93 | } 94 | 95 | /** 96 | * Returns always the uploaded chunk file. 97 | * 98 | * @return UploadedFile|null 99 | */ 100 | public function getUploadedFile() 101 | { 102 | return $this->file; 103 | } 104 | 105 | /** 106 | * Passes all the function into the file. 107 | * 108 | * @param $name string 109 | * @param $arguments array 110 | * 111 | * @return mixed 112 | * 113 | * @see http://php.net/manual/en/language.oop5.overloading.php#language.oop5.overloading.methods 114 | */ 115 | public function __call($name, $arguments) 116 | { 117 | return call_user_func_array([$this->getFile(), $name], $arguments); 118 | } 119 | 120 | /** 121 | * @return AbstractHandler 122 | */ 123 | public function handler() 124 | { 125 | return $this->handler; 126 | } 127 | 128 | /** 129 | * Returns the current config. 130 | * 131 | * @return AbstractConfig 132 | */ 133 | public function config() 134 | { 135 | return $this->config; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Save/ChunkSave.php: -------------------------------------------------------------------------------- 1 | chunkStorage = $chunkStorage; 60 | 61 | $this->isLastChunk = $handler->isLastChunk(); 62 | $this->chunkFileName = $handler->getChunkFileName(); 63 | 64 | // build the full disk path 65 | $this->chunkFullFilePath = $this->getChunkFilePath(true); 66 | 67 | $this->handleChunk(); 68 | } 69 | 70 | /** 71 | * Checks if the file upload is finished (last chunk). 72 | * 73 | * @return bool 74 | */ 75 | public function isFinished() 76 | { 77 | return parent::isFinished() && $this->isLastChunk; 78 | } 79 | 80 | /** 81 | * Returns the chunk file path in the current disk instance. 82 | * 83 | * @param bool $absolutePath 84 | * 85 | * @return string 86 | */ 87 | public function getChunkFilePath($absolutePath = false) 88 | { 89 | return $this->getChunkDirectory($absolutePath).$this->chunkFileName; 90 | } 91 | 92 | /** 93 | * Returns the full file path. 94 | * 95 | * @return string 96 | */ 97 | public function getChunkFullFilePath() 98 | { 99 | return $this->chunkFullFilePath; 100 | } 101 | 102 | /** 103 | * Returns the folder for the cunks in the storage path on current disk instance. 104 | * 105 | * @param bool $absolutePath 106 | * 107 | * @return string 108 | */ 109 | public function getChunkDirectory($absolutePath = false) 110 | { 111 | $paths = []; 112 | 113 | if ($absolutePath) { 114 | $paths[] = $this->chunkStorage()->getDiskPathPrefix(); 115 | } 116 | 117 | $paths[] = $this->chunkStorage()->directory(); 118 | 119 | return implode('', $paths); 120 | } 121 | 122 | /** 123 | * Returns the uploaded file if the chunk if is not completed, otherwise passes the 124 | * final chunk file. 125 | * 126 | * @return UploadedFile|null 127 | */ 128 | public function getFile() 129 | { 130 | if ($this->isLastChunk) { 131 | return $this->fullChunkFile; 132 | } 133 | 134 | return parent::getFile(); 135 | } 136 | 137 | /** 138 | * @deprecated 139 | * @since v1.1.8 140 | */ 141 | protected function handleChunkMerge() 142 | { 143 | $this->handleChunk(); 144 | } 145 | 146 | /** 147 | * Appends the new uploaded data to the final file. 148 | * 149 | * @throws ChunkSaveException 150 | */ 151 | protected function handleChunk() 152 | { 153 | // prepare the folder and file path 154 | $this->createChunksFolderIfNeeded(); 155 | $file = $this->getChunkFilePath(); 156 | 157 | $this->handleChunkFile($file) 158 | ->tryToBuildFullFileFromChunks(); 159 | } 160 | 161 | /** 162 | * Checks if the current chunk is last. 163 | * 164 | * @return $this 165 | */ 166 | protected function tryToBuildFullFileFromChunks() 167 | { 168 | // build the last file because of the last chunk 169 | if ($this->isLastChunk) { 170 | $this->buildFullFileFromChunks(); 171 | } 172 | 173 | return $this; 174 | } 175 | 176 | /** 177 | * Appends the current uploaded file to chunk file. 178 | * 179 | * @param string $file Relative path to chunk 180 | * 181 | * @return $this 182 | * 183 | * @throws ChunkSaveException 184 | */ 185 | protected function handleChunkFile($file) 186 | { 187 | // delete the old chunk 188 | if ($this->handler()->isFirstChunk() && $this->chunkDisk()->exists($file)) { 189 | $this->chunkDisk()->delete($file); 190 | } 191 | 192 | // Append the data to the file 193 | (new FileMerger($this->getChunkFullFilePath())) 194 | ->appendFile($this->file->getPathname()) 195 | ->close(); 196 | 197 | return $this; 198 | } 199 | 200 | /** 201 | * Builds the final file. 202 | */ 203 | protected function buildFullFileFromChunks() 204 | { 205 | // try to get local path 206 | $finalPath = $this->getChunkFullFilePath(); 207 | 208 | // build the new UploadedFile 209 | $this->fullChunkFile = $this->createFullChunkFile($finalPath); 210 | } 211 | 212 | /** 213 | * Creates the UploadedFile object for given chunk file. 214 | * 215 | * @param string $finalPath 216 | * 217 | * @return UploadedFile 218 | */ 219 | protected function createFullChunkFile($finalPath) 220 | { 221 | // We must pass the true as test to force the upload file 222 | // to use a standard copy method, not move uploaded file 223 | $test = true; 224 | $clientOriginalName = $this->file->getClientOriginalName(); 225 | $clientMimeType = $this->file->getClientMimeType(); 226 | $error = $this->file->getError(); 227 | 228 | // Passing a size as 4th (filesize) argument to the constructor is deprecated since Symfony 4.1. 229 | if (SymfonyKernel::VERSION_ID >= 40100) { 230 | return new UploadedFile($finalPath, $clientOriginalName, $clientMimeType, $error, $test); 231 | } 232 | 233 | $fileSize = filesize($finalPath); 234 | 235 | return new UploadedFile($finalPath, $clientOriginalName, $clientMimeType, $fileSize, $error, $test); 236 | } 237 | 238 | /** 239 | * Returns the current chunk storage. 240 | * 241 | * @return ChunkStorage 242 | */ 243 | public function chunkStorage() 244 | { 245 | return $this->chunkStorage; 246 | } 247 | 248 | /** 249 | * Returns the disk adapter for the chunk. 250 | * 251 | * @return \Illuminate\Filesystem\FilesystemAdapter 252 | */ 253 | public function chunkDisk() 254 | { 255 | return $this->chunkStorage()->disk(); 256 | } 257 | 258 | /** 259 | * Crates the chunks folder if doesn't exists. Uses recursive create. 260 | */ 261 | protected function createChunksFolderIfNeeded() 262 | { 263 | $path = $this->getChunkDirectory(true); 264 | 265 | // creates the chunks dir 266 | if (!file_exists($path)) { 267 | mkdir($path, 0777, true); 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/Save/ParallelSave.php: -------------------------------------------------------------------------------- 1 | isFileValid = $file->isValid(); 53 | 54 | // Handle the file upload 55 | parent::__construct($file, $handler, $chunkStorage, $config); 56 | } 57 | 58 | public function isValid() 59 | { 60 | return $this->isFileValid; 61 | } 62 | 63 | /** 64 | * Moves the uploaded chunk file to separate chunk file for merging. 65 | * 66 | * @param string $file Relative path to chunk 67 | * 68 | * @return $this 69 | */ 70 | protected function handleChunkFile($file) 71 | { 72 | // Move the uploaded file to chunk folder 73 | $this->file->move($this->getChunkDirectory(true), $this->chunkFileName); 74 | 75 | // Found current number of chunks to determine if we have all chunks (we cant use the 76 | // index because order of chunks are different. 77 | $this->foundChunks = $this->getSavedChunksFiles()->all(); 78 | 79 | $percentage = floor((count($this->foundChunks)) / $this->handler()->getTotalChunks() * 100); 80 | // We need to update the handler with correct percentage 81 | $this->handler()->setPercentageDone($percentage); 82 | $this->isLastChunk = $percentage >= 100; 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * Searches for all chunk files. 89 | * 90 | * @return \Illuminate\Support\Collection 91 | */ 92 | protected function getSavedChunksFiles() 93 | { 94 | $chunkFileName = preg_replace( 95 | "/\.[\d]+\.".ChunkStorage::CHUNK_EXTENSION.'$/', '', $this->handler()->getChunkFileName() 96 | ); 97 | 98 | return $this->chunkStorage->files(function ($file) use ($chunkFileName) { 99 | return false === Str::contains($file, $chunkFileName); 100 | }); 101 | } 102 | 103 | /** 104 | * @throws MissingChunkFilesException 105 | * @throws ChunkSaveException 106 | */ 107 | protected function buildFullFileFromChunks() 108 | { 109 | $chunkFiles = $this->foundChunks; 110 | 111 | if (0 === count($chunkFiles)) { 112 | throw new MissingChunkFilesException(); 113 | } 114 | 115 | // Sort the chunk order 116 | natcasesort($chunkFiles); 117 | 118 | // Get chunk files that matches the current chunk file name, also sort the chunk 119 | // files. 120 | $rootDirectory = $this->getChunkDirectory(true); 121 | $finalFilePath = $rootDirectory.'./'.$this->handler()->createChunkFileName(); 122 | 123 | // Delete the file if exists 124 | if (file_exists($finalFilePath)) { 125 | @unlink($finalFilePath); 126 | } 127 | 128 | $fileMerger = new FileMerger($finalFilePath); 129 | 130 | // Append each chunk file 131 | foreach ($chunkFiles as $filePath) { 132 | // Build the chunk file 133 | $chunkFile = new ChunkFile($filePath, null, $this->chunkStorage()); 134 | 135 | // Append the data 136 | $fileMerger->appendFile($chunkFile->getAbsolutePath()); 137 | 138 | // Delete the chunk file 139 | $chunkFile->delete(); 140 | } 141 | 142 | $fileMerger->close(); 143 | 144 | // Build the chunk file instance 145 | $this->fullChunkFile = $this->createFullChunkFile($finalFilePath); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Save/SingleSave.php: -------------------------------------------------------------------------------- 1 | config = $config; 58 | $this->usingDeprecatedLaravel = class_exists(LocalFilesystemAdapter::class) === false; 59 | $this->disk = $disk; 60 | 61 | if ($this->usingDeprecatedLaravel === false) { 62 | 63 | // try to get the adapter 64 | if (!method_exists($this->disk, 'getAdapter')) { 65 | throw new RuntimeException('FileSystem driver must have an adapter implemented'); 66 | } 67 | 68 | // get the disk adapter 69 | $this->diskAdapter = $this->disk->getAdapter(); 70 | 71 | // check if its local adapter 72 | $this->isLocalDisk = $this->diskAdapter instanceof LocalFilesystemAdapter; 73 | } else { 74 | $driver = $this->driver(); 75 | 76 | // try to get the adapter 77 | if (!method_exists($driver, 'getAdapter')) { 78 | throw new RuntimeException('FileSystem driver must have an adapter implemented'); 79 | } 80 | 81 | // get the disk adapter 82 | $this->diskAdapter = $driver->getAdapter(); 83 | 84 | // check if its local adapter 85 | $this->isLocalDisk = $this->diskAdapter instanceof Local; 86 | } 87 | 88 | } 89 | 90 | /** 91 | * The current path for chunks directory. 92 | * 93 | * @return string 94 | * 95 | * @throws RuntimeException when the adapter is not local 96 | */ 97 | public function getDiskPathPrefix() 98 | { 99 | if ($this->usingDeprecatedLaravel === true && $this->isLocalDisk) { 100 | return $this->diskAdapter->getPathPrefix(); 101 | } 102 | 103 | if ($this->isLocalDisk) { 104 | return $this->disk->path(''); 105 | } 106 | 107 | throw new RuntimeException('The full path is not supported on current disk - local adapter supported only'); 108 | } 109 | 110 | /** 111 | * The current chunks directory. 112 | * 113 | * @return string 114 | */ 115 | public function directory() 116 | { 117 | return $this->config->chunksStorageDirectory() . '/'; 118 | } 119 | 120 | /** 121 | * Returns an array of files in the chunks directory. 122 | * 123 | * @param \Closure|null $rejectClosure 124 | * 125 | * @return Collection 126 | * 127 | * @see FilesystemAdapter::files() 128 | * @see AbstractConfig::chunksStorageDirectory() 129 | */ 130 | public function files($rejectClosure = null) 131 | { 132 | // we need to filter files we don't support, lets use the collection 133 | $filesCollection = new Collection($this->disk->files($this->directory(), false)); 134 | 135 | return $filesCollection->reject(function ($file) use ($rejectClosure) { 136 | // ensure the file ends with allowed extension 137 | $shouldReject = !preg_match('/.' . self::CHUNK_EXTENSION . '$/', $file); 138 | if ($shouldReject) { 139 | return true; 140 | } 141 | if (is_callable($rejectClosure)) { 142 | return $rejectClosure($file); 143 | } 144 | 145 | return false; 146 | }); 147 | } 148 | 149 | /** 150 | * Returns the old chunk files. 151 | * 152 | * @return Collection collection of a ChunkFile objects 153 | */ 154 | public function oldChunkFiles() 155 | { 156 | $files = $this->files(); 157 | // if there are no files, lets return the empty collection 158 | if ($files->isEmpty()) { 159 | return $files; 160 | } 161 | 162 | // build the timestamp 163 | $timeToCheck = strtotime($this->config->clearTimestampString()); 164 | $collection = new Collection(); 165 | 166 | // filter the collection with files that are not correct chunk file 167 | // loop all current files and filter them by the time 168 | $files->each(function ($file) use ($timeToCheck, $collection) { 169 | // get the last modified time to check if the chunk is not new 170 | $modified = $this->disk()->lastModified($file); 171 | 172 | // delete only old chunk 173 | if ($modified < $timeToCheck) { 174 | $collection->push(new ChunkFile($file, $modified, $this)); 175 | } 176 | }); 177 | 178 | return $collection; 179 | } 180 | 181 | /** 182 | * @return AbstractConfig 183 | */ 184 | public function config() 185 | { 186 | return $this->config; 187 | } 188 | 189 | /** 190 | * @return FilesystemAdapter 191 | */ 192 | public function disk() 193 | { 194 | return $this->disk; 195 | } 196 | 197 | /** 198 | * Returns the driver. 199 | * 200 | * @return FilesystemOperator|FilesystemInterface 201 | */ 202 | public function driver() 203 | { 204 | return $this->disk()->getDriver(); 205 | } 206 | } 207 | --------------------------------------------------------------------------------