Merge pull request #54876 from nextcloud/carl/cleanup-commands-trash

refactor: Commands and background jobs for the trashbin
This commit is contained in:
Andy Scherzinger 2026-01-29 13:44:17 +01:00 committed by GitHub
commit b1a114ded5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 233 additions and 229 deletions

View file

@ -7,18 +7,20 @@
*/ */
namespace OCA\Files_Trashbin\BackgroundJob; namespace OCA\Files_Trashbin\BackgroundJob;
use OC\Files\View;
use OCA\Files_Trashbin\AppInfo\Application; use OCA\Files_Trashbin\AppInfo\Application;
use OCA\Files_Trashbin\Expiration; use OCA\Files_Trashbin\Expiration;
use OCA\Files_Trashbin\Trashbin; use OCA\Files_Trashbin\Trashbin;
use OCP\AppFramework\Utility\ITimeFactory; use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob; use OCP\BackgroundJob\TimedJob;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\ISetupManager; use OCP\Files\ISetupManager;
use OCP\IAppConfig; use OCP\IAppConfig;
use OCP\IUser; use OCP\IUser;
use OCP\IUserManager; use OCP\IUserManager;
use OCP\Lock\ILockingProvider; use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException; use OCP\Lock\LockedException;
use Override;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
class ExpireTrash extends TimedJob { class ExpireTrash extends TimedJob {
@ -28,19 +30,21 @@ class ExpireTrash extends TimedJob {
private const USER_BATCH_SIZE = 10; private const USER_BATCH_SIZE = 10;
public function __construct( public function __construct(
private IAppConfig $appConfig, private readonly IAppConfig $appConfig,
private IUserManager $userManager, private readonly IUserManager $userManager,
private Expiration $expiration, private readonly Expiration $expiration,
private LoggerInterface $logger, private readonly LoggerInterface $logger,
private ISetupManager $setupManager, private readonly ISetupManager $setupManager,
private ILockingProvider $lockingProvider, private readonly ILockingProvider $lockingProvider,
private readonly IRootFolder $rootFolder,
ITimeFactory $time, ITimeFactory $time,
) { ) {
parent::__construct($time); parent::__construct($time);
$this->setInterval(self::THIRTY_MINUTES); $this->setInterval(self::THIRTY_MINUTES);
} }
protected function run($argument) { #[Override]
protected function run($argument): void {
$backgroundJob = $this->appConfig->getValueBool(Application::APP_ID, self::TOGGLE_CONFIG_KEY_NAME, true); $backgroundJob = $this->appConfig->getValueBool(Application::APP_ID, self::TOGGLE_CONFIG_KEY_NAME, true);
if (!$backgroundJob) { if (!$backgroundJob) {
return; return;
@ -64,9 +68,8 @@ class ExpireTrash extends TimedJob {
$count++; $count++;
try { try {
if ($this->setupFS($user)) { $folder = $this->getTrashRoot($user);
Trashbin::expire($uid); Trashbin::expire($folder, $user);
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->logger->error('Error while expiring trashbin for user ' . $uid, ['exception' => $e]); $this->logger->error('Error while expiring trashbin for user ' . $uid, ['exception' => $e]);
} finally { } finally {
@ -82,23 +85,19 @@ class ExpireTrash extends TimedJob {
} }
} }
/** private function getTrashRoot(IUser $user): Folder {
* Act on behalf on trash item owner $this->setupManager->tearDown();
*/
protected function setupFS(IUser $user): bool {
$this->setupManager->setupForUser($user); $this->setupManager->setupForUser($user);
// Check if this user has a trashbin directory $folder = $this->rootFolder->getUserFolder($user->getUID())->getParent()->get('files_trashbin');
$view = new View('/' . $user->getUID()); if (!$folder instanceof Folder) {
if (!$view->is_dir('/files_trashbin/files')) { throw new \LogicException("Didn't expect files_trashbin to be a file instead of a folder");
return false;
} }
return $folder;
return true;
} }
private function getNextOffset(): int { private function getNextOffset(): int {
return $this->runMutexOperation(function () { return $this->runMutexOperation(function (): int {
$this->appConfig->clearCache(); $this->appConfig->clearCache();
$offset = $this->appConfig->getValueInt(Application::APP_ID, self::OFFSET_CONFIG_KEY_NAME, 0); $offset = $this->appConfig->getValueInt(Application::APP_ID, self::OFFSET_CONFIG_KEY_NAME, 0);
@ -109,13 +108,18 @@ class ExpireTrash extends TimedJob {
} }
private function resetOffset() { private function resetOffset(): void {
$this->runMutexOperation(function (): void { $this->runMutexOperation(function (): void {
$this->appConfig->setValueInt(Application::APP_ID, self::OFFSET_CONFIG_KEY_NAME, 0); $this->appConfig->setValueInt(Application::APP_ID, self::OFFSET_CONFIG_KEY_NAME, 0);
}); });
} }
private function runMutexOperation($operation): mixed { /**
* @template T
* @param callable(): T $operation
* @return T
*/
private function runMutexOperation(callable $operation): mixed {
$acquired = false; $acquired = false;
while ($acquired === false) { while ($acquired === false) {

View file

@ -7,29 +7,36 @@
*/ */
namespace OCA\Files_Trashbin\Command; namespace OCA\Files_Trashbin\Command;
use OC\Core\Command\Base;
use OC\Files\SetupManager;
use OC\User\LazyUser;
use OCP\Files\IRootFolder; use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IDBConnection; use OCP\IDBConnection;
use OCP\IUser;
use OCP\IUserBackend; use OCP\IUserBackend;
use OCP\IUserManager; use OCP\IUserManager;
use OCP\Util; use OCP\Util;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidOptionException; use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
class CleanUp extends Command { class CleanUp extends Base {
public function __construct( public function __construct(
protected IRootFolder $rootFolder, protected IRootFolder $rootFolder,
protected IUserManager $userManager, protected IUserManager $userManager,
protected IDBConnection $dbConnection, protected IDBConnection $dbConnection,
protected SetupManager $setupManager,
) { ) {
parent::__construct(); parent::__construct();
} }
protected function configure() { protected function configure(): void {
parent::configure();
$this $this
->setName('trashbin:cleanup') ->setName('trashbin:cleanup')
->setDescription('Remove deleted files') ->setDescription('Remove deleted files')
@ -47,17 +54,18 @@ class CleanUp extends Command {
} }
protected function execute(InputInterface $input, OutputInterface $output): int { protected function execute(InputInterface $input, OutputInterface $output): int {
$users = $input->getArgument('user_id'); $userIds = $input->getArgument('user_id');
$verbose = $input->getOption('verbose'); $verbose = $input->getOption('verbose');
if (!empty($users) && $input->getOption('all-users')) { if (!empty($userIds) && $input->getOption('all-users')) {
throw new InvalidOptionException('Either specify a user_id or --all-users'); throw new InvalidOptionException('Either specify a user_id or --all-users');
} elseif (!empty($users)) { } elseif (!empty($userIds)) {
foreach ($users as $user) { foreach ($userIds as $userId) {
if ($this->userManager->userExists($user)) { $user = $this->userManager->get($userId);
$output->writeln("Remove deleted files of <info>$user</info>"); if ($user) {
$output->writeln("Remove deleted files of <info>$userId</info>");
$this->removeDeletedFiles($user, $output, $verbose); $this->removeDeletedFiles($user, $output, $verbose);
} else { } else {
$output->writeln("<error>Unknown user $user</error>"); $output->writeln("<error>Unknown user $userId</error>");
return 1; return 1;
} }
} }
@ -72,13 +80,14 @@ class CleanUp extends Command {
$limit = 500; $limit = 500;
$offset = 0; $offset = 0;
do { do {
$users = $backend->getUsers('', $limit, $offset); $userIds = $backend->getUsers('', $limit, $offset);
foreach ($users as $user) { foreach ($userIds as $userId) {
$output->writeln(" <info>$user</info>"); $output->writeln(" <info>$userId</info>");
$user = new LazyUser($userId, $this->userManager, null, $backend);
$this->removeDeletedFiles($user, $output, $verbose); $this->removeDeletedFiles($user, $output, $verbose);
} }
$offset += $limit; $offset += $limit;
} while (count($users) >= $limit); } while (count($userIds) >= $limit);
} }
} else { } else {
throw new InvalidOptionException('Either specify a user_id or --all-users'); throw new InvalidOptionException('Either specify a user_id or --all-users');
@ -87,32 +96,33 @@ class CleanUp extends Command {
} }
/** /**
* remove deleted files for the given user * Remove deleted files for the given user.
*/ */
protected function removeDeletedFiles(string $uid, OutputInterface $output, bool $verbose): void { protected function removeDeletedFiles(IUser $user, OutputInterface $output, bool $verbose): void {
\OC_Util::tearDownFS(); $this->setupManager->tearDown();
\OC_Util::setupFS($uid); $this->setupManager->setupForUser($user);
$path = '/' . $uid . '/files_trashbin'; $path = '/' . $user->getUID() . '/files_trashbin';
if ($this->rootFolder->nodeExists($path)) { try {
$node = $this->rootFolder->get($path); $node = $this->rootFolder->get($path);
} catch (NotFoundException|NotPermittedException) {
if ($verbose) { if ($verbose) {
$output->writeln('Deleting <info>' . Util::humanFileSize($node->getSize()) . "</info> in trash for <info>$uid</info>."); $output->writeln("No trash found for <info>{$user->getUID()}</info>");
}
$node->delete();
if ($this->rootFolder->nodeExists($path)) {
$output->writeln('<error>Trash folder sill exists after attempting to delete it</error>');
return;
}
$query = $this->dbConnection->getQueryBuilder();
$query->delete('files_trash')
->where($query->expr()->eq('user', $query->createParameter('uid')))
->setParameter('uid', $uid);
$query->executeStatement();
} else {
if ($verbose) {
$output->writeln("No trash found for <info>$uid</info>");
} }
return;
} }
if ($verbose) {
$output->writeln('Deleting <info>' . Util::humanFileSize($node->getSize()) . "</info> in trash for <info>{$user->getUID()}</info>.");
}
$node->delete();
if ($this->rootFolder->nodeExists($path)) {
$output->writeln('<error>Trash folder sill exists after attempting to delete it</error>');
return;
}
$query = $this->dbConnection->getQueryBuilder();
$query->delete('files_trash')
->where($query->expr()->eq('user', $query->createParameter('uid')))
->setParameter('uid', $user->getUID());
$query->executeStatement();
} }
} }

View file

@ -8,32 +8,48 @@
namespace OCA\Files_Trashbin\Command; namespace OCA\Files_Trashbin\Command;
use OC\Command\FileAccess; use OC\Command\FileAccess;
use OC\Files\SetupManager;
use OCA\Files_Trashbin\Trashbin; use OCA\Files_Trashbin\Trashbin;
use OCP\Command\ICommand; use OCP\Command\ICommand;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\IUserManager; use OCP\IUserManager;
use OCP\Server; use OCP\Server;
use Override;
use Psr\Log\LoggerInterface;
class Expire implements ICommand { class Expire implements ICommand {
use FileAccess; use FileAccess;
/**
* @param string $user
*/
public function __construct( public function __construct(
private $user, private readonly string $userId,
) { ) {
} }
public function handle() { #[Override]
public function handle(): void {
// can't use DI because Expire needs to be serializable
$userManager = Server::get(IUserManager::class); $userManager = Server::get(IUserManager::class);
if (!$userManager->userExists($this->user)) { $user = $userManager->get($this->userId);
if (!$user) {
// User has been deleted already // User has been deleted already
return; return;
} }
\OC_Util::tearDownFS(); try {
\OC_Util::setupFS($this->user); $setupManager = Server::get(SetupManager::class);
Trashbin::expire($this->user); $setupManager->tearDown();
\OC_Util::tearDownFS(); $setupManager->setupForUser($user);
$trashRoot = Server::get(IRootFolder::class)->getUserFolder($user->getUID())->getParent()->get('files_trashbin');
if (!$trashRoot instanceof Folder) {
throw new \LogicException("Didn't expect files_trashbin to be a file instead of a folder");
}
Trashbin::expire($trashRoot, $user);
} catch (\Exception $e) {
Server::get(LoggerInterface::class)->error('Error while expiring trashbin for user ' . $user->getUID(), ['exception' => $e]);
} finally {
$setupManager->tearDown();
}
} }
} }

View file

@ -7,33 +7,32 @@
*/ */
namespace OCA\Files_Trashbin\Command; namespace OCA\Files_Trashbin\Command;
use OC\Files\View; use OC\Core\Command\Base;
use OC\Files\SetupManager;
use OCA\Files_Trashbin\Expiration; use OCA\Files_Trashbin\Expiration;
use OCA\Files_Trashbin\Trashbin; use OCA\Files_Trashbin\Trashbin;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\IUser; use OCP\IUser;
use OCP\IUserManager; use OCP\IUserManager;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
class ExpireTrash extends Command { class ExpireTrash extends Base {
/**
* @param IUserManager|null $userManager
* @param Expiration|null $expiration
*/
public function __construct( public function __construct(
private LoggerInterface $logger, private readonly ?IUserManager $userManager,
private ?IUserManager $userManager = null, private readonly ?Expiration $expiration,
private ?Expiration $expiration = null, private readonly SetupManager $setupManager,
private readonly IRootFolder $rootFolder,
) { ) {
parent::__construct(); parent::__construct();
} }
protected function configure() { protected function configure(): void {
parent::configure();
$this $this
->setName('trashbin:expire') ->setName('trashbin:expire')
->setDescription('Expires the users trashbin') ->setDescription('Expires the users trashbin')
@ -52,15 +51,17 @@ class ExpireTrash extends Command {
return 1; return 1;
} }
$users = $input->getArgument('user_id'); $userIds = $input->getArgument('user_id');
if (!empty($users)) { if (!empty($userIds)) {
foreach ($users as $user) { foreach ($userIds as $userId) {
if ($this->userManager->userExists($user)) { $user = $this->userManager->get($userId);
$output->writeln("Remove deleted files of <info>$user</info>"); if ($user) {
$userObject = $this->userManager->get($user); $output->writeln("Remove deleted files of <info>$userId</info>");
$this->expireTrashForUser($userObject); $this->expireTrashForUser($user, $output);
$output->writeln("<error>Unknown user $userId</error>");
return 1;
} else { } else {
$output->writeln("<error>Unknown user $user</error>"); $output->writeln("<error>Unknown user $userId</error>");
return 1; return 1;
} }
} }
@ -71,7 +72,7 @@ class ExpireTrash extends Command {
$users = $this->userManager->getSeenUsers(); $users = $this->userManager->getSeenUsers();
foreach ($users as $user) { foreach ($users as $user) {
$p->advance(); $p->advance();
$this->expireTrashForUser($user); $this->expireTrashForUser($user, $output);
} }
$p->finish(); $p->finish();
$output->writeln(''); $output->writeln('');
@ -79,33 +80,25 @@ class ExpireTrash extends Command {
return 0; return 0;
} }
public function expireTrashForUser(IUser $user) { private function expireTrashForUser(IUser $user, OutputInterface $output): void {
try { try {
$uid = $user->getUID(); $trashRoot = $this->getTrashRoot($user);
if (!$this->setupFS($uid)) { Trashbin::expire($trashRoot, $user);
return;
}
Trashbin::expire($uid);
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->logger->error('Error while expiring trashbin for user ' . $user->getUID(), ['exception' => $e]); $output->writeln('<error>Error while expiring trashbin for user ' . $user->getUID() . '</error>');
throw $e;
} finally {
$this->setupManager->tearDown();
} }
} }
/** private function getTrashRoot(IUser $user): Folder {
* Act on behalf on trash item owner $this->setupManager->setupForUser($user);
* @param string $user
* @return boolean
*/
protected function setupFS($user) {
\OC_Util::tearDownFS();
\OC_Util::setupFS($user);
// Check if this user has a trashbin directory $folder = $this->rootFolder->getUserFolder($user->getUID())->getParent()->get('files_trashbin');
$view = new View('/' . $user); if (!$folder instanceof Folder) {
if (!$view->is_dir('/files_trashbin/files')) { throw new \LogicException("Didn't expect files_trashbin to be a file instead of a folder");
return false;
} }
return $folder;
return true;
} }
} }

View file

@ -7,6 +7,7 @@
namespace OCA\Files_Trashbin\Command; namespace OCA\Files_Trashbin\Command;
use OC\Core\Command\Base; use OC\Core\Command\Base;
use OC\Files\SetupManager;
use OCA\Files_Trashbin\Trash\ITrashManager; use OCA\Files_Trashbin\Trash\ITrashManager;
use OCA\Files_Trashbin\Trash\TrashItem; use OCA\Files_Trashbin\Trash\TrashItem;
use OCP\Files\IRootFolder; use OCP\Files\IRootFolder;
@ -14,6 +15,7 @@ use OCP\IDBConnection;
use OCP\IL10N; use OCP\IL10N;
use OCP\IUserBackend; use OCP\IUserBackend;
use OCP\IUserManager; use OCP\IUserManager;
use OCP\IUserSession;
use OCP\L10N\IFactory; use OCP\L10N\IFactory;
use Symfony\Component\Console\Exception\InvalidOptionException; use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@ -48,6 +50,8 @@ class RestoreAllFiles extends Base {
protected IUserManager $userManager, protected IUserManager $userManager,
protected IDBConnection $dbConnection, protected IDBConnection $dbConnection,
protected ITrashManager $trashManager, protected ITrashManager $trashManager,
protected SetupManager $setupManager,
protected IUserSession $userSession,
IFactory $l10nFactory, IFactory $l10nFactory,
) { ) {
parent::__construct(); parent::__construct();
@ -140,17 +144,16 @@ class RestoreAllFiles extends Base {
* Restore deleted files for the given user according to the given filters * Restore deleted files for the given user according to the given filters
*/ */
protected function restoreDeletedFiles(string $uid, int $scope, ?int $since, ?int $until, bool $dryRun, OutputInterface $output): void { protected function restoreDeletedFiles(string $uid, int $scope, ?int $since, ?int $until, bool $dryRun, OutputInterface $output): void {
\OC_Util::tearDownFS();
\OC_Util::setupFS($uid);
\OC_User::setUserId($uid);
$user = $this->userManager->get($uid); $user = $this->userManager->get($uid);
if (!$user) {
if ($user === null) {
$output->writeln("<error>Unknown user $uid</error>"); $output->writeln("<error>Unknown user $uid</error>");
return; return;
} }
$this->setupManager->tearDown();
$this->setupManager->setupForUser($user);
$this->userSession->setUser($user);
$userTrashItems = $this->filterTrashItems( $userTrashItems = $this->filterTrashItems(
$this->trashManager->listTrashRoot($user), $this->trashManager->listTrashRoot($user),
$scope, $scope,

View file

@ -10,10 +10,12 @@ namespace OCA\Files_Trashbin\Command;
use OC\Core\Command\Base; use OC\Core\Command\Base;
use OCP\Command\IBus; use OCP\Command\IBus;
use OCP\IAppConfig;
use OCP\IConfig; use OCP\IConfig;
use OCP\IUser; use OCP\IUser;
use OCP\IUserManager; use OCP\IUserManager;
use OCP\Util; use OCP\Util;
use Override;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
@ -21,14 +23,16 @@ use Symfony\Component\Console\Output\OutputInterface;
class Size extends Base { class Size extends Base {
public function __construct( public function __construct(
private IConfig $config, private readonly IAppConfig $appConfig,
private IUserManager $userManager, private readonly IConfig $config,
private IBus $commandBus, private readonly IUserManager $userManager,
private readonly IBus $commandBus,
) { ) {
parent::__construct(); parent::__construct();
} }
protected function configure() { #[Override]
protected function configure(): void {
parent::configure(); parent::configure();
$this $this
->setName('trashbin:size') ->setName('trashbin:size')
@ -41,6 +45,7 @@ class Size extends Base {
); );
} }
#[Override]
protected function execute(InputInterface $input, OutputInterface $output): int { protected function execute(InputInterface $input, OutputInterface $output): int {
$user = $input->getOption('user'); $user = $input->getOption('user');
$size = $input->getArgument('size'); $size = $input->getArgument('size');
@ -55,7 +60,7 @@ class Size extends Base {
$this->config->setUserValue($user, 'files_trashbin', 'trashbin_size', (string)$parsedSize); $this->config->setUserValue($user, 'files_trashbin', 'trashbin_size', (string)$parsedSize);
$this->commandBus->push(new Expire($user)); $this->commandBus->push(new Expire($user));
} else { } else {
$this->config->setAppValue('files_trashbin', 'trashbin_size', (string)$parsedSize); $this->appConfig->setValueInt('files_trashbin', 'trashbin_size', $parsedSize);
$output->writeln('<info>Warning: changing the default trashbin size will automatically trigger cleanup of existing trashbins,</info>'); $output->writeln('<info>Warning: changing the default trashbin size will automatically trigger cleanup of existing trashbins,</info>');
$output->writeln('<info>a users trashbin can exceed the configured size until they move a new file to the trashbin.</info>'); $output->writeln('<info>a users trashbin can exceed the configured size until they move a new file to the trashbin.</info>');
} }
@ -66,8 +71,8 @@ class Size extends Base {
return 0; return 0;
} }
private function printTrashbinSize(InputInterface $input, OutputInterface $output, ?string $user) { private function printTrashbinSize(InputInterface $input, OutputInterface $output, ?string $user): void {
$globalSize = (int)$this->config->getAppValue('files_trashbin', 'trashbin_size', '-1'); $globalSize = $this->appConfig->getValueInt('files_trashbin', 'trashbin_size', -1);
if ($globalSize < 0) { if ($globalSize < 0) {
$globalHumanSize = 'default (50% of available space)'; $globalHumanSize = 'default (50% of available space)';
} else { } else {

View file

@ -15,11 +15,18 @@ use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener; use OCP\EventDispatcher\IEventListener;
use OCP\Files\Events\BeforeFileSystemSetupEvent; use OCP\Files\Events\BeforeFileSystemSetupEvent;
use OCP\Files\Events\Node\NodeWrittenEvent; use OCP\Files\Events\Node\NodeWrittenEvent;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IUserManager;
use OCP\User\Events\BeforeUserDeletedEvent; use OCP\User\Events\BeforeUserDeletedEvent;
/** @template-implements IEventListener<NodeWrittenEvent|BeforeUserDeletedEvent|BeforeFileSystemSetupEvent> */ /** @template-implements IEventListener<NodeWrittenEvent|BeforeUserDeletedEvent|BeforeFileSystemSetupEvent> */
class EventListener implements IEventListener { class EventListener implements IEventListener {
public function __construct( public function __construct(
private IUserManager $userManager,
private IRootFolder $rootFolder,
private ?string $userId = null, private ?string $userId = null,
) { ) {
} }
@ -27,8 +34,19 @@ class EventListener implements IEventListener {
public function handle(Event $event): void { public function handle(Event $event): void {
if ($event instanceof NodeWrittenEvent) { if ($event instanceof NodeWrittenEvent) {
// Resize trash // Resize trash
if (!empty($this->userId)) { if (empty($this->userId)) {
Trashbin::resizeTrash($this->userId); return;
}
try {
/** @var Folder $trashRoot */
$trashRoot = $this->rootFolder->get('/' . $this->userId . '/files_trashbin');
} catch (NotFoundException|NotPermittedException) {
return;
}
$user = $this->userManager->get($this->userId);
if ($user) {
Trashbin::resizeTrash($trashRoot, $user);
} }
} }

View file

@ -44,6 +44,7 @@ use OCP\IConfig;
use OCP\IDBConnection; use OCP\IDBConnection;
use OCP\IRequest; use OCP\IRequest;
use OCP\IURLGenerator; use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager; use OCP\IUserManager;
use OCP\Lock\ILockingProvider; use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException; use OCP\Lock\LockedException;
@ -774,24 +775,19 @@ class Trashbin implements IEventListener {
} }
/** /**
* calculate remaining free space for trash bin * Calculate remaining free space for trash bin
* *
* @param int|float $trashbinSize current size of the trash bin * @param int|float $trashbinSize current size of the trash bin
* @param string $user
* @return int|float available free space for trash bin * @return int|float available free space for trash bin
*/ */
private static function calculateFreeSpace(int|float $trashbinSize, string $user): int|float { private static function calculateFreeSpace(Folder $userFolder, int|float $trashbinSize, IUser $user): int|float {
$configuredTrashbinSize = static::getConfiguredTrashbinSize($user); $configuredTrashbinSize = static::getConfiguredTrashbinSize($user->getUID());
if ($configuredTrashbinSize > -1) { if ($configuredTrashbinSize > -1) {
return $configuredTrashbinSize - $trashbinSize; return $configuredTrashbinSize - $trashbinSize;
} }
$userObject = Server::get(IUserManager::class)->get($user);
if (is_null($userObject)) {
return 0;
}
$softQuota = true; $softQuota = true;
$quota = $userObject->getQuota(); $quota = $user->getQuota();
if ($quota === null || $quota === 'none') { if ($quota === null || $quota === 'none') {
$quota = Filesystem::free_space('/'); $quota = Filesystem::free_space('/');
$softQuota = false; $softQuota = false;
@ -810,10 +806,6 @@ class Trashbin implements IEventListener {
// calculate available space for trash bin // calculate available space for trash bin
// subtract size of files and current trash bin size from quota // subtract size of files and current trash bin size from quota
if ($softQuota) { if ($softQuota) {
$userFolder = \OC::$server->getUserFolder($user);
if (is_null($userFolder)) {
return 0;
}
$free = $quota - $userFolder->getSize(false); // remaining free space for user $free = $quota - $userFolder->getSize(false); // remaining free space for user
if ($free > 0) { if ($free > 0) {
$availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions $availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions
@ -828,38 +820,34 @@ class Trashbin implements IEventListener {
} }
/** /**
* resize trash bin if necessary after a new file was added to Nextcloud * Resize trash bin if necessary after a new file was added to Nextcloud
*
* @param string $user user id
*/ */
public static function resizeTrash($user) { public static function resizeTrash(Folder $trashRoot, IUser $user): void {
$size = self::getTrashbinSize($user); $trashBinSize = $trashRoot->getSize();
$freeSpace = self::calculateFreeSpace($size, $user); $freeSpace = self::calculateFreeSpace($trashRoot->getParent(), $trashBinSize, $user);
if ($freeSpace < 0) { if ($freeSpace < 0) {
self::scheduleExpire($user); self::scheduleExpire($user->getUID());
} }
} }
/** /**
* clean up the trash bin * Clean up the trash bin
*
* @param string $user
*/ */
public static function expire($user) { public static function expire(Folder $trashRoot, IUser $user): void {
$trashBinSize = self::getTrashbinSize($user); $trashBinSize = $trashRoot->getSize();
$availableSpace = self::calculateFreeSpace($trashBinSize, $user); $availableSpace = self::calculateFreeSpace($trashRoot->getParent(), $trashBinSize, $user);
$dirContent = Helper::getTrashFiles('/', $user, 'mtime'); $dirContent = Helper::getTrashFiles('/', $user->getUID(), 'mtime');
// delete all files older then $retention_obligation // delete all files older then $retention_obligation
[$delSize, $count] = self::deleteExpiredFiles($dirContent, $user); [$delSize, $count] = self::deleteExpiredFiles($dirContent, $user->getUID());
$availableSpace += $delSize; $availableSpace += $delSize;
// delete files from trash until we meet the trash bin size limit again // delete files from trash until we meet the trash bin size limit again
self::deleteFiles(array_slice($dirContent, $count), $user, $availableSpace); self::deleteFiles(array_slice($dirContent, $count), $user->getUID(), $availableSpace);
} }
/** /**

View file

@ -14,6 +14,7 @@ use OCA\Files_Trashbin\BackgroundJob\ExpireTrash;
use OCA\Files_Trashbin\Expiration; use OCA\Files_Trashbin\Expiration;
use OCP\AppFramework\Utility\ITimeFactory; use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList; use OCP\BackgroundJob\IJobList;
use OCP\Files\IRootFolder;
use OCP\Files\ISetupManager; use OCP\Files\ISetupManager;
use OCP\IAppConfig; use OCP\IAppConfig;
use OCP\IUserManager; use OCP\IUserManager;
@ -31,6 +32,7 @@ class ExpireTrashTest extends TestCase {
private ITimeFactory&MockObject $time; private ITimeFactory&MockObject $time;
private ISetupManager&MockObject $setupManager; private ISetupManager&MockObject $setupManager;
private ILockingProvider&MockObject $lockingProvider; private ILockingProvider&MockObject $lockingProvider;
private IRootFolder&MockObject $rootFolder;
protected function setUp(): void { protected function setUp(): void {
parent::setUp(); parent::setUp();
@ -42,6 +44,7 @@ class ExpireTrashTest extends TestCase {
$this->logger = $this->createMock(LoggerInterface::class); $this->logger = $this->createMock(LoggerInterface::class);
$this->setupManager = $this->createMock(ISetupManager::class); $this->setupManager = $this->createMock(ISetupManager::class);
$this->lockingProvider = $this->createMock(ILockingProvider::class); $this->lockingProvider = $this->createMock(ILockingProvider::class);
$this->rootFolder = $this->createMock(IRootFolder::class);
$this->time = $this->createMock(ITimeFactory::class); $this->time = $this->createMock(ITimeFactory::class);
$this->time->method('getTime') $this->time->method('getTime')
@ -68,6 +71,7 @@ class ExpireTrashTest extends TestCase {
$this->logger, $this->logger,
$this->setupManager, $this->setupManager,
$this->lockingProvider, $this->lockingProvider,
$this->rootFolder,
$this->time, $this->time,
); );
$job->start($this->jobList); $job->start($this->jobList);
@ -87,6 +91,7 @@ class ExpireTrashTest extends TestCase {
$this->logger, $this->logger,
$this->setupManager, $this->setupManager,
$this->lockingProvider, $this->lockingProvider,
$this->rootFolder,
$this->time, $this->time,
); );
$job->start($this->jobList); $job->start($this->jobList);

View file

@ -8,9 +8,12 @@ declare(strict_types=1);
*/ */
namespace OCA\Files_Trashbin\Tests\Command; namespace OCA\Files_Trashbin\Tests\Command;
use OC\Files\SetupManager;
use OCA\Files_Trashbin\Command\CleanUp; use OCA\Files_Trashbin\Command\CleanUp;
use OCP\Files\IRootFolder; use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\IDBConnection; use OCP\IDBConnection;
use OCP\IUser;
use OCP\IUserManager; use OCP\IUserManager;
use OCP\Server; use OCP\Server;
use OCP\UserInterface; use OCP\UserInterface;
@ -34,16 +37,22 @@ class CleanUpTest extends TestCase {
protected IDBConnection $dbConnection; protected IDBConnection $dbConnection;
protected CleanUp $cleanup; protected CleanUp $cleanup;
protected string $trashTable = 'files_trash'; protected string $trashTable = 'files_trash';
protected string $user0 = 'user0'; protected IUser&MockObject $user0;
protected SetupManager&MockObject $setupManager;
protected function setUp(): void { protected function setUp(): void {
parent::setUp(); parent::setUp();
$this->user0 = $this->createMock(IUser::class);
$this->user0->method('getUID')->willReturn('user0');
$this->rootFolder = $this->createMock(IRootFolder::class); $this->rootFolder = $this->createMock(IRootFolder::class);
$this->userManager = $this->createMock(IUserManager::class); $this->userManager = $this->createMock(IUserManager::class);
$this->dbConnection = Server::get(IDBConnection::class); $this->dbConnection = Server::get(IDBConnection::class);
$this->setupManager = $this->createMock(SetupManager::class);
$this->cleanup = new CleanUp($this->rootFolder, $this->userManager, $this->dbConnection); $this->cleanup = new CleanUp($this->rootFolder, $this->userManager, $this->dbConnection, $this->setupManager);
} }
/** /**
@ -74,17 +83,20 @@ class CleanUpTest extends TestCase {
$this->initTable(); $this->initTable();
$this->rootFolder $this->rootFolder
->method('nodeExists') ->method('nodeExists')
->with('/' . $this->user0 . '/files_trashbin') ->with('/' . $this->user0->getUID() . '/files_trashbin')
->willReturnOnConsecutiveCalls($nodeExists, false); ->willReturn(false);
if ($nodeExists) { if ($nodeExists) {
$this->rootFolder $this->rootFolder
->method('get') ->method('get')
->with('/' . $this->user0 . '/files_trashbin') ->with('/' . $this->user0->getUID() . '/files_trashbin')
->willReturn($this->rootFolder); ->willReturn($this->rootFolder);
$this->rootFolder $this->rootFolder
->method('delete'); ->method('delete');
} else { } else {
$this->rootFolder->expects($this->never())->method('get'); $this->rootFolder
->method('get')
->with('/' . $this->user0->getUID() . '/files_trashbin')
->willThrowException(new NotFoundException());
$this->rootFolder->expects($this->never())->method('delete'); $this->rootFolder->expects($this->never())->method('delete');
} }
self::invokePrivate($this->cleanup, 'removeDeletedFiles', [$this->user0, new NullOutput(), false]); self::invokePrivate($this->cleanup, 'removeDeletedFiles', [$this->user0, new NullOutput(), false]);
@ -129,15 +141,19 @@ class CleanUpTest extends TestCase {
$userIds = ['user1', 'user2', 'user3']; $userIds = ['user1', 'user2', 'user3'];
$instance = $this->getMockBuilder(CleanUp::class) $instance = $this->getMockBuilder(CleanUp::class)
->onlyMethods(['removeDeletedFiles']) ->onlyMethods(['removeDeletedFiles'])
->setConstructorArgs([$this->rootFolder, $this->userManager, $this->dbConnection]) ->setConstructorArgs([$this->rootFolder, $this->userManager, $this->dbConnection, $this->setupManager])
->getMock(); ->getMock();
$instance->expects($this->exactly(count($userIds))) $instance->expects($this->exactly(count($userIds)))
->method('removeDeletedFiles') ->method('removeDeletedFiles')
->willReturnCallback(function ($user) use ($userIds): void { ->willReturnCallback(function (IUser $user) use ($userIds): void {
$this->assertTrue(in_array($user, $userIds)); $this->assertTrue(in_array($user->getUID(), $userIds));
}); });
$this->userManager->expects($this->exactly(count($userIds))) $this->userManager->expects($this->exactly(count($userIds)))
->method('userExists')->willReturn(true); ->method('get')->willReturnCallback(function (string $userId): IUser {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn($userId);
return $user;
});
$inputInterface = $this->createMock(\Symfony\Component\Console\Input\InputInterface::class); $inputInterface = $this->createMock(\Symfony\Component\Console\Input\InputInterface::class);
$inputInterface->method('getArgument') $inputInterface->method('getArgument')
->with('user_id') ->with('user_id')
@ -159,7 +175,7 @@ class CleanUpTest extends TestCase {
$backendUsers = ['user1', 'user2']; $backendUsers = ['user1', 'user2'];
$instance = $this->getMockBuilder(CleanUp::class) $instance = $this->getMockBuilder(CleanUp::class)
->onlyMethods(['removeDeletedFiles']) ->onlyMethods(['removeDeletedFiles'])
->setConstructorArgs([$this->rootFolder, $this->userManager, $this->dbConnection]) ->setConstructorArgs([$this->rootFolder, $this->userManager, $this->dbConnection, $this->setupManager])
->getMock(); ->getMock();
$backend = $this->createMock(UserInterface::class); $backend = $this->createMock(UserInterface::class);
$backend->method('getUsers') $backend->method('getUsers')
@ -167,8 +183,8 @@ class CleanUpTest extends TestCase {
->willReturn($backendUsers); ->willReturn($backendUsers);
$instance->expects($this->exactly(count($backendUsers))) $instance->expects($this->exactly(count($backendUsers)))
->method('removeDeletedFiles') ->method('removeDeletedFiles')
->willReturnCallback(function ($user) use ($backendUsers): void { ->willReturnCallback(function (IUser $user) use ($backendUsers): void {
$this->assertTrue(in_array($user, $backendUsers)); $this->assertTrue(in_array($user->getUID(), $backendUsers));
}); });
$inputInterface = $this->createMock(InputInterface::class); $inputInterface = $this->createMock(InputInterface::class);
$inputInterface->method('getArgument') $inputInterface->method('getArgument')

View file

@ -6,6 +6,7 @@
*/ */
namespace OCA\Files_Trashbin\Tests\Command; namespace OCA\Files_Trashbin\Tests\Command;
use OC\Files\SetupManager;
use OCA\Files_Trashbin\Command\ExpireTrash; use OCA\Files_Trashbin\Command\ExpireTrash;
use OCA\Files_Trashbin\Expiration; use OCA\Files_Trashbin\Expiration;
use OCA\Files_Trashbin\Helper; use OCA\Files_Trashbin\Helper;
@ -16,9 +17,9 @@ use OCP\IConfig;
use OCP\IUser; use OCP\IUser;
use OCP\IUserManager; use OCP\IUserManager;
use OCP\Server; use OCP\Server;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Test\TestCase; use Test\TestCase;
@ -38,7 +39,6 @@ class ExpireTrashTest extends TestCase {
private IUser $user; private IUser $user;
private ITimeFactory&MockObject $timeFactory; private ITimeFactory&MockObject $timeFactory;
protected function setUp(): void { protected function setUp(): void {
parent::setUp(); parent::setUp();
@ -66,7 +66,7 @@ class ExpireTrashTest extends TestCase {
parent::tearDown(); parent::tearDown();
} }
#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'retentionObligationProvider')] #[DataProvider(methodName: 'retentionObligationProvider')]
public function testRetentionObligation(string $obligation, string $quota, int $elapsed, int $fileSize, bool $shouldExpire): void { public function testRetentionObligation(string $obligation, string $quota, int $elapsed, int $fileSize, bool $shouldExpire): void {
$this->config->setSystemValues(['trashbin_retention_obligation' => $obligation]); $this->config->setSystemValues(['trashbin_retention_obligation' => $obligation]);
$this->expiration->setRetentionObligation($obligation); $this->expiration->setRetentionObligation($obligation);
@ -99,9 +99,10 @@ class ExpireTrashTest extends TestCase {
->willReturn([$userId]); ->willReturn([$userId]);
$command = new ExpireTrash( $command = new ExpireTrash(
Server::get(LoggerInterface::class),
Server::get(IUserManager::class), Server::get(IUserManager::class),
$this->expiration $this->expiration,
Server::get(SetupManager::class),
Server::get(IRootFolder::class),
); );
$this->invokePrivate($command, 'execute', [$inputInterface, $outputInterface]); $this->invokePrivate($command, 'execute', [$inputInterface, $outputInterface]);

View file

@ -1707,72 +1707,18 @@
<code><![CDATA[moveMount]]></code> <code><![CDATA[moveMount]]></code>
</UndefinedMethod> </UndefinedMethod>
</file> </file>
<file src="apps/files_trashbin/lib/BackgroundJob/ExpireTrash.php">
<InternalClass>
<code><![CDATA[new View('/' . $user->getUID())]]></code>
</InternalClass>
<InternalMethod>
<code><![CDATA[is_dir]]></code>
<code><![CDATA[new View('/' . $user->getUID())]]></code>
</InternalMethod>
</file>
<file src="apps/files_trashbin/lib/Command/CleanUp.php">
<DeprecatedClass>
<code><![CDATA[\OC_Util::setupFS($uid)]]></code>
<code><![CDATA[\OC_Util::tearDownFS()]]></code>
</DeprecatedClass>
<DeprecatedMethod>
<code><![CDATA[\OC_Util::tearDownFS()]]></code>
</DeprecatedMethod>
</file>
<file src="apps/files_trashbin/lib/Command/Expire.php"> <file src="apps/files_trashbin/lib/Command/Expire.php">
<DeprecatedClass>
<code><![CDATA[\OC_Util::setupFS($this->user)]]></code>
<code><![CDATA[\OC_Util::tearDownFS()]]></code>
<code><![CDATA[\OC_Util::tearDownFS()]]></code>
</DeprecatedClass>
<DeprecatedInterface> <DeprecatedInterface>
<code><![CDATA[Expire]]></code> <code><![CDATA[Expire]]></code>
</DeprecatedInterface> </DeprecatedInterface>
<DeprecatedMethod>
<code><![CDATA[\OC_Util::tearDownFS()]]></code>
<code><![CDATA[\OC_Util::tearDownFS()]]></code>
</DeprecatedMethod>
</file>
<file src="apps/files_trashbin/lib/Command/ExpireTrash.php">
<DeprecatedClass>
<code><![CDATA[\OC_Util::setupFS($user)]]></code>
<code><![CDATA[\OC_Util::tearDownFS()]]></code>
</DeprecatedClass>
<DeprecatedMethod>
<code><![CDATA[\OC_Util::tearDownFS()]]></code>
</DeprecatedMethod>
<InternalClass>
<code><![CDATA[new View('/' . $user)]]></code>
</InternalClass>
<InternalMethod>
<code><![CDATA[is_dir]]></code>
<code><![CDATA[new View('/' . $user)]]></code>
</InternalMethod>
</file>
<file src="apps/files_trashbin/lib/Command/RestoreAllFiles.php">
<DeprecatedClass>
<code><![CDATA[\OC_Util::setupFS($uid)]]></code>
<code><![CDATA[\OC_Util::tearDownFS()]]></code>
</DeprecatedClass>
<DeprecatedMethod>
<code><![CDATA[\OC_Util::tearDownFS()]]></code>
</DeprecatedMethod>
</file> </file>
<file src="apps/files_trashbin/lib/Command/Size.php"> <file src="apps/files_trashbin/lib/Command/Size.php">
<DeprecatedInterface> <DeprecatedInterface>
<code><![CDATA[private]]></code> <code><![CDATA[private]]></code>
</DeprecatedInterface> </DeprecatedInterface>
<DeprecatedMethod> <DeprecatedMethod>
<code><![CDATA[getAppValue]]></code>
<code><![CDATA[getUserValue]]></code> <code><![CDATA[getUserValue]]></code>
<code><![CDATA[getUserValueForUsers]]></code> <code><![CDATA[getUserValueForUsers]]></code>
<code><![CDATA[setAppValue]]></code>
<code><![CDATA[setUserValue]]></code> <code><![CDATA[setUserValue]]></code>
</DeprecatedMethod> </DeprecatedMethod>
</file> </file>
@ -1850,7 +1796,6 @@
<code><![CDATA[Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => $targetPath, 'trashPath' => $sourcePath])]]></code> <code><![CDATA[Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => $targetPath, 'trashPath' => $sourcePath])]]></code>
<code><![CDATA[getUserFolder]]></code> <code><![CDATA[getUserFolder]]></code>
<code><![CDATA[getUserFolder]]></code> <code><![CDATA[getUserFolder]]></code>
<code><![CDATA[getUserFolder]]></code>
</DeprecatedMethod> </DeprecatedMethod>
<InternalClass> <InternalClass>
<code><![CDATA[new View('/' . $owner)]]></code> <code><![CDATA[new View('/' . $owner)]]></code>