diff --git a/lib/private/Cache/File.php b/lib/private/Cache/File.php index 0d312bd97976b..c6795fa05f217 100644 --- a/lib/private/Cache/File.php +++ b/lib/private/Cache/File.php @@ -5,29 +5,29 @@ * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only */ + namespace OC\Cache; -use OC\Files\Filesystem; -use OC\Files\View; use OC\ForbiddenException; use OC\User\NoUserException; -use OCP\Files\LockNotAcquiredException; +use OCP\Files\File as FileNode; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; use OCP\ICache; use OCP\IConfig; use OCP\IUserSession; -use OCP\Lock\LockedException; use OCP\Security\ISecureRandom; use OCP\Server; use Psr\Log\LoggerInterface; class File implements ICache { - /** @var View */ - protected $storage; + protected ?Folder $storage = null; /** - * Returns the cache storage for the logged in user + * Returns the cache folder for the logged in user * - * @return View cache storage + * @return Folder cache folder * @throws ForbiddenException * @throws NoUserException */ @@ -36,14 +36,19 @@ protected function getStorage() { return $this->storage; } $session = Server::get(IUserSession::class); - if ($session->isLoggedIn()) { - $rootView = new View(); - $userId = $session->getUser()->getUID(); - Filesystem::initMountPoints($userId); - if (!$rootView->file_exists('/' . $userId . '/cache')) { - $rootView->mkdir('/' . $userId . '/cache'); + $user = $session->getUser(); + $rootFolder = Server::get(IRootFolder::class); + if ($user) { + $userId = $user->getUID(); + try { + $cacheFolder = $rootFolder->get('/' . $userId . '/cache'); + if (!$cacheFolder instanceof Folder) { + throw new \Exception('Cache folder is a file'); + } + } catch (NotFoundException $e) { + $cacheFolder = $rootFolder->newFolder('/' . $userId . '/cache'); } - $this->storage = new View('/' . $userId . '/cache'); + $this->storage = $cacheFolder; return $this->storage; } else { Server::get(LoggerInterface::class)->error('Can\'t get cache storage, user not logged in', ['app' => 'core']); @@ -58,27 +63,29 @@ protected function getStorage() { */ #[\Override] public function get($key) { - $result = null; - if ($this->hasKey($key)) { - $storage = $this->getStorage(); - $result = $storage->file_get_contents($key); + $storage = $this->getStorage(); + try { + /** @var FileNode $item */ + $item = $storage->get($key); + return $item->getContent(); + } catch (NotFoundException $e) { + return null; } - return $result; } /** * Returns the size of the stored/cached data * * @param string $key - * @return int + * @return int|float */ public function size($key) { - $result = 0; - if ($this->hasKey($key)) { - $storage = $this->getStorage(); - $result = $storage->filesize($key); + $storage = $this->getStorage(); + try { + return $storage->get($key)->getSize(); + } catch (NotFoundException $e) { + return 0; } - return $result; } /** @@ -91,24 +98,23 @@ public function size($key) { #[\Override] public function set($key, $value, $ttl = 0) { $storage = $this->getStorage(); - $result = false; // unique id to avoid chunk collision, just in case $uniqueId = Server::get(ISecureRandom::class)->generate( 16, ISecureRandom::CHAR_ALPHANUMERIC ); - // use part file to prevent hasKey() to find the key + // use a temporary file to prevent hasKey() to find the key // while it is being written - $keyPart = $key . '.' . $uniqueId . '.part'; - if ($storage && $storage->file_put_contents($keyPart, $value)) { - if ($ttl === 0) { - $ttl = 86400; // 60*60*24 - } - $result = $storage->touch($keyPart, time() + $ttl); - $result &= $storage->rename($keyPart, $key); + $keyPart = $key . '.' . $uniqueId; + $file = $storage->newFile($keyPart, $value); + if ($ttl === 0) { + $ttl = 86400; // 60*60*24 } - return $result; + $file->move($storage->getFullPath($key)); + $file->touch(time() + $ttl); + + return true; } /** @@ -118,11 +124,7 @@ public function set($key, $value, $ttl = 0) { */ #[\Override] public function hasKey($key) { - $storage = $this->getStorage(); - if ($storage && $storage->is_file($key) && $storage->isReadable($key)) { - return true; - } - return false; + return $this->getStorage()->nodeExists($key); } /** @@ -133,10 +135,12 @@ public function hasKey($key) { #[\Override] public function remove($key) { $storage = $this->getStorage(); - if (!$storage) { + try { + $storage->get($key)->delete(); + return true; + } catch (NotFoundException $e) { return false; } - return $storage->unlink($key); } /** @@ -147,14 +151,9 @@ public function remove($key) { #[\Override] public function clear($prefix = '') { $storage = $this->getStorage(); - if ($storage && $storage->is_dir('/')) { - $dh = $storage->opendir('/'); - if (is_resource($dh)) { - while (($file = readdir($dh)) !== false) { - if ($file !== '.' && $file !== '..' && ($prefix === '' || str_starts_with($file, $prefix))) { - $storage->unlink('/' . $file); - } - } + foreach ($storage->getDirectoryListing() as $file) { + if ($prefix === '' || str_starts_with($file->getName(), $prefix)) { + $file->delete(); } } return true; @@ -162,34 +161,28 @@ public function clear($prefix = '') { /** * Runs GC + * * @throws ForbiddenException */ public function gc() { $storage = $this->getStorage(); - if ($storage) { - $ttl = Server::get(IConfig::class)->getSystemValueInt('cache_chunk_gc_ttl', 60 * 60 * 24); - $now = time() - $ttl; + $ttl = Server::get(IConfig::class)->getSystemValueInt('cache_chunk_gc_ttl', 60 * 60 * 24); + // extra hour safety, in case of stray part chunks that take longer to write, + // because touch() is only called after the chunk was finished - $dh = $storage->opendir('/'); - if (!is_resource($dh)) { - return null; - } - while (($file = readdir($dh)) !== false) { - if ($file !== '.' && $file !== '..') { - try { - $mtime = $storage->filemtime('/' . $file); - if ($mtime < $now) { - $storage->unlink('/' . $file); - } - } catch (LockedException $e) { - // ignore locked chunks - Server::get(LoggerInterface::class)->debug('Could not cleanup locked chunk "' . $file . '"', ['app' => 'core']); - } catch (\OCP\Files\ForbiddenException $e) { - Server::get(LoggerInterface::class)->debug('Could not cleanup forbidden chunk "' . $file . '"', ['app' => 'core']); - } catch (LockNotAcquiredException $e) { - Server::get(LoggerInterface::class)->debug('Could not cleanup locked chunk "' . $file . '"', ['app' => 'core']); - } + $now = time() - $ttl; + foreach ($storage->getDirectoryListing() as $file) { + try { + if ($file->getMTime() < $now) { + $file->delete(); } + } catch (\OCP\Lock\LockedException $e) { + // ignore locked chunks + Server::get(LoggerInterface::class)->debug('Could not cleanup locked chunk "' . $file->getName() . '"', ['app' => 'core']); + } catch (\OCP\Files\ForbiddenException $e) { + Server::get(LoggerInterface::class)->debug('Could not cleanup forbidden chunk "' . $file->getName() . '"', ['app' => 'core']); + } catch (\OCP\Files\LockNotAcquiredException $e) { + Server::get(LoggerInterface::class)->debug('Could not cleanup locked chunk "' . $file->getName() . '"', ['app' => 'core']); } } } diff --git a/tests/lib/Cache/FileCacheTest.php b/tests/lib/Cache/FileCacheTest.php index 8cf5eecf809b1..dce82c2908560 100644 --- a/tests/lib/Cache/FileCacheTest.php +++ b/tests/lib/Cache/FileCacheTest.php @@ -9,16 +9,14 @@ namespace Test\Cache; use OC\Cache\File; -use OC\Files\Filesystem; -use OC\Files\Storage\Local; -use OC\Files\Storage\Storage; use OC\Files\Storage\Temporary; -use OC\Files\View; -use OCP\Files\LockNotAcquiredException; +use OCP\Files\ISetupManager; use OCP\Files\Mount\IMountManager; -use OCP\ITempManager; -use OCP\Lock\LockedException; +use OCP\Files\Storage\IStorage; +use OCP\IUserSession; +use OCP\Lock\ILockingProvider; use OCP\Server; +use Test\Traits\MountProviderTrait; use Test\Traits\UserTrait; /** @@ -30,51 +28,25 @@ #[\PHPUnit\Framework\Attributes\Group('DB')] class FileCacheTest extends TestCache { use UserTrait; + use MountProviderTrait; - /** - * @var string - * */ - private $user; - /** - * @var string - * */ - private $datadir; - /** - * @var Storage - * */ - private $storage; - /** - * @var View - * */ - private $rootView; - - public function skip() { - //$this->skipUnless(OC_User::isLoggedIn()); - } + private IStorage $storage; #[\Override] protected function setUp(): void { parent::setUp(); - //login - $this->createUser('test', 'test'); - - $this->user = \OC_User::getUser(); - \OC_User::setUserId('test'); + $user = $this->createUser('test', 'test'); - //clear all proxies and hooks so we can do clean testing - \OC_Hook::clear('OC_Filesystem'); + $userSession = Server::get(IUserSession::class); + $userSession->setUser($user); /** @var IMountManager $manager */ $manager = Server::get(IMountManager::class); $manager->removeMount('/test'); - $storage = new Temporary([]); - Filesystem::mount($storage, [], '/test/cache'); - - //set up the users dir - $this->rootView = new View(''); - $this->rootView->mkdir('/test'); + $this->storage = new Temporary([]); + $this->registerMount($user->getUID(), $this->storage, '/' . $user->getUID() . '/cache/'); $this->instance = new File(); @@ -84,80 +56,55 @@ protected function setUp(): void { #[\Override] protected function tearDown(): void { - if ($this->instance) { - $this->instance->remove('hack', 'hack'); - } - - \OC_User::setUserId($this->user); - if ($this->instance) { $this->instance->clear(); $this->instance = null; } - parent::tearDown(); - } - - private function setupMockStorage() { - $mockStorage = $this->getMockBuilder(Local::class) - ->onlyMethods(['filemtime', 'unlink']) - ->setConstructorArgs([['datadir' => Server::get(ITempManager::class)->getTemporaryFolder()]]) - ->getMock(); - - Filesystem::mount($mockStorage, [], '/test/cache'); + Server::get(ISetupManager::class)->tearDown(); - return $mockStorage; + parent::tearDown(); } public function testGarbageCollectOldKeys(): void { - $mockStorage = $this->setupMockStorage(); + $this->instance->set('key1', 'value1'); - $mockStorage->expects($this->atLeastOnce()) - ->method('filemtime') - ->willReturn(100); - $mockStorage->expects($this->once()) - ->method('unlink') - ->with('key1') - ->willReturn(true); + $this->assertTrue($this->storage->file_exists('key1')); + $this->storage->getCache()->put('key1', ['mtime' => 100]); - $this->instance->set('key1', 'value1'); $this->instance->gc(); + $this->assertFalse($this->storage->file_exists('key1')); } public function testGarbageCollectLeaveRecentKeys(): void { - $mockStorage = $this->setupMockStorage(); - - $mockStorage->expects($this->atLeastOnce()) - ->method('filemtime') - ->willReturn(time() + 3600); - $mockStorage->expects($this->never()) - ->method('unlink') - ->with('key1'); $this->instance->set('key1', 'value1'); + + $this->assertTrue($this->storage->file_exists('key1')); + $this->storage->getCache()->put('key1', ['mtime' => time() + 3600]); + $this->instance->gc(); - } - public static function lockExceptionProvider(): array { - return [ - [new LockedException('key1')], - [new LockNotAcquiredException('key1', 1)], - ]; + $this->assertTrue($this->storage->file_exists('key1')); } - #[\PHPUnit\Framework\Attributes\DataProvider('lockExceptionProvider')] - public function testGarbageCollectIgnoreLockedKeys($testException): void { - $mockStorage = $this->setupMockStorage(); - - $mockStorage->expects($this->atLeastOnce()) - ->method('filemtime') - ->willReturn(100); - $mockStorage->expects($this->atLeastOnce()) - ->method('unlink') - ->willReturnOnConsecutiveCalls($this->throwException($testException), true); + public function testGarbageCollectIgnoreLockedKeys(): void { + $lockingProvider = \OC::$server->get(ILockingProvider::class); $this->instance->set('key1', 'value1'); + $this->storage->getCache()->put('key1', ['mtime' => 100]); $this->instance->set('key2', 'value2'); + $this->storage->getCache()->put('key2', ['mtime' => 100]); + $this->storage->acquireLock('key2', ILockingProvider::LOCK_SHARED, $lockingProvider); + + $this->assertTrue($this->storage->file_exists('key1')); + $this->assertTrue($this->storage->file_exists('key2')); $this->instance->gc(); + + $this->storage->releaseLock('key2', ILockingProvider::LOCK_SHARED, $lockingProvider); + + $this->assertFalse($this->storage->file_exists('key1')); + $this->assertFalse($this->storage->file_exists('key2')); + } }