Corregge validazione upload copertina e generazione slate al go-live.

Rifiuta file troppo grandi o non-immagine via Marcel prima di attach, e genera la slate in sync anche quando cover_image esiste ma cover_slate non è ancora pronta.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-20 09:51:33 +02:00
co-authored by Cursor
parent 41d4235258
commit e09b3ffcf8
7 changed files with 50 additions and 8 deletions
@@ -2,6 +2,7 @@ module Api
module V1 module V1
class BaseController < ApplicationController class BaseController < ApplicationController
rescue_from Teams::EntitlementError, with: :render_entitlement_error rescue_from Teams::EntitlementError, with: :render_entitlement_error
rescue_from BrandingAttachments::CoverUploadError, with: :render_cover_upload_error
rescue_from Youtube::BroadcastService::Error, with: :render_youtube_error rescue_from Youtube::BroadcastService::Error, with: :render_youtube_error
private private
@@ -20,6 +21,10 @@ module Api
billing_url: error.billing_url billing_url: error.billing_url
}, status: :forbidden }, status: :forbidden
end end
def render_cover_upload_error(error)
render json: { error: error.message, error_code: "cover_upload_invalid" }, status: :unprocessable_entity
end
end end
end end
end end
@@ -20,10 +20,32 @@ module BrandingAttachments
return if file.blank? return if file.blank?
entitlements.assert_can_upload_cover! entitlements.assert_can_upload_cover!
validate_cover_file!(file)
record.cover_slate.purge if record.cover_slate.attached? record.cover_slate.purge if record.cover_slate.attached?
record.cover_image.attach(file) record.cover_image.attach(file)
end end
def validate_cover_file!(file)
allowed_types = Coverable::COVER_IMAGE_TYPES
# Rileva MIME dal contenuto (magic bytes) ignorando il declared_type del client.
io = file.respond_to?(:tempfile) ? file.tempfile : file
io.rewind if io.respond_to?(:rewind)
detected = Marcel::MimeType.for(io, name: file.respond_to?(:original_filename) ? file.original_filename : nil)
io.rewind if io.respond_to?(:rewind)
unless detected.in?(allowed_types)
raise CoverUploadError, I18n.t("coverable.errors.invalid_type")
end
byte_size = file.respond_to?(:size) ? file.size : File.size(file.path)
if byte_size > Coverable::MAX_COVER_BYTES
raise CoverUploadError, I18n.t("coverable.errors.too_large")
end
end
class CoverUploadError < StandardError; end
def enqueue_cover_slate_generation(record) def enqueue_cover_slate_generation(record)
GenerateCoverSlateJob.enqueue_for(record) if record.cover_image.attached? GenerateCoverSlateJob.enqueue_for(record) if record.cover_image.attached?
end end
@@ -80,7 +80,7 @@ module Public
@entitlements = @club.teams.first&.entitlements @entitlements = @club.teams.first&.entitlements
flash.now[:alert] = e.record.errors.full_messages.join(", ") flash.now[:alert] = e.record.errors.full_messages.join(", ")
render :edit, status: :unprocessable_entity render :edit, status: :unprocessable_entity
rescue Teams::EntitlementError => e rescue Teams::EntitlementError, BrandingAttachments::CoverUploadError => e
@entitlements = @club.teams.first&.entitlements @entitlements = @club.teams.first&.entitlements
flash.now[:alert] = e.message flash.now[:alert] = e.message
render :edit, status: :unprocessable_entity render :edit, status: :unprocessable_entity
@@ -59,7 +59,7 @@ module Public
@entitlements = @team.entitlements @entitlements = @team.entitlements
flash.now[:alert] = e.record.errors.full_messages.join(", ") flash.now[:alert] = e.record.errors.full_messages.join(", ")
render :edit, status: :unprocessable_entity render :edit, status: :unprocessable_entity
rescue Teams::EntitlementError => e rescue Teams::EntitlementError, BrandingAttachments::CoverUploadError => e
@club = @team.club @club = @team.club
@entitlements = @team.entitlements @entitlements = @team.entitlements
flash.now[:alert] = e.message flash.now[:alert] = e.message
+2 -2
View File
@@ -8,8 +8,8 @@ module Coverable
has_one_attached :cover_image has_one_attached :cover_image
has_one_attached :cover_slate has_one_attached :cover_slate
validate :cover_image_file_type, if: -> { cover_image.attached? && cover_image.blob&.new_record? } validate :cover_image_file_type, if: -> { cover_image.attached? }
validate :cover_image_file_size, if: -> { cover_image.attached? && cover_image.blob&.new_record? } validate :cover_image_file_size, if: -> { cover_image.attached? }
end end
def cover_image_attached? def cover_image_attached?
@@ -41,6 +41,17 @@ module Streams
nil nil
end end
# Restituisce il record con cover_image nella catena anche se la slate non è ancora su disco.
# Usato da CoverSlateEnsurer per sapere cosa generare al go-live.
def pending_cover_record
return nil unless @entitlements.can_use_custom_cover?
candidates.each do |record, _source|
return record if record.cover_image.attached?
end
nil
end
def default_cover_url def default_cover_url
DEFAULT_COVER_URL DEFAULT_COVER_URL
end end
@@ -11,10 +11,8 @@ module Streams
end end
def ensure! def ensure!
result = @resolver.resolve record = find_pending_cover_record
return unless result return unless record
record = result.record
return if slate_ready?(record) return if slate_ready?(record)
GenerateCoverSlate.call(record) GenerateCoverSlate.call(record)
@@ -25,6 +23,12 @@ module Streams
private private
# Cerca il record con cover_image nella catena di ereditarietà,
# anche se la slate non è ancora pronta (a differenza di resolve che richiede la slate su disco).
def find_pending_cover_record
@resolver.pending_cover_record
end
def slate_ready?(record) def slate_ready?(record)
return false unless record.cover_slate.attached? return false unless record.cover_slate.attached?