Skip to content
Open
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
58 changes: 58 additions & 0 deletions core/BackgroundJobs/FileCacheGcJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OC\Core\BackgroundJobs;

use OC\Cache\File;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\IAppConfig;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;

class FileCacheGcJob extends TimedJob {
public function __construct(
ITimeFactory $time,
private readonly LoggerInterface $logger,
private readonly IAppConfig $appConfig,
private readonly IUserManager $userManager,
) {
parent::__construct($time);

$this->setTimeSensitivity(self::TIME_INSENSITIVE);
$this->setInterval(24 * 60 * 60);
}

protected function run(mixed $argument): void {
$offset = $this->appConfig->getValueInt('core', 'files_gc_offset');

$users = $this->userManager->getSeenUsers($offset);
$start = time();
$count = 0;
foreach ($users as $user) {
$cache = new File();
try {
$cache->gc($user);
} catch (\Exception $e) {
$this->logger->warning('Exception when running cache gc.', [
'app' => 'core',
'exception' => $e,
]);
}
$count++;
$now = time();

// almost time for the next job run, stop early and save our location
if ($now - $start > 23 * 60 * 60) {
$this->appConfig->setValueInt('core', 'files_gc_offset', $offset + $count);
return;
}
}
$this->appConfig->setValueInt('core', 'files_gc_offset', 0);
}
}
17 changes: 0 additions & 17 deletions lib/base.php
Original file line number Diff line number Diff line change
Expand Up @@ -874,23 +874,6 @@ public static function registerCleanupHooks(\OC\SystemConfig $systemConfig): voi
$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,
]);
}
});
}
}
Expand Down
2 changes: 2 additions & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,7 @@
'OC\\Core\\BackgroundJobs\\BackgroundCleanupUpdaterBackupsJob' => $baseDir . '/core/BackgroundJobs/BackgroundCleanupUpdaterBackupsJob.php',
'OC\\Core\\BackgroundJobs\\CheckForUserCertificates' => $baseDir . '/core/BackgroundJobs/CheckForUserCertificates.php',
'OC\\Core\\BackgroundJobs\\CleanupLoginFlowV2' => $baseDir . '/core/BackgroundJobs/CleanupLoginFlowV2.php',
'OC\\Core\\BackgroundJobs\\FileCacheGcJob' => $baseDir . '/core/BackgroundJobs/FileCacheGcJob.php',
'OC\\Core\\BackgroundJobs\\GenerateMetadataJob' => $baseDir . '/core/BackgroundJobs/GenerateMetadataJob.php',
'OC\\Core\\BackgroundJobs\\LookupServerSendCheckBackgroundJob' => $baseDir . '/core/BackgroundJobs/LookupServerSendCheckBackgroundJob.php',
'OC\\Core\\Command\\App\\Disable' => $baseDir . '/core/Command/App/Disable.php',
Expand Down Expand Up @@ -1903,6 +1904,7 @@
'OC\\Repair\\NC29\\SanitizeAccountProperties' => $baseDir . '/lib/private/Repair/NC29/SanitizeAccountProperties.php',
'OC\\Repair\\NC29\\SanitizeAccountPropertiesJob' => $baseDir . '/lib/private/Repair/NC29/SanitizeAccountPropertiesJob.php',
'OC\\Repair\\NC30\\RemoveLegacyDatadirFile' => $baseDir . '/lib/private/Repair/NC30/RemoveLegacyDatadirFile.php',
'OC\\Repair\\NC32\\AddFileCacheGcBackgroundJob' => $baseDir . '/lib/private/Repair/NC32/AddFileCacheGcBackgroundJob.php',
'OC\\Repair\\OldGroupMembershipShares' => $baseDir . '/lib/private/Repair/OldGroupMembershipShares.php',
'OC\\Repair\\Owncloud\\CleanPreviews' => $baseDir . '/lib/private/Repair/Owncloud/CleanPreviews.php',
'OC\\Repair\\Owncloud\\CleanPreviewsBackgroundJob' => $baseDir . '/lib/private/Repair/Owncloud/CleanPreviewsBackgroundJob.php',
Expand Down
2 changes: 2 additions & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\BackgroundJobs\\BackgroundCleanupUpdaterBackupsJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/BackgroundCleanupUpdaterBackupsJob.php',
'OC\\Core\\BackgroundJobs\\CheckForUserCertificates' => __DIR__ . '/../../..' . '/core/BackgroundJobs/CheckForUserCertificates.php',
'OC\\Core\\BackgroundJobs\\CleanupLoginFlowV2' => __DIR__ . '/../../..' . '/core/BackgroundJobs/CleanupLoginFlowV2.php',
'OC\\Core\\BackgroundJobs\\FileCacheGcJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/FileCacheGcJob.php',
'OC\\Core\\BackgroundJobs\\GenerateMetadataJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/GenerateMetadataJob.php',
'OC\\Core\\BackgroundJobs\\LookupServerSendCheckBackgroundJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/LookupServerSendCheckBackgroundJob.php',
'OC\\Core\\Command\\App\\Disable' => __DIR__ . '/../../..' . '/core/Command/App/Disable.php',
Expand Down Expand Up @@ -1952,6 +1953,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Repair\\NC29\\SanitizeAccountProperties' => __DIR__ . '/../../..' . '/lib/private/Repair/NC29/SanitizeAccountProperties.php',
'OC\\Repair\\NC29\\SanitizeAccountPropertiesJob' => __DIR__ . '/../../..' . '/lib/private/Repair/NC29/SanitizeAccountPropertiesJob.php',
'OC\\Repair\\NC30\\RemoveLegacyDatadirFile' => __DIR__ . '/../../..' . '/lib/private/Repair/NC30/RemoveLegacyDatadirFile.php',
'OC\\Repair\\NC32\\AddFileCacheGcBackgroundJob' => __DIR__ . '/../../..' . '/lib/private/Repair/NC32/AddFileCacheGcBackgroundJob.php',
'OC\\Repair\\OldGroupMembershipShares' => __DIR__ . '/../../..' . '/lib/private/Repair/OldGroupMembershipShares.php',
'OC\\Repair\\Owncloud\\CleanPreviews' => __DIR__ . '/../../..' . '/lib/private/Repair/Owncloud/CleanPreviews.php',
'OC\\Repair\\Owncloud\\CleanPreviewsBackgroundJob' => __DIR__ . '/../../..' . '/lib/private/Repair/Owncloud/CleanPreviewsBackgroundJob.php',
Expand Down
142 changes: 69 additions & 73 deletions lib/private/Cache/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,47 @@
*/
namespace OC\Cache;

