Skip to content

Commit eb5bc3e

Browse files
committed
fix(quota): invalidate user folder etag when quota changes
Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>
1 parent 456684f commit eb5bc3e

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

lib/private/User/User.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use OCP\Comments\ICommentsManager;
1616
use OCP\EventDispatcher\IEventDispatcher;
1717
use OCP\Files\FileInfo;
18+
use OCP\Files\IRootFolder;
1819
use OCP\Group\Events\BeforeUserRemovedEvent;
1920
use OCP\Group\Events\UserRemovedEvent;
2021
use OCP\IAvatarManager;
@@ -572,6 +573,20 @@ public function setQuota($quota): void {
572573
if ($quota !== $oldQuota) {
573574
$this->config->setUserValue($this->uid, 'files', 'quota', $quota);
574575
$this->triggerChange('quota', $quota, $oldQuota);
576+
// Invalidate the etag of the user's home folder so desktop clients
577+
// re-fetch quota-available-bytes via PROPFIND on the next sync cycle.
578+
// ICache::update() routes by fileid (the shard key), so this is safe
579+
// in sharded database setups. getUserFolder() uses LazyUserFolder when
580+
// no active mount exists, deferring setup until the getId() call below.
581+
try {
582+
$userFolder = Server::get(IRootFolder::class)->getUserFolder($this->uid);
583+
$userFolder->getStorage()->getCache()->update(
584+
$userFolder->getId(),
585+
['etag' => uniqid()]
586+
);
587+
} catch (\Throwable) {
588+
// Non-fatal: best-effort etag invalidation; stale quota corrects on next full sync.
589+
}
575590
}
576591
\OC_Helper::clearStorageInfo('/' . $this->uid . '/files');
577592
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
namespace Test\User;
9+
10+
use OCP\Files\IRootFolder;
11+
use OCP\IUserManager;
12+
use OCP\Server;
13+
use PHPUnit\Framework\Attributes\Group;
14+
use Test\TestCase;
15+
use Test\Traits\UserTrait;
16+
17+
#[Group('DB')]
18+
class UserIntegrationTest extends TestCase {
19+
use UserTrait;
20+
21+
protected function setUp(): void {
22+
parent::setUp();
23+
$this->setUpUserTrait();
24+
}
25+
26+
protected function tearDown(): void {
27+
$this->tearDownUserTrait();
28+
parent::tearDown();
29+
}
30+
31+
public function testSetQuotaInvalidatesUserFolderEtag(): void {
32+
$userId = $this->getUniqueID('quota_etag_test_');
33+
$userManager = Server::get(IUserManager::class);
34+
$this->createUser($userId, 'password123');
35+
36+
try {
37+
static::loginAsUser($userId);
38+
39+
$etagBefore = $this->readFilesEtag($userId);
40+
41+
$user = $userManager->get($userId);
42+
$user->setQuota('5 GB');
43+
44+
$etagAfter = $this->readFilesEtag($userId);
45+
46+
$this->assertNotNull($etagBefore, 'files/ cache entry must exist before quota change');
47+
$this->assertNotEquals($etagBefore, $etagAfter, 'User folder etag must change when quota is updated');
48+
} finally {
49+
static::logout();
50+
$userManager->get($userId)?->delete();
51+
}
52+
}
53+
54+
/**
55+
* Reads the current etag of the user's 'files' directory via the storage's
56+
* own ICache implementation. This uses the storage-scoped, shard-aware query
57+
* builder and avoids cross-partition JOINs, so it works correctly in both
58+
* standard and sharded database setups.
59+
*/
60+
private function readFilesEtag(string $userId): ?string {
61+
$userFolder = Server::get(IRootFolder::class)->getUserFolder($userId);
62+
$entry = $userFolder->getStorage()->getCache()->get('files');
63+
return $entry !== false ? (string)$entry['etag'] : null;
64+
}
65+
}

0 commit comments

Comments
 (0)