2015-08-17 17:00:26 -07:00
< ? php
namespace Doctrine\Common\Cache ;
2018-11-23 12:29:20 +00:00
use const DIRECTORY_SEPARATOR ;
use const PATHINFO_DIRNAME ;
use function bin2hex ;
use function chmod ;
use function defined ;
use function disk_free_space ;
use function file_exists ;
use function file_put_contents ;
use function gettype ;
use function hash ;
use function is_dir ;
use function is_int ;
use function is_writable ;
use function mkdir ;
use function pathinfo ;
use function realpath ;
use function rename ;
use function rmdir ;
use function sprintf ;
use function strlen ;
use function strrpos ;
use function substr ;
use function tempnam ;
use function unlink ;
2015-08-17 17:00:26 -07:00
/**
* Base file cache driver .
*/
abstract class FileCache extends CacheProvider
{
/**
* The cache directory .
*
* @ var string
*/
protected $directory ;
/**
* The cache file extension .
*
2015-10-08 11:40:12 -07:00
* @ var string
*/
private $extension ;
2018-11-23 12:29:20 +00:00
/** @var int */
2017-04-13 15:53:35 +01:00
private $umask ;
2015-10-08 11:40:12 -07:00
2018-11-23 12:29:20 +00:00
/** @var int */
2017-04-13 15:53:35 +01:00
private $directoryStringLength ;
2015-10-08 11:40:12 -07:00
2018-11-23 12:29:20 +00:00
/** @var int */
2017-04-13 15:53:35 +01:00
private $extensionStringLength ;
2018-11-23 12:29:20 +00:00
/** @var bool */
2017-04-13 15:53:35 +01:00
private $isRunningOnWindows ;
2015-08-17 17:00:26 -07:00
/**
2015-10-08 11:40:12 -07:00
* @ param string $directory The cache directory .
* @ param string $extension The cache file extension .
2015-08-17 17:00:26 -07:00
*
* @ throws \InvalidArgumentException
*/
2015-10-08 11:40:12 -07:00
public function __construct ( $directory , $extension = '' , $umask = 0002 )
2015-08-17 17:00:26 -07:00
{
2015-10-08 11:40:12 -07:00
// YES, this needs to be *before* createPathIfNeeded()
2018-11-23 12:29:20 +00:00
if ( ! is_int ( $umask )) {
2015-10-08 11:40:12 -07:00
throw new \InvalidArgumentException ( sprintf (
'The umask parameter is required to be integer, was: %s' ,
gettype ( $umask )
));
}
$this -> umask = $umask ;
2018-11-23 12:29:20 +00:00
if ( ! $this -> createPathIfNeeded ( $directory )) {
2015-08-17 17:00:26 -07:00
throw new \InvalidArgumentException ( sprintf (
'The directory "%s" does not exist and could not be created.' ,
$directory
));
}
2018-11-23 12:29:20 +00:00
if ( ! is_writable ( $directory )) {
2015-08-17 17:00:26 -07:00
throw new \InvalidArgumentException ( sprintf (
'The directory "%s" is not writable.' ,
$directory
));
}
2015-10-08 11:40:12 -07:00
// YES, this needs to be *after* createPathIfNeeded()
2015-08-17 17:00:26 -07:00
$this -> directory = realpath ( $directory );
2015-10-08 11:40:12 -07:00
$this -> extension = ( string ) $extension ;
2017-04-13 15:53:35 +01:00
$this -> directoryStringLength = strlen ( $this -> directory );
$this -> extensionStringLength = strlen ( $this -> extension );
$this -> isRunningOnWindows = defined ( 'PHP_WINDOWS_VERSION_BUILD' );
2015-08-17 17:00:26 -07:00
}
/**
* Gets the cache directory .
*
* @ return string
*/
public function getDirectory ()
{
return $this -> directory ;
}
/**
* Gets the cache file extension .
*
2017-04-13 15:53:35 +01:00
* @ return string
2015-08-17 17:00:26 -07:00
*/
public function getExtension ()
{
return $this -> extension ;
}
/**
* @ param string $id
*
* @ return string
*/
protected function getFilename ( $id )
{
2017-04-13 15:53:35 +01:00
$hash = hash ( 'sha256' , $id );
// This ensures that the filename is unique and that there are no invalid chars in it.
2018-11-23 12:29:20 +00:00
if ( $id === ''
2017-04-13 15:53:35 +01:00
|| (( strlen ( $id ) * 2 + $this -> extensionStringLength ) > 255 )
|| ( $this -> isRunningOnWindows && ( $this -> directoryStringLength + 4 + strlen ( $id ) * 2 + $this -> extensionStringLength ) > 258 )
) {
// Most filesystems have a limit of 255 chars for each path component. On Windows the the whole path is limited
// to 260 chars (including terminating null char). Using long UNC ("\\?\" prefix) does not work with the PHP API.
// And there is a bug in PHP (https://bugs.php.net/bug.php?id=70943) with path lengths of 259.
// So if the id in hex representation would surpass the limit, we use the hash instead. The prefix prevents
// collisions between the hash and bin2hex.
$filename = '_' . $hash ;
} else {
$filename = bin2hex ( $id );
}
2015-10-08 11:40:12 -07:00
return $this -> directory
. DIRECTORY_SEPARATOR
2017-04-13 15:53:35 +01:00
. substr ( $hash , 0 , 2 )
2015-10-08 11:40:12 -07:00
. DIRECTORY_SEPARATOR
2017-04-13 15:53:35 +01:00
. $filename
2015-10-08 11:40:12 -07:00
. $this -> extension ;
2015-08-17 17:00:26 -07:00
}
/**
* { @ inheritdoc }
*/
protected function doDelete ( $id )
{
2017-04-13 15:53:35 +01:00
$filename = $this -> getFilename ( $id );
return @ unlink ( $filename ) || ! file_exists ( $filename );
2015-08-17 17:00:26 -07:00
}
/**
* { @ inheritdoc }
*/
protected function doFlush ()
{
foreach ( $this -> getIterator () as $name => $file ) {
2017-04-13 15:53:35 +01:00
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 );
}
2015-08-17 17:00:26 -07:00
}
return true ;
}
/**
* { @ inheritdoc }
*/
protected function doGetStats ()
{
$usage = 0 ;
2017-04-13 15:53:35 +01:00
foreach ( $this -> getIterator () as $name => $file ) {
2018-11-23 12:29:20 +00:00
if ( $file -> isDir () || ! $this -> isFilenameEndingWithExtension ( $name )) {
continue ;
2017-04-13 15:53:35 +01:00
}
2018-11-23 12:29:20 +00:00
$usage += $file -> getSize ();
2015-08-17 17:00:26 -07:00
}
$free = disk_free_space ( $this -> directory );
2018-11-23 12:29:20 +00:00
return [
2015-08-17 17:00:26 -07:00
Cache :: STATS_HITS => null ,
Cache :: STATS_MISSES => null ,
Cache :: STATS_UPTIME => null ,
Cache :: STATS_MEMORY_USAGE => $usage ,
Cache :: STATS_MEMORY_AVAILABLE => $free ,
2018-11-23 12:29:20 +00:00
];
2015-08-17 17:00:26 -07:00
}
2015-10-08 11:40:12 -07:00
/**
* Create path if needed .
*
* @ return bool TRUE on success or if path already exists , FALSE if path cannot be created .
*/
2018-11-23 12:29:20 +00:00
private function createPathIfNeeded ( string $path ) : bool
2015-10-08 11:40:12 -07:00
{
2018-11-23 12:29:20 +00:00
if ( ! is_dir ( $path )) {
if ( @ mkdir ( $path , 0777 & ( ~ $this -> umask ), true ) === false && ! is_dir ( $path )) {
2015-10-08 11:40:12 -07:00
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 .
*/
2018-11-23 12:29:20 +00:00
protected function writeFile ( string $filename , string $content ) : bool
2015-10-08 11:40:12 -07:00
{
$filepath = pathinfo ( $filename , PATHINFO_DIRNAME );
2018-11-23 12:29:20 +00:00
if ( ! $this -> createPathIfNeeded ( $filepath )) {
2015-10-08 11:40:12 -07:00
return false ;
}
2018-11-23 12:29:20 +00:00
if ( ! is_writable ( $filepath )) {
2015-10-08 11:40:12 -07:00
return false ;
}
$tmpFile = tempnam ( $filepath , 'swap' );
@ chmod ( $tmpFile , 0666 & ( ~ $this -> umask ));
if ( file_put_contents ( $tmpFile , $content ) !== false ) {
2018-11-23 12:29:20 +00:00
@ chmod ( $tmpFile , 0666 & ( ~ $this -> umask ));
2015-10-08 11:40:12 -07:00
if ( @ rename ( $tmpFile , $filename )) {
return true ;
}
@ unlink ( $tmpFile );
}
return false ;
}
2018-11-23 12:29:20 +00:00
private function getIterator () : \Iterator
2015-08-17 17:00:26 -07:00
{
2017-04-13 15:53:35 +01:00
return new \RecursiveIteratorIterator (
new \RecursiveDirectoryIterator ( $this -> directory , \FilesystemIterator :: SKIP_DOTS ),
\RecursiveIteratorIterator :: CHILD_FIRST
2015-10-08 11:40:12 -07:00
);
2015-08-17 17:00:26 -07:00
}
2017-04-13 15:53:35 +01:00
/**
* @ param string $name The filename
*/
2018-11-23 12:29:20 +00:00
private function isFilenameEndingWithExtension ( string $name ) : bool
2017-04-13 15:53:35 +01:00
{
2018-11-23 12:29:20 +00:00
return $this -> extension === ''
2017-04-13 15:53:35 +01:00
|| strrpos ( $name , $this -> extension ) === ( strlen ( $name ) - $this -> extensionStringLength );
}
2015-08-17 17:00:26 -07:00
}