Skip to content

Commit dda91c4

Browse files
Merge pull request #53836 from nextcloud/backport/53112/stable30
2 parents e6f3d7c + 6ac8901 commit dda91c4

File tree

3 files changed

+176
-4
lines changed

3 files changed

+176
-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
namespace OCA\Files_Trashbin\Command;
99

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;
@@ -56,8 +55,9 @@ protected function configure() {
5655
}
5756

5857
protected function execute(InputInterface $input, OutputInterface $output): int {
58+
$minAge = $this->expiration->getMinAgeAsTimestamp();
5959
$maxAge = $this->expiration->getMaxAgeAsTimestamp();
60-
if (!$maxAge) {
60+
if ($minAge === false && $maxAge === false) {
6161
$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)');
6262
return 1;
6363
}
@@ -95,8 +95,7 @@ public function expireTrashForUser(IUser $user) {
9595
if (!$this->setupFS($uid)) {
9696
return;
9797
}
98-
$dirContent = Helper::getTrashFiles('/', $uid, 'mtime');
99-
Trashbin::deleteExpiredFiles($dirContent, $uid);
98+
Trashbin::expire($uid);
10099
} catch (\Throwable $e) {
101100
$this->logger->error('Error while expiring trashbin for user ' . $user->getUID(), ['exception' => $e]);
102101
}

apps/files_trashbin/lib/Expiration.php

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

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

0 commit comments

Comments
 (0)