Merge branch 'main' into mobile/bridge-components

* main: (22 commits)
  Disable card column buttons when active
  Prevent comment content from overlapping with reaction button
  Delete pins belonging to cards in a board with revoked access
  Update development dependencies in SAAS lockfile
  Update runtime dependencies
  Update development dependencies
  Balance magic link instructions
  Add `.txt-balance` utility
  Move handleDesktop inside restoreColumnsDisablingTransitions
  Adjust border radius
  Clean up blank slates
  Add dialog manager to events page
  More sturated mini bubbles in light mode
  Brighten mini bubbles in dark mode
  Use separate cache namespaces for each beta instance
  Bump boost size up a tick on mobile
  Move Undo form inside closure message element
  Phrasing tweak for exceeding storage limit
  Keep strong tags words on same line
  Allow ActionText embed reuse and safe purge (#2346)
  ...
This commit is contained in:
Adrien Maston
2026-01-14 10:34:13 +01:00
36 changed files with 338 additions and 156 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
default_options: &default_options
store_options:
max_age: <%= 60.days.to_i %>
namespace: <%= Rails.env %>
namespace: <%= "#{Rails.env}#{"-#{ENV["CACHE_NAMESPACE"]}" if ENV["CACHE_NAMESPACE"]}" %>
default_connection: &default_connection
database: cache
+19 -16
View File
@@ -16,6 +16,8 @@
# attached to an untracked type (avatar/export) could theoretically be reused in a tracked
# context. This is acceptable - user-accessible blob IDs from untracked contexts are
# basically nonexistent in practice.
#
# Exception: ActionText embeds are allowed to reuse blobs to support copy/paste.
ActiveSupport.on_load(:active_storage_attachment) do
validate :blob_account_matches_record, on: :create
@@ -29,30 +31,31 @@ ActiveSupport.on_load(:active_storage_attachment) do
# bypass tenancy checks via try(:account) returning nil - this is intentional as
# these classes don't participate in storage tracking.
def blob_account_matches_record
return unless record&.try(:account).present?
return if whitelisted_for_cross_account?
unless blob&.account_id == record.account.id
errors.add(:blob_id, "blob account must match record account")
if record&.try(:account).present? && !whitelisted_for_cross_account?
unless blob&.account_id == record.account.id
errors.add(:blob_id, "blob account must match record account")
end
end
end
# Ledger integrity: blob can only have one tracked attachment
def no_tracked_blob_reuse
tracked_record = record&.try(:storage_tracked_record)
return unless tracked_record.present?
return if whitelisted_for_cross_account?
if tracked_record.present? &&
!whitelisted_for_cross_account? &&
!(record_type == "ActionText::RichText" && name == "embeds")
# Check for existing attachment of this blob in tracked contexts
# Uses Storage::TRACKED_RECORD_TYPES constant to stay generic
existing = ActiveStorage::Attachment
.where(blob_id: blob_id)
.where(record_type: Storage::TRACKED_RECORD_TYPES)
.where.not(id: id)
.exists?
# Check for existing attachment of this blob in tracked contexts
# Uses Storage::TRACKED_RECORD_TYPES constant to stay generic
existing = ActiveStorage::Attachment
.where(blob_id: blob_id)
.where(record_type: Storage::TRACKED_RECORD_TYPES)
.where.not(id: id)
.exists?
if existing
errors.add(:blob_id, "cannot reuse blob in tracked storage context")
if existing
errors.add(:blob_id, "cannot reuse blob in tracked storage context")
end
end
end
@@ -0,0 +1,40 @@
# Fizzy-specific override: ActiveStorage's default purge path uses `delete`,
# which skips attachment callbacks. We need `destroy` so storage ledger detaches
# are recorded and reused blobs (ActionText embeds) aren't purged until the
# last attachment is gone. Keep this local to Fizzy; it's not a Rails default.
module ActiveStorage
module PurgeOnLastAttachment
def purge
@purge_mode = :purge
destroy
purge_blob_if_last(:purge) if destroyed?
ensure
@purge_mode = nil
end
def purge_later
@purge_mode = :purge_later
destroy
purge_blob_if_last(:purge_later) if destroyed?
ensure
@purge_mode = nil
end
private
def purge_dependent_blob_later
if dependent == :purge_later && !@purge_mode
purge_blob_if_last(:purge_later)
end
end
def purge_blob_if_last(mode)
if blob && !blob.attachments.exists?
mode == :purge ? blob.purge : blob.purge_later
end
end
end
end
ActiveSupport.on_load(:active_storage_attachment) do
prepend ActiveStorage::PurgeOnLastAttachment
end