From 71442ef5fbbf70f73bf05e0f49f63b231bd72523 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 24 Sep 2026 14:00:45 +0200 Subject: [PATCH 1/6] test(files_sharing): Check mount events are dispatched for share recipients Apps keep per-user data in sync with file access by listening to UserMountAddedEvent and UserMountRemovedEvent. When a share is created or deleted, these events should reach the recipient at the latest when the recipient next sets up their file system. Assisted-by: ClaudeCode:claude-opus-5.5 Signed-off-by: Marcel Klehr --- .../tests/ShareMountEventsTest.php | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 apps/files_sharing/tests/ShareMountEventsTest.php diff --git a/apps/files_sharing/tests/ShareMountEventsTest.php b/apps/files_sharing/tests/ShareMountEventsTest.php new file mode 100644 index 0000000000000..02faac5783f47 --- /dev/null +++ b/apps/files_sharing/tests/ShareMountEventsTest.php @@ -0,0 +1,130 @@ + */ + private array $events = []; + /** @var \Closure(Event): void */ + private \Closure $listener; + + protected function setUp(): void { + parent::setUp(); + + $this->eventDispatcher = Server::get(IEventDispatcher::class); + $this->listener = function (Event $event): void { + $this->events[] = $event; + }; + $this->eventDispatcher->addListener(UserMountAddedEvent::class, $this->listener); + $this->eventDispatcher->addListener(UserMountRemovedEvent::class, $this->listener); + + $this->rootFolder->getUserFolder(self::TEST_FILES_SHARING_API_USER1)->newFolder(self::FOLDER); + + // The recipient has used their file system before + $this->setupFileSystem(self::TEST_FILES_SHARING_API_USER2); + $this->loginHelper(self::TEST_FILES_SHARING_API_USER1); + $this->events = []; + } + + protected function tearDown(): void { + $this->eventDispatcher->removeListener(UserMountAddedEvent::class, $this->listener); + $this->eventDispatcher->removeListener(UserMountRemovedEvent::class, $this->listener); + Server::get(SharesUpdatedListener::class)->setCutOffMarkTime(-1); + + parent::tearDown(); + } + + /** + * @return array + */ + public static function shareProvider(): array { + // A cutoff time of -1 updates the share mounts of recipients immediately, + // 0 marks recipients so their share mounts are updated on their next setup + return [ + 'user share, immediate update' => [IShare::TYPE_USER, self::TEST_FILES_SHARING_API_USER2, -1], + 'user share, deferred update' => [IShare::TYPE_USER, self::TEST_FILES_SHARING_API_USER2, 0], + 'group share, immediate update' => [IShare::TYPE_GROUP, self::TEST_FILES_SHARING_API_GROUP1, -1], + 'group share, deferred update' => [IShare::TYPE_GROUP, self::TEST_FILES_SHARING_API_GROUP1, 0], + ]; + } + + #[DataProvider(methodName: 'shareProvider')] + public function testMountAddedEventForCreatedShare(int $shareType, string $recipient, float $cutOffTime): void { + Server::get(SharesUpdatedListener::class)->setCutOffMarkTime($cutOffTime); + + $this->share($shareType, self::FOLDER, self::TEST_FILES_SHARING_API_USER1, $recipient, Constants::PERMISSION_ALL); + $this->setupFileSystem(self::TEST_FILES_SHARING_API_USER2); + + $this->assertEquals( + ['/' . self::TEST_FILES_SHARING_API_USER2 . '/files/' . self::FOLDER . '/'], + $this->getMountPointsOfEvents(UserMountAddedEvent::class, self::TEST_FILES_SHARING_API_USER2), + 'A UserMountAddedEvent should be dispatched for the share mount of the recipient', + ); + } + + #[DataProvider(methodName: 'shareProvider')] + public function testMountRemovedEventForDeletedShare(int $shareType, string $recipient, float $cutOffTime): void { + $share = $this->share($shareType, self::FOLDER, self::TEST_FILES_SHARING_API_USER1, $recipient, Constants::PERMISSION_ALL); + $this->setupFileSystem(self::TEST_FILES_SHARING_API_USER2); + $this->loginHelper(self::TEST_FILES_SHARING_API_USER1); + $this->events = []; + + Server::get(SharesUpdatedListener::class)->setCutOffMarkTime($cutOffTime); + $this->shareManager->deleteShare($share); + $this->setupFileSystem(self::TEST_FILES_SHARING_API_USER2); + + $this->assertEquals( + ['/' . self::TEST_FILES_SHARING_API_USER2 . '/files/' . self::FOLDER . '/'], + $this->getMountPointsOfEvents(UserMountRemovedEvent::class, self::TEST_FILES_SHARING_API_USER2), + 'A UserMountRemovedEvent should be dispatched for the share mount of the recipient', + ); + } + + /** + * Log in as the user and access their files, as a new request by them would + */ + private function setupFileSystem(string $userId): void { + $this->loginHelper($userId); + $this->rootFolder->getUserFolder($userId)->getDirectoryListing(); + } + + /** + * @param class-string $eventClass + * @return list + */ + private function getMountPointsOfEvents(string $eventClass, string $userId): array { + $mountPoints = []; + foreach ($this->events as $event) { + if ($event instanceof $eventClass && $event->mountPoint->getUser()->getUID() === $userId) { + $mountPoints[] = $event->mountPoint->getMountPoint(); + } + } + return $mountPoints; + } +} From d35b2d4c5ebadcae4ea63f7696914c28566f9605 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 24 Sep 2026 16:15:35 +0200 Subject: [PATCH 2/6] fix: Address review comments Signed-off-by: Marcel Klehr --- .../tests/ShareMountEventsTest.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/apps/files_sharing/tests/ShareMountEventsTest.php b/apps/files_sharing/tests/ShareMountEventsTest.php index 02faac5783f47..28cd0bdcdcd67 100644 --- a/apps/files_sharing/tests/ShareMountEventsTest.php +++ b/apps/files_sharing/tests/ShareMountEventsTest.php @@ -13,6 +13,7 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Config\Event\UserMountAddedEvent; use OCP\Files\Config\Event\UserMountRemovedEvent; +use OCP\Files\Folder; use OCP\Server; use OCP\Share\IShare; use PHPUnit\Framework\Attributes\DataProvider; @@ -47,7 +48,7 @@ protected function setUp(): void { $this->rootFolder->getUserFolder(self::TEST_FILES_SHARING_API_USER1)->newFolder(self::FOLDER); // The recipient has used their file system before - $this->setupFileSystem(self::TEST_FILES_SHARING_API_USER2); + $this->assertFalse($this->getUserFolder(self::TEST_FILES_SHARING_API_USER2)->nodeExists(self::FOLDER)); $this->loginHelper(self::TEST_FILES_SHARING_API_USER1); $this->events = []; } @@ -79,7 +80,7 @@ public function testMountAddedEventForCreatedShare(int $shareType, string $recip Server::get(SharesUpdatedListener::class)->setCutOffMarkTime($cutOffTime); $this->share($shareType, self::FOLDER, self::TEST_FILES_SHARING_API_USER1, $recipient, Constants::PERMISSION_ALL); - $this->setupFileSystem(self::TEST_FILES_SHARING_API_USER2); + $this->getUserFolder(self::TEST_FILES_SHARING_API_USER2)->get(self::FOLDER); $this->assertEquals( ['/' . self::TEST_FILES_SHARING_API_USER2 . '/files/' . self::FOLDER . '/'], @@ -91,13 +92,16 @@ public function testMountAddedEventForCreatedShare(int $shareType, string $recip #[DataProvider(methodName: 'shareProvider')] public function testMountRemovedEventForDeletedShare(int $shareType, string $recipient, float $cutOffTime): void { $share = $this->share($shareType, self::FOLDER, self::TEST_FILES_SHARING_API_USER1, $recipient, Constants::PERMISSION_ALL); - $this->setupFileSystem(self::TEST_FILES_SHARING_API_USER2); + $this->getUserFolder(self::TEST_FILES_SHARING_API_USER2)->get(self::FOLDER); $this->loginHelper(self::TEST_FILES_SHARING_API_USER1); $this->events = []; Server::get(SharesUpdatedListener::class)->setCutOffMarkTime($cutOffTime); $this->shareManager->deleteShare($share); - $this->setupFileSystem(self::TEST_FILES_SHARING_API_USER2); + $this->assertFalse( + $this->getUserFolder(self::TEST_FILES_SHARING_API_USER2)->nodeExists(self::FOLDER), + 'The share should no longer be mounted for the recipient', + ); $this->assertEquals( ['/' . self::TEST_FILES_SHARING_API_USER2 . '/files/' . self::FOLDER . '/'], @@ -107,11 +111,11 @@ public function testMountRemovedEventForDeletedShare(int $shareType, string $rec } /** - * Log in as the user and access their files, as a new request by them would + * Log in as the user and get their user folder, as a new request by them would */ - private function setupFileSystem(string $userId): void { + private function getUserFolder(string $userId): Folder { $this->loginHelper($userId); - $this->rootFolder->getUserFolder($userId)->getDirectoryListing(); + return $this->rootFolder->getUserFolder($userId); } /** From 8ae19797839fa958a5260a2610840ca582d8f0b6 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 28 Sep 2026 17:24:02 +0200 Subject: [PATCH 3/6] fix: emit UserMountAddedEvent from UserMountcache::addMount Signed-off-by: Robin Appelman --- lib/private/Files/Config/UserMountCache.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/private/Files/Config/UserMountCache.php b/lib/private/Files/Config/UserMountCache.php index 1430ab469eff3..ed031f19fb9ee 100644 --- a/lib/private/Files/Config/UserMountCache.php +++ b/lib/private/Files/Config/UserMountCache.php @@ -596,12 +596,24 @@ public function addMount( try { $query->executeStatement(); + unset($this->mountsForUsers[$user->getUID()]); } catch (DbalException $e) { if ($e->getReason() !== DbalException::REASON_UNIQUE_CONSTRAINT_VIOLATION) { throw $e; } } + + $cachedMount = new CachedMountInfo( + $user, + $rootCacheEntry->getStorageId(), + $rootCacheEntry->getId(), + $mountPoint, + $mountProvider, + $mountId, + $rootCacheEntry->getPath(), + ); + $this->eventDispatcher->dispatchTyped(new UserMountAddedEvent($cachedMount)); } /** From 302e8690e372061b5fed8091d3c7cfa723d96d13 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 29 Sep 2026 14:28:41 +0200 Subject: [PATCH 4/6] fix: emit UserMountRemovedEvent from UserMountcache::removeMount Signed-off-by: Robin Appelman --- .../lib/Repair/CleanupShareTarget.php | 2 +- lib/private/Files/Config/UserMountCache.php | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/apps/files_sharing/lib/Repair/CleanupShareTarget.php b/apps/files_sharing/lib/Repair/CleanupShareTarget.php index 872fbb70930b0..d4d678305ed29 100644 --- a/apps/files_sharing/lib/Repair/CleanupShareTarget.php +++ b/apps/files_sharing/lib/Repair/CleanupShareTarget.php @@ -124,7 +124,7 @@ public function run(IOutput $output) { $userMounts[$newMountPoint] = $mount; unset($userMounts[$oldMountPoint]); - $this->userMountCache->removeMount($oldMountPoint); + $this->userMountCache->removeMount($oldMountPoint, $recipient); $this->userMountCache->addMount($recipient, $newMountPoint, new CacheEntry([ 'fileid' => $mount->getRootId(), 'storage' => $mount->getStorageId(), diff --git a/lib/private/Files/Config/UserMountCache.php b/lib/private/Files/Config/UserMountCache.php index ed031f19fb9ee..df89c4c84966a 100644 --- a/lib/private/Files/Config/UserMountCache.php +++ b/lib/private/Files/Config/UserMountCache.php @@ -557,8 +557,26 @@ public function getMountsInPath(IUser $user, string $path): array { return $result; } + private function loadMountInfo(IUser $user, string $mountPoint): ?CachedMountInfo { + $builder = $this->connection->getQueryBuilder(); + $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class') + ->from('mounts', 'm') + ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid')) + ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID()))) + ->andWhere($builder->expr()->eq('mount_point_hash', $builder->createNamedParameter(hash('xxh128', $mountPoint), IQueryBuilder::PARAM_STR))); + + $row = $query->executeQuery()->fetch(); + return $row ? $this->dbRowToMountInfo($row) : null; + } + #[\Override] public function removeMount(string $mountPoint, ?IUser $user = null): void { + if ($user) { + $mount = $this->loadMountInfo($user, $mountPoint); + } else { + $mount = null; + } + $query = $this->connection->getQueryBuilder(); $query->delete('mounts') ->where($query->expr()->eq('mount_point_hash', $query->createNamedParameter(hash('xxh128', $mountPoint)))); @@ -572,6 +590,10 @@ public function removeMount(string $mountPoint, ?IUser $user = null): void { [, $userId] = $parts; unset($this->mountsForUsers[$userId]); } + + if ($user && $mount) { + $this->eventDispatcher->dispatchTyped(new UserMountRemovedEvent($mount)); + } } #[\Override] From d1b5fad42452ccf8535671a89813aa4439c23365 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 1 Oct 2026 11:02:12 +0200 Subject: [PATCH 5/6] fix: Address review comments Signed-off-by: Marcel Klehr --- .../files_sharing/tests/ShareMountEventsTest.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/files_sharing/tests/ShareMountEventsTest.php b/apps/files_sharing/tests/ShareMountEventsTest.php index 28cd0bdcdcd67..5cd8820fd5266 100644 --- a/apps/files_sharing/tests/ShareMountEventsTest.php +++ b/apps/files_sharing/tests/ShareMountEventsTest.php @@ -47,16 +47,14 @@ protected function setUp(): void { $this->rootFolder->getUserFolder(self::TEST_FILES_SHARING_API_USER1)->newFolder(self::FOLDER); - // The recipient has used their file system before - $this->assertFalse($this->getUserFolder(self::TEST_FILES_SHARING_API_USER2)->nodeExists(self::FOLDER)); $this->loginHelper(self::TEST_FILES_SHARING_API_USER1); - $this->events = []; } protected function tearDown(): void { $this->eventDispatcher->removeListener(UserMountAddedEvent::class, $this->listener); $this->eventDispatcher->removeListener(UserMountRemovedEvent::class, $this->listener); Server::get(SharesUpdatedListener::class)->setCutOffMarkTime(-1); + $this->events = []; parent::tearDown(); } @@ -82,8 +80,8 @@ public function testMountAddedEventForCreatedShare(int $shareType, string $recip $this->share($shareType, self::FOLDER, self::TEST_FILES_SHARING_API_USER1, $recipient, Constants::PERMISSION_ALL); $this->getUserFolder(self::TEST_FILES_SHARING_API_USER2)->get(self::FOLDER); - $this->assertEquals( - ['/' . self::TEST_FILES_SHARING_API_USER2 . '/files/' . self::FOLDER . '/'], + $this->assertContains( + '/' . self::TEST_FILES_SHARING_API_USER2 . '/files/' . self::FOLDER . '/', $this->getMountPointsOfEvents(UserMountAddedEvent::class, self::TEST_FILES_SHARING_API_USER2), 'A UserMountAddedEvent should be dispatched for the share mount of the recipient', ); @@ -93,8 +91,6 @@ public function testMountAddedEventForCreatedShare(int $shareType, string $recip public function testMountRemovedEventForDeletedShare(int $shareType, string $recipient, float $cutOffTime): void { $share = $this->share($shareType, self::FOLDER, self::TEST_FILES_SHARING_API_USER1, $recipient, Constants::PERMISSION_ALL); $this->getUserFolder(self::TEST_FILES_SHARING_API_USER2)->get(self::FOLDER); - $this->loginHelper(self::TEST_FILES_SHARING_API_USER1); - $this->events = []; Server::get(SharesUpdatedListener::class)->setCutOffMarkTime($cutOffTime); $this->shareManager->deleteShare($share); @@ -111,7 +107,11 @@ public function testMountRemovedEventForDeletedShare(int $shareType, string $rec } /** - * Log in as the user and get their user folder, as a new request by them would + * Get the user folder of the user as a new request by them would + * + * Logging in tears down the file system, so the mounts of the user are set up again. + * Otherwise the mounts already set up for the user earlier in this process are + * reused and changes to the shares of the user are not picked up. */ private function getUserFolder(string $userId): Folder { $this->loginHelper($userId); From 8c8b57c2b4189a74babd1e125dc9a99c11c37c20 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 20 Oct 2026 16:46:51 +0200 Subject: [PATCH 6/6] fix: set path for cacheentry when cleaning share targets Signed-off-by: Robin Appelman --- apps/files_sharing/lib/Repair/CleanupShareTarget.php | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/files_sharing/lib/Repair/CleanupShareTarget.php b/apps/files_sharing/lib/Repair/CleanupShareTarget.php index d4d678305ed29..459dd6194035e 100644 --- a/apps/files_sharing/lib/Repair/CleanupShareTarget.php +++ b/apps/files_sharing/lib/Repair/CleanupShareTarget.php @@ -128,6 +128,7 @@ public function run(IOutput $output) { $this->userMountCache->addMount($recipient, $newMountPoint, new CacheEntry([ 'fileid' => $mount->getRootId(), 'storage' => $mount->getStorageId(), + 'path' => $mount->getRootInternalPath(), ]), $mount->getMountProvider(), $mount->getMountId()); } }