Blame | Last modification | View Log | RSS feed
<?phpclass CacheFichier {/*** Options disponibles** ====> (string) stockage_chemin :* Chemin vers le dossier devant contenir l'arborescence du cache.** =====> (boolean) fichier_verrou :* - Active / Désactive le verrouillage des fichiers* - Peut éviter la corruption du cache dans de mauvaises circonstances, mais cela ne fonctionne pas sur des serveur* multithread et sur les systèmes de fichiers NFS par exemple.** =====> (boolean) controle_lecture :* - Activer / désactiver le contrôle de lecture* - S'il est activé, une clé de contrôle est ajoutée dans le fichier de cache et cette clé est comparée avec celle calculée* après la lecture.** =====> (string) controle_lecture_type :* Type de contrôle de lecture (seulement si le contrôle de lecture est activé).* Les valeurs disponibles sont:* - «md5» pour un contrôle md5 (le meilleur mais le plus lent)* - «crc32» pour un contrôle de hachage crc32 (un peu moins sécurisé, mais plus rapide, un meilleur choix)* - «adler32» pour un contrôle de hachage adler32 (excellent choix aussi, plus rapide que crc32)* - «strlen» pour un test de longueur uniquement (le plus rapide)** =====> (int) dossier_niveau :* - Permet de réglez le nombre de niveau de sous-dossier que contiendra l'arborescence des dossiers du cache.* 0 signifie "pas de sous-dossier pour le cache",* 1 signifie "un niveau de sous-dossier",* 2 signifie "deux niveaux" ...* Cette option peut accélérer le cache seulement lorsque vous avez plusieurs centaines de fichiers de cache.* Seuls des tests spécifiques peuvent vous aider à choisir la meilleure valeur possible pour vous.* 1 ou 2 peut être est un bon début.** =====> (int) dossier_umask :* - Umask pour les sous-dossiers de l'arborescence du cache.** =====> (string) fichier_prefixe :* - préfixe pour les fichiers du cache* - ATTENTION : faite vraiment attention avec cette option, car une valeur trop générique dans le dossier cache du système* (comme /tmp) peut provoquer des catastrophes lors du nettoyage du cache.** =====> (int) fichier_umask :* - Umask pour les fichiers de cache** =====> (int) metadonnees_max_taille :* - taille maximum pour le tableau de métadonnées du cache (ne changer pas cette valeur sauf si vous savez ce que vous faite)** @var array options disponibles*/protected $options = array('stockage_chemin' => null,'fichier_verrou' => true,'controle_lecture' => true,'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();private $Cache = null;/*** Constructor** @param array $options associative array of options* @throws Zend_Cache_Exception* @return void*/public function __construct(array $options = array(), Cache $cache) {$this->Cache = $cache;$this->initialiserOptionsParConfig();$this->setOptions($options);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 initialiserOptionsParConfig() {while (list($nom, $valeur) = each($this->options)) {if (Config::existe($nom)) {$this->options[$nom] = Config::get($nom);}}}private function setOptions($options) {while (list($nom, $valeur) = each($options)) {if (!is_string($nom)) {trigger_error("Nom d'option incorecte : $nom", E_USER_WARNING);}$nom = strtolower($nom);if (array_key_exists($nom, $this->options)) {$this->options[$nom] = $valeur;}}}public 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 sauver($donnees, $id, $tags = array(), $duree_vie_specifique = false) {clearstatcache();$fichier = $this->getFichierNom($id);$chemin = $this->getChemin($id);$resultat = true;if ($this->options['dossier_niveau'] > 0) {if (!is_writable($chemin)) {// maybe, we just have to build the directory structure$this->lancerMkdirEtChmodRecursif($id);}if (!is_writable($chemin)) {$resultat = false;}}if ($resultat === true) {if ($this->options['controle_lecture']) {$cle_secu = $this->genererCleSecu($donnees, $this->options['controle_lecture_type']);} else {$cle_secu = '';}$metadonnees = array('hash' => $cle_secu,'mtime' => time(),'expiration' => $this->Cache->getTimestampExpiration($duree_vie_specifique),'tags' => $tags);if (! $resultat = $this->setMetadonnees($id, $metadonnees)) {// TODO : ajouter un log} else {$resultat = $this->setContenuFichier($fichier, $donnees);}}return $resultat;}/*** 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_chemin'], $mode, $tags);}/*** Return an array of stored cache ids** @return array array of stored cache ids (string)*/public function getIds() {return $this->analyserCache($this->options['stockage_chemin'], 'ids', array());}/*** Return an array of stored tags** @return array array of stored tags (string)*/public function getTags() {return $this->analyserCache($this->options['stockage_chemin'], '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 getIdsAvecLesTags($tags = array()) {return $this->analyserCache($this->options['stockage_chemin'], '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 getIdsSansLesTags($tags = array()) {return $this->analyserCache($this->options['stockage_chemin'], '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 getIdsAvecUnTag($tags = array()) {return $this->analyserCache($this->options['stockage_chemin'], 'matchingAny', $tags);}/*** Return the filling percentage of the backend storage** @throws Zend_Cache_Exception* @return int integer between 0 and 100*/public function getPourcentageRemplissage() {$libre = disk_free_space($this->options['stockage_chemin']);$total = disk_total_space($this->options['stockage_chemin']);$pourcentage = 0;if ($total == 0) {trigger_error("Impossible d'utiliser la fonction disk_total_space", E_USER_WARNING);} else {$pourcentage = ($libre >= $total) ? 100 : ((int) (100. * ($total - $libre) / $total));}return $pourcentage;}/*** 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 getMetadonnees($id) {if ($metadonnees = $this->getMetadonneesFichier($id)) {if (time() > $metadonnees['expiration']) {$metadonnees = false;} else {$metadonnees = array('expiration' => $metadonnees['expiration'],'tags' => $metadonnees['tags'],'mtime' => $metadonnees['mtime']);}}return $metadonnees;}/*** 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 ajouterSupplementDureeDeVie($id, $supplement_duree_de_vie) {$augmentation = true;if ($metadonnees = $this->getMetadonneesFichier($id)) {if (time() > $metadonnees['expiration']) {$augmentation = false;} else {$metadonnees_nouvelle = array('hash' => $metadonnees['hash'],'mtime' => time(),'expiration' => $metadonnees['expiration'] + $supplement_duree_de_vie,'tags' => $metadonnees['tags']);$augmentation = $this->setMetadonnees($id, $metadonnees_nouvelle);}}return $augmentation;}/*** 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('interne-meta---'.$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) == 'interne-meta---') ? 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)) {// Pour le mode Cache::NETTOYAGE_MODE_TOUS, nous essayons de tous supprimer même les vieux fichiers métaif ($mode != Cache::NETTOYAGE_MODE_TOUS) {continue;}}$id = $this->transformerNomFichierEnId($fichier_nom);$metadonnees = $this->getMetadonneesFichier($id);if ($metadonnees === FALSE) {$metadonnees = array('expiration' => 1, 'tags' => array());}switch ($mode) {case Cache::NETTOYAGE_MODE_TOUS :if ($resultat_suppression = $this->supprimer($id)) {// Dans ce cas seulement, nous acception qu'il y ait un problème avec la suppresssion du fichier meta$resultat_suppression = $this->supprimerFichier($fichier);}$resultat = $resultat && $resultat_suppression;break;case Cache::NETTOYAGE_MODE_EXPIRATION :if (time() > $metadonnees['expiration']) {$resultat = $this->supprimer($id) && $resultat;}break;case Cache::NETTOYAGE_MODE_AVEC_LES_TAGS :$correspondance = true;foreach ($tags as $tag) {if (!in_array($tag, $metadonnees['tags'])) {$correspondance = false;break;}}if ($correspondance) {$resultat = $this->supprimer($id) && $resultat;}break;case Cache::NETTOYAGE_MODE_SANS_LES_TAGS :$correspondance = false;foreach ($tags as $tag) {if (in_array($tag, $metadonnees['tags'])) {$correspondance = true;break;}}if (!$correspondance) {$resultat = $this->supprimer($id) && $resultat;}break;case Cache::NETTOYAGE_MODE_AVEC_UN_TAG :$correspondance = false;foreach ($tags as $tag) {if (in_array($tag, $metadonnees['tags'])) {$correspondance = true;break;}}if ($correspondance) {$resultat = $this->supprimer($id) && $resultat;}break;default:trigger_error("Mode de nettoyage invalide pour la méthode nettoyer()", E_USER_WARNING);break;}}if ((is_dir($fichier)) and ($this->options['dossier_niveau'] > 0)) {// Appel récursif$resultat = $this->nettoyerFichiers($fichier.DS, $mode, $tags) && $resultat;if ($mode == Cache::NETTOYAGE_MODE_TOUS) {// Si mode == Cache::NETTOYAGE_MODE_TOUS, nous essayons de supprimer la structure aussi@rmdir($fichier);}}}return $resultat;}protected function analyserCache($dossier, $mode, $tags = array()) {if (!is_dir($dossier)) {return false;}$resultat = array();$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 array();}foreach ($glob as $fichier) {if (is_file($fichier)) {$nom_fichier = basename($fichier);$id = $this->transformerNomFichierEnId($nom_fichier);$metadonnees = $this->getMetadonneesFichier($id);if ($metadonnees === FALSE) {continue;}if (time() > $metadonnees['expiration']) {continue;}switch ($mode) {case 'ids':$resultat[] = $id;break;case 'tags':$resultat = array_unique(array_merge($resultat, $metadonnees['tags']));break;case 'matching':$correspondance = true;foreach ($tags as $tag) {if (!in_array($tag, $metadonnees['tags'])) {$correspondance = false;break;}}if ($correspondance) {$resultat[] = $id;}break;case 'notMatching':$correspondance = false;foreach ($tags as $tag) {if (in_array($tag, $metadonnees['tags'])) {$correspondance = true;break;}}if (!$correspondance) {$resultat[] = $id;}break;case 'matchingAny':$correspondance = false;foreach ($tags as $tag) {if (in_array($tag, $metadonnees['tags'])) {$correspondance = true;break;}}if ($correspondance) {$resultat[] = $id;}break;default:trigger_error("Mode invalide pour la méthode analyserCache()", E_USER_WARNING);break;}}if ((is_dir($fichier)) and ($this->options['dossier_niveau'] > 0)) {// Appel récursif$resultat_analyse_recursive = $this->analyserCache($fichier.DS, $mode, $tags);if ($resultat_analyse_recursive === false) {// TODO : ajoute un log} else {$resultat = array_unique(array_merge($resultat, $resultat_analyse_recursive));}}}return array_unique($resultat);}/*** 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'];$resultat = $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 .= $prefixe.'--'.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);}}?>