Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## 3.1.1 under development

- Enh #88: Don't create cache directory on `FileCache` initialization (@vjik)
- Bug #88: Set correct permissions for nested directories (@vjik)
- Bug #85: Clear stat cache in `FileCache::set()` (@samdark)

## 3.1.0 October 09, 2023
Expand Down
42 changes: 20 additions & 22 deletions src/FileCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use function array_keys;
use function array_map;
use function closedir;
use function dirname;
use function error_get_last;
use function filemtime;
use function fileowner;
Expand Down Expand Up @@ -89,11 +88,8 @@
*/
public function __construct(
private string $cachePath,
private int $directoryMode = 0775,

Check warning on line 91 in src/FileCache.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "IncrementInteger": --- Original +++ New @@ @@ * * @throws CacheException If failed to create cache directory. */ - public function __construct(private string $cachePath, private int $directoryMode = 0775) + public function __construct(private string $cachePath, private int $directoryMode = 510) { } public function get(string $key, mixed $default = null) : mixed
) {
if (!$this->createDirectoryIfNotExists($cachePath)) {
throw new CacheException("Failed to create cache directory \"$cachePath\".");
}
}

public function get(string $key, mixed $default = null): mixed
Expand All @@ -107,7 +103,7 @@

flock($filePointer, LOCK_SH);
$value = stream_get_contents($filePointer);
flock($filePointer, LOCK_UN);

Check warning on line 106 in src/FileCache.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "FunctionCallRemoval": --- Original +++ New @@ @@ } flock($filePointer, LOCK_SH); $value = stream_get_contents($filePointer); - flock($filePointer, LOCK_UN); + fclose($filePointer); return unserialize($value); }
fclose($filePointer);

return unserialize($value);
Expand All @@ -119,23 +115,16 @@
$this->gc();
$expiration = $this->ttlToExpiration($ttl);

if ($expiration <= self::EXPIRATION_EXPIRED) {

Check warning on line 118 in src/FileCache.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "LessThanOrEqualTo": --- Original +++ New @@ @@ $this->validateKey($key); $this->gc(); $expiration = $this->ttlToExpiration($ttl); - if ($expiration <= self::EXPIRATION_EXPIRED) { + if ($expiration < self::EXPIRATION_EXPIRED) { return $this->delete($key); } $file = $this->getCacheFile($key, ensureDirectory: true);
return $this->delete($key);
}

$file = $this->getCacheFile($key);
$cacheDirectory = dirname($file);

if (!is_dir($this->cachePath)
|| $this->directoryLevel > 0 && !$this->createDirectoryIfNotExists($cacheDirectory)
) {
throw new CacheException("Failed to create cache directory \"$cacheDirectory\".");
}
$file = $this->getCacheFile($key, ensureDirectory: true);

// If ownership differs, the touch call will fail, so we try to
// rebuild the file from scratch by deleting it first
// https://github.com/yiisoft/yii2/pull/16120
if (function_exists('posix_geteuid') && is_file($file) && fileowner($file) !== posix_geteuid()) {

Check warning on line 127 in src/FileCache.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "LogicalAndSingleSubExprNegation": --- Original +++ New @@ @@ // If ownership differs, the touch call will fail, so we try to // rebuild the file from scratch by deleting it first // https://github.com/yiisoft/yii2/pull/16120 - if (function_exists('posix_geteuid') && is_file($file) && fileowner($file) !== posix_geteuid()) { + if (!function_exists('posix_geteuid') && is_file($file) && fileowner($file) !== posix_geteuid()) { @Unlink($file); } if (file_put_contents($file, serialize($value), LOCK_EX) === false) {
@unlink($file);
}

Expand Down Expand Up @@ -325,25 +314,27 @@
}

/**
* Ensures that the directory is created.
* Ensures that the directory is existing.
*
* @param string $path The path to the directory.
*
* @return bool Whether the directory was created.
*/
private function createDirectoryIfNotExists(string $path): bool
private function ensureDirectory(string $path): void
{
if (is_dir($path)) {
return true;
return;
}

if (is_file($path)) {
throw new CacheException("Failed to create cache directory \"$path\".");
}

$result = !is_file($path) && mkdir(directory: $path, recursive: true) && is_dir($path);
mkdir($path, recursive: true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it fail in case of race condition? is_dir + mkdir is not an atomic operation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will. Race condition don't fixed in this PR. I make it next PR.


if ($result) {
chmod($path, $this->directoryMode);
if (!is_dir($path)) {
throw new CacheException("Failed to create cache directory \"$path\".");

Check warning on line 334 in src/FileCache.php

View check run for this annotation

Codecov / codecov/patch

src/FileCache.php#L334

Added line #L334 was not covered by tests
}

return $result;
chmod($path, $this->directoryMode);
}

/**
Expand All @@ -353,8 +344,12 @@
*
* @return string The cache file path.
*/
private function getCacheFile(string $key): string
private function getCacheFile(string $key, bool $ensureDirectory = false): string
{
if ($ensureDirectory) {
$this->ensureDirectory($this->cachePath);
}

if ($this->directoryLevel < 1) {
return $this->cachePath . DIRECTORY_SEPARATOR . $key . $this->fileSuffix;
}
Expand All @@ -364,6 +359,9 @@
for ($i = 0; $i < $this->directoryLevel; ++$i) {
if (($prefix = substr($key, $i + $i, 2)) !== '') {
$base .= DIRECTORY_SEPARATOR . $prefix;
if ($ensureDirectory) {
$this->ensureDirectory($base);
}
}
}

Expand Down
23 changes: 7 additions & 16 deletions tests/FileCacheTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -449,17 +449,16 @@ public function testDirectoryMode(): void
$this->markTestSkipped('Can not test permissions on Windows');
}

$cache = new FileCache($this->tmpDir, 0777);
$cache = (new FileCache($this->tmpDir, 0777))->withDirectoryLevel(2);

$this->assertInstanceOf(FileCache::class, $cache);

$cache->set('a', 1);
$this->assertSameExceptObject(1, $cache->get('a'));

$cacheFile = $this->invokeMethod($cache, 'getCacheFile', ['a']);
$permissions = substr(sprintf('%o', fileperms(dirname($cacheFile))), -4);
$cache->set('test', 1);
$this->assertSameExceptObject(1, $cache->get('test'));

$this->assertEquals('0777', $permissions);
$cacheFile = $this->invokeMethod($cache, 'getCacheFile', ['test']);
$this->assertEquals('0777', substr(sprintf('%o', fileperms(dirname($cacheFile))), -4));
$this->assertEquals('0777', substr(sprintf('%o', fileperms(dirname($cacheFile, 2))), -4));

// also check top level cache dir permissions
$permissions = substr(sprintf('%o', fileperms($this->tmpDir)), -4);
Expand Down Expand Up @@ -539,21 +538,13 @@ public function testSetThrowExceptionForInvalidCacheDirectory(): void
$directory = self::RUNTIME_DIRECTORY . '/cache/fail';
$cache = new FileCache($directory);

$this->removeDirectory($directory);
mkdir(dirname($directory), recursive: true);
file_put_contents($directory, 'fail');

$this->expectException(CacheException::class);
$cache->set('key', 'value');
}

public function testConstructorThrowExceptionForInvalidCacheDirectory(): void
{
$file = self::RUNTIME_DIRECTORY . '/fail';
file_put_contents($file, 'fail');
$this->expectException(CacheException::class);
new FileCache($file);
}

public function invalidKeyProvider(): array
{
return [
Expand Down
Loading