Skip to content

Commit bba3e3c

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 13f2306 commit bba3e3c

File tree

5 files changed

+107
-3
lines changed

5 files changed

+107
-3
lines changed

apps/files_trashbin/lib/Sabre/TrashbinPlugin.php

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

11+
use OC\Files\View;
1112
use OCA\DAV\Connector\Sabre\FilesPlugin;
1213
use OCA\Files_Trashbin\Trash\ITrashItem;
1314
use OCP\IPreview;
@@ -32,6 +33,7 @@ class TrashbinPlugin extends ServerPlugin {
3233

3334
public function __construct(
3435
private IPreview $previewManager,
36+
private View $view,
3537
) {
3638
}
3739

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

4143
$this->server->on('propFind', [$this, 'propFind']);
4244
$this->server->on('afterMethod:GET', [$this,'httpGet']);
45+
$this->server->on('beforeMove', [$this, 'beforeMove']);
4346
}
4447

4548

@@ -129,4 +132,40 @@ public function httpGet(RequestInterface $request, ResponseInterface $response):
129132
$response->addHeader('Content-Disposition', 'attachment; filename="' . $node->getFilename() . '"');
130133
}
131134
}
135+
136+
/**
137+
* Check if a user has available space before attempting to
138+
* restore from trashbin unless they have unlimited quota.
139+
*
140+
* @param string $sourcePath
141+
* @param string $destinationPath
142+
* @return bool
143+
*/
144+
public function beforeMove(string $sourcePath, string $destinationPath): bool {
145+
try {
146+
$node = $this->server->tree->getNodeForPath($sourcePath);
147+
$destinationNode = $this->server->tree->getNodeForPath(dirname($destinationPath));
148+
} catch (\Exception $e) {
149+
return true;
150+
}
151+
152+
// Check if a file is being restored before proceeding
153+
if (!$node instanceof ITrash || !$destinationNode instanceof RestoreFolder) {
154+
return true;
155+
}
156+
157+
$fileInfo = $node->getFileInfo();
158+
if (!$fileInfo instanceof ITrashItem) {
159+
return true;
160+
}
161+
$restoreFolder = dirname($fileInfo->getOriginalLocation());
162+
$freeSpace = $this->view->free_space($restoreFolder);
163+
$filesize = $fileInfo->getSize();
164+
if ($freeSpace >= 0 && $freeSpace < $filesize) {
165+
$this->server->httpResponse->setStatus(507);
166+
return false;
167+
}
168+
169+
return true;
170+
}
132171
}

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: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
$trashNode = $this->createMock(ITrash::class);
38+
$trashNode->method('getFileInfo')->willReturn($fileInfo);
39+
$restoreNode = $this->createMock(RestoreFolder::class);
40+
$this->server->tree->method('getNodeForPath')->willReturn($trashNode, $restoreNode);
41+
$previewManager = $this->createMock(IPreview::class);
42+
$view = $this->createMock(View::class);
43+
$view->method('free_space')->willReturn($freeSpace);
44+
$plugin = new TrashbinPlugin($previewManager, $view);
45+
$plugin->initialize($this->server);
46+
47+
$sourcePath = 'trashbin/test/trash/file1';
48+
$destinationPath = 'trashbin/test/restore/file1';
49+
$this->assertEquals($expectedResult, $plugin->beforeMove($sourcePath, $destinationPath));
50+
}
51+
52+
public function quotaProvider(): array {
53+
return [
54+
[ 1024, 512, true ],
55+
[ 512, 513, false ],
56+
[ FileInfo::SPACE_NOT_COMPUTED, 1024, true ],
57+
[ FileInfo::SPACE_UNKNOWN, 1024, true ],
58+
[ FileInfo::SPACE_UNLIMITED, 1024, true ]
59+
];
60+
}
61+
}

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)