début implémentation cache

This commit is contained in:
Jephté Clain 2025-05-23 06:48:57 +04:00
parent 4c91327bac
commit ec0c0eef3e
4 changed files with 353 additions and 0 deletions

7
bin/nucache.php Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/php
<?php
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
use nulib\tools\NucacheApp;
NucacheApp::run();

49
src/file/cache/CacheData.php vendored Normal file
View File

@ -0,0 +1,49 @@
<?php
namespace nulib\file\cache;
use nulib\cl;
use nulib\php\func;
use Traversable;
class CacheData {
/** @var string identifiant de cette donnée */
const NAME = null;
/** @var callable une fonction permettant de calculer la donnée */
const COMPUTE = null;
function __construct(?array $params=null) {
$this->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;
}
}

178
src/file/cache/CacheFile.php vendored Normal file
View File

@ -0,0 +1,178 @@
<?php
namespace nulib\file\cache;
use nulib\file\SharedFile;
use nulib\os\path;
use nulib\php\time\DateTime;
use nulib\php\time\Delay;
use nulib\str;
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
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");
$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->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;
}
}

119
src/tools/NucacheApp.php Normal file
View File

@ -0,0 +1,119 @@
<?php
namespace nulib\tools;
use Exception;
use nulib\app\cli\Application;
use nulib\ext\yaml;
use nulib\file\cache\CacheFile;
use nulib\os\path;
use nulib\output\msg;
class NucacheApp extends Application {
const ACTION_READ = 10, ACTION_INFOS = 20, ACTION_CLEAN = 30;
const ACTION_UPDATE = 40, ACTION_UPDATE_ADD = 41, ACTION_UPDATE_SUB = 42, ACTION_UPDATE_SET = 43;
const ARGS = [
"merge" => 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");
}
}
}
}