From 274b213b13dccf7f37b4ae0603ac691800cfcbb7 Mon Sep 17 00:00:00 2001 From: Git'Fellow <12234510+solracsf@users.noreply.github.com> Date: Wed, 26 Aug 2026 20:28:00 +0200 Subject: [PATCH] fix: Stop trash expiry once a folder is back under its quota The quota flag handed to Expiration::isExpired() was computed from a value that never changed while the loop ran: bb5862ed replaced "$folder['size'] -= ..." with "$size -= ...", right after "$size += ...", so the two cancel out. Once a folder was over quota every remaining trash item was therefore reported as expired and the whole trashbin was purged in one run, instead of stopping as soon as enough space had been freed. Decrement $sizeInTrash instead, which is the value the quota check actually sums, and keep the folder size in its own variable. That also leaves $size as what the return value is documented to be, the number of bytes freed, rather than the last folder's root size. Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com> --- lib/Trash/TrashBackend.php | 6 +++--- tests/Trash/TrashBackendTest.php | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/lib/Trash/TrashBackend.php b/lib/Trash/TrashBackend.php index 5bae78243..43676776c 100644 --- a/lib/Trash/TrashBackend.php +++ b/lib/Trash/TrashBackend.php @@ -656,7 +656,7 @@ public function expire(Expiration $expiration): array { $sizeInTrash += $node->getSize(); } - $size = $folder->rootCacheEntry->getSize(); + $folderSize = $folder->rootCacheEntry->getSize(); foreach ($trashItems as $groupTrashItem) { $nodeName = $groupTrashItem['name'] . '.d' . $groupTrashItem['deleted_time']; @@ -666,7 +666,7 @@ public function expire(Expiration $expiration): array { $node = $nodes[$nodeName]; - if ($expiration->isExpired($groupTrashItem['deleted_time'], $folder->quota > 0 && $folder->quota < ($size + $sizeInTrash))) { + if ($expiration->isExpired($groupTrashItem['deleted_time'], $folder->quota > 0 && $folder->quota < ($folderSize + $sizeInTrash))) { $this->logger->debug('expiring ' . $node->getPath()); if (!$this->unlinkTrashNode($node)) { $this->logger->error('Failed to remove item from trashbin: ' . $node->getPath()); @@ -676,7 +676,7 @@ public function expire(Expiration $expiration): array { // only count up after checking if removal is possible $count += 1; $size += $node->getSize(); - $size -= $node->getSize(); + $sizeInTrash -= $node->getSize(); $node->getStorage()->getCache()->remove($node->getInternalPath()); $this->trashManager->removeItem($folderId, $groupTrashItem['name'], $groupTrashItem['deleted_time']); if (!is_null($groupTrashItem['file_id']) && !is_null($this->versionsBackend)) { diff --git a/tests/Trash/TrashBackendTest.php b/tests/Trash/TrashBackendTest.php index 4ab316b7b..522d6d7b7 100644 --- a/tests/Trash/TrashBackendTest.php +++ b/tests/Trash/TrashBackendTest.php @@ -485,4 +485,30 @@ public function testRemoveItemWithMissingTrashFile(): void { $this->logout(); } + + public function testExpireStopsOnceBackUnderQuota(): void { + $this->loginAsUser('manager'); + + foreach (['a.txt', 'b.txt', 'c.txt'] as $name) { + $file = $this->managerUserFolder->newFile("{$this->folderName}/$name", str_repeat('x', 100)); + $this->trashBackend->moveToTrash($file->getStorage(), $file->getInternalPath()); + } + + $this->assertCount(3, $this->trashManager->listTrashForFolders([$this->folderId])); + + // over quota by less than one trash item, so expiring a single item is enough + $folderSize = $this->folderManager->getAllFoldersWithSize()[$this->folderId]->rootCacheEntry->getSize(); + $this->folderManager->setFolderQuota($this->folderId, (int)$folderSize + 250); + + $expiration = $this->createMock(Expiration::class); + $expiration->method('isExpired')->willReturnCallback(fn (int $timestamp, bool $quotaExceeded): bool => $quotaExceeded); + + [$count, $size] = $this->trashBackend->expire($expiration); + + $this->assertSame(1, $count); + $this->assertSame(100, $size); + $this->assertCount(2, $this->trashManager->listTrashForFolders([$this->folderId])); + + $this->logout(); + } }