Skip to content

Commit dfff623

Browse files
author
Kent Delante
committed
fix(files_trashbin): Expire trashbin items when space is needed
Signed-off-by: Kent Delante <kent.delante@proton.me>
1 parent a4d4226 commit dfff623

4 files changed

Lines changed: 181 additions & 2 deletions

File tree

apps/files_trashbin/lib/Command/ExpireTrash.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ protected function configure() {
4545
}
4646

4747
protected function execute(InputInterface $input, OutputInterface $output): int {
48+
$minAge = $this->expiration->getMinAgeAsTimestamp();
4849
$maxAge = $this->expiration->getMaxAgeAsTimestamp();
49-
if (!$maxAge) {
50+
if ($minAge === false && $maxAge === false) {
5051
$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)');
5152
return 1;
5253
}

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
*/

apps/files_trashbin/lib/Trashbin.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -909,10 +909,12 @@ public static function deleteExpiredFiles($files, $user) {
909909
$expiration = Server::get(Expiration::class);
910910
$size = 0;
911911
$count = 0;
912+
$trashbinSize = self::getTrashbinSize($user);
913+
$freeSpace = self::calculateFreeSpace($trashbinSize, $user);
912914
foreach ($files as $file) {
913915
$timestamp = $file['mtime'];
914916
$filename = $file['name'];
915-
if ($expiration->isExpired($timestamp)) {
917+
if ($expiration->isExpired($timestamp, $freeSpace <= 0)) {
916918
try {
917919
$size += self::delete($filename, $user, $timestamp);
918920
$count++;
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<?php
2+
/**
3+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
*/
6+
namespace OCA\Files_Trashbin\Tests\Command;
7+
8+
use OCP\AppFramework\Bootstrap\IRegistrationContext;
9+
use OC\AllConfig;
10+
use OC\Files\View;
11+
use OCA\Files_Trashbin\Command\ExpireTrash;
12+
use OCA\Files_Trashbin\Expiration;
13+
use OCA\Files_Trashbin\Helper;
14+
use OCA\Files_Trashbin\Trashbin;
15+
use OCP\AppFramework\Utility\ITimeFactory;
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 ExpireTrash $command;
35+
private View $userView;
36+
private IConfig $config;
37+
private IUserManager $userManager;
38+
private IUser $user;
39+
private ITimeFactory $timeFactory;
40+
41+
42+
protected function setUp(): void {
43+
parent::setUp();
44+
45+
$this->config = Server::get(IConfig::class);
46+
$this->timeFactory = $this->createMock(ITimeFactory::class);
47+
$this->expiration = Server::get(Expiration::class);
48+
$this->invokePrivate($this->expiration, 'timeFactory', [$this->timeFactory]);
49+
50+
$userId = self::getUniqueID('user');
51+
$this->userManager = Server::get(IUserManager::class);
52+
53+
$this->user = $this->userManager->createUser($userId, $userId);
54+
$this->loginAsUser($userId);
55+
56+
$this->userView = new View('/' . $userId . '/files/');
57+
}
58+
59+
protected function tearDown(): void {
60+
$view = new View('/' . $this->user->getUID());
61+
$view->deleteAll('files');
62+
$view->deleteAll('files_trashbin');
63+
64+
$this->logout();
65+
66+
if (isset($this->user)) {
67+
$this->user->delete();
68+
}
69+
70+
parent::tearDown();
71+
}
72+
73+
/**
74+
* @dataProvider retentionObligationProvider
75+
*/
76+
public function testRetentionObligation(string $obligation, string $quota, int $elapsed, int $fileSize, bool $shouldExpire): void {
77+
$this->config->setSystemValues(['trashbin_retention_obligation' => $obligation]);
78+
$this->expiration->setRetentionObligation($obligation);
79+
80+
$this->command = new ExpireTrash(
81+
Server::get(LoggerInterface::class),
82+
Server::get(IUserManager::class),
83+
$this->expiration
84+
);
85+
86+
$this->user->setQuota($quota);
87+
88+
$file = 'foo.txt';
89+
$handle = $this->userView->fopen($file, 'w');
90+
if (is_resource($handle)) {
91+
fseek($handle, $fileSize, SEEK_CUR);
92+
fwrite($handle, 'a');
93+
fclose($handle);
94+
}
95+
$filemtime = $this->userView->filemtime($file);
96+
$this->timeFactory->expects($this->any())
97+
->method('getTime')
98+
->willReturn($filemtime + $elapsed);
99+
Trashbin::move2trash($file);
100+
101+
$userId = $this->user->getUID();
102+
$trashFiles = Helper::getTrashFiles('/', $userId);
103+
$this->assertEquals(1, count($trashFiles));
104+
105+
$outputInterface = $this->createMock(OutputInterface::class);
106+
$inputInterface = $this->createMock(InputInterface::class);
107+
$inputInterface->expects($this->any())
108+
->method('getArgument')
109+
->with('user_id')
110+
->willReturn([$userId]);
111+
112+
$this->invokePrivate($this->command, 'execute', [$inputInterface, $outputInterface]);
113+
114+
$trashFiles = Helper::getTrashFiles('/', $userId);
115+
$this->assertEquals($shouldExpire ? 0 : 1, count($trashFiles));
116+
}
117+
118+
public function retentionObligationProvider(): array {
119+
$megabyte = 1048576; // 1024 * 1024
120+
$hour = 3600; // 60 * 60
121+
122+
$oneDay = 24 * $hour;
123+
$fiveDays = 24 * 5 * $hour;
124+
$tenDays = 24 * 10 * $hour;
125+
$elevenDays = 24 * 11 * $hour;
126+
127+
return [
128+
['disabled', '20 MB', 0, 1 * $megabyte, false],
129+
130+
['auto', '20 MB', 0, 5 * $megabyte, false],
131+
['auto', '20 MB', 0, 21 * $megabyte, true],
132+
133+
['0, auto', '20 MB', 0, 21 * $megabyte, true],
134+
['0, auto', '20 MB', $oneDay, 5 * $megabyte, false],
135+
['0, auto', '20 MB', $oneDay, 19 * $megabyte, true],
136+
['0, auto', '20 MB', 0, 19 * $megabyte, true],
137+
138+
['auto, 0', '20 MB', $oneDay, 19 * $megabyte, true],
139+
['auto, 0', '20 MB', $oneDay, 21 * $megabyte, true],
140+
['auto, 0', '20 MB', 0, 5 * $megabyte, false],
141+
['auto, 0', '20 MB', 0, 19 * $megabyte, true],
142+
143+
['1, auto', '20 MB', 0, 5 * $megabyte, false],
144+
['1, auto', '20 MB', $fiveDays, 5 * $megabyte, false],
145+
['1, auto', '20 MB', $fiveDays, 21 * $megabyte, true],
146+
147+
['auto, 1', '20 MB', 0, 21 * $megabyte, true],
148+
['auto, 1', '20 MB', 0, 5 * $megabyte, false],
149+
['auto, 1', '20 MB', $fiveDays, 5 * $megabyte, true],
150+
['auto, 1', '20 MB', $oneDay, 5 * $megabyte, false],
151+
152+
['2, 10', '20 MB', $fiveDays, 5 * $megabyte, false],
153+
['2, 10', '20 MB', $fiveDays, 20 * $megabyte, true],
154+
['2, 10', '20 MB', $elevenDays, 5 * $megabyte, true],
155+
156+
['10, 2', '20 MB', $fiveDays, 5 * $megabyte, false],
157+
['10, 2', '20 MB', $fiveDays, 21 * $megabyte, false],
158+
['10, 2', '20 MB', $tenDays, 5 * $megabyte, false],
159+
['10, 2', '20 MB', $elevenDays, 5 * $megabyte, true]
160+
];
161+
}
162+
}

0 commit comments

Comments
 (0)