349 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			349 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| namespace nulib\file\cache;
 | |
| 
 | |
| use Exception;
 | |
| use nulib\cl;
 | |
| use nulib\cv;
 | |
| use nulib\file;
 | |
| use nulib\file\SharedFile;
 | |
| use nulib\os\path;
 | |
| use nulib\php\time\DateTime;
 | |
| use nulib\php\time\Delay;
 | |
| use nulib\str;
 | |
| use nulib\ValueException;
 | |
| 
 | |
| class CacheFile extends SharedFile {
 | |
|   /** @var string|int durée de vie par défaut des données mises en cache  */
 | |
|   const DURATION = "1D"; // jusqu'au lendemain
 | |
| 
 | |
|   const EXT = ".cache";
 | |
| 
 | |
|   function __construct($file, ?array $params=null) {
 | |
|     if ($file === null) {
 | |
|       $rand = bin2hex(random_bytes(8));
 | |
|       $file = sys_get_temp_dir()."/$rand";
 | |
|     }
 | |
|     $file = path::ensure_ext($file, self::EXT);
 | |
|     $this->basedir = path::dirname($file);
 | |
|     $basename = path::filename($file);
 | |
|     $this->basename = str::without_suffix(self::EXT, $basename);
 | |
|     $this->initialDuration = Delay::with($params["duration"] ?? static::DURATION);
 | |
|     $this->overrideDuration = $params["override_duration"] ?? false;
 | |
|     $this->readonly = $params["readonly"] ?? false;
 | |
|     $this->cacheNull = $params["cache_null"] ?? false;
 | |
|     $data = $params["data"] ?? null;
 | |
|     if ($data === null) {
 | |
|       $this->dataDefs = null;
 | |
|       $this->data = null;
 | |
|     } elseif ($data instanceof CacheData) {
 | |
|       $this->dataDefs = null;
 | |
|       $this->data = $data;
 | |
|     } elseif (!is_array($data)) {
 | |
|       $this->dataDefs = null;
 | |
|       $this->data = $this->getData($data);
 | |
|     } else {
 | |
|       $dataDefs = [];
 | |
|       $tmpdefs = $data;
 | |
|       $index = 0;
 | |
|       foreach ($tmpdefs as $key => $data) {
 | |
|         $odef = $data;
 | |
|         if ($data instanceof CacheData) {
 | |
|         } elseif (cv::subclass_of($data, CacheData::class)) {
 | |
|           $data = new $data();
 | |
|         } else {
 | |
|           throw ValueException::invalid_type($odef, CacheData::class);
 | |
|         }
 | |
|         if ($key === $index) {
 | |
|           $index++;
 | |
|           $key = $data->getName();
 | |
|         }
 | |
|         $dataDefs[$key] = $data;
 | |
|       }
 | |
|       $this->dataDefs = $dataDefs;
 | |
|     }
 | |
|     parent::__construct($file);
 | |
|   }
 | |
| 
 | |
|   /** @var string répertoire de base des fichiers de cache */
 | |
|   protected string $basedir;
 | |
| 
 | |
|   /** @var string nom de base des fichiers de cache */
 | |
|   protected string $basename;
 | |
| 
 | |
|   protected Delay $initialDuration;
 | |
| 
 | |
|   protected bool $overrideDuration;
 | |
| 
 | |
|   protected bool $readonly;
 | |
| 
 | |
|   protected bool $cacheNull;
 | |
| 
 | |
|   protected ?array $dataDefs;
 | |
| 
 | |
|   protected ?CacheData $data;
 | |
| 
 | |
|   protected function getData($data): CacheData {
 | |
|     if ($data === null) {
 | |
|       return $this->data ??= new CacheData(function() {
 | |
|         return $this->compute();
 | |
|       });
 | |
|     }
 | |
|     $odata = $data;
 | |
|     if (is_string($data) || is_int($data)) {
 | |
|       $data = $this->dataDefs[$data] ?? null;
 | |
|       if ($data === null) throw ValueException::invalid_key($odata);
 | |
|     }
 | |
|     if ($data instanceof CacheData) return $data;
 | |
|     throw ValueException::invalid_type($odata, CacheData::class);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * vérifier si le fichier est valide. s'il est invalide, il faut le recréer.
 | |
|    *
 | |
|    * on assume que le fichier existe, vu qu'il a été ouvert en c+b
 | |
|    */
 | |
|   protected function isValid(): bool {
 | |
|     # considèrer que le fichier est invalide s'il est de taille nulle
 | |
|     return $this->getSize() > 0;
 | |
|   }
 | |
| 
 | |
|   protected ?DateTime $start;
 | |
| 
 | |
|   protected ?Delay $duration;
 | |
| 
 | |
|   protected ?array $datafilenames;
 | |
| 
 | |
|   /** charger les données. le fichier a déjà été verrouillé en lecture */
 | |
|   protected function loadMetadata(): void {
 | |
|     if ($this->isValid()) {
 | |
|       $this->rewind();
 | |
|       [
 | |
|         "start" => $start,
 | |
|         "duration" => $duration,
 | |
|         "datafiles" => $datafilenames,
 | |
|       ] = $this->unserialize(null, false, true);
 | |
|       if ($this->overrideDuration) {
 | |
|         $duration = Delay::with($this->initialDuration, $start);
 | |
|       }
 | |
|       $datafilenames = array_fill_keys($datafilenames, true);
 | |
|     } else {
 | |
|       $start = null;
 | |
|       $duration = null;
 | |
|       $datafilenames = [];
 | |
|     }
 | |
|     $this->start = $start;
 | |
|     $this->duration = $duration;
 | |
|     $this->datafilenames = $datafilenames;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * tester s'il faut mettre les données à jour. le fichier a déjà été
 | |
|    * verrouillé en lecture
 | |
|    */
 | |
|   protected function shouldUpdate(bool $noCache=false): bool {
 | |
|     if ($this->isValid()) {
 | |
|       $expired = $this->duration->isElapsed();
 | |
|     } else {
 | |
|       $expired = false;
 | |
|       $noCache = true;
 | |
|     }
 | |
|     return $noCache || $expired;
 | |
|   }
 | |
| 
 | |
|   /** sauvegarder les données. le fichier a déjà été verrouillé en écriture */
 | |
|   protected function saveMetadata(): void {
 | |
|     $this->duration ??= $this->initialDuration;
 | |
|     if ($this->start === null) {
 | |
|       $this->start = new DateTime();
 | |
|       $this->duration = Delay::with($this->duration, $this->start);
 | |
|     }
 | |
|     $datafilenames = array_keys($this->datafilenames);
 | |
|     $this->ftruncate();
 | |
|     $this->serialize([
 | |
|       "start" => $this->start,
 | |
|       "duration" => $this->duration,
 | |
|       "datafiles" => $datafilenames,
 | |
|     ], false, true);
 | |
|   }
 | |
| 
 | |
|   protected function loadData(string $datafile) {
 | |
|     return file::reader($datafile)->unserialize();
 | |
|   }
 | |
| 
 | |
|   protected function saveData(string $datafile, $data): void {
 | |
|     file::writer($datafile)->serialize($data);
 | |
|   }
 | |
| 
 | |
|   protected function unlinkDatafile(string $datafilename): void {
 | |
|     $datafile = path::join($this->basedir, $datafilename);
 | |
|     @unlink($datafile);
 | |
|     unset($this->datafilenames[$datafilename]);
 | |
|   }
 | |
| 
 | |
|   protected function unlinkFiles(bool $datafilesOnly=false): void {
 | |
|     if (!$datafilesOnly) @unlink($this->file);
 | |
|     foreach ($this->datafilenames as $datafilename) {
 | |
|       $this->unlinkDatafile($datafilename);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /** tester si $value peut être mis en cache */
 | |
|   protected function shouldCache($value): bool {
 | |
|     return $this->cacheNull || $value !== null;
 | |
|   }
 | |
| 
 | |
|   protected ?DateTime $ostart;
 | |
| 
 | |
|   protected ?Delay $oduration;
 | |
| 
 | |
|   protected ?array $odatafilenames;
 | |
| 
 | |
|   protected function beforeAction() {
 | |
|     $this->loadMetadata();
 | |
|     $this->ostart = cv::clone($this->start);
 | |
|     $this->oduration = cv::clone($this->duration);
 | |
|     $this->odatafilenames = cv::clone($this->datafilenames);
 | |
|   }
 | |
| 
 | |
|   protected function afterAction() {
 | |
|     $start = $this->start;
 | |
|     $duration = $this->duration;
 | |
|     $oduration = $this->oduration;
 | |
|     $datafilenames = $this->datafilenames;
 | |
|     $modified = false;
 | |
|     if ($start != $this->ostart) $modified = true;
 | |
|     if ($duration === null || $oduration === null) $modified = true;
 | |
|     elseif ($duration->getDest() != $oduration->getDest()) $modified = true;
 | |
|     # égalité stricte uniquement pour $datafiles qui est un array
 | |
|     if ($datafilenames !== $this->odatafilenames) $modified = true;
 | |
|     if ($modified && !$this->readonly) {
 | |
|       $this->lockWrite();
 | |
|       $this->saveMetadata();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   protected function action(callable $callback, bool $willWrite=false) {
 | |
|     if ($willWrite && !$this->readonly) $this->lockWrite();
 | |
|     else $this->lockRead();
 | |
|     try {
 | |
|       $this->beforeAction();
 | |
|       $result = $callback();
 | |
|       $this->afterAction();
 | |
|       return $result;
 | |
|     } finally {
 | |
|       $this->start = null;
 | |
|       $this->duration = null;
 | |
|       $this->datafilenames = null;
 | |
|       $this->unlock(true);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   protected function compute() {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   function get($data=null, bool $noCache=false) {
 | |
|     return $this->action(function () use ($data, $noCache) {
 | |
|       $data = $this->getData($data);
 | |
|       $dataname = $data->getName();
 | |
|       $datafilename = ".{$this->basename}.{$dataname}".self::EXT;
 | |
|       $datafile = path::join($this->basedir, $datafilename);
 | |
| 
 | |
|       $updateMetadata = $this->shouldUpdate($noCache);
 | |
|       $updateData = !array_key_exists($datafilename, $this->datafilenames);
 | |
|       if (!$this->readonly && ($updateMetadata || $updateData)) {
 | |
|         $this->lockWrite();
 | |
|         if ($updateMetadata) {
 | |
|           # il faut refaire tout le cache
 | |
|           $this->unlinkFiles(true);
 | |
|           $updateData = true;
 | |
|         }
 | |
|         if ($updateData) {
 | |
|           # calculer un fichier
 | |
|           try {
 | |
|             $data = $data->get();
 | |
|           } catch (Exception $e) {
 | |
|             # ne pas garder le fichier en cas d'exception
 | |
|             $this->unlinkDatafile($datafile);
 | |
|             throw $e;
 | |
|           }
 | |
|         } elseif (file_exists($datafile)) {
 | |
|           $data = $this->loadData($datafile);
 | |
|         } else {
 | |
|           $data = null;
 | |
|         }
 | |
|         if ($this->shouldCache($data)) {
 | |
|           $this->saveData($datafile, $data);
 | |
|           $this->datafilenames[$datafilename] = true;
 | |
|         } else {
 | |
|           # ne pas garder le fichier s'il ne faut pas mettre en cache
 | |
|           $this->unlinkDatafile($datafile);
 | |
|         }
 | |
|       } elseif (file_exists($datafile)) {
 | |
|         $data = $this->loadData($datafile);
 | |
|       } else {
 | |
|         $data = null;
 | |
|       }
 | |
|       return $data;
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /** obtenir les informations sur le fichier */
 | |
|   function getInfos(): array {
 | |
|     return $this->action(function () {
 | |
|       if (!$this->isValid()) {
 | |
|         return ["valid" => false];
 | |
|       }
 | |
|       $start = $this->start;
 | |
|       $duration = $this->duration;
 | |
|       $datafilenames = $this->datafilenames;
 | |
|       $datafiles = [];
 | |
|       foreach (array_keys($datafilenames) as $datafilename) {
 | |
|         $datafile = path::join($this->basedir, $datafilename);
 | |
|         $name = $datafilename;
 | |
|         str::del_prefix($name, ".{$this->basename}.");
 | |
|         str::del_suffix($name, self::EXT);
 | |
|         if (file_exists($datafile)) {
 | |
|           $size = filesize($datafile);
 | |
|         } else {
 | |
|           $size = null;
 | |
|         }
 | |
|         $datafiles[$name] = $size;
 | |
|       }
 | |
|       return [
 | |
|         "valid" => true,
 | |
|         "start" => $start,
 | |
|         "duration" => strval($duration),
 | |
|         "date_start" => $start->format(),
 | |
|         "date_end" => $duration->getDest()->format(),
 | |
|         "datafiles" => $datafiles,
 | |
|       ];
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   const UPDATE_SUB = -1, UPDATE_SET = 0, UPDATE_ADD = 1;
 | |
| 
 | |
|   /** mettre à jour la durée de validité du fichier */
 | |
|   function updateDuration($nduration, int $action=1): void {
 | |
|     if ($this->readonly) return;
 | |
|     $this->action(function () use ($nduration, $action) {
 | |
|       if (!$this->isValid()) return;
 | |
|       $duration = $this->duration;
 | |
|       if ($action < 0) $duration->subDuration($nduration);
 | |
|       elseif ($action > 0) $duration->addDuration($nduration);
 | |
|     }, true);
 | |
|   }
 | |
| 
 | |
|   /** supprimer les fichiers s'ils ont expiré */
 | |
|   function deleteExpired(bool $force=false): bool {
 | |
|     if ($this->readonly) return false;
 | |
|     return $this->action(function () use ($force) {
 | |
|       if ($force || $this->shouldUpdate()) {
 | |
|         $this->unlinkFiles();
 | |
|         return true;
 | |
|       }
 | |
|       return false;
 | |
|     }, true);
 | |
|   }
 | |
| }
 |