From d3b9b41faf93306176c2f55cf7adae4a1b450550 Mon Sep 17 00:00:00 2001 From: Dirk van der Laarse Date: Sat, 20 Jun 2026 10:22:06 +0000 Subject: [PATCH 1/3] fix: prevent infinite recursion when associating duplicate-content files `associate_files()` is called from `validate`, and for Files sharing a `content_hash` it calls `.save()` on the existing duplicate row. That save re-enters `validate` -> `associate_files`, which saves back into the original file, producing an endless A->B->A loop (maximum recursion depth exceeded). - Guard `associate_files()` in `validate` with a new `associating_files` flag so a save triggered from within association does not re-run association. - Set `existing_file.flags.associating_files = True` before saving the duplicate so the recursion is broken. - Skip appending a `file_association` row that already links the same doctype/name, avoiding duplicate child rows on repeated associations. --- cloud_storage/cloud_storage/overrides/file.py | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/cloud_storage/cloud_storage/overrides/file.py b/cloud_storage/cloud_storage/overrides/file.py index b509f34..96e5ab0 100644 --- a/cloud_storage/cloud_storage/overrides/file.py +++ b/cloud_storage/cloud_storage/overrides/file.py @@ -53,7 +53,11 @@ def validate(self) -> None: PATH: frappe/core/doctype/file/file.py METHOD: validate """ - self.associate_files() + # `associate_files` can call `.save()` on a duplicate-content File, which re-enters + # `validate`. Without this guard, two or more File rows sharing a `content_hash` save + # each other in an endless A->B->A loop (maximum recursion depth exceeded). + if not self.flags.associating_files: + self.associate_files() if self.flags.cloud_storage or self.flags.ignore_file_validate: return if not self.is_remote_file: @@ -228,10 +232,18 @@ def associate_files( existing_file = frappe.get_doc("File", associated_doc) existing_file.attached_to_doctype = attached_to_doctype existing_file.attached_to_name = attached_to_name - existing_file.append( - "file_association", - add_child_file_association(attached_to_doctype, attached_to_name), + already_linked = any( + assoc.link_doctype == attached_to_doctype and assoc.link_name == attached_to_name + for assoc in existing_file.file_association ) + if not already_linked: + existing_file.append( + "file_association", + add_child_file_association(attached_to_doctype, attached_to_name), + ) + # Prevent `existing_file.save()` -> `validate` -> `associate_files` from recursing + # back into this file (and looping between duplicate-content rows). + existing_file.flags.associating_files = True existing_file.save() else: if self.file_association: From 18490a834bb880224d6c09ace8fed00a51944064 Mon Sep 17 00:00:00 2001 From: Dirk van der Laarse Date: Sat, 20 Jun 2026 10:22:30 +0000 Subject: [PATCH 2/3] chore: bump to v15.7.3 --- cloud_storage/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud_storage/__init__.py b/cloud_storage/__init__.py index 45fc226..3a2ec68 100644 --- a/cloud_storage/__init__.py +++ b/cloud_storage/__init__.py @@ -1,7 +1,7 @@ # Copyright (c) 2024, AgriTheory and contributors # For license information, please see license.txt -__version__ = "15.7.2" +__version__ = "15.7.3" import frappe.desk.form.load From 56e7a711891daa52e1b0e3820963c131901ebb0b Mon Sep 17 00:00:00 2001 From: Dirk van der Laarse Date: Sat, 20 Jun 2026 10:26:10 +0000 Subject: [PATCH 3/3] chore: clean-up comments --- cloud_storage/cloud_storage/overrides/file.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cloud_storage/cloud_storage/overrides/file.py b/cloud_storage/cloud_storage/overrides/file.py index 96e5ab0..94c9c74 100644 --- a/cloud_storage/cloud_storage/overrides/file.py +++ b/cloud_storage/cloud_storage/overrides/file.py @@ -53,9 +53,7 @@ def validate(self) -> None: PATH: frappe/core/doctype/file/file.py METHOD: validate """ - # `associate_files` can call `.save()` on a duplicate-content File, which re-enters - # `validate`. Without this guard, two or more File rows sharing a `content_hash` save - # each other in an endless A->B->A loop (maximum recursion depth exceeded). + # guard against recursion: associate_files() can save another File, re-entering validate if not self.flags.associating_files: self.associate_files() if self.flags.cloud_storage or self.flags.ignore_file_validate: @@ -241,8 +239,6 @@ def associate_files( "file_association", add_child_file_association(attached_to_doctype, attached_to_name), ) - # Prevent `existing_file.save()` -> `validate` -> `associate_files` from recursing - # back into this file (and looping between duplicate-content rows). existing_file.flags.associating_files = True existing_file.save() else: