refactor(trashbin): Move some parts of Trashbin to reusable services

Signed-off-by: Carl Schwan <carl.schwan@nextcloud.com>
This commit is contained in:
Carl Schwan 2025-09-01 20:18:08 +02:00
parent 6ee28229d5
commit 042a773eba
14 changed files with 453 additions and 266 deletions

View file

@ -42,6 +42,8 @@ return array(
'OCA\\Files_Trashbin\\Sabre\\TrashRoot' => $baseDir . '/../lib/Sabre/TrashRoot.php',
'OCA\\Files_Trashbin\\Sabre\\TrashbinPlugin' => $baseDir . '/../lib/Sabre/TrashbinPlugin.php',
'OCA\\Files_Trashbin\\Service\\ConfigService' => $baseDir . '/../lib/Service/ConfigService.php',
'OCA\\Files_Trashbin\\Service\\ExpireService' => $baseDir . '/../lib/Service/ExpireService.php',
'OCA\\Files_Trashbin\\Service\\TrashFolderService' => $baseDir . '/../lib/Service/TrashFolderService.php',
'OCA\\Files_Trashbin\\Storage' => $baseDir . '/../lib/Storage.php',
'OCA\\Files_Trashbin\\Trash\\BackendNotFoundException' => $baseDir . '/../lib/Trash/BackendNotFoundException.php',
'OCA\\Files_Trashbin\\Trash\\ITrashBackend' => $baseDir . '/../lib/Trash/ITrashBackend.php',

View file

@ -7,14 +7,14 @@ namespace Composer\Autoload;
class ComposerStaticInitFiles_Trashbin
{
public static $prefixLengthsPsr4 = array (
'O' =>
'O' =>
array (
'OCA\\Files_Trashbin\\' => 19,
),
);
public static $prefixDirsPsr4 = array (
'OCA\\Files_Trashbin\\' =>
'OCA\\Files_Trashbin\\' =>
array (
0 => __DIR__ . '/..' . '/../lib',
),
@ -57,6 +57,8 @@ class ComposerStaticInitFiles_Trashbin
'OCA\\Files_Trashbin\\Sabre\\TrashRoot' => __DIR__ . '/..' . '/../lib/Sabre/TrashRoot.php',
'OCA\\Files_Trashbin\\Sabre\\TrashbinPlugin' => __DIR__ . '/..' . '/../lib/Sabre/TrashbinPlugin.php',
'OCA\\Files_Trashbin\\Service\\ConfigService' => __DIR__ . '/..' . '/../lib/Service/ConfigService.php',
'OCA\\Files_Trashbin\\Service\\ExpireService' => __DIR__ . '/..' . '/../lib/Service/ExpireService.php',
'OCA\\Files_Trashbin\\Service\\TrashFolderService' => __DIR__ . '/..' . '/../lib/Service/TrashFolderService.php',
'OCA\\Files_Trashbin\\Storage' => __DIR__ . '/..' . '/../lib/Storage.php',
'OCA\\Files_Trashbin\\Trash\\BackendNotFoundException' => __DIR__ . '/..' . '/../lib/Trash/BackendNotFoundException.php',
'OCA\\Files_Trashbin\\Trash\\ITrashBackend' => __DIR__ . '/..' . '/../lib/Trash/ITrashBackend.php',

View file

@ -7,10 +7,9 @@
*/
namespace OCA\Files_Trashbin\BackgroundJob;
use OC\Files\View;
use OC\Files\SetupManager;
use OCA\Files_Trashbin\Expiration;
use OCA\Files_Trashbin\Helper;
use OCA\Files_Trashbin\Trashbin;
use OCA\Files_Trashbin\Service\ExpireService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\IAppConfig;
@ -19,10 +18,12 @@ use Psr\Log\LoggerInterface;
class ExpireTrash extends TimedJob {
public function __construct(
private IAppConfig $appConfig,
private IUserManager $userManager,
private Expiration $expiration,
private LoggerInterface $logger,
readonly private IAppConfig $appConfig,
readonly private IUserManager $userManager,
readonly private Expiration $expiration,
readonly private ExpireService $expireService,
readonly private SetupManager $setupManager,
readonly private LoggerInterface $logger,
ITimeFactory $time,
) {
parent::__construct($time);
@ -47,12 +48,7 @@ class ExpireTrash extends TimedJob {
foreach ($users as $user) {
try {
$uid = $user->getUID();
if (!$this->setupFS($uid)) {
continue;
}
$dirContent = Helper::getTrashFiles('/', $uid, 'mtime');
Trashbin::deleteExpiredFiles($dirContent, $uid);
$this->expireService->expireTrashForUser($user);
} catch (\Throwable $e) {
$this->logger->error('Error while expiring trashbin for user ' . $user->getUID(), ['exception' => $e]);
}
@ -61,28 +57,12 @@ class ExpireTrash extends TimedJob {
if ($stopTime < time()) {
$this->appConfig->setValueInt('files_trashbin', 'background_job_expire_trash_offset', $offset);
\OC_Util::tearDownFS();
$this->setupManager->tearDown();
return;
}
}
$this->appConfig->setValueInt('files_trashbin', 'background_job_expire_trash_offset', 0);
\OC_Util::tearDownFS();
}
/**
* Act on behalf on trash item owner
*/
protected function setupFS(string $user): bool {
\OC_Util::tearDownFS();
\OC_Util::setupFS($user);
// Check if this user has a trashbin directory
$view = new View('/' . $user);
if (!$view->is_dir('/files_trashbin/files')) {
return false;
}
return true;
$this->setupManager->tearDown();
}
}

View file

@ -8,32 +8,33 @@
namespace OCA\Files_Trashbin\Command;
use OC\Command\FileAccess;
use OCA\Files_Trashbin\Trashbin;
use OC\Files\SetupManager;
use OCA\Encryption\Users\Setup;
use OCA\Files_Trashbin\Service\ExpireService;
use OCP\Command\ICommand;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Server;
use Psr\Log\LoggerInterface;
class Expire implements ICommand {
use FileAccess;
/**
* @param string $user
*/
public function __construct(
private $user,
readonly private string $user,
) {
}
public function handle() {
$userManager = Server::get(IUserManager::class);
if (!$userManager->userExists($this->user)) {
// User has been deleted already
return;
public function handle(): void {
try {
$user = Server::get(IUserManager::class)->get($this->user);
if (!$user) {
return;
}
Server::get(ExpireService::class)->expireTrashForUser($user);
Server::get(SetupManager::class)->execute();
} catch (\Throwable $e) {
Server::get(LoggerInterface::class)->error('Error while expiring trashbin for user ' . $this->user, ['exception' => $e]);
}
\OC_Util::tearDownFS();
\OC_Util::setupFS($this->user);
Trashbin::expire($this->user);
\OC_Util::tearDownFS();
}
}

View file

@ -7,12 +7,11 @@
*/
namespace OCA\Files_Trashbin\Command;
use OC\Files\View;
use OC\Files\SetupManager;
use OCA\Files_Trashbin\Expiration;
use OCA\Files_Trashbin\Trashbin;
use OCP\IUser;
use OCA\Files_Trashbin\Service\ExpireService;
use OCP\Files\IRootFolder;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputArgument;
@ -20,15 +19,12 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ExpireTrash extends Command {
/**
* @param IUserManager|null $userManager
* @param Expiration|null $expiration
*/
public function __construct(
private LoggerInterface $logger,
private ?IUserManager $userManager = null,
private ?Expiration $expiration = null,
readonly SetupManager $setupManager,
readonly IRootFolder $rootFolder,
readonly private IUserManager $userManager,
readonly private Expiration $expiration,
readonly private ExpireService $expireService,
) {
parent::__construct();
}
@ -55,10 +51,11 @@ class ExpireTrash extends Command {
$users = $input->getArgument('user_id');
if (!empty($users)) {
foreach ($users as $user) {
if ($this->userManager->userExists($user)) {
$output->writeln("Remove deleted files of <info>$user</info>");
$userObject = $this->userManager->get($user);
$this->expireTrashForUser($userObject);
$userObject = $this->userManager->get($user);
if ($userObject) {
$output->writeln("Remove deleted files of <info>$user</info>");
$this->expireService->expireTrashForUser($userObject);
$this->setupManager->tearDown();
} else {
$output->writeln("<error>Unknown user $user</error>");
return 1;
@ -71,41 +68,18 @@ class ExpireTrash extends Command {
$users = $this->userManager->getSeenUsers();
foreach ($users as $user) {
$p->advance();
$this->expireTrashForUser($user);
try {
$this->expireService->expireTrashForUser($user);
$this->setupManager->tearDown();
} catch (\Throwable $e) {
$displayName = $user->getDisplayName();
$output->writeln("<error>Error while expiring trashbin for user $displayName</error>");
throw $e;
}
}
$p->finish();
$output->writeln('');
}
return 0;
}
public function expireTrashForUser(IUser $user) {
try {
$uid = $user->getUID();
if (!$this->setupFS($uid)) {
return;
}
Trashbin::expire($uid);
} catch (\Throwable $e) {
$this->logger->error('Error while expiring trashbin for user ' . $user->getUID(), ['exception' => $e]);
}
}
/**
* Act on behalf on trash item owner
* @param string $user
* @return boolean
*/
protected function setupFS($user) {
\OC_Util::tearDownFS();
\OC_Util::setupFS($user);
// Check if this user has a trashbin directory
$view = new View('/' . $user);
if (!$view->is_dir('/files_trashbin/files')) {
return false;
}
return true;
}
}

View file

@ -9,11 +9,14 @@ declare(strict_types=1);
namespace OCA\Files_Trashbin\Command;
use OC\Core\Command\Base;
use OC\Files\SetupManager;
use OCA\Files_Trashbin\Service\ExpireService;
use OCP\Command\IBus;
use OCP\IConfig;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Util;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@ -21,9 +24,9 @@ use Symfony\Component\Console\Output\OutputInterface;
class Size extends Base {
public function __construct(
private IConfig $config,
private IUserManager $userManager,
private IBus $commandBus,
readonly private IConfig $config,
readonly private IUserManager $userManager,
readonly private ExpireService $expireService,
) {
parent::__construct();
}
@ -53,7 +56,10 @@ class Size extends Base {
}
if ($user) {
$this->config->setUserValue($user, 'files_trashbin', 'trashbin_size', (string)$parsedSize);
$this->commandBus->push(new Expire($user));
$userObject = $this->userManager->get($user);
if ($userObject) {
$this->expireService->scheduleExpirationJob($userObject);
}
} else {
$this->config->setAppValue('files_trashbin', 'trashbin_size', (string)$parsedSize);
$output->writeln('<info>Warning: changing the default trashbin size will automatically trigger cleanup of existing trashbins,</info>');

View file

@ -9,18 +9,23 @@ declare(strict_types=1);
namespace OCA\Files_Trashbin\Listener;
use OCA\Files_Trashbin\Service\ExpireService;
use OCA\Files_Trashbin\Storage;
use OCA\Files_Trashbin\Trashbin;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\Events\BeforeFileSystemSetupEvent;
use OCP\Files\Events\Node\NodeWrittenEvent;
use OCP\IUser;
use OCP\IUserManager;
use OCP\User\Events\BeforeUserDeletedEvent;
/** @template-implements IEventListener<NodeWrittenEvent|BeforeUserDeletedEvent|BeforeFileSystemSetupEvent> */
class EventListener implements IEventListener {
public function __construct(
private ?string $userId = null,
readonly private ExpireService $expireService,
readonly private IUserManager $userManager,
readonly private ?string $userId = null,
) {
}
@ -28,7 +33,10 @@ class EventListener implements IEventListener {
if ($event instanceof NodeWrittenEvent) {
// Resize trash
if (!empty($this->userId)) {
Trashbin::resizeTrash($this->userId);
$user = $this->userManager->get($this->userId);
if ($user) {
$this->expireService->scheduleExpirationJobIfNeeded($user);
}
}
}

View file

@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files_Trashbin\Service;
use OCA\Files_Trashbin\Command\Expire;
use OCA\Files_Trashbin\Expiration;
use OCA\Files_Trashbin\Trashbin;
use OCP\Command\IBus;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IDBConnection;
use OCP\IUser;
use OCP\Server;
use Psr\Log\LoggerInterface;
class ExpireService {
public function __construct(
readonly private TrashFolderService $trashFolderService,
readonly private Expiration $expiration,
readonly private IBus $ibus,
readonly private LoggerInterface $logger,
) {
}
public function expireTrashForUser(IUser $user): void {
$trashFolderRoot = $this->trashFolderService->getTrashFolderRoot($user);
if (!$trashFolderRoot) {
return;
}
$availableSpace = $this->trashFolderService->getAvailableSpace($trashFolderRoot, $user);
try {
/** @var Folder $trashFolder */
$trashFolder = $trashFolderRoot->get('files');
} catch (NotFoundException) {
echo "bug";
return; // Nothing to expire
}
$nodes = $trashFolder->getDirectoryListing();
usort($nodes, fn (Node $a, Node $b): int => $a->getMTime() <=> $b->getMTime());
// delete all files older then $retention_obligation
[$delSize, $count] = $this->deleteExpiredNodes($trashFolder, $nodes, $user);
$availableSpace += $delSize;
// delete files from trash until we meet the trash bin size limit again
Trashbin::deleteNodes(array_slice($nodes, $count), $user, $availableSpace);
}
public function scheduleExpirationJobIfNeeded(IUser $user): void {
$trashFolderRoot = $this->trashFolderService->getTrashFolderRoot($user);
if (!$trashFolderRoot) {
return;
}
$freeSpace = $this->trashFolderService->getAvailableSpace($trashFolderRoot, $user);
if ($freeSpace < 0) {
$this->scheduleExpirationJob($user);
}
}
public function scheduleExpirationJob(IUser $user): void {
// let the admin disable auto expire
if ($this->expiration->isEnabled()) {
$this->ibus->push(new Expire($user->getUID()));
}
}
/**
* @param Node[] $nodes
*/
private function deleteExpiredNodes(Folder $trashFolder, array $nodes, IUser $user) {
/** @var Expiration $expiration */
$expiration = Server::get(Expiration::class);
$size = 0;
$count = 0;
foreach ($nodes as $node) {
$timestamp = $node->getMTime();
if (!$expiration->isExpired($timestamp)) {
break; // Since the nodes are sorted by mtime, we can already abord
}
try {
$size += $this->trashFolderService->delete($trashFolder, $node, $user, $timestamp);
$count++;
} catch (NotPermittedException $e) {
$this->logger->warning('Removing "' . $node->getName() . '" from trashbin failed for user "{user}"',
[
'exception' => $e,
'app' => 'files_trashbin',
'user' => $user,
]
);
continue;
}
$this->logger->info(
'Remove "' . $node->getName() . '" from trashbin for user "{user}" because it exceeds max retention obligation term.',
[
'app' => 'files_trashbin',
'user' => $user,
],
);
}
return [$size, $count];
}
}

View file

@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files_Trashbin\Service;
use OCA\Files_Trashbin\Trashbin;
use OCP\App\IAppManager;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\IDBConnection;
use OCP\IUser;
use OCP\Server;
use OCP\Util;
/**
* @psalm-type TrashFolderSpaceInfo array{available: int|float, used: int|float}
*/
class TrashFolderService {
public function __construct(
readonly private IRootFolder $rootFolder,
readonly private IAppManager $appManager,
) {
}
public function getTrashFolderRoot(IUser $user): false|Folder {
$userRoot = $this->rootFolder->getUserFolder($user->getUID())->getParent();
try {
/** @var Folder $folder */
$folder = $userRoot->get('files_trashbin');
return $folder;
} catch (NotFoundException) {
return false;
}
}
public function getTrashFolder(IUser $user): false|Folder {
$rootTrashFolder = $this->getTrashFolderRoot($user);
if (!$rootTrashFolder) {
return false;
}
/** @var Folder $folder */
try {
/** @var Folder $folder */
$folder = $rootTrashFolder->get('files');
return $folder;
} catch (NotFoundException) {
return $rootTrashFolder->newFolder('files');
}
}
/**
* Calculate remaining free space for trash bin
*
* @return int|float The available space
*/
public function getAvailableSpace(Folder $trashFolderRoot, IUser $user): int|float {
$configuredTrashBinSize = TrashBin::getConfiguredTrashbinSize($user->getUID());
$trashBinSize = $trashFolderRoot->getSize(false);
if ($configuredTrashBinSize > -1) {
return $configuredTrashBinSize - $trashBinSize;
}
$softQuota = true;
$quota = $user->getQuota();
if ($quota === null || $quota === 'none') {
$quota = $this->rootFolder->getFreeSpace();
$softQuota = false;
// inf or unknown free space
if ($quota < 0) {
$quota = PHP_INT_MAX;
}
} else {
$quota = Util::computerFileSize($quota);
// invalid quota
if ($quota === false) {
$quota = PHP_INT_MAX;
}
}
// calculate available space for trash bin
// subtract size of files and current trash bin size from quota
if ($softQuota) {
$userFolder = $trashFolderRoot->getParent();
if (is_null($userFolder)) {
return 0;
}
$free = $quota - $userFolder->getSize(false); // remaining free space for user
if ($free > 0) {
$availableSpace = ($free * Trashbin::DEFAULTMAXSIZE / 100) - $trashBinSize; // how much space can be used for versions
} else {
$availableSpace = $free - $trashBinSize;
}
} else {
$availableSpace = $quota;
}
return Util::numericToNumber($availableSpace);
}
public static function delete(Folder $trashFolder, Node $node, IUser $user, $timestamp = null) {
$size = 0;
if ($timestamp) {
$query = Server::get(IDBConnection::class)->getQueryBuilder();
$query->delete('files_trash')
->where($query->expr()->eq('user', $query->createNamedParameter($user)))
->andWhere($query->expr()->eq('id', $query->createNamedParameter($node->getName())))
->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
$query->executeStatement();
$file = Trashbin::getTrashFilename($node->getName(), $timestamp);
} else {
$file = $node->getName();
}
//$size += Trashbin::deleteVersions($view, $file, $node, $timestamp, $user);
try {
$node = $trashFolder->get($file);
} catch (NotFoundException) {
return $size;
}
if ($node instanceof Folder) {
$size += Trashbin::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
} elseif ($node instanceof File) {
$size += $view->filesize('/files_trashbin/files/' . $file);
}
Trashbin::emitTrashbinPreDelete('/files_trashbin/files/' . $file);
$node->delete();
Trashbin::emitTrashbinPostDelete('/files_trashbin/files/' . $file);
return $size;
}
/**
* @param string $file
* @param string $filename
* @param ?int $timestamp
*/
private function deleteVersions(Folder $trashFolderRoot, $fileName, Node $node, ?int $timestamp, IUser $user): int|float {
$size = 0;
if ($this->appManager->isEnabledForUser('files_versions')) {
$trashFolderRoot->get('versions/' . $fileName);
if ($view->is_dir('files_trashbin/versions/' . $file)) {
$size += Trashbin::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
$view->unlink('files_trashbin/versions/' . $file);
} elseif ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
foreach ($versions as $v) {
if ($timestamp) {
$size += $view->filesize('/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v, $timestamp));
$view->unlink('/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v, $timestamp));
} else {
$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
}
}
}
}
return $size;
}
}

View file

@ -10,6 +10,7 @@ namespace OCA\Files_Trashbin;
use OC\Files\Cache\Cache;
use OC\Files\Cache\CacheEntry;
use OC\Files\Cache\CacheQueryBuilder;
use OC\Files\FileInfo;
use OC\Files\Filesystem;
use OC\Files\Node\NonExistingFile;
use OC\Files\Node\NonExistingFolder;
@ -17,14 +18,13 @@ use OC\Files\View;
use OC\User\NoUserException;
use OC_User;
use OCA\Files_Trashbin\AppInfo\Application;
use OCA\Files_Trashbin\Command\Expire;
use OCA\Files_Trashbin\Events\BeforeNodeRestoredEvent;
use OCA\Files_Trashbin\Events\NodeRestoredEvent;
use OCA\Files_Trashbin\Exceptions\CopyRecursiveException;
use OCA\Files_Trashbin\Service\ExpireService;
use OCA\Files_Versions\Storage;
use OCP\App\IAppManager;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Command\IBus;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\EventDispatcher\IEventListener;
@ -42,6 +42,7 @@ use OCP\FilesMetadata\IFilesMetadataManager;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
@ -350,17 +351,23 @@ class Trashbin implements IEventListener {
$trashStorage->releaseLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
self::scheduleExpire($user);
$userManager = Server::get(IUserManager::class);
$expireServie = Server::get(ExpireService::class);
if ($userObject = $userManager->get($user)) {
$expireServie->scheduleExpirationJob($userObject);
}
// if owner !== user we also need to update the owners trash size
if ($owner !== $user) {
self::scheduleExpire($owner);
if ($ownerObject = $userManager->get($owner)) {
$expireServie->scheduleExpirationJob($ownerObject);
}
}
return $moveSuccessful;
}
private static function getConfiguredTrashbinSize(string $user): int|float {
public static function getConfiguredTrashbinSize(string $user): int|float {
$config = Server::get(IConfig::class);
$userTrashbinSize = $config->getUserValue($user, 'files_trashbin', 'trashbin_size', '-1');
if (is_numeric($userTrashbinSize) && ($userTrashbinSize > -1)) {
@ -643,7 +650,7 @@ class Trashbin implements IEventListener {
*
* @param string $path
*/
protected static function emitTrashbinPreDelete($path) {
public static function emitTrashbinPreDelete($path) {
\OC_Hook::emit('\OCP\Trashbin', 'preDelete', ['path' => $path]);
}
@ -652,7 +659,7 @@ class Trashbin implements IEventListener {
*
* @param string $path
*/
protected static function emitTrashbinPostDelete($path) {
public static function emitTrashbinPostDelete($path) {
\OC_Hook::emit('\OCP\Trashbin', 'delete', ['path' => $path]);
}
@ -664,6 +671,7 @@ class Trashbin implements IEventListener {
* @param int $timestamp of deletion time
*
* @return int|float size of deleted files
* @throws NotPermittedException
*/
public static function delete($filename, $user, $timestamp = null) {
$userRoot = \OC::$server->getUserFolder($user)->getParent();
@ -704,32 +712,6 @@ class Trashbin implements IEventListener {
return $size;
}
/**
* @param string $file
* @param string $filename
* @param ?int $timestamp
*/
private static function deleteVersions(View $view, $file, $filename, $timestamp, string $user): int|float {
$size = 0;
if (Server::get(IAppManager::class)->isEnabledForUser('files_versions')) {
if ($view->is_dir('files_trashbin/versions/' . $file)) {
$size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
$view->unlink('files_trashbin/versions/' . $file);
} elseif ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
foreach ($versions as $v) {
if ($timestamp) {
$size += $view->filesize('/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v, $timestamp));
$view->unlink('/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v, $timestamp));
} else {
$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
}
}
}
}
return $size;
}
/**
* check to see whether a file exists in trashbin
*
@ -762,113 +744,11 @@ class Trashbin implements IEventListener {
return (bool)$query->executeStatement();
}
/**
* calculate remaining free space for trash bin
*
* @param int|float $trashbinSize current size of the trash bin
* @param string $user
* @return int|float available free space for trash bin
*/
private static function calculateFreeSpace(int|float $trashbinSize, string $user): int|float {
$configuredTrashbinSize = static::getConfiguredTrashbinSize($user);
if ($configuredTrashbinSize > -1) {
return $configuredTrashbinSize - $trashbinSize;
}
$userObject = Server::get(IUserManager::class)->get($user);
if (is_null($userObject)) {
return 0;
}
$softQuota = true;
$quota = $userObject->getQuota();
if ($quota === null || $quota === 'none') {
$quota = Filesystem::free_space('/');
$softQuota = false;
// inf or unknown free space
if ($quota < 0) {
$quota = PHP_INT_MAX;
}
} else {
$quota = Util::computerFileSize($quota);
// invalid quota
if ($quota === false) {
$quota = PHP_INT_MAX;
}
}
// calculate available space for trash bin
// subtract size of files and current trash bin size from quota
if ($softQuota) {
$userFolder = \OC::$server->getUserFolder($user);
if (is_null($userFolder)) {
return 0;
}
$free = $quota - $userFolder->getSize(false); // remaining free space for user
if ($free > 0) {
$availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions
} else {
$availableSpace = $free - $trashbinSize;
}
} else {
$availableSpace = $quota;
}
return Util::numericToNumber($availableSpace);
}
/**
* resize trash bin if necessary after a new file was added to Nextcloud
*
* @param string $user user id
*/
public static function resizeTrash($user) {
$size = self::getTrashbinSize($user);
$freeSpace = self::calculateFreeSpace($size, $user);
if ($freeSpace < 0) {
self::scheduleExpire($user);
}
}
/**
* clean up the trash bin
*
* @param string $user
*/
public static function expire($user) {
$trashBinSize = self::getTrashbinSize($user);
$availableSpace = self::calculateFreeSpace($trashBinSize, $user);
$dirContent = Helper::getTrashFiles('/', $user, 'mtime');
// delete all files older then $retention_obligation
[$delSize, $count] = self::deleteExpiredFiles($dirContent, $user);
$availableSpace += $delSize;
// delete files from trash until we meet the trash bin size limit again
self::deleteFiles(array_slice($dirContent, $count), $user, $availableSpace);
}
/**
* @param string $user
*/
private static function scheduleExpire($user) {
// let the admin disable auto expire
/** @var Application $application */
$application = Server::get(Application::class);
$expiration = $application->getContainer()->query('Expiration');
if ($expiration->isEnabled()) {
Server::get(IBus::class)->push(new Expire($user));
}
}
/**
* if the size limit for the trash bin is reached, we delete the oldest
* files in the trash bin until we meet the limit again
*
* @param array $files
* @param FileInfo[] $files
* @param string $user
* @param int|float $availableSpace available disc space
* @return int|float size of deleted files
@ -900,6 +780,40 @@ class Trashbin implements IEventListener {
return $size;
}
/**
* if the size limit for the trash bin is reached, we delete the oldest
* files in the trash bin until we meet the limit again
*
* @param Node[] $nodes
* @return int|float size of deleted files
*/
public static function deleteNodes(array $nodes, IUser $user, int|float $availableSpace): int|float {
/** @var Application $application */
$application = Server::get(Application::class);
$expiration = $application->getContainer()->get('Expiration');
$size = 0;
if ($availableSpace < 0) {
foreach ($nodes as $node) {
if ($availableSpace < 0 && $expiration->isExpired($node->getMTime(), true)) {
$tmp = self::delete($node->getName(), $user->getUID(), $node->getMTime());
Server::get(LoggerInterface::class)->info(
'remove "' . $node->getName() . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota) for user "{user}"',
[
'app' => 'files_trashbin',
'user' => $user,
]
);
$availableSpace += $tmp;
$size += $tmp;
} else {
break;
}
}
}
return $size;
}
/**
* delete files older then max storage time
*
@ -1108,18 +1022,6 @@ class Trashbin implements IEventListener {
return $size;
}
/**
* get current size of trash bin from a given user
*
* @param string $user user who owns the trash bin
* @return int|float trash bin size
*/
private static function getTrashbinSize(string $user): int|float {
$view = new View('/' . $user);
$fileInfo = $view->getFileInfo('/files_trashbin');
return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
}
/**
* check if trash bin is empty for a given user
*

View file

@ -9,8 +9,10 @@ declare(strict_types=1);
namespace OCA\Files_Trashbin\Tests\BackgroundJob;
use OC\Files\SetupManager;
use OCA\Files_Trashbin\BackgroundJob\ExpireTrash;
use OCA\Files_Trashbin\Expiration;
use OCA\Files_Trashbin\Service\ExpireService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
use OCP\IAppConfig;
@ -23,6 +25,8 @@ class ExpireTrashTest extends TestCase {
private IAppConfig&MockObject $appConfig;
private IUserManager&MockObject $userManager;
private Expiration&MockObject $expiration;
private ExpireService&MockObject $expireService;
private SetupManager&MockObject $setupManager;
private IJobList&MockObject $jobList;
private LoggerInterface&MockObject $logger;
private ITimeFactory&MockObject $time;
@ -35,6 +39,8 @@ class ExpireTrashTest extends TestCase {
$this->expiration = $this->createMock(Expiration::class);
$this->jobList = $this->createMock(IJobList::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->expireService = $this->createMock(ExpireService::class);
$this->setupManager = $this->createMock(SetupManager::class);
$this->time = $this->createMock(ITimeFactory::class);
$this->time->method('getTime')
@ -54,7 +60,7 @@ class ExpireTrashTest extends TestCase {
->with('files_trashbin', 'background_job_expire_trash_offset', 0)
->willReturn(0);
$job = new ExpireTrash($this->appConfig, $this->userManager, $this->expiration, $this->logger, $this->time);
$job = new ExpireTrash($this->appConfig, $this->userManager, $this->expiration, $this->expireService, $this->setupManager, $this->logger, $this->time);
$job->start($this->jobList);
}
@ -65,7 +71,7 @@ class ExpireTrashTest extends TestCase {
$this->expiration->expects($this->never())
->method('getMaxAgeAsTimestamp');
$job = new ExpireTrash($this->appConfig, $this->userManager, $this->expiration, $this->logger, $this->time);
$job = new ExpireTrash($this->appConfig, $this->userManager, $this->expiration, $this->expireService, $this->setupManager, $this->logger, $this->time);
$job->start($this->jobList);
}
}

View file

@ -6,10 +6,13 @@
*/
namespace OCA\Files_Trashbin\Tests\Command;
use OC\Files\SetupManager;
use OCA\Files_Trashbin\Command\ExpireTrash;
use OCA\Files_Trashbin\Expiration;
use OCA\Files_Trashbin\Helper;
use OCA\Files_Trashbin\Service\ExpireService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\IConfig;
@ -30,11 +33,14 @@ use Test\TestCase;
*/
class ExpireTrashTest extends TestCase {
private Expiration $expiration;
private Node $userFolder;
private Folder $userFolder;
private IRootFolder $rootFolder;
private IConfig $config;
private IUserManager $userManager;
private IUser $user;
private ITimeFactory $timeFactory;
private ExpireService $expireService;
private SetupManager $setupManager;
protected function setUp(): void {
@ -47,10 +53,13 @@ class ExpireTrashTest extends TestCase {
$userId = self::getUniqueID('user');
$this->userManager = Server::get(IUserManager::class);
$this->rootFolder = Server::get(IRootFolder::class);
$this->user = $this->userManager->createUser($userId, $userId);
$this->expireService = Server::get(ExpireService::class);
$this->setupManager = Server::get(SetupManager::class);
$this->loginAsUser($userId);
$this->userFolder = Server::get(IRootFolder::class)->getUserFolder($userId);
$this->userFolder = $this->rootFolder->getUserFolder($userId);
}
protected function tearDown(): void {
@ -99,9 +108,11 @@ class ExpireTrashTest extends TestCase {
->willReturn([$userId]);
$command = new ExpireTrash(
Server::get(LoggerInterface::class),
$this->setupManager,
$this->rootFolder,
Server::get(IUserManager::class),
$this->expiration
$this->expiration,
$this->expireService,
);
$this->invokePrivate($command, 'execute', [$inputInterface, $outputInterface]);

View file

@ -19,6 +19,7 @@ use OCA\Files_Sharing\AppInfo\Application;
use OCA\Files_Trashbin\AppInfo\Application as TrashbinApplication;
use OCA\Files_Trashbin\Expiration;
use OCA\Files_Trashbin\Helper;
use OCA\Files_Trashbin\Service\ExpireService;
use OCA\Files_Trashbin\Trashbin;
use OCP\App\IAppManager;
use OCP\AppFramework\Utility\ITimeFactory;
@ -177,6 +178,8 @@ class TrashbinTest extends \Test\TestCase {
// every second file will get a date in the past so that it will get expired
$manipulatedList = $this->manipulateDeleteTime($filesInTrash, $this->trashRoot1, $expiredDate);
$expireService = Server::get(ExpireService::class);
$expireService->
$testClass = new TrashbinForTesting();
[$sizeOfDeletedFiles, $count] = $testClass->dummyDeleteExpiredFiles($manipulatedList, $expireAt);
@ -681,15 +684,6 @@ class TrashbinTest extends \Test\TestCase {
// just a dummy class to make protected methods available for testing
class TrashbinForTesting extends Trashbin {
/**
* @param FileInfo[] $files
* @param integer $limit
*/
public function dummyDeleteExpiredFiles($files) {
// dummy value for $retention_obligation because it is not needed here
return parent::deleteExpiredFiles($files, TrashbinTest::TEST_TRASHBIN_USER1);
}
/**
* @param FileInfo[] $files
* @param integer $availableSpace

View file

@ -351,12 +351,17 @@ class LDAP implements ILDAPWrapper {
* @throws \Exception
*/
private function processLDAPError($resource, string $functionName, int $errorCode, string $errorMsg): void {
$this->logger->debug('LDAP error {message} ({code}) after calling {func}', [
$args = [
'app' => 'user_ldap',
'message' => $errorMsg,
'code' => $errorCode,
'func' => $functionName,
]);
];
if ($errorCode === 1) {
$this->logger->warning('LDAP error {message} ({code}) after calling {func}', $args);
} else {
$this->logger->debug('LDAP error {message} ({code}) after calling {func}', $args);
}
if ($functionName === 'ldap_get_entries'
&& $errorCode === -4) {
} elseif ($errorCode === 32) {