Revert "perf(base): Stop setting up the FS for every basic auth request"
parent
40117dced3
commit
3df6d90a4c
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
namespace OC\Cache;
|
||||
|
||||
use OCP\ICache;
|
||||
|
||||
/**
|
||||
* In-memory cache with a capacity limit to keep memory usage in check
|
||||
*
|
||||
* Uses a simple FIFO expiry mechanism
|
||||
* @template T
|
||||
* @deprecated 25.0.0 use OCP\Cache\CappedMemoryCache instead
|
||||
*/
|
||||
class CappedMemoryCache implements ICache, \ArrayAccess {
|
||||
private $capacity;
|
||||
/** @var T[] */
|
||||
private $cache = [];
|
||||
|
||||
public function __construct($capacity = 512) {
|
||||
$this->capacity = $capacity;
|
||||
}
|
||||
|
||||
public function hasKey($key): bool {
|
||||
return isset($this->cache[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?T
|
||||
*/
|
||||
public function get($key) {
|
||||
return $this->cache[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param T $value
|
||||
* @param int $ttl
|
||||
* @return bool
|
||||
*/
|
||||
public function set($key, $value, $ttl = 0): bool {
|
||||
if (is_null($key)) {
|
||||
$this->cache[] = $value;
|
||||
} else {
|
||||
$this->cache[$key] = $value;
|
||||
}
|
||||
$this->garbageCollect();
|
||||
return true;
|
||||
}
|
||||
|
||||
public function remove($key) {
|
||||
unset($this->cache[$key]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function clear($prefix = '') {
|
||||
$this->cache = [];
|
||||
return true;
|
||||
}
|
||||
|
||||
public function offsetExists($offset): bool {
|
||||
return $this->hasKey($offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return T
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function &offsetGet($offset) {
|
||||
return $this->cache[$offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $offset
|
||||
* @param T $value
|
||||
* @return void
|
||||
*/
|
||||
public function offsetSet($offset, $value): void {
|
||||
$this->set($offset, $value);
|
||||
}
|
||||
|
||||
public function offsetUnset($offset): void {
|
||||
$this->remove($offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return T[]
|
||||
*/
|
||||
public function getData() {
|
||||
return $this->cache;
|
||||
}
|
||||
|
||||
|
||||
private function garbageCollect() {
|
||||
while (count($this->cache) > $this->capacity) {
|
||||
reset($this->cache);
|
||||
$key = key($this->cache);
|
||||
$this->remove($key);
|
||||
}
|
||||
}
|
||||
|
||||
public static function isAvailable(): bool {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
namespace OC\Cache;
|
||||
|
||||
use OC\Files\Filesystem;
|
||||
use OC\Files\View;
|
||||
use OCP\ICache;
|
||||
use OCP\IUserSession;
|
||||
use OCP\Security\ISecureRandom;
|
||||
use OCP\Server;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class File implements ICache {
|
||||
/** @var View */
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* Returns the cache storage for the logged in user
|
||||
*
|
||||
* @return \OC\Files\View cache storage
|
||||
* @throws \OC\ForbiddenException
|
||||
* @throws \OC\User\NoUserException
|
||||
*/
|
||||
protected function getStorage() {
|
||||
if ($this->storage !== null) {
|
||||
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');
|
||||
}
|
||||
$this->storage = new View('/' . $userId . '/cache');
|
||||
return $this->storage;
|
||||
} else {
|
||||
Server::get(LoggerInterface::class)->error('Can\'t get cache storage, user not logged in', ['app' => 'core']);
|
||||
throw new \OC\ForbiddenException('Can\t get cache storage, user not logged in');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return mixed|null
|
||||
* @throws \OC\ForbiddenException
|
||||
*/
|
||||
public function get($key) {
|
||||
$result = null;
|
||||
if ($this->hasKey($key)) {
|
||||
$storage = $this->getStorage();
|
||||
$result = $storage->file_get_contents($key);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the stored/cached data
|
||||
*
|
||||
* @param string $key
|
||||
* @return int
|
||||
*/
|
||||
public function size($key) {
|
||||
$result = 0;
|
||||
if ($this->hasKey($key)) {
|
||||
$storage = $this->getStorage();
|
||||
$result = $storage->filesize($key);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param int $ttl
|
||||
* @return bool|mixed
|
||||
* @throws \OC\ForbiddenException
|
||||
*/
|
||||
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
|
||||
// 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);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return bool
|
||||
* @throws \OC\ForbiddenException
|
||||
*/
|
||||
public function hasKey($key) {
|
||||
$storage = $this->getStorage();
|
||||
if ($storage && $storage->is_file($key) && $storage->isReadable($key)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return bool|mixed
|
||||
* @throws \OC\ForbiddenException
|
||||
*/
|
||||
public function remove($key) {
|
||||
$storage = $this->getStorage();
|
||||
if (!$storage) {
|
||||
return false;
|
||||
}
|
||||
return $storage->unlink($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $prefix
|
||||
* @return bool
|
||||
* @throws \OC\ForbiddenException
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs GC
|
||||
* @throws \OC\ForbiddenException
|
||||
*/
|
||||
public function gc() {
|
||||
$storage = $this->getStorage();
|
||||
if ($storage) {
|
||||
// extra hour safety, in case of stray part chunks that take longer to write,
|
||||
// because touch() is only called after the chunk was finished
|
||||
$now = time() - 3600;
|
||||
$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 (\OCP\Lock\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 (\OCP\Files\LockNotAcquiredException $e) {
|
||||
Server::get(LoggerInterface::class)->debug('Could not cleanup locked chunk "' . $file . '"', ['app' => 'core']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function isAvailable(): bool {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace Test\Cache;
|
||||
|
||||
use OCP\Cache\CappedMemoryCache;
|
||||
|
||||
/**
|
||||
* Class CappedMemoryCacheTest
|
||||
*
|
||||
* @package Test\Cache
|
||||
*/
|
||||
class CappedMemoryCacheTest extends TestCache {
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->instance = new CappedMemoryCache();
|
||||
}
|
||||
|
||||
public function testSetOverCap(): void {
|
||||
$instance = new CappedMemoryCache(3);
|
||||
|
||||
$instance->set('1', 'a');
|
||||
$instance->set('2', 'b');
|
||||
$instance->set('3', 'c');
|
||||
$instance->set('4', 'd');
|
||||
$instance->set('5', 'e');
|
||||
|
||||
$this->assertFalse($instance->hasKey('1'));
|
||||
$this->assertFalse($instance->hasKey('2'));
|
||||
$this->assertTrue($instance->hasKey('3'));
|
||||
$this->assertTrue($instance->hasKey('4'));
|
||||
$this->assertTrue($instance->hasKey('5'));
|
||||
}
|
||||
|
||||
public function testClear(): void {
|
||||
$value = 'ipsum lorum';
|
||||
$this->instance->set('1_value1', $value);
|
||||
$this->instance->set('1_value2', $value);
|
||||
$this->instance->set('2_value1', $value);
|
||||
$this->instance->set('3_value1', $value);
|
||||
|
||||
$this->assertTrue($this->instance->clear());
|
||||
$this->assertFalse($this->instance->hasKey('1_value1'));
|
||||
$this->assertFalse($this->instance->hasKey('1_value2'));
|
||||
$this->assertFalse($this->instance->hasKey('2_value1'));
|
||||
$this->assertFalse($this->instance->hasKey('3_value1'));
|
||||
}
|
||||
|
||||
public function testIndirectSet(): void {
|
||||
$this->instance->set('array', []);
|
||||
|
||||
$this->instance['array'][] = 'foo';
|
||||
|
||||
$this->assertEquals(['foo'], $this->instance->get('array'));
|
||||
|
||||
$this->instance['array']['bar'] = 'qwerty';
|
||||
|
||||
$this->assertEquals(['foo', 'bar' => 'qwerty'], $this->instance->get('array'));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
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\Mount\IMountManager;
|
||||
use OCP\ITempManager;
|
||||
use OCP\Lock\LockedException;
|
||||
use OCP\Server;
|
||||
use Test\Traits\UserTrait;
|
||||
|
||||
/**
|
||||
* Class FileCacheTest
|
||||
*
|
||||
* @group DB
|
||||
*
|
||||
* @package Test\Cache
|
||||
*/
|
||||
class FileCacheTest extends TestCache {
|
||||
use UserTrait;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* */
|
||||
private $user;
|
||||
/**
|
||||
* @var string
|
||||
* */
|
||||
private $datadir;
|
||||
/**
|
||||
* @var Storage
|
||||
* */
|
||||
private $storage;
|
||||
/**
|
||||
* @var View
|
||||
* */
|
||||
private $rootView;
|
||||
|
||||
public function skip() {
|
||||
//$this->skipUnless(OC_User::isLoggedIn());
|
||||
}
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
//login
|
||||
$this->createUser('test', 'test');
|
||||
|
||||
$this->user = \OC_User::getUser();
|
||||
\OC_User::setUserId('test');
|
||||
|
||||
//clear all proxies and hooks so we can do clean testing
|
||||
\OC_Hook::clear('OC_Filesystem');
|
||||
|
||||
/** @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->instance = new File();
|
||||
|
||||
// forces creation of cache folder for subsequent tests
|
||||
$this->instance->set('hack', 'hack');
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
return $mockStorage;
|
||||
}
|
||||
|
||||
public function testGarbageCollectOldKeys(): void {
|
||||
$mockStorage = $this->setupMockStorage();
|
||||
|
||||
$mockStorage->expects($this->atLeastOnce())
|
||||
->method('filemtime')
|
||||
->willReturn(100);
|
||||
$mockStorage->expects($this->once())
|
||||
->method('unlink')
|
||||
->with('key1')
|
||||
->willReturn(true);
|
||||
|
||||
$this->instance->set('key1', 'value1');
|
||||
$this->instance->gc();
|
||||
}
|
||||
|
||||
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->instance->gc();
|
||||
}
|
||||
|
||||
public static function lockExceptionProvider(): array {
|
||||
return [
|
||||
[new LockedException('key1')],
|
||||
[new LockNotAcquiredException('key1', 1)],
|
||||
];
|
||||
}
|
||||
|
||||
#[\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), $this->returnValue(true));
|
||||
|
||||
$this->instance->set('key1', 'value1');
|
||||
$this->instance->set('key2', 'value2');
|
||||
|
||||
$this->instance->gc();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue