Subversion Repositories Applications.framework

Compare Revisions

Ignore whitespace Rev 245 → Rev 246

/trunk/framework/CacheFichier.php
New file
0,0 → 1,862
<?php
class 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 stuff
clearstatcache();
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 files
if ($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);
}
}
?>