Skip to content

Commit 97c9a56

Browse files
Merge pull request #61254 from nextcloud/jtr/fix-crossStorageRenameOverlap
fix(files_versions): avoid version snapshot races during cross-storage renames
2 parents 8aaa539 + 3f16f34 commit 97c9a56

1 file changed

Lines changed: 80 additions & 0 deletions

File tree

apps/files_versions/lib/Listener/FileEventsListener.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ class FileEventsListener implements IEventListener {
5656
* @var array<string, Node>
5757
*/
5858
private array $versionsDeleted = [];
59+
/**
60+
* Source paths currently involved in a cross-backend rename.
61+
*
62+
* Cross-backend renames can emit write events as part of their copy/unlink
63+
* implementation. For nodes under these paths, version creation is handled
64+
* by VersionStorageMoveListener and must not be re-triggered by write_hook().
65+
*
66+
* @var array<string, true>
67+
*/
68+
private array $crossBackendRenamePaths = [];
5969

6070
public function __construct(
6171
private IRootFolder $rootFolder,
@@ -105,6 +115,7 @@ public function handle(Event $event): void {
105115
}
106116

107117
if ($event instanceof BeforeNodeRenamedEvent) {
118+
$this->markCrossBackendRenamePath($event->getSource(), $event->getTarget());
108119
$this->pre_renameOrCopy_hook($event->getSource(), $event->getTarget());
109120
}
110121

@@ -113,6 +124,54 @@ public function handle(Event $event): void {
113124
}
114125
}
115126

127+
private function markCrossBackendRenamePath(Node $source, Node $target): void {
128+
$sourceBackend = $this->versionManager->getBackendForStorage($source->getStorage());
129+
$targetBackend = $this->versionManager->getBackendForStorage($target->getParent()->getStorage());
130+
131+
if ($sourceBackend === $targetBackend) {
132+
return;
133+
}
134+
135+
$sourcePath = $this->getPathForNode($source);
136+
if ($sourcePath === null) {
137+
return;
138+
}
139+
140+
$this->crossBackendRenamePaths[$this->normalizeRelativePath($sourcePath)] = true;
141+
}
142+
143+
private function unmarkCrossBackendRenamePath(Node $source, Node $target): void {
144+
$sourceBackend = $this->versionManager->getBackendForStorage($source->getParent()->getStorage());
145+
$targetBackend = $this->versionManager->getBackendForStorage($target->getStorage());
146+
147+
if ($sourceBackend === $targetBackend) {
148+
return;
149+
}
150+
151+
$sourcePath = $this->getPathForNode($source);
152+
if ($sourcePath === null) {
153+
return;
154+
}
155+
156+
unset($this->crossBackendRenamePaths[$this->normalizeRelativePath($sourcePath)]);
157+
}
158+
159+
private function normalizeRelativePath(string $path): string {
160+
return trim($path, '/');
161+
}
162+
163+
private function isCrossBackendRenamePath(string $path): bool {
164+
$path = $this->normalizeRelativePath($path);
165+
166+
foreach ($this->crossBackendRenamePaths as $renamePath => $_true) {
167+
if ($path === $renamePath || ($renamePath !== '' && str_starts_with($path, $renamePath . '/'))) {
168+
return true;
169+
}
170+
}
171+
172+
return false;
173+
}
174+
116175
public function pre_touch_hook(Node $node): void {
117176
// Do not handle folders.
118177
if ($node instanceof Folder) {
@@ -211,6 +270,25 @@ public function write_hook(Node $node): void {
211270
if ($path === null) {
212271
return;
213272
}
273+
274+
// Cross-backend renames can emit write events while the file is being
275+
// copied away from the source storage. In that case, the dedicated
276+
// VersionStorageMoveListener handles preserving versions.
277+
if ($this->isCrossBackendRenamePath($path)) {
278+
$this->logger->debug('Skipping version creation during cross-backend rename', [
279+
'path' => $path,
280+
'node' => [
281+
'id' => $node->getId(),
282+
'path' => $node->getPath(),
283+
'size' => $node->getSize(),
284+
'mtime' => $node->getMTime(),
285+
],
286+
'activeCrossBackendRenamePaths' => array_keys($this->crossBackendRenamePaths),
287+
]);
288+
289+
return;
290+
}
291+
214292
$result = Storage::store($path);
215293

216294
// Store the result of the version creation so it can be used in post_write_hook.
@@ -345,6 +423,8 @@ public function pre_remove_hook(Node $node): void {
345423
* of the stored versions along the actual file
346424
*/
347425
public function rename_hook(Node $source, Node $target): void {
426+
$this->unmarkCrossBackendRenamePath($source, $target);
427+
348428
$sourceBackend = $this->versionManager->getBackendForStorage($source->getParent()->getStorage());
349429
$targetBackend = $this->versionManager->getBackendForStorage($target->getStorage());
350430
// If different backends, do nothing.

0 commit comments

Comments
 (0)