From 6ef98ad444631b1d5ba67bb16aaaa2afdfa53ae0 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Thu, 23 Jul 2026 04:12:39 +0200 Subject: Witness asset destruction, naming every node it strips --- app/models/asset.rb | 30 ++++++++++++++++++++++++++++++ app/models/node_action.rb | 9 +++++++++ 2 files changed, 39 insertions(+) (limited to 'app/models') 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 Node.where("head_id IN (:ids) OR draft_id IN (:ids) OR autosave_id IN (:ids)", :ids => page_ids).distinct end + + # Witnessed destruction. Destroying an asset is a public-facing act + # even when unattached. The original and its variants are publicly + # reachable under /system/uploads, so an entry is always written, + # before the row and its files die. Every currently-attached node + # participates so its zoomed history shows the loss; the asset itself + # participates as the first non-Node subject (its participant row + # dangles after destroy, by design, the name lives on in metadata). + def destroy_witnessed! user: + ActiveRecord::Base.transaction do + affected = attached_nodes.to_a + headline_losses = affected.select do |node| + [node.head, node.draft, node.autosave].compact.any? do |row| + row.related_assets.exists?(:asset_id => id, :headline => true) + end + end + + metadata = { + :asset_name => name, + :content_type => upload_content_type, + :path => upload.url.sub(/\?\d+$/, ""), + } + metadata[:detached_from] = affected.map(&:unique_name) if affected.any? + metadata[:headline_removed_from] = headline_losses.map(&:unique_name) if headline_losses.any? + + NodeAction.record!(:participants => [self] + affected, :user => user, + :action => "asset_destroy", **metadata) + destroy! + end + end 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 # "path" -- final path, flat string (create-symmetric) # "destroyed_descendants" -- integer, only when positive; one entry # at the root, per the subtree rule. + # "asset_destroy" (witnessed asset deletion; always written, even for + # unattached assets -- the files were publicly reachable; node column + # nil, subjects via participants: the asset plus every then-attached + # node): + # "asset_name" -- flat string + # "content_type" -- flat string + # "path" -- public original path, flat string + # "detached_from" -- array of unique_names, only when any + # "headline_removed_from" -- array of unique_names, only when any # # Reserved: "demote" (via "trash" | "depublish") for an explicit # depublish workflow, if ever built. -- cgit v1.3