gestion des fichiers de cache

This commit is contained in:
Jephté Clain 2025-05-23 16:52:26 +04:00
parent c80d99e829
commit 3ea51e70b4
5 changed files with 305 additions and 119 deletions

View File

@ -12,8 +12,8 @@ class CacheData {
/** @var callable une fonction permettant de calculer la donnée */ /** @var callable une fonction permettant de calculer la donnée */
const COMPUTE = null; const COMPUTE = null;
function __construct(?string $name=null, $compute=null) { function __construct($compute=null, ?string $name=null) {
$this->name = $name ?? static::NAME ?? bin2hex(random_bytes(8)); $this->name = $name ?? static::NAME ?? "";
$this->compute = func::withn($compute ?? static::COMPUTE); $this->compute = func::withn($compute ?? static::COMPUTE);
} }

View File

@ -2,29 +2,65 @@
namespace nulib\file\cache; namespace nulib\file\cache;
use Exception; use Exception;
use nulib\cl;
use nulib\cv;
use nulib\file; use nulib\file;
use nulib\file\SharedFile; use nulib\file\SharedFile;
use nulib\os\path; use nulib\os\path;
use nulib\php\time\DateTime; use nulib\php\time\DateTime;
use nulib\php\time\Delay; use nulib\php\time\Delay;
use nulib\str; use nulib\str;
use nulib\ValueException;
class CacheFile extends SharedFile { class CacheFile extends SharedFile {
/** @var string|int durée de vie par défaut des données mises en cache */ /** @var string|int durée de vie par défaut des données mises en cache */
const DURATION = "1D"; // jusqu'au lendemain const DURATION = "1D"; // jusqu'au lendemain
const EXT = ".cache";
function __construct($file, ?array $params=null) { function __construct($file, ?array $params=null) {
if ($file === null) { if ($file === null) {
$rand = bin2hex(random_bytes(8)); $rand = bin2hex(random_bytes(8));
$file = sys_get_temp_dir()."/$rand"; $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); $this->basedir = path::dirname($file);
$basename = path::filename($file); $basename = path::filename($file);
$this->basename = str::without_suffix(path::ext($basename), $basename); $this->basename = str::without_suffix(self::EXT, $basename);
$this->duration = Delay::with($params["duration"] ?? static::DURATION); $this->initialDuration = Delay::with($params["duration"] ?? static::DURATION);
$this->overrideDuration = $params["override_duration"] ?? false; $this->overrideDuration = $params["override_duration"] ?? false;
$this->readonly = $params["readonly"] ?? false;
$this->cacheNull = $params["cache_null"] ?? 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); parent::__construct($file);
} }
@ -34,12 +70,33 @@ class CacheFile extends SharedFile {
/** @var string nom de base des fichiers de cache */ /** @var string nom de base des fichiers de cache */
protected string $basename; protected string $basename;
protected Delay $duration; protected Delay $initialDuration;
protected bool $overrideDuration; protected bool $overrideDuration;
protected bool $readonly;
protected bool $cacheNull; 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. * 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; 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 */ /** charger les données. le fichier a déjà été verrouillé en lecture */
protected function loadMetadata(): ?array { protected function loadMetadata(): void {
$this->rewind(); if ($this->isValid()) {
[ $this->rewind();
"start" => $start, [
"duration" => $duration, "start" => $start,
"datafiles" => $datafiles, "duration" => $duration,
] = $this->unserialize(null, false, true); "datafiles" => $datafilenames,
if ($this->overrideDuration) { ] = $this->unserialize(null, false, true);
$duration = Delay::with($this->duration, $start); if ($this->overrideDuration) {
$duration = Delay::with($this->initialDuration, $start);
}
$datafilenames = array_fill_keys($datafilenames, true);
} else {
$start = null;
$duration = null;
$datafilenames = [];
} }
return [ $this->start = $start;
"start" => $start, $this->duration = $duration;
"duration" => $duration, $this->datafilenames = $datafilenames;
"datafiles" => $datafiles,
];
} }
/** 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 { protected function shouldUpdate(bool $noCache=false): bool {
if ($this->isValid()) { if ($this->isValid()) {
/** @var Delay $duration */ $expired = $this->duration->isElapsed();
["duration" => $duration,
] = $this->loadMetadata();
$expired = $duration->isElapsed();
} else { } else {
$expired = false; $expired = false;
$noCache = true; $noCache = true;
@ -83,49 +151,39 @@ class CacheFile extends SharedFile {
} }
/** sauvegarder les données. le fichier a déjà été verrouillé en écriture */ /** sauvegarder les données. le fichier a déjà été verrouillé en écriture */
protected function saveMetadata(?DateTime $start, ?Delay $duration, array $datafiles): void { protected function saveMetadata(): void {
$duration ??= $this->duration; $this->duration ??= $this->initialDuration;
if ($start === null) { if ($this->start === null) {
$start = new DateTime(); $this->start = new DateTime();
$duration = Delay::with($duration, $start); $this->duration = Delay::with($this->duration, $this->start);
} }
$datafilenames = array_keys($this->datafilenames);
$this->ftruncate(); $this->ftruncate();
$this->serialize([ $this->serialize([
"start" => $start, "start" => $this->start,
"duration" => $duration, "duration" => $this->duration,
"datafiles" => $datafiles, "datafiles" => $datafilenames,
], false, true); ], false, true);
} }
function loadData(string $datafile) { protected function loadData(string $datafile) {
$datafile = path::join($this->basedir, $datafile);
return file::reader($datafile)->unserialize(); return file::reader($datafile)->unserialize();
} }
function saveData(string $datafile, $data): void { protected function saveData(string $datafile, $data): void {
$datafile = path::join($this->basedir, $datafile);
file::writer($datafile)->serialize($data); file::writer($datafile)->serialize($data);
} }
protected function unlinkDatafile(string $datafile): void { protected function unlinkDatafile(string $datafilename): void {
@unlink(path::join($this->basedir, $datafile)); $datafile = path::join($this->basedir, $datafilename);
@unlink($datafile);
unset($this->datafilenames[$datafilename]);
} }
protected function unlinkFiles(?array $datafiles): void { protected function unlinkFiles(bool $datafilesOnly=false): void {
if ($datafiles === null && $this->isValid()) { if (!$datafilesOnly) @unlink($this->file);
$this->lockRead(); foreach ($this->datafilenames as $datafilename) {
try { $this->unlinkDatafile($datafilename);
["datafiles" => $datafiles
] = $this->loadMetadata();
} finally {
$this->unlock();
}
}
@unlink($this->file);
if ($datafiles !== null) {
foreach ($datafiles as $datafile) {
$this->unlinkDatafile($datafile);
}
} }
} }
@ -134,101 +192,157 @@ class CacheFile extends SharedFile {
return $this->cacheNull || $value !== null; return $this->cacheNull || $value !== null;
} }
function get(?CacheData $data=null, bool $noCache=false) { protected ?DateTime $ostart;
$this->lockRead();
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 { try {
$datafile = $data->getName(); $this->beforeAction();
$datafile = "{$this->basename}.data.{$datafile}.cache"; $result = $callback();
#XXX mettre à jour datafiles dans metadata $this->afterAction();
if ($this->shouldUpdate($noCache)) { 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(); $this->lockWrite();
try { if ($updateMetadata) {
$data = $data->get(); # il faut refaire tout le cache
if ($this->shouldCache($data)) { $this->unlinkFiles(true);
$this->saveData($datafile, $data); $updateData = true;
} 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;
} }
} 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); $data = $this->loadData($datafile);
} else {
$data = null;
} }
return $data; return $data;
} finally { });
$this->unlock();
}
} }
/** obtenir les informations sur le fichier */ /** obtenir les informations sur le fichier */
function getInfos(): array { function getInfos(): array {
$this->lockRead(); return $this->action(function () {
try {
if (!$this->isValid()) { if (!$this->isValid()) {
return ["valid" => false]; return ["valid" => false];
} }
/** $start = $this->start;
* @var DateTime $start $duration = $this->duration;
* @var Delay $duration $datafilenames = $this->datafilenames;
*/ $datafiles = [];
[ foreach (array_keys($datafilenames) as $datafilename) {
"start" => $start, $datafile = path::join($this->basedir, $datafilename);
"duration" => $duration, $name = $datafilename;
"datafiles" => $datafiles, str::del_prefix($name, ".{$this->basename}.");
] = $this->loadMetadata(); str::del_suffix($name, self::EXT);
if (file_exists($datafile)) {
$size = filesize($datafile);
} else {
$size = null;
}
$datafiles[$name] = $size;
}
return [ return [
"valid" => true, "valid" => true,
"size" => $this->getSize(),
"start" => $start, "start" => $start,
"duration" => strval($duration), "duration" => strval($duration),
"date_start" => $start->format(), "date_start" => $start->format(),
"date_end" => $duration->getDest()->format(), "date_end" => $duration->getDest()->format(),
"datafiles" => $datafiles, "datafiles" => $datafiles,
]; ];
} finally { });
$this->unlock();
}
} }
const UPDATE_SUB = -1, UPDATE_SET = 0, UPDATE_ADD = 1; const UPDATE_SUB = -1, UPDATE_SET = 0, UPDATE_ADD = 1;
/** mettre à jour la durée de validité du fichier */ /** mettre à jour la durée de validité du fichier */
function updateDuration($nduration, int $action=1): void { function updateDuration($nduration, int $action=1): void {
$this->lockRead(); if ($this->readonly) return;
try { $this->action(function () use ($nduration, $action) {
if (!$this->isValid()) return; if (!$this->isValid()) return;
$this->lockWrite(); $duration = $this->duration;
/**
* @var DateTime $tstart
* @var Delay $duration
*/
[
"start" => $start,
"duration" => $duration,
] = $this->loadMetadata();
if ($action < 0) $duration->subDuration($nduration); if ($action < 0) $duration->subDuration($nduration);
elseif ($action > 0) $duration->addDuration($nduration); elseif ($action > 0) $duration->addDuration($nduration);
$this->saveMetadata($start, $duration); }, true);
} finally {
$this->unlock();
}
} }
/** supprimer les fichiers s'ils ont expiré */ /** supprimer les fichiers s'ils ont expiré */
function deleteExpired(bool $force=false): bool { function deleteExpired(bool $force=false): bool {
$this->lockRead(); if ($this->readonly) return false;
try { return $this->action(function () use ($force) {
if ($force || $this->shouldUpdate()) { if ($force || $this->shouldUpdate()) {
$this->lockWrite();
$this->unlinkFiles(); $this->unlinkFiles();
return true; return true;
} }
} finally { return false;
$this->unlock(); }, true);
}
return false;
} }
} }

