├── .github └── workflows │ └── run-tests.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── asset-cdn.php └── src ├── AssetCdnServiceProvider.php ├── Commands ├── BaseCommand.php ├── EmptyCommand.php ├── PushCommand.php └── SyncCommand.php ├── Config.php ├── Finder.php └── helpers.php /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: "Run Tests" 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | php: [7.1, 7.2, 7.3, 7.4, 8.0] 12 | laravel: [5.4.*, 5.5.*, 5.6.*, 5.7.*, 5.8.*, 6.*, 7.*, 8.*] 13 | include: 14 | - laravel: 5.4.* 15 | testbench: 3.4.* 16 | phpunit: ~5.7.21 17 | - laravel: 5.5.* 18 | testbench: 3.5.* 19 | phpunit: ~6.0 20 | - laravel: 5.6.* 21 | testbench: 3.6.* 22 | phpunit: ^7.0 23 | - laravel: 5.7.* 24 | testbench: 3.7.* 25 | phpunit: ^7.0 26 | - laravel: 5.8.* 27 | testbench: 3.8.* 28 | phpunit: ^7.5 29 | - laravel: 6.* 30 | testbench: 4.* 31 | phpunit: ^8.0 32 | - laravel: 7.* 33 | testbench: 5.* 34 | phpunit: ^8.4 35 | - laravel: 8.* 36 | testbench: 6.* 37 | phpunit: ^8.5 38 | exclude: 39 | - laravel: 6.* 40 | php: 7.1 41 | - laravel: 7.* 42 | php: 7.1 43 | - laravel: 8.* 44 | php: 7.1 45 | - laravel: 8.* 46 | php: 7.2 47 | - laravel: 8.* 48 | php: 7.3 49 | 50 | 51 | name: P${{ matrix.php }} - L${{ matrix.laravel }} 52 | 53 | steps: 54 | - name: Checkout code 55 | uses: actions/checkout@v2 56 | 57 | - name: Cache dependencies 58 | uses: actions/cache@v1 59 | with: 60 | path: ~/.composer/cache/files 61 | key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 62 | 63 | - name: Setup PHP 64 | uses: shivammathur/setup-php@v2 65 | with: 66 | php-version: ${{ matrix.php }} 67 | extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv 68 | coverage: xdebug 69 | 70 | - name: Install dependencies 71 | run: | 72 | composer require "orchestra/testbench:${{ matrix.testbench }}" "phpunit/phpunit:${{ matrix.phpunit }}" --no-interaction --no-update --dev 73 | composer update --prefer-dist --no-interaction --no-suggest 74 | 75 | - name: Execute tests 76 | run: vendor/bin/phpunit --coverage-clover=coverage.xml 77 | 78 | - name: Upload coverage to Codecov 79 | uses: codecov/codecov-action@v1 80 | with: 81 | file: ./coverage.xml -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `asset-cdn` will be documented in this file 4 | 5 | ## 1.0.0 - 201X-XX-XX 6 | 7 | - initial release 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) Christopher Lass 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 13 | > all 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 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | Latest Stable Version 5 | GitHub Workflow Status 6 | Codecov 7 | Quality Score 8 | Total Downloads 9 |

10 | 11 |

12 | Serve Laravel Assets from a Content Delivery Network (CDN) 13 |

