Skip to content

Remove a book's revision history when the book is destroyed - #489

Merged
jeremy merged 1 commit into
mainfrom
security/purge-revisions-on-book-destroy
Sep 10, 2026
Merged

Remove a book's revision history when the book is destroyed#489
jeremy merged 1 commit into
mainfrom
security/purge-revisions-on-book-destroy

Conversation

@jeremy

@jeremy jeremy commented Sep 10, 2026

Copy link
Copy Markdown
Member

Destroying a book cascades to its leaves, but Leaf has_many :edits, dependent: :delete_all deleted the edit rows with a raw DELETE, so the dependent: :destroy on Edit#leafable never ran. The Page or Picture each revision had superseded stayed behind with its ActionText markdown record, its Active Storage attachments and their blobs. Deleting a private book left its earlier drafts and images in the database and on disk; #486 stopped those orphans from being served, this removes them.

Fix

  • Leaf#edits is dependent: :destroy, so each edit runs its callbacks.
  • Edit destroys its leafable only for revisions. A revision holds the superseded copy, which nothing else references. A trash edit shares the leaf's current leafable, and destroying that out from under the leaf broke the leaf's own destroy (Leaf::Searchable's after_destroy_commit reads the leafable). The unconditional dependent: :destroy on Edit#leafable was never sound for trash edits; delete_all just meant it never fired.
  • Markdown uploads use Active Storage's default dependent: :purge_later. :destroy is not an Active Storage option: only :purge_later purges a blob, so even with the cascade fixed a destroyed book's embedded images stayed in storage. Nothing destroys a markdown record except destroying its page, and nothing destroys a page except destroying its book (directly, or through a revision), so the old value's only observable effect was orphaning blobs on book delete.

Purging is _later, matching Book#cover; the Procfile's resque-pool worker runs the purge jobs.

Tests

test/models/book_test.rb:

  • destroys a book holding a revised page (with a markdown upload) and a revised picture, and asserts the superseded page, its markdown record, both attachments, both blobs and both files on disk are gone — red on main at the superseded-page assertion, and still red at the upload-blob assertion with only the cascade fixed
  • destroys a book holding a trashed leaf, guarding the shared-leafable path the cascade newly reaches

Full suite green (222 runs); rubocop, brakeman and importmap audit clean.

Destroying a book cascades to its leaves, but Leaf#edits was declared
dependent: :delete_all, which deletes the edit rows with a raw DELETE
and never runs the edits' own callbacks. The Page or Picture each
revision had superseded stayed behind with its markdown record, its
Active Storage attachments and their blobs: deleting a private book
left its earlier drafts and images in the database and on disk.

Leaf#edits now destroys, and Edit destroys the leafable it superseded.
That is only ever a revision's leafable: a trash edit shares the leaf's
current leafable, and destroying that out from under the leaf broke the
leaf's own destroy, so the unconditional dependent: :destroy on
Edit#leafable moves to a callback that runs for revisions alone.

Markdown uploads also drop their dependent: :destroy. Active Storage
only purges a blob for dependent: :purge_later, so even with the cascade
in place a destroyed book's embedded images stayed in storage. Nothing
destroys a markdown record except destroying its page, and nothing
destroys a page except destroying its book, so the default is right and
the old value's only effect was to orphan blobs.
Copilot AI balanced review requested due to automatic review settings September 10, 2026 00:25
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-10T00:28:14.867293Z adf8b02 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The deletion cascade correctly distinguishes owned revision content from shared trash content and is covered by focused regression tests.

Pull request overview

This PR fixes book deletion so revision content and associated Active Storage files are fully removed without prematurely destroying content shared by trash edits.

Changes:

  • Destroys Edit records through callbacks when a leaf is removed.
  • Destroys superseded leafables only for revision edits.
  • Restores default purge_later behavior for Markdown uploads and adds deletion regression tests.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File summaries
File Description
app/models/edit.rb Adds revision-specific cleanup of superseded leafables.
app/models/leaf/editable.rb Runs edit destruction callbacks during leaf deletion.
lib/rails_ext/action_text_markdown.rb Uses default deferred blob purging for Markdown uploads.
test/models/book_test.rb Covers revision, trash, attachment, blob, and file cleanup.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 0
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@jeremy
jeremy merged commit 0e619aa into main Sep 10, 2026
8 checks passed
@jeremy
jeremy deleted the security/purge-revisions-on-book-destroy branch September 10, 2026 08:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants