From e44e78c5c8b4b3c1614b3ce211a39e046598ae04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darel=20Mart=C3=ADnez=20Caballero?= Date: Tue, 13 Oct 2026 20:52:48 +0100 Subject: [PATCH] =?UTF-8?q?fix(dav):=20skip=20quota=20check=20when=20movin?= =?UTF-8?q?g=20file=20within=20the=20same=20storage=20Signed-off-by:=20Dar?= =?UTF-8?q?el=20Mart=C3=ADnez=20Caballero=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/dav/lib/Connector/Sabre/QuotaPlugin.php | 16 ++++ .../unit/Connector/Sabre/QuotaPluginTest.php | 95 +++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/apps/dav/lib/Connector/Sabre/QuotaPlugin.php b/apps/dav/lib/Connector/Sabre/QuotaPlugin.php index df28c02242abe..337a7ef5eb2aa 100644 --- a/apps/dav/lib/Connector/Sabre/QuotaPlugin.php +++ b/apps/dav/lib/Connector/Sabre/QuotaPlugin.php @@ -167,6 +167,22 @@ public function beforeMove(string $sourcePath, string $destinationPath): bool { return true; } + // When moving a file within the same storage, net space consumed on that storage is zero. + // Bypassing the quota check prevents false InsufficientStorage exceptions when free space + // on the disk is smaller than the file being moved. + try { + if ($sourceNode instanceof Node) { + $sourceStorage = $sourceNode->getFileInfo()?->getStorage(); + $destInfo = $this->view->getFileInfo(ltrim($quotaPath, '/')); + $destStorage = $destInfo?->getStorage(); + if ($sourceStorage !== null && $destStorage !== null && $sourceStorage->getId() === $destStorage->getId()) { + return true; + } + } + } catch (\Throwable) { + // Fallback to regular quota check if storage detection fails + } + return $this->checkQuota($quotaPath, $sourceNode->getSize()); } diff --git a/apps/dav/tests/unit/Connector/Sabre/QuotaPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/QuotaPluginTest.php index d355ee87f6f7d..30bf90e50e009 100644 --- a/apps/dav/tests/unit/Connector/Sabre/QuotaPluginTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/QuotaPluginTest.php @@ -63,6 +63,101 @@ public function testCheckQuotaOnPath(int $quota, array $headers): void { $this->assertTrue($result); } + public function testBeforeMoveSameStorageSkipsQuota(): void { + $sourceStorage = $this->createMock(\OCP\Files\Storage\IStorage::class); + $sourceStorage->method('getId')->willReturn('storage1'); + + $sourceFileInfo = $this->createMock(FileInfo::class); + $sourceFileInfo->method('getStorage')->willReturn($sourceStorage); + + $sourceNode = $this->createMock(\OCA\DAV\Connector\Sabre\File::class); + $sourceNode->method('getFileInfo')->willReturn($sourceFileInfo); + $sourceNode->method('getSize')->willReturn(1024 * 1024 * 1024); + + $destStorage = $this->createMock(\OCP\Files\Storage\IStorage::class); + $destStorage->method('getId')->willReturn('storage1'); + + $destFileInfo = $this->createMock(FileInfo::class); + $destFileInfo->method('getStorage')->willReturn($destStorage); + + $parentNode = $this->createMock(\OCA\DAV\Connector\Sabre\Directory::class); + $parentNode->method('getPath')->willReturn('dest_folder'); + + $tree = $this->createMock(\Sabre\DAV\Tree::class); + $tree->method('getNodeForPath') + ->willReturnMap([ + ['source_file', $sourceNode], + ['dest_folder', $parentNode], + ]); + $tree->method('nodeExists') + ->with('dest_folder/target_file') + ->willReturn(false); + + $view = $this->getMockBuilder(View::class) + ->onlyMethods(['free_space', 'getFileInfo']) + ->disableOriginalConstructor() + ->getMock(); + $view->method('free_space')->willReturn(100); + $view->method('getFileInfo')->with('dest_folder')->willReturn($destFileInfo); + + $server = new \Sabre\DAV\Server(); + $server->tree = $tree; + + $plugin = new QuotaPlugin($view); + $plugin->initialize($server); + + $result = $plugin->beforeMove('source_file', 'dest_folder/target_file'); + $this->assertTrue($result); + } + + public function testBeforeMoveDifferentStorageExceedsQuota(): void { + $this->expectException(\Sabre\DAV\Exception\InsufficientStorage::class); + + $sourceStorage = $this->createMock(\OCP\Files\Storage\IStorage::class); + $sourceStorage->method('getId')->willReturn('storage1'); + + $sourceFileInfo = $this->createMock(FileInfo::class); + $sourceFileInfo->method('getStorage')->willReturn($sourceStorage); + + $sourceNode = $this->createMock(\OCA\DAV\Connector\Sabre\File::class); + $sourceNode->method('getFileInfo')->willReturn($sourceFileInfo); + $sourceNode->method('getSize')->willReturn(1024 * 1024 * 1024); + + $destStorage = $this->createMock(\OCP\Files\Storage\IStorage::class); + $destStorage->method('getId')->willReturn('storage2'); + + $destFileInfo = $this->createMock(FileInfo::class); + $destFileInfo->method('getStorage')->willReturn($destStorage); + + $parentNode = $this->createMock(\OCA\DAV\Connector\Sabre\Directory::class); + $parentNode->method('getPath')->willReturn('dest_folder'); + + $tree = $this->createMock(\Sabre\DAV\Tree::class); + $tree->method('getNodeForPath') + ->willReturnMap([ + ['source_file', $sourceNode], + ['dest_folder', $parentNode], + ]); + $tree->method('nodeExists') + ->with('dest_folder/target_file') + ->willReturn(false); + + $view = $this->getMockBuilder(View::class) + ->onlyMethods(['free_space', 'getFileInfo']) + ->disableOriginalConstructor() + ->getMock(); + $view->method('free_space')->willReturn(100); + $view->method('getFileInfo')->with('dest_folder')->willReturn($destFileInfo); + + $server = new \Sabre\DAV\Server(); + $server->tree = $tree; + + $plugin = new QuotaPlugin($view); + $plugin->initialize($server); + + $plugin->beforeMove('source_file', 'dest_folder/target_file'); + } + public static function quotaOkayProvider(): array { return [ [1024, []],