pthumbcachecleaner.class.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. <?php
  2. /**
  3. * pThumb
  4. * Copyright 2013, 2014 Jason Grant
  5. *
  6. * Please see the GitHub page for documentation or to report bugs:
  7. * https://github.com/oo12/phpThumbOf
  8. *
  9. * pThumb is free software; you can redistribute it and/or modify it
  10. * under the terms of the GNU General Public License as published by the Free
  11. * Software Foundation; either version 2 of the License, or (at your option) any
  12. * later version.
  13. *
  14. * pThumb is distributed in the hope that it will be useful, but WITHOUT ANY
  15. * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  16. * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License along with
  19. * phpThumbOf; if not, write to the Free Software Foundation, Inc., 59 Temple
  20. * Place, Suite 330, Boston, MA 02111-1307 USA
  21. */
  22. require_once MODX_CORE_PATH . 'components/phpthumbof/model/phpthumbof.class.php';
  23. /*
  24. * Used for recursing through the pThumb Cache
  25. */
  26. class FilenameFilter extends RecursiveRegexIterator {
  27. protected $regex;
  28. function __construct(RecursiveIterator $it, $regex) {
  29. $this->regex = $regex;
  30. parent::__construct($it, $regex);
  31. }
  32. function accept() {
  33. return (!$this->isFile() || preg_match($this->regex, $this->getFilename()));
  34. }
  35. }
  36. /*
  37. * Extends main pThumb class to add a cache cleaning method
  38. * Used by phpThumbOfCacheManager
  39. */
  40. class pThumbCacheCleaner extends phpThumbOf {
  41. private function pluralize($count, $thing = 'image') {
  42. return $count == 1 ? "1 $thing" : "$count $thing" . 's';
  43. }
  44. private function rmfile($file, $isS3, $s3obj) {
  45. if ($isS3) {
  46. $response = $s3obj->driver->delete_object($s3obj->bucket, $file);
  47. return $response->isOK();
  48. }
  49. else { return @unlink($file); }
  50. }
  51. /*
  52. * Clean up the pThumb cache directories
  53. * Adapted from phpThumb ( http://phpthumb.sourceforge.net/ )
  54. * Clean Levels:
  55. * 0: no cleaning
  56. * 1: clean based on system phpThumb cache settings
  57. * 2: remove all cached images
  58. */
  59. public function cleanCache() {
  60. $config['clean_level'] = (int) $this->modx->getOption('pthumb.clean_level', null, 0);
  61. $config['cache_maxage'] = $this->modx->getOption('phpthumb_cache_maxage', null, 365);
  62. $description['maxage'] = $this->pluralize($config['cache_maxage'], 'day');
  63. $config['cache_maxage'] *= 86400; // convert to seconds
  64. $config['cache_maxsize'] = $this->modx->getOption('phpthumb_cache_maxsize', null, 300);
  65. $description['maxsize'] = "{$config['cache_maxsize']} MB";
  66. $config['cache_maxsize'] *= 1048576; // convert to bytes
  67. $config['cache_maxfiles'] = (int) $this->modx->getOption('phpthumb_cache_maxfiles', null, 10000);
  68. $this->modx->log(modX::LOG_LEVEL_INFO, "[pThumb Cache Manager] Clean Level: {$config['clean_level']} || Max Age: {$description['maxage']} || Max Size: {$description['maxsize']} || Max Files: {$config['cache_maxfiles']}");
  69. if ($config['clean_level'] < 1 || ($config['clean_level'] === 1 && !($config['cache_maxage'] || $config['cache_maxsize'] || $config['cache_maxfiles']))) {
  70. $this->modx->log(modX::LOG_LEVEL_INFO, ':: Skipping cache cleanup based on settings');
  71. $this->modx->log(modX::LOG_LEVEL_INFO, '');
  72. return; // that was easy.
  73. }
  74. $cachepath = array(); // gather up cache paths
  75. $cachepath['pThumb'] = $this->modx->getOption('pthumb.ptcache_location', null, 'assets/image-cache', true);
  76. if ($cachepath['pThumb'] === '/') { // for safety, pThumb cache location has to be a subdir, can't be the web root
  77. $cachepath['pThumb'] = 'assets/image-cache';
  78. }
  79. $cachepath['pThumb'] = MODX_BASE_PATH . $cachepath['pThumb'];
  80. $cachepath['phpThumbOf'] = $this->modx->getOption('phpthumbof.cache_path', null, "{$this->config['assetsPath']}components/phpthumbof/cache", true);
  81. $cachepath['Remote Images'] = $this->config['remoteImagesCachePath'];
  82. foreach ($cachepath as $path) {
  83. $path = rtrim(str_replace('//', '/', $path), '/'); // normalize path
  84. if (!is_dir($path)) {
  85. $path = false;
  86. }
  87. }
  88. $cachefiles = array(); // gather up cache files
  89. foreach (array('pThumb', 'Remote Images') as $cachename) {
  90. if (is_writeable($cachepath[$cachename])) { // recurse through all subdirectories looking for jpeg, jpg, png and gif
  91. $filter = new FilenameFilter(
  92. new RecursiveDirectoryIterator($cachepath[$cachename], FilesystemIterator::SKIP_DOTS),
  93. $cachename === 'pThumb' ? '/.+\.[0-9a-f]{8}\.(jpg|png|gif)$/' : '/\.(?:jpe?g|png|gif)$/i' // for pThumb cache, only select images with what appears to be an 8-character hash
  94. );
  95. $cachefiles[$cachename] = array();
  96. foreach(new RecursiveIteratorIterator($filter) as $file) {
  97. $cachefiles[$cachename][] = $file->getPathName();
  98. }
  99. }
  100. }
  101. if ($cachepath['phpThumbOf']) {
  102. if ( ! $cachefiles['phpThumbOf'] = glob("{$cachepath['phpThumbOf']}/*.{jp*g,png,gif}", GLOB_BRACE)) {
  103. $cachefiles['phpThumbOf'] = array(); // empty array if glob didn't find anything
  104. }
  105. }
  106. $s3out = false;
  107. if ($this->config['s3outputMS']) {
  108. $s3out =& $this->config[$this->config['s3outKey']];
  109. $cachefiles['S3'] = array_keys($this->config[$this->config['s3outKey'] . '_images']);
  110. }
  111. foreach ($cachefiles as $cachename => $fileset) {
  112. $isS3 = $cachename === 'S3';
  113. $totalimages = count($fileset);
  114. $DeletedKeys = array();
  115. $CacheDirOldFilesAge = array();
  116. $CacheDirOldFilesSize = array();
  117. if ($isS3) {
  118. foreach($this->config[$this->config['s3outKey'] . '_images'] as $k => $v) {
  119. $CacheDirOldFilesAge[$k] = $v['mod'];
  120. $CacheDirOldFilesSize[$k] = $v['size'];
  121. }
  122. }
  123. else {
  124. foreach ($fileset as $fullfilename) { // get accessed (or modified) time and size for each file
  125. if ( ! $CacheDirOldFilesAge[$fullfilename] = @fileatime($fullfilename)) {
  126. $CacheDirOldFilesAge[$fullfilename] = @filemtime($fullfilename); // fall back to filemtime if fileatime doesn't work
  127. }
  128. $CacheDirOldFilesSize[$fullfilename] = @filesize($fullfilename);
  129. }
  130. }
  131. $this->modx->log(modX::LOG_LEVEL_INFO, ":: $cachename Cache: " . $this->pluralize($totalimages) . ' (' . round(array_sum($CacheDirOldFilesSize) / 1048576, 2) . ' MB)');
  132. if ($config['clean_level'] === 2) { // simply delete all the files
  133. $deleted = 0;
  134. if ($isS3) {
  135. $response = $s3out->driver->delete_all_objects($s3out->bucket, $this->cacheimgRegex);
  136. if ($response) {
  137. $deleted = $totalimages;
  138. }
  139. }
  140. else {
  141. foreach ($fileset as $file) {
  142. if (@unlink($file)) {
  143. ++$deleted;
  144. }
  145. }
  146. }
  147. $this->modx->log(modX::LOG_LEVEL_INFO, ':: ' . $this->pluralize($deleted, 'file') . ' purged');
  148. }
  149. else { // clean up based on system phpThumb settings
  150. $madedeletions = false;
  151. $DeletedKeys['zerobyte'] = 0;
  152. foreach ($CacheDirOldFilesSize as $fullfilename => $filesize) { // remove any 0-byte files
  153. // but only if they're more than 10 min old (to prevent trying to delete just-created or in-use files)
  154. $cutofftime = time() - 600;
  155. if (!$filesize && $CacheDirOldFilesAge[$fullfilename] < $cutofftime) {
  156. if ($this->rmfile($fullfilename, $isS3, $s3out)) {
  157. ++$DeletedKeys['zerobyte'];
  158. unset($CacheDirOldFilesSize[$fullfilename]);
  159. unset($CacheDirOldFilesAge[$fullfilename]);
  160. }
  161. }
  162. }
  163. if ($DeletedKeys['zerobyte']) {
  164. $this->modx->log(modX::LOG_LEVEL_INFO, ':: Purged ' . $this->pluralize($DeletedKeys['zerobyte'], 'zero-byte image'));
  165. $madedeletions = true;
  166. }
  167. asort($CacheDirOldFilesAge); // all deletions start with the least recently accesed (or oldest) files first
  168. if ($config['cache_maxage']) { // delete any files older that maxage
  169. $mindate = time() - $config['cache_maxage'];
  170. $DeletedKeys['maxage'] = 0;
  171. foreach ($CacheDirOldFilesAge as $fullfilename => $filedate) {
  172. if ($filedate) {
  173. if ($filedate < $mindate) {
  174. if ($this->rmfile($fullfilename, $isS3, $s3out)) {
  175. ++$DeletedKeys['maxage'];
  176. unset($CacheDirOldFilesAge[$fullfilename]);
  177. unset($CacheDirOldFilesSize[$fullfilename]);
  178. }
  179. }
  180. else { // the rest of the files are new enough to keep
  181. break;
  182. }
  183. }
  184. }
  185. if ($DeletedKeys['maxage']) {
  186. $this->modx->log(modX::LOG_LEVEL_INFO, ':: Purged ' . $this->pluralize($DeletedKeys['maxage']) . " based on (cache_maxage={$description['maxage']})");
  187. $madedeletions = true;
  188. }
  189. }
  190. if ($config['cache_maxfiles']) { // delete any files in excess of maxfiles
  191. $TotalCachedFiles = count($CacheDirOldFilesAge);
  192. $DeletedKeys['maxfiles'] = 0;
  193. foreach ($CacheDirOldFilesAge as $fullfilename => $filedate) {
  194. if ($TotalCachedFiles > $config['cache_maxfiles']) {
  195. if ($this->rmfile($fullfilename, $isS3, $s3out)) {
  196. --$TotalCachedFiles;
  197. ++$DeletedKeys['maxfiles'];
  198. unset($CacheDirOldFilesAge[$fullfilename]);
  199. unset($CacheDirOldFilesSize[$fullfilename]);
  200. }
  201. }
  202. else { // there are few enough files to keep the rest
  203. break;
  204. }
  205. }
  206. if ($DeletedKeys['maxfiles']) {
  207. $this->modx->log(modX::LOG_LEVEL_INFO, ':: Purged ' . $this->pluralize($DeletedKeys['maxfiles']) . " based on (cache_maxfiles={$config['cache_maxfiles']})");
  208. $madedeletions = true;
  209. }
  210. }
  211. if ($config['cache_maxsize']) { // delete files to get the total cache size under the maxsize limit
  212. $TotalCachedFileSize = array_sum($CacheDirOldFilesSize);
  213. $DeletedKeys['maxsize'] = 0;
  214. foreach ($CacheDirOldFilesAge as $fullfilename => $filedate) {
  215. if ($TotalCachedFileSize > $config['cache_maxsize']) {
  216. if ($this->rmfile($fullfilename, $isS3, $s3out)) {
  217. $TotalCachedFileSize -= $CacheDirOldFilesSize[$fullfilename];
  218. ++$DeletedKeys['maxsize'];
  219. unset($CacheDirOldFilesAge[$fullfilename]);
  220. unset($CacheDirOldFilesSize[$fullfilename]);
  221. }
  222. }
  223. else { // the total filesizes are small enough to keep the rest of the files
  224. break;
  225. }
  226. }
  227. if ($DeletedKeys['maxsize']) {
  228. $this->modx->log(modX::LOG_LEVEL_INFO, ':: Purged ' . $this->pluralize($DeletedKeys['maxsize']) . " based on (cache_maxsize={$description['maxsize']})");
  229. $madedeletions = true;
  230. }
  231. }
  232. }
  233. $DeletedSubdirs = 0;
  234. if ($cachename !== 'phpThumbOf' && !$isS3 && is_writable($cachepath[$cachename])) { // remove any empty subdirs in pThumb and Remote Images caches
  235. $ritit = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($cachepath[$cachename], FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);
  236. foreach ($ritit as $dirname => $pathObj) {
  237. if ($pathObj->isDir() && iterator_count($ritit->getSubIterator()->getChildren()) === 0) {
  238. rmdir($dirname);
  239. ++$DeletedSubdirs;
  240. }
  241. }
  242. }
  243. if ($config['clean_level'] != 2) {
  244. $totalpurged = 0;
  245. foreach ($DeletedKeys as $value) {
  246. $totalpurged += $value;
  247. }
  248. $this->modx->log(modX::LOG_LEVEL_INFO, ':: Purged ' . $this->pluralize($totalpurged) . ($DeletedSubdirs ? ', ' . $this->pluralize($DeletedSubdirs, 'empty subdir') : '') . ($madedeletions ? ' || New cache size: ' . $this->pluralize(count($CacheDirOldFilesSize)) . ' (' . round(array_sum($CacheDirOldFilesSize) / 1048576, 2) . ' MB)': ''));
  249. }
  250. }
  251. $this->modx->log(modX::LOG_LEVEL_INFO, '');
  252. }
  253. }