Rev 250 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
<?phpclass CacheFichier {/*** Available options** =====> (string) cache_dir :* - Directory where to put the cache files** =====> (boolean) file_locking :* - Enable / disable file_locking* - Can avoid cache corruption under bad circumstances but it doesn't work on multithread* webservers and on NFS filesystems for example** =====> (boolean) read_control :* - Enable / disable read control* - If enabled, a control key is embeded in cache file and this key is compared with the one* calculated after the reading.** =====> (string) read_control_type :* - Type of read control (only if read control is enabled). Available values are :* 'md5' for a md5 hash control (best but slowest)* 'crc32' for a crc32 hash control (lightly less safe but faster, better choice)* 'adler32' for an adler32 hash control (excellent choice too, faster than crc32)* 'strlen' for a length only test (fastest)** =====> (int) hashed_directory_level :* - Hashed directory level* - Set the hashed directory structure level. 0 means "no hashed directory* structure", 1 means "one level of directory", 2 means "two levels"...* This option can speed up the cache only when you have many thousands of* cache file. Only specific benchs can help you to choose the perfect value* for you. Maybe, 1 or 2 is a good start.** =====> (int) hashed_directory_umask :* - Umask for hashed directory structure** =====> (string) file_name_prefix :* - prefix for cache files* - be really carefull with this option because a too generic value in a system cache dir* (like /tmp) can cause disasters when cleaning the cache** =====> (int) cache_file_umask :* - Umask for cache files** =====> (int) metatadatas_array_max_size :* - max size for the metadatas array (don't change this value unless you* know what you are doing)** @var array available options*/protected $options = array('stockage_chemin' => null,'fichier_verrou' => true,// file_locking'controle_lecture' => true,// controle de lecture'controle_lecture_type' => 'crc32','dossier_niveau' => 0,'dossier_umask' => 0700,'fichier_prefixe' => 'tbf','fichier_umask' => 0600,'metadonnees_max_taille' => 100);/*** Array of metadatas (each item is an associative array)** @var array*/protected $metadonnees = array(); // metadatasArray/*** Constructor** @param array $options associative array of options* @throws Zend_Cache_Exception* @return void*/public function __construct(array $options = array()) {if (isset($this->options['prefixe_fichier'])) {if (!preg_match('~^[a-zA-Z0-9_]+$~D', $this->options['prefixe_fichier'])) {trigger_error("Préfixe de nom de fichier invalide : doit contenir seulement [a-zA-Z0-9_]", E_USER_WARNING);}}if ($this->_options['metadonnees_max_taille'] < 10) {trigger_error("Taille du tableau des méta-données invalide, elle doit être > 10", E_USER_WARNING);}if (isset($options['dossier_umask']) && is_string($options['dossier_umask'])) {// See #ZF-4422$this->options['dossier_umask'] = octdec($this->options['dossier_umask']);}if (isset($options['fichier_umask']) && is_string($options['fichier_umask'])) {// See #ZF-4422$this->options['fichier_umask'] = octdec($this->options['fichier_umask']);}}private function setEmplacement($emplacement) {if (!is_dir($emplacement)) {trigger_error("L'emplacement doit être un dossier.", E_USER_WARNING);}if (!is_writable($emplacement)) {trigger_error("Le dossier de stockage du cache n'est pas accessible en écriture", E_USER_WARNING);}$emplacement = rtrim(realpath($emplacement), '\\/').DS;$this->options['stockage_chemin'] = $emplacement;}/*** Test if a cache is available for the given id and (if yes) return it (false else)** @param string $id cache id* @param boolean $doNotTestCacheValidity if set to true, the cache validity won't be tested* @return string|false cached datas*/public function charger($id, $ne_pas_tester_validiter_du_cache = false) {$donnees = false;if ($this->tester($id, $ne_pas_tester_validiter_du_cache)) {$metadonnees = $this->getMetadonneesFichier($id);$fichier = $this->getFichierNom($id);$donnees = $this->getContenuFichier($fichier);if ($this->options['controle_lecture']) {$cle_secu_donnees = $this->genererCleSecu($donnees, $this->options['controle_lecture_type']);$cle_secu_controle = $metadonnees['hash'];if ($cle_secu_donnees != $cle_secu_controle) {// Probléme détecté par le contrôle de lecture !// TODO : loguer le pb de sécu$this->supprimer($id);$donnees = false;}}}return $donnees;}/*** Teste si un enregistrement en cache est disponible ou pas (pour l'id passé en paramètre).** @param string $id identifiant de cache.* @return mixed false (le cache n'est pas disponible) ou timestamp (int) "de dernière modification" de l'enregistrement en cache*/public function tester($id) {clearstatcache();return $this->testerExistenceCache($id, false);}/*** Save some string datas into a cache record** Note : $data is always "string" (serialization is done by the* core not by the backend)** @param string $data Datas to cache* @param string $id Cache id* @param array $tags Array of strings, the cache record will be tagged by each string entry* @param int $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime)* @return boolean true if no problem*/public function save($data, $id, $tags = array(), $specificLifetime = false) {clearstatcache();$file = $this->getFichierNom($id);$path = $this->getChemin($id);if ($this->_options['hashed_directory_level'] > 0) {if (!is_writable($path)) {// maybe, we just have to build the directory structure$this->lancerMkdirEtChmodRecursif($id);}if (!is_writable($path)) {return false;}}if ($this->_options['read_control']) {$hash = $this->genererCleSecu($data, $this->_options['read_control_type']);} else {$hash = '';}$metadatas = array('hash' => $hash,'mtime' => time(),'expire' => $this->_expireTime($this->getLifetime($specificLifetime)),'tags' => $tags);$res = $this->_setMetadatas($id, $metadatas);if (!$res) {$this->_log('Zend_Cache_Backend_File::save() / error on saving metadata');return false;}$res = $this->setContenuFichier($file, $data);return $res;}/*** Remove a cache record** @param string $id cache id* @return boolean true if no problem*/public function supprimer($id) {$fichier = $this->getFichierNom($id);$suppression_fichier = $this->supprimerFichier($fichier);$suppression_metadonnees = $this->supprimerMetadonnees($id);return $suppression_metadonnees && $suppression_fichier;}/*** Clean some cache records** Available modes are :* 'all' (default) => remove all cache entries ($tags is not used)* 'old' => remove too old cache entries ($tags is not used)* 'matchingTag' => remove cache entries matching all given tags* ($tags can be an array of strings or a single string)* 'notMatchingTag' => remove cache entries not matching one of the given tags* ($tags can be an array of strings or a single string)* 'matchingAnyTag' => remove cache entries matching any given tags* ($tags can be an array of strings or a single string)** @param string $mode clean mode* @param tags array $tags array of tags* @return boolean true if no problem*/public function nettoyer($mode = Cache::NETTOYAGE_MODE_TOUS, $tags = array()) {// We use this protected method to hide the recursive stuffclearstatcache();return $this->nettoyerFichiers($this->options['stockage_dossier'], $mode, $tags);}/*** Return an array of stored cache ids** @return array array of stored cache ids (string)*/public function getIds(){return $this->_get($this->_options['cache_dir'], 'ids', array());}/*** Return an array of stored tags** @return array array of stored tags (string)*/public function getTags(){return $this->_get($this->_options['cache_dir'], 'tags', array());}/*** Return an array of stored cache ids which match given tags** In case of multiple tags, a logical AND is made between tags** @param array $tags array of tags* @return array array of matching cache ids (string)*/public function getIdsMatchingTags($tags = array()){return $this->_get($this->_options['cache_dir'], 'matching', $tags);}/*** Return an array of stored cache ids which don't match given tags** In case of multiple tags, a logical OR is made between tags** @param array $tags array of tags* @return array array of not matching cache ids (string)*/public function getIdsNotMatchingTags($tags = array()){return $this->_get($this->_options['cache_dir'], 'notMatching', $tags);}/*** Return an array of stored cache ids which match any given tags** In case of multiple tags, a logical AND is made between tags** @param array $tags array of tags* @return array array of any matching cache ids (string)*/public function getIdsMatchingAnyTags($tags = array()){return $this->_get($this->_options['cache_dir'], 'matchingAny', $tags);}/*** Return the filling percentage of the backend storage** @throws Zend_Cache_Exception* @return int integer between 0 and 100*/public function getFillingPercentage(){$free = disk_free_space($this->_options['cache_dir']);$total = disk_total_space($this->_options['cache_dir']);if ($total == 0) {Zend_Cache::throwException('can\'t get disk_total_space');} else {if ($free >= $total) {return 100;}return ((int) (100. * ($total - $free) / $total));}}/*** Return an array of metadatas for the given cache id** The array must include these keys :* - expire : the expire timestamp* - tags : a string array of tags* - mtime : timestamp of last modification time** @param string $id cache id* @return array array of metadatas (false if the cache id is not found)*/public function getMetadatas($id){$metadatas = $this->getMetadonneesFichier($id);if (!$metadatas) {return false;}if (time() > $metadatas['expire']) {return false;}return array('expire' => $metadatas['expire'],'tags' => $metadatas['tags'],'mtime' => $metadatas['mtime']);}/*** Give (if possible) an extra lifetime to the given cache id** @param string $id cache id* @param int $extraLifetime* @return boolean true if ok*/public function touch($id, $extraLifetime){$metadatas = $this->getMetadonneesFichier($id);if (!$metadatas) {return false;}if (time() > $metadatas['expire']) {return false;}$newMetadatas = array('hash' => $metadatas['hash'],'mtime' => time(),'expire' => $metadatas['expire'] + $extraLifetime,'tags' => $metadatas['tags']);$res = $this->_setMetadatas($id, $newMetadatas);if (!$res) {return false;}return true;}/*** Get a metadatas record** @param string $id Cache id* @return array|false Associative array of metadatas*/protected function getMetadonneesFichier($id) {$metadonnees = false;if (isset($this->metadonnees[$id])) {$metadonnees = $this->metadonnees[$id];} else {if ($metadonnees = $this->chargerMetadonnees($id)) {$this->setMetadonnees($id, $metadonnees, false);}}return $metadonnees;}/*** Set a metadatas record** @param string $id Cache id* @param array $metadatas Associative array of metadatas* @param boolean $save optional pass false to disable saving to file* @return boolean True if no problem*/protected function setMetadonnees($id, $metadonnees, $sauvegarde = true) {if (count($this->metadonnees) >= $this->options['metadonnees_max_taille']) {$n = (int) ($this->options['metadonnees_max_taille'] / 10);$this->metadonnees = array_slice($this->metadonnees, $n);}$resultat = true;if ($sauvegarde) {$resultat = $this->sauverMetadonnees($id, $metadonnees);}if ($resultat == true) {$this->metadonnees[$id] = $metadonnees;}return $resultat;}/*** Drop a metadata record** @param string $id Cache id* @return boolean True if no problem*/protected function supprimerMetadonnees($id) {if (isset($this->metadonnees[$id])) {unset($this->metadonnees[$id]);}$fichier_meta = $this->getNomFichierMeta($id);return $this->supprimerFichier($fichier_meta);}/*** Clear the metadatas array** @return void*/protected function nettoyerMetadonnees() {$this->metadonnees = array();}/*** Load metadatas from disk** @param string $id Cache id* @return array|false Metadatas associative array*/protected function chargerMetadonnees($id) {$fichier = $this->getNomFichierMeta($id);if ($resultat = $this->getContenuFichier($fichier)) {$resultat = @unserialize($resultat);}return $resultat;}/*** Save metadatas to disk** @param string $id Cache id* @param array $metadatas Associative array* @return boolean True if no problem*/protected function sauverMetadonnees($id, $metadonnees) {$fichier = $this->getNomFichierMeta($id);$resultat = $this->setContenuFichier($fichier, serialize($metadonnees));return $resultat;}/*** Make and return a file name (with path) for metadatas** @param string $id Cache id* @return string Metadatas file name (with path)*/protected function getNomFichierMeta($id) {$chemin = $this->getChemin($id);$fichier_nom = $this->transformaterIdEnNomFichier('internal-metadatas---'.$id);return $chemin.$fichier_nom;}/*** Check if the given filename is a metadatas one** @param string $fileName File name* @return boolean True if it's a metadatas one*/protected function etreFichierMeta($fichier_nom) {$id = $this->transformerNomFichierEnId($fichier_nom);return (substr($id, 0, 21) == 'internal-metadatas---') ? true : false;}/*** Remove a file** If we can't remove the file (because of locks or any problem), we will touch* the file to invalidate it** @param string $file Complete file path* @return boolean True if ok*/protected function supprimerFichier($fichier) {$resultat = false;if (is_file($fichier)) {if ($resultat = @unlink($fichier)) {// TODO : ajouter un log}}return $resultat;}/*** Clean some cache records (protected method used for recursive stuff)** Available modes are :* Zend_Cache::CLEANING_MODE_ALL (default) => remove all cache entries ($tags is not used)* Zend_Cache::CLEANING_MODE_OLD => remove too old cache entries ($tags is not used)* Zend_Cache::CLEANING_MODE_MATCHING_TAG => remove cache entries matching all given tags* ($tags can be an array of strings or a single string)* Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags}* ($tags can be an array of strings or a single string)* Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags* ($tags can be an array of strings or a single string)** @param string $dir Directory to clean* @param string $mode Clean mode* @param array $tags Array of tags* @throws Zend_Cache_Exception* @return boolean True if no problem*/protected function nettoyerFichiers($dossier, $mode = Cache::NETTOYAGE_MODE_TOUS, $tags = array()) {if (!is_dir($dossier)) {return false;}$resultat = true;$prefixe = $this->options['fichier_prefixe'];$glob = @glob($dossier.$prefixe.'--*');if ($glob === false) {// On some systems it is impossible to distinguish between empty match and an error.return true;}foreach ($glob as $fichier) {if (is_file($fichier)) {$fichier_nom = basename($fichier);if ($this->etreFichierMeta($fichier_nom)) {// in CLEANING_MODE_ALL, we drop anything, even remainings old metadatas filesif ($mode != Cache::NETTOYAGE_MODE_TOUS) {continue;}}$id = $this->transformerNomFichierEnId($fichier_nom);$metadatas = $this->getMetadonneesFichier($id);if ($metadatas === FALSE) {$metadatas = array('expire' => 1, 'tags' => array());}switch ($mode) {case Zend_Cache::CLEANING_MODE_ALL:$res = $this->remove($id);if (!$res) {// in this case only, we accept a problem with the metadatas file drop$res = $this->supprimerFichier($file);}$resultat = $resultat && $res;break;case Zend_Cache::CLEANING_MODE_OLD:if (time() > $metadatas['expire']) {$resultat = $this->remove($id) && $resultat;}break;case Zend_Cache::CLEANING_MODE_MATCHING_TAG:$matching = true;foreach ($tags as $tag) {if (!in_array($tag, $metadatas['tags'])) {$matching = false;break;}}if ($matching) {$resultat = $this->remove($id) && $resultat;}break;case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG:$matching = false;foreach ($tags as $tag) {if (in_array($tag, $metadatas['tags'])) {$matching = true;break;}}if (!$matching) {$resultat = $this->remove($id) && $resultat;}break;case Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG:$matching = false;foreach ($tags as $tag) {if (in_array($tag, $metadatas['tags'])) {$matching = true;break;}}if ($matching) {$resultat = $this->remove($id) && $resultat;}break;default:Zend_Cache::throwException('Invalid mode for clean() method');break;}}if ((is_dir($file)) and ($this->_options['hashed_directory_level']>0)) {// Recursive call$resultat = $this->nettoyerFichiers($file . DIRECTORY_SEPARATOR, $mode, $tags) && $resultat;if ($mode=='all') {// if mode=='all', we try to drop the structure too@rmdir($file);}}}return $resultat;}protected function _get($dir, $mode, $tags = array()) {if (!is_dir($dir)) {return false;}$result = array();$prefix = $this->_options['file_name_prefix'];$glob = @glob($dir . $prefix . '--*');if ($glob === false) {// On some systems it is impossible to distinguish between empty match and an error.return array();}foreach ($glob as $file) {if (is_file($file)) {$fileName = basename($file);$id = $this->transformerNomFichierEnId($fileName);$metadatas = $this->getMetadonneesFichier($id);if ($metadatas === FALSE) {continue;}if (time() > $metadatas['expire']) {continue;}switch ($mode) {case 'ids':$result[] = $id;break;case 'tags':$result = array_unique(array_merge($result, $metadatas['tags']));break;case 'matching':$matching = true;foreach ($tags as $tag) {if (!in_array($tag, $metadatas['tags'])) {$matching = false;break;}}if ($matching) {$result[] = $id;}break;case 'notMatching':$matching = false;foreach ($tags as $tag) {if (in_array($tag, $metadatas['tags'])) {$matching = true;break;}}if (!$matching) {$result[] = $id;}break;case 'matchingAny':$matching = false;foreach ($tags as $tag) {if (in_array($tag, $metadatas['tags'])) {$matching = true;break;}}if ($matching) {$result[] = $id;}break;default:Zend_Cache::throwException('Invalid mode for _get() method');break;}}if ((is_dir($file)) and ($this->_options['hashed_directory_level']>0)) {// Recursive call$recursiveRs = $this->_get($file . DIRECTORY_SEPARATOR, $mode, $tags);if ($recursiveRs === false) {$this->_log('Zend_Cache_Backend_File::_get() / recursive call : can\'t list entries of "'.$file.'"');} else {$result = array_unique(array_merge($result, $recursiveRs));}}}return array_unique($result);}/*** Compute & return the expire time** @return int expire time (unix timestamp)*/protected function _expireTime($lifetime) {if ($lifetime === null) {return 9999999999;}return time() + $lifetime;}/*** Make a control key with the string containing datas** @param string $data Data* @param string $controlType Type of control 'md5', 'crc32' or 'strlen'* @throws Zend_Cache_Exception* @return string Control key*/protected function genererCleSecu($donnees, $type_de_controle) {switch ($type_de_controle) {case 'md5':return md5($donnees);case 'crc32':return crc32($donnees);case 'strlen':return strlen($donnees);case 'adler32':return hash('adler32', $donnees);default:trigger_error("Fonction de génération de clé de sécurité introuvable : $type_de_controle", E_USER_WARNING);}}/*** Transform a cache id into a file name and return it** @param string $id Cache id* @return string File name*/protected function transformaterIdEnNomFichier($id) {$prefixe = $this->options['fichier_prefixe'];$resulta = $prefixe.'---'.$id;return $resultat;}/*** Make and return a file name (with path)** @param string $id Cache id* @return string File name (with path)*/protected function getFichierNom($id) {$path = $this->getChemin($id);$fileName = $this->transformaterIdEnNomFichier($id);return $path . $fileName;}/*** Return the complete directory path of a filename (including hashedDirectoryStructure)** @param string $id Cache id* @param boolean $decoupage if true, returns array of directory parts instead of single string* @return string Complete directory path*/protected function getChemin($id, $decoupage = false) {$morceaux = array();$chemin = $this->options['stockage_chemin'];$prefixe = $this->options['fichier_prefixe'];if ($this->options['dossier_niveau'] > 0) {$hash = hash('adler32', $id);for ($i = 0 ; $i < $this->options['dossier_niveau'] ; $i++) {$chemin .= $prefix.'--'.substr($hash, 0, $i + 1).DS;$morceaux[] = $chemin;}}return ($decoupage) ? $morceaux : $chemin;}/*** Make the directory strucuture for the given id** @param string $id cache id* @return boolean true*/protected function lancerMkdirEtChmodRecursif($id) {$resultat = true;if ($this->options['dossier_niveau'] > 0) {$chemins = $this->getChemin($id, true);foreach ($chemins as $chemin) {if (!is_dir($chemin)) {@mkdir($chemin, $this->options['dossier_umask']);@chmod($chemin, $this->options['dossier_umask']); // see #ZF-320 (this line is required in some configurations)}}}return $resultat;}/*** Test if the given cache id is available (and still valid as a cache record)** @param string $id Cache id* @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested* @return boolean|mixed false (a cache is not available) or "last modified" timestamp (int) of the available cache record*/protected function testerExistenceCache($id, $ne_pas_tester_validiter_du_cache) {$resultat = false;if ($metadonnees = $this->getMetadonnees($id)) {if ($ne_pas_tester_validiter_du_cache || (time() <= $metadonnees['expiration'])) {$resultat = $metadonnees['mtime'];}}return $resultat;}/*** Return the file content of the given file** @param string $file File complete path* @return string File content (or false if problem)*/protected function getContenuFichier($fichier) {$resultat = false;if (is_file($fichier)) {$f = @fopen($fichier, 'rb');if ($f) {if ($this->options['fichier_verrou']) @flock($f, LOCK_SH);$resultat = stream_get_contents($f);if ($this->options['fichier_verrou']) @flock($f, LOCK_UN);@fclose($f);}}return $resultat;}/*** Put the given string into the given file** @param string $file File complete path* @param string $string String to put in file* @return boolean true if no problem*/protected function setContenuFichier($fichier, $chaine) {$resultat = false;$f = @fopen($fichier, 'ab+');if ($f) {if ($this->options['fichier_verrou']) @flock($f, LOCK_EX);fseek($f, 0);ftruncate($f, 0);$tmp = @fwrite($f, $chaine);if (!($tmp === FALSE)) {$resultat = true;}@fclose($f);}@chmod($fichier, $this->options['fichier_umask']);return $resultat;}/*** Transform a file name into cache id and return it** @param string $fileName File name* @return string Cache id*/protected function transformerNomFichierEnId($nom_de_fichier) {$prefixe = $this->options['fichier_prefixe'];return preg_replace('~^' . $prefixe . '---(.*)$~', '$1', $nom_de_fichier);}}?>