From f884ab8e71d18b30c8a17359b1353461a9cf760a Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 28 Jul 2026 20:58:07 +0200 Subject: [PATCH 1/3] fix: prevent restoring a file that's larger than the free space Signed-off-by: Robin Appelman --- apps/files_trashbin/lib/Trashbin.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/files_trashbin/lib/Trashbin.php b/apps/files_trashbin/lib/Trashbin.php index 65ffe47e97c9e..48c12fc362cfe 100644 --- a/apps/files_trashbin/lib/Trashbin.php +++ b/apps/files_trashbin/lib/Trashbin.php @@ -34,6 +34,7 @@ use OCP\Files\IMimeTypeLoader; use OCP\Files\IRootFolder; use OCP\Files\Node; +use OCP\Files\NotEnoughSpaceException; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\Files\Storage\ILockingStorage; @@ -561,6 +562,14 @@ public static function restore($file, $filename, $timestamp) { $sourceNode = self::getNodeForPath($user, $sourcePath); $targetNode = self::getNodeForPath($user, $targetPath, 'files'); + + $targetParent = $targetNode->getParent(); + + $free = $targetParent->getFreeSpace(); + if ($free >= 0 && $free < $sourceNode->getSize(false)) { + throw new NotEnoughSpaceException('Not enough free space in ' . $targetParent->getPath() . ' to restore ' . $sourceNode->getPath()); + } + $run = true; $event = new BeforeNodeRestoredEvent($sourceNode, $targetNode, $run); $dispatcher = Server::get(IEventDispatcher::class); From 6e207b9dc4776dc635616db108ba268a341f2b34 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 28 Jul 2026 21:31:35 +0200 Subject: [PATCH 2/3] fix: check free space in the proper folder when restoring from trash Signed-off-by: Robin Appelman --- .../lib/Sabre/TrashbinPlugin.php | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/apps/files_trashbin/lib/Sabre/TrashbinPlugin.php b/apps/files_trashbin/lib/Sabre/TrashbinPlugin.php index b4dc7b6b25576..458d7b185f662 100644 --- a/apps/files_trashbin/lib/Sabre/TrashbinPlugin.php +++ b/apps/files_trashbin/lib/Sabre/TrashbinPlugin.php @@ -9,9 +9,10 @@ namespace OCA\Files_Trashbin\Sabre; use OC\Files\FileInfo; -use OC\Files\View; use OCA\DAV\Connector\Sabre\FilesPlugin; use OCA\Files_Trashbin\Trash\ITrashItem; +use OCP\Files\IRootFolder; +use OCP\Files\Mount\IMountManager; use OCP\IPreview; use Psr\Log\LoggerInterface; use Sabre\DAV\INode; @@ -36,7 +37,8 @@ class TrashbinPlugin extends ServerPlugin { public function __construct( private IPreview $previewManager, - private View $view, + private IRootFolder $rootFolder, + private IMountManager $mountManager, ) { } @@ -164,9 +166,20 @@ public function beforeMove(string $sourcePath, string $destinationPath): bool { if (!$fileInfo instanceof ITrashItem) { return true; } - $restoreFolder = dirname($fileInfo->getOriginalLocation()); - $freeSpace = $this->view->free_space($restoreFolder); - if ($freeSpace === FileInfo::SPACE_NOT_COMPUTED + + $userFolder = $this->rootFolder->getUserFolder($fileInfo->getUser()->getUID()); + $originalPath = $userFolder->getFullPath(dirname($fileInfo->getOriginalLocation())); + + // Since the parent folder might no longer exist, we don't try to get the parent node to check the free space + // instead we resolve the mount where the restore would go to, and check that for the free space + $originalMount = $this->mountManager->find($originalPath); + if (!$originalMount) { + throw new \Exception('no mount found when looking for restore path'); + } + $freeSpace = $originalMount->getStorage()->free_space($originalMount->getInternalPath($originalPath)); + + if ( + $freeSpace === FileInfo::SPACE_NOT_COMPUTED || $freeSpace === FileInfo::SPACE_UNKNOWN || $freeSpace === FileInfo::SPACE_UNLIMITED) { return true; From 16b548f70b5f081db1ed9a75aca6e3305af5e6a0 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 3 Aug 2026 23:42:04 +0200 Subject: [PATCH 3/3] test: adjust tests to new trashbin quota check Signed-off-by: Robin Appelman --- .../tests/Sabre/TrashbinPluginTest.php | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/apps/files_trashbin/tests/Sabre/TrashbinPluginTest.php b/apps/files_trashbin/tests/Sabre/TrashbinPluginTest.php index 87aca2753effd..995be9df70aca 100644 --- a/apps/files_trashbin/tests/Sabre/TrashbinPluginTest.php +++ b/apps/files_trashbin/tests/Sabre/TrashbinPluginTest.php @@ -9,12 +9,17 @@ namespace OCA\Files_Trashbin\Tests\Sabre; use OC\Files\FileInfo; -use OC\Files\View; use OCA\Files_Trashbin\Sabre\ITrash; use OCA\Files_Trashbin\Sabre\RestoreFolder; use OCA\Files_Trashbin\Sabre\TrashbinPlugin; use OCA\Files_Trashbin\Trash\ITrashItem; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\Mount\IMountManager; +use OCP\Files\Mount\IMountPoint; +use OCP\Files\Storage\IStorage; use OCP\IPreview; +use OCP\IUser; use Sabre\DAV\Server; use Sabre\DAV\Tree; use Test\TestCase; @@ -34,6 +39,13 @@ public function testQuota(int $quota, int $fileSize, bool $expectedResult): void $fileInfo = $this->createMock(ITrashItem::class); $fileInfo->method('getSize') ->willReturn($fileSize); + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('test'); + $fileInfo->method('getUser') + ->willReturn($user); + $fileInfo->method('getOriginalLocation') + ->willReturn('relative/original/location'); $trashNode = $this->createMock(ITrash::class); $trashNode->method('getFileInfo') @@ -46,11 +58,30 @@ public function testQuota(int $quota, int $fileSize, bool $expectedResult): void $previewManager = $this->createMock(IPreview::class); - $view = $this->createMock(View::class); - $view->method('free_space') + $userFolder = $this->createMock(Folder::class); + $userFolder->method('getFullPath') + ->with('relative/original') // the parent path + ->willReturn('/full/path/to/original'); + $rootFolder = $this->createMock(IRootFolder::class); + $rootFolder->method('getUserFolder') + ->willReturn($userFolder); + + $storage = $this->createMock(IStorage::class); + $storage->method('free_space') + ->with('path/to/original') ->willReturn($quota); + $mount = $this->createMock(IMountPoint::class); + $mount->method('getStorage') + ->willReturn($storage); + $mount->method('getInternalPath') + ->with('/full/path/to/original') + ->willReturn('path/to/original'); + $mountManager = $this->createMock(IMountManager::class); + $mountManager->method('find') + ->with('/full/path/to/original') + ->willReturn($mount); - $plugin = new TrashbinPlugin($previewManager, $view); + $plugin = new TrashbinPlugin($previewManager, $rootFolder, $mountManager); $plugin->initialize($this->server); $sourcePath = 'trashbin/test/trash/file1'; @@ -63,7 +94,7 @@ public static function quotaProvider(): array { [ 1024, 512, true ], [ 512, 513, false ], [ FileInfo::SPACE_NOT_COMPUTED, 1024, true ], - [ FileInfo::SPACE_UNKNOWN, 1024, true ], + [ FileInfo::SPACE_UNKNOWN, 1024, true], [ FileInfo::SPACE_UNLIMITED, 1024, true ] ]; }