From 3f16f3438e389b81493d9f1a9c63842620efdcb8 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 12 Jun 2026 10:40:41 -0400 Subject: [PATCH] fix(files_versions): skip write-hook version creation during cross-storage renames Signed-off-by: Josh --- .../lib/Listener/FileEventsListener.php | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/apps/files_versions/lib/Listener/FileEventsListener.php b/apps/files_versions/lib/Listener/FileEventsListener.php index 71cb25bb02952..c9a3d7c10a186 100644 --- a/apps/files_versions/lib/Listener/FileEventsListener.php +++ b/apps/files_versions/lib/Listener/FileEventsListener.php @@ -56,6 +56,16 @@ class FileEventsListener implements IEventListener { * @var array */ private array $versionsDeleted = []; + /** + * Source paths currently involved in a cross-backend rename. + * + * Cross-backend renames can emit write events as part of their copy/unlink + * implementation. For nodes under these paths, version creation is handled + * by VersionStorageMoveListener and must not be re-triggered by write_hook(). + * + * @var array + */ + private array $crossBackendRenamePaths = []; public function __construct( private IRootFolder $rootFolder, @@ -105,6 +115,7 @@ public function handle(Event $event): void { } if ($event instanceof BeforeNodeRenamedEvent) { + $this->markCrossBackendRenamePath($event->getSource(), $event->getTarget()); $this->pre_renameOrCopy_hook($event->getSource(), $event->getTarget()); } @@ -113,6 +124,54 @@ public function handle(Event $event): void { } } + private function markCrossBackendRenamePath(Node $source, Node $target): void { + $sourceBackend = $this->versionManager->getBackendForStorage($source->getStorage()); + $targetBackend = $this->versionManager->getBackendForStorage($target->getParent()->getStorage()); + + if ($sourceBackend === $targetBackend) { + return; + } + + $sourcePath = $this->getPathForNode($source); + if ($sourcePath === null) { + return; + } + + $this->crossBackendRenamePaths[$this->normalizeRelativePath($sourcePath)] = true; + } + + private function unmarkCrossBackendRenamePath(Node $source, Node $target): void { + $sourceBackend = $this->versionManager->getBackendForStorage($source->getParent()->getStorage()); + $targetBackend = $this->versionManager->getBackendForStorage($target->getStorage()); + + if ($sourceBackend === $targetBackend) { + return; + } + + $sourcePath = $this->getPathForNode($source); + if ($sourcePath === null) { + return; + } + + unset($this->crossBackendRenamePaths[$this->normalizeRelativePath($sourcePath)]); + } + + private function normalizeRelativePath(string $path): string { + return trim($path, '/'); + } + + private function isCrossBackendRenamePath(string $path): bool { + $path = $this->normalizeRelativePath($path); + + foreach ($this->crossBackendRenamePaths as $renamePath => $_true) { + if ($path === $renamePath || ($renamePath !== '' && str_starts_with($path, $renamePath . '/'))) { + return true; + } + } + + return false; + } + public function pre_touch_hook(Node $node): void { // Do not handle folders. if ($node instanceof Folder) { @@ -211,6 +270,25 @@ public function write_hook(Node $node): void { if ($path === null) { return; } + + // Cross-backend renames can emit write events while the file is being + // copied away from the source storage. In that case, the dedicated + // VersionStorageMoveListener handles preserving versions. + if ($this->isCrossBackendRenamePath($path)) { + $this->logger->debug('Skipping version creation during cross-backend rename', [ + 'path' => $path, + 'node' => [ + 'id' => $node->getId(), + 'path' => $node->getPath(), + 'size' => $node->getSize(), + 'mtime' => $node->getMTime(), + ], + 'activeCrossBackendRenamePaths' => array_keys($this->crossBackendRenamePaths), + ]); + + return; + } + $result = Storage::store($path); // 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 { * of the stored versions along the actual file */ public function rename_hook(Node $source, Node $target): void { + $this->unmarkCrossBackendRenamePath($source, $target); + $sourceBackend = $this->versionManager->getBackendForStorage($source->getParent()->getStorage()); $targetBackend = $this->versionManager->getBackendForStorage($target->getStorage()); // If different backends, do nothing.