From 8b82352d63b6220dc231458792aff73699f31355 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 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/files_trashbin/lib/Trashbin.php b/apps/files_trashbin/lib/Trashbin.php index 091f1d3d1e41c..16f809dfa0c00 100644 --- a/apps/files_trashbin/lib/Trashbin.php +++ b/apps/files_trashbin/lib/Trashbin.php @@ -27,6 +27,7 @@ use OCP\EventDispatcher\IEventDispatcher; 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; @@ -487,6 +488,13 @@ public static function restore($file, $filename, $timestamp) { $sourceNode = self::getNodeForPath($sourcePath); $targetNode = self::getNodeForPath($targetPath); + + $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 = \OC::$server->get(IEventDispatcher::class); From d032b905f6f9fef41a0caf86dd0663644a65fe08 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 | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/apps/files_trashbin/lib/Sabre/TrashbinPlugin.php b/apps/files_trashbin/lib/Sabre/TrashbinPlugin.php index 362e0b9fb1a48..d82ae04da4673 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,15 +37,20 @@ class TrashbinPlugin extends ServerPlugin { /** @var IPreview */ private $previewManager; - /** @var View */ - private $view; + /** @var IRootFolder */ + private $rootFolder; + + /** @var IMountManager */ + private $mountManager; public function __construct( IPreview $previewManager, - View $view + IRootFolder $rootFolder, + IMountManager $mountManager, ) { $this->previewManager = $previewManager; - $this->view = $view; + $this->rootFolder = $rootFolder; + $this->mountManager = $mountManager; } public function initialize(Server $server) { @@ -163,9 +169,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 89415b62e91d5458c687ffc9654d03752bffb8b9 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 60cc69ba09d83..c1dddd8c37bea 100644 --- a/apps/files_trashbin/tests/Sabre/TrashbinPluginTest.php +++ b/apps/files_trashbin/tests/Sabre/TrashbinPluginTest.php @@ -8,12 +8,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 @@ protected function setUp(): void { 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')->willReturn($fileInfo); @@ -44,10 +56,29 @@ public function testQuota(int $quota, int $fileSize, bool $expectedResult): void $previewManager = $this->createMock(IPreview::class); - $view = $this->createMock(View::class); - $view->method('free_space')->willReturn($quota); + $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); - $plugin = new TrashbinPlugin($previewManager, $view); + $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, $rootFolder, $mountManager); $plugin->initialize($this->server); $sourcePath = 'trashbin/test/trash/file1'; @@ -60,7 +91,7 @@ public 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 ] ]; }