Development

/branches/1.0/lib/cache/sfFileCache.class.php

You must first sign up to be able to contribute.

root/branches/1.0/lib/cache/sfFileCache.class.php

Revision 16899, 14.1 kB (checked in by fabien, 5 years ago)

[1.0, 1.1, 1.2, 1.3] optimized windows rename() problem hack (based on a patch from Agavi)

  • Property svn:mime-type set to text/x-php
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id Rev Date
Line 
1 <?php
2
3 /*
4  * This file is part of the symfony package.
5  * (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com>
6  *
7  * For the full copyright and license information, please view the LICENSE
8  * file that was distributed with this source code.
9  */
10
11 /**
12  * Cache class that stores content in files.
13  *
14  * This class is based on the PEAR_Cache_Lite class.
15  * All cache files are stored in files in the [sf_root_dir].'/cache/'.[sf_app].'/template' directory.
16  * To disable all caching, you can set to false [sf_cache] setting.
17  *
18  * @package    symfony
19  * @subpackage cache
20  * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
21  * @author     Fabien Marty <fab@php.net>
22  * @version    SVN: $Id$
23  */
24 class sfFileCache extends sfCache
25 {
26   const DEFAULT_NAMESPACE = '';
27
28  /**
29   * Directory where to put the cache files
30   */
31   protected $cacheDir = '';
32
33  /**
34   * Enable / disable fileLocking (can avoid cache corruption under bad circumstances)
35   * @var boolean $fileLocking
36   */
37   protected $fileLocking = true;
38
39  /**
40   * Enable / disable write control (the cache is read just after writing to detect corrupt entries)
41   *
42   * Enable write control will lightly slow the cache writing but not the cache reading
43   * Write control can detect some corrupt cache files but maybe it's not a perfect control
44   */
45   protected $writeControl = false;
46
47  /**
48   * Enable / disable read control
49   *
50   * If enabled, a control key is embeded in cache file and this key is compared with the one calculated after the reading.
51   */
52   protected $readControl = false;
53
54  /**
55   * File Name protection
56   *
57   * if set to true, you can use any cache id or namespace name
58   * if set to false, it can be faster but cache ids and namespace names
59   * will be used directly in cache file names so be carefull with
60   * special characters...
61   */
62   protected $fileNameProtection = false;
63
64  /**
65   * Disable / Tune the automatic cleaning process
66   *
67   * The automatic cleaning process destroy too old (for the given life time)
68   * cache files when a new cache file is written.
69   * 0               => no automatic cache cleaning
70   * 1               => systematic cache cleaning
71   * x (integer) > 1 => automatic cleaning randomly 1 times on x cache write
72   */
73   protected $automaticCleaningFactor = 500;
74
75  /**
76   * Nested directory level
77   */
78   protected $hashedDirectoryLevel = 0;
79
80  /**
81   * Cache suffix
82   */
83   protected
84     $suffix = '.cache';
85
86  /**
87   * Constructor.
88   *
89   * @param string The cache root directory
90   */
91   public function __construct($cacheDir = null)
92   {
93     if (!is_null($cacheDir))
94     {
95       $this->setCacheDir($cacheDir);
96     }
97   }
98
99   /**
100    * Initializes the cache.
101    *
102    * @param array An array of options
103    * Available options:
104    *  - cacheDir:                cache root directory
105    *  - fileLocking:             enable / disable file locking (boolean)
106    *  - writeControl:            enable / disable write control (boolean)
107    *  - readControl:             enable / disable read control (boolean)
108    *  - fileNameProtection:      enable / disable automatic file name protection (boolean)
109    *  - automaticCleaningFactor: disable / tune automatic cleaning process (int)
110    *  - hashedDirectoryLevel:    level of the hashed directory system (int)
111    *  - lifeTime:                default life time
112    *
113    */
114   public function initialize($options = array())
115   {
116     if (isset($options['cacheDir']))
117     {
118       $this->setCacheDir($options['cacheDir']);
119       unset($options['cacheDir']);
120     }
121
122     $availableOptions = array('fileLocking', 'writeControl', 'readControl', 'fileNameProtection', 'automaticCleaningFactor', 'hashedDirectoryLevel', 'lifeTime');
123     foreach ($options as $key => $value)
124     {
125       if (!in_array($key, $availableOptions) && sfConfig::get('sf_logging_enabled'))
126       {
127         sfLogger::getInstance()->err(sprintf('sfFileCache cannot take "%s" as an option', $key));
128       }
129
130       $this->$key = $value;
131     }
132   }
133
134   /**
135    * Sets the suffix for cache files.
136    *
137    * @param string The suffix name (with the leading .)
138    */
139   public function setSuffix($suffix)
140   {
141     $this->suffix = $suffix;
142   }
143
144  /**
145   * Enables / disables write control.
146   *
147   * @param boolean
148   */
149   public function setWriteControl($boolean)
150   {
151     $this->writeControl = $boolean;
152   }
153
154   /**
155    * Gets the value of the writeControl option.
156    *
157    * @return boolean
158    */
159   public function getWriteControl()
160   {
161     return $this->writeControl;
162   }
163
164  /**
165   * Enables / disables file locking.
166   *
167   * @param boolean
168   */
169   public function setFileLocking($boolean)
170   {
171     $this->fileLocking = $boolean;
172   }
173
174   /**
175    * Gets the value of the fileLocking option.
176    *
177    * @return boolean
178    */
179   public function getFileLocking()
180   {
181     return $this->fileLocking;
182   }
183
184   /**
185    * Sets the cache root directory.
186    *
187    * @param string The directory where to put the cache files
188    */
189   public function setCacheDir($cacheDir)
190   {
191     // remove last DIRECTORY_SEPARATOR
192     if (DIRECTORY_SEPARATOR == substr($cacheDir, -1))
193     {
194       $cacheDir = substr($cacheDir, 0, -1);
195     }
196
197     // create cache dir if needed
198     if (!is_dir($cacheDir))
199     {
200       $current_umask = umask(0000);
201       @mkdir($cacheDir, 0777, true);
202       umask($current_umask);
203     }
204
205      $this->cacheDir = $cacheDir;
206    }
207
208     public function getCacheDir()
209     {
210       return $this->cacheDir;
211     }
212
213  /**
214   * Tests if a cache is available and (if yes) returns it.
215   *
216   * @param  string  The cache id
217   * @param  string  The name of the cache namespace
218   * @param  boolean If set to true, the cache validity won't be tested
219   *
220   * @return string  Data of the cache (or null if no cache available)
221   *
222   * @see sfCache
223   */
224   public function get($id, $namespace = self::DEFAULT_NAMESPACE, $doNotTestCacheValidity = false)
225   {
226     $data = null;
227
228     list($path, $file) = $this->getFileName($id, $namespace);
229
230     if ($doNotTestCacheValidity)
231     {
232       if (file_exists($path.$file))
233       {
234         $data = $this->read($path, $file);
235       }
236     }
237     else
238     {
239       if ((file_exists($path.$file)) && (@filemtime($path.$file) > $this->refreshTime))
240       {
241         $data = $this->read($path, $file);
242       }
243     }
244
245     return $data ? $data : null;
246   }
247
248   /**
249    * Returns true if there is a cache for the given id and namespace.
250    *
251    * @param  string  The cache id
252    * @param  string  The name of the cache namespace
253    * @param  boolean If set to true, the cache validity won't be tested
254    *
255    * @return boolean true if the cache exists, false otherwise
256    *
257    * @see sfCache
258    */
259   public function has($id, $namespace = self::DEFAULT_NAMESPACE, $doNotTestCacheValidity = false)
260   {
261     list($path, $file) = $this->getFileName($id, $namespace);
262
263     if ($doNotTestCacheValidity)
264     {
265       if (file_exists($path.$file))
266       {
267         return true;
268       }
269     }
270     else
271     {
272       if ((file_exists($path.$file)) && (@filemtime($path.$file) > $this->refreshTime))
273       {
274         return true;
275       }
276     }
277
278     return false;
279   }
280
281  /**
282   * Saves some data in a cache file.
283   *
284   * @param string The cache id
285   * @param string The name of the cache namespace
286   * @param string The data to put in cache
287   *
288   * @return boolean true if no problem
289   *
290   * @see sfCache
291   */
292   public function set($id, $namespace = self::DEFAULT_NAMESPACE, $data)
293   {
294     list($path, $file) = $this->getFileName($id, $namespace);
295
296     if ($this->automaticCleaningFactor > 0)
297     {
298       $rand = rand(1, $this->automaticCleaningFactor);
299       if ($rand == 1)
300       {
301         $this->clean(false, 'old');
302       }
303     }
304
305     if ($this->writeControl)
306     {
307       return $this->writeAndControl($path, $file, $data);
308     }
309     else
310     {
311       return $this->write($path, $file, $data);
312     }
313   }
314
315  /**
316   * Removes a cache file.
317   *
318   * @param string The cache id
319   * @param string The name of the cache namespace
320   *
321   * @return boolean true if no problem
322   */
323   public function remove($id, $namespace = self::DEFAULT_NAMESPACE)
324   {
325     list($path, $file) = $this->getFileName($id, $namespace);
326
327     return $this->unlink($path.$file);
328   }
329
330  /**
331   * Cleans the cache.
332   *
333   * If no namespace is specified all cache files will be destroyed
334   * else only cache files of the specified namespace will be destroyed.
335   *
336   * @param string The name of the cache namespace
337   *
338   * @return boolean true if no problem
339   */
340   public function clean($namespace = null, $mode = 'all')
341   {
342     $namespace = str_replace('/', DIRECTORY_SEPARATOR, $namespace);
343
344     $dir = $this->cacheDir.DIRECTORY_SEPARATOR.$namespace;
345     if (!file_exists($dir))
346     {
347       return true;
348     }
349
350     return $this->cleanDir($dir, $mode);
351   }
352
353   /**
354    * Returns the cache last modification time.
355    *
356    * @return int The last modification time
357    */
358   public function lastModified($id, $namespace = self::DEFAULT_NAMESPACE)
359   {
360     list($path, $file) = $this->getFileName($id, $namespace);
361
362     return (file_exists($path.$file) ? filemtime($path.$file) : 0);
363   }
364
365  /**
366   * Makes a file name (with path).
367   *
368   * @param string The cache id
369   * @param string The name of the namespace
370   *
371   * @return array An array containing the path and the file name
372   */
373   protected function getFileName($id, $namespace)
374   {
375     $file = ($this->fileNameProtection) ? md5($id).$this->suffix : $id.$this->suffix;
376
377     if ($namespace)
378     {
379       $namespace = str_replace('/', DIRECTORY_SEPARATOR, $namespace);
380       $path = $this->cacheDir.DIRECTORY_SEPARATOR.$namespace.DIRECTORY_SEPARATOR;
381     }
382     else
383     {
384       $path = $this->cacheDir.DIRECTORY_SEPARATOR;
385     }
386     if ($this->hashedDirectoryLevel > 0)
387     {
388       $hash = md5($file);
389       for ($i = 0; $i < $this->hashedDirectoryLevel; $i++)
390       {
391         $path = $path.substr($hash, 0, $i + 1).DIRECTORY_SEPARATOR;
392       }
393     }
394
395     return array($path, $file);
396   }
397
398  /**
399   * Removes a file.
400   *
401   * @param string The complete file path and name
402   *
403   * @return boolean true if no problem
404   */
405   protected function unlink($file)
406   {
407     return @unlink($file) ? true : false;
408   }
409
410  /**
411   * Recursive function for cleaning cache file in the given directory.
412   *
413   * @param  string  The directory complete path
414   * @param  string  The name of the cache namespace
415   * @param  string  The flush cache mode : 'old', 'all'
416   *
417   * @return boolean true if no problem
418   *
419   * @throws sfCacheException
420   */
421   protected function cleanDir($dir, $mode)
422   {
423     if (!($dh = opendir($dir)))
424     {
425       throw new sfCacheException('Unable to open cache directory "'.$dir.'"');
426     }
427
428     $result = true;
429     while ($file = readdir($dh))
430     {
431       if (($file != '.') && ($file != '..'))
432       {
433         $file2 = $dir.DIRECTORY_SEPARATOR.$file;
434         if (is_file($file2))
435         {
436           $unlink = 1;
437           if ($mode == 'old')
438           {
439             // files older than lifeTime get deleted from cache
440             if ((time() - filemtime($file2)) < $this->lifeTime)
441             {
442               $unlink = 0;
443             }
444           }
445
446           if ($unlink)
447           {
448             $result = ($result and ($this->unlink($file2)));
449           }
450         }
451         else if (is_dir($file2))
452         {
453           $result = ($result and ($this->cleanDir($file2.DIRECTORY_SEPARATOR, $mode)));
454         }
455       }
456     }
457
458     return $result;
459   }
460
461  /**
462   * Reads the cache file and returns the content.
463   *
464   * @param string The file path
465   * @param string The file name
466   *
467   * @return string The content of the cache file.
468   *
469   * @throws sfCacheException
470   */
471   protected function read($path, $file)
472   {
473     $fp = @fopen($path.$file, "rb");
474     if ($this->fileLocking)
475     {
476       @flock($fp, LOCK_SH);
477     }
478
479     if ($fp)
480     {
481       clearstatcache(); // because the filesize can be cached by PHP itself...
482       $length = @filesize($path.$file);
483       $mqr = get_magic_quotes_runtime();
484       set_magic_quotes_runtime(0);
485       if ($this->readControl)
486       {
487         $hashControl = @fread($fp, 32);
488         $length = $length - 32;
489       }
490       $data = ($length) ? @fread($fp, $length) : '';
491       set_magic_quotes_runtime($mqr);
492       if ($this->fileLocking)
493       {
494         @flock($fp, LOCK_UN);
495       }
496       @fclose($fp);
497       if ($this->readControl)
498       {
499         $hashData = $this->hash($data);
500         if ($hashData != $hashControl)
501         {
502           @touch($path.$file, time() - 2 * abs($this->lifeTime));
503           return false;
504         }
505       }
506
507       return $data;
508     }
509
510     throw new sfCacheException('Unable to read cache file "'.$path.$file.'"');
511   }
512
513  /**
514   * Writes the given data in the cache file.
515   *
516   * @param string The file path
517   * @param string The file name
518   * @param string The data to put in cache
519   *
520   * @return boolean true if ok
521   *
522   * @throws sfCacheException
523   */
524   protected function write($path, $file, $data)
525   {
526     $current_umask = umask();
527     umask(0000);
528     if (!is_dir($path))
529     {
530       // create directory structure if needed
531       mkdir($path, 0777, true);
532     }
533
534     $tmpFile = tempnam(dirname($path), basename($file));
535
536     if (!$fp = @fopen($tmpFile, 'wb'))
537     {
538       throw new sfCacheException(sprintf('Unable to write cache file "%s".', $path.$file));
539     }
540
541     if ($this->readControl)
542     {
543       @fwrite($fp, $this->hash($data), 32);
544     }
545     @fwrite($fp, $data);
546     @fclose($fp);
547
548     // Hack from Agavi (http://trac.agavi.org/changeset/3979)
549     // With php < 5.2.6 on win32, renaming to an already existing file doesn't work, but copy does,
550     // so we simply assume that when rename() fails that we are on win32 and try to use copy()
551     if (!@rename($tmpFile, $path.$file))
552     {
553       if (copy($tmpFile, $path.$file))
554       {
555         unlink($tmpFile);
556       }
557     }
558
559     chmod($path.$file, 0666);
560     umask($current_umask);
561
562     return true;
563   }
564
565  /**
566   * Writes the given data in the cache file and controls it just after to avoid corrupted cache entries.
567   *
568   * @param string The file path
569   * @param string The file name
570   * @param string The data to put in cache
571   *
572   * @return boolean true if the test is ok
573   */
574   protected function writeAndControl($path, $file, $data)
575   {
576     $this->write($path, $file, $data);
577     $dataRead = $this->read($path, $file);
578
579     return ($dataRead == $data);
580   }
581
582  /**
583   * Makes a control key with the string containing datas.
584   *
585   * @param string $data data
586   *
587   * @return string control key
588   */
589   protected function hash($data)
590   {
591     return sprintf('% 32d', crc32($data));
592   }
593 }
594
Note: See TracBrowser for help on using the browser.