aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKent Delante <kent.delante@proton.me>2025-05-19 06:58:22 +0800
committerKent Delante <kent.delante@proton.me>2025-07-16 08:20:23 +0800
commit39f888c3e664bf5bdaed8f8356ef457124fd2095 (patch)
tree35c3d3f96168e7aa0b8cd41b844d3ff7ec2d6b4a
parent470c2a312bb6e21fc1e0a3bc1435061617f0a5d3 (diff)
downloadnextcloud-server-backport/53112/stable30.tar.gz
nextcloud-server-backport/53112/stable30.zip
fix(files_trashbin): Expire trashbin items when space is neededbackport/53112/stable30
Signed-off-by: Kent Delante <kent.delante@proton.me>
-rw-r--r--apps/files_trashbin/lib/Command/ExpireTrash.php7
-rw-r--r--apps/files_trashbin/lib/Expiration.php14
-rw-r--r--apps/files_trashbin/tests/Command/ExpireTrashTest.php156
3 files changed, 173 insertions, 4 deletions
diff --git a/apps/files_trashbin/lib/Command/ExpireTrash.php b/apps/files_trashbin/lib/Command/ExpireTrash.php
index 0348e08f87c..3048ee2bdf0 100644
--- a/apps/files_trashbin/lib/Command/ExpireTrash.php
+++ b/apps/files_trashbin/lib/Command/ExpireTrash.php
@@ -8,7 +8,6 @@
namespace OCA\Files_Trashbin\Command;
use OCA\Files_Trashbin\Expiration;
-use OCA\Files_Trashbin\Helper;
use OCA\Files_Trashbin\Trashbin;
use OCP\IUser;
use OCP\IUserManager;
@@ -56,8 +55,9 @@ class ExpireTrash extends Command {
}
protected function execute(InputInterface $input, OutputInterface $output): int {
+ $minAge = $this->expiration->getMinAgeAsTimestamp();
$maxAge = $this->expiration->getMaxAgeAsTimestamp();
- if (!$maxAge) {
+ if ($minAge === false && $maxAge === false) {
$output->writeln('Auto expiration is configured - keeps files and folders in the trash bin for 30 days and automatically deletes anytime after that if space is needed (note: files may not be deleted if space is not needed)');
return 1;
}
@@ -95,8 +95,7 @@ class ExpireTrash extends Command {
if (!$this->setupFS($uid)) {
return;
}
- $dirContent = Helper::getTrashFiles('/', $uid, 'mtime');
- Trashbin::deleteExpiredFiles($dirContent, $uid);
+ Trashbin::expire($uid);
} catch (\Throwable $e) {
$this->logger->error('Error while expiring trashbin for user ' . $user->getUID(), ['exception' => $e]);
}
diff --git a/apps/files_trashbin/lib/Expiration.php b/apps/files_trashbin/lib/Expiration.php
index 05aa2e8c38b..1b29ae66032 100644
--- a/apps/files_trashbin/lib/Expiration.php
+++ b/apps/files_trashbin/lib/Expiration.php
@@ -96,6 +96,20 @@ class Expiration {
}
/**
+ * Get minimal retention obligation as a timestamp
+ *
+ * @return int|false
+ */
+ public function getMinAgeAsTimestamp() {
+ $minAge = false;
+ if ($this->isEnabled() && $this->minAge !== self::NO_OBLIGATION) {
+ $time = $this->timeFactory->getTime();
+ $minAge = $time - ($this->minAge * 86400);
+ }
+ return $minAge;
+ }
+
+ /**
* @return bool|int
*/
public function getMaxAgeAsTimestamp() {
diff --git a/apps/files_trashbin/tests/Command/ExpireTrashTest.php b/apps/files_trashbin/tests/Command/ExpireTrashTest.php
new file mode 100644
index 00000000000..23bf0d8f121
--- /dev/null
+++ b/apps/files_trashbin/tests/Command/ExpireTrashTest.php
@@ -0,0 +1,156 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_Trashbin\Tests\Command;
+
+use OCA\Files_Trashbin\Command\ExpireTrash;
+use OCA\Files_Trashbin\Expiration;
+use OCA\Files_Trashbin\Helper;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Files\IRootFolder;
+use OCP\Files\Node;
+use OCP\IConfig;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\Server;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+/**
+ * Class ExpireTrashTest
+ *
+ * @group DB
+ *
+ * @package OCA\Files_Trashbin\Tests\Command
+ */
+class ExpireTrashTest extends TestCase {
+ private Expiration $expiration;
+ private Node $userFolder;
+ private IConfig $config;
+ private IUserManager $userManager;
+ private IUser $user;
+ private ITimeFactory $timeFactory;
+
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->config = Server::get(IConfig::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->expiration = Server::get(Expiration::class);
+ $this->invokePrivate($this->expiration, 'timeFactory', [$this->timeFactory]);
+
+ $userId = self::getUniqueID('user');
+ $this->userManager = Server::get(IUserManager::class);
+ $this->user = $this->userManager->createUser($userId, $userId);
+
+ $this->loginAsUser($userId);
+ $this->userFolder = Server::get(IRootFolder::class)->getUserFolder($userId);
+ }
+
+ protected function tearDown(): void {
+ $this->logout();
+
+ if (isset($this->user)) {
+ $this->user->delete();
+ }
+
+ $this->invokePrivate($this->expiration, 'timeFactory', [Server::get(ITimeFactory::class)]);
+ parent::tearDown();
+ }
+
+ /**
+ * @dataProvider retentionObligationProvider
+ */
+ public function testRetentionObligation(string $obligation, string $quota, int $elapsed, int $fileSize, bool $shouldExpire): void {
+ $this->config->setSystemValues(['trashbin_retention_obligation' => $obligation]);
+ $this->expiration->setRetentionObligation($obligation);
+
+ $this->user->setQuota($quota);
+
+ $bytes = 'ABCDEFGHIKLMNOPQRSTUVWXYZ';
+
+ $file = 'foo.txt';
+ $this->userFolder->newFile($file, substr($bytes, 0, $fileSize));
+
+ $filemtime = $this->userFolder->get($file)->getMTime();
+ $this->timeFactory->expects($this->any())
+ ->method('getTime')
+ ->willReturn($filemtime + $elapsed);
+ $this->userFolder->get($file)->delete();
+ $this->userFolder->getStorage()
+ ->getCache()
+ ->put('files_trashbin', ['size' => $fileSize, 'unencrypted_size' => $fileSize]);
+
+ $userId = $this->user->getUID();
+ $trashFiles = Helper::getTrashFiles('/', $userId);
+ $this->assertEquals(1, count($trashFiles));
+
+ $outputInterface = $this->createMock(OutputInterface::class);
+ $inputInterface = $this->createMock(InputInterface::class);
+ $inputInterface->expects($this->any())
+ ->method('getArgument')
+ ->with('user_id')
+ ->willReturn([$userId]);
+
+ $command = new ExpireTrash(
+ Server::get(LoggerInterface::class),
+ Server::get(IUserManager::class),
+ $this->expiration
+ );
+
+ $this->invokePrivate($command, 'execute', [$inputInterface, $outputInterface]);
+
+ $trashFiles = Helper::getTrashFiles('/', $userId);
+ $this->assertEquals($shouldExpire ? 0 : 1, count($trashFiles));
+ }
+
+ public function retentionObligationProvider(): array {
+ $hour = 3600; // 60 * 60
+
+ $oneDay = 24 * $hour;
+ $fiveDays = 24 * 5 * $hour;
+ $tenDays = 24 * 10 * $hour;
+ $elevenDays = 24 * 11 * $hour;
+
+ return [
+ ['disabled', '20 B', 0, 1, false],
+
+ ['auto', '20 B', 0, 5, false],
+ ['auto', '20 B', 0, 21, true],
+
+ ['0, auto', '20 B', 0, 21, true],
+ ['0, auto', '20 B', $oneDay, 5, false],
+ ['0, auto', '20 B', $oneDay, 19, true],
+ ['0, auto', '20 B', 0, 19, true],
+
+ ['auto, 0', '20 B', $oneDay, 19, true],
+ ['auto, 0', '20 B', $oneDay, 21, true],
+ ['auto, 0', '20 B', 0, 5, false],
+ ['auto, 0', '20 B', 0, 19, true],
+
+ ['1, auto', '20 B', 0, 5, false],
+ ['1, auto', '20 B', $fiveDays, 5, false],
+ ['1, auto', '20 B', $fiveDays, 21, true],
+
+ ['auto, 1', '20 B', 0, 21, true],
+ ['auto, 1', '20 B', 0, 5, false],
+ ['auto, 1', '20 B', $fiveDays, 5, true],
+ ['auto, 1', '20 B', $oneDay, 5, false],
+
+ ['2, 10', '20 B', $fiveDays, 5, false],
+ ['2, 10', '20 B', $fiveDays, 20, true],
+ ['2, 10', '20 B', $elevenDays, 5, true],
+
+ ['10, 2', '20 B', $fiveDays, 5, false],
+ ['10, 2', '20 B', $fiveDays, 21, false],
+ ['10, 2', '20 B', $tenDays, 5, false],
+ ['10, 2', '20 B', $elevenDays, 5, true]
+ ];
+ }
+}