├── README.markdown ├── app ├── code │ └── community │ │ └── Netzarbeiter │ │ └── Cache │ │ ├── Block │ │ └── Adminhtml │ │ │ └── Cache.php │ │ ├── Helper │ │ └── Data.php │ │ ├── Model │ │ └── Symlink.php │ │ ├── Test │ │ └── Model │ │ │ ├── Symlink.php │ │ │ └── Symlink │ │ │ └── fixtures │ │ │ └── backend.yaml │ │ ├── controllers │ │ └── Adminhtml │ │ │ └── Netzarbeiter │ │ │ └── CacheController.php │ │ ├── etc │ │ ├── adminhtml.xml │ │ └── config.xml │ │ ├── readme.txt │ │ └── shell │ │ ├── benchmark.php │ │ └── initTags.php ├── design │ └── adminhtml │ │ └── default │ │ └── default │ │ ├── layout │ │ └── netzarbeiter │ │ │ └── cache.xml │ │ └── template │ │ └── netzarbeiter │ │ └── cache │ │ └── tool.phtml └── etc │ └── modules │ └── Netzarbeiter_Cache.xml └── modman /README.markdown: -------------------------------------------------------------------------------- 1 | ## NOT MAINTAINED ## 2 | 3 | This extension is no longer maintained and is only left here to point anybody visiting to the successor extension here: https://github.com/colinmollenhour/Cm_Cache_Backend_File 4 | 5 | ## Old README: ## 6 | 7 | This module is intended to improve the performance of the file cache backend 8 | with large cache pools (as fast or slow backend). 9 | 10 | There is a post on [MageBase][] about it which contains more details: 11 | 12 | [MageBase]: http://magebase.com/magento-tutorials/improving-the-file-cache-backend/ 13 | 14 | This module is provided with no warranty, you use it at your own risk. 15 | I know it is being used successfully on several sites, both as a primary cache 16 | backend and as a slow backend in combination with APC or memcached. 17 | I invite you to have a look at it and try it out, but please start with a test 18 | instance and not your live store. 19 | 20 | If you find bugs or have improvements, please send them in. 21 | 22 | 23 | According to the [php reference manual][] symlinks works under Linux or since PHP 5.3 under Windows 24 | Vista/Windows Server 2008 or greater (see in the php manual under the section titled Notes). 25 | 26 | [php reference manual]: http://php.net/manual/en/function.symlink.php 27 | 28 | After installation, clear the cache and check under "System > Tools > Symlink 29 | Cache" for further instructions. 30 | -------------------------------------------------------------------------------- /app/code/community/Netzarbeiter/Cache/Block/Adminhtml/Cache.php: -------------------------------------------------------------------------------- 1 | isSymlinkCacheInUse()) 34 | { 35 | $this->setChild('initSymlinksButton', 36 | $this->getLayout()->createBlock('adminhtml/widget_button') 37 | ->setData(array( 38 | 'label' => $this->__('Initialize Tag Symlinks'), 39 | 'onclick' => "window.location.href='" . $this->getUrl('*/*/initSymlinks') . "'", 40 | 'class' => 'task' 41 | )) 42 | ); 43 | } 44 | return $this; 45 | } 46 | 47 | public function getFastBackend() 48 | { 49 | return (string) Mage::getConfig()->getNode('global/cache/backend'); 50 | } 51 | 52 | public function getSlowBackend() 53 | { 54 | return (string) Mage::getConfig()->getNode('global/cache/slow_backend'); 55 | } 56 | 57 | public function isSymlinkCacheInUse() 58 | { 59 | return $this->getFastBackend() == $this->_symlinkCacheClass || $this->getSlowBackend() == $this->_symlinkCacheClass; 60 | } 61 | 62 | public function getSymlinkCacheStatus() 63 | { 64 | if ($this->isSymlinkCacheInUse()) 65 | { 66 | return $this->__('enabled'); 67 | } 68 | return $this->__('disabled'); 69 | } 70 | 71 | public function getResultInfo() 72 | { 73 | $data = Mage::getSingleton('adminhtml/session')->getResultInfo(true); 74 | return (array) $data; 75 | } 76 | 77 | public function getFastBackendInstructions() 78 | { 79 | $string = << 81 | 82 | 83 | Netzarbeiter_Cache_Model_Symlink 84 | 85 | var/cache 86 | 1 87 | 0777 88 | mage 89 | 90 | 91 | 92 | 93 | EOT; 94 | return $string; 95 | } 96 | 97 | public function getSlowBackendInstructions() 98 | { 99 | $string = << 101 | 102 | 103 | Netzarbeiter_Cache_Model_Symlink 104 | 105 | 106 | 107 | EOT; 108 | return $string; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/code/community/Netzarbeiter/Cache/Helper/Data.php: -------------------------------------------------------------------------------- 1 | _cacheDir = Mage::getBaseDir('cache'); 70 | $this->_tagDir = $this->_cacheDir . DS . $this->_tagDirName; 71 | } 72 | 73 | /** 74 | * Initialize the cache metadata by tag symlinks 75 | * 76 | * @return Netzarbeiter_Cache_Helper_Data 77 | */ 78 | public function initTagSymlinks() 79 | { 80 | $this->_result = array(); 81 | 82 | $this->_removeTagLinks(); 83 | 84 | $files = $this->getRealMetadataFiles(); 85 | foreach ($files as $file) 86 | { 87 | $metadata = $this->_readMetadatas($file); 88 | if ($metadata && is_array($metadata) && isset($metadata['tags'])) 89 | { 90 | $this->_createLinks($file, $metadata['tags']); 91 | } 92 | } 93 | return $this; 94 | } 95 | /** 96 | * Return an array with all metadata files in the file system cache 97 | * 98 | * @return array 99 | */ 100 | public function getRealMetadataFiles() 101 | { 102 | $files = array(); 103 | // @hack: assume hashed_directory_level == 1 104 | $cacheSubDirs = @glob("{$this->_cacheDir}/{$this->_fileNamePrefix}--*"); 105 | if ($cacheSubDirs) 106 | { 107 | foreach ($cacheSubDirs as $subDir) 108 | { 109 | $glob = @glob("{$subDir}/{$this->_fileNamePrefix}---internal-metadatas---*"); 110 | if ($glob) 111 | { 112 | $files = array_merge($files, $glob); 113 | } 114 | } 115 | } 116 | return $files; 117 | } 118 | 119 | /** 120 | * Return the unserialized contents of a metadata file or false on error. 121 | * 122 | * @param string $file 123 | * @return array|false 124 | */ 125 | protected function _readMetadatas($file) 126 | { 127 | $metadata = false; 128 | if (!is_readable($file)) 129 | { 130 | throw new Exception($this->__("Unable to read metadatas file %s", $file)); 131 | } 132 | $serialized = file_get_contents($file); 133 | if (is_string($serialized) && strlen($serialized) > 0) 134 | { 135 | $metadata = unserialize($serialized); 136 | } 137 | return $metadata; 138 | } 139 | 140 | /** 141 | * Create symlinks to the $target dile in every $tag directory 142 | * 143 | * @param string $target Target file 144 | * @param array $tags 145 | * @return bool 146 | */ 147 | protected function _createLinks($target, array $tags) 148 | { 149 | $basename = basename($target); 150 | $result = true; 151 | foreach ($tags as $tag) 152 | { 153 | $dir = $this->_tagDir . DS . str_replace(DS, '-', $tag); 154 | $link = $dir . DS . $basename; 155 | if (file_exists($link)) 156 | { 157 | @unlink($link); 158 | } 159 | if (! is_dir($dir)) 160 | { 161 | @mkdir($dir, 0777, true); 162 | } 163 | if ($result = $result && @symlink($target, $link)) 164 | { 165 | $this->_result[] = array( 166 | 'target' => basename($target), 167 | 'tag' => $tag 168 | ); 169 | } 170 | } 171 | return $result; 172 | } 173 | 174 | /** 175 | * Remove all existing tag symlinks 176 | * 177 | * @return bool 178 | */ 179 | protected function _removeTagLinks() 180 | { 181 | $result = true; 182 | if (file_exists($this->_tagDir) && is_dir($this->_tagDir)) 183 | { 184 | $tags = scandir($this->_tagDir); 185 | if ($tags) 186 | { 187 | foreach ($tags as $tag) { 188 | $dir = $this->_tagDir . DS . $tag; 189 | if (! is_dir($dir) || in_array($tag, array('.', '..'))) 190 | { 191 | continue; 192 | } 193 | $links = scandir($dir); 194 | if ($links) 195 | { 196 | foreach ($links as $linkName) 197 | { 198 | $link = $dir . DS . $linkName; 199 | if (is_link($link)) 200 | { 201 | $result = $result && @unlink($link); 202 | } 203 | } 204 | } 205 | } 206 | } 207 | } 208 | return $result; 209 | } 210 | 211 | /** 212 | * 213 | * @return array 214 | */ 215 | public function getResults() 216 | { 217 | return $this->_result; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /app/code/community/Netzarbeiter/Cache/Model/Symlink.php: -------------------------------------------------------------------------------- 1 | _getFilesMatchingAnyTags($tags); 66 | } 67 | elseif ('matching' === $mode) 68 | { 69 | $glob = $this->_getFilesMatchingAllTags($tags); 70 | } 71 | elseif ('notMatching' === $mode) 72 | { 73 | $glob = $this->_getFilesNotMatchingTags($tags); 74 | } 75 | elseif ('tags' === $mode) 76 | { 77 | return $this->_getAllTags(); 78 | } 79 | else 80 | { 81 | $prefix = $this->_options['file_name_prefix']; 82 | $glob = @glob($dir . $prefix . '--*'); 83 | if ($glob === false) 84 | { 85 | // On some systems it is impossible to distinguish between empty match and an error. 86 | return array(); 87 | } 88 | } 89 | foreach ($glob as $file) 90 | { 91 | if ($this->_isFile($file)) 92 | { 93 | $fileName = basename($file); 94 | $id = $this->_fileNameToId($fileName); 95 | $metadatas = $this->_getMetadatas($id); 96 | if ($metadatas === false) 97 | { 98 | continue; 99 | } 100 | if (time() > $metadatas['expire']) 101 | { 102 | continue; 103 | } 104 | switch ($mode) 105 | { 106 | case 'ids': 107 | case 'matching': 108 | case 'notMatching': 109 | case 'matchingAny': 110 | $result[] = $id; 111 | break; 112 | case 'tags': 113 | $result = array_unique(array_merge($result, $metadatas['tags'])); 114 | break; 115 | default: 116 | Zend_Cache::throwException('Invalid mode for _get() method'); 117 | break; 118 | } 119 | } 120 | elseif ((is_dir($file)) && ($this->_options['hashed_directory_level'] > 0)) 121 | { 122 | // Recursive call 123 | $recursiveRs = $this->_get($file . DIRECTORY_SEPARATOR, $mode, $tags); 124 | if ($recursiveRs === false) 125 | { 126 | $this->_log('Zend_Cache_Backend_File::_get() / recursive call : can\'t list entries of "' . $file . '"'); 127 | } 128 | else 129 | { 130 | $result = array_unique(array_merge($result, $recursiveRs)); 131 | } 132 | } 133 | } 134 | return array_unique($result); 135 | } 136 | 137 | /** 138 | * Clean some cache records (protected method used for recursive stuff) 139 | * 140 | * Available modes are : 141 | * Zend_Cache::CLEANING_MODE_ALL (default) => remove all cache entries ($tags is not used) 142 | * Zend_Cache::CLEANING_MODE_OLD => remove too old cache entries ($tags is not used) 143 | * Zend_Cache::CLEANING_MODE_MATCHING_TAG => remove cache entries matching all given tags 144 | * ($tags can be an array of strings or a single string) 145 | * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags} 146 | * ($tags can be an array of strings or a single string) 147 | * Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags 148 | * ($tags can be an array of strings or a single string) 149 | * 150 | * @param string $dir Directory to clean 151 | * @param string $mode Clean mode 152 | * @param array $tags Array of tags 153 | * @throws Zend_Cache_Exception 154 | * @return boolean True if no problem 155 | */ 156 | protected function _clean($dir, $mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) 157 | { 158 | if (!is_dir($dir)) 159 | { 160 | return false; 161 | } 162 | $result = true; 163 | $basenames = array(); 164 | switch ($mode) 165 | { 166 | case Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG: 167 | $files = $this->_getFilesMatchingAnyTags($tags); 168 | //$f = fopen('var/log/cache.log','a');fwrite($f, print_r('HIT ANY TAG WITH ' . count($files) . ' FILES',1)."\n");fclose($f); 169 | foreach ($files as $fileName) 170 | { 171 | $basename = basename($fileName); 172 | if (!isset($basenames[$basename])) 173 | { 174 | $id = $this->_fileNameToId($basename); 175 | $result = $this->remove($id) && $result; 176 | $basenames[$basename] = 1; 177 | } 178 | } 179 | return $result; 180 | break; 181 | case Zend_Cache::CLEANING_MODE_MATCHING_TAG: 182 | $files = $this->_getFilesMatchingAllTags($tags); 183 | //$f = fopen('var/log/cache.log','a');fwrite($f, print_r('HIT EVERY TAG WITH ' . count($files) . ' FILES',1)."\n");fclose($f); 184 | foreach ($files as $fileName) 185 | { 186 | $basename = basename($fileName); 187 | if (!isset($basenames[$basename])) 188 | { 189 | $id = $this->_fileNameToId($basename); 190 | $result = $this->remove($id) && $result; 191 | $basenames[$basename] = 1; 192 | } 193 | } 194 | return $result; 195 | break; 196 | case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG: 197 | $files = $this->_getFilesNotMatchingTags($tags); 198 | //$f = fopen('var/log/cache.log','a');fwrite($f, print_r('HIT NOT ANY TAG WITH ' . count($files) . ' FILES',1)."\n");fclose($f); 199 | foreach ($files as $fileName) 200 | { 201 | $basename = basename($fileName); 202 | if (!isset($basenames[$basename])) 203 | { 204 | $id = $this->_fileNameToId($basename); 205 | $result = $this->remove($id) && $result; 206 | } 207 | } 208 | return $result; 209 | break; 210 | } 211 | 212 | // Default: handle clean modes ALL and OLD 213 | $prefix = $this->_options['file_name_prefix']; 214 | $glob = @glob($dir . $prefix . '--*'); 215 | if ($glob === false) 216 | { 217 | // On some systems it is impossible to distinguish between empty match and an error. 218 | return true; 219 | } 220 | foreach ($glob as $file) 221 | { 222 | if ($this->_isFile($file)) 223 | { 224 | $fileName = basename($file); 225 | if ($this->_isMetadatasFile($fileName)) 226 | { 227 | // in CLEANING_MODE_ALL, we drop anything, even remainings old metadatas files 228 | if ($mode != Zend_Cache::CLEANING_MODE_ALL) 229 | { 230 | continue; 231 | } 232 | } 233 | $id = $this->_fileNameToId($fileName); 234 | $metadatas = $this->_getMetadatas($id); 235 | if ($metadatas === false) 236 | { 237 | $metadatas = array('expire' => 1, 'tags' => array()); 238 | } 239 | switch ($mode) 240 | { 241 | case Zend_Cache::CLEANING_MODE_ALL: 242 | $res = $this->remove($id); 243 | if (!$res) 244 | { 245 | // in this case only, we accept a problem with the metadatas file drop 246 | $res = $this->_remove($file); 247 | } 248 | $result = $result && $res; 249 | break; 250 | case Zend_Cache::CLEANING_MODE_OLD: 251 | if (time() > $metadatas['expire']) 252 | { 253 | $result = $this->remove($id) && $result; 254 | } 255 | break; 256 | default: 257 | Zend_Cache::throwException('Invalid mode for clean() method'); 258 | break; 259 | } 260 | } 261 | if ((is_dir($file)) && ($this->_options['hashed_directory_level'] > 0)) 262 | { 263 | // Recursive call 264 | $result = $this->_clean($file . DIRECTORY_SEPARATOR, $mode, $tags) && $result; 265 | if ($mode == Zend_Cache::CLEANING_MODE_ALL) 266 | { 267 | // if mode=='all', we try to drop the structure too 268 | @rmdir($file); 269 | } 270 | } 271 | } 272 | return $result; 273 | } 274 | 275 | /** 276 | * Transform a file name into cache id and return it 277 | * 278 | * @param string $fileName File name 279 | * @return string Cache id 280 | */ 281 | protected function _fileNameToId($fileName) 282 | { 283 | $prefix = $this->_options['file_name_prefix']; 284 | $fileName = str_replace('internal-metadatas---', '', $fileName); 285 | return preg_replace('~^' . $prefix . '---(.*)$~', '$1', $fileName); 286 | } 287 | 288 | /** 289 | * 290 | * @param array $tags 291 | * @return array 292 | */ 293 | protected function _getFilesMatchingAnyTags(array $tags) 294 | { 295 | //$f = fopen('var/log/cache.log','a');fwrite($f, print_r(array(__METHOD__, $tags),1)."\n");fclose($f); 296 | $files = array(); 297 | $prefix = $this->_options['file_name_prefix']; 298 | foreach ($tags as $tag) 299 | { 300 | $dir = $this->_getTagDir($tag) . DIRECTORY_SEPARATOR; 301 | if (false !== ($glob = @glob($dir . $prefix . '--*'))) 302 | { 303 | $files = array_merge($glob, $files); 304 | } 305 | } 306 | return $files; 307 | } 308 | 309 | /** 310 | * 311 | * @param array $tags 312 | * @return array 313 | */ 314 | protected function _getFilesMatchingAllTags(array $tags) 315 | { 316 | $files = array(); 317 | $prefix = $this->_options['file_name_prefix']; 318 | $firstTag = array_pop($tags); 319 | $firstDir = $this->_getTagDir($firstTag) . DIRECTORY_SEPARATOR; 320 | if (false !== ($glob = @glob($firstDir . $prefix . '--*'))) 321 | { 322 | foreach ($glob as $file) 323 | { 324 | $basename = basename($file); 325 | $match = true; 326 | foreach ($tags as $tag) 327 | { 328 | $checkLink = $this->_getTagDir($tag) . DIRECTORY_SEPARATOR . $basename; 329 | if (!$this->_isFile($checkLink)) 330 | { 331 | continue 2; 332 | } 333 | } 334 | $files[] = $file; 335 | } 336 | } 337 | return $files; 338 | } 339 | 340 | /** 341 | * 342 | * @param array $tags 343 | * @return array 344 | */ 345 | protected function _getFilesNotMatchingTags(array $tags) 346 | { 347 | $files = array(); 348 | $otherTags = array_diff($this->_getAllTags(), $tags); 349 | $excludeBasenames = $this->_baseNameArray($this->_getFilesMatchingAnyTags($tags)); 350 | $notMatchingFiles = $this->_getFilesMatchingAnyTags($otherTags); 351 | 352 | foreach ($notMatchingFiles as $notMatchingFile) 353 | { 354 | $notMatchingBasename = basename($notMatchingFile); 355 | if (!in_array($notMatchingBasename, $excludeBasenames)) 356 | { 357 | $files[] = $notMatchingFile; 358 | } 359 | } 360 | return $files; 361 | } 362 | 363 | /** 364 | * 365 | * @param array $arrayOfFiles 366 | * @return array 367 | */ 368 | protected function _baseNameArray(array $arrayOfFiles) 369 | { 370 | $files = array(); 371 | foreach ($arrayOfFiles as $file) 372 | { 373 | $file = basename($file); 374 | if (!in_array($file, $files)) 375 | { 376 | $files[] = $file; 377 | } 378 | } 379 | return $files; 380 | } 381 | 382 | /** 383 | * 384 | * @return array 385 | */ 386 | protected function _getAllTags() 387 | { 388 | $tags = scandir($this->_getTagBaseDir()); 389 | if (!$tags) 390 | { 391 | return array(); 392 | } 393 | foreach (array('..', '.') as $filter) 394 | { 395 | if (false !== ($key = array_search($filter, $tags))) 396 | { 397 | unset($tags[$key]); 398 | } 399 | } 400 | return $tags; 401 | } 402 | 403 | /** 404 | * Set a metadatas record 405 | * 406 | * @param string $id Cache id 407 | * @param array $metadatas Associative array of metadatas 408 | * @param boolean $save optional pass false to disable saving to file 409 | * @return boolean True if no problem 410 | */ 411 | protected function _setMetadatas($id, $metadatas, $save = true) 412 | { 413 | $result = parent::_setMetadatas($id, $metadatas, $save); 414 | if ($result) 415 | { 416 | if (isset($metadatas['tags']) && $metadatas['tags']) 417 | { 418 | $this->_createMetadataTagSymlinks($id, $metadatas['tags']); 419 | } 420 | } 421 | return $result; 422 | } 423 | 424 | /** 425 | * 426 | * @param string $id 427 | * @param array $tags 428 | */ 429 | protected function _createMetadataTagSymlinks($id, $tags) 430 | { 431 | $metadataFile = $this->_metadatasFile($id); 432 | foreach ($tags as $tag) 433 | { 434 | $link = $this->_getMetadataTagSymlinkFilename($metadataFile, $tag, true); 435 | if (!file_exists($link)) 436 | { 437 | @symlink($metadataFile, $link); 438 | } 439 | } 440 | } 441 | 442 | /** 443 | * 444 | * @param string $metadataFile 445 | * @param string $tag 446 | * @param bool $createDir 447 | * @return string Filename of the link 448 | */ 449 | protected function _getMetadataTagSymlinkFilename($metadataFile, $tag, $createDir = false) 450 | { 451 | $dir = $this->_getTagDir($tag, $createDir); 452 | $file = $dir . DIRECTORY_SEPARATOR . basename($metadataFile); 453 | return $file; 454 | } 455 | 456 | /** 457 | * 458 | * @return string 459 | */ 460 | protected function _getTagBaseDir() 461 | { 462 | return $this->_options['cache_dir'] . 'tags'; 463 | } 464 | 465 | /** 466 | * 467 | * @param string $tag 468 | * @param bool $createDir 469 | * @return string 470 | */ 471 | protected function _getTagDir($tag, $createDir = false) 472 | { 473 | $dir = $this->_getTagBaseDir() . DIRECTORY_SEPARATOR . str_replace(DIRECTORY_SEPARATOR, '-', $tag); 474 | if ($createDir) 475 | { 476 | $partsArray = array( 477 | $this->_getTagBaseDir(), $dir 478 | ); 479 | foreach ($partsArray as $part) 480 | { 481 | if (!is_dir($part)) 482 | { 483 | @mkdir($part, $this->_options['hashed_directory_umask']); 484 | @chmod($part, $this->_options['hashed_directory_umask']); // see #ZF-320 (this line is required in some configurations) 485 | } 486 | } 487 | } 488 | return $dir; 489 | } 490 | 491 | /** 492 | * Drop a metadata record 493 | * 494 | * @param string $id Cache id 495 | * @return boolean True if no problem 496 | */ 497 | protected function _delMetadatas($id) 498 | { 499 | $metadata = $this->_loadMetadatas($id); 500 | $file = $this->_metadatasFile($id); 501 | if (isset($metadata['tags']) && is_array($metadata['tags'])) 502 | { 503 | foreach ($metadata['tags'] as $tag) 504 | { 505 | $link = $this->_getMetadataTagSymlinkFilename($file, $tag); 506 | $this->_remove($link); 507 | 508 | // remove directory if empty 509 | @rmdir(dirname($link)); 510 | } 511 | } 512 | return parent::_delMetadatas($id); 513 | } 514 | 515 | /** 516 | * Remove a file 517 | * 518 | * If we can't remove the file (because of locks or any problem), we will touch 519 | * the file to invalidate it 520 | * 521 | * @param string $file Complete file path 522 | * @return boolean True if ok 523 | */ 524 | protected function _remove($file) 525 | { 526 | if (!$this->_isFile($file)) 527 | { 528 | return false; 529 | } 530 | if (!@unlink($file)) 531 | { 532 | # we can't remove the file (because of locks or any problem) 533 | $this->_log("Zend_Cache_Backend_File::_remove() : we can't remove $file"); 534 | return false; 535 | } 536 | return true; 537 | } 538 | 539 | /** 540 | * Return the file content of the given file 541 | * 542 | * @param string $file File complete path 543 | * @return string File content (or false if problem) 544 | */ 545 | protected function _fileGetContents($file) 546 | { 547 | $result = false; 548 | if (!$this->_isFile($file)) 549 | { 550 | return false; 551 | } 552 | $f = @fopen($file, 'rb'); 553 | if ($f) 554 | { 555 | if ($this->_options['file_locking']) 556 | @flock($f, LOCK_SH); 557 | $result = stream_get_contents($f); 558 | if ($this->_options['file_locking']) 559 | @flock($f, LOCK_UN); 560 | @fclose($f); 561 | } 562 | return $result; 563 | } 564 | 565 | /** 566 | * 567 | * @param string $file 568 | * @return bool 569 | */ 570 | protected function _isFile($file) 571 | { 572 | return file_exists($file) && (is_file($file) || is_link($file)); 573 | } 574 | 575 | } 576 | -------------------------------------------------------------------------------- /app/code/community/Netzarbeiter/Cache/Test/Model/Symlink.php: -------------------------------------------------------------------------------- 1 | _options = array( 35 | 'cache_dir' => Mage::getBaseDir('cache'), 36 | 'file_name_prefix' => 'mage', 37 | 'hashed_directory_level' => 1, 38 | 'hashed_directory_umask' => 0777, 39 | 'cache_file_umask' => 0777, 40 | ); 41 | } 42 | 43 | public function tearDown() 44 | { 45 | $backend = new Netzarbeiter_Cache_Model_Symlink($this->_options); 46 | $backend->clean(); 47 | parent::tearDown(); 48 | } 49 | 50 | protected function _idToFilename($id) 51 | { 52 | return $this->_options['file_name_prefix'] . '---' . $id; 53 | } 54 | 55 | protected function _idToMetadataFilename($id) 56 | { 57 | return $this->_options['file_name_prefix'] . '---internal-metadatas---' . $id; 58 | } 59 | 60 | protected function _idToPath($id) 61 | { 62 | return $this->_options['cache_dir'] . DIRECTORY_SEPARATOR . $this->_options['file_name_prefix'] . '--' . substr(hash('adler32', $id), 0, 1); 63 | } 64 | 65 | protected function _tagToPath($tag) 66 | { 67 | return $this->_options['cache_dir'] . DIRECTORY_SEPARATOR . 'tags' . DIRECTORY_SEPARATOR . str_replace(DIRECTORY_SEPARATOR, '-', $tag); 68 | } 69 | 70 | /** 71 | * 72 | * @return Netzarbeiter_Cache_Model_Symlink 73 | */ 74 | protected function _getBackendModel() 75 | { 76 | return new Netzarbeiter_Cache_Model_Symlink($this->_options); 77 | } 78 | 79 | /** 80 | * @test 81 | */ 82 | public function backend() 83 | { 84 | $backend = $this->_getBackendModel(); 85 | $this->assertInstanceOf('Netzarbeiter_Cache_Model_Symlink', $backend); 86 | return $backend; 87 | } 88 | 89 | /** 90 | * @test 91 | * @depends backend 92 | */ 93 | public function save() 94 | { 95 | $backend = $this->_getBackendModel(); 96 | 97 | $data = '1234567890'; 98 | $id = 'test' . __METHOD__; 99 | $tags = array('test', 'test2'); 100 | 101 | $result = $backend->save($data, $id, $tags); 102 | $this->assertTrue($result, get_class($backend) . '::save() returned false'); 103 | 104 | $filename = $this->_idToFilename($id); 105 | $metadatafilename = $this->_idToMetadataFilename($id); 106 | $path = $this->_idToPath($id); 107 | 108 | $this->assertFileExists($path . DIRECTORY_SEPARATOR . $filename); 109 | $this->assertFileExists($path . DIRECTORY_SEPARATOR . $metadatafilename); 110 | 111 | $expectedHash = md5_file($path . DIRECTORY_SEPARATOR . $metadatafilename); 112 | 113 | foreach ($tags as $tag) 114 | { 115 | $path = $this->_tagToPath($tag); 116 | $symlink = $path . DIRECTORY_SEPARATOR . $metadatafilename; 117 | $this->assertFileExists($symlink); 118 | $actualHash = md5_file($symlink); 119 | $this->assertEquals($expectedHash, $actualHash); 120 | $this->assertTrue(is_link($symlink), $symlink . " is not a symlink"); 121 | } 122 | 123 | return $backend; 124 | } 125 | 126 | /** 127 | * @test 128 | * @depends save 129 | */ 130 | public function remove() 131 | { 132 | $backend = $this->_getBackendModel(); 133 | 134 | $data = '1234567890'; 135 | $id = 'test' . __METHOD__; 136 | $tags = array('test', 'test2'); 137 | 138 | $backend->save($data, $id, $tags); 139 | 140 | 141 | $filename = $this->_idToFilename($id); 142 | $metadatafilename = $this->_idToMetadataFilename($id); 143 | $path = $this->_idToPath($id); 144 | 145 | $result = $backend->remove($id); 146 | $this->assertTrue($result, get_class($backend) . '::remove() returned false'); 147 | 148 | $result = $backend->load($id); 149 | $this->assertFalse($result); 150 | 151 | $this->assertFileNotExists($path . DIRECTORY_SEPARATOR . $filename); 152 | $this->assertFileNotExists($path . DIRECTORY_SEPARATOR . $metadatafilename); 153 | 154 | foreach ($tags as $tag) 155 | { 156 | $path = $this->_tagToPath($tag); 157 | $this->assertFileNotExists($path . DIRECTORY_SEPARATOR . $metadatafilename); 158 | } 159 | 160 | return $backend; 161 | } 162 | 163 | /** 164 | * @test 165 | * @depends remove 166 | */ 167 | public function clean() 168 | { 169 | $backend = $this->_getBackendModel(); 170 | 171 | $data = '1234567890'; 172 | $baseId = 'test' . __METHOD__; 173 | $tags = array('test', 'test2'); 174 | $creatdIds = array($baseId . 'X', $baseId . 'Y'); 175 | 176 | foreach ($creatdIds as $id) 177 | { 178 | $backend->save($data, $id, $tags); 179 | } 180 | 181 | $result = $backend->clean(); 182 | $this->assertTrue($result, get_class($backend) . '::clean() returned false'); 183 | 184 | foreach ($creatdIds as $id) 185 | { 186 | $filename = $this->_idToFilename($id); 187 | $metadatafilename = $this->_idToMetadataFilename($id); 188 | $path = $this->_idToPath($id); 189 | 190 | $result = $backend->load($id); 191 | $this->assertFalse($result); 192 | 193 | $this->assertFileNotExists($path . DIRECTORY_SEPARATOR . $filename); 194 | $this->assertFileNotExists($path . DIRECTORY_SEPARATOR . $metadatafilename); 195 | 196 | foreach ($tags as $tag) 197 | { 198 | $path = $this->_tagToPath($tag); 199 | $this->assertFileNotExists($path . DIRECTORY_SEPARATOR . $metadatafilename); 200 | } 201 | } 202 | return $backend; 203 | } 204 | 205 | /** 206 | * @test 207 | * @depends clean 208 | */ 209 | public function getAllIds() 210 | { 211 | $backend = $this->_getBackendModel(); 212 | 213 | $data = '1234567890'; 214 | $baseId = 'test' . __METHOD__; 215 | $tags = array('test', 'test2'); 216 | 217 | $creatdIds = array($baseId . 'X', $baseId . 'Y'); 218 | 219 | foreach ($creatdIds as $id) 220 | { 221 | $backend->save($data, $id, $tags); 222 | } 223 | 224 | $ids = $backend->getIds(); 225 | $this->assertEquals(count($creatdIds), count($ids)); 226 | foreach ($creatdIds as $id) 227 | { 228 | $this->assertTrue(array_search($id, $ids) !== false, 'Cache ID ' . $id . ' was not returned by getIds()'); 229 | } 230 | } 231 | 232 | /** 233 | * @test 234 | * @depends clean 235 | */ 236 | public function getIdsMatchingTags() 237 | { 238 | $backend = $this->_getBackendModel(); 239 | 240 | $data = '1234567890'; 241 | $baseId = 'test' . __METHOD__; 242 | $tags = array('test', 'test2'); 243 | 244 | $creatdIds = array($baseId . 'X', $baseId . 'Y'); 245 | 246 | foreach ($creatdIds as $id) 247 | { 248 | $backend->save($data, $id, $tags); 249 | } 250 | 251 | $extraId = $baseId . 'Z'; 252 | $extraTag = array('test3'); 253 | $backend->save($data, $extraId, $extraTag); 254 | 255 | $extra2Id = $baseId . 'W'; 256 | $extra2Tag = array('test2'); 257 | $backend->save($data, $extra2Id, $extra2Tag); 258 | 259 | $ids = $backend->getIdsMatchingTags($tags); 260 | $this->assertEquals(count($creatdIds), count($ids)); 261 | foreach ($creatdIds as $id) 262 | { 263 | $this->assertTrue(array_search($id, $ids) !== false, 'Cache ID ' . $id . ' was not returned by getIdsMatchingTags()'); 264 | } 265 | $this->assertTrue(array_search($extraId, $ids) === false, 'Cache ID ' . $extraId . ' was falsly returned by getIdsMatchingTags()'); 266 | $this->assertTrue(array_search($extra2Id, $ids) === false, 'Cache ID ' . $extra2Id . ' was falsly returned by getIdsMatchingTags()'); 267 | 268 | $ids = $backend->getIdsMatchingTags($extraTag); 269 | $this->assertEquals(array($extraId), $ids); 270 | 271 | $expectedIds = array_merge($creatdIds, array($extra2Id)); 272 | $ids = $backend->getIdsMatchingTags($extra2Tag); 273 | $this->assertEquals(count($expectedIds), count($ids)); 274 | foreach ($expectedIds as $id) 275 | { 276 | $this->assertTrue(array_search($id, $ids) !== false, 'Cache ID ' . $id . ' was not returned by getIdsMatchingTags()'); 277 | } 278 | $this->assertTrue(array_search($extraId, $ids) === false, 'Cache ID ' . $extraId . ' was falsly returned by getIdsMatchingTags()'); 279 | } 280 | 281 | /** 282 | * @test 283 | * @depends clean 284 | */ 285 | public function getIdsMatchingAnyTags() 286 | { 287 | $backend = $this->_getBackendModel(); 288 | 289 | $data = '1234567890'; 290 | $baseId = 'test' . __METHOD__; 291 | $tags = array('test', 'test2'); 292 | 293 | $creatdIds = array($baseId . 'X', $baseId . 'Y'); 294 | 295 | foreach ($creatdIds as $id) 296 | { 297 | $backend->save($data, $id, $tags); 298 | } 299 | 300 | $extraId = $baseId . 'Z'; 301 | $extraTag = array('test3'); 302 | $backend->save($data, $extraId, $extraTag); 303 | 304 | $extra2Id = $baseId . 'W'; 305 | $extra2Tag = array('test2'); 306 | $backend->save($data, $extra2Id, $extra2Tag); 307 | 308 | $extra3Id = $baseId . 'V'; 309 | $extra3Tag = array('test'); 310 | $backend->save($data, $extra3Id, $extra3Tag); 311 | 312 | $ids = $backend->getIdsMatchingAnyTags($tags); 313 | $expectedIds = array_merge($creatdIds, array($extra2Id, $extra3Id)); 314 | $this->assertEquals(count($expectedIds), count($ids)); 315 | foreach ($expectedIds as $id) 316 | { 317 | $this->assertTrue(array_search($id, $ids) !== false, 'Cache ID ' . $id . ' was not returned by getIdsMatchingAnyTags()'); 318 | } 319 | 320 | $ids = $backend->getIdsMatchingAnyTags($extraTag); 321 | $expectedIds = array($extraId); 322 | $this->assertEquals(count($expectedIds), count($ids)); 323 | foreach ($expectedIds as $id) 324 | { 325 | $this->assertTrue(array_search($id, $ids) !== false, 'Cache ID ' . $id . ' was not returned by getIdsMatchingAnyTags()'); 326 | } 327 | 328 | $ids = $backend->getIdsMatchingAnyTags($extra2Tag); 329 | $expectedIds = array_merge($creatdIds, array($extra2Id)); 330 | $this->assertEquals(count($expectedIds), count($ids)); 331 | foreach ($expectedIds as $id) 332 | { 333 | $this->assertTrue(array_search($id, $ids) !== false, 'Cache ID ' . $id . ' was not returned by getIdsMatchingAnyTags()'); 334 | } 335 | 336 | $ids = $backend->getIdsMatchingAnyTags($extra3Tag); 337 | $expectedIds = array_merge($creatdIds, array($extra3Id)); 338 | $this->assertEquals(count($expectedIds), count($ids)); 339 | foreach ($expectedIds as $id) 340 | { 341 | $this->assertTrue(array_search($id, $ids) !== false, 'Cache ID ' . $id . ' was not returned by getIdsMatchingAnyTags()'); 342 | } 343 | } 344 | 345 | /** 346 | * @test 347 | * @depends clean 348 | */ 349 | public function getIdsNotMatchingTags() 350 | { 351 | $backend = $this->_getBackendModel(); 352 | 353 | $data = '1234567890'; 354 | $baseId = 'test' . __METHOD__; 355 | $tags = array('test', 'test2'); 356 | 357 | $creatdIds = array($baseId . 'X', $baseId . 'Y'); 358 | 359 | foreach ($creatdIds as $id) 360 | { 361 | $backend->save($data, $id, $tags); 362 | } 363 | 364 | $extraId = $baseId . 'Z'; 365 | $extraTag = array('test3'); 366 | $backend->save($data, $extraId, $extraTag); 367 | 368 | $extra2Id = $baseId . 'W'; 369 | $extra2Tag = array('test2'); 370 | $backend->save($data, $extra2Id, $extra2Tag); 371 | 372 | $extra3Id = $baseId . 'V'; 373 | $extra3Tag = array('test'); 374 | $backend->save($data, $extra3Id, $extra3Tag); 375 | 376 | 377 | $ids = $backend->getIdsNotMatchingTags($tags); 378 | $expectedIds = array($extraId); 379 | $this->assertEquals(count($expectedIds), count($ids)); 380 | foreach ($expectedIds as $id) 381 | { 382 | $this->assertTrue(array_search($id, $ids) !== false, 'Cache ID ' . $id . ' was not returned by getIdsMatchingAnyTags()'); 383 | } 384 | 385 | $ids = $backend->getIdsNotMatchingTags($extra2Tag); 386 | $expectedIds = array($extraId, $extra3Id); 387 | $this->assertEquals(count($expectedIds), count($ids)); 388 | foreach ($expectedIds as $id) 389 | { 390 | $this->assertTrue(array_search($id, $ids) !== false, 'Cache ID ' . $id . ' was not returned by getIdsMatchingAnyTags()'); 391 | } 392 | 393 | $ids = $backend->getIdsNotMatchingTags($extra3Tag); 394 | $expectedIds = array($extraId, $extra2Id); 395 | $this->assertEquals(count($expectedIds), count($ids)); 396 | foreach ($expectedIds as $id) 397 | { 398 | $this->assertTrue(array_search($id, $ids) !== false, 'Cache ID ' . $id . ' was not returned by getIdsMatchingAnyTags()'); 399 | } 400 | 401 | $ids = $backend->getIdsNotMatchingTags(array_merge($tags, $extraTag)); 402 | $expectedIds = array(); 403 | $this->assertEquals(count($expectedIds), count($ids)); 404 | foreach ($expectedIds as $id) 405 | { 406 | $this->assertTrue(array_search($id, $ids) !== false, 'Cache ID ' . $id . ' was not returned by getIdsMatchingAnyTags()'); 407 | } 408 | } 409 | 410 | /** 411 | * @test 412 | * @depends clean 413 | */ 414 | public function clearAnyMatchingTag() 415 | { 416 | $backend = $this->_getBackendModel(); 417 | 418 | $data = '1234567890'; 419 | $baseId = 'test' . __METHOD__; 420 | $tags = array('test', 'test2'); 421 | 422 | $creatdIds = array($baseId . 'X', $baseId . 'Y'); 423 | 424 | foreach ($creatdIds as $id) 425 | { 426 | $backend->save($data, $id, $tags); 427 | } 428 | 429 | $extraId = $baseId . 'Z'; 430 | $extraTag = array('test3'); 431 | $backend->save($data, $extraId, $extraTag); 432 | 433 | $extra2Id = $baseId . 'W'; 434 | $extra2Tag = array('test2'); 435 | $backend->save($data, $extra2Id, $extra2Tag); 436 | 437 | $extra3Id = $baseId . 'V'; 438 | $extra3Tag = array('test'); 439 | $backend->save($data, $extra3Id, $extra3Tag); 440 | 441 | $result = $backend->clean(Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, $tags); 442 | $this->assertTrue($result, get_class($backend) . '::clean(' . Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG . ') returned false'); 443 | 444 | foreach (array_merge($creatdIds, array($extra2Id, $extra3Id)) as $id) 445 | { 446 | $filename = $this->_idToFilename($id); 447 | $metadatafilename = $this->_idToMetadataFilename($id); 448 | $path = $this->_idToPath($id); 449 | 450 | $result = $backend->load($id); 451 | $this->assertFalse($result, 'Loading of id ' . $id . ' successfull after the matching tag was cleaned'); 452 | 453 | $this->assertFileNotExists($path . DIRECTORY_SEPARATOR . $filename); 454 | $this->assertFileNotExists($path . DIRECTORY_SEPARATOR . $metadatafilename); 455 | 456 | foreach ($tags as $tag) 457 | { 458 | $path = $this->_tagToPath($tag); 459 | $this->assertFileNotExists($path . DIRECTORY_SEPARATOR . $metadatafilename); 460 | } 461 | } 462 | 463 | $result = $backend->load($extraId); 464 | $this->assertEquals($data, $result); 465 | } 466 | 467 | /** 468 | * @test 469 | * @depends clean 470 | */ 471 | public function clearAllMatchingTags() 472 | { 473 | $backend = $this->_getBackendModel(); 474 | 475 | $data = '1234567890'; 476 | $baseId = 'test' . __METHOD__; 477 | $tags = array('test', 'test2'); 478 | 479 | $creatdIds = array($baseId . 'X', $baseId . 'Y'); 480 | 481 | foreach ($creatdIds as $id) 482 | { 483 | $backend->save($data, $id, $tags); 484 | } 485 | 486 | $extraId = $baseId . 'Z'; 487 | $extraTag = array('test3'); 488 | $backend->save($data, $extraId, $extraTag); 489 | 490 | $extra2Id = $baseId . 'W'; 491 | $extra2Tag = array('test2'); 492 | $backend->save($data, $extra2Id, $extra2Tag); 493 | 494 | $extra3Id = $baseId . 'V'; 495 | $extra3Tag = array('test'); 496 | $backend->save($data, $extra3Id, $extra3Tag); 497 | 498 | $result = $backend->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, $tags); 499 | $this->assertTrue($result, get_class($backend) . '::clean(' . Zend_Cache::CLEANING_MODE_MATCHING_TAG . ') returned false'); 500 | 501 | foreach ($creatdIds as $id) 502 | { 503 | $filename = $this->_idToFilename($id); 504 | $metadatafilename = $this->_idToMetadataFilename($id); 505 | $path = $this->_idToPath($id); 506 | 507 | $result = $backend->load($id); 508 | $this->assertFalse($result, 'Loading of id ' . $id . ' successfull after the matching tags where cleaned'); 509 | 510 | $this->assertFileNotExists($path . DIRECTORY_SEPARATOR . $filename); 511 | $this->assertFileNotExists($path . DIRECTORY_SEPARATOR . $metadatafilename); 512 | 513 | foreach ($tags as $tag) 514 | { 515 | $path = $this->_tagToPath($tag); 516 | $this->assertFileNotExists($path . DIRECTORY_SEPARATOR . $metadatafilename); 517 | } 518 | } 519 | 520 | $result = $backend->load($extraId); 521 | $this->assertEquals($data, $result); 522 | 523 | $result = $backend->load($extra2Id); 524 | $this->assertEquals($data, $result); 525 | 526 | $result = $backend->load($extra3Id); 527 | $this->assertEquals($data, $result); 528 | } 529 | 530 | /** 531 | * @test 532 | * @depends clean 533 | */ 534 | public function clearNotMatchingTag() 535 | { 536 | $backend = $this->_getBackendModel(); 537 | 538 | $data = '1234567890'; 539 | $baseId = 'test' . __METHOD__; 540 | $tags = array('test', 'test2'); 541 | 542 | $creatdIds = array($baseId . 'X', $baseId . 'Y'); 543 | 544 | foreach ($creatdIds as $id) 545 | { 546 | $backend->save($data, $id, $tags); 547 | } 548 | 549 | $extraId = $baseId . 'Z'; 550 | $extraTag = array('test3'); 551 | $backend->save($data, $extraId, $extraTag); 552 | 553 | $extra2Id = $baseId . 'W'; 554 | $extra2Tag = array('test2'); 555 | $backend->save($data, $extra2Id, $extra2Tag); 556 | 557 | $extra3Id = $baseId . 'V'; 558 | $extra3Tag = array('test'); 559 | $backend->save($data, $extra3Id, $extra3Tag); 560 | 561 | $result = $backend->clean(Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG, $tags); 562 | $this->assertTrue($result, get_class($backend) . '::clean(' . Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG . ') returned false'); 563 | 564 | foreach (array_merge($creatdIds, array($extra2Id, $extra3Id)) as $id) 565 | { 566 | $result = $backend->load($id); 567 | $this->assertEquals($data, $result); 568 | } 569 | 570 | foreach ($extraTag as $id) 571 | { 572 | $filename = $this->_idToFilename($id); 573 | $metadatafilename = $this->_idToMetadataFilename($id); 574 | $path = $this->_idToPath($id); 575 | 576 | $result = $backend->load($id); 577 | $this->assertFalse($result, 'Loading of id ' . $id . ' successfull after the matching tags where cleaned'); 578 | 579 | $this->assertFileNotExists($path . DIRECTORY_SEPARATOR . $filename); 580 | $this->assertFileNotExists($path . DIRECTORY_SEPARATOR . $metadatafilename); 581 | 582 | foreach ($tags as $tag) 583 | { 584 | $path = $this->_tagToPath($tag); 585 | $this->assertFileNotExists($path . DIRECTORY_SEPARATOR . $metadatafilename); 586 | } 587 | } 588 | } 589 | 590 | /** 591 | * @test 592 | * @depends clean 593 | */ 594 | public function clearOld() 595 | { 596 | $backend = $this->_getBackendModel(); 597 | 598 | $data = '1234567890'; 599 | $baseId = 'test' . __METHOD__; 600 | $tags = array('test'); 601 | 602 | $creatdIds = array($baseId . 'X', $baseId . 'Y'); 603 | 604 | foreach ($creatdIds as $id) 605 | { 606 | $backend->save($data, $id, $tags, -1); 607 | } 608 | 609 | $extraId = $baseId . 'Z'; 610 | $extraTag = array('test3'); 611 | $backend->save($data, $extraId, $extraTag, 100); 612 | 613 | $result = $backend->clean(Zend_Cache::CLEANING_MODE_OLD); 614 | $this->assertTrue($result, get_class($backend) . '::clean(' . Zend_Cache::CLEANING_MODE_OLD . ') returned false'); 615 | 616 | 617 | foreach ($creatdIds as $id) 618 | { 619 | $filename = $this->_idToFilename($id); 620 | $metadatafilename = $this->_idToMetadataFilename($id); 621 | $path = $this->_idToPath($id); 622 | 623 | $this->assertFileNotExists($path . DIRECTORY_SEPARATOR . $filename); 624 | $this->assertFileNotExists($path . DIRECTORY_SEPARATOR . $metadatafilename); 625 | } 626 | 627 | $filename = $this->_idToFilename($extraId); 628 | $metadatafilename = $this->_idToMetadataFilename($extraId); 629 | $path = $this->_idToPath($extraId); 630 | $this->assertFileExists($path . DIRECTORY_SEPARATOR . $filename); 631 | $this->assertFileExists($path . DIRECTORY_SEPARATOR . $metadatafilename); 632 | 633 | foreach ($extraTag as $tag) 634 | { 635 | $path = $this->_tagToPath($tag); 636 | $this->assertFileExists($path . DIRECTORY_SEPARATOR . $metadatafilename); 637 | } 638 | } 639 | 640 | /** 641 | * @test 642 | * @depends clean 643 | */ 644 | public function load() 645 | { 646 | $backend = $this->_getBackendModel(); 647 | 648 | $data = '1234567890'; 649 | $id = 'test' . __METHOD__; 650 | $tags = array(); 651 | 652 | $creatdIds = array($baseId . 'X', $baseId . 'Y'); 653 | 654 | $backend->save($data, $id, $tags); 655 | 656 | $result = $backend->load($id); 657 | $this->assertEquals($data, $result); 658 | } 659 | 660 | /** 661 | * @test 662 | * @depends clean 663 | */ 664 | public function loadExpired() 665 | { 666 | $backend = $this->_getBackendModel(); 667 | 668 | $data = '1234567890'; 669 | $id = 'test' . __METHOD__; 670 | $tags = array(); 671 | $expires = -1; 672 | 673 | $creatdIds = array($baseId . 'X', $baseId . 'Y'); 674 | 675 | $backend->save($data, $id, $tags, $expires); 676 | 677 | $result = $backend->load($id); 678 | $this->assertFalse($result, 'Loading of id ' . $id . ' successfull after the matching tags where cleaned'); 679 | } 680 | 681 | } 682 | -------------------------------------------------------------------------------- /app/code/community/Netzarbeiter/Cache/Test/Model/Symlink/fixtures/backend.yaml: -------------------------------------------------------------------------------- 1 | config: 2 | global/cache/slow_backend_options/hashed_directory_level: 1 3 | global/cache/slow_backend_options/hashed_directory_umask: 0777 4 | global/cache/slow_backend_options/file_name_prefix: mage 5 | -------------------------------------------------------------------------------- /app/code/community/Netzarbeiter/Cache/controllers/Adminhtml/Netzarbeiter/CacheController.php: -------------------------------------------------------------------------------- 1 | loadLayout(); 32 | $this->_setActiveMenu('system'); 33 | $this->renderLayout(); 34 | } 35 | 36 | public function initSymlinksAction() 37 | { 38 | try 39 | { 40 | $results = Mage::helper('netzarbeiter_cache')->initTagSymlinks()->getResults(); 41 | $this->_getSession()->addSuccess($this->__('Created %s symlinks', count($results))); 42 | $this->_getSession()->setResultInfo($results); 43 | } 44 | catch(Exception $e) 45 | { 46 | $this->_getSession()->addError($e->getMessage()); 47 | Mage::logException($e); 48 | } 49 | $this->_redirect('*/*/index'); 50 | } 51 | 52 | protected function _isAllowed() 53 | { 54 | return Mage::getSingleton('admin/session')->isAllowed('system/tools/netzarbeiter_cache'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/code/community/Netzarbeiter/Cache/etc/adminhtml.xml: -------------------------------------------------------------------------------- 1 | 2 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | Symlink Cache 35 | adminhtml/netzarbeiter_cache/ 36 | 555 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Symlink Cache 53 | 555 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /app/code/community/Netzarbeiter/Cache/etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 27 | 28 | 29 | 30 | 0.0.5 31 | 32 | 33 | 34 | 35 | 36 | Netzarbeiter_Cache_Helper 37 | 38 | 39 | 40 | 41 | Netzarbeiter_Cache_Block 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Netzarbeiter_Cache_Adminhtml 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | netzarbeiter/cache.xml 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /app/code/community/Netzarbeiter/Cache/readme.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * Magento 3 | * 4 | * NOTICE OF LICENSE 5 | * 6 | * This source file is subject to the Open Software License (OSL 3.0) 7 | * that is bundled with this package in the file LICENSE.txt. 8 | * It is also available through the world-wide-web at this URL: 9 | * http://opensource.org/licenses/osl-3.0.php 10 | * If you did not receive a copy of the license and are unable to 11 | * obtain it through the world-wide-web, please send an email 12 | * to license@magentocommerce.com so we can send you a copy immediately. 13 | * 14 | * DISCLAIMER 15 | * 16 | * Do not edit or add to this file if you wish to upgrade this extension 17 | * to newer versions in the future. 18 | * 19 | * @category Netzarbeiter 20 | * @package Netzarbeiter_Cache 21 | * @copyright Copyright (c) 2011 Vinai Kopp http://netzarbeiter.com 22 | * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) 23 | */ 24 | 25 | This module is intended to improve the performance of the file cache backend 26 | with large cache pools (as fast or slow backend). 27 | 28 | This module is provided with no warranty, you use it at your own risk. 29 | I know it is being used successfully on several sites, both as a primary cache 30 | backend and as a slow backend in combination with APC or memcached. 31 | I invite you to have a look at it and try it out, but please start with a test 32 | instance and not your live store. 33 | You can download the module from Magento Connect or from github 34 | https://github.com/Vinai/Symlink-Cache 35 | 36 | If you find bugs or have improvements, please send them in. 37 | 38 | 39 | According to the php reference manual symlinks works under Linux or since PHP 5.3 under Windows 40 | Vista/Windows Server 2008 or greater (see http://php.net/manual/en/function.symlink.php 41 | under the section titled Notes). 42 | 43 | After installation, clear the cache and check under "System > Tools > Symlink 44 | Cache" for further instructions. -------------------------------------------------------------------------------- /app/code/community/Netzarbeiter/Cache/shell/benchmark.php: -------------------------------------------------------------------------------- 1 | _echoCacheConfig(); 68 | 69 | $nEntries = (int) $this->getArg('init'); 70 | $verbose = $this->getArg('v') || $this->getArg('verbose'); 71 | 72 | if ($nEntries) 73 | { 74 | 75 | if (0 === ($nTags = (int) $this->getArg('tag'))) 76 | { 77 | $nTags = (int) $this->getArg('tags'); 78 | } 79 | $minTags = (int) $this->getArg('min'); 80 | $maxTags = (int) $this->getArg('max'); 81 | 82 | if ($nEntries < 1) 83 | { 84 | throw new InvalidArgumentException('Number of records to create must be one or more'); 85 | } 86 | if ($nTags < 1) $nTags = 30; 87 | if ($minTags < 1) $minTags = 5; 88 | if ($maxTags < 1) $maxTags = $minTags + 5; 89 | if ($maxTags > $nTags) $maxTags = $nTags; 90 | 91 | if ($minTags != $maxTags) 92 | { 93 | $rangeString = sprintf('%d-%d', $minTags, $maxTags); 94 | } 95 | else 96 | { 97 | $rangeString = $minTags; 98 | } 99 | 100 | $this->_println(sprintf('Clearing & Initialising cache with %d records using %s out of %d tags per record...', 101 | $nEntries, $rangeString, $nTags 102 | )); 103 | Mage::app()->cleanCache(); 104 | $this->_createTagList($nTags); 105 | $this->_createCacheRecords($nEntries, $minTags, $maxTags); 106 | 107 | } 108 | else 109 | { 110 | printf('Analyzing current cache contents (please be patient)... '); 111 | $start = microtime(true); 112 | 113 | $nTags = $this->_readTags(); 114 | if (1 > $nTags) 115 | { 116 | throw new OutOfRangeException('No cache tags found in cache'); 117 | } 118 | $nEntries = $this->_countCacheRecords(); 119 | if (1 > $nEntries) 120 | { 121 | throw new OutOfRangeException('No cache records found in cache'); 122 | } 123 | 124 | $time = microtime(true) - $start; 125 | $this->_println(sprintf('%d cache IDs and %d cache tags read in %ss', $nEntries, $nTags, $time)); 126 | } 127 | 128 | $this->_println(sprintf('Benchmarking %d cache records with %d tags', $nEntries, $nTags)); 129 | 130 | $times = $this->_benchmarkByTag($verbose); 131 | 132 | $this->_echoAverage($times); 133 | 134 | return $this; 135 | } 136 | 137 | /** 138 | * Display average values from given tag benchmark times. 139 | * 140 | * @param array $times 141 | */ 142 | protected function _echoAverage(array $times) 143 | { 144 | $totalTime = $totalIdCount = 0; 145 | $numTags = count($times); 146 | foreach ($times as $time) 147 | { 148 | $totalTime += $time['time']; 149 | $totalIdCount += $time['count']; 150 | } 151 | $this->_echoTime(sprintf('Average for %s tags:', $numTags), $totalTime / $numTags, $totalIdCount / $numTags); 152 | } 153 | 154 | /** 155 | * Display the configured cache backend(s). 156 | */ 157 | protected function _echoCacheConfig() 158 | { 159 | $backend = (string) Mage::getConfig()->getNode('global/cache/backend'); 160 | $realBackend = Mage::app()->getCache()->getBackend(); 161 | $slowBackend = (string) Mage::getConfig()->getNode('global/cache/slow_backend'); 162 | 163 | if ('' === $backend) 164 | { 165 | $backend = get_class($realBackend); 166 | } 167 | if ($realBackend instanceof Zend_Cache_Backend_TwoLevels && '' === $slowBackend) 168 | { 169 | $slowBackend = 'Zend_Cache_Backend_File'; 170 | } 171 | 172 | if ('' === $slowBackend) 173 | { 174 | $this->_println(sprintf('Cache Configuration: single backend %s', $backend)); 175 | } 176 | else 177 | { 178 | $this->_println(sprintf('Cache Configuration: fast backend: %s, slow backend: %s', $backend, $slowBackend)); 179 | } 180 | } 181 | 182 | /** 183 | * Create internal list of cache tags. 184 | * 185 | * @param int $nTags The number of tags to create 186 | */ 187 | protected function _createTagList($nTags) 188 | { 189 | $length = strlen('' . $nTags); 190 | for ($i = 1; $i <= $nTags; $i++) 191 | { 192 | $this->_tags[] = sprintf('TAG_%0' . $length . 'd', $i); 193 | } 194 | } 195 | 196 | /** 197 | * Read in list of cache tags. Remove the cache prefix to get the tags 198 | * specified to Mage_Core_Model_App::saveCache() 199 | * 200 | * @return int The number of tags 201 | */ 202 | protected function _readTags() 203 | { 204 | $this->_tags = array(); 205 | $prefix = $this->_getCachePrefix(); 206 | $tags = (array) Mage::app()->getCache()->getTags(); 207 | $prefixLen = strlen($prefix); 208 | foreach ($tags as $tag) 209 | { 210 | $tag = substr($tag, $prefixLen); 211 | 212 | // since all records saved through Magento are associated with the 213 | // MAGE cache tag it is not representative for benchmarking. 214 | if ('MAGE' === $tag) continue; 215 | 216 | $this->_tags[] = $tag; 217 | } 218 | sort($this->_tags); 219 | return count($this->_tags); 220 | } 221 | 222 | /** 223 | * Return the configured cache prefix according to the logic in core/cache. 224 | * 225 | * @return string The used cache prefix 226 | * @see Mage_Core_Model_Cache::__construct() 227 | */ 228 | protected function _getCachePrefix() 229 | { 230 | if (! $this->_cachePrefix) 231 | { 232 | $options = Mage::getConfig()->getNode('global/cache'); 233 | $prefix = ''; 234 | if ($options) 235 | { 236 | $options = $options->asArray(); 237 | if (isset($options['id_prefix'])) 238 | { 239 | $prefix = $options['id_prefix']; 240 | } 241 | elseif (isset($options['prefix'])) 242 | { 243 | $prefix = $options['prefix']; 244 | } 245 | } 246 | if ('' === $prefix) 247 | { 248 | $prefix = substr(md5(Mage::getConfig()->getOptions()->getEtcDir()), 0, 3).'_';; 249 | } 250 | $this->_cachePrefix = $prefix; 251 | } 252 | return $this->_cachePrefix; 253 | } 254 | 255 | /** 256 | * Return the current number of cache records. 257 | * 258 | * @return int The current number of cache records 259 | */ 260 | protected function _countCacheRecords() 261 | { 262 | $ids = (array) Mage::app()->getCache()->getIds(); 263 | return count($ids); 264 | } 265 | 266 | /** 267 | * Create the given number of cache records. 268 | * Each record only contains the sample data "X". 269 | * Each records is associated with a random number of cache tags between the 270 | * given min and max. 271 | * 272 | * @param int $nEntries The number of Cache records to create 273 | * @param int $minTags The minimum number of cache tags for each record 274 | * @param int $maxTags The maximum number of cache tags for each record 275 | */ 276 | protected function _createCacheRecords($nEntries, $minTags, $maxTags) 277 | { 278 | $data = 'X'; 279 | $progressBar = $this->_getProgressBar(0, $nEntries); 280 | 281 | $start = microtime(true); 282 | for ($i = 0; $i < $nEntries; $i++) 283 | { 284 | $id = sprintf('rec_%010d', $i); 285 | $tags = $this->_getRandomTags($minTags, $maxTags); 286 | Mage::app()->saveCache($data, $id, $tags); 287 | $progressBar->update($i+1); 288 | } 289 | $time = microtime(true) - $start; 290 | $progressBar->finish(); 291 | $this->_println(sprintf('Initialization finished in %ss', $time)); 292 | } 293 | 294 | /** 295 | * Since many operations can take quite a long time for large cache pools, 296 | * this might help ease the waiting time with a nice console progress bar. 297 | * 298 | * @return Zend_ProgressBar A fresh Zend_ProgressBar instance 299 | */ 300 | protected function _getProgressBar($min, $max) 301 | { 302 | $progressBar = new Zend_ProgressBar(new Zend_ProgressBar_Adapter_Console(), $min, $max); 303 | $progressBar->getAdapter()->setFinishAction(Zend_ProgressBar_Adapter_Console::FINISH_ACTION_CLEAR_LINE); 304 | return $progressBar; 305 | } 306 | 307 | /** 308 | * Return a random number of cache tags between the given range. 309 | * 310 | * @param int $min 311 | * @param int $max 312 | * @return array 313 | */ 314 | protected function _getRandomTags($min, $max) 315 | { 316 | $tags = array(); 317 | $num = mt_rand($min, $max); 318 | $keys = array_rand($this->_tags, $num); 319 | if (1 === $num) { 320 | $keys = array($keys); // array_rand returns the key directly if $num is 1 321 | } 322 | foreach ($keys as $i) { 323 | $tags[] = $this->_tags[$i]; 324 | } 325 | return $tags; 326 | } 327 | 328 | /** 329 | * Display the given string with a trailing newline. 330 | * 331 | * @param string $msg The string to display 332 | */ 333 | protected function _println($msg) 334 | { 335 | printf("%s\n", $msg); 336 | } 337 | 338 | /** 339 | * Get the time used for calling getIdsMatchingTags() for every cache tag in 340 | * the property $_tags. 341 | * If $verbose is set to true, display detailed statistics for each tag, 342 | * otherwise display a progress bar. 343 | * 344 | * @param bool $verbose If true output statistics for every cache tag 345 | * @return array Return an array of timing statistics 346 | */ 347 | protected function _benchmarkByTag($verbose = false) 348 | { 349 | $times = array(); 350 | 351 | if (! $verbose) 352 | { 353 | $progressBar = $this->_getProgressBar(0, count($this->_tags)); 354 | $counter = 0; 355 | } 356 | 357 | foreach ($this->_tags as $tag) 358 | { 359 | $start = microtime(true); 360 | $ids = Mage::app()->getCache()->getIdsMatchingTags(array($tag)); 361 | $end = microtime(true); 362 | $times[$tag] = array('time' => $end - $start, 'count' => count($ids)); 363 | if ($verbose) 364 | { 365 | $this->_echoTime($tag, $times[$tag]['time'], $times[$tag]['count']); 366 | } 367 | else 368 | { 369 | $progressBar->update(++$counter); 370 | } 371 | } 372 | if (! $verbose) 373 | { 374 | $progressBar->finish(); 375 | } 376 | return $times; 377 | } 378 | 379 | /** 380 | * Display the given timing statistics. 381 | * 382 | * @param string $tag The tag 383 | * @param float $time The time used to run getIdsMatchingTags() for the tag 384 | * @param int $count The number of IDs associated with the tag 385 | */ 386 | protected function _echoTime($tag, $time, $count) 387 | { 388 | $length = $this->_getLongestTagLength(); 389 | $pattern = '%-' . $length . 's (%4s IDs) %ss'; 390 | $this->_println(sprintf($pattern, $tag, $count, $time)); 391 | } 392 | 393 | /** 394 | * Return the length of the longest tag in the $_tags property. 395 | * 396 | * @param bool $force If true don't use the cached value 397 | * @return int The length of the longest tag 398 | */ 399 | protected function _getLongestTagLength($force = false) 400 | { 401 | if (0 === $this->_maxTagLength || $force) 402 | { 403 | $len = 0; 404 | foreach ($this->_tags as $tag) 405 | { 406 | $tagLen = strlen($tag); 407 | if ($tagLen > $len) 408 | { 409 | $len = $tagLen; 410 | } 411 | } 412 | $this->_maxTagLength = $len; 413 | } 414 | return $this->_maxTagLength; 415 | } 416 | 417 | /** 418 | * Return the usage help. 419 | * 420 | * @return string 421 | */ 422 | public function usageHelp() 423 | { 424 | return << Clear existing cache records and create entries 431 | --tag If init was used, specify the number of tags to create (default to 30) 432 | --min If init was used, the min number of tags to use for each record (default 5) 433 | --max If init was used, the max number of tags to use for each record (default min +5) 434 | -v Display statistics for every cache tag 435 | --help This help 436 | 437 | USAGE; 438 | } 439 | } 440 | 441 | 442 | $init = new benchmark(); 443 | $init->run(); -------------------------------------------------------------------------------- /app/code/community/Netzarbeiter/Cache/shell/initTags.php: -------------------------------------------------------------------------------- 1 | initTagSymlinks()->getResults(); 53 | foreach ($result as $line) { 54 | printf('Created symlink to "%s" for tag "%s"', basename($line['target']), $line['tag']); 55 | echo "\n"; 56 | } 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * Return the usage help. 63 | * 64 | * @return string 65 | */ 66 | public function usageHelp() 67 | { 68 | return <<run(); -------------------------------------------------------------------------------- /app/design/adminhtml/default/default/layout/netzarbeiter/cache.xml: -------------------------------------------------------------------------------- 1 | 2 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/design/adminhtml/default/default/template/netzarbeiter/cache/tool.phtml: -------------------------------------------------------------------------------- 1 | 26 |
27 | 28 | 29 | 30 | 31 | 32 |

