@@ -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