├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── composer.json ├── config └── file-uploader.php └── src ├── Exceptions ├── InvalidFile.php ├── InvalidUpload.php ├── MissingFile.php └── UploadFailed.php ├── FileUploader.php └── FileUploaderServiceProvider.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | .DS_Store 4 | .idea 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `erlandmuchasaj/laravel-file-uploader` will be documented in this file. 4 | 5 | ## 1.0.0 - 2023-03-06 6 | 7 | - initial release 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | erland.muchasaj@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skills, strengths, and weaknesses. 18 | Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 19 | 20 | ## Viability 21 | 22 | When requesting or submitting new features, first consider whether it might be useful to others. Open 23 | source projects are used by many developers, who may have entirely different needs to your own. Think about 24 | whether your feature is likely to be used by other users of the project. 25 | 26 | ## Procedure 27 | 28 | Before filing an issue: 29 | 30 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 31 | - Check to make sure your feature suggestion isn't already present within the project. 32 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 33 | - Check the pull requests tab to ensure that the feature isn't already in progress. 34 | 35 | Before submitting a pull request: 36 | 37 | - Check the codebase to ensure that your feature doesn't already exist. 38 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 39 | 40 | ## Requirements 41 | 42 | If the project maintainer has any additional requirements, you will find them listed here. 43 | 44 | - **[PSR-12 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-12-extended-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 45 | 46 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 47 | 48 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 49 | 50 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 51 | 52 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 53 | 54 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 55 | 56 | **Happy coding**! 57 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Erland Muchasaj 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel File Uploader 2 | 3 | Laravel File Uploader offers an easy way to upload files to different disks. 4 | The main purpose of the package is to remove the repeated and cumbersome code and simplify it into some simple methods. 5 | 6 | ## Installation 7 | 8 | You can install the package via composer: 9 | 10 | ```bash 11 | composer require erlandmuchasaj/laravel-file-uploader 12 | ``` 13 | 14 | ## Usage 15 | 16 | This package has an very easy and straight-forward usage. 17 | Just import the package and pass the file as parameter, and it will handle the rest. 18 | 19 | ```php 20 | use ErlandMuchasaj\LaravelFileUploader\FileUploader; 21 | 22 | Route::post('/files', function (\Illuminate\Http\Request $request) { 23 | 24 | $max_size = (int) ini_get('upload_max_filesize') * 1000; 25 | 26 | // FileUploader::images() get all image extensions ex: jpg, png, jpeg, gif, etc. 27 | // FileUploader::documents() get all documents extensions ex: 'csv', 'html', 'pdf', 'doc', 'docx', 'ppt' etc. 28 | $extensions = implode(',', FileUploader::images()); 29 | 30 | $request->validate([ 31 | 'file' => [ 32 | 'required', 33 | 'file', 34 | 'image', 35 | 'mimes:' . $extensions, 36 | 'max:'.$max_size, 37 | ] 38 | ]); 39 | 40 | $file = $request->file('file'); 41 | 42 | $response = FileUploader::store($file); 43 | // $response = FileUploader::store($file, $options); 44 | // available options (as key=>value pare) are: 45 | // `disk`, 'user_id`, `path`, `visibility` 46 | 47 | // do something with the $response 48 | // you can save it into your model etc. 49 | 50 | return redirect() 51 | ->back() 52 | ->with('success', __('File has been uploaded.')) 53 | ->with('file', $response); 54 | })->name('files.store'); 55 | 56 | /** 57 | * $response = [ 58 | * "type" => "image" 59 | * "extension" => "png" 60 | * "_extension" => "png" 61 | * "name" => "blog3" 62 | * "original_name" => "blog3.png" 63 | * "size" => 549247 64 | * "mime_type" => "image/png" 65 | * "dimensions" => "670x841" 66 | * "path" => "uploads/1/image/blog3_1678118034.png" // <== 67 | * "url" => "/storage/uploads/1/image/blog3_1678118034.png" 68 | * "user_id" => 1 69 | * "disk" => "local" 70 | * "visibility" => "public" 71 | * "uuid" => "dd5889c0-5057-49ef-a6ef-e3da961a47d1" 72 | * ] 73 | */ 74 | 75 | ``` 76 | 77 | If you need to modify the config files, you should publish the migration and the config/permission.php config file 78 | with: 79 | ```bash 80 | php artisan vendor:publish --provider="ErlandMuchasaj\LaravelFileUploader\FileUploaderServiceProvider" 81 | ``` 82 | 83 | Some other helper methods: 84 | 85 | ```php 86 | $path = 'uploads/1/image/blog3_1678118034.png'; // the path of the image where is stored. 87 | $response = FileUploader::get($path); // get file as StreamedResponse 88 | $response = FileUploader::getFile($path); // get file as content. 89 | $response = FileUploader::url($path); // full path url - /storage/uploads/1/image/blog3_1678118034.png 90 | $response = FileUploader::path($path); // C:\wamp\www\laravel-app\storage\app\uploads/1/image/blog3_1678118034.png 91 | $response = FileUploader::meta($path); // metadata about the file. 92 | /** 93 | * [ 94 | * "path" => "C:\wamp\www\laravel-app\storage\app\uploads/1/image/blog3_1678118034.png" 95 | * "url" => "/storage/uploads/1/image/blog3_1678118034.png" 96 | * "visibility" => "public" 97 | * "mimeType" => "image/png" 98 | * "size" => "536.37 KB" 99 | * "last_modified" => "1 hour ago" 100 | * "name" => "blog3_1678118034.png" 101 | * "pathinfo" => [ 102 | * "dirname" => "uploads/1/image" 103 | * "basename" => "blog3_1678118034.png" 104 | * "extension" => "png" 105 | * "filename" => "blog3_1678118034" 106 | * ] 107 | * ] 108 | */ 109 | 110 | $response = FileUploader::download($path, 'something_nice'); // download the file as StreamedResponse 111 | $response = FileUploader::getVisibility($path); // file visibility when applicable private/public 112 | $response = FileUploader::setVisibility($path, 'private'); // change file visibility 113 | $response = FileUploader::remove($path); // delete a file 114 | ``` 115 | 116 | Also, some other size converting helper functions are available for example: 117 | 118 | ```php 119 | $size = 549247; 120 | FileUploader::formatBytes($size); // "536.37 KB" 121 | 122 | FileUploader::convertBytesToSpecified($size, 'KB'); // 536.37KB 123 | FileUploader::convertBytesToSpecified($size, 'MB'); // 0.52MB 124 | ``` 125 | 126 | --- 127 | 128 | ## Support me 129 | 130 | I invest a lot of time and resources into creating [best in class open source packages](https://github.com/erlandmuchasaj?tab=repositories). 131 | 132 | If you found this package helpful you can show support by clicking on the following button below and donating some amount to help me work on these projects frequently. 133 | 134 | 135 | buy me a coffee 136 | 137 | 138 | ## Changelog 139 | 140 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 141 | 142 | ## Contributing 143 | 144 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 145 | 146 | ## Security Vulnerabilities 147 | 148 | Please see [SECURITY](SECURITY.md) for details. 149 | 150 | ## Credits 151 | 152 | - [Erland Muchasaj](https://github.com/erlandmuchasaj) 153 | 154 | ## License 155 | 156 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 157 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.x.x | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | If you discover a security vulnerability within this package, 12 | please send an e-mail to Erland Muchasaj via [erland.muchasaj@gmail.com](mailto:erland.muchasaj@gmail.com). 13 | All security vulnerabilities will be promptly addressed. 14 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "erlandmuchasaj/laravel-file-uploader", 3 | "type": "library", 4 | "description": "A simple package to help you easily upload files to your laravel project.", 5 | "minimum-stability": "dev", 6 | "prefer-stable": true, 7 | "homepage": "https://github.com/erlandmuchasaj/laravel-file-uploader", 8 | "license": "MIT", 9 | "keywords": [ 10 | "file", 11 | "media", 12 | "images", 13 | "upload", 14 | "laravel", 15 | "library", 16 | "cms", 17 | "emcms", 18 | "package" 19 | ], 20 | "autoload": { 21 | "psr-4": { 22 | "ErlandMuchasaj\\LaravelFileUploader\\": "src/" 23 | } 24 | }, 25 | "extra": { 26 | "laravel": { 27 | "providers": [ 28 | "ErlandMuchasaj\\LaravelFileUploader\\FileUploaderServiceProvider" 29 | ] 30 | } 31 | }, 32 | "authors": [ 33 | { 34 | "name": "Erland Muchasaj", 35 | "email": "erland.muchasaj@gmail.com", 36 | "homepage": "https://erlandmuchasaj.tech/", 37 | "role": "Developer" 38 | } 39 | ], 40 | "support": { 41 | "issues": "https://github.com/erlandmuchasaj/laravel-file-uploader/issues", 42 | "source": "https://github.com/erlandmuchasaj/laravel-file-uploader", 43 | "email": "erland.muchasaj@gmail.com", 44 | "irc": "irc://irc.freenode.org/composer" 45 | }, 46 | "funding": [ 47 | { 48 | "type": "patreon", 49 | "url": "https://www.patreon.com/erlandmuchasaj" 50 | }, 51 | { 52 | "type": "Ko-fi", 53 | "url": "https://ko-fi.com/erlandmuchasaj" 54 | }, 55 | { 56 | "type": "PayPal", 57 | "url": "https://paypal.me/emcms?country.x=AL&locale.x=en_US" 58 | } 59 | ], 60 | "config": { 61 | "sort-packages": true, 62 | "preferred-install": "dist", 63 | "optimize-autoloader": true 64 | }, 65 | "require": { 66 | "php": "^8.2", 67 | "ext-exif": "*", 68 | "ext-fileinfo": "*", 69 | "ext-json": "*", 70 | "nesbot/carbon": "^2|^3", 71 | "illuminate/http": "^8|^9|^10|^11|^12", 72 | "illuminate/support": "^8|^9|^10|^11|^12", 73 | "illuminate/contracts": "^8|^9|^10|^11|^12", 74 | "illuminate/filesystem": "^8|^9|^10|^11|^12" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /config/file-uploader.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DISK', 'local'), 15 | 16 | 'visibility' => 'public', 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Filesystem path 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure the path structure of the uploaded files 24 | | uploads/{user_id}/{type}/{filename} 25 | | 26 | | Supported variables: "{user_id}", "{type}", "{filename}" 27 | | 28 | */ 29 | 30 | 'path' => 'uploads/{user_id}/{type}/{filename}', 31 | 32 | /* 33 | |-------------------------------------------------------------------------- 34 | | Default files user group 35 | |-------------------------------------------------------------------------- 36 | | 37 | | All files will be grouped by default to root user aka ID=1. 38 | | If you have a different default user id you can set it here. 39 | | 40 | */ 41 | 42 | 'user_id' => 1, 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Shall we use safe extension and name extraction 47 | |-------------------------------------------------------------------------- 48 | | 49 | | getClientOriginalName() and getClientOriginalExtension() 50 | | are considered unsafe. 51 | | 52 | */ 53 | 54 | 'safe' => false, 55 | ]; 56 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidFile.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | protected static array $studlyCache = []; 33 | 34 | /** 35 | * default constants used to build file path 36 | */ 37 | const UPLOAD_PATH = 'uploads/{user_id}/{type}/{filename}'; 38 | 39 | /** 40 | * The public visibility setting. 41 | * - default 42 | * 43 | * @var string 44 | */ 45 | const VISIBILITY_PUBLIC = 'public'; 46 | 47 | /** 48 | * The private visibility setting. 49 | * 50 | * @var string 51 | */ 52 | const VISIBILITY_PRIVATE = 'private'; 53 | 54 | /** 55 | * Available file folder for sizes 56 | */ 57 | const ORIGINAL = 'original'; // this is not a directory just used as name 58 | 59 | const THUMB = 'thumb'; 60 | 61 | const XSMALL = 'xs'; 62 | 63 | const SMALL = 'sm'; 64 | 65 | const MEDIUM = 'md'; 66 | 67 | const LARGE = 'lg'; 68 | 69 | const XLARGE = 'xl'; 70 | 71 | /** 72 | * Available file types 73 | * suppoerted by fileService 74 | */ 75 | const IMAGE = 'image'; 76 | 77 | const AUDIO = 'audio'; 78 | 79 | const VIDEO = 'video'; 80 | 81 | const FILE = 'file'; 82 | 83 | const FONT = 'font'; 84 | 85 | const ARCHIVE = 'archive'; 86 | 87 | const DOCUMENT = 'document'; 88 | 89 | const SPREADSHEETS = 'spreadsheets'; 90 | 91 | /** 92 | * Available file types 93 | * 94 | * @var array 95 | */ 96 | public static array $validTypes = [ 97 | self::IMAGE, 98 | self::AUDIO, 99 | self::VIDEO, 100 | self::FILE, 101 | self::FONT, 102 | self::ARCHIVE, 103 | self::DOCUMENT, 104 | self::SPREADSHEETS, 105 | ]; 106 | 107 | /** 108 | * Available sizes for images 109 | * 110 | * @var array 111 | */ 112 | public static array $validSizes = [ 113 | self::THUMB => 60, 114 | self::XSMALL => 150, 115 | self::SMALL => 300, 116 | self::MEDIUM => 768, 117 | self::LARGE => 1024, 118 | self::XLARGE => 2048, 119 | ]; 120 | 121 | /** 122 | * validOptions of visibility 123 | * 124 | * @var array 125 | */ 126 | public static array $validOptions = [ 127 | self::VISIBILITY_PUBLIC, 128 | self::VISIBILITY_PRIVATE, 129 | ]; 130 | 131 | /** 132 | * $image_ext 133 | * 134 | * @var array 135 | */ 136 | private static array $image_ext = ['jpg', 'pjpg', 'jpe', 'jpeg', 'png', 'bmp', 'gif', 'svg', 'svgz', 'tiff', 'tif', 'webp', 'ico', 'avif']; 137 | 138 | /** 139 | * $font_ext 140 | * 141 | * @var array 142 | */ 143 | private static array $font_ext = ['ttc', 'otf', 'ttf', 'woff', 'woff2']; 144 | 145 | /** 146 | * $audio_ext 147 | * 148 | * @var array 149 | */ 150 | private static array $audio_ext = ['mp3', 'm4a', 'ogg', 'mpga', 'wav']; 151 | 152 | /** 153 | * $video_ext 154 | * 155 | * @var array 156 | */ 157 | private static array $video_ext = ['smv', 'movie', 'mov', 'wvx', 'wmx', 'wm', 'mp4', 'mp4', 'mp4v', 'mpg4', 'mpeg', 'mpg', 'mpe', 'wmv', 'avi', 'ogv', '3gp', '3g2']; 158 | 159 | /** 160 | * $document_ext 161 | * 162 | * @var array 163 | */ 164 | private static array $document_ext = ['css', 'csv', 'html', 'htm', 'conf', 'log', 'txt', 'text', 'pdf', 'doc', 'docx', 'ppt', 'pptx', 'pps', 'ppsx', 'odt', 'xls', 'xlsx']; 165 | 166 | /** 167 | * $archive 168 | * 169 | * @var array 170 | * 171 | * @example application/zip 172 | */ 173 | private static array $archives_ext = ['gzip', 'rar', 'tar', 'zip', '7z']; 174 | 175 | /** 176 | * Upload a file into specified disk using 177 | * specified visibility and then store into DB. 178 | * 179 | * @param array $args 180 | * @return array 181 | * 182 | * @throws UploadFailed 183 | */ 184 | public static function store( 185 | UploadedFile $file, 186 | array $args = [] 187 | ): array { 188 | try { 189 | $data = self::upload($file, $args); 190 | } catch (Exception $e) { 191 | throw new UploadFailed($e->getMessage(), $e->getCode()); 192 | } 193 | 194 | return $data; 195 | } 196 | 197 | /** 198 | * Upload an image to specific FileSystem 199 | * 200 | * @param array $options 201 | * @return array 202 | * 203 | * @throws InvalidUpload 204 | * @throws InvalidFile 205 | */ 206 | public static function upload(UploadedFile $file, array $options = []): array 207 | { 208 | if (! $file->isValid()) { 209 | throw new InvalidFile($file->getErrorMessage()); 210 | } 211 | 212 | if ($file->getSize() === false) { 213 | throw new InvalidFile('File failed to load.'); 214 | } 215 | 216 | // // here you can put as many default values as you want. 217 | // $defaults = [ 218 | // 'disk' => self::$disk, // The disk where the file is being saved. 219 | // 'safe' => false, // weather or not to use safe client file operators 220 | // 'user_id' => 1, // files are grouped by user. 221 | // 'path' => self::UPLOAD_PATH, // Where files are being stored. 222 | // 'visibility' => self::VISIBILITY_PUBLIC, // public | private 223 | // ]; 224 | $defaults = self::getConfig(); 225 | 226 | // merge default options with passed parameters 227 | $args = array_merge($defaults, $options); 228 | 229 | if (! in_array($args['visibility'], self::$validOptions, true)) { 230 | $args['visibility'] = self::VISIBILITY_PUBLIC; 231 | } 232 | 233 | $disk = $args['disk'] ?? self::$disk; 234 | 235 | $user_id = $args['user_id'] ?? null; 236 | 237 | /** 238 | * getClientOriginalName() and getClientOriginalExtension() are considered 239 | * unsafe therefor we use hashName() and extension() 240 | */ 241 | // get filename with extension 242 | $filenameWithExtension = $file->getClientOriginalName(); 243 | // $filenameWithExtension = $file->hashName() ?: $file->getClientOriginalName(); 244 | 245 | // get file extension 246 | $extension = $file->getClientOriginalExtension(); 247 | // $extension = $file->extension() ?: $file->getClientOriginalExtension(); 248 | 249 | // get filename without extension 250 | $filename = pathinfo($filenameWithExtension, PATHINFO_FILENAME); 251 | 252 | // @todo - EM: Check filename normalizer 253 | $filename = (new WhitespacePathNormalizer)->normalizePath($filename); 254 | $filename = self::defaultSanitizer($filename); 255 | 256 | // filename to store 257 | $filenameToStore = $filename.'_'.time().'.'.$extension; 258 | 259 | // Get the type of file we are storing 260 | $type = self::getType($extension); 261 | 262 | // Make a file path where image will be stored [uploads/{user_id}/{type}/{filename}.{ext}] 263 | $filePath = self::getUserDir($filenameToStore, $type, (int) $user_id); 264 | 265 | // Upload File to storage disk 266 | $fileContent = fopen($file, 'r+'); 267 | if (! $fileContent) { 268 | throw new InvalidUpload('Could not read file from disk...'); 269 | } 270 | 271 | $path = Storage::disk($disk)->put($filePath, $fileContent, $args['visibility']); // very nice for very big files 272 | 273 | // Store $filePath in the database 274 | if (! $path) { 275 | throw new InvalidUpload('The file could not be written to disk...'); 276 | } 277 | 278 | // dd([ 279 | // 'type' => $type, 280 | // 'extension' => $file->getClientOriginalExtension(), 281 | // '_extension' => $file->extension() ?: $file->getClientOriginalExtension(), 282 | // 'name' => $filename, 283 | // 'original_name' => $file->getClientOriginalName(), 284 | // 'size' => $file->getSize(), 285 | // 'mime_type' => $file->getClientMimeType(), 286 | // 'dimensions' => self::getDimensions($file, $type), 287 | // 'path' => $filePath, 288 | // 'url' => Storage::disk($disk)->url($filePath), 289 | // 'user_id' => $user_id, 290 | // 'disk' => $disk, 291 | // 'visibility' => $args['visibility'], // indicate if file is public or private 292 | // 'hash_file' => self::getHashFile($file), 293 | // 'uuid' => Str::uuid()->toString(), 294 | // ]); 295 | 296 | return [ 297 | 'type' => $type, 298 | 'extension' => $file->getClientOriginalExtension(), 299 | '_extension' => $file->extension() ?: $file->getClientOriginalExtension(), 300 | 'name' => $filename, 301 | 'original_name' => $file->getClientOriginalName(), 302 | 'size' => $file->getSize(), 303 | 'mime_type' => $file->getClientMimeType(), 304 | 'dimensions' => self::getDimensions($file, $type), 305 | 'path' => $filePath, 306 | 'url' => Storage::disk($disk)->url($filePath), 307 | 'user_id' => $user_id, 308 | 'disk' => $disk, 309 | 'visibility' => $args['visibility'], // indicate if file is public or private 310 | 'uuid' => Str::uuid()->toString(), 311 | ]; 312 | } 313 | 314 | protected static function getUser(int $user_id = null): ?int 315 | { 316 | $defaults = self::getConfig(); 317 | 318 | return $user_id ?? (! empty($defaults['user_id']) ? (int) $defaults['user_id'] : 1); 319 | } 320 | 321 | protected static function getDisk(string $disk = null): string 322 | { 323 | $key = $disk; 324 | 325 | if (isset(FileUploader::$studlyCache['disk_'.$key])) { 326 | return FileUploader::$studlyCache['disk_'.$key]; 327 | } 328 | 329 | $defaults = self::getConfig(); 330 | 331 | return FileUploader::$studlyCache['disk_'.$key] = $disk ?: $defaults['disk']; 332 | } 333 | 334 | /** 335 | * Get FileUploader configurations 336 | * 337 | * @return array 338 | */ 339 | protected static function getConfig(): array 340 | { 341 | $config = config(FileUploaderServiceProvider::$abstract); 342 | 343 | return empty($config) ? [] : Arr::wrap($config); 344 | } 345 | 346 | /** 347 | * Create a streamed response for a given file. 348 | * 349 | * 350 | * @return StreamedResponse Content 351 | */ 352 | public static function get(string $path, string $disk = null): StreamedResponse 353 | { 354 | return Storage::disk(self::getDisk($disk))->response($path); 355 | } 356 | 357 | /** 358 | * Create a streamed response for a given file. 359 | * 360 | * 361 | * @return string|null Content 362 | * 363 | * @throws MissingFile 364 | */ 365 | public static function getFile(string $path, string $disk = null): ?string 366 | { 367 | if (! Storage::disk(self::getDisk($disk))->exists($path)) { 368 | throw new MissingFile("File $path does not exists."); 369 | } 370 | 371 | return Storage::disk(self::getDisk($disk))->get($path); 372 | } 373 | 374 | /** 375 | * Get public path url. 376 | * Mainly used to access public files. 377 | */ 378 | public static function url(string $path, string $disk = null): string 379 | { 380 | return Storage::disk(self::getDisk($disk))->url($path); 381 | } 382 | 383 | /** 384 | * Get file path. 385 | */ 386 | public static function path(string $path, string $disk = null): string 387 | { 388 | return Storage::disk(self::getDisk($disk))->path($path); 389 | } 390 | 391 | /** 392 | * Download a specific resource 393 | * 394 | * @param string|null $name name with extensions. 395 | */ 396 | public static function download(string $path, string|null $name, string $disk = null): StreamedResponse 397 | { 398 | return Storage::disk(self::getDisk($disk))->download($path, $name); 399 | } 400 | 401 | /** 402 | * Get Visibility of a file 403 | * 404 | * @return string public|private 405 | */ 406 | public static function getVisibility(string $path, string $disk = null): string 407 | { 408 | return Storage::disk(self::getDisk($disk))->getVisibility($path); 409 | } 410 | 411 | /** 412 | * Set Visibility of a file 413 | */ 414 | public static function setVisibility(string $path, string $visibility, string $disk = null): bool 415 | { 416 | if (! in_array($visibility, self::$validOptions, true)) { 417 | return false; 418 | } 419 | 420 | return Storage::disk(self::getDisk($disk))->setVisibility($path, $visibility); 421 | } 422 | 423 | /** 424 | * Delete file from disk 425 | * 426 | * @throws MissingFile 427 | */ 428 | public static function remove(string $path, bool $throwError = true, string $disk = null): bool 429 | { 430 | if ($throwError && ! Storage::disk(self::getDisk($disk))->exists($path)) { 431 | throw new MissingFile('File does not exist!'); 432 | } 433 | 434 | return Storage::disk(self::getDisk($disk))->delete($path); 435 | } 436 | 437 | /** 438 | * Get meta data for a specific file. 439 | * 440 | * 441 | * @return array 442 | */ 443 | public static function meta(string $path, string $disk = null): array 444 | { 445 | $diskFrom = Storage::disk(self::getDisk($disk)); 446 | 447 | return [ 448 | 'path' => $diskFrom->path($path), 449 | 'url' => $diskFrom->url($path), 450 | 'visibility' => $diskFrom->getVisibility($path), 451 | 'mimeType' => $diskFrom->mimeType($path), 452 | 'size' => self::formatBytes($diskFrom->size($path)), 453 | 'last_modified' => Carbon::createFromTimestamp($diskFrom->lastModified($path))->diffForHumans(), 454 | 'name' => basename($path), 455 | ]; 456 | } 457 | 458 | /** 459 | * Get all extensions 460 | * 461 | * @return array Extensions of all file types 462 | */ 463 | public static function allExtensions(): array 464 | { 465 | return array_merge(self::$image_ext, self::$audio_ext, self::$video_ext, self::$document_ext, self::$archives_ext, self::$font_ext); 466 | } 467 | 468 | /** 469 | * Get all allowed image extensions 470 | * 471 | * @return array 472 | */ 473 | public static function images(): array 474 | { 475 | return self::$image_ext; 476 | } 477 | 478 | /** 479 | * Get all allowed document extensions 480 | * 481 | * @return array 482 | */ 483 | public static function documents(): array 484 | { 485 | return self::$document_ext; 486 | } 487 | 488 | /** 489 | * Get type by extension 490 | * 491 | * @param string $ext Specific extension 492 | * @return string Type 493 | */ 494 | private static function getType(string $ext): string 495 | { 496 | if (self::in_array($ext, self::$image_ext)) { 497 | return self::IMAGE; 498 | } 499 | 500 | if (self::in_array($ext, self::$audio_ext)) { 501 | return self::AUDIO; 502 | } 503 | 504 | if (self::in_array($ext, self::$video_ext)) { 505 | return self::VIDEO; 506 | } 507 | 508 | if (self::in_array($ext, self::$document_ext)) { 509 | return self::DOCUMENT; 510 | } 511 | 512 | if (self::in_array($ext, self::$font_ext)) { 513 | return self::FONT; 514 | } 515 | 516 | if (self::in_array($ext, self::$archives_ext)) { 517 | return self::ARCHIVE; 518 | } 519 | 520 | return self::FILE; 521 | } 522 | 523 | private static function defaultSanitizer(string $fileName): string 524 | { 525 | $fileName = (string) preg_replace('#\p{C}+#u', '', $fileName); 526 | 527 | return str_replace(['#', '/', '\\', ' '], '-', $fileName); 528 | } 529 | 530 | /** 531 | * Get directory for the specific user 532 | * 533 | * @return string Specific user directory 534 | * 535 | * @example uploads/{user_id}/{type}/{filename} 536 | */ 537 | private static function getUserDir(string $filename, string $type = self::FILE, int $user_id = null): string 538 | { 539 | $defaults = self::getConfig(); 540 | 541 | $dir = $defaults['path'] ?? self::UPLOAD_PATH; 542 | 543 | return trim(strtr($dir, [ 544 | '{user_id}' => $user_id, 545 | '{type}' => $type, 546 | '{filename}' => $filename, 547 | ]), '/\\'); 548 | } 549 | 550 | /** 551 | * Grab dimensions of an image. 552 | * 553 | * @return string|null string|null 554 | */ 555 | private static function getDimensions(UploadedFile $file, string $type = self::IMAGE): ?string 556 | { 557 | if ('image' !== $type) { 558 | return null; 559 | } 560 | 561 | if (self::isValidFileInstance($file) && $file->getClientMimeType() === 'image/svg+xml') { 562 | return null; 563 | } 564 | 565 | if (! self::isValidFileInstance($file) || ! $sizeDetails = @getimagesize($file->getRealPath())) { 566 | return null; 567 | } 568 | 569 | [$width, $height] = $sizeDetails; 570 | 571 | return $width.'x'.$height; 572 | } 573 | 574 | /** 575 | * Check that the given value is a valid file instance. 576 | */ 577 | private static function isValidFileInstance(mixed $file): bool 578 | { 579 | if ($file instanceof SymfonyUploadedFile && ! $file->isValid()) { 580 | return false; 581 | } 582 | 583 | return $file instanceof SymfonyFile; 584 | } 585 | 586 | /** 587 | * @param array $haystack 588 | */ 589 | private static function in_array(string $needle, array $haystack): bool 590 | { 591 | return in_array(strtolower($needle), array_map('strtolower', $haystack)); 592 | } 593 | 594 | /** 595 | * get icon path 596 | */ 597 | public static function getIconPath(string $mimeType): string 598 | { 599 | $file_type_icons_path = 'img'.DIRECTORY_SEPARATOR.'file-type-icons'.DIRECTORY_SEPARATOR; 600 | 601 | $icon_file = match ($mimeType) { 602 | 'image/jpeg', 'image/pjpeg', 'image/x-jps' => 'jpeg.png', 603 | 'image/png' => 'png.png', 604 | 'image/gif' => 'gif.png', 605 | 'image/bmp', 'image/x-windows-bmp' => 'bmp.png', 606 | 'text/html', 'text/asp', 'text/javascript', 'text/ecmascript', 'application/x-javascript', 'application/javascript', 'application/ecmascript' => 'html.png', 607 | 'text/plain' => 'conf.png', 608 | 'text/css' => 'css.png', 609 | 'audio/aiff', 'audio/x-aiff', 'audio/midi' => 'midi.png', 610 | 'application/x-troff-msvideo', 'video/avi', 'video/msvideo', 'video/x-msvideo', 'video/avs-video' => 'avi.png', 611 | 'video/animaflex' => 'fla.png', 612 | 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-word.document.macroEnabled.12', 'application/vnd.ms-word.template.macroEnabled.12', 'application/vnd.oasis.opendocument.text', 'application/vnd.apple.pages', 'application/vnd.ms-xpsdocument', 'application/oxps', 'application/rtf', 'application/wordperfect', 'application/octet-stream' => 'docx.png', 613 | 'application/x-compressed', 'application/x-7z-compressed', 'application/x-gzip', 'application/zip', 'multipart/x-gzip', 'multipart/x-zip' => 'zip.png', 614 | 'application/x-gtar', 'application/rar', 'application/x-tar' => 'rar.png', 615 | 'video/mpeg', 'audio/mpeg' => 'mpeg.png', 616 | 'application/pdf' => 'pdf.png', 617 | 'application/mspowerpoint', 'application/vnd.ms-powerpoint', 'application/powerpoint' => 'ms-pptx.png', 618 | 'application/excel', 'application/x-excel', 'application/x-msexcel', 'application/vnd.apple.numbers', 'application/application/vnd.oasis.opendocument.spreadsheet', 'application/vnd.ms-excel.sheet.macroEnabled.12', 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'ms-xlsx.png', 619 | 'image/vnd.adobe.photoshop' => 'psd.png', 620 | 'not-found' => 'not-found.png', 621 | default => 'unknown.png', 622 | }; 623 | 624 | return $file_type_icons_path.$icon_file; 625 | } 626 | 627 | /** 628 | * getFileType 629 | * Return file mimetype default: 'application/octet-stream' 630 | */ 631 | public static function getFileType(string $filename): bool|string 632 | { 633 | $mime_types = [ 634 | 635 | 'txt' => 'text/plain', 636 | 'htm' => 'text/html', 637 | 'html' => 'text/html', 638 | 'php' => 'text/html', 639 | 'css' => 'text/css', 640 | 'js' => 'application/javascript', 641 | 'json' => 'application/json', 642 | 'xml' => 'application/xml', 643 | 'swf' => 'application/x-shockwave-flash', 644 | 'flv' => 'video/x-flv', 645 | 646 | // images 647 | 'png' => 'image/png', 648 | 'jpe' => 'image/jpeg', 649 | 'jpeg' => 'image/jpeg', 650 | 'jpg' => 'image/jpeg', 651 | 'gif' => 'image/gif', 652 | 'bmp' => 'image/bmp', 653 | 'ico' => 'image/vnd.microsoft.icon', 654 | 'tiff' => 'image/tiff', 655 | 'tif' => 'image/tiff', 656 | 'svg' => 'image/svg+xml', 657 | 'svgz' => 'image/svg+xml', 658 | 659 | // archives 660 | 'zip' => 'application/zip', 661 | 'rar' => 'application/x-rar-compressed', 662 | 'exe' => 'application/x-msdownload', 663 | 'msi' => 'application/x-msdownload', 664 | 'cab' => 'application/vnd.ms-cab-compressed', 665 | 666 | // audio/video 667 | 'mp3' => 'audio/mpeg', 668 | 'qt' => 'video/quicktime', 669 | 'mov' => 'video/quicktime', 670 | 671 | // adobe 672 | 'pdf' => 'application/pdf', 673 | 'psd' => 'image/vnd.adobe.photoshop', 674 | 'ai' => 'application/postscript', 675 | 'eps' => 'application/postscript', 676 | 'ps' => 'application/postscript', 677 | 678 | // ms-office 679 | 'doc' => 'application/msword', 680 | 'rtf' => 'application/rtf', 681 | 'xls' => 'application/vnd.ms-excel', 682 | 'ppt' => 'application/vnd.ms-powerpoint', 683 | 684 | // open office 685 | 'odt' => 'application/vnd.oasis.opendocument.text', 686 | 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', 687 | ]; 688 | 689 | $arr = explode('.', $filename); 690 | $ext = strtolower(array_pop($arr)); 691 | if (array_key_exists($ext, $mime_types)) { 692 | return $mime_types[$ext]; 693 | } elseif (function_exists('finfo_open')) { 694 | $fileInfo = finfo_open(FILEINFO_MIME); 695 | if ($fileInfo) { 696 | $mimetype = finfo_file($fileInfo, $filename); 697 | finfo_close($fileInfo); 698 | 699 | return $mimetype; 700 | } 701 | } 702 | 703 | return 'application/octet-stream'; 704 | } 705 | 706 | /** 707 | * helper to format bytes to other units 708 | * 709 | * @param int $size in-bytes 710 | */ 711 | public static function formatBytes(int $size, int $precision = 2): string 712 | { 713 | $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 714 | $bytes = max($size, 0); 715 | $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); 716 | $pow = min($pow, count($units) - 1); 717 | $bytes /= pow(1024, $pow); 718 | 719 | return round($bytes, $precision).' '.$units[$pow]; 720 | } 721 | 722 | /** 723 | * converts KB,MB,GB,TB,PB,EB,ZB,YB to bytes 724 | * 725 | * 726 | * @example 1KB => 1000 (bytes) 727 | */ 728 | public static function convertToBytes(string $from): float|int|string 729 | { 730 | $number = (int) substr($from, 0, -2); 731 | 732 | return match (strtoupper(substr($from, -2))) { 733 | 'KB' => $number * 1024, 734 | 'MB' => $number * pow(1024, 2), 735 | 'GB' => $number * pow(1024, 3), 736 | 'TB' => $number * pow(1024, 4), 737 | 'PB' => $number * pow(1024, 5), 738 | 'EB' => $number * pow(1024, 6), 739 | 'ZB' => $number * pow(1024, 7), 740 | 'YB' => $number * pow(1024, 8), 741 | default => $from, 742 | }; 743 | } 744 | 745 | /** 746 | * Convert bytes to the unit specified by the $to parameter. 747 | * 748 | * @param int $bytes The filesize in Bytes. 749 | * @param string $to The unit type to convert to. Accepts KB, MB, GB, TB or PB for Kilobytes, Megabytes, Gigabytes, Terabytes or PetaBytes, respectively. 750 | * @param int $decimal_places The number of decimal places to return. 751 | * @return string Returns only the number of units, not the type letter. Returns 0 if the $to unit type is out of scope. 752 | * 753 | * @example 1024 (KB) => 1MB 754 | */ 755 | public static function convertBytesToSpecified(int $bytes, string $to = 'MB', int $decimal_places = 2): string 756 | { 757 | $formulas = [ 758 | 'KB' => number_format($bytes / 1024, $decimal_places), 759 | 'MB' => number_format($bytes / pow(1024, 2), $decimal_places), 760 | 'GB' => number_format($bytes / pow(1024, 3), $decimal_places), 761 | 'TB' => number_format($bytes / pow(1024, 4), $decimal_places), 762 | 'PB' => number_format($bytes / pow(1024, 5), $decimal_places), 763 | 'EB' => number_format($bytes / pow(1024, 6), $decimal_places), 764 | 'ZB' => number_format($bytes / pow(1024, 7), $decimal_places), 765 | 'YB' => number_format($bytes / pow(1024, 8), $decimal_places), 766 | ]; 767 | 768 | return isset($formulas[$to]) ? $formulas[$to].$to : 0 .$to; 769 | } 770 | } 771 | -------------------------------------------------------------------------------- /src/FileUploaderServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom( 18 | __DIR__.'/../config/file-uploader.php', 19 | static::$abstract 20 | ); 21 | } 22 | 23 | public function boot(): void 24 | { 25 | if ($this->app->runningInConsole()) { 26 | $this->publishes([ 27 | __DIR__.'/../config/file-uploader.php' => config_path(static::$abstract.'.php'), 28 | ], 'config'); 29 | } 30 | } 31 | 32 | /** 33 | * Get the services provided by the provider. 34 | * 35 | * @return array 36 | */ 37 | public function provides(): array 38 | { 39 | return [static::$abstract]; 40 | } 41 | } 42 | --------------------------------------------------------------------------------