. */ namespace Doctrine\Common\Cache; /** * Base file cache driver. * * @since 2.3 * @author Fabio B. Silva */ abstract class FileCache extends CacheProvider { /** * The cache directory. * * @var string */ protected $directory; /** * The cache file extension. * * @var string */ private $extension; /** * @var string[] regular expressions for replacing disallowed characters in file name */ private $disallowedCharacterPatterns = array( '/\-/', // replaced to disambiguate original `-` and `-` derived from replacements '/[^a-zA-Z0-9\-_\[\]]/' // also excludes non-ascii chars (not supported, depending on FS) ); /** * @var string[] replacements for disallowed file characters */ private $replacementCharacters = array('__', '-'); /** * @var int */ private $umask; /** * Constructor. * * @param string $directory The cache directory. * @param string $extension The cache file extension. * * @throws \InvalidArgumentException */ public function __construct($directory, $extension = '', $umask = 0002) { // YES, this needs to be *before* createPathIfNeeded() if ( ! is_int($umask)) { throw new \InvalidArgumentException(sprintf( 'The umask parameter is required to be integer, was: %s', gettype($umask) )); } $this->umask = $umask; if ( ! $this->createPathIfNeeded($directory)) { throw new \InvalidArgumentException(sprintf( 'The directory "%s" does not exist and could not be created.', $directory )); } if ( ! is_writable($directory)) { throw new \InvalidArgumentException(sprintf( 'The directory "%s" is not writable.', $directory )); } // YES, this needs to be *after* createPathIfNeeded() $this->directory = realpath($directory); $this->extension = (string) $extension; } /** * Gets the cache directory. * * @return string */ public function getDirectory() { return $this->directory; } /** * Gets the cache file extension. * * @return string|null */ public function getExtension() { return $this->extension; } /** * @param string $id * * @return string */ protected function getFilename($id) { return $this->directory . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, str_split(hash('sha256', $id), 2)) . DIRECTORY_SEPARATOR . preg_replace($this->disallowedCharacterPatterns, $this->replacementCharacters, $id) . $this->extension; } /** * {@inheritdoc} */ protected function doDelete($id) { $filename = $this->getFilename($id); return @unlink($filename) || ! file_exists($filename); } /** * {@inheritdoc} */ protected function doFlush() { foreach ($this->getIterator() as $name => $file) { if ($file->isDir()) { // Remove the intermediate directories which have been created to balance the tree. It only takes effect // if the directory is empty. If several caches share the same directory but with different file extensions, // the other ones are not removed. @rmdir($name); } elseif ($this->isFilenameEndingWithExtension($name)) { // If an extension is set, only remove files which end with the given extension. // If no extension is set, we have no other choice than removing everything. @unlink($name); } } return true; } /** * {@inheritdoc} */ protected function doGetStats() { $usage = 0; foreach ($this->getIterator() as $name => $file) { if (! $file->isDir() && $this->isFilenameEndingWithExtension($name)) { $usage += $file->getSize(); } } $free = disk_free_space($this->directory); return array( Cache::STATS_HITS => null, Cache::STATS_MISSES => null, Cache::STATS_UPTIME => null, Cache::STATS_MEMORY_USAGE => $usage, Cache::STATS_MEMORY_AVAILABLE => $free, ); } /** * Create path if needed. * * @param string $path * @return bool TRUE on success or if path already exists, FALSE if path cannot be created. */ private function createPathIfNeeded($path) { if ( ! is_dir($path)) { if (false === @mkdir($path, 0777 & (~$this->umask), true) && !is_dir($path)) { return false; } } return true; } /** * Writes a string content to file in an atomic way. * * @param string $filename Path to the file where to write the data. * @param string $content The content to write * * @return bool TRUE on success, FALSE if path cannot be created, if path is not writable or an any other error. */ protected function writeFile($filename, $content) { $filepath = pathinfo($filename, PATHINFO_DIRNAME); if ( ! $this->createPathIfNeeded($filepath)) { return false; } if ( ! is_writable($filepath)) { return false; } $tmpFile = tempnam($filepath, 'swap'); @chmod($tmpFile, 0666 & (~$this->umask)); if (file_put_contents($tmpFile, $content) !== false) { if (@rename($tmpFile, $filename)) { return true; } @unlink($tmpFile); } return false; } /** * @return \Iterator */ private function getIterator() { return new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($this->directory, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST ); } /** * @param string $name The filename * * @return bool */ private function isFilenameEndingWithExtension($name) { return '' === $this->extension || strrpos($name, $this->extension) === (strlen($name) - strlen($this->extension)); } }