14 | 15 | ## Introduction 16 | 17 | This package lets you **push**, **sync**, **delete** and **serve** assets to/from a CDN of your choice e.g. AWS Cloudfront. 18 | It adds helper methods **`mix_cdn()`** and **`asset_cdn()`**. 19 | 20 | #### Simple Illustration 21 | ```bash 22 | >>> env('USE_CDN') 23 | => true 24 | ``` 25 | ```bash 26 | $ php artisan asset-cdn:sync 27 | ``` 28 | ```php 29 | // head.blade.php 30 | 31 | ``` 32 | ```html 33 | 34 | 35 | ``` 36 | 37 | ## Installation 38 | Install this package via composer: 39 | 40 | ```bash 41 | $ composer require arubacao/asset-cdn 42 | ``` 43 | 44 | Also register the service provider: 45 | _Only required for Laravel `<=5.4`, for Laravel `>=5.5` [auto-discovery](composer.json#L45) is enabled._ 46 | ```PHP 47 | // config/app.php 48 | 49 | 'providers' => [ 50 | // Other Service Providers 51 | \Arubacao\AssetCdn\AssetCdnServiceProvider::class, 52 | ], 53 | ``` 54 | Notes: 55 | 56 | - `arubacao/asset-cdn` is functional and fully tested for Laravel `5.4` - `8.*` on PHP `7.0`, `7.1`, `7.2`, `7.3, 7.4` 57 | 58 | ## Configuration 59 | 60 | #### 1. Configure Filesystem 61 | 62 | _Only required if you plan to manage your assets via the provided commands: `asset-cdn:push`, `asset-cdn:sync`, `asset-cdn:empty`_ 63 | 64 | 65 | `arubacao/asset-cdn` utilizes [Laravel's Filesystem](https://laravel.com/docs/5.6/filesystem) to **push**, **sync**, **delete** assets to/from the CDN of your choice. 66 | Therefore, you have to configure and define a filesystem specific for CDN purposes. 67 | Please follow the [official documentation]((https://laravel.com/docs/5.6/filesystem)). 68 | 69 | If you plan to use AWS S3/Cloudfront you can use this configuration: 70 | ```php 71 | // config/filesystem.php 72 | 73 | 'asset-cdn' => [ 74 | 'driver' => 's3', 75 | 'key' => env('AWS_ACCESS_KEY_ID'), 76 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 77 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 78 | 'bucket' => env('AWS_CDN_BUCKET'), 79 | ], 80 | ``` 81 | 82 | #### 2. Publish Config File 83 | ```bash 84 | $ php artisan vendor:publish --provider="Arubacao\AssetCdn\AssetCdnServiceProvider" 85 | ``` 86 | 87 | #### 3. Edit `cdn_url` and `filesystem.disk` 88 | ```php 89 | // config/asset-cdn.php 90 | 91 | [ 92 | 'cdn_url' => 'https://cdn.mysite.com', 93 | 'filesystem' => [ 94 | 'disk' => 'asset-cdn', 95 | ], 96 | ] 97 | ``` 98 | 99 | #### 4. Edit `files` in `config/asset-cdn.php` 100 | _Only required if you plan to manage your assets via the provided commands: `asset-cdn:push`, `asset-cdn:sync`, `asset-cdn:empty`_ 101 | 102 | **`files` always assumes a relative path from the `public` directoy** 103 | 104 | - `ignoreDotFiles` 105 | Excludes "hidden" directories and files (starting with a dot). 106 | 107 | - `ignoreVCS` 108 | Ignore version control directories. 109 | 110 | - `include` 111 | **Any** file that matches at least one `include` rule, will be included. **No** file is included by default. 112 | 113 | * `paths` 114 | Define **paths** that should be available on the CDN. 115 | The following example will match **any** file in **any** `js` or `css` path it can find in the `public` directory. 116 | 117 | ```php 118 | 'include' => [ 119 | 'paths' => [ 120 | 'js', 121 | 'css' 122 | ], 123 | ] 124 | 125 | /* 126 | * This config would try to find: 127 | * '/var/www/html/public/js' 128 | * '/var/www/html/public/css' 129 | * but also any other 'js' or 'css' path e.g. 130 | * '/var/www/html/public/vendor/js' 131 | * '/var/www/html/public/vendor/css' 132 | * You could explicitly exclude paths later 133 | */ 134 | ``` 135 | 136 | * `files` 137 | Define **files** that should be available on the CDN. 138 | The following example will match **any** file that starts with `js/back.app.js` in the `public` directory. 139 | 140 | ```php 141 | 'include' => [ 142 | 'files' => [ 143 | 'js/app.js', 144 | ], 145 | ], 146 | 147 | /* 148 | * This config would try to find: 149 | * '/var/www/html/public/js/app.js' 150 | * but also any other file that matches the path + filename e.g. 151 | * '/var/www/html/public/vendor/js/app.js' 152 | * You could explicitly exclude these files later 153 | */ 154 | ``` 155 | 156 | * `extensions` 157 | Define **filetypes** that should be available on the CDN. 158 | The following example will match **any** file of type `*.css` or `*.js` in the `public` directory. 159 | 160 | ```php 161 | 'include' => [ 162 | 'extensions' => [ 163 | '*.js', 164 | '*.css', 165 | ], 166 | ], 167 | ``` 168 | 169 | * `patterns` 170 | Define **patterns** for files that should be available on the CDN. 171 | The following example will match **any** file that starts with letters `a` or `b` in the `public` directory. 172 | 173 | ```php 174 | /* 175 | * Patterns can be globs, strings, or regexes 176 | */ 177 | 178 | 'include' => [ 179 | 'patterns' => [ 180 | '/^[a-b]/i', // starting with letters a-b 181 | ], 182 | ], 183 | ``` 184 | 185 | - `exclude` 186 | **Any** file that matches at least one `exclude` rule, will be excluded. Files that are excluded will **never** be included, even if they have been explicitly included. 187 | Rules are identical as described above. 188 | 189 | 190 | #### 5. Set Additional Configurations for Uploaded Files 191 | 192 | `filesystem.options` are passed directly to the [Filesystem](https://github.com/thephpleague/flysystem/blob/1.0.43/src/FilesystemInterface.php#L232) 193 | which eventually calls the underlying Storage driver e.g. S3. 194 | Please refer to the corresponding storage driver documentation for available configuration options. 195 | The following example is recommended for AWS S3. 196 | 197 | ```php 198 | // config/asset-cdn.php 199 | 200 | [ 201 | 'filesystem' => [ 202 | 'disk' => 'asset-cdn', 203 | 'options' => [ 204 | 'ACL' => 'public-read', // File is available to the public, independent of the S3 Bucket policy 205 | 'CacheControl' => 'max-age=31536000, public', // Sets HTTP Header 'cache-control'. The client should cache the file for max 1 year 206 | ], 207 | ], 208 | ] 209 | ``` 210 | 211 | #### 6. Set Environment Variable `USE_CDN` 212 | ```dotenv 213 | # .env 214 | 215 | USE_CDN=true # Enables asset-cdn 216 | USE_CDN=false # Disables asset-cdn (default) 217 | ``` 218 | 219 | ## Usage 220 | 221 | #### Commands 222 | 223 | **Recommended** 224 | Sync assets that have been defined in the config to the CDN. Only pushes changes/new assets. Deletes locally removed files on CDN. 225 | ```bash 226 | $ php artisan asset-cdn:sync 227 | ``` 228 | 229 | Pushes assets that have been defined in the config to the CDN. Pushes all assets. Does not delete files on CDN. 230 | ```bash 231 | $ php artisan asset-cdn:push 232 | ``` 233 | 234 | Deletes all assets from CDN, independent from config file. 235 | ```bash 236 | $ php artisan asset-cdn:empty 237 | ``` 238 | 239 | #### Serving Assets 240 | Replace [`mix()`](https://laravel.com/docs/5.6/helpers#method-mix) with `mix_cdn()`. 241 | Replace [`asset()`](https://laravel.com/docs/5.6/helpers#method-asset) with `asset_cdn()`. 242 | 243 | 244 | ## Credits: 245 | Icon from [www.flaticon.com](http://www.flaticon.com/) 246 | Unmaintained git repo by [Vinelab](https://github.com/Vinelab/cdn) for inspiration only 247 | 248 | ## Todo's: 249 | 250 | - Video Tutorial: How to use S3/Cloudfront 251 | - Write test for `ignoreVCS` finder config 252 | - Write test for `ignoreDotFiles` finder config 253 | - Extend `CombinedFinderTest` 254 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arubacao/asset-cdn", 3 | "description": "Serve Laravel Assets from a Content Delivery Network (CDN)", 4 | "keywords": [ 5 | "Laravel", 6 | "CDN", 7 | "Content Delivery Network", 8 | "AWS Cloudfront" 9 | ], 10 | "homepage": "https://github.com/arubacao/asset-cdn", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Christopher Lass", 15 | "homepage": "https://c-lass.com/" 16 | } 17 | ], 18 | "require": { 19 | "php": "^7.0|^8.0", 20 | "laravel/framework": "~5.4|^6.0|^7.0|^8.0" 21 | }, 22 | "require-dev": { 23 | "league/flysystem-aws-s3-v3": "^1.0", 24 | "mockery/mockery": "^1.0", 25 | "orchestra/testbench": "~3.4|^4.0|^5.0|^6.0", 26 | "phpunit/phpunit": "~5.7|~6.0|^7.0|^8.0", 27 | "spatie/temporary-directory": "^1.1" 28 | }, 29 | "autoload": { 30 | "files": [ 31 | "src/helpers.php" 32 | ], 33 | "psr-4": { 34 | "Arubacao\\AssetCdn\\": "src" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Arubacao\\AssetCdn\\Test\\": "tests" 40 | } 41 | }, 42 | "config": { 43 | "sort-packages": true 44 | }, 45 | "extra": { 46 | "laravel": { 47 | "providers": [ 48 | "Arubacao\\AssetCdn\\AssetCdnServiceProvider" 49 | ] 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /config/asset-cdn.php: -------------------------------------------------------------------------------- 1 | env('USE_CDN', false), 6 | 7 | 'cdn_url' => '', 8 | 9 | 'filesystem' => [ 10 | 'disk' => 'asset-cdn', 11 | 12 | 'options' => [ 13 | // 14 | ], 15 | ], 16 | 17 | 'files' => [ 18 | 'ignoreDotFiles' => true, 19 | 20 | 'ignoreVCS' => true, 21 | 22 | 'include' => [ 23 | 'paths' => [ 24 | // 25 | ], 26 | 'files' => [ 27 | // 28 | ], 29 | 'extensions' => [ 30 | // 31 | ], 32 | 'patterns' => [ 33 | // 34 | ], 35 | ], 36 | 37 | 'exclude' => [ 38 | 'paths' => [ 39 | // 40 | ], 41 | 'files' => [ 42 | // 43 | ], 44 | 'extensions' => [ 45 | // 46 | ], 47 | 'patterns' => [ 48 | // 49 | ], 50 | ], 51 | ], 52 | 53 | ]; 54 | -------------------------------------------------------------------------------- /src/AssetCdnServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 20 | __DIR__.'/../config/asset-cdn.php' => config_path('asset-cdn.php'), 21 | ], 'config'); 22 | } 23 | 24 | /** 25 | * Register the service provider. 26 | * 27 | * @return void 28 | */ 29 | public function register() 30 | { 31 | $this->mergeConfigFrom(__DIR__.'/../config/asset-cdn.php', 'asset-cdn'); 32 | 33 | $this->app->singleton(Finder::class, function ($app) { 34 | return new Finder(new Config($app->make('config'), $app->make('path.public'))); 35 | }); 36 | 37 | $this->app->bind('command.asset-cdn:push', PushCommand::class); 38 | $this->app->bind('command.asset-cdn:sync', SyncCommand::class); 39 | $this->app->bind('command.asset-cdn:empty', EmptyCommand::class); 40 | 41 | $this->commands([ 42 | 'command.asset-cdn:push', 43 | 'command.asset-cdn:sync', 44 | 'command.asset-cdn:empty', 45 | ]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Commands/BaseCommand.php: -------------------------------------------------------------------------------- 1 | getRelativePathname(); 24 | }, $files); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Commands/EmptyCommand.php: -------------------------------------------------------------------------------- 1 | get('asset-cdn.filesystem.disk'); 35 | $filesOnCdn = $filesystemManager 36 | ->disk($filesystem) 37 | ->allFiles(); 38 | 39 | if ($filesystemManager 40 | ->disk($filesystem) 41 | ->delete($filesOnCdn)) { 42 | foreach ($filesOnCdn as $file) { 43 | $this->info("Successfully deleted: {$file}"); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Commands/PushCommand.php: -------------------------------------------------------------------------------- 1 | getFiles(); 38 | 39 | foreach ($files as $file) { 40 | $bool = $filesystemManager 41 | ->disk($config->get('asset-cdn.filesystem.disk')) 42 | ->putFileAs( 43 | $file->getRelativePath(), 44 | new File($file->getPathname()), 45 | $file->getFilename(), 46 | $config->get('asset-cdn.filesystem.options') 47 | ); 48 | 49 | if (! $bool) { 50 | $this->error("Problem uploading: {$file->getRelativePathname()}"); 51 | } else { 52 | $this->info("Successfully uploaded: {$file->getRelativePathname()}"); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Commands/SyncCommand.php: -------------------------------------------------------------------------------- 1 | filesystem = $config->get('asset-cdn.filesystem.disk'); 49 | $this->filesystemManager = $filesystemManager; 50 | $filesOnCdn = $this->filesystemManager 51 | ->disk($this->filesystem) 52 | ->allFiles(); 53 | $localFiles = $finder->getFiles(); 54 | $filesToDelete = $this->filesToDelete($filesOnCdn, $localFiles); 55 | $filesToSync = $this->filesToSync($filesOnCdn, $localFiles); 56 | 57 | foreach ($filesToSync as $file) { 58 | $bool = $this->filesystemManager 59 | ->disk($this->filesystem) 60 | ->putFileAs( 61 | $file->getRelativePath(), 62 | new File($file->getPathname()), 63 | $file->getFilename(), 64 | $config->get('asset-cdn.filesystem.options') 65 | ); 66 | 67 | if (! $bool) { 68 | $this->error("Problem uploading: {$file->getRelativePathname()}"); 69 | } else { 70 | $this->info("Successfully uploaded: {$file->getRelativePathname()}"); 71 | } 72 | } 73 | 74 | if ($this->filesystemManager 75 | ->disk($this->filesystem) 76 | ->delete($filesToDelete)) { 77 | foreach ($filesToDelete as $file) { 78 | $this->info("Successfully deleted: {$file}"); 79 | } 80 | } 81 | } 82 | 83 | /** 84 | * @param string[] $filesOnCdn 85 | * @param SplFileInfo[] $localFiles 86 | * @return SplFileInfo[] 87 | */ 88 | private function filesToSync(array $filesOnCdn, array $localFiles): array 89 | { 90 | $array = array_filter($localFiles, function (SplFileInfo $localFile) use ($filesOnCdn) { 91 | $localFilePathname = $localFile->getRelativePathname(); 92 | if (! in_array($localFilePathname, $filesOnCdn)) { 93 | return true; 94 | } 95 | 96 | $filesizeOfCdn = $this->filesystemManager 97 | ->disk($this->filesystem) 98 | ->size($localFilePathname); 99 | 100 | if ($filesizeOfCdn != $localFile->getSize()) { 101 | return true; 102 | } 103 | 104 | $md5OfCdn = md5( 105 | $this->filesystemManager 106 | ->disk($this->filesystem) 107 | ->get($localFilePathname) 108 | ); 109 | 110 | $md5OfLocal = md5_file($localFile->getRealPath()); 111 | 112 | if ($md5OfLocal != $md5OfCdn) { 113 | return true; 114 | } 115 | 116 | return false; 117 | }); 118 | 119 | return array_values($array); 120 | } 121 | 122 | /** 123 | * @param string[] $filesOnCdn 124 | * @param SplFileInfo[] $localFiles 125 | * @return string[] 126 | */ 127 | private function filesToDelete(array $filesOnCdn, array $localFiles): array 128 | { 129 | $localFiles = $this->mapToPathname($localFiles); 130 | 131 | $array = array_filter($filesOnCdn, function (string $fileOnCdn) use ($localFiles) { 132 | return ! in_array($fileOnCdn, $localFiles); 133 | }); 134 | 135 | return array_values($array); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | config = $config->get('asset-cdn.files'); 29 | $this->publicPath = $publicPath; 30 | } 31 | 32 | public function ignoreDotFiles(): bool 33 | { 34 | return $this->config['ignoreDotFiles']; 35 | } 36 | 37 | public function ignoreVCS(): bool 38 | { 39 | return $this->config['ignoreVCS']; 40 | } 41 | 42 | public function getIncludedPaths(): array 43 | { 44 | return $this->paths(self::INCLUDE); 45 | } 46 | 47 | public function getExcludedPaths(): array 48 | { 49 | return $this->paths(self::EXCLUDE); 50 | } 51 | 52 | public function getIncludedExtensions(): array 53 | { 54 | return $this->extensions(self::INCLUDE); 55 | } 56 | 57 | public function getExcludedExtensions(): array 58 | { 59 | return $this->extensions(self::EXCLUDE); 60 | } 61 | 62 | public function getIncludedPatterns(): array 63 | { 64 | return $this->config[self::INCLUDE]['patterns']; 65 | } 66 | 67 | public function getExcludedPatterns(): array 68 | { 69 | return $this->config[self::EXCLUDE]['patterns']; 70 | } 71 | 72 | public function getIncludedFiles(): array 73 | { 74 | return $this->files(self::INCLUDE); 75 | } 76 | 77 | public function getExcludedFiles(): array 78 | { 79 | return $this->files(self::EXCLUDE); 80 | } 81 | 82 | private function paths(string $type): array 83 | { 84 | return array_map( 85 | function ($path) { 86 | return $this->cleanPath($path).'/'; 87 | }, 88 | $this->config[$type]['paths'] 89 | ); 90 | } 91 | 92 | private function files(string $type): array 93 | { 94 | return array_map( 95 | function ($path) { 96 | return $this->cleanPath($path); 97 | }, 98 | $this->config[$type]['files'] 99 | ); 100 | } 101 | 102 | private function extensions(string $type): array 103 | { 104 | return array_map( 105 | function ($extension) { 106 | return '*'.$this->start($extension, '.'); 107 | }, 108 | $this->config[$type]['extensions'] 109 | ); 110 | } 111 | 112 | /** 113 | * Remove any extra slashes '/' from the path. 114 | * 115 | * @param string $path 116 | * @return string 117 | */ 118 | private function cleanPath(string $path): string 119 | { 120 | return rtrim(ltrim($path, '/'), '/'); 121 | } 122 | 123 | /** 124 | * Begin a string with a single instance of a given value. 125 | * 126 | * @param string $value 127 | * @param string $prefix 128 | * @return string 129 | */ 130 | private function start($value, $prefix) 131 | { 132 | $quoted = preg_quote($prefix, '/'); 133 | 134 | return $prefix.preg_replace('/^(?:'.$quoted.')+/u', '', $value); 135 | } 136 | 137 | /** 138 | * @return string 139 | */ 140 | public function getPublicPath(): string 141 | { 142 | return $this->publicPath; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Finder.php: -------------------------------------------------------------------------------- 1 | config = $config; 23 | } 24 | 25 | /** 26 | * Get all of the files from the given directory (recursive). 27 | * 28 | * @return \Symfony\Component\Finder\SplFileInfo[] 29 | */ 30 | public function getFiles() 31 | { 32 | $pathFinder = $this->excluded(); 33 | $nameFinder = clone $pathFinder; 34 | 35 | $includedPaths = $this->includedPaths($pathFinder); 36 | $includedNames = $this->includedNames($nameFinder); 37 | 38 | return $this->mergeFileInfos($includedPaths, $includedNames); 39 | } 40 | 41 | private function getBaseFinder(): SymfonyFinder 42 | { 43 | return SymfonyFinder::create() 44 | ->files() 45 | ->in($this->config->getPublicPath()) 46 | ->ignoreDotFiles($this->config->ignoreDotFiles()) 47 | ->ignoreVCS($this->config->ignoreVCS()); 48 | } 49 | 50 | /** 51 | * @param \Symfony\Component\Finder\Finder $pathFinder 52 | * @return \Symfony\Component\Finder\SplFileInfo[] 53 | */ 54 | private function includedPaths(SymfonyFinder $pathFinder): array 55 | { 56 | /** 57 | * Include directories. 58 | * @see http://symfony.com/doc/current/components/finder.html#location 59 | */ 60 | $includedPaths = $this->config->getIncludedPaths(); 61 | foreach ($includedPaths as $path) { 62 | $pathFinder->path($path); 63 | } 64 | 65 | /** 66 | * Include Files. 67 | * @see http://symfony.com/doc/current/components/finder.html#file-name 68 | */ 69 | $includedFiles = $this->config->getIncludedFiles(); 70 | foreach ($includedFiles as $file) { 71 | $pathFinder->path($file); 72 | } 73 | 74 | if (empty($includedPaths) && empty($includedFiles)) { 75 | $pathFinder->notPath(''); 76 | } 77 | 78 | return iterator_to_array( 79 | $pathFinder, 80 | false 81 | ); 82 | } 83 | 84 | /** 85 | * @param \Symfony\Component\Finder\Finder $nameFinder 86 | * @return \Symfony\Component\Finder\SplFileInfo[] 87 | */ 88 | private function includedNames(SymfonyFinder $nameFinder): array 89 | { 90 | /** 91 | * Include Extensions. 92 | * @see http://symfony.com/doc/current/components/finder.html#file-name 93 | */ 94 | $includedExtensions = $this->config->getIncludedExtensions(); 95 | foreach ($includedExtensions as $extension) { 96 | $nameFinder->name($extension); 97 | } 98 | 99 | /** 100 | * Include Patterns - globs, strings, or regexes. 101 | * @see http://symfony.com/doc/current/components/finder.html#file-name 102 | */ 103 | $includedPatterns = $this->config->getIncludedPatterns(); 104 | foreach ($includedPatterns as $pattern) { 105 | $nameFinder->name($pattern); 106 | } 107 | 108 | if (empty($includedExtensions) && empty($includedPatterns)) { 109 | $nameFinder->notPath(''); 110 | } 111 | 112 | return iterator_to_array( 113 | $nameFinder, 114 | false 115 | ); 116 | } 117 | 118 | private function excluded() 119 | { 120 | $finder = $this->getBaseFinder(); 121 | 122 | /* 123 | * Exclude directories 124 | * @see http://symfony.com/doc/current/components/finder.html#location 125 | */ 126 | $finder->exclude($this->config->getExcludedPaths()); 127 | 128 | /* 129 | * Exclude Files 130 | * @see http://symfony.com/doc/current/components/finder.html#file-name 131 | */ 132 | foreach ($this->config->getExcludedFiles() as $file) { 133 | $finder->notPath($file); 134 | } 135 | 136 | /* 137 | * Exclude Extensions 138 | * @see http://symfony.com/doc/current/components/finder.html#file-name 139 | */ 140 | foreach ($this->config->getExcludedExtensions() as $pattern) { 141 | $finder->notName($pattern); 142 | } 143 | 144 | /* 145 | * Exclude Patterns - globs, strings, or regexes 146 | * @see http://symfony.com/doc/current/components/finder.html#file-name 147 | */ 148 | foreach ($this->config->getExcludedPatterns() as $pattern) { 149 | $finder->notName($pattern); 150 | } 151 | 152 | return $finder; 153 | } 154 | 155 | /** 156 | * @param \Symfony\Component\Finder\SplFileInfo[] $includedPaths 157 | * @param \Symfony\Component\Finder\SplFileInfo[] $includedNames 158 | * @return \Symfony\Component\Finder\SplFileInfo[] 159 | */ 160 | private function mergeFileInfos(array $includedPaths, array $includedNames): array 161 | { 162 | return collect(array_merge($includedPaths, $includedNames)) 163 | ->unique(function (SplFileInfo $file) { 164 | return $file->getPathname(); 165 | }) 166 | ->values() 167 | ->toArray(); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 |