%PDF- %PDF-
| Direktori : /www/varak.net/losik.varak.net/vendor/nette/caching/src/Caching/Storages/ |
| Current File : //www/varak.net/losik.varak.net/vendor/nette/caching/src/Caching/Storages/FileStorage.php |
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Caching\Storages;
use Nette;
use Nette\Caching\Cache;
/**
* Cache file storage.
*/
class FileStorage implements Nette\Caching\Storage
{
use Nette\SmartObject;
/**
* Atomic thread safe logic:
*
* 1) reading: open(r+b), lock(SH), read
* - delete?: delete*, close
* 2) deleting: delete*
* 3) writing: open(r+b || wb), lock(EX), truncate*, write data, write meta, close
*
* delete* = try unlink, if fails (on NTFS) { lock(EX), truncate, close, unlink } else close (on ext3)
*/
/** @internal cache file structure: meta-struct size + serialized meta-struct + data */
private const
MetaHeaderLen = 6,
// meta structure: array of
MetaTime = 'time', // timestamp
MetaSerialized = 'serialized', // is content serialized?
MetaExpire = 'expire', // expiration timestamp
MetaDelta = 'delta', // relative (sliding) expiration
MetaItems = 'di', // array of dependent items (file => timestamp)
MetaCallbacks = 'callbacks'; // array of callbacks (function, args)
/** additional cache structure */
private const
File = 'file',
Handle = 'handle';
/** @var float probability that the clean() routine is started */
public static $gcProbability = 0.001;
/** @deprecated */
public static $useDirectories = true;
/** @var string */
private $dir;
/** @var Journal */
private $journal;
/** @var array */
private $locks;
public function __construct(string $dir, ?Journal $journal = null)
{
if (!is_dir($dir)) {
throw new Nette\DirectoryNotFoundException("Directory '$dir' not found.");
}
$this->dir = $dir;
$this->journal = $journal;
if (mt_rand() / mt_getrandmax() < static::$gcProbability) {
$this->clean([]);
}
}
public function read(string $key)
{
$meta = $this->readMetaAndLock($this->getCacheFile($key), LOCK_SH);
return $meta && $this->verify($meta)
? $this->readData($meta) // calls fclose()
: null;
}
/**
* Verifies dependencies.
*/
private function verify(array $meta): bool
{
do {
if (!empty($meta[self::MetaDelta])) {
// meta[file] was added by readMetaAndLock()
if (filemtime($meta[self::File]) + $meta[self::MetaDelta] < time()) {
break;
}
touch($meta[self::File]);
} elseif (!empty($meta[self::MetaExpire]) && $meta[self::MetaExpire] < time()) {
break;
}
if (!empty($meta[self::MetaCallbacks]) && !Cache::checkCallbacks($meta[self::MetaCallbacks])) {
break;
}
if (!empty($meta[self::MetaItems])) {
foreach ($meta[self::MetaItems] as $depFile => $time) {
$m = $this->readMetaAndLock($depFile, LOCK_SH);
if (($m[self::MetaTime] ?? null) !== $time || ($m && !$this->verify($m))) {
break 2;
}
}
}
return true;
} while (false);
$this->delete($meta[self::File], $meta[self::Handle]); // meta[handle] & meta[file] was added by readMetaAndLock()
return false;
}
public function lock(string $key): void
{
$cacheFile = $this->getCacheFile($key);
if (!is_dir($dir = dirname($cacheFile))) {
@mkdir($dir); // @ - directory may already exist
}
$handle = fopen($cacheFile, 'c+b');
if (!$handle) {
return;
}
$this->locks[$key] = $handle;
flock($handle, LOCK_EX);
}
public function write(string $key, $data, array $dp): void
{
$meta = [
self::MetaTime => microtime(),
];
if (isset($dp[Cache::Expire])) {
if (empty($dp[Cache::Sliding])) {
$meta[self::MetaExpire] = $dp[Cache::Expire] + time(); // absolute time
} else {
$meta[self::MetaDelta] = (int) $dp[Cache::Expire]; // sliding time
}
}
if (isset($dp[Cache::Items])) {
foreach ($dp[Cache::Items] as $item) {
$depFile = $this->getCacheFile($item);
$m = $this->readMetaAndLock($depFile, LOCK_SH);
$meta[self::MetaItems][$depFile] = $m[self::MetaTime] ?? null;
unset($m);
}
}
if (isset($dp[Cache::Callbacks])) {
$meta[self::MetaCallbacks] = $dp[Cache::Callbacks];
}
if (!isset($this->locks[$key])) {
$this->lock($key);
if (!isset($this->locks[$key])) {
return;
}
}
$handle = $this->locks[$key];
unset($this->locks[$key]);
$cacheFile = $this->getCacheFile($key);
if (isset($dp[Cache::Tags]) || isset($dp[Cache::Priority])) {
if (!$this->journal) {
throw new Nette\InvalidStateException('CacheJournal has not been provided.');
}
$this->journal->write($cacheFile, $dp);
}
ftruncate($handle, 0);
if (!is_string($data)) {
$data = serialize($data);
$meta[self::MetaSerialized] = true;
}
$head = serialize($meta);
$head = str_pad((string) strlen($head), 6, '0', STR_PAD_LEFT) . $head;
$headLen = strlen($head);
do {
if (fwrite($handle, str_repeat("\x00", $headLen)) !== $headLen) {
break;
}
if (fwrite($handle, $data) !== strlen($data)) {
break;
}
fseek($handle, 0);
if (fwrite($handle, $head) !== $headLen) {
break;
}
flock($handle, LOCK_UN);
fclose($handle);
return;
} while (false);
$this->delete($cacheFile, $handle);
}
public function remove(string $key): void
{
unset($this->locks[$key]);
$this->delete($this->getCacheFile($key));
}
public function clean(array $conditions): void
{
$all = !empty($conditions[Cache::All]);
$collector = empty($conditions);
$namespaces = $conditions[Cache::Namespaces] ?? null;
// cleaning using file iterator
if ($all || $collector) {
$now = time();
foreach (Nette\Utils\Finder::find('_*')->from($this->dir)->childFirst() as $entry) {
$path = (string) $entry;
if ($entry->isDir()) { // collector: remove empty dirs
@rmdir($path); // @ - removing dirs is not necessary
continue;
}
if ($all) {
$this->delete($path);
} else { // collector
$meta = $this->readMetaAndLock($path, LOCK_SH);
if (!$meta) {
continue;
}
if ((!empty($meta[self::MetaDelta]) && filemtime($meta[self::File]) + $meta[self::MetaDelta] < $now)
|| (!empty($meta[self::MetaExpire]) && $meta[self::MetaExpire] < $now)
) {
$this->delete($path, $meta[self::Handle]);
continue;
}
flock($meta[self::Handle], LOCK_UN);
fclose($meta[self::Handle]);
}
}
if ($this->journal) {
$this->journal->clean($conditions);
}
return;
} elseif ($namespaces) {
foreach ($namespaces as $namespace) {
$dir = $this->dir . '/_' . urlencode($namespace);
if (!is_dir($dir)) {
continue;
}
foreach (Nette\Utils\Finder::findFiles('_*')->in($dir) as $entry) {
$this->delete((string) $entry);
}
@rmdir($dir); // may already contain new files
}
}
// cleaning using journal
if ($this->journal) {
foreach ($this->journal->clean($conditions) as $file) {
$this->delete($file);
}
}
}
/**
* Reads cache data from disk.
*/
protected function readMetaAndLock(string $file, int $lock): ?array
{
$handle = @fopen($file, 'r+b'); // @ - file may not exist
if (!$handle) {
return null;
}
flock($handle, $lock);
$size = (int) stream_get_contents($handle, self::MetaHeaderLen);
if ($size) {
$meta = stream_get_contents($handle, $size, self::MetaHeaderLen);
$meta = unserialize($meta);
$meta[self::File] = $file;
$meta[self::Handle] = $handle;
return $meta;
}
flock($handle, LOCK_UN);
fclose($handle);
return null;
}
/**
* Reads cache data from disk and closes cache file handle.
* @return mixed
*/
protected function readData(array $meta)
{
$data = stream_get_contents($meta[self::Handle]);
flock($meta[self::Handle], LOCK_UN);
fclose($meta[self::Handle]);
return empty($meta[self::MetaSerialized]) ? $data : unserialize($data);
}
/**
* Returns file name.
*/
protected function getCacheFile(string $key): string
{
$file = urlencode($key);
if ($a = strrpos($file, '%00')) { // %00 = urlencode(Nette\Caching\Cache::NamespaceSeparator)
$file = substr_replace($file, '/_', $a, 3);
}
return $this->dir . '/_' . $file;
}
/**
* Deletes and closes file.
* @param resource $handle
*/
private static function delete(string $file, $handle = null): void
{
if (@unlink($file)) { // @ - file may not already exist
if ($handle) {
flock($handle, LOCK_UN);
fclose($handle);
}
return;
}
if (!$handle) {
$handle = @fopen($file, 'r+'); // @ - file may not exist
}
if (!$handle) {
return;
}
flock($handle, LOCK_EX);
ftruncate($handle, 0);
flock($handle, LOCK_UN);
fclose($handle);
@unlink($file); // @ - file may not already exist
}
}