You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
185 lines
5.1 KiB
185 lines
5.1 KiB
1 year ago
|
<?php
|
||
|
|
||
|
declare(strict_types=1);
|
||
|
|
||
|
/*
|
||
|
* This file is part of PHP CS Fixer.
|
||
|
*
|
||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||
|
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||
|
*
|
||
|
* This source file is subject to the MIT license that is bundled
|
||
|
* with this source code in the file LICENSE.
|
||
|
*/
|
||
|
|
||
|
namespace PhpCsFixer\Cache;
|
||
|
|
||
|
use Symfony\Component\Filesystem\Exception\IOException;
|
||
|
|
||
|
/**
|
||
|
* @author Andreas Möller <am@localheinz.com>
|
||
|
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||
|
*
|
||
|
* @internal
|
||
|
*/
|
||
|
final class FileHandler implements FileHandlerInterface
|
||
|
{
|
||
|
private \SplFileInfo $fileInfo;
|
||
|
|
||
|
private int $fileMTime = 0;
|
||
|
|
||
|
public function __construct(string $file)
|
||
|
{
|
||
|
$this->fileInfo = new \SplFileInfo($file);
|
||
|
}
|
||
|
|
||
|
public function getFile(): string
|
||
|
{
|
||
|
return $this->fileInfo->getPathname();
|
||
|
}
|
||
|
|
||
|
public function read(): ?CacheInterface
|
||
|
{
|
||
|
if (!$this->fileInfo->isFile() || !$this->fileInfo->isReadable()) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
$fileObject = $this->fileInfo->openFile('r');
|
||
|
|
||
|
$cache = $this->readFromHandle($fileObject);
|
||
|
$this->fileMTime = $this->getFileCurrentMTime();
|
||
|
|
||
|
unset($fileObject); // explicitly close file handler
|
||
|
|
||
|
return $cache;
|
||
|
}
|
||
|
|
||
|
public function write(CacheInterface $cache): void
|
||
|
{
|
||
|
$this->ensureFileIsWriteable();
|
||
|
|
||
|
$fileObject = $this->fileInfo->openFile('r+');
|
||
|
|
||
|
if (method_exists($cache, 'backfillHashes') && $this->fileMTime < $this->getFileCurrentMTime()) {
|
||
|
$resultOfFlock = $fileObject->flock(LOCK_EX);
|
||
|
if (false === $resultOfFlock) {
|
||
|
// Lock failed, OK - we continue without the lock.
|
||
|
// noop
|
||
|
}
|
||
|
|
||
|
$oldCache = $this->readFromHandle($fileObject);
|
||
|
|
||
|
$fileObject->rewind();
|
||
|
|
||
|
if (null !== $oldCache) {
|
||
|
$cache->backfillHashes($oldCache);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$resultOfTruncate = $fileObject->ftruncate(0);
|
||
|
if (false === $resultOfTruncate) {
|
||
|
// Truncate failed. OK - we do not save the cache.
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$resultOfWrite = $fileObject->fwrite($cache->toJson());
|
||
|
if (false === $resultOfWrite) {
|
||
|
// Write failed. OK - we did not save the cache.
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$resultOfFlush = $fileObject->fflush();
|
||
|
if (false === $resultOfFlush) {
|
||
|
// Flush failed. OK - part of cache can be missing, in case this was last chunk in this pid.
|
||
|
// noop
|
||
|
}
|
||
|
|
||
|
$this->fileMTime = time(); // we could take the fresh `mtime` of file that we just modified with `$this->getFileCurrentMTime()`, but `time()` should be good enough here and reduce IO operation
|
||
|
}
|
||
|
|
||
|
private function getFileCurrentMTime(): int
|
||
|
{
|
||
|
clearstatcache(true, $this->fileInfo->getPathname());
|
||
|
|
||
|
$mtime = $this->fileInfo->getMTime();
|
||
|
|
||
|
if (false === $mtime) {
|
||
|
// cannot check mtime? OK - let's pretend file is old.
|
||
|
$mtime = 0;
|
||
|
}
|
||
|
|
||
|
return $mtime;
|
||
|
}
|
||
|
|
||
|
private function readFromHandle(\SplFileObject $fileObject): ?CacheInterface
|
||
|
{
|
||
|
try {
|
||
|
$size = $fileObject->getSize();
|
||
|
if (false === $size || 0 === $size) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
$content = $fileObject->fread($size);
|
||
|
|
||
|
if (false === $content) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
return Cache::fromJson($content);
|
||
|
} catch (\InvalidArgumentException $exception) {
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private function ensureFileIsWriteable(): void
|
||
|
{
|
||
|
if ($this->fileInfo->isFile() && $this->fileInfo->isWritable()) {
|
||
|
// all good
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if ($this->fileInfo->isDir()) {
|
||
|
throw new IOException(
|
||
|
sprintf('Cannot write cache file "%s" as the location exists as directory.', $this->fileInfo->getRealPath()),
|
||
|
0,
|
||
|
null,
|
||
|
$this->fileInfo->getPathname()
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if ($this->fileInfo->isFile() && !$this->fileInfo->isWritable()) {
|
||
|
throw new IOException(
|
||
|
sprintf('Cannot write to file "%s" as it is not writable.', $this->fileInfo->getRealPath()),
|
||
|
0,
|
||
|
null,
|
||
|
$this->fileInfo->getPathname()
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$this->createFile($this->fileInfo->getPathname());
|
||
|
}
|
||
|
|
||
|
private function createFile(string $file): void
|
||
|
{
|
||
|
$dir = \dirname($file);
|
||
|
|
||
|
// Ensure path is created, but ignore if already exists. FYI: ignore EA suggestion in IDE,
|
||
|
// `mkdir()` returns `false` for existing paths, so we can't mix it with `is_dir()` in one condition.
|
||
|
if (!@is_dir($dir)) {
|
||
|
@mkdir($dir, 0777, true);
|
||
|
}
|
||
|
|
||
|
if (!@is_dir($dir)) {
|
||
|
throw new IOException(
|
||
|
sprintf('Directory of cache file "%s" does not exists and couldn\'t be created.', $file),
|
||
|
0,
|
||
|
null,
|
||
|
$file
|
||
|
);
|
||
|
}
|
||
|
|
||
|
@touch($file);
|
||
|
@chmod($file, 0666);
|
||
|
}
|
||
|
}
|