Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion lib/Folder/FolderManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -976,7 +976,50 @@ public function setFolderQuota(int $folderId, int $quota): void {
->where($query->expr()->eq('folder_id', $query->createNamedParameter($folderId)));
$query->executeStatement();

$this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('The quota for groupfolder with id %d was set to %d bytes', [$folderId, $quota]));
$this->eventDispatcher->dispatchTyped(
new CriticalActionPerformedEvent(
'The quota for groupfolder with id %d was set to %d bytes',
[$folderId, $quota]
)
);

// Invalidate the root etag of this group folder directly in the filecache
// using the root_id already stored in group_folders. This forces desktop
// clients to issue a fresh PROPFIND on the next sync cycle and receive the
// updated quota-available-bytes value.
//
// Direct filecache update is used intentionally:
// - propagateChange() requires a mounted storage (i.e. an active user
// session), which is not guaranteed when an admin changes quota.
// - root_id is always present in group_folders after folder creation,
// so this path works regardless of whether any user is logged in.
try {
$rootIdQuery = $this->connection->getQueryBuilder();
$rootIdQuery->select('root_id')
->from('group_folders')
->where($rootIdQuery->expr()->eq(
'folder_id',
$rootIdQuery->createNamedParameter($folderId, IQueryBuilder::PARAM_INT)
));
$rootIdResult = $rootIdQuery->executeQuery();
$raw = $rootIdResult->fetchOne();
$rootIdResult->closeCursor();
$rootId = is_numeric($raw) ? (int)$raw : 0;

if ($rootId > 0) {
$etagQuery = $this->connection->getQueryBuilder();
$etagQuery->update('filecache')
->set('etag', $etagQuery->createNamedParameter(uniqid()))
->where($etagQuery->expr()->eq(
'fileid',
$etagQuery->createNamedParameter($rootId, IQueryBuilder::PARAM_INT)
));
$etagQuery->executeStatement();
}
} catch (\Throwable) {
// Non-fatal: best-effort invalidation.
// If this fails the stale etag will correct itself on the next full sync.
}
}

/**
Expand Down
87 changes: 83 additions & 4 deletions tests/Folder/FolderManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use OCA\GroupFolders\Mount\FolderStorageManager;
use OCA\GroupFolders\ResponseDefinitions;
use OCP\Constants;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\FileInfo;
use OCP\Files\IMimeTypeLoader;
Expand Down Expand Up @@ -73,11 +74,57 @@ protected function setUp(): void {
}

private function clean(): void {
$query = Server::get(IDBConnection::class)->getQueryBuilder();
$query->delete('group_folders')->executeStatement();
$db = Server::get(IDBConnection::class);

$db->getQueryBuilder()->delete('group_folders')->executeStatement();
$db->getQueryBuilder()->delete('group_folders_groups')->executeStatement();

// Remove any filecache rows seeded by seedFilecacheForFolder().
// storage = 0 is used as a sentinel as no real storage is
// ever assigned numeric ID 0, so this delete is always safe.
$q = $db->getQueryBuilder();
$q->delete('filecache')
->where($q->expr()->eq('storage', $q->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
->executeStatement();
}

$query = Server::get(IDBConnection::class)->getQueryBuilder();
$query->delete('group_folders_groups')->executeStatement();
/**
* Insert a minimal filecache row so that getFolder()'s INNER JOIN on
* filecache succeeds, then patch group_folders.root_id to point at it.
* storage = 0 as a sentinel value that clean() sweeps unconditionally.
*/
private function seedFilecacheForFolder(int $folderId): void {
$db = Server::get(IDBConnection::class);
$now = time();

$insert = $db->getQueryBuilder();
$insert->insert('filecache')
->values([
'storage' => $insert->createNamedParameter(0, IQueryBuilder::PARAM_INT),
'path' => $insert->createNamedParameter(''),
'path_hash' => $insert->createNamedParameter(md5('')),
'parent' => $insert->createNamedParameter(-1, IQueryBuilder::PARAM_INT),
'name' => $insert->createNamedParameter(''),
'mimetype' => $insert->createNamedParameter(0, IQueryBuilder::PARAM_INT),
'mimepart' => $insert->createNamedParameter(0, IQueryBuilder::PARAM_INT),
'size' => $insert->createNamedParameter(0, IQueryBuilder::PARAM_INT),
'mtime' => $insert->createNamedParameter($now, IQueryBuilder::PARAM_INT),
'storage_mtime' => $insert->createNamedParameter($now, IQueryBuilder::PARAM_INT),
'etag' => $insert->createNamedParameter(uniqid()),
'encrypted' => $insert->createNamedParameter(0, IQueryBuilder::PARAM_INT),
'permissions' => $insert->createNamedParameter(0, IQueryBuilder::PARAM_INT),
]);
$insert->executeStatement();
$filecacheId = $insert->getLastInsertId();

$update = $db->getQueryBuilder();
$update->update('group_folders')
->set('root_id', $update->createNamedParameter($filecacheId, IQueryBuilder::PARAM_INT))
->where($update->expr()->eq(
'folder_id',
$update->createNamedParameter($folderId, IQueryBuilder::PARAM_INT)
))
->executeStatement();
}

/**
Expand Down Expand Up @@ -494,6 +541,38 @@ public function testGetFolderPermissionsForUserMerge(): void {
$this->assertEquals(0, $permissions);
}

public function testSetFolderQuotaInvalidatesEtag(): void {
$this->config->expects($this->any())
->method('getSystemValueInt')
->with('groupfolders.quota.default', FileInfo::SPACE_UNLIMITED)
->willReturn(FileInfo::SPACE_UNLIMITED);

$folderId = $this->manager->createFolder('quota-etag-test');
$this->seedFilecacheForFolder($folderId);

$folderBefore = $this->manager->getFolder($folderId);
$this->assertNotNull($folderBefore);
$etagBefore = $folderBefore->rootCacheEntry->getEtag();

$this->manager->setFolderQuota($folderId, 1024 * 1024 * 1024);

$folderAfter = $this->manager->getFolder($folderId);
$this->assertNotNull($folderAfter);
$etagAfter = $folderAfter->rootCacheEntry->getEtag();

$this->assertNotEquals(
$etagBefore,
$etagAfter,
'Etag must change after quota update so desktop clients re-fetch quota-available-bytes via PROPFIND',
);

$this->assertSame(
1024 * 1024 * 1024,
$folderAfter->quota,
'Quota value must be persisted correctly',
);
}

public function testQuotaDefaultValue(): void {
$folderId1 = $this->manager->createFolder('foo');

Expand Down
Loading