diff --git a/apps/files_trashbin/composer/composer/autoload_classmap.php b/apps/files_trashbin/composer/composer/autoload_classmap.php index 23e2e7baff6..22c89f7cc2b 100644 --- a/apps/files_trashbin/composer/composer/autoload_classmap.php +++ b/apps/files_trashbin/composer/composer/autoload_classmap.php @@ -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', diff --git a/apps/files_trashbin/composer/composer/autoload_static.php b/apps/files_trashbin/composer/composer/autoload_static.php index fc604299261..67e279bdb84 100644 --- a/apps/files_trashbin/composer/composer/autoload_static.php +++ b/apps/files_trashbin/composer/composer/autoload_static.php @@ -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', diff --git a/apps/files_trashbin/lib/BackgroundJob/ExpireTrash.php b/apps/files_trashbin/lib/BackgroundJob/ExpireTrash.php index bb383dab78d..73b770dc22c 100644 --- a/apps/files_trashbin/lib/BackgroundJob/ExpireTrash.php +++ b/apps/files_trashbin/lib/BackgroundJob/ExpireTrash.php @@ -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(); } } diff --git a/apps/files_trashbin/lib/Command/Expire.php b/apps/files_trashbin/lib/Command/Expire.php index 73a42cd4749..72e2d3fedda 100644 --- a/apps/files_trashbin/lib/Command/Expire.php +++ b/apps/files_trashbin/lib/Command/Expire.php @@ -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(); } } diff --git a/apps/files_trashbin/lib/Command/ExpireTrash.php b/apps/files_trashbin/lib/Command/ExpireTrash.php index 422d8379984..24c5940ed6f 100644 --- a/apps/files_trashbin/lib/Command/ExpireTrash.php +++ b/apps/files_trashbin/lib/Command/ExpireTrash.php @@ -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 $user"); - $userObject = $this->userManager->get($user); - $this->expireTrashForUser($userObject); + $userObject = $this->userManager->get($user); + if ($userObject) { + $output->writeln("Remove deleted files of $user"); + $this->expireService->expireTrashForUser($userObject); + $this->setupManager->tearDown(); } else { $output->writeln("Unknown user $user"); 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 while expiring trashbin for user $displayName"); + 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; - } } diff --git a/apps/files_trashbin/lib/Command/Size.php b/apps/files_trashbin/lib/Command/Size.php index 9c19d4d92b3..dd4da791f57 100644 --- a/apps/files_trashbin/lib/Command/Size.php +++ b/apps/files_trashbin/lib/Command/Size.php @@ -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('Warning: changing the default trashbin size will automatically trigger cleanup of existing trashbins,'); diff --git a/apps/files_trashbin/lib/Listener/EventListener.php b/apps/files_trashbin/lib/Listener/EventListener.php index 63ecc9c81f7..0e952c27aa0 100644 --- a/apps/files_trashbin/lib/Listener/EventListener.php +++ b/apps/files_trashbin/lib/Listener/EventListener.php @@ -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 */ 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); + } } } diff --git a/apps/files_trashbin/lib/Service/ExpireService.php b/apps/files_trashbin/lib/Service/ExpireService.php new file mode 100644 index 00000000000..3830980faf0 --- /dev/null +++ b/apps/files_trashbin/lib/Service/ExpireService.php @@ -0,0 +1,122 @@ +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]; + } +} diff --git a/apps/files_trashbin/lib/Service/TrashFolderService.php b/apps/files_trashbin/lib/Service/TrashFolderService.php new file mode 100644 index 00000000000..4a8732efbfa --- /dev/null +++ b/apps/files_trashbin/lib/Service/TrashFolderService.php @@ -0,0 +1,174 @@ +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; + } +} diff --git a/apps/files_trashbin/lib/Trashbin.php b/apps/files_trashbin/lib/Trashbin.php index 63b3f28d33c..59cdbf4cd73 100644 --- a/apps/files_trashbin/lib/Trashbin.php +++ b/apps/files_trashbin/lib/Trashbin.php @@ -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 * diff --git a/apps/files_trashbin/tests/BackgroundJob/ExpireTrashTest.php b/apps/files_trashbin/tests/BackgroundJob/ExpireTrashTest.php index 9468fb7add0..da87377aa09 100644 --- a/apps/files_trashbin/tests/BackgroundJob/ExpireTrashTest.php +++ b/apps/files_trashbin/tests/BackgroundJob/ExpireTrashTest.php @@ -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); } } diff --git a/apps/files_trashbin/tests/Command/ExpireTrashTest.php b/apps/files_trashbin/tests/Command/ExpireTrashTest.php index 0d0ee98ca7a..511da1dd4ef 100644 --- a/apps/files_trashbin/tests/Command/ExpireTrashTest.php +++ b/apps/files_trashbin/tests/Command/ExpireTrashTest.php @@ -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]); diff --git a/apps/files_trashbin/tests/TrashbinTest.php b/apps/files_trashbin/tests/TrashbinTest.php index 6104a242104..0c39ffc55d3 100644 --- a/apps/files_trashbin/tests/TrashbinTest.php +++ b/apps/files_trashbin/tests/TrashbinTest.php @@ -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 diff --git a/apps/user_ldap/lib/LDAP.php b/apps/user_ldap/lib/LDAP.php index 1cf20c4b939..5d2bf09b2ba 100644 --- a/apps/user_ldap/lib/LDAP.php +++ b/apps/user_ldap/lib/LDAP.php @@ -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) {