Skip to content

Commit be84f82

Browse files
Merge pull request #53837 from nextcloud/backport/53112/stable31
2 parents f58887e + f8cce08 commit be84f82

File tree

3 files changed

+173
-4
lines changed

3 files changed

+173
-4
lines changed

apps/files_trashbin/lib/Command/ExpireTrash.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
use OC\Files\View;
1010
use OCA\Files_Trashbin\Expiration;
11-
use OCA\Files_Trashbin\Helper;
1211
use OCA\Files_Trashbin\Trashbin;
1312
use OCP\IUser;
1413
use OCP\IUserManager;
@@ -45,8 +44,9 @@ protected function configure() {
4544
}
4645

4746
protected function execute(InputInterface $input, OutputInterface $output): int {
47+
$minAge = $this->expiration->getMinAgeAsTimestamp();
4848
$maxAge = $this->expiration->getMaxAgeAsTimestamp();
49-
if (!$maxAge) {
49+
if ($minAge === false && $maxAge === false) {
5050
$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)');
5151
return 1;
5252
}
@@ -84,8 +84,7 @@ public function expireTrashForUser(IUser $user) {
8484
if (!$this->setupFS($uid)) {
8585
return;
8686
}
87-
$dirContent = Helper::getTrashFiles('/', $uid, 'mtime');
88-
Trashbin::deleteExpiredFiles($dirContent, $uid);
87+
Trashbin::expire($uid);
8988
} catch (\Throwable $e) {
9089
$this->logger->error('Error while expiring trashbin for user ' . $user->getUID(), ['exception' => $e]);
9190
}

apps/files_trashbin/lib/Expiration.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,20 @@ public function isExpired($timestamp, $quotaExceeded = false) {
9393
return $isOlderThanMax || $isMinReached;
9494
}
9595

96+
/**
97+
* Get minimal retention obligation as a timestamp
98+
*
99+
* @return int|false
100+
*/
101+
public function getMinAgeAsTimestamp() {
102+
$minAge = false;
103+
if ($this->isEnabled() && $this->minAge !== self::NO_OBLIGATION) {
104+
$time = $this->timeFactory->getTime();
105+
$minAge = $time - ($this->minAge * 86400);
106+
}
107+
return $minAge;
108+
}
109+
96110
/**
97111
* @return bool|int
98112
*/
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-only
6+
*/
7+
namespace OCA\Files_Trashbin\Tests\Command;
8+
9+
use OCA\Files_Trashbin\Command\ExpireTrash;
10+
use OCA\Files_Trashbin\Expiration;
11+
use OCA\Files_Trashbin\Helper;
12+
use OCP\AppFramework\Utility\ITimeFactory;
13+
use OCP\Files\IRootFolder;
14+
use OCP\Files\Node;
15+
use OCP\IConfig;
16+
use OCP\IUser;
17+
use OCP\IUserManager;
18+
use OCP\Server;
19+
use Psr\Log\LoggerInterface;
20+
use Symfony\Component\Console\Input\InputInterface;
21+
use Symfony\Component\Console\Output\OutputInterface;
22+
use Test\TestCase;
23+
24+
/**
25+
* Class ExpireTrashTest
26+
*
27+
* @group DB
28+
*
29+
* @package OCA\Files_Trashbin\Tests\Command
30+
*/
31+
class ExpireTrashTest extends TestCase {
32+
private Expiration $expiration;
33+
private Node $userFolder;
34+
private IConfig $config;
35+
private IUserManager $userManager;
36+
private IUser $user;
37+
private ITimeFactory $timeFactory;
38+
39+
40+
protected function setUp(): void {
41+
parent::setUp();
42+
43+
$this->config = Server::get(IConfig::class);
44+
$this->timeFactory = $this->createMock(ITimeFactory::class);
45+
$this->expiration = Server::get(Expiration::class);
46+
$this->invokePrivate($this->expiration, 'timeFactory', [$this->timeFactory]);
47+
48+
$userId = self::getUniqueID('user');
49+
$this->userManager = Server::get(IUserManager::class);
50+
$this->user = $this->userManager->createUser($userId, $userId);
51+
52+
$this->loginAsUser($userId);
53+
$this->userFolder = Server::get(IRootFolder::class)->getUserFolder($userId);
54+
}
55+
56+
protected function tearDown(): void {
57+
$this->logout();
58+
59+
if (isset($this->user)) {
60+
$this->user->delete();
61+
}
62+
63+
$this->invokePrivate($this->expiration, 'timeFactory', [Server::get(ITimeFactory::class)]);
64+
parent::tearDown();
65+
}
66+
67+
/**
68+
* @dataProvider retentionObligationProvider
69+
*/
70+
public function testRetentionObligation(string $obligation, string $quota, int $elapsed, int $fileSize, bool $shouldExpire): void {
71+
$this->config->setSystemValues(['trashbin_retention_obligation' => $obligation]);
72+
$this->expiration->setRetentionObligation($obligation);
73+
74+
$this->user->setQuota($quota);
75+
76+
$bytes = 'ABCDEFGHIKLMNOPQRSTUVWXYZ';
77+
78+
$file = 'foo.txt';
79+
$this->userFolder->newFile($file, substr($bytes, 0, $fileSize));
80+
81+
$filemtime = $this->userFolder->get($file)->getMTime();
82+
$this->timeFactory->expects($this->any())
83+
->method('getTime')
84+
->willReturn($filemtime + $elapsed);
85+
$this->userFolder->get($file)->delete();
86+
$this->userFolder->getStorage()
87+
->getCache()
88+
->put('files_trashbin', ['size' => $fileSize, 'unencrypted_size' => $fileSize]);
89+
90+
$userId = $this->user->getUID();
91+
$trashFiles = Helper::getTrashFiles('/', $userId);
92+
$this->assertEquals(1, count($trashFiles));
93+
94+
$outputInterface = $this->createMock(OutputInterface::class);
95+
$inputInterface = $this->createMock(InputInterface::class);
96+
$inputInterface->expects($this->any())
97+
->method('getArgument')
98+
->with('user_id')
99+
->willReturn([$userId]);
100+
101+
$command = new ExpireTrash(
102+
Server::get(LoggerInterface::class),
103+
Server::get(IUserManager::class),
104+
$this->expiration
105+
);
106+
107+
$this->invokePrivate($command, 'execute', [$inputInterface, $outputInterface]);
108+
109+
$trashFiles = Helper::getTrashFiles('/', $userId);
110+
$this->assertEquals($shouldExpire ? 0 : 1, count($trashFiles));
111+
}
112+
113+
public function retentionObligationProvider(): array {
114+
$hour = 3600; // 60 * 60
115+
116+
$oneDay = 24 * $hour;
117+
$fiveDays = 24 * 5 * $hour;
118+
$tenDays = 24 * 10 * $hour;
119+
$elevenDays = 24 * 11 * $hour;
120+
121+
return [
122+
['disabled', '20 B', 0, 1, false],
123+
124+
['auto', '20 B', 0, 5, false],
125+
['auto', '20 B', 0, 21, true],
126+
127+
['0, auto', '20 B', 0, 21, true],
128+
['0, auto', '20 B', $oneDay, 5, false],
129+
['0, auto', '20 B', $oneDay, 19, true],
130+
['0, auto', '20 B', 0, 19, true],
131+
132+
['auto, 0', '20 B', $oneDay, 19, true],
133+
['auto, 0', '20 B', $oneDay, 21, true],
134+
['auto, 0', '20 B', 0, 5, false],
135+
['auto, 0', '20 B', 0, 19, true],
136+
137+
['1, auto', '20 B', 0, 5, false],
138+
['1, auto', '20 B', $fiveDays, 5, false],
139+
['1, auto', '20 B', $fiveDays, 21, true],
140+
141+
['auto, 1', '20 B', 0, 21, true],
142+
['auto, 1', '20 B', 0, 5, false],
143+
['auto, 1', '20 B', $fiveDays, 5, true],
144+
['auto, 1', '20 B', $oneDay, 5, false],
145+
146+
['2, 10', '20 B', $fiveDays, 5, false],
147+
['2, 10', '20 B', $fiveDays, 20, true],
148+
['2, 10', '20 B', $elevenDays, 5, true],
149+
150+
['10, 2', '20 B', $fiveDays, 5, false],
151+
['10, 2', '20 B', $fiveDays, 21, false],
152+
['10, 2', '20 B', $tenDays, 5, false],
153+
['10, 2', '20 B', $elevenDays, 5, true]
154+
];
155+
}
156+
}

0 commit comments

Comments
 (0)