diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-07-23 04:12:39 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-07-23 04:12:39 +0200 |
| commit | 6ef98ad444631b1d5ba67bb16aaaa2afdfa53ae0 (patch) | |
| tree | 44c23d4efccfdbe0496a3d09fbdb1b33212999de /app | |
| parent | 49a80c2f48531edc2a2719d77d3c5a8c111289dd (diff) | |
Witness asset destruction, naming every node it strips
Diffstat (limited to 'app')
| -rw-r--r-- | app/controllers/assets_controller.rb | 2 | ||||
| -rw-r--r-- | app/helpers/node_actions_helper.rb | 11 | ||||
| -rw-r--r-- | app/models/asset.rb | 30 | ||||
| -rw-r--r-- | app/models/node_action.rb | 9 |
4 files changed, 51 insertions, 1 deletions
diff --git a/app/controllers/assets_controller.rb b/app/controllers/assets_controller.rb index fbede0a7..8df4c94d 100644 --- a/app/controllers/assets_controller.rb +++ b/app/controllers/assets_controller.rb | |||
| @@ -81,7 +81,7 @@ class AssetsController < ApplicationController | |||
| 81 | # DELETE /assets/1.xml | 81 | # DELETE /assets/1.xml |
| 82 | def destroy | 82 | def destroy |
| 83 | @asset = Asset.find(params[:id]) | 83 | @asset = Asset.find(params[:id]) |
| 84 | @asset.destroy | 84 | @asset.destroy_witnessed!(:user => current_user) |
| 85 | 85 | ||
| 86 | respond_to do |format| | 86 | respond_to do |format| |
| 87 | format.html { redirect_to(assets_url) } | 87 | format.html { redirect_to(assets_url) } |
diff --git a/app/helpers/node_actions_helper.rb b/app/helpers/node_actions_helper.rb index fd8cc36e..02d1ba83 100644 --- a/app/helpers/node_actions_helper.rb +++ b/app/helpers/node_actions_helper.rb | |||
| @@ -180,4 +180,15 @@ module NodeActionsHelper | |||
| 180 | t("node_actions.destroy", :actor => actor_ref(action), :subject => subject_ref(action), | 180 | t("node_actions.destroy", :actor => actor_ref(action), :subject => subject_ref(action), |
| 181 | :path => h(action.metadata["path"])).html_safe | 181 | :path => h(action.metadata["path"])).html_safe |
| 182 | end | 182 | end |
| 183 | |||
| 184 | def summarize_asset_destroy action | ||
| 185 | m = action.metadata | ||
| 186 | parts = [t("node_actions.asset_destroy", :actor => actor_ref(action), | ||
| 187 | :asset => h(m["asset_name"].presence || m["path"]))] | ||
| 188 | parts << t("node_actions.asset_destroy_detached", | ||
| 189 | :paths => h(Array(m["detached_from"]).join(", "))) if m["detached_from"].present? | ||
| 190 | parts << t("node_actions.asset_destroy_headlines", | ||
| 191 | :paths => h(Array(m["headline_removed_from"]).join(", "))) if m["headline_removed_from"].present? | ||
| 192 | safe_join(parts, " ") | ||
| 193 | end | ||
| 183 | end | 194 | end |
diff --git a/app/models/asset.rb b/app/models/asset.rb index 73970e21..8cec4371 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb | |||
| @@ -40,4 +40,34 @@ class Asset < ApplicationRecord | |||
| 40 | Node.where("head_id IN (:ids) OR draft_id IN (:ids) OR autosave_id IN (:ids)", | 40 | Node.where("head_id IN (:ids) OR draft_id IN (:ids) OR autosave_id IN (:ids)", |
| 41 | :ids => page_ids).distinct | 41 | :ids => page_ids).distinct |
| 42 | end | 42 | end |
| 43 | |||
| 44 | # Witnessed destruction. Destroying an asset is a public-facing act | ||
| 45 | # even when unattached. The original and its variants are publicly | ||
| 46 | # reachable under /system/uploads, so an entry is always written, | ||
| 47 | # before the row and its files die. Every currently-attached node | ||
| 48 | # participates so its zoomed history shows the loss; the asset itself | ||
| 49 | # participates as the first non-Node subject (its participant row | ||
| 50 | # dangles after destroy, by design, the name lives on in metadata). | ||
| 51 | def destroy_witnessed! user: | ||
| 52 | ActiveRecord::Base.transaction do | ||
| 53 | affected = attached_nodes.to_a | ||
| 54 | headline_losses = affected.select do |node| | ||
| 55 | [node.head, node.draft, node.autosave].compact.any? do |row| | ||
| 56 | row.related_assets.exists?(:asset_id => id, :headline => true) | ||
| 57 | end | ||
| 58 | end | ||
| 59 | |||
| 60 | metadata = { | ||
| 61 | :asset_name => name, | ||
| 62 | :content_type => upload_content_type, | ||
| 63 | :path => upload.url.sub(/\?\d+$/, ""), | ||
| 64 | } | ||
| 65 | metadata[:detached_from] = affected.map(&:unique_name) if affected.any? | ||
| 66 | metadata[:headline_removed_from] = headline_losses.map(&:unique_name) if headline_losses.any? | ||
| 67 | |||
| 68 | NodeAction.record!(:participants => [self] + affected, :user => user, | ||
| 69 | :action => "asset_destroy", **metadata) | ||
| 70 | destroy! | ||
| 71 | end | ||
| 72 | end | ||
| 43 | end | 73 | end |
diff --git a/app/models/node_action.rb b/app/models/node_action.rb index 8a3dd8b7..9ed0b628 100644 --- a/app/models/node_action.rb +++ b/app/models/node_action.rb | |||
| @@ -69,6 +69,15 @@ class NodeAction < ApplicationRecord | |||
| 69 | # "path" -- final path, flat string (create-symmetric) | 69 | # "path" -- final path, flat string (create-symmetric) |
| 70 | # "destroyed_descendants" -- integer, only when positive; one entry | 70 | # "destroyed_descendants" -- integer, only when positive; one entry |
| 71 | # at the root, per the subtree rule. | 71 | # at the root, per the subtree rule. |
| 72 | # "asset_destroy" (witnessed asset deletion; always written, even for | ||
| 73 | # unattached assets -- the files were publicly reachable; node column | ||
| 74 | # nil, subjects via participants: the asset plus every then-attached | ||
| 75 | # node): | ||
| 76 | # "asset_name" -- flat string | ||
| 77 | # "content_type" -- flat string | ||
| 78 | # "path" -- public original path, flat string | ||
| 79 | # "detached_from" -- array of unique_names, only when any | ||
| 80 | # "headline_removed_from" -- array of unique_names, only when any | ||
| 72 | # | 81 | # |
| 73 | # Reserved: "demote" (via "trash" | "depublish") for an explicit | 82 | # Reserved: "demote" (via "trash" | "depublish") for an explicit |
| 74 | # depublish workflow, if ever built. | 83 | # depublish workflow, if ever built. |