use OC\Files\Filesystem;
use OC\Files\View;
use OCP\Files\File as FileNode;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\ICache;
use OCP\IUser;
use OCP\IUserSession;
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 \OC\Files\View cache storage
* @return Folder cache folder
* @throws \OC\ForbiddenException
* @throws \OC\User\NoUserException
*/
protected function getStorage() {
protected function getStorage(?IUser $user = null): Folder {
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');
if (!$user) {
$session = Server::get(IUserSession::class);
$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']);
Expand All @@ -52,27 +61,29 @@ protected function getStorage() {
* @throws \OC\ForbiddenException
*/
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;
}

/**
Expand All @@ -94,14 +105,14 @@ public function set($key, $value, $ttl = 0) {
// 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);
$file = $storage->newFile($keyPart, $value);
if ($ttl === 0) {
$ttl = 86400; // 60*60*24
}
return $result;
$file->touch(time() + $ttl);
$file->move($storage->getFullPath($key));

return true;
}

/**
Expand All @@ -110,11 +121,7 @@ public function set($key, $value, $ttl = 0) {
* @throws \OC\ForbiddenException
*/
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);
}

/**
Expand All @@ -124,10 +131,12 @@ public function hasKey($key) {
*/
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);
}

/**
Expand All @@ -137,14 +146,9 @@ public function remove($key) {
*/
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;
Expand All @@ -154,32 +158,24 @@ public function clear($prefix = '') {
* 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 function gc(?IUser $user = null) {
$storage = $this->getStorage($user);
// 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;
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']);
}
}
}
Expand Down
29 changes: 29 additions & 0 deletions lib/private/Repair/NC32/AddFileCacheGcBackgroundJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Repair\NC32;

use OC\Core\BackgroundJobs\FileCacheGcJob;
use OCP\BackgroundJob\IJobList;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;

class AddFileCacheGcBackgroundJob implements IRepairStep {
public function __construct(
private readonly IJobList $jobList,
) {
}

public function getName(): string {
return 'Add background job to cleanup file cache';
}

public function run(IOutput $output) {
$this->jobList->add(FileCacheGcJob::class);
}
}
Loading
Loading