Skip to content

Commit b9a51a9

Browse files
committed
[FIX] fs_attachment: reconcile fs_filename when datas/raw is rewritten
``ir.attachment.write`` only called ``_enforce_meaningful_storage_filename`` when ``name`` was in ``vals``. Every other code path that rewrites attachment bytes without touching the name — asset-bundle regeneration, ORM image field updates, inline kanban image writes, the ``_force_storage`` migration itself — replaces ``store_fname`` via ``_storage_file_write`` but leaves ``fs_filename`` / ``fs_storage_code`` / ``fs_storage_id`` stale. A stale ``NULL`` ``fs_filename`` defeats the gate in ``IrBinary._get_fs_attachment_for_field``, so base Odoo falls through to ``Stream.from_attachment``. The base implementation passes the raw ``store_fname`` (e.g. ``"azure_attachments://<hash>"``) straight to ``os.stat``, which raises ``FileNotFoundError`` — user-visible as 500s on every ``/web/image/<res_model>/<id>/<field>`` call that touches one of these records. Observed on production 2026-04-20 → 2026-04-22: when a site flipped ``fs.storage.use_as_default_for_attachments`` to an ``abfs`` (Azure Blob) backend, the Odoo deploy that followed regenerated thousands of asset bundles and ran background image updates, every one via ``write({"datas": ...})``. 36,804 ``ir.attachment`` rows entered the broken state in under 48 h and every ``/web/image`` hit for those records returned 500 — assets included, so the webclient rendered as a blank page in every browser. Widen the guard to also fire on ``"datas"`` / ``"raw"`` so every write that actually changes the bytes leaves ``fs_filename`` consistent with the new ``store_fname``. ``_enforce_meaningful_storage_filename`` is idempotent and already no-ops when nothing needs to change, so the broader guard has no downside for the existing ``"name"`` path. Signed-off-by: TecnologiaIG <tecnologia@intensegroupgt.com>
1 parent a482807 commit b9a51a9

3 files changed

Lines changed: 43 additions & 1 deletion

File tree

fs_attachment/__manifest__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
{
66
"name": "Base Attachment Object Store",
77
"summary": "Store attachments on external object store",
8-
"version": "16.0.2.0.1",
8+
"version": "16.0.2.0.4",
99
"author": "Camptocamp, ACSONE SA/NV, Odoo Community Association (OCA)",
1010
"license": "AGPL-3",
1111
"development_status": "Beta",

fs_attachment/models/ir_attachment.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,29 @@ def write(self, vals):
330330
).write(vals)
331331

332332
if "name" in vals:
333+
# Renaming requires rebuilding the storage filename so URLs
334+
# stay meaningful — apply to every record in the recordset.
333335
self._enforce_meaningful_storage_filename()
336+
elif "datas" in vals or "raw" in vals:
337+
# Content was rewritten without touching the name.
338+
# ``_storage_file_write`` just replaced ``store_fname`` but
339+
# the sibling ``fs_filename`` / ``fs_storage_*`` fields are
340+
# NOT recomputed. A stale ``NULL`` ``fs_filename`` defeats
341+
# the gate in ``IrBinary._get_fs_attachment_for_field``, so
342+
# base Odoo falls through to ``Stream.from_attachment``,
343+
# which passes the raw ``store_fname``
344+
# (e.g. ``"azure_attachments://<hash>"``) straight to
345+
# ``os.stat`` and raises ``FileNotFoundError`` → 500 on
346+
# every ``/web/image/...`` call.
347+
# Only reconcile rows actually in the broken shape: running
348+
# ``_enforce_meaningful_storage_filename`` on a record that
349+
# already has an ``fs_filename`` would trigger a spurious
350+
# ``fs.rename()`` round-trip against the storage.
351+
broken = self.filtered(
352+
lambda a: a.store_fname and not a.fs_filename
353+
)
354+
if broken:
355+
broken._enforce_meaningful_storage_filename()
334356

335357
return True
336358

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Reconcile ``fs_filename`` when ``datas`` / ``raw`` is rewritten.
2+
3+
``ir.attachment.write`` only called ``_enforce_meaningful_storage_filename``
4+
when ``name`` was in ``vals``. Every code path that updates an attachment's
5+
content without touching its name — asset-bundle regeneration, ORM image
6+
field updates, inline kanban image writes, the ``_force_storage``
7+
migration itself — rewrote ``store_fname`` via ``_storage_file_write``
8+
but left ``fs_filename`` / ``fs_storage_code`` / ``fs_storage_id``
9+
stale. A ``NULL`` ``fs_filename`` defeats the gate in
10+
``IrBinary._get_fs_attachment_for_field``, so base Odoo falls through
11+
to ``Stream.from_attachment``, which passes the raw ``store_fname``
12+
(e.g. ``"azure_attachments://<hash>"``) straight to ``os.stat`` and
13+
raises ``FileNotFoundError`` — observed in production as 500s on every
14+
``/web/image/...`` request for that record.
15+
16+
Extending the guard to fire on ``"datas"`` / ``"raw"`` keeps
17+
``fs_filename`` consistent with ``store_fname`` after every content
18+
rewrite. ``_enforce_meaningful_storage_filename`` is idempotent and
19+
already no-ops when nothing needs to change, so the widened guard has
20+
no downside for the existing ``"name"`` path.

0 commit comments

Comments
 (0)