diff --git a/bin/nucache.php b/bin/nucache.php new file mode 100755 index 0000000..290263c --- /dev/null +++ b/bin/nucache.php @@ -0,0 +1,7 @@ +#!/usr/bin/php +name = $params["name"] ?? static::NAME; + $this->name ??= bin2hex(random_bytes(8)); + $this->compute = func::withn($params["compute"] ?? static::COMPUTE); + } + + protected string $name; + + protected ?func $compute; + + protected function compute() { + $compute = $this->compute; + return $compute !== null? $compute->invoke(): null; + } + + /** obtenir la donnée, en l'itérant au préalable si elle est traversable */ + function get(?string &$name, $compute=null) { + $name = $this->name; + $this->compute ??= func::withn($compute); + $data = $this->compute(); + if ($data instanceof Traversable) { + $data = cl::all($data); + } + return $data; + } + + /** obtenir un itérateur sur la donnée ou null s'il n'y a pas de données */ + function all(?string &$name, $compute=null): ?iterable { + $name = $this->name; + $this->compute ??= func::withn($compute); + $data = $this->compute(); + if ($data !== null && !is_iterable($data)) $data = [$data]; + return $data; + } +} diff --git a/src/file/cache/CacheFile.php b/src/file/cache/CacheFile.php new file mode 100644 index 0000000..07b30cc --- /dev/null +++ b/src/file/cache/CacheFile.php @@ -0,0 +1,178 @@ +basedir = path::dirname($file); + $basename = path::filename($file); + $this->basename = str::without_suffix(path::ext($basename), $basename); + $this->duration = Delay::with($params["duration"] ?? static::DURATION); + $this->overrideDuration = $params["override_duration"] ?? false; + $this->cacheNull = $params["cache_null"] ?? false; + $this->datafiles = []; + 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 $duration; + + protected bool $overrideDuration; + + protected bool $cacheNull; + + protected array $datafiles; + + /** + * 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; + } + + /** charger les données. le fichier a déjà été verrouillé en lecture */ + protected function loadData(): ?array { + $this->rewind(); + [ + "start" => $start, + "duration" => $duration, + "datafiles" => $datafiles, + ] = $this->unserialize(null, false, true); + if ($this->overrideDuration) { + $duration = Delay::with($this->duration, $start); + } + return [ + "start" => $start, + "duration" => $duration, + "datafiles" => $datafiles, + ]; + } + + /** 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()) { + /** @var Delay $duration */ + ["duration" => $duration, + ] = $this->loadData(); + $expired = $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 saveData(?DateTime $start=null, ?Delay $duration=null): void { + $duration ??= $this->duration; + if ($start === null) { + $start = new DateTime(); + $duration = Delay::with($duration, $start); + } + $this->ftruncate(); + $this->serialize([ + "start" => $start, + "duration" => $duration, + "datafiles" => $this->datafiles, + ], false, true); + } + + /** tester si $value peut être mis en cache */ + function shouldCache($value): bool { + return $this->cacheNull || $value !== null; + } + + /** obtenir les informations sur le fichier */ + function getInfos(): array { + $this->lockRead(); + try { + if (!$this->isValid()) { + return ["valid" => false]; + } + /** + * @var DateTime $start + * @var Delay $duration + */ + [ + "start" => $start, + "duration" => $duration, + "datafiles" => $datafiles, + ] = $this->loadData(); + return [ + "valid" => true, + "size" => $this->getSize(), + "start" => $start, + "duration" => strval($duration), + "date_start" => $start->format(), + "date_end" => $duration->getDest()->format(), + "datafiles" => $datafiles, + ]; + } finally { + $this->unlock(); + } + } + + 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 { + $this->lockRead(); + try { + if (!$this->isValid()) return; + $this->lockWrite(); + /** + * @var DateTime $tstart + * @var Delay $duration + */ + [ + "start" => $start, + "duration" => $duration, + ] = $this->loadData(); + if ($action < 0) $duration->subDuration($nduration); + elseif ($action > 0) $duration->addDuration($nduration); + $this->saveData($start, $duration); + } finally { + $this->unlock(); + } + } + + /** supprimer les fichiers s'ils ont expiré */ + function deleteExpired(bool $force=false): bool { + $this->lockRead(); + try { + if ($force || $this->shouldUpdate()) { + $this->lockWrite(); + @unlink($this->file); + $basedir = $this->basedir; + foreach ($this->datafiles as $datafile) { + @unlink(path::join($basedir, $datafile)); + } + return true; + } + } finally { + $this->unlock(); + } + return false; + } +} diff --git a/src/tools/NucacheApp.php b/src/tools/NucacheApp.php new file mode 100644 index 0000000..9fc01d4 --- /dev/null +++ b/src/tools/NucacheApp.php @@ -0,0 +1,119 @@ + parent::ARGS, + "purpose" => "gestion de fichiers cache", + ["-r", "--read", "name" => "action", "value" => self::ACTION_READ, + "help" => "Afficher le contenu d'un fichier cache", + ], + ["-i", "--infos", "name" => "action", "value" => self::ACTION_INFOS, + "help" => "Afficher des informations sur le fichier cache", + ], + ["-k", "--clean", "name" => "action", "value" => self::ACTION_CLEAN, + "help" => "Supprimer le fichier cache s'il a expiré", + ], + ["-a", "--add-duration", "args" => 1, + "action" => [null, "->setActionUpdate", self::ACTION_UPDATE_ADD], + "help" => "Ajouter le nombre de secondes spécifié à la durée du cache", + ], + ["-b", "--sub-duration", "args" => 1, + "action" => [null, "->setActionUpdate", self::ACTION_UPDATE_SUB], + "help" => "Enlever le nombre de secondes spécifié à la durée du cache", + ], + ["-s", "--set-duration", "args" => 1, + "action" => [null, "->setActionUpdate", self::ACTION_UPDATE_SET], + "help" => "Mettre à jour la durée du cache à la valeur spécifiée", + ], + ]; + + protected $action = self::ACTION_READ; + + protected $updateAction, $updateDuration; + + protected $args; + + function setActionUpdate(int $action, $updateDuration): void { + $this->action = self::ACTION_UPDATE; + switch ($action) { + case self::ACTION_UPDATE_SUB: $this->updateAction = CacheFile::UPDATE_SUB; break; + case self::ACTION_UPDATE_SET: $this->updateAction = CacheFile::UPDATE_SET; break; + case self::ACTION_UPDATE_ADD: $this->updateAction = CacheFile::UPDATE_ADD; break; + } + $this->updateDuration = $updateDuration; + } + + protected function findCaches(string $dir, ?array &$files): void { + foreach (glob("$dir/*") as $file) { + if (is_dir($file)) { + $this->findCaches($file, $files); + } elseif (is_file($file) && fnmatch("*.cache", $file)) { + $files[] = $file; + } + } + } + + function main() { + $files = []; + foreach ($this->args as $arg) { + if (is_dir($arg)) { + $this->findCaches($arg, $files); + } elseif (is_file($arg)) { + $files[] = $arg; + } else { + msg::warning("$arg: fichier invalide ou introuvable"); + } + } + $showSection = count($files) > 1; + foreach ($files as $file) { + switch ($this->action) { + case self::ACTION_READ: + if ($showSection) msg::section($file); + $cache = new CacheFile($file, [ + "duration" => "INF", + "override_duration" => true, + ]); + //yaml::dump($cache->get()); + break; + case self::ACTION_INFOS: + if ($showSection) msg::section($file); + $cache = new CacheFile($file); + yaml::dump($cache->getInfos()); + break; + case self::ACTION_CLEAN: + msg::action(path::ppath($file)); + $cache = new CacheFile($file); + try { + if ($cache->deleteExpired()) msg::asuccess("fichier supprimé"); + else msg::astep("fichier non expiré"); + } catch (Exception $e) { + msg::afailure($e); + } + break; + case self::ACTION_UPDATE: + msg::action(path::ppath($file)); + $cache = new CacheFile($file); + try { + $cache->updateDuration($this->updateDuration, $this->updateAction); + msg::asuccess("fichier mis à jour"); + } catch (Exception $e) { + msg::afailure($e); + } + break; + default: + self::die("$this->action: action non implémentée"); + } + } + } +}