Skip to content

Commit 705aee5

Browse files
committed
fix(files_trashbin): check if there is enough space before restoring
Signed-off-by: Kent Delante <kent.delante@proton.me>
1 parent cc22d74 commit 705aee5

File tree

5 files changed

+122
-3
lines changed

5 files changed

+122
-3
lines changed

apps/files_trashbin/lib/Sabre/TrashbinPlugin.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
*/
99
namespace OCA\Files_Trashbin\Sabre;
1010

11+
use OC\Files\FileInfo;
12+
use OC\Files\View;
1113
use OCA\DAV\Connector\Sabre\FilesPlugin;
1214
use OCA\Files_Trashbin\Trash\ITrashItem;
1315
use OCP\IPreview;
16+
use Psr\Log\LoggerInterface;
1417
use Sabre\DAV\INode;
1518
use Sabre\DAV\PropFind;
1619
use Sabre\DAV\Server;
@@ -32,6 +35,7 @@ class TrashbinPlugin extends ServerPlugin {
3235

3336
public function __construct(
3437
private IPreview $previewManager,
38+
private View $view,
3539
) {
3640
}
3741

@@ -40,6 +44,7 @@ public function initialize(Server $server) {
4044

4145
$this->server->on('propFind', [$this, 'propFind']);
4246
$this->server->on('afterMethod:GET', [$this,'httpGet']);
47+
$this->server->on('beforeMove', [$this, 'beforeMove']);
4348
}
4449

4550

@@ -129,4 +134,47 @@ public function httpGet(RequestInterface $request, ResponseInterface $response):
129134
$response->addHeader('Content-Disposition', 'attachment; filename="' . $node->getFilename() . '"');
130135
}
131136
}
137+
138+
/**
139+
* Check if a user has available space before attempting to
140+
* restore from trashbin unless they have unlimited quota.
141+
*
142+
* @param string $sourcePath
143+
* @param string $destinationPath
144+
* @return bool
145+
*/
146+
public function beforeMove(string $sourcePath, string $destinationPath): bool {
147+
try {
148+
$node = $this->server->tree->getNodeForPath($sourcePath);
149+
$destinationNodeParent = $this->server->tree->getNodeForPath(dirname($destinationPath));
150+
} catch (\Sabre\DAV\Exception $e) {
151+
\OCP\Server::get(LoggerInterface::class)
152+
->error($e->getMessage(), ['app' => 'files_trashbin', 'exception' => $e]);
153+
return true;
154+
}
155+
156+
// Check if a file is being restored before proceeding
157+
if (!$node instanceof ITrash || !$destinationNodeParent instanceof RestoreFolder) {
158+
return true;
159+
}
160+
161+
$fileInfo = $node->getFileInfo();
162+
if (!$fileInfo instanceof ITrashItem) {
163+
return true;
164+
}
165+
$restoreFolder = dirname($fileInfo->getOriginalLocation());
166+
$freeSpace = $this->view->free_space($restoreFolder);
167+
if ($freeSpace === FileInfo::SPACE_NOT_COMPUTED ||
168+
$freeSpace === FileInfo::SPACE_UNKNOWN ||
169+
$freeSpace === FileInfo::SPACE_UNLIMITED) {
170+
return true;
171+
}
172+
$filesize = $fileInfo->getSize();
173+
if ($freeSpace < $filesize) {
174+
$this->server->httpResponse->setStatus(507);
175+
return false;
176+
}
177+
178+
return true;
179+
}
132180
}

apps/files_trashbin/src/files_actions/restoreAction.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55
import { getCurrentUser } from '@nextcloud/auth'
6+
import { showError } from '@nextcloud/dialogs'
67
import { emit } from '@nextcloud/event-bus'
78
import { Permission, Node, View, FileAction } from '@nextcloud/files'
89
import { t } from '@nextcloud/l10n'
@@ -52,6 +53,9 @@ export const restoreAction = new FileAction({
5253
emit('files:node:deleted', node)
5354
return true
5455
} catch (error) {
56+
if (error.response?.status === 507) {
57+
showError(t('files_trashbin', 'Not enough free space to restore the file/folder'))
58+
}
5559
logger.error('Failed to restore node', { error, node })
5660
return false
5761
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
namespace OCA\Files_Trashbin\Tests\Sabre;
9+
10+
use OC\Files\FileInfo;
11+
use OC\Files\View;
12+
use OCA\Files_Trashbin\Sabre\ITrash;
13+
use OCA\Files_Trashbin\Sabre\RestoreFolder;
14+
use OCA\Files_Trashbin\Sabre\TrashbinPlugin;
15+
use OCA\Files_Trashbin\Trash\ITrashItem;
16+
use OCP\IPreview;
17+
use Sabre\DAV\Server;
18+
use Sabre\DAV\Tree;
19+
use Test\TestCase;
20+
21+
class TrashbinPluginTest extends TestCase {
22+
private Server $server;
23+
24+
protected function setUp(): void {
25+
parent::setUp();
26+
27+
$tree = $this->createMock(Tree::class);
28+
$this->server = new Server($tree);
29+
}
30+
31+
/**
32+
* @dataProvider quotaProvider
33+
*/
34+
public function testQuota(int $quota, int $fileSize, bool $expectedResult): void {
35+
$fileInfo = $this->createMock(ITrashItem::class);
36+
$fileInfo->method('getSize')->willReturn($fileSize);
37+
38+
$trashNode = $this->createMock(ITrash::class);
39+
$trashNode->method('getFileInfo')->willReturn($fileInfo);
40+
41+
$restoreNode = $this->createMock(RestoreFolder::class);
42+
43+
$this->server->tree->method('getNodeForPath')->willReturn($trashNode, $restoreNode);
44+
45+
$previewManager = $this->createMock(IPreview::class);
46+
47+
$view = $this->createMock(View::class);
48+
$view->method('free_space')->willReturn($quota);
49+
50+
$plugin = new TrashbinPlugin($previewManager, $view);
51+
$plugin->initialize($this->server);
52+
53+
$sourcePath = 'trashbin/test/trash/file1';
54+
$destinationPath = 'trashbin/test/restore/file1';
55+
$this->assertEquals($expectedResult, $plugin->beforeMove($sourcePath, $destinationPath));
56+
}
57+
58+
public function quotaProvider(): array {
59+
return [
60+
[ 1024, 512, true ],
61+
[ 512, 513, false ],
62+
[ FileInfo::SPACE_NOT_COMPUTED, 1024, true ],
63+
[ FileInfo::SPACE_UNKNOWN, 1024, true ],
64+
[ FileInfo::SPACE_UNLIMITED, 1024, true ]
65+
];
66+
}
67+
}

dist/files_trashbin-init.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/files_trashbin-init.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)