Skip to content

Commit a8a307f

Browse files
Merge pull request #2359 from nextcloud/backport/2330/stable33
[stable33] fix: cache invalidation issues
2 parents 756e204 + 004584b commit a8a307f

File tree

3 files changed

+204
-117
lines changed

3 files changed

+204
-117
lines changed

lib/AppInfo/Application.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
use OCA\Circles\Listeners\GroupDeleted;
4040
use OCA\Circles\Listeners\GroupMemberAdded;
4141
use OCA\Circles\Listeners\GroupMemberRemoved;
42+
use OCA\Circles\Listeners\NodeEventListener;
4243
use OCA\Circles\Listeners\Notifications\RequestingMember as ListenerNotificationsRequestingMember;
4344
use OCA\Circles\Listeners\UserCreated;
4445
use OCA\Circles\Listeners\UserDeleted;
@@ -53,6 +54,10 @@
5354
use OCP\AppFramework\Bootstrap\IBootstrap;
5455
use OCP\AppFramework\Bootstrap\IRegistrationContext;
5556
use OCP\Files\Config\IMountProviderCollection;
57+
use OCP\Files\Events\Node\NodeCreatedEvent;
58+
use OCP\Files\Events\Node\NodeDeletedEvent;
59+
use OCP\Files\Events\Node\NodeRenamedEvent;
60+
use OCP\Files\Events\Node\NodeWrittenEvent;
5661
use OCP\Group\Events\GroupChangedEvent;
5762
use OCP\Group\Events\GroupCreatedEvent;
5863
use OCP\Group\Events\GroupDeletedEvent;
@@ -116,6 +121,12 @@ public function register(IRegistrationContext $context): void {
116121
$context->registerEventListener(RequestingCircleMemberEvent::class, ListenerNotificationsRequestingMember::class);
117122
$context->registerEventListener(DestroyingCircleEvent::class, ListenerFilesDestroyingCircle::class);
118123

124+
// Node events
125+
$context->registerEventListener(NodeCreatedEvent::class, NodeEventListener::class);
126+
$context->registerEventListener(NodeDeletedEvent::class, NodeEventListener::class);
127+
$context->registerEventListener(NodeRenamedEvent::class, NodeEventListener::class);
128+
$context->registerEventListener(NodeWrittenEvent::class, NodeEventListener::class);
129+
119130
$context->registerSearchProvider(UnifiedSearchProvider::class);
120131
$context->registerWellKnownHandler(WebfingerHandler::class);
121132

@@ -125,7 +136,6 @@ public function register(IRegistrationContext $context): void {
125136
$context->registerConfigLexicon(ConfigLexicon::class);
126137
}
127138

128-
129139
/**
130140
* @throws Throwable
131141
*/
@@ -137,7 +147,6 @@ public function boot(IBootContext $context): void {
137147
$context->injectFn(Closure::fromCallable([$this, 'registerMountProvider']));
138148
}
139149

140-
141150
public function registerMountProvider(ContainerInterface $container): void {
142151
$configService = $container->get(ConfigService::class);
143152
if (!$configService->isGSAvailable()) {
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Circles\Listeners;
11+
12+
use OCA\Circles\Db\ShareWrapperRequest;
13+
use OCA\Circles\Service\ShareWrapperService;
14+
use OCP\EventDispatcher\Event;
15+
use OCP\EventDispatcher\IEventListener;
16+
use OCP\Files\Events\Node\NodeCreatedEvent;
17+
use OCP\Files\Events\Node\NodeDeletedEvent;
18+
use OCP\Files\Events\Node\NodeRenamedEvent;
19+
use OCP\Files\Events\Node\NodeWrittenEvent;
20+
use OCP\Files\Node;
21+
use OCP\Files\NotFoundException;
22+
use OCP\Files\NotPermittedException;
23+
use Psr\Log\LoggerInterface;
24+
25+
/**
26+
* Event listener for file/folder operations to invalidate team share cache
27+
*
28+
* @template-implements IEventListener<NodeCreatedEvent|NodeDeletedEvent|NodeRenamedEvent|NodeWrittenEvent>
29+
*/
30+
class NodeEventListener implements IEventListener {
31+
public function __construct(
32+
private readonly ShareWrapperService $shareWrapperService,
33+
private readonly ShareWrapperRequest $shareWrapperRequest,
34+
private readonly LoggerInterface $logger,
35+
) {
36+
}
37+
38+
public function handle(Event $event): void {
39+
if (!($event instanceof NodeCreatedEvent
40+
|| $event instanceof NodeDeletedEvent
41+
|| $event instanceof NodeRenamedEvent
42+
|| $event instanceof NodeWrittenEvent)) {
43+
return;
44+
}
45+
46+
try {
47+
/** @var Node $node */
48+
$node = $event instanceof NodeRenamedEvent ? $event->getTarget() : $event->getNode();
49+
$this->invalidateCacheForNode($node);
50+
} catch (NotFoundException|NotPermittedException $e) {
51+
$this->logger->error('Failed to process node event: ' . $e->getMessage());
52+
}
53+
}
54+
55+
/**
56+
* Invalidate cache for a node and all its parent folders
57+
* This ensures cache is cleared when files in team-shared folders are modified
58+
*/
59+
private function invalidateCacheForNode(Node $node): void {
60+
$affectedCircles = [];
61+
$visitedNodeIds = [];
62+
63+
// Check if this node is directly shared with circles
64+
try {
65+
$shares = $this->shareWrapperRequest->getSharesByFileId($node->getId(), false);
66+
foreach ($shares as $share) {
67+
$affectedCircles[$share->getSharedWith()] = true;
68+
}
69+
$visitedNodeIds[$node->getId()] = true;
70+
} catch (\Exception $e) {
71+
$this->logger->error('Failed to get shares for node ' . $node->getId() . ': ' . $e->getMessage());
72+
}
73+
74+
// Check parent folders (file might be inside a shared folder)
75+
try {
76+
$current = $node;
77+
78+
while (true) {
79+
$parent = $current->getParent();
80+
81+
// Stop if we've reached the root (parent is null or same as current)
82+
if ($parent === null || $parent->getId() === $current->getId()) {
83+
break;
84+
}
85+
86+
// Detect infinite loop: if we've already visited this node ID, stop
87+
if (isset($visitedNodeIds[$parent->getId()])) {
88+
$this->logger->debug('Detected cycle in folder hierarchy at node ' . $parent->getId());
89+
break;
90+
}
91+
92+
$visitedNodeIds[$parent->getId()] = true;
93+
94+
// Check if parent is shared
95+
$parentShares = $this->shareWrapperRequest->getSharesByFileId($parent->getId(), false);
96+
foreach ($parentShares as $share) {
97+
$affectedCircles[$share->getSharedWith()] = true;
98+
}
99+
100+
$current = $parent;
101+
}
102+
} catch (NotFoundException|NotPermittedException $e) {
103+
$this->logger->debug('Stopped parent traversal: ' . $e->getMessage());
104+
}
105+
106+
// Invalidate cache for all affected circles
107+
if ($affectedCircles !== []) {
108+
foreach (array_keys($affectedCircles) as $circleId) {
109+
$this->shareWrapperService->clearCacheForCircle($circleId);
110+
}
111+
112+
$this->logger->debug(
113+
'Invalidated cache for node ' . $node->getId() .
114+
' affecting ' . count($affectedCircles) . ' circle(s), ' .
115+
'traversed ' . count($visitedNodeIds) . ' level(s)'
116+
);
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)