Skip to content

Don't create cache directory in constructor + Fix nested directory permissions #88

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 11, 2025
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -91,9 +90,6 @@
private string $cachePath,
private int $directoryMode = 0775,
) {
if (!$this->createDirectoryIfNotExists($cachePath)) {
throw new CacheException("Failed to create cache directory \"$cachePath\".");
}
}

public function get(string $key, mixed $default = null): mixed
Expand All @@ -105,7 +101,7 @@
return $default;
}

flock($filePointer, LOCK_SH);

Check warning on line 104 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 @@ @@ if (!$this->existsAndNotExpired($file) || ($filePointer = @fopen($file, 'rb')) === false) { return $default; } - flock($filePointer, LOCK_SH); + $value = stream_get_contents($filePointer); flock($filePointer, LOCK_UN); fclose($filePointer);
$value = stream_get_contents($filePointer);
flock($filePointer, LOCK_UN);
fclose($filePointer);
Expand All @@ -123,14 +119,7 @@
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
Expand Down Expand Up @@ -196,7 +185,7 @@
public function setMultiple(iterable $values, null|int|DateInterval $ttl = null): bool
{
$values = $this->iterableToArray($values);
$this->validateKeys(array_map('\strval', array_keys($values)));

Check warning on line 188 in src/FileCache.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "MethodCallRemoval": --- Original +++ New @@ @@ public function setMultiple(iterable $values, null|int|DateInterval $ttl = null) : bool { $values = $this->iterableToArray($values); - $this->validateKeys(array_map('\\strval', array_keys($values))); + foreach ($values as $key => $value) { $this->set((string) $key, $value, $ttl); }

foreach ($values as $key => $value) {
$this->set((string) $key, $value, $ttl);
Expand Down Expand Up @@ -325,25 +314,27 @@
}

/**
* Ensures that the directory is created.
* Ensures that the directory exists.
*
* @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, file with the same name exists: \"$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