artabro/wire/core/CacheFile.php

312 lines
7.8 KiB
PHP
Raw Normal View History

2024-08-27 11:35:37 +02:00
<?php namespace ProcessWire;
/**
* ProcessWire CacheFile
*
* Class to manage individual cache files
*
* Each cache file creates it's own directory based on the '$id' given.
* The dir is created so that secondary cache files can be created too,
* and these are automatically removed when the remove() method is called.
*
* This file is licensed under the MIT license
* https://processwire.com/about/license/mit/
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* https://processwire.com
*
*/
class CacheFile extends Wire {
const cacheFileExtension = ".cache";
const globalExpireFilename = "lastgood";
/**
* Max secondaryID cache files that will be allowed in a directory before it starts removing them.
*
*/
const maxCacheFiles = 999;
protected $path;
protected $primaryID = '';
protected $secondaryID = '';
protected $cacheTimeSeconds = 0;
protected $globalExpireFile = '';
protected $globalExpireTime = 0;
protected $chmodFile = "0666";
protected $chmodDir = "0777";
/**
* Construct the CacheFile
*
* @param string $path Path where cache files will be created
* @param string|int $id An identifier for this particular cache, unique from others.
* Or leave blank if you are instantiating this class for no purpose except to expire cache files (optional).
* @param int $cacheTimeSeconds The number of seconds that this cache file remains valid
* @throws WireException
*
*/
public function __construct($path, $id, $cacheTimeSeconds) {
parent::__construct();
$this->useFuel(false);
$path = rtrim($path, '/') . '/';
$this->globalExpireFile = $path . self::globalExpireFilename;
$this->path = $id ? $path . $id . '/' : $path;
if(!is_dir($path)) {
if(!wire()->files->mkdir($path, true)) throw new WireException("Unable to create path: $path");
}
if(!is_dir($this->path)) {
if(!wire()->files->mkdir($this->path)) throw new WireException("Unable to create path: $this->path");
}
if(is_file($this->globalExpireFile)) {
$this->globalExpireTime = @filemtime($this->globalExpireFile);
}
$this->primaryID = $id ? $id : 'primaryID';
$this->cacheTimeSeconds = (int) $cacheTimeSeconds;
}
/**
* An extra part to be appended to the filename
*
* When a call to remove the cache is included, then all 'secondary' versions of it will be included too
*
* @param string|int $id
*
*/
public function setSecondaryID($id) {
if(!ctype_alnum("$id")) {
$id = preg_replace('/[^-+_a-zA-Z0-9]/', '_', $id);
}
$this->secondaryID = $id;
}
/**
* Build a filename for use by the cache
*
* Filename takes this form: /path/primaryID/primaryID.cache
* Or /path/primaryID/secondaryID.cache
*
* @return string
*
*/
protected function buildFilename() {
$filename = $this->path;
if($this->secondaryID) {
$filename .= $this->secondaryID;
} else {
$filename .= $this->primaryID;
}
$filename .= self::cacheFileExtension;
return $filename;
}
/**
* Does the cache file exist?
*
* @return bool
*
*/
public function exists() {
if(!$this->secondaryID) return is_dir($this->path);
$filename = $this->buildFilename();
return is_file($filename);
}
/**
* Get the contents of the cache based on the primary or secondary ID
*
* @return string
*
*/
public function get() {
$filename = $this->buildFilename();
if(self::isCacheFile($filename) && $this->isCacheFileExpired($filename)) {
$this->removeFilename($filename);
return false;
}
// note file_get_contents returns boolean false if file can't be opened (i.e. if it's locked from a write)
return @file_get_contents($filename);
}
/**
* Is the given cache filename expired?
*
* @param string $filename
* @return bool
*
*/
protected function isCacheFileExpired($filename) {
if(!$mtime = @filemtime($filename)) return false;
if(($mtime + $this->cacheTimeSeconds < time()) || ($this->globalExpireTime && $mtime < $this->globalExpireTime)) {
return true;
}
return false;
}
/**
* Is the given filename a cache file?
*
* @param string $filename
* @return bool
*
*/
static protected function isCacheFile($filename) {
$ext = self::cacheFileExtension;
if(is_file($filename) && substr($filename, -1 * strlen($ext)) == $ext) return true;
return false;
}
/**
* Saves $data to the cache
*
* @param string $data
* @return bool
*
*/
public function save($data) {
$filename = $this->buildFilename();
if(!is_file($filename)) {
$dirname = dirname($filename);
if(is_dir($dirname)) {
$files = glob("$dirname/*.*");
$numFiles = count($files);
if($numFiles >= self::maxCacheFiles) {
// if the cache file doesn't already exist, and there are too many files here
// then abort the cache save for security (we don't want to fill up the disk)
// also go through and remove any expired files while we are here, to avoid
// this limit interrupting more cache saves.
foreach($files as $file) {
if(self::isCacheFile($file) && $this->isCacheFileExpired($file))
$this->removeFilename($file);
}
return false;
}
} else {
$this->wire()->files->mkdir("$dirname/", true);
}
}
$result = file_put_contents($filename, $data);
$this->wire()->files->chmod($filename);
return $result;
}
/**
* Removes all cache files for primaryID
*
* If any secondaryIDs were used, those are removed too
*
*/
public function remove() {
$dir = new \DirectoryIterator($this->path);
foreach($dir as $file) {
if($file->isDir() || $file->isDot()) continue;
//if(strpos($file->getFilename(), self::cacheFileExtension)) @unlink($file->getPathname());
if(self::isCacheFile($file->getPathname())) $this->wire()->files->unlink($file->getPathname());
}
return @rmdir($this->path);
}
/**
* Removes just the given file, as opposed to remove() which removes the entire cache for primaryID
*
* @param string $filename
*
*/
protected function removeFilename($filename) {
$this->wire()->files->unlink($filename);
}
/**
* Remove all cache files in the given path, recursively
*
* @param string $path Full path to the directory you want to clear out
* @param bool $rmdir Set to true if you want to also remove the directory
* @return int Number of files/dirs removed
*
*/
static public function removeAll($path, $rmdir = false) {
$dir = new \DirectoryIterator($path);
$numRemoved = 0;
$files = wire()->files;
foreach($dir as $file) {
if($file->isDot()) continue;
$pathname = $file->getPathname();
if($file->isDir()) {
$numRemoved += self::removeAll($pathname, true);
} else if($file->isFile() && (self::isCacheFile($pathname) || ($file->getFilename() == self::globalExpireFilename))) {
if($files->unlink($pathname)) $numRemoved++;
}
}
if($rmdir && rmdir($path)) $numRemoved++;
return $numRemoved;
}
/**
* Causes all cache files in this type to be immediately expired
*
* Note it does not remove any files, only places a globalExpireFile with an mtime newer than the cache files
*
*/
public function expireAll() {
$note =
"The modification time of this file represents the time of the last usable cache file. " .
"Cache files older than this file are considered expired. " . date('m/d/y H:i:s');
@file_put_contents($this->globalExpireFile, $note, LOCK_EX);
}
/**
* Set the octal mode for files created by CacheFile
*
* @param string $mode
* @deprecated No longer used
*
*/
public function setChmodFile($mode) {
$this->chmodFile = $mode;
}
/**
* Set the octal mode for dirs created by CacheFile
*
* @param string $mode
* @deprecated No longer used
*
*/
public function setChmodDir($mode) {
$this->chmodDir = $mode;
}
/**
* CacheFile classes return a string of their cache filename
*
*/
public function __toString() {
return $this->buildFilename();
}
}