2016-11-15 15:56:29 +00:00
# frozen_string_literal: true
2023-02-20 05:58:28 +00:00
2017-05-02 00:14:47 +00:00
# == Schema Information
#
# Table name: media_attachments
#
2020-04-26 21:29:08 +00:00
# id :bigint(8) not null, primary key
# status_id :bigint(8)
# file_file_name :string
# file_content_type :string
# file_file_size :integer
# file_updated_at :datetime
# remote_url :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# shortcode :string
# type :integer default("image"), not null
# file_meta :json
# account_id :bigint(8)
# description :text
# scheduled_status_id :bigint(8)
# blurhash :string
# processing :integer
# file_storage_schema_version :integer
2020-06-29 11:56:55 +00:00
# thumbnail_file_name :string
# thumbnail_content_type :string
# thumbnail_file_size :integer
# thumbnail_updated_at :datetime
# thumbnail_remote_url :string
2017-05-02 00:14:47 +00:00
#
2016-11-15 15:56:29 +00:00
2016-09-05 15:46:36 +00:00
class MediaAttachment < ApplicationRecord
2017-03-04 21:17:10 +00:00
self . inheritance_column = nil
2021-09-29 21:52:36 +00:00
include Attachmentable
2024-02-16 14:54:23 +00:00
enum :type , { image : 0 , gifv : 1 , video : 2 , unknown : 3 , audio : 4 }
enum :processing , { queued : 0 , in_progress : 1 , complete : 2 , failed : 3 } , prefix : true
2017-03-04 21:17:10 +00:00
2019-11-04 12:00:16 +00:00
MAX_DESCRIPTION_LENGTH = 1_500
2023-03-25 09:00:03 +00:00
IMAGE_LIMIT = 16 . megabytes
VIDEO_LIMIT = 99 . megabytes
2022-02-22 16:11:22 +00:00
2023-03-25 09:00:03 +00:00
MAX_VIDEO_MATRIX_LIMIT = 8_294_400 # 3840x2160px
MAX_VIDEO_FRAME_RATE = 120
2023-08-28 17:40:08 +00:00
MAX_VIDEO_FRAMES = 36_000 # Approx. 5 minutes at 120 fps
2022-02-22 16:11:22 +00:00
2022-11-01 21:08:41 +00:00
IMAGE_FILE_EXTENSIONS = %w( .jpg .jpeg .png .gif .webp .heic .heif .avif ) . freeze
2019-07-18 01:02:30 +00:00
VIDEO_FILE_EXTENSIONS = %w( .webm .mp4 .m4v .mov ) . freeze
2019-08-30 02:30:29 +00:00
AUDIO_FILE_EXTENSIONS = %w( .ogg .oga .mp3 .wav .flac .opus .aac .m4a .3gp .wma ) . freeze
2017-09-20 17:07:23 +00:00
2020-07-05 16:28:25 +00:00
META_KEYS = % i (
focus
colors
original
small
) . freeze
2022-11-01 21:08:41 +00:00
IMAGE_MIME_TYPES = %w( image/jpeg image/png image/gif image/heic image/heif image/webp image/avif ) . freeze
2024-12-11 02:28:21 +00:00
IMAGE_ANIMATED_MIME_TYPES = %w( image/png image/gif ) . freeze
2023-08-01 17:34:11 +00:00
IMAGE_CONVERTIBLE_MIME_TYPES = %w( image/heic image/heif image/avif ) . freeze
2019-07-18 01:02:30 +00:00
VIDEO_MIME_TYPES = %w( video/webm video/mp4 video/quicktime video/ogg ) . freeze
VIDEO_CONVERTIBLE_MIME_TYPES = %w( video/webm video/quicktime ) . freeze
2022-06-28 17:49:35 +00:00
AUDIO_MIME_TYPES = %w( audio/wave audio/wav audio/x-wav audio/x-pn-wave audio/vnd.wave audio/ogg audio/vorbis audio/mpeg audio/mp3 audio/webm audio/flac audio/aac audio/m4a audio/x-m4a audio/mp4 audio/3gpp video/x-ms-asf ) . freeze
2016-09-12 16:22:43 +00:00
2019-04-27 01:24:09 +00:00
BLURHASH_OPTIONS = {
x_comp : 4 ,
y_comp : 4 ,
} . freeze
2018-02-21 02:40:12 +00:00
IMAGE_STYLES = {
original : {
2023-03-25 09:00:03 +00:00
pixels : 8_294_400 , # 3840x2160px
2018-02-21 02:40:12 +00:00
file_geometry_parser : FastGeometryParser ,
2020-06-29 11:56:55 +00:00
} . freeze ,
2018-02-21 02:40:12 +00:00
small : {
2022-11-01 12:01:39 +00:00
pixels : 230_400 , # 640x360px
2018-02-21 02:40:12 +00:00
file_geometry_parser : FastGeometryParser ,
2019-04-27 01:24:09 +00:00
blurhash : BLURHASH_OPTIONS ,
2020-06-29 11:56:55 +00:00
} . freeze ,
2018-02-21 02:40:12 +00:00
} . freeze
2022-11-01 15:26:25 +00:00
IMAGE_CONVERTED_STYLES = {
original : {
format : 'jpeg' ,
2022-11-14 06:13:14 +00:00
content_type : 'image/jpeg' ,
2022-11-01 15:26:25 +00:00
} . merge ( IMAGE_STYLES [ :original ] ) . freeze ,
small : {
format : 'jpeg' ,
} . merge ( IMAGE_STYLES [ :small ] ) . freeze ,
} . freeze
2020-03-09 01:20:18 +00:00
VIDEO_FORMAT = {
format : 'mp4' ,
content_type : 'video/mp4' ,
2022-02-22 16:11:22 +00:00
vfr_frame_rate_threshold : MAX_VIDEO_FRAME_RATE ,
2020-03-09 01:20:18 +00:00
convert_options : {
output : {
'loglevel' = > 'fatal' ,
2023-08-28 17:40:08 +00:00
'preset' = > 'veryfast' ,
2023-08-31 15:21:06 +00:00
'movflags' = > 'faststart' , # Move metadata to start of file so playback can begin before download finishes
'pix_fmt' = > 'yuv420p' , # Ensure color space for cross-browser compatibility
2024-12-11 02:28:21 +00:00
'filter_complex' = > 'drawbox=t=fill:c=white[bg];[bg][0]overlay,crop=trunc(iw/2)*2:trunc(ih/2)*2' , # Remove transparency. h264 requires width and height to be even; crop instead of scale to avoid blurring
2020-03-09 01:20:18 +00:00
'c:v' = > 'h264' ,
2023-08-28 17:40:08 +00:00
'c:a' = > 'aac' ,
'b:a' = > '192k' ,
2020-03-09 01:20:18 +00:00
'map_metadata' = > '-1' ,
2023-08-28 17:40:08 +00:00
'frames:v' = > MAX_VIDEO_FRAMES ,
2020-06-29 11:56:55 +00:00
} . freeze ,
} . freeze ,
2020-03-09 01:20:18 +00:00
} . freeze
2020-03-09 22:15:59 +00:00
VIDEO_PASSTHROUGH_OPTIONS = {
2020-06-29 11:56:55 +00:00
video_codecs : [ 'h264' ] . freeze ,
audio_codecs : [ 'aac' , nil ] . freeze ,
colorspaces : [ 'yuv420p' ] . freeze ,
2020-03-09 22:15:59 +00:00
options : {
format : 'mp4' ,
convert_options : {
output : {
'loglevel' = > 'fatal' ,
'map_metadata' = > '-1' ,
'c:v' = > 'copy' ,
'c:a' = > 'copy' ,
2020-06-29 11:56:55 +00:00
} . freeze ,
} . freeze ,
} . freeze ,
2020-03-09 22:15:59 +00:00
} . freeze
2017-03-04 21:17:10 +00:00
VIDEO_STYLES = {
small : {
convert_options : {
output : {
2019-10-06 17:48:26 +00:00
'loglevel' = > 'fatal' ,
2023-08-28 17:40:08 +00:00
:vf = > 'scale=\'min(640\, iw):min(640\, ih)\':force_original_aspect_ratio=decrease' ,
2020-06-29 11:56:55 +00:00
} . freeze ,
} . freeze ,
2017-03-04 21:17:10 +00:00
format : 'png' ,
time : 0 ,
2019-04-27 01:24:09 +00:00
file_geometry_parser : FastGeometryParser ,
blurhash : BLURHASH_OPTIONS ,
2020-06-29 11:56:55 +00:00
} . freeze ,
2019-10-02 23:09:12 +00:00
2020-06-29 11:56:55 +00:00
original : VIDEO_FORMAT . merge ( passthrough_options : VIDEO_PASSTHROUGH_OPTIONS ) . freeze ,
2017-03-04 21:17:10 +00:00
} . freeze
2019-06-19 21:42:38 +00:00
AUDIO_STYLES = {
original : {
2019-06-21 20:59:44 +00:00
format : 'mp3' ,
content_type : 'audio/mpeg' ,
convert_options : {
output : {
2019-10-06 17:48:26 +00:00
'loglevel' = > 'fatal' ,
2019-06-21 20:59:44 +00:00
'q:a' = > 2 ,
2020-06-29 11:56:55 +00:00
} . freeze ,
} . freeze ,
} . freeze ,
2019-06-19 21:42:38 +00:00
} . freeze
VIDEO_CONVERTED_STYLES = {
2020-06-29 11:56:55 +00:00
small : VIDEO_STYLES [ :small ] . freeze ,
original : VIDEO_FORMAT . freeze ,
} . freeze
THUMBNAIL_STYLES = {
original : IMAGE_STYLES [ :small ] . freeze ,
} . freeze
2023-05-04 03:33:55 +00:00
DEFAULT_STYLES = [ :original ] . freeze
2020-06-29 11:56:55 +00:00
GLOBAL_CONVERT_OPTIONS = {
2023-09-25 17:21:07 +00:00
all : '-quality 90 +profile "!icc,*" +set date:modify +set date:create +set date:timestamp -define jpeg:dct-method=float' ,
2019-06-19 21:42:38 +00:00
} . freeze
2019-01-05 11:43:28 +00:00
belongs_to :account , inverse_of : :media_attachments , optional : true
belongs_to :status , inverse_of : :media_attachments , optional : true
belongs_to :scheduled_status , inverse_of : :media_attachments , optional : true
2016-09-05 15:46:36 +00:00
2016-10-08 13:15:43 +00:00
has_attached_file :file ,
2017-03-04 21:17:10 +00:00
styles : - > ( f ) { file_styles f } ,
processors : - > ( f ) { file_processors f } ,
2020-06-29 11:56:55 +00:00
convert_options : GLOBAL_CONVERT_OPTIONS
2017-05-18 13:43:10 +00:00
2021-10-06 13:49:32 +00:00
before_file_validate :set_type_and_extension
before_file_validate :check_video_dimensions
Fix larger video files not being transcoded (#14306)
Since #14145, the `set_type_and_extension` has been moved from
`before_post_process` to `before_file_post_process`, but while the former
runs before all validations performed by Paperclip, the latter is dependent
on the order validations and hooks are defined.
In our case, this meant video files could be checked against the generic 10MB
limit, causing validation failures, which, internally, make Paperclip skip
post-processing, and thus, transcoding of the video file.
The actual validation would then happen after the type is correctly set, so
the large file would pass validation, but without being transcoded first.
This commit moves the hook definition so that it is run before checking for
the file size.
2020-07-14 16:50:19 +00:00
2019-06-19 21:42:38 +00:00
validates_attachment_content_type :file , content_type : IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES
2021-10-06 13:49:32 +00:00
validates_attachment_size :file , less_than : - > ( m ) { m . larger_media_format? ? VIDEO_LIMIT : IMAGE_LIMIT }
2020-06-29 11:56:55 +00:00
remotable_attachment :file , VIDEO_LIMIT , suppress_errors : false , download_on_assign : false , attribute_name : :remote_url
has_attached_file :thumbnail ,
styles : THUMBNAIL_STYLES ,
2020-07-05 16:28:25 +00:00
processors : [ :lazy_thumbnail , :blurhash_transcoder , :color_extractor ] ,
2020-06-29 11:56:55 +00:00
convert_options : GLOBAL_CONVERT_OPTIONS
validates_attachment_content_type :thumbnail , content_type : IMAGE_MIME_TYPES
validates_attachment_size :thumbnail , less_than : IMAGE_LIMIT
remotable_attachment :thumbnail , IMAGE_LIMIT , suppress_errors : true , download_on_assign : false
2016-09-05 15:46:36 +00:00
validates :account , presence : true
2022-03-25 23:38:44 +00:00
validates :description , length : { maximum : MAX_DESCRIPTION_LENGTH }
2020-01-23 20:40:03 +00:00
validates :file , presence : true , if : :local?
2020-06-29 11:56:55 +00:00
validates :thumbnail , absence : true , if : - > { local? && ! audio_or_video? }
2016-09-05 15:46:36 +00:00
2024-01-24 10:32:54 +00:00
scope :attached , - > { where . not ( status_id : nil ) . or ( where . not ( scheduled_status_id : nil ) ) }
scope :cached , - > { remote . where . not ( file_file_name : nil ) }
scope :created_before , - > ( value ) { where ( arel_table [ :created_at ] . lt ( value ) ) }
scope :local , - > { where ( remote_url : '' ) }
scope :ordered , - > { order ( id : :asc ) }
scope :remote , - > { where . not ( remote_url : '' ) }
2023-11-30 13:30:35 +00:00
scope :unattached , - > { where ( status_id : nil , scheduled_status_id : nil ) }
2024-01-24 10:32:54 +00:00
scope :updated_before , - > ( value ) { where ( arel_table [ :updated_at ] . lt ( value ) ) }
2016-11-28 12:49:42 +00:00
2022-12-15 17:09:48 +00:00
attr_accessor :skip_download
2016-09-05 15:46:36 +00:00
def local?
2016-09-29 19:28:21 +00:00
remote_url . blank?
2016-09-05 15:46:36 +00:00
end
2016-09-05 16:39:53 +00:00
2020-03-08 22:56:18 +00:00
def not_processed?
processing . present? && ! processing_complete?
end
2017-09-16 01:01:45 +00:00
def needs_redownload?
file . blank? && remote_url . present?
end
2022-02-09 23:15:30 +00:00
def significantly_changed?
description_previously_changed? || thumbnail_updated_at_previously_changed? || file_meta_previously_changed?
end
2019-06-19 21:42:38 +00:00
def larger_media_format?
video? || gifv? || audio?
end
def audio_or_video?
audio? || video?
2019-02-04 03:46:05 +00:00
end
2017-01-05 23:21:12 +00:00
def to_param
2021-10-13 13:27:19 +00:00
shortcode . presence || id & . to_s
2017-01-05 23:21:12 +00:00
end
2018-02-21 23:35:46 +00:00
def focus = ( point )
return if point . blank?
x , y = ( point . is_a? ( Enumerable ) ? point : point . split ( ',' ) ) . map ( & :to_f )
2020-07-05 16:28:25 +00:00
meta = ( file . instance_read ( :meta ) || { } ) . with_indifferent_access . slice ( * META_KEYS )
2018-02-21 23:35:46 +00:00
meta [ 'focus' ] = { 'x' = > x , 'y' = > y }
file . instance_write ( :meta , meta )
end
def focus
2020-06-24 23:33:01 +00:00
x = file . meta & . dig ( 'focus' , 'x' )
y = file . meta & . dig ( 'focus' , 'y' )
return if x . nil? || y . nil?
2018-02-21 23:35:46 +00:00
" #{ x } , #{ y } "
end
2020-03-08 22:56:18 +00:00
attr_writer :delay_processing
def delay_processing?
2022-11-01 14:27:58 +00:00
@delay_processing && larger_media_format?
2020-03-08 22:56:18 +00:00
end
2020-06-29 11:56:55 +00:00
def delay_processing_for_attachment? ( attachment_name )
2022-11-01 14:27:58 +00:00
delay_processing? && attachment_name == :file
2020-06-29 11:56:55 +00:00
end
2021-09-13 16:59:37 +00:00
before_create :set_unknown_type
2020-03-08 22:56:18 +00:00
before_create :set_processing
2020-01-04 00:54:07 +00:00
2024-08-09 12:48:34 +00:00
before_destroy :prepare_cache_bust! , prepend : true
after_destroy :bust_cache!
2023-05-02 16:23:35 +00:00
after_commit :enqueue_processing , on : :create
after_commit :reset_parent_cache , on : :update
2020-06-29 11:56:55 +00:00
after_post_process :set_meta
2016-10-23 09:56:04 +00:00
class << self
2019-06-22 00:50:36 +00:00
def supported_mime_types
IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES
end
def supported_file_extensions
IMAGE_FILE_EXTENSIONS + VIDEO_FILE_EXTENSIONS + AUDIO_FILE_EXTENSIONS
end
2016-10-23 09:56:04 +00:00
private
2020-06-29 11:56:55 +00:00
def file_styles ( attachment )
2024-12-11 02:28:21 +00:00
if attachment . instance . animated_image? || VIDEO_CONVERTIBLE_MIME_TYPES . include? ( attachment . instance . file_content_type )
2019-06-19 21:42:38 +00:00
VIDEO_CONVERTED_STYLES
2022-11-01 15:26:25 +00:00
elsif IMAGE_CONVERTIBLE_MIME_TYPES . include? ( attachment . instance . file_content_type )
IMAGE_CONVERTED_STYLES
2020-06-29 11:56:55 +00:00
elsif IMAGE_MIME_TYPES . include? ( attachment . instance . file_content_type )
2017-03-04 21:17:10 +00:00
IMAGE_STYLES
2020-06-29 11:56:55 +00:00
elsif VIDEO_MIME_TYPES . include? ( attachment . instance . file_content_type )
2017-03-04 21:17:10 +00:00
VIDEO_STYLES
2019-06-19 21:42:38 +00:00
else
AUDIO_STYLES
2017-03-04 21:17:10 +00:00
end
end
2020-06-29 11:56:55 +00:00
def file_processors ( instance )
2024-12-11 02:28:21 +00:00
if instance . animated_image?
[ :gifv_transcoder , :blurhash_transcoder ]
2020-06-29 11:56:55 +00:00
elsif VIDEO_MIME_TYPES . include? ( instance . file_content_type )
2021-05-05 17:44:01 +00:00
[ :transcoder , :blurhash_transcoder , :type_corrector ]
2020-06-29 11:56:55 +00:00
elsif AUDIO_MIME_TYPES . include? ( instance . file_content_type )
[ :image_extractor , :transcoder , :type_corrector ]
2017-03-04 21:17:10 +00:00
else
2019-06-20 08:52:36 +00:00
[ :lazy_thumbnail , :blurhash_transcoder , :type_corrector ]
2016-10-23 09:56:04 +00:00
end
2016-10-08 13:15:43 +00:00
end
end
2017-01-05 23:21:12 +00:00
2024-12-11 02:28:21 +00:00
def animated_image?
if processing_complete?
gifv?
elsif IMAGE_ANIMATED_MIME_TYPES . include? ( file_content_type )
@animated_image = FastImage . animated? ( file . queued_for_write [ :original ] . path ) unless defined? ( @animated_image )
@animated_image
else
false
end
end
2017-01-05 23:21:12 +00:00
private
2021-09-13 16:59:37 +00:00
def set_unknown_type
2017-04-28 22:18:32 +00:00
self . type = :unknown if file . blank? && ! type_changed?
2017-01-05 23:21:12 +00:00
end
2017-03-04 21:17:10 +00:00
2017-04-18 21:15:44 +00:00
def set_type_and_extension
2019-06-19 21:42:38 +00:00
self . type = begin
if VIDEO_MIME_TYPES . include? ( file_content_type )
:video
elsif AUDIO_MIME_TYPES . include? ( file_content_type )
:audio
else
:image
end
end
2017-04-19 21:21:00 +00:00
end
2020-03-08 22:56:18 +00:00
def set_processing
self . processing = delay_processing? ? :queued : :complete
end
2020-03-09 01:19:07 +00:00
def check_video_dimensions
return unless ( video? || gifv? ) && file . queued_for_write [ :original ] . present?
2020-06-29 11:56:55 +00:00
movie = ffmpeg_data ( file . queued_for_write [ :original ] . path )
2020-03-09 01:19:07 +00:00
return unless movie . valid?
2020-07-19 20:28:27 +00:00
raise Mastodon :: StreamValidationError , 'Video has no video stream' if movie . width . nil? || movie . frame_rate . nil?
2020-03-09 01:19:07 +00:00
raise Mastodon :: DimensionsValidationError , " #{ movie . width } x #{ movie . height } videos are not supported " if movie . width * movie . height > MAX_VIDEO_MATRIX_LIMIT
2020-08-29 23:54:30 +00:00
raise Mastodon :: DimensionsValidationError , " #{ movie . frame_rate . floor } fps videos are not supported " if movie . frame_rate . floor > MAX_VIDEO_FRAME_RATE
2020-03-09 01:19:07 +00:00
end
2017-04-26 01:48:12 +00:00
def set_meta
2020-06-24 23:33:01 +00:00
file . instance_write :meta , populate_meta
2017-04-26 01:48:12 +00:00
end
def populate_meta
2020-07-05 16:28:25 +00:00
meta = ( file . instance_read ( :meta ) || { } ) . with_indifferent_access . slice ( * META_KEYS )
2017-09-01 14:20:16 +00:00
2017-04-26 01:48:12 +00:00
file . queued_for_write . each do | style , file |
2018-02-16 06:22:20 +00:00
meta [ style ] = style == :small || image? ? image_geometry ( file ) : video_metadata ( file )
2017-04-26 01:48:12 +00:00
end
2017-09-01 14:20:16 +00:00
2020-06-29 11:56:55 +00:00
meta [ :small ] = image_geometry ( thumbnail . queued_for_write [ :original ] ) if thumbnail . queued_for_write . key? ( :original )
2017-04-26 01:48:12 +00:00
meta
end
2018-02-16 06:22:20 +00:00
def image_geometry ( file )
2018-02-21 02:40:12 +00:00
width , height = FastImage . size ( file . path )
return { } if width . nil?
2018-02-16 06:22:20 +00:00
{
2023-02-20 05:58:28 +00:00
width : width ,
2018-02-21 02:40:12 +00:00
height : height ,
size : " #{ width } x #{ height } " ,
2019-12-02 17:25:43 +00:00
aspect : width . to_f / height ,
2018-02-16 06:22:20 +00:00
}
end
def video_metadata ( file )
2020-06-29 11:56:55 +00:00
movie = ffmpeg_data ( file . path )
2018-02-16 06:22:20 +00:00
return { } unless movie . valid?
{
width : movie . width ,
height : movie . height ,
frame_rate : movie . frame_rate ,
duration : movie . duration ,
bitrate : movie . bitrate ,
2019-06-19 21:42:38 +00:00
} . compact
2018-02-16 06:22:20 +00:00
end
2018-10-28 05:42:34 +00:00
2020-06-29 11:56:55 +00:00
# We call this method about 3 different times on potentially different
# paths but ultimately the same file, so it makes sense to memoize the
# result while disregarding the path
def ffmpeg_data ( path = nil )
2021-05-05 17:44:01 +00:00
@ffmpeg_data || = VideoMetadataExtractor . new ( path )
2020-06-29 11:56:55 +00:00
end
2020-03-08 22:56:18 +00:00
def enqueue_processing
PostProcessMediaWorker . perform_async ( id ) if delay_processing?
end
2019-10-02 23:09:12 +00:00
2020-03-08 22:56:18 +00:00
def reset_parent_cache
2023-09-15 17:52:28 +00:00
Rails . cache . delete ( " v3:statuses/ #{ status_id } " ) if status_id . present?
2018-10-28 05:42:34 +00:00
end
2024-08-09 12:48:34 +00:00
# Record the cache keys to burst before the file get actually deleted
def prepare_cache_bust!
return unless Rails . configuration . x . cache_buster_enabled
@paths_to_cache_bust = MediaAttachment . attachment_definitions . keys . flat_map do | attachment_name |
attachment = public_send ( attachment_name )
styles = DEFAULT_STYLES | attachment . styles . keys
styles . map { | style | attachment . path ( style ) }
2024-08-14 08:57:42 +00:00
end . compact
2024-08-09 12:48:34 +00:00
rescue = > e
# We really don't want any error here preventing media deletion
Rails . logger . warn " Error #{ e . class } busting cache: #{ e . message } "
end
# Once Paperclip has deleted the files, we can't recover the cache keys,
# so use the previously-saved ones
def bust_cache!
return unless Rails . configuration . x . cache_buster_enabled
CacheBusterWorker . push_bulk ( @paths_to_cache_bust ) { | path | [ path ] }
rescue = > e
# We really don't want any error here preventing media deletion
Rails . logger . warn " Error #{ e . class } busting cache: #{ e . message } "
end
2016-09-05 15:46:36 +00:00
end