Company logos: storing or clearing one deletes the old file before the row is saved #525
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#525
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, reading how files are replaced across the jobs app.Checked again on 2026-10-01 against
mainat0a5cad587: still present.jobs/logos.pyand its callers have not moved;pages._discard, cited for comparison, is now atjobs/pages.py:661-682.logos.storeandlogos.cleardelete the company's current logo file at once, and only then open the transaction that saves the row. If that save fails, the row still names the old file, which is gone;storealso leaves the new file behind with no row pointing at it. The rest of the jobs app keeps the rule #217 set: remove a file only after the change commits, and only when no row still uses it (merging.py:505,pages._discard).jobs/logos.py:155-175(store:company.logo.delete(save=False)at 168-169, the new file written at 170,company.save(update_fields=LOGO_FIELDS)insidetransaction.atomic()at 174-175);jobs/logos.py:178-185(clear, the same order). Callers outside the request transaction:CompanyLogoActionView.post(jobs/views.py:436-462, Remove) and thelogoerrandfind_a_logo(jobs/slow.py:130-157, Find logo and Refresh). For comparison:jobs/merging.py:502-505andjobs/pages.py:689-710.IMMEDIATEwith a 20-second busy timeout (config/sqlite.py:48-52). In the action view and the errand, which run outsideATOMIC_REQUESTS(#220), the lock is only asked for after the file is gone. If another writer holds it past the timeout, the save raisesOperationalError; after Remove logo the company row still names the deleted file, and every page shows a missing logo (CompanyLogoViewanswers 404) until a new one is stored.logo-<pk>.<ext>, so a failed save then leaves the new picture under the old row's name with the oldlogo_sourceandlogo_source_url. Where the name changes (a PNG replaced by an SVG since #264, or a logo that came from an import, which keeps the archive's file name atcore/importer.py:581), the row names a file that no longer exists.save(update_fields=...)raiseDatabaseError(no rows affected): the errand fails with an unhandled error rather thanRefused, and the file it has just written stays on disk with no row.store, remember the old name, write the new file, then save the row inside the atomic block; on failure delete the new file and re-raise (aspages._writedoes), and on success remove the old name withtransaction.on_commit, only if it differs from the new one and noCompanyrow still uses it. Inclear, save the row first and remove the file on commit under the same check. Catch the no-rowsDatabaseErrorinfind_a_logoand answerRefused(_("That company is no longer here.")).Done when
tests/test_logos.py: withCompany.savepatched to raise,logos.clear(company)leaves the logo file in place, andlogos.store(...)with a different extension leaves both the old file in place and no new file behind. Both fail today.