diff --git a/fs_attachment/__manifest__.py b/fs_attachment/__manifest__.py index bb8fe1ca10..911482d813 100644 --- a/fs_attachment/__manifest__.py +++ b/fs_attachment/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Base Attachment Object Store", "summary": "Store attachments on external object store", - "version": "16.0.2.0.1", + "version": "16.0.2.0.4", "author": "Camptocamp, ACSONE SA/NV, Odoo Community Association (OCA)", "license": "AGPL-3", "development_status": "Beta", diff --git a/fs_attachment/models/ir_attachment.py b/fs_attachment/models/ir_attachment.py index 8e1500a51b..2d15413611 100644 --- a/fs_attachment/models/ir_attachment.py +++ b/fs_attachment/models/ir_attachment.py @@ -330,7 +330,29 @@ def write(self, vals): ).write(vals) if "name" in vals: + # Renaming requires rebuilding the storage filename so URLs + # stay meaningful — apply to every record in the recordset. self._enforce_meaningful_storage_filename() + elif "datas" in vals or "raw" in vals: + # Content was rewritten without touching the name. + # ``_storage_file_write`` just replaced ``store_fname`` but + # the sibling ``fs_filename`` / ``fs_storage_*`` fields are + # NOT recomputed. A stale ``NULL`` ``fs_filename`` defeats + # the gate in ``IrBinary._get_fs_attachment_for_field``, so + # base Odoo falls through to ``Stream.from_attachment``, + # which passes the raw ``store_fname`` + # (e.g. ``"azure_attachments://"``) straight to + # ``os.stat`` and raises ``FileNotFoundError`` → 500 on + # every ``/web/image/...`` call. + # Only reconcile rows actually in the broken shape: running + # ``_enforce_meaningful_storage_filename`` on a record that + # already has an ``fs_filename`` would trigger a spurious + # ``fs.rename()`` round-trip against the storage. + broken = self.filtered( + lambda a: a.store_fname and not a.fs_filename + ) + if broken: + broken._enforce_meaningful_storage_filename() return True diff --git a/fs_attachment/readme/newsfragments/write_guard_datas.bugfix b/fs_attachment/readme/newsfragments/write_guard_datas.bugfix new file mode 100644 index 0000000000..2a950807cd --- /dev/null +++ b/fs_attachment/readme/newsfragments/write_guard_datas.bugfix @@ -0,0 +1,20 @@ +Reconcile ``fs_filename`` when ``datas`` / ``raw`` is rewritten. + +``ir.attachment.write`` only called ``_enforce_meaningful_storage_filename`` +when ``name`` was in ``vals``. Every code path that updates an attachment's +content without touching its name — asset-bundle regeneration, ORM image +field updates, inline kanban image writes, the ``_force_storage`` +migration itself — rewrote ``store_fname`` via ``_storage_file_write`` +but left ``fs_filename`` / ``fs_storage_code`` / ``fs_storage_id`` +stale. A ``NULL`` ``fs_filename`` defeats the gate in +``IrBinary._get_fs_attachment_for_field``, so base Odoo falls through +to ``Stream.from_attachment``, which passes the raw ``store_fname`` +(e.g. ``"azure_attachments://"``) straight to ``os.stat`` and +raises ``FileNotFoundError`` — observed in production as 500s on every +``/web/image/...`` request for that record. + +Extending the guard to fire on ``"datas"`` / ``"raw"`` keeps +``fs_filename`` consistent with ``store_fname`` after every content +rewrite. ``_enforce_meaningful_storage_filename`` is idempotent and +already no-ops when nothing needs to change, so the widened guard has +no downside for the existing ``"name"`` path.