From 3ea51e70b453c1e4e112b3255526efad10c9bad9 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Fri, 23 May 2025 16:52:26 +0400 Subject: [PATCH] gestion des fichiers de cache --- src/file/cache/CacheData.php | 4 +- src/file/cache/CacheFile.php | 334 +++++++++++++++++++++++------------ src/tools/NucacheApp.php | 23 ++- tbin/.gitignore | 1 + tbin/test-nucache.php | 62 +++++++ 5 files changed, 305 insertions(+), 119 deletions(-) create mode 100755 tbin/test-nucache.php diff --git a/src/file/cache/CacheData.php b/src/file/cache/CacheData.php index a680f9e..f81186e 100644 --- a/src/file/cache/CacheData.php +++ b/src/file/cache/CacheData.php @@ -12,8 +12,8 @@ class CacheData { /** @var callable une fonction permettant de calculer la donnée */ const COMPUTE = null; - function __construct(?string $name=null, $compute=null) { - $this->name = $name ?? static::NAME ?? bin2hex(random_bytes(8)); + function __construct($compute=null, ?string $name=null) { + $this->name = $name ?? static::NAME ?? ""; $this->compute = func::withn($compute ?? static::COMPUTE); } diff --git a/src/file/cache/CacheFile.php b/src/file/cache/CacheFile.php index 0c9e810..1a47182 100644 --- a/src/file/cache/CacheFile.php +++ b/src/file/cache/CacheFile.php @@ -2,29 +2,65 @@ 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, ".metadata.cache", ".cache"); + $file = path::ensure_ext($file, self::EXT); $this->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->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); } @@ -34,12 +70,33 @@ class CacheFile extends SharedFile { /** @var string nom de base des fichiers de cache */ protected string $basename; - protected Delay $duration; + 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. * @@ -50,31 +107,42 @@ class CacheFile extends SharedFile { 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(): ?array { - $this->rewind(); - [ - "start" => $start, - "duration" => $duration, - "datafiles" => $datafiles, - ] = $this->unserialize(null, false, true); - if ($this->overrideDuration) { - $duration = Delay::with($this->duration, $start); + 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 = []; } - return [ - "start" => $start, - "duration" => $duration, - "datafiles" => $datafiles, - ]; + $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 */ + /** + * 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->loadMetadata(); - $expired = $duration->isElapsed(); + $expired = $this->duration->isElapsed(); } else { $expired = false; $noCache = true; @@ -83,49 +151,39 @@ class CacheFile extends SharedFile { } /** sauvegarder les données. le fichier a déjà été verrouillé en écriture */ - protected function saveMetadata(?DateTime $start, ?Delay $duration, array $datafiles): void { - $duration ??= $this->duration; - if ($start === null) { - $start = new DateTime(); - $duration = Delay::with($duration, $start); + 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" => $start, - "duration" => $duration, - "datafiles" => $datafiles, + "start" => $this->start, + "duration" => $this->duration, + "datafiles" => $datafilenames, ], false, true); } - function loadData(string $datafile) { - $datafile = path::join($this->basedir, $datafile); + protected function loadData(string $datafile) { return file::reader($datafile)->unserialize(); } - function saveData(string $datafile, $data): void { - $datafile = path::join($this->basedir, $datafile); + protected function saveData(string $datafile, $data): void { file::writer($datafile)->serialize($data); } - protected function unlinkDatafile(string $datafile): void { - @unlink(path::join($this->basedir, $datafile)); + protected function unlinkDatafile(string $datafilename): void { + $datafile = path::join($this->basedir, $datafilename); + @unlink($datafile); + unset($this->datafilenames[$datafilename]); } - protected function unlinkFiles(?array $datafiles): void { - if ($datafiles === null && $this->isValid()) { - $this->lockRead(); - try { - ["datafiles" => $datafiles - ] = $this->loadMetadata(); - } finally { - $this->unlock(); - } - } - @unlink($this->file); - if ($datafiles !== null) { - foreach ($datafiles as $datafile) { - $this->unlinkDatafile($datafile); - } + protected function unlinkFiles(bool $datafilesOnly=false): void { + if (!$datafilesOnly) @unlink($this->file); + foreach ($this->datafilenames as $datafilename) { + $this->unlinkDatafile($datafilename); } } @@ -134,101 +192,157 @@ class CacheFile extends SharedFile { return $this->cacheNull || $value !== null; } - function get(?CacheData $data=null, bool $noCache=false) { - $this->lockRead(); + 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 { - $datafile = $data->getName(); - $datafile = "{$this->basename}.data.{$datafile}.cache"; - #XXX mettre à jour datafiles dans metadata - if ($this->shouldUpdate($noCache)) { + $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(); - try { - $data = $data->get(); - if ($this->shouldCache($data)) { - $this->saveData($datafile, $data); - } else { - # ne pas garder le fichier s'il ne faut pas mettre en cache - $this->unlinkDatafile($datafile); - } - } catch (Exception $e) { - $this->unlinkDatafile($datafile); - throw $e; + if ($updateMetadata) { + # il faut refaire tout le cache + $this->unlinkFiles(true); + $updateData = true; } - } else { + 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; - } finally { - $this->unlock(); - } + }); } /** obtenir les informations sur le fichier */ function getInfos(): array { - $this->lockRead(); - try { + return $this->action(function () { if (!$this->isValid()) { return ["valid" => false]; } - /** - * @var DateTime $start - * @var Delay $duration - */ - [ - "start" => $start, - "duration" => $duration, - "datafiles" => $datafiles, - ] = $this->loadMetadata(); + $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, - "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->readonly) return; + $this->action(function () use ($nduration, $action) { if (!$this->isValid()) return; - $this->lockWrite(); - /** - * @var DateTime $tstart - * @var Delay $duration - */ - [ - "start" => $start, - "duration" => $duration, - ] = $this->loadMetadata(); + $duration = $this->duration; if ($action < 0) $duration->subDuration($nduration); elseif ($action > 0) $duration->addDuration($nduration); - $this->saveMetadata($start, $duration); - } finally { - $this->unlock(); - } + }, true); } /** supprimer les fichiers s'ils ont expiré */ function deleteExpired(bool $force=false): bool { - $this->lockRead(); - try { + if ($this->readonly) return false; + return $this->action(function () use ($force) { if ($force || $this->shouldUpdate()) { - $this->lockWrite(); $this->unlinkFiles(); return true; } - } finally { - $this->unlock(); - } - return false; + return false; + }, true); } } diff --git a/src/tools/NucacheApp.php b/src/tools/NucacheApp.php index 9fc01d4..4c6687b 100644 --- a/src/tools/NucacheApp.php +++ b/src/tools/NucacheApp.php @@ -47,9 +47,15 @@ class NucacheApp extends Application { 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; + 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; } @@ -72,7 +78,7 @@ class NucacheApp extends Application { } elseif (is_file($arg)) { $files[] = $arg; } else { - msg::warning("$arg: fichier invalide ou introuvable"); + msg::warning("$arg: fichier introuvable"); } } $showSection = count($files) > 1; @@ -81,14 +87,17 @@ class NucacheApp extends Application { case self::ACTION_READ: if ($showSection) msg::section($file); $cache = new CacheFile($file, [ + "readonly" => true, "duration" => "INF", "override_duration" => true, ]); - //yaml::dump($cache->get()); + yaml::dump($cache->get()); break; case self::ACTION_INFOS: if ($showSection) msg::section($file); - $cache = new CacheFile($file); + $cache = new CacheFile($file, [ + "readonly" => true, + ]); yaml::dump($cache->getInfos()); break; case self::ACTION_CLEAN: @@ -96,7 +105,7 @@ class NucacheApp extends Application { $cache = new CacheFile($file); try { if ($cache->deleteExpired()) msg::asuccess("fichier supprimé"); - else msg::astep("fichier non expiré"); + else msg::adone("fichier non expiré"); } catch (Exception $e) { msg::afailure($e); } diff --git a/tbin/.gitignore b/tbin/.gitignore index 74dd8df..f9f3cae 100644 --- a/tbin/.gitignore +++ b/tbin/.gitignore @@ -1,2 +1,3 @@ /output-forever.log /devel/ +/*.cache diff --git a/tbin/test-nucache.php b/tbin/test-nucache.php new file mode 100755 index 0000000..ddaf641 --- /dev/null +++ b/tbin/test-nucache.php @@ -0,0 +1,62 @@ +#!/usr/bin/php +get()); +} + +if (in_array("one", $what)) { + $one = new class("one") extends CacheFile { + protected function compute() { + return 1; + } + }; + Txx("one=", $one->get()); +} + +if (in_array("two", $what)) { + $two = new CacheFile("two", [ + "data" => new CacheData(function () { + return 2; + }), + ]); + Txx("two=", $two->get()); +} + +if (in_array("three", $what)) { + $data31 = new CacheData(function () { + return 31; + }, "data31name"); + + $data32 = new CacheData(function () { + return 32; + }); + + $three = new CacheFile("three", [ + "data" => [ + "data31" => $data31, + $data31, # name=data31name + "data32" => $data32, + $data32, # name="" + ], + ]); + Txx("three.0=", $three->get("data31")); + Txx("three.1=", $three->get("data31name")); + Txx("three.2=", $three->get("data32")); + Txx("three.3=", $three->get("")); +} \ No newline at end of file