1<?php 2// +----------------------------------------------------------------------+ 3// | PEAR :: Cache | 4// +----------------------------------------------------------------------+ 5// | Copyright (c) 1997-2003 The PHP Group | 6// +----------------------------------------------------------------------+ 7// | This source file is subject to version 2.0 of the PHP license, | 8// | that is bundled with this package in the file LICENSE, and is | 9// | available at through the world-wide-web at | 10// | http://www.php.net/license/2_02.txt. | 11// | If you did not receive a copy of the PHP license and are unable to | 12// | obtain it through the world-wide-web, please send a note to | 13// | license@php.net so we can mail you a copy immediately. | 14// +----------------------------------------------------------------------+ 15// | Authors: Ulf Wendel <ulf.wendel@phpdoc.de> | 16// | Sebastian Bergmann <sb@sebastian-bergmann.de> | 17// +----------------------------------------------------------------------+ 18// 19// $Id: file.php 293864 2010-01-23 03:49:21Z clockwerx $ 20 21require_once 'Cache/Container.php'; 22 23/** 24* Stores cache contents in a file. 25* 26* @author Ulf Wendel <ulf.wendel@phpdoc.de> 27* @version $Id: file.php 293864 2010-01-23 03:49:21Z clockwerx $ 28*/ 29class Cache_Container_file extends Cache_Container 30{ 31 32 /** 33 * File locking 34 * 35 * With file container, it's possible, that you get corrupted 36 * data-entries under bad circumstances. The file locking must 37 * improve this problem but it's experimental stuff. So the 38 * default value is false. But it seems to give good results 39 * 40 * @var boolean 41 */ 42 var $fileLocking = false; 43 44 /** 45 * Directory where to put the cache files. 46 * 47 * @var string Make sure to add a trailing slash 48 */ 49 var $cache_dir = ''; 50 51 /** 52 * Filename prefix for cache files. 53 * 54 * You can use the filename prefix to implement a "domain" based cache or just 55 * to give the files a more descriptive name. The word "domain" is borroed from 56 * a user authentification system. One user id (cached dataset with the ID x) 57 * may exists in different domains (different filename prefix). You might want 58 * to use this to have different cache values for a production, development and 59 * quality assurance system. If you want the production cache not to be influenced 60 * by the quality assurance activities, use different filename prefixes for them. 61 * 62 * I personally don't think that you'll never need this, but 640kb happend to be 63 * not enough, so... you know what I mean. If you find a useful application of the 64 * feature please update this inline doc. 65 * 66 * @var string 67 */ 68 var $filename_prefix = ''; 69 70 71 /** 72 * List of cache entries, used within a gc run 73 * 74 * @var array 75 */ 76 var $entries; 77 78 /** 79 * Total number of bytes required by all cache entries, used within a gc run. 80 * 81 * @var int 82 */ 83 var $total_size = 0; 84 85 86 /** 87 * Max Line Length of userdata 88 * 89 * If set to 0, it will take the default 90 * ( 1024 in php 4.2, unlimited in php 4.3) 91 * see http://ch.php.net/manual/en/function.fgets.php 92 * for details 93 * 94 * @var int 95 */ 96 var $max_userdata_linelength = 257; 97 98 /** 99 * Creates the cache directory if neccessary 100 * 101 * @param array Config options: ["cache_dir" => ..., "filename_prefix" => ...] 102 */ 103 function Cache_Container_file($options = '') 104 { 105 if (is_array($options)) { 106 $this->setOptions($options, array_merge($this->allowed_options, array('cache_dir', 'filename_prefix', 'max_userdata_linelength'))); 107 } 108 clearstatcache(); 109 if ($this->cache_dir) { 110 // make relative paths absolute for use in deconstructor. 111 // it looks like the deconstructor has problems with relative paths 112 if (OS_UNIX && '/' != $this->cache_dir{0} ) 113 $this->cache_dir = realpath( getcwd() . '/' . $this->cache_dir) . '/'; 114 115 // check if a trailing slash is in cache_dir 116 if ($this->cache_dir{strlen($this->cache_dir)-1} != DIRECTORY_SEPARATOR) 117 $this->cache_dir .= '/'; 118 119 if (!file_exists($this->cache_dir) || !is_dir($this->cache_dir)) 120 mkdir($this->cache_dir, 0755); 121 } 122 $this->entries = array(); 123 $this->group_dirs = array(); 124 125 } // end func contructor 126 127 function fetch($id, $group) 128 { 129 $file = $this->getFilename($id, $group); 130 if (PEAR::isError($file)) { 131 return $file; 132 } 133 134 if (!file_exists($file)) { 135 return array(null, null, null); 136 } 137 // retrive the content 138 if (!($fh = @fopen($file, 'rb'))) { 139 return new Cache_Error("Can't access cache file '$file'. Check access rights and path.", __FILE__, __LINE__); 140 } 141 // File locking (shared lock) 142 if ($this->fileLocking) { 143 flock($fh, LOCK_SH); 144 } 145 // file format: 146 // 1st line: expiration date 147 // 2nd line: user data 148 // 3rd+ lines: cache data 149 $expire = trim(fgets($fh, 12)); 150 if ($this->max_userdata_linelength == 0 ) { 151 $userdata = trim(fgets($fh)); 152 } else { 153 $userdata = trim(fgets($fh, $this->max_userdata_linelength)); 154 } 155 $buffer = ''; 156 while (!feof($fh)) { 157 $buffer .= fread($fh, 8192); 158 } 159 $cachedata = $this->decode($buffer); 160 161 // Unlocking 162 if ($this->fileLocking) { 163 flock($fh, LOCK_UN); 164 } 165 fclose($fh); 166 167 // last usage date used by the gc - maxlifetime 168 // touch without second param produced stupid entries... 169 touch($file,time()); 170 clearstatcache(); 171 172 return array($expire, $cachedata, $userdata); 173 } // end func fetch 174 175 /** 176 * Stores a dataset. 177 * 178 * WARNING: If you supply userdata it must not contain any linebreaks, 179 * otherwise it will break the filestructure. 180 */ 181 function save($id, $cachedata, $expires, $group, $userdata) 182 { 183 $this->flushPreload($id, $group); 184 185 $file = $this->getFilename($id, $group); 186 if (!($fh = @fopen($file, 'wb'))) { 187 return new Cache_Error("Can't access '$file' to store cache data. Check access rights and path.", __FILE__, __LINE__); 188 } 189 190 // File locking (exclusive lock) 191 if ($this->fileLocking) { 192 flock($fh, LOCK_EX); 193 } 194 // file format: 195 // 1st line: expiration date 196 // 2nd line: user data 197 // 3rd+ lines: cache data 198 $expires = $this->getExpiresAbsolute($expires); 199 fwrite($fh, $expires . "\n"); 200 fwrite($fh, $userdata . "\n"); 201 fwrite($fh, $this->encode($cachedata)); 202 203 // File unlocking 204 if ($this->fileLocking) { 205 flock($fh, LOCK_UN); 206 } 207 fclose($fh); 208 209 // I'm not sure if we need this 210 // i don't think we need this (chregu) 211 // touch($file); 212 213 return true; 214 } // end func save 215 216 function remove($id, $group) 217 { 218 $this->flushPreload($id, $group); 219 220 $file = $this->getFilename($id, $group); 221 if (PEAR::isError($file)) { 222 return $file; 223 } 224 225 if (file_exists($file)) { 226 $ok = unlink($file); 227 clearstatcache(); 228 229 return $ok; 230 } 231 232 return false; 233 } // end func remove 234 235 function flush($group) 236 { 237 $this->flushPreload(); 238 $dir = ($group) ? $this->cache_dir . $group . '/' : $this->cache_dir; 239 240 $num_removed = $this->deleteDir($dir); 241 unset($this->group_dirs[$group]); 242 clearstatcache(); 243 244 return $num_removed; 245 } // end func flush 246 247 function idExists($id, $group) 248 { 249 return file_exists($this->getFilename($id, $group)); 250 } // end func idExists 251 252 /** 253 * Deletes all expired files. 254 * 255 * Garbage collection for files is a rather "expensive", "long time" 256 * operation. All files in the cache directory have to be examined which 257 * means that they must be opened for reading, the expiration date has to be 258 * read from them and if neccessary they have to be unlinked (removed). 259 * If you have a user comment for a good default gc probability please add it to 260 * to the inline docs. 261 * 262 * @param integer Maximum lifetime in seconds of an no longer used/touched entry 263 * @throws Cache_Error 264 */ 265 function garbageCollection($maxlifetime) 266 { 267 $this->flushPreload(); 268 clearstatcache(); 269 270 $ok = $this->doGarbageCollection($maxlifetime, $this->cache_dir); 271 272 // check the space used by the cache entries 273 if ($this->total_size > $this->highwater) { 274 275 krsort($this->entries); 276 reset($this->entries); 277 278 while ($this->total_size > $this->lowwater && list($lastmod, $entry) = each($this->entries)) { 279 if (@unlink($entry['file'])) { 280 $this->total_size -= $entry['size']; 281 } else { 282 new CacheError("Can't delete {$entry['file']}. Check the permissions."); 283 } 284 } 285 286 } 287 288 $this->entries = array(); 289 $this->total_size = 0; 290 291 return $ok; 292 } // end func garbageCollection 293 294 /** 295 * Does the recursive gc procedure, protected. 296 * 297 * @param integer Maximum lifetime in seconds of an no longer used/touched entry 298 * @param string directory to examine - don't sets this parameter, it's used for a 299 * recursive function call! 300 * @throws Cache_Error 301 */ 302 function doGarbageCollection($maxlifetime, $dir) 303 { 304 if (!is_writable($dir) || !is_readable($dir) || !($dh = opendir($dir))) { 305 return new Cache_Error("Can't remove directory '$dir'. Check permissions and path.", __FILE__, __LINE__); 306 } 307 308 while ($file = readdir($dh)) { 309 if ('.' == $file || '..' == $file) 310 continue; 311 312 $file = $dir . $file; 313 if (is_dir($file)) { 314 $this->doGarbageCollection($maxlifetime,$file . '/'); 315 continue; 316 } 317 318 // skip trouble makers but inform the user 319 if (!($fh = @fopen($file, 'rb'))) { 320 new Cache_Error("Can't access cache file '$file', skipping it. Check permissions and path.", __FILE__, __LINE__); 321 continue; 322 } 323 324 $expire = fgets($fh, 11); 325 fclose($fh); 326 $lastused = filemtime($file); 327 328 $this->entries[$lastused] = array('file' => $file, 'size' => filesize($file)); 329 $this->total_size += filesize($file); 330 331 // remove if expired 332 if (( ($expire && $expire <= time()) || ($lastused <= (time() - $maxlifetime)) ) && !unlink($file)) { 333 new Cache_Error("Can't unlink cache file '$file', skipping. Check permissions and path.", __FILE__, __LINE__); 334 } 335 } 336 337 closedir($dh); 338 339 // flush the disk state cache 340 clearstatcache(); 341 342 } // end func doGarbageCollection 343 344 /** 345 * Returns the filename for the specified id. 346 * 347 * @param string dataset ID 348 * @param string cache group 349 * @return string full filename with the path 350 * @access public 351 */ 352 function getFilename($id, $group) 353 { 354 if (isset($this->group_dirs[$group])) { 355 return $this->group_dirs[$group] . $this->filename_prefix . $id; 356 } 357 358 $dir = $this->cache_dir . $group . '/'; 359 if (is_writeable($this->cache_dir)) { 360 if (!file_exists($dir)) { 361 mkdir($dir, 0755, true); 362 clearstatcache(); 363 } 364 } else { 365 return new Cache_Error("Can't make directory '$dir'. Check permissions and path.", __FILE__, __LINE__); 366 } 367 $this->group_dirs[$group] = $dir; 368 369 return $dir . $this->filename_prefix . $id; 370 } // end func getFilename 371 372 /** 373 * Deletes a directory and all files in it. 374 * 375 * @param string directory 376 * @return integer number of removed files 377 * @throws Cache_Error 378 */ 379 function deleteDir($dir) 380 { 381 if (!is_writable($dir) || !is_readable($dir) || !($dh = opendir($dir))) { 382 return new Cache_Error("Can't remove directory '$dir'. Check permissions and path.", __FILE__, __LINE__); 383 } 384 385 $num_removed = 0; 386 387 while (false !== $file = readdir($dh)) { 388 if ('.' == $file || '..' == $file) 389 continue; 390 391 $file = $dir . $file; 392 if (is_dir($file)) { 393 $file .= '/'; 394 $num = $this->deleteDir($file . '/'); 395 if (is_int($num)) 396 $num_removed += $num; 397 } else { 398 if (unlink($file)) 399 $num_removed++; 400 } 401 } 402 // according to php-manual the following is needed for windows installations. 403 closedir($dh); 404 unset( $dh); 405 if ($dir != $this->cache_dir) { //delete the sub-dir entries itself also, but not the cache-dir. 406 rmDir($dir); 407 $num_removed++; 408 } 409 410 return $num_removed; 411 } // end func deleteDir 412 413} // end class file 414?> 415