Revert "perf(base): Stop setting up the FS for every basic auth request"

pull/54717/head
Joas Schilling 2025-08-28 17:11:31 +07:00 committed by GitHub
parent 40117dced3
commit 3df6d90a4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 930 additions and 27 deletions

@ -196,7 +196,6 @@ abstract class TestCase extends \Test\TestCase {
Server::get(IUserSession::class)->setUser(null);
Filesystem::tearDown();
Server::get(IUserSession::class)->login($user, $password);
Filesystem::initMountPoints($user);
\OC::$server->getUserFolder($user);
\OC_Util::setupFS($user);

@ -182,7 +182,8 @@ export default {
if (data.status === 'success') {
this.handleAvatarUpdate(false)
} else if (data.data === 'notsquare') {
this.$refs.cropper.replace(data.image)
const tempAvatar = generateUrl('/avatar/tmp') + '?requesttoken=' + encodeURIComponent(OC.requestToken) + '#' + Math.floor(Math.random() * 1000)
this.$refs.cropper.replace(tempAvatar)
this.showCropper = true
} else {
showError(data.data.message)

@ -21,9 +21,32 @@ Feature: avatar
And last avatar is a square of size 512
And last avatar is not a single color
Scenario: get temporary non-square user avatar before cropping it
Given Logging in using web as "user0"
And logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
When logged in user gets temporary avatar
Then The following headers should be set
| Content-Type | image/png |
# "last avatar" also includes the last temporary avatar
And last avatar is not a square
And last avatar is not a single color
Scenario: get non-square user avatar before cropping it
Given Logging in using web as "user0"
And logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
# Avatar needs to be cropped to finish setting it
When user "user0" gets avatar for user "user0"
Then The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 0 |
And last avatar is a square of size 512
And last avatar is not a single color
Scenario: set square user avatar from file
Given Logging in using web as "user0"
When logged in user posts avatar from file "data/green-square-256.png"
When logged in user posts temporary avatar from file "data/green-square-256.png"
And user "user0" gets avatar for user "user0"
And The following headers should be set
| Content-Type | image/png |
@ -41,7 +64,7 @@ Feature: avatar
Scenario: set square user avatar from internal path
Given user "user0" uploads file "data/green-square-256.png" to "/internal-green-square-256.png"
And Logging in using web as "user0"
When logged in user posts avatar from internal path "internal-green-square-256.png"
When logged in user posts temporary avatar from internal path "internal-green-square-256.png"
And user "user0" gets avatar for user "user0" with size "64"
And The following headers should be set
| Content-Type | image/png |
@ -55,21 +78,82 @@ Feature: avatar
And last avatar is a square of size 64
And last avatar is a single "#00FF00" color
Scenario: delete user avatar
Scenario: set non-square user avatar from file
Given Logging in using web as "user0"
And logged in user posts avatar from file "data/green-square-256.png"
When logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
And logged in user crops temporary avatar
| x | 384 |
| y | 256 |
| w | 128 |
| h | 128 |
Then logged in user gets temporary avatar with 404
And user "user0" gets avatar for user "user0"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 512
And last avatar is a single "#00FF00" color
And last avatar is a single "#FF0000" color
And user "anonymous" gets avatar for user "user0"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 512
And last avatar is a single "#FF0000" color
Scenario: set non-square user avatar from internal path
Given user "user0" uploads file "data/coloured-pattern-non-square.png" to "/internal-coloured-pattern-non-square.png"
And Logging in using web as "user0"
When logged in user posts temporary avatar from internal path "internal-coloured-pattern-non-square.png"
And logged in user crops temporary avatar
| x | 704 |
| y | 320 |
| w | 64 |
| h | 64 |
Then logged in user gets temporary avatar with 404
And user "user0" gets avatar for user "user0" with size "64"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 64
And last avatar is a single "#00FF00" color
And user "anonymous" gets avatar for user "user0" with size "64"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 64
And last avatar is a single "#00FF00" color
Scenario: cropped user avatar needs to be squared
Given Logging in using web as "user0"
And logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
When logged in user crops temporary avatar with 400
| x | 384 |
| y | 256 |
| w | 192 |
| h | 128 |
Scenario: delete user avatar
Given Logging in using web as "user0"
And logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
And logged in user crops temporary avatar
| x | 384 |
| y | 256 |
| w | 128 |
| h | 128 |
And user "user0" gets avatar for user "user0"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 512
And last avatar is a single "#FF0000" color
And user "anonymous" gets avatar for user "user0"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 512
And last avatar is a single "#FF0000" color
When logged in user deletes the user avatar
Then user "user0" gets avatar for user "user0"
And The following headers should be set
@ -84,6 +168,40 @@ Feature: avatar
And last avatar is a square of size 512
And last avatar is not a single color
Scenario: get user avatar with a larger size than the original one
Given Logging in using web as "user0"
And logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
And logged in user crops temporary avatar
| x | 384 |
| y | 256 |
| w | 128 |
| h | 128 |
When user "user0" gets avatar for user "user0" with size "192"
Then The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 512
And last avatar is a single "#FF0000" color
Scenario: get user avatar with a smaller size than the original one
Given Logging in using web as "user0"
And logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
And logged in user crops temporary avatar
| x | 384 |
| y | 256 |
| w | 128 |
| h | 128 |
When user "user0" gets avatar for user "user0" with size "96"
Then The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 512
And last avatar is a single "#FF0000" color
Scenario: get default guest avatar
When user "user0" gets avatar for guest "guest0"
Then The following headers should be set

@ -4,6 +4,7 @@
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
use Behat\Gherkin\Node\TableNode;
use PHPUnit\Framework\Assert;
require __DIR__ . '/../../vendor/autoload.php';
@ -67,11 +68,30 @@ trait Avatar {
}
/**
* @When logged in user posts avatar from file :source
* @When logged in user gets temporary avatar
*/
public function loggedInUserGetsTemporaryAvatar() {
$this->loggedInUserGetsTemporaryAvatarWith('200');
}
/**
* @When logged in user gets temporary avatar with :statusCode
*
* @param string $statusCode
*/
public function loggedInUserGetsTemporaryAvatarWith(string $statusCode) {
$this->sendingAToWithRequesttoken('GET', '/index.php/avatar/tmp');
$this->theHTTPStatusCodeShouldBe($statusCode);
$this->getLastAvatar();
}
/**
* @When logged in user posts temporary avatar from file :source
*
* @param string $source
*/
public function loggedInUserPostsAvatarFromFile(string $source) {
public function loggedInUserPostsTemporaryAvatarFromFile(string $source) {
$file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r'));
$this->sendingAToWithRequesttoken('POST', '/index.php/avatar',
@ -87,15 +107,40 @@ trait Avatar {
}
/**
* @When logged in user posts avatar from internal path :path
* @When logged in user posts temporary avatar from internal path :path
*
* @param string $path
*/
public function loggedInUserPostsAvatarFromInternalPath(string $path) {
public function loggedInUserPostsTemporaryAvatarFromInternalPath(string $path) {
$this->sendingAToWithRequesttoken('POST', '/index.php/avatar?path=' . $path);
$this->theHTTPStatusCodeShouldBe('200');
}
/**
* @When logged in user crops temporary avatar
*
* @param TableNode $crop
*/
public function loggedInUserCropsTemporaryAvatar(TableNode $crop) {
$this->loggedInUserCropsTemporaryAvatarWith('200', $crop);
}
/**
* @When logged in user crops temporary avatar with :statusCode
*
* @param string $statusCode
* @param TableNode $crop
*/
public function loggedInUserCropsTemporaryAvatarWith(string $statusCode, TableNode $crop) {
$parameters = [];
foreach ($crop->getRowsHash() as $key => $value) {
$parameters[] = 'crop[' . $key . ']=' . $value;
}
$this->sendingAToWithRequesttoken('POST', '/index.php/avatar/cropped?' . implode('&', $parameters));
$this->theHTTPStatusCodeShouldBe($statusCode);
}
/**
* @When logged in user deletes the user avatar
*/

@ -46,7 +46,7 @@ Feature: sharing
Scenario: Correct webdav share-permissions for owned folder
Given user "user0" exists
And user "user0" created a folder "/tmp"
When as "user0" gets properties of folder "/tmp" with
When as "user0" gets properties of folder "/" with
|{http://open-collaboration-services.org/ns}share-permissions |
Then the single response should contain a property "{http://open-collaboration-services.org/ns}share-permissions" with value "31"

@ -3440,6 +3440,17 @@
<code><![CDATA[$this->providers]]></code>
</UndefinedInterfaceMethod>
</file>
<file src="lib/private/Cache/CappedMemoryCache.php">
<MissingTemplateParam>
<code><![CDATA[\ArrayAccess]]></code>
</MissingTemplateParam>
</file>
<file src="lib/private/Cache/File.php">
<LessSpecificImplementedReturnType>
<code><![CDATA[bool|mixed]]></code>
<code><![CDATA[bool|mixed]]></code>
</LessSpecificImplementedReturnType>
</file>
<file src="lib/private/Calendar/Manager.php">
<LessSpecificReturnStatement>
<code><![CDATA[array_merge(

@ -8,6 +8,7 @@
namespace OC\Core\Controller;
use OC\AppFramework\Utility\TimeFactory;
use OC\NotSquareException;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
@ -15,6 +16,7 @@ use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
@ -22,6 +24,7 @@ use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\Files\NotPermittedException;
use OCP\IAvatarManager;
use OCP\ICache;
use OCP\IL10N;
use OCP\Image;
use OCP\IRequest;
@ -38,6 +41,7 @@ class AvatarController extends Controller {
string $appName,
IRequest $request,
protected IAvatarManager $avatarManager,
protected ICache $cache,
protected IL10N $l10n,
protected IUserManager $userManager,
protected IRootFolder $rootFolder,
@ -198,7 +202,8 @@ class AvatarController extends Controller {
Http::STATUS_BAD_REQUEST
);
}
$content = file_get_contents($files['tmp_name'][0]);
$this->cache->set('avatar_upload', file_get_contents($files['tmp_name'][0]), 7200);
$content = $this->cache->get('avatar_upload');
unlink($files['tmp_name'][0]);
} else {
$phpFileUploadErrors = [
@ -245,6 +250,8 @@ class AvatarController extends Controller {
try {
$avatar = $this->avatarManager->getAvatar($this->userId);
$avatar->set($image);
// Clean up
$this->cache->remove('tmpAvatar');
return new JSONResponse(['status' => 'success']);
} catch (\Throwable $e) {
$this->logger->error($e->getMessage(), ['exception' => $e, 'app' => 'core']);
@ -252,8 +259,9 @@ class AvatarController extends Controller {
}
}
$this->cache->set('tmpAvatar', $image->data(), 7200);
return new JSONResponse(
['data' => 'notsquare', 'image' => 'data:' . $mimeType . ';base64,' . base64_encode($image->data())],
['data' => 'notsquare'],
Http::STATUS_OK
);
} else {
@ -280,4 +288,71 @@ class AvatarController extends Controller {
return new JSONResponse(['data' => ['message' => $this->l10n->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST);
}
}
/**
* @return JSONResponse|DataDisplayResponse
*/
#[NoAdminRequired]
#[FrontpageRoute(verb: 'GET', url: '/avatar/tmp')]
public function getTmpAvatar() {
$tmpAvatar = $this->cache->get('tmpAvatar');
if (is_null($tmpAvatar)) {
return new JSONResponse(['data' => [
'message' => $this->l10n->t('No temporary profile picture available, try again')
]],
Http::STATUS_NOT_FOUND);
}
$image = new Image();
$image->loadFromData($tmpAvatar);
$resp = new DataDisplayResponse(
$image->data() ?? '',
Http::STATUS_OK,
['Content-Type' => $image->mimeType()]);
$resp->setETag((string)crc32($image->data() ?? ''));
$resp->cacheFor(0);
$resp->setLastModified(new \DateTime('now', new \DateTimeZone('GMT')));
return $resp;
}
#[NoAdminRequired]
#[FrontpageRoute(verb: 'POST', url: '/avatar/cropped')]
public function postCroppedAvatar(?array $crop = null): JSONResponse {
if (is_null($crop)) {
return new JSONResponse(['data' => ['message' => $this->l10n->t('No crop data provided')]],
Http::STATUS_BAD_REQUEST);
}
if (!isset($crop['x'], $crop['y'], $crop['w'], $crop['h'])) {
return new JSONResponse(['data' => ['message' => $this->l10n->t('No valid crop data provided')]],
Http::STATUS_BAD_REQUEST);
}
$tmpAvatar = $this->cache->get('tmpAvatar');
if (is_null($tmpAvatar)) {
return new JSONResponse(['data' => [
'message' => $this->l10n->t('No temporary profile picture available, try again')
]],
Http::STATUS_BAD_REQUEST);
}
$image = new Image();
$image->loadFromData($tmpAvatar);
$image->crop($crop['x'], $crop['y'], (int)round($crop['w']), (int)round($crop['h']));
try {
$avatar = $this->avatarManager->getAvatar($this->userId);
$avatar->set($image);
// Clean up
$this->cache->remove('tmpAvatar');
return new JSONResponse(['status' => 'success']);
} catch (NotSquareException $e) {
return new JSONResponse(['data' => ['message' => $this->l10n->t('Crop is not square')]],
Http::STATUS_BAD_REQUEST);
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e, 'app' => 'core']);
return new JSONResponse(['data' => ['message' => $this->l10n->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST);
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -882,6 +882,23 @@ class OC {
$throttler = Server::get(IThrottler::class);
$throttler->resetDelay($request->getRemoteAddress(), 'login', ['user' => $uid]);
}
try {
$cache = new \OC\Cache\File();
$cache->gc();
} catch (\OC\ServerNotAvailableException $e) {
// not a GC exception, pass it on
throw $e;
} catch (\OC\ForbiddenException $e) {
// filesystem blocked for this request, ignore
} catch (\Exception $e) {
// a GC exception should not prevent users from using OC,
// so log the exception
Server::get(LoggerInterface::class)->warning('Exception when running cache gc.', [
'app' => 'core',
'exception' => $e,
]);
}
});
}
}

@ -1183,6 +1183,8 @@ return array(
'OC\\BinaryFinder' => $baseDir . '/lib/private/BinaryFinder.php',
'OC\\Blurhash\\Listener\\GenerateBlurhashMetadata' => $baseDir . '/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php',
'OC\\Broadcast\\Events\\BroadcastEvent' => $baseDir . '/lib/private/Broadcast/Events/BroadcastEvent.php',
'OC\\Cache\\CappedMemoryCache' => $baseDir . '/lib/private/Cache/CappedMemoryCache.php',
'OC\\Cache\\File' => $baseDir . '/lib/private/Cache/File.php',
'OC\\Calendar\\AvailabilityResult' => $baseDir . '/lib/private/Calendar/AvailabilityResult.php',
'OC\\Calendar\\CalendarEventBuilder' => $baseDir . '/lib/private/Calendar/CalendarEventBuilder.php',
'OC\\Calendar\\CalendarQuery' => $baseDir . '/lib/private/Calendar/CalendarQuery.php',

@ -1224,6 +1224,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\BinaryFinder' => __DIR__ . '/../../..' . '/lib/private/BinaryFinder.php',
'OC\\Blurhash\\Listener\\GenerateBlurhashMetadata' => __DIR__ . '/../../..' . '/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php',
'OC\\Broadcast\\Events\\BroadcastEvent' => __DIR__ . '/../../..' . '/lib/private/Broadcast/Events/BroadcastEvent.php',
'OC\\Cache\\CappedMemoryCache' => __DIR__ . '/../../..' . '/lib/private/Cache/CappedMemoryCache.php',
'OC\\Cache\\File' => __DIR__ . '/../../..' . '/lib/private/Cache/File.php',
'OC\\Calendar\\AvailabilityResult' => __DIR__ . '/../../..' . '/lib/private/Calendar/AvailabilityResult.php',
'OC\\Calendar\\CalendarEventBuilder' => __DIR__ . '/../../..' . '/lib/private/Calendar/CalendarEventBuilder.php',
'OC\\Calendar\\CalendarQuery' => __DIR__ . '/../../..' . '/lib/private/Calendar/CalendarQuery.php',

@ -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;
}
}

@ -585,16 +585,7 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerAlias(IURLGenerator::class, URLGenerator::class);
$this->registerService(ICache::class, function ($c) {
/** @var LoggerInterface $logger */
$logger = $c->get(LoggerInterface::class);
$logger->debug('The requested service "' . ICache::class . '" is deprecated. Please use "' . ICacheFactory::class . '" instead to create a cache. This service will be removed in a future Nextcloud version.', ['app' => 'serverDI']);
/** @var ICacheFactory $cacheFactory */
$cacheFactory = $c->get(ICacheFactory::class);
return $cacheFactory->isLocalCacheAvailable() ? $cacheFactory->createLocal() : $cacheFactory->createInMemory();
});
$this->registerAlias(ICache::class, Cache\File::class);
$this->registerService(Factory::class, function (Server $c) {
$profiler = $c->get(IProfiler::class);
$logger = $c->get(LoggerInterface::class);

@ -29,6 +29,7 @@ use OCP\Files\NotPermittedException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\IAvatar;
use OCP\IAvatarManager;
use OCP\ICache;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IUser;
@ -54,6 +55,8 @@ class AvatarControllerTest extends \Test\TestCase {
private $avatarFile;
/** @var IAvatarManager|\PHPUnit\Framework\MockObject\MockObject */
private $avatarManager;
/** @var ICache|\PHPUnit\Framework\MockObject\MockObject */
private $cache;
/** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
private $l;
/** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
@ -71,6 +74,8 @@ class AvatarControllerTest extends \Test\TestCase {
parent::setUp();
$this->avatarManager = $this->getMockBuilder('OCP\IAvatarManager')->getMock();
$this->cache = $this->getMockBuilder('OCP\ICache')
->disableOriginalConstructor()->getMock();
$this->l = $this->getMockBuilder(IL10N::class)->getMock();
$this->l->method('t')->willReturnArgument(0);
$this->userManager = $this->getMockBuilder(IUserManager::class)->getMock();
@ -93,6 +98,7 @@ class AvatarControllerTest extends \Test\TestCase {
'core',
$this->request,
$this->avatarManager,
$this->cache,
$this->l,
$this->userManager,
$this->rootFolder,
@ -292,6 +298,25 @@ class AvatarControllerTest extends \Test\TestCase {
$this->assertEquals($expectedResponse, $this->avatarController->deleteAvatar());
}
/**
* Trying to get a tmp avatar when it is not available. 404
*/
public function testTmpAvatarNoTmp(): void {
$response = $this->avatarController->getTmpAvatar();
$this->assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus());
}
/**
* Fetch tmp avatar
*/
public function testTmpAvatarValid(): void {
$this->cache->method('get')->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg'));
$response = $this->avatarController->getTmpAvatar();
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
}
/**
* When trying to post a new avatar a path or image should be posted.
*/
@ -310,6 +335,9 @@ class AvatarControllerTest extends \Test\TestCase {
$copyRes = copy(\OC::$SERVERROOT . '/tests/data/testimage.jpg', $fileName);
$this->assertTrue($copyRes);
//Create file in cache
$this->cache->method('get')->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg'));
//Create request return
$reqRet = ['error' => [0], 'tmp_name' => [$fileName], 'size' => [filesize(\OC::$SERVERROOT . '/tests/data/testimage.jpg')]];
$this->request->method('getUploadedFile')->willReturn($reqRet);
@ -345,6 +373,9 @@ class AvatarControllerTest extends \Test\TestCase {
$copyRes = copy(\OC::$SERVERROOT . '/tests/data/testimage.gif', $fileName);
$this->assertTrue($copyRes);
//Create file in cache
$this->cache->method('get')->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.gif'));
//Create request return
$reqRet = ['error' => [0], 'tmp_name' => [$fileName], 'size' => [filesize(\OC::$SERVERROOT . '/tests/data/testimage.gif')]];
$this->request->method('getUploadedFile')->willReturn($reqRet);
@ -433,6 +464,93 @@ class AvatarControllerTest extends \Test\TestCase {
$this->assertEquals($expectedResponse, $this->avatarController->postAvatar('avatar.jpg'));
}
/**
* Test what happens if the upload of the avatar fails
*/
public function testPostAvatarException(): void {
$this->cache->expects($this->once())
->method('set')
->willThrowException(new \Exception('foo'));
$file = $this->getMockBuilder('OCP\Files\File')
->disableOriginalConstructor()->getMock();
$file->expects($this->once())
->method('getContent')
->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg'));
$file->expects($this->once())
->method('getMimeType')
->willReturn('image/jpeg');
$userFolder = $this->getMockBuilder('OCP\Files\Folder')->getMock();
$this->rootFolder->method('getUserFolder')->with('userid')->willReturn($userFolder);
$userFolder->method('get')->willReturn($file);
$this->logger->expects($this->once())
->method('error')
->with('foo', ['exception' => new \Exception('foo'), 'app' => 'core']);
$expectedResponse = new Http\JSONResponse(['data' => ['message' => 'An error occurred. Please contact your admin.']], Http::STATUS_OK);
$this->assertEquals($expectedResponse, $this->avatarController->postAvatar('avatar.jpg'));
}
/**
* Test invalid crop argument
*/
public function testPostCroppedAvatarInvalidCrop(): void {
$response = $this->avatarController->postCroppedAvatar([]);
$this->assertEquals(Http::STATUS_BAD_REQUEST, $response->getStatus());
}
/**
* Test no tmp avatar to crop
*/
public function testPostCroppedAvatarNoTmpAvatar(): void {
$response = $this->avatarController->postCroppedAvatar(['x' => 0, 'y' => 0, 'w' => 10, 'h' => 10]);
$this->assertEquals(Http::STATUS_BAD_REQUEST, $response->getStatus());
}
/**
* Test with non square crop
*/
public function testPostCroppedAvatarNoSquareCrop(): void {
$this->cache->method('get')->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg'));
$this->avatarMock->method('set')->willThrowException(new \OC\NotSquareException);
$this->avatarManager->method('getAvatar')->willReturn($this->avatarMock);
$response = $this->avatarController->postCroppedAvatar(['x' => 0, 'y' => 0, 'w' => 10, 'h' => 11]);
$this->assertEquals(Http::STATUS_BAD_REQUEST, $response->getStatus());
}
/**
* Check for proper reply on proper crop argument
*/
public function testPostCroppedAvatarValidCrop(): void {
$this->cache->method('get')->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg'));
$this->avatarManager->method('getAvatar')->willReturn($this->avatarMock);
$response = $this->avatarController->postCroppedAvatar(['x' => 0, 'y' => 0, 'w' => 10, 'h' => 10]);
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
$this->assertEquals('success', $response->getData()['status']);
}
/**
* Test what happens if the cropping of the avatar fails
*/
public function testPostCroppedAvatarException(): void {
$this->cache->method('get')->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg'));
$this->avatarMock->method('set')->willThrowException(new \Exception('foo'));
$this->avatarManager->method('getAvatar')->willReturn($this->avatarMock);
$this->logger->expects($this->once())
->method('error')
->with('foo', ['exception' => new \Exception('foo'), 'app' => 'core']);
$expectedResponse = new Http\JSONResponse(['data' => ['message' => 'An error occurred. Please contact your admin.']], Http::STATUS_BAD_REQUEST);
$this->assertEquals($expectedResponse, $this->avatarController->postCroppedAvatar(['x' => 0, 'y' => 0, 'w' => 10, 'h' => 11]));
}
/**
* Check for proper reply on proper crop argument
*/

@ -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();
}
}