__('Netzarbeiter Symlink Filecache') ?>

getChildHtml('initSymlinksButton') ?>
33 |
34 |
35 |

__('Status') ?>

36 | __('The Symlink Cache is currently %s', $this->getSymlinkCacheStatus()) ?>
37 | __('Fast Cache Backend: %s', $this->getFastBackend()) ?> 38 | getSlowBackend()): ?> 39 | __(', Slow Cache Backend: %s', $this->getSlowBackend()) ?> 40 | 41 |
42 | isSymlinkCacheInUse()): ?> 43 |
44 |

__('Usage') ?>

45 | __('To use the Netzarbeiter symlink cache as the main cache backend, put the following code into your app/etc/local.xml file:') ?>
46 | 47 |
48 |
49 | __('To use the Netzarbeiter symlink cache as the slow backend, put the following code into your app/etc/local.xml file'); ?>
50 | __('but note you will have to configure a fast backend that requires the use of a slow backend in order to make this work (e.g. apc or memcached):') ?>
51 | 52 |
53 | 54 | getResultInfo()): ?> 55 | 63 | 64 | -------------------------------------------------------------------------------- /app/etc/modules/Netzarbeiter_Cache.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | community 7 | 8 | 9 | -------------------------------------------------------------------------------- /modman: -------------------------------------------------------------------------------- 1 | 2 | app/code/community/Netzarbeiter/Cache app/code/community/Netzarbeiter/Cache 3 | app/etc/modules/Netzarbeiter_Cache.xml app/etc/modules/Netzarbeiter_Cache.xml 4 | app/design/adminhtml/default/default/layout/netzarbeiter/cache.xml app/design/adminhtml/default/default/layout/netzarbeiter/cache.xml 5 | app/design/adminhtml/default/default/template/netzarbeiter/cache app/design/adminhtml/default/default/template/netzarbeiter/cache 6 | --------------------------------------------------------------------------------