View File

@ -47,9 +47,15 @@ class NucacheApp extends Application {
function setActionUpdate(int $action, $updateDuration): void { function setActionUpdate(int $action, $updateDuration): void {
$this->action = self::ACTION_UPDATE; $this->action = self::ACTION_UPDATE;
switch ($action) { switch ($action) {
case self::ACTION_UPDATE_SUB: $this->updateAction = CacheFile::UPDATE_SUB; break; case self::ACTION_UPDATE_SUB:
case self::ACTION_UPDATE_SET: $this->updateAction = CacheFile::UPDATE_SET; break; $this->updateAction = CacheFile::UPDATE_SUB;
case self::ACTION_UPDATE_ADD: $this->updateAction = CacheFile::UPDATE_ADD; break; 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; $this->updateDuration = $updateDuration;
} }
@ -72,7 +78,7 @@ class NucacheApp extends Application {
} elseif (is_file($arg)) { } elseif (is_file($arg)) {
$files[] = $arg; $files[] = $arg;
} else { } else {
msg::warning("$arg: fichier invalide ou introuvable"); msg::warning("$arg: fichier introuvable");
} }
} }
$showSection = count($files) > 1; $showSection = count($files) > 1;
@ -81,14 +87,17 @@ class NucacheApp extends Application {
case self::ACTION_READ: case self::ACTION_READ:
if ($showSection) msg::section($file); if ($showSection) msg::section($file);
$cache = new CacheFile($file, [ $cache = new CacheFile($file, [
"readonly" => true,
"duration" => "INF", "duration" => "INF",
"override_duration" => true, "override_duration" => true,
]); ]);
//yaml::dump($cache->get()); yaml::dump($cache->get());
break; break;
case self::ACTION_INFOS: case self::ACTION_INFOS:
if ($showSection) msg::section($file); if ($showSection) msg::section($file);
$cache = new CacheFile($file); $cache = new CacheFile($file, [
"readonly" => true,
]);
yaml::dump($cache->getInfos()); yaml::dump($cache->getInfos());
break; break;
case self::ACTION_CLEAN: case self::ACTION_CLEAN:
@ -96,7 +105,7 @@ class NucacheApp extends Application {
$cache = new CacheFile($file); $cache = new CacheFile($file);
try { try {
if ($cache->deleteExpired()) msg::asuccess("fichier supprimé"); if ($cache->deleteExpired()) msg::asuccess("fichier supprimé");
else msg::astep("fichier non expiré"); else msg::adone("fichier non expiré");
} catch (Exception $e) { } catch (Exception $e) {
msg::afailure($e); msg::afailure($e);
} }

1
tbin/.gitignore vendored
View File

@ -1,2 +1,3 @@
/output-forever.log /output-forever.log
/devel/ /devel/
/*.cache

62
tbin/test-nucache.php Executable file
View File

@ -0,0 +1,62 @@
#!/usr/bin/php
<?php
require __DIR__.'/../vendor/autoload.php';
use nulib\file\cache\CacheData;
use nulib\file\cache\CacheFile;
use nulib\os\sh;
//system("rm -f *.cache .*.cache");
$what = [
"null",
"one",
"two",
"three",
];
if (in_array("null", $what)) {
$null = new CacheFile("null");
Txx("null=", $null->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(""));
}