├── .gitignore ├── CHANGELOG.md ├── README.md ├── composer.json └── src ├── Plugin.php ├── Volume.php ├── icon.svg └── templates └── volumeSettings.html /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **Warning** 2 | > NOT in active development. May work for your needs, but has several limitations. 3 | > If anyone cares to take of development, please contact me. 4 | 5 | Cloudinary for Craft CMS 3 6 | ======================= 7 | This plugin is an integration of the cloud-based image managment [cloudinary](https://cloudinary.com/) to your Craft3 project. 8 | 9 | ## Installation 10 | ### Cloudinary 11 | 1. Create a free [cloudinary account](https://cloudinary.com/) 12 | 2. Logged in to your account and go to the cloudinary dashboard 13 | 3. On top you can see your credentials (Cloud Name, API Key and API Secret). Copy them 14 | 15 | ### Install the plugin 16 | 1. `composer require timkelty/craft3-cloudinary` 17 | 2. Go to your Craft admin panel --> Settings --> Plugins and install the Cloudinary plugin 18 | 19 | ### Setup up the volume 20 | 3. In the Craft admin panel, go to Settings -> Assets 21 | 4. Create a new volume 22 | 5. Type in a name you like (f.e. `Cloud Images`) 23 | 6. Enable `Assets in this volume have public URLs` 24 | 7. You can decide on your Base URL (f.e. `/images` if you want to have `http://yourwebsite.com/images/` as your public image path) 25 | 8. Volume Type must be `Cloudinary` 26 | 9. Now fill in the Cloudinary credentials 27 | 10. Go to the Assets and upload the first image 28 | ![Craft Cloudinary asset volume settings](https://res.cloudinary.com/dsteinel/image/upload/v1532443782/craft-cloudinary-asset-volume-settings.png) 29 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timkelty/craft3-cloudinary", 3 | "description": "Cloudinary integration for Craft CMS", 4 | "type": "craft-plugin", 5 | "keywords": [ 6 | "cloudinary", 7 | "cms", 8 | "craftcms", 9 | "flysystem", 10 | "yii2" 11 | ], 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Tim Kelty", 16 | "homepage": "https://github.com/timkelty" 17 | } 18 | ], 19 | "require": { 20 | "craftcms/cms": "^3.0.0-beta.24", 21 | "enl/flysystem-cloudinary": "^1.0" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "craft\\cloudinary\\": "src/" 26 | } 27 | }, 28 | "extra": { 29 | "name": "Cloudinary", 30 | "handle": "cloudinary", 31 | "changelogUrl": "https://raw.githubusercontent.com/timkelty/craft3-cloudinary/master/CHANGELOG.md", 32 | "downloadUrl": "https://github.com/timkelty/craft3-cloudinary/archive/master.zip" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | 12 | * @since 3.0 13 | */ 14 | class Plugin extends \craft\base\Plugin 15 | { 16 | // Public Methods 17 | // ========================================================================= 18 | 19 | /** 20 | * @inheritdoc 21 | */ 22 | public function init() 23 | { 24 | parent::init(); 25 | 26 | Event::on(Volumes::class, Volumes::EVENT_REGISTER_VOLUME_TYPES, function (RegisterComponentTypesEvent $event) { 27 | $event->types[] = Volume::class; 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Volume.php: -------------------------------------------------------------------------------- 1 | 16 | * @since 3.0 17 | */ 18 | class Volume extends FlysystemVolume 19 | { 20 | // Static 21 | // ========================================================================= 22 | 23 | /** 24 | * @inheritdoc 25 | */ 26 | public static function displayName(): string 27 | { 28 | return 'Cloudinary'; 29 | } 30 | 31 | // Properties 32 | // ========================================================================= 33 | 34 | /** 35 | * @var bool Whether this is a local source or not. Defaults to false. 36 | */ 37 | protected $isSourceLocal = false; 38 | 39 | /** 40 | * @var string Path to the root of this sources local folder. 41 | */ 42 | public $subfolder = ''; 43 | 44 | /** 45 | * @var string Cloudinary API key 46 | */ 47 | public $apiKey = ''; 48 | 49 | /** 50 | * @var string Cloudinary API secret 51 | */ 52 | public $apiSecret = ''; 53 | 54 | /** 55 | * @var string Cloudinary cloud name to use 56 | */ 57 | public $cloudName = ''; 58 | 59 | /** 60 | * @var bool Overwrite existing files on Cloudinary 61 | */ 62 | public $overwrite = true; 63 | 64 | // Public Methods 65 | // ========================================================================= 66 | 67 | public function getFileMetadata(string $uri): array 68 | { 69 | return parent::getFileMetadata($this->_removeExtension($uri)); 70 | } 71 | 72 | /** 73 | * @inheritdoc 74 | */ 75 | public function createFileByStream(string $path, $stream, array $config) 76 | { 77 | parent::createFileByStream($this->_removeExtension($path), $stream, $config); 78 | } 79 | 80 | /** 81 | * @inheritdoc 82 | */ 83 | public function updateFileByStream(string $path, $stream, array $config) 84 | { 85 | parent::updateFileByStream($this->_removeExtension($path), $stream, $config); 86 | } 87 | 88 | /** 89 | * @inheritdoc 90 | */ 91 | public function createDir(string $path) 92 | { 93 | parent::createDir($this->_removeExtension($path)); 94 | } 95 | 96 | /** 97 | * @inheritdoc 98 | */ 99 | public function fileExists(string $path): bool 100 | { 101 | return parent::fileExists($this->_removeExtension($path)); 102 | } 103 | 104 | /** 105 | * @inheritdoc 106 | */ 107 | public function folderExists(string $path): bool 108 | { 109 | return parent::folderExists($this->_removeExtension($path)); 110 | } 111 | 112 | /** 113 | * @inheritdoc 114 | */ 115 | public function renameFile(string $path, string $newPath) 116 | { 117 | parent::renameFile($this->_removeExtension($path), $this->_removeExtension($newPath)); 118 | } 119 | 120 | /** 121 | * @inheritdoc 122 | */ 123 | public function deleteFile(string $path) 124 | { 125 | parent::deleteFile($this->_removeExtension($path)); 126 | } 127 | 128 | /** 129 | * @inheritdoc 130 | */ 131 | public function copyFile(string $path, string $newPath) 132 | { 133 | parent::copyFile($this->_removeExtension($path), $this->_removeExtension($newPath)); 134 | } 135 | 136 | /** 137 | * @inheritdoc 138 | */ 139 | public function getFileStream(string $uriPath) 140 | { 141 | return parent::getFileStream($this->_removeExtension($uriPath)); 142 | } 143 | 144 | /** 145 | * @inheritdoc 146 | */ 147 | public function init() 148 | { 149 | parent::init(); 150 | 151 | $this->foldersHaveTrailingSlashes = false; 152 | } 153 | 154 | /** 155 | * @inheritdoc 156 | */ 157 | public function rules() 158 | { 159 | $rules = parent::rules(); 160 | $rules[] = [['cloudName', 'apiKey', 'apiSecret'], 'required']; 161 | 162 | return $rules; 163 | } 164 | 165 | /** 166 | * @inheritdoc 167 | * 168 | * @return string|null 169 | */ 170 | public function getSettingsHtml() 171 | { 172 | return Craft::$app->getView()->renderTemplate('cloudinary/volumeSettings', [ 173 | 'volume' => $this 174 | ]); 175 | } 176 | 177 | /** 178 | * @inheritdoc 179 | */ 180 | public function getRootUrl() 181 | { 182 | return rtrim(rtrim($this->url, '/').'/'.$this->subfolder, '/').'/'; 183 | } 184 | 185 | // Protected Methods 186 | // ========================================================================= 187 | 188 | /** 189 | * @inheritdoc 190 | * 191 | * @return CloudinaryAdapter 192 | */ 193 | protected function createAdapter(): CloudinaryAdapter 194 | { 195 | $client = static::client([ 196 | 'cloud_name' => $this->cloudName, 197 | 'api_key' => $this->apiKey, 198 | 'api_secret' => $this->apiSecret, 199 | 'overwrite' => $this->overwrite, 200 | ]); 201 | 202 | return new CloudinaryAdapter($client); 203 | } 204 | 205 | protected static function client(array $config = []): CloudinaryClient 206 | { 207 | return new CloudinaryClient($config); 208 | } 209 | 210 | // Private Methods 211 | // ========================================================================= 212 | 213 | private function _removeExtension(string $path) 214 | { 215 | $pathInfo = pathinfo($path); 216 | 217 | return implode('/', [ 218 | $pathInfo['dirname'], 219 | $pathInfo['filename'], 220 | ]); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 13 | 30 | 49 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/templates/volumeSettings.html: -------------------------------------------------------------------------------- 1 | {% import "_includes/forms" as forms %} 2 | 3 | {{ forms.textField({ 4 | label: "API Key"|t('cloudinary'), 5 | id: 'apiKey', 6 | name: 'apiKey', 7 | value: volume.apiKey, 8 | errors: volume.getErrors('apiKey'), 9 | required: true 10 | }) }} 11 | 12 | {{ forms.textField({ 13 | label: "API Secret"|t('cloudinary'), 14 | id: 'apiSecret', 15 | name: 'apiSecret', 16 | value: volume.apiSecret, 17 | errors: volume.getErrors('apiSecret'), 18 | required: true 19 | }) }} 20 | 21 | {{ forms.textField({ 22 | label: "Cloud Name"|t('cloudinary'), 23 | id: 'cloudName', 24 | name: 'cloudName', 25 | value: volume.cloudName, 26 | errors: volume.getErrors('cloudName'), 27 | required: true 28 | }) }} 29 | --------------------------------------------------------------------------------