Files
huntingclubroster/vendor/tecnickcom/tc-lib-file/src/Cache.php
T
2026-10-18 17:01:36 +00:00

268 lines
8.5 KiB
PHP

<?php
declare(strict_types=1);
/**
* Cache.php
*
* @since 2011-05-23
* @category Library
* @package File
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-file
*
* This file is part of tc-lib-file software library.
*/
namespace Com\Tecnick\File;
/**
* Com\Tecnick\File\Cache
*
* File caching system with per-instance path and prefix.
* Each Cache instance maintains its own cache directory path and file prefix.
*
* @since 2011-05-23
* @category Library
* @package File
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-file
*/
class Cache
{
/**
* Pattern matching characters that are not allowed in cache filename tokens
* (prefix, type, key). Stripping them keeps generated names valid on every
* platform (notably Windows) and prevents glob metacharacters from reaching
* delete()/deleteOlderThan(). getNewFileName() and delete() must use the
* same pattern so the prefix-based glob matches the files actually created.
*/
private const SAFE_NAME_PATTERN = '/[^a-zA-Z0-9_\-]/';
/**
* Cache path (per-instance)
*
* @var string
*/
protected string $path = '';
/**
* File prefix (per-instance)
*/
protected string $prefix;
/**
* Set the file prefix (common name)
*
* @param ?string $prefix Common prefix to be used for all cache files
*/
public function __construct(?string $prefix = null)
{
$this->defineSystemCachePath();
$this->setCachePath();
$prefix ??= \rtrim(
\base64_encode(\pack('H*', \md5(\uniqid((string) \mt_rand(0, \mt_getrandmax()), true)))),
'=',
);
$safePrefix = \preg_replace(self::SAFE_NAME_PATTERN, '', \strtr($prefix, '+/', '-_')) ?? '';
$this->prefix = '_' . $safePrefix . '_';
}
/**
* Get the cache directory path
*/
public function getCachePath(): string
{
return $this->path;
}
/**
* Set the default cache directory path
*
* @param ?string $path Cache directory path; if null use the K_PATH_CACHE value
*/
public function setCachePath(?string $path = null): void
{
if ($path === null || \str_contains($path, '://') || !\is_writable($path)) {
// Normalize the fallback too: K_PATH_CACHE may be supplied by the host
// application without a trailing separator, and getNewFileName()/delete()
// concatenate $this->path directly. Without normalization the generated
// file would escape the cache directory (e.g. ".../cachedir" + "_pfx_..."
// lands next to the directory instead of inside it).
$this->path = $this->normalizePath((string) \constant('K_PATH_CACHE'));
return;
}
$this->path = $this->normalizePath($path);
}
/**
* Get the file prefix
*/
public function getFilePrefix(): string
{
return $this->prefix;
}
/**
* Returns a temporary filename for caching files.
* Throws an exception when tempnam() fails, consistent with the rest of the library.
*
* @param string $type Type of file
* @param string $key File key (used to retrieve file from cache)
*
* @return string Temporary filename
*
* @throws \Com\Tecnick\File\Exception when a temporary file cannot be created.
*/
public function getNewFileName(string $type = 'tmp', string $key = '0'): string
{
// Sanitize so the generated name matches the patterns used by delete()
// and contains no characters that are invalid in Windows filenames.
$safeType = \preg_replace(self::SAFE_NAME_PATTERN, '', $type) ?? '';
$safeKey = \preg_replace(self::SAFE_NAME_PATTERN, '', $key) ?? '';
// tempnam() atomically creates a unique, private file, but on Windows it
// keeps only the first three characters of the prefix. Create the file
// with tempnam(), then rename it to a name carrying the full prefix so
// the prefix-based glob in delete()/deleteOlderThan() works on every
// platform. The rename preserves tempnam()'s restrictive permissions.
$file = \tempnam($this->path, $this->prefix);
if ($file === false) {
throw new Exception('unable to create a temporary file in: ' . $this->path);
}
$unique = \str_replace('.', '', \uniqid('', true));
$target = $this->path . $this->prefix . $safeType . '_' . $safeKey . '_' . $unique;
$renamed = $this->withoutFsWarnings(static fn(): bool => \rename($file, $target));
return $renamed ? $target : $file;
}
/**
* Delete cached files
*
* @param ?string $type Type of files to delete
* @param ?string $key Specific file key to delete
*/
public function delete(?string $type = null, ?string $key = null): void
{
$safeType = $type !== null ? \preg_replace(self::SAFE_NAME_PATTERN, '', $type) : null;
$safeKey = $key !== null ? \preg_replace(self::SAFE_NAME_PATTERN, '', $key) : null;
$path = $this->path . $this->prefix;
if ($safeType !== null) {
$path .= $safeType . '_';
if ($safeKey !== null) {
$path .= $safeKey . '_';
}
}
$path .= '*';
$files = \glob($path);
if ($files === [] || $files === false) {
return;
}
// Suppress warnings: a file may vanish (concurrent cleanup, external
// process) between glob() and unlink(); such races are non-fatal.
$this->withoutFsWarnings(static function () use ($files): void {
foreach ($files as $file) {
\unlink($file);
}
});
}
/**
* Delete cache files older than the given number of seconds.
*
* @param int $seconds Maximum age in seconds; files whose mtime is older are removed.
*/
public function deleteOlderThan(int $seconds): void
{
$pattern = $this->path . $this->prefix . '*';
$files = \glob($pattern);
if ($files === [] || $files === false) {
return;
}
$cutoff = \time() - $seconds;
// Suppress warnings: filemtime()/unlink() may fail if a file disappears
// between glob() and the call (concurrent cleanup); such races are non-fatal.
$this->withoutFsWarnings(static function () use ($files, $cutoff): void {
foreach ($files as $file) {
$mtime = \filemtime($file);
if ($mtime !== false && $mtime < $cutoff) {
\unlink($file);
}
}
});
}
/**
* Execute a callable while suppressing expected filesystem warnings.
*
* The cache helpers race against other processes and against files that may
* vanish between glob() and the operation; those calls already signal failure
* via their return values, so the raw PHP warnings only add noise.
*
* @template T
*
* @param callable():T $callback
*
* @return T
*/
private function withoutFsWarnings(callable $callback): mixed
{
\set_error_handler(static fn(): bool => true, E_WARNING);
try {
return $callback();
} finally {
\restore_error_handler();
}
}
/**
* Set the K_PATH_CACHE constant (if not set) to the default system directory for temporary files
*/
protected function defineSystemCachePath(): void
{
if (\defined('K_PATH_CACHE')) {
return;
}
$kPathCache = \ini_get('upload_tmp_dir');
if ($kPathCache === false || $kPathCache === '') {
$kPathCache = \sys_get_temp_dir();
}
\define('K_PATH_CACHE', $this->normalizePath($kPathCache));
}
/**
* Normalize cache path
*
* @param string $path Path to normalize
*/
protected function normalizePath(string $path): string
{
$rpath = \realpath($path);
if ($rpath === false) {
return '';
}
if (!\str_ends_with($rpath, \DIRECTORY_SEPARATOR)) {
$rpath .= \DIRECTORY_SEPARATOR;
}
return $rpath;
}
}