summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-08-03 04:13:34 +0200
committererdgeist <erdgeist@erdgeist.org>2026-08-03 04:13:34 +0200
commit0fa491188456332bacddb6310d86014cd8c87688 (patch)
treea1238383f29c28c179858f5643512de36836c6ff /app
parent45bb56ad1d809bead00379e68144e91bb471da06 (diff)
Believe the file over the browser about what was uploaded
Diffstat (limited to 'app')
-rw-r--r--app/models/concerns/file_attachment.rb54
1 files changed, 49 insertions, 5 deletions
diff --git a/app/models/concerns/file_attachment.rb b/app/models/concerns/file_attachment.rb
index fd0cabed..38b0b706 100644
--- a/app/models/concerns/file_attachment.rb
+++ b/app/models/concerns/file_attachment.rb
@@ -37,6 +37,20 @@ module FileAttachment
37 "MAGICK_CONFIGURE_PATH" => Rails.root.join("config", "imagemagick").to_s 37 "MAGICK_CONFIGURE_PATH" => Rails.root.join("config", "imagemagick").to_s
38 }.freeze 38 }.freeze
39 39
40 # ImageMagick picks its decoder from the file, not from the multipart
41 # header. Where the two disagree, believe ImageMagick: otherwise
42 # generate_all_variants dispatches on a claim and hands a file to a
43 # pipeline built for something else.
44 MAGICK_FORMAT_CONTENT_TYPES = {
45 "JPEG" => "image/jpeg",
46 "PNG" => "image/png",
47 "GIF" => "image/gif",
48 "WEBP" => "image/webp",
49 "SVG" => "image/svg+xml",
50 "MSVG" => "image/svg+xml",
51 "PDF" => "application/pdf"
52 }.freeze
53
40 IMAGE_CONTENT_TYPES = %w[image/jpeg image/gif image/png image/webp].freeze 54 IMAGE_CONTENT_TYPES = %w[image/jpeg image/gif image/png image/webp].freeze
41 VECTOR_CONTENT_TYPES = %w[image/svg+xml].freeze 55 VECTOR_CONTENT_TYPES = %w[image/svg+xml].freeze
42 RASTERIZED_CONTENT_TYPES = %w[application/pdf].freeze 56 RASTERIZED_CONTENT_TYPES = %w[application/pdf].freeze
@@ -88,6 +102,7 @@ module FileAttachment
88 after_initialize :build_upload_proxy 102 after_initialize :build_upload_proxy
89 after_save :process_upload 103 after_save :process_upload
90 before_destroy :delete_upload_files 104 before_destroy :delete_upload_files
105 validate :upload_must_be_readable_when_it_claims_to_be_an_image
91 end 106 end
92 107
93 def upload=(uploaded_file) 108 def upload=(uploaded_file)
@@ -95,7 +110,10 @@ module FileAttachment
95 @pending_upload = uploaded_file 110 @pending_upload = uploaded_file
96 # Populate the database columns immediately so validations can use them 111 # Populate the database columns immediately so validations can use them
97 self.upload_file_name = sanitize_filename(uploaded_file.original_filename) 112 self.upload_file_name = sanitize_filename(uploaded_file.original_filename)
98 self.upload_content_type = uploaded_file.content_type.to_s.split(';').first.strip 113 detected = detected_content_type(uploaded_file)
114 @upload_unreadable = detected.nil?
115 self.upload_content_type = detected ||
116 uploaded_file.content_type.to_s.split(';').first.strip
99 self.upload_file_size = uploaded_file.size 117 self.upload_file_size = uploaded_file.size
100 self.upload_updated_at = Time.current 118 self.upload_updated_at = Time.current
101 build_upload_proxy 119 build_upload_proxy
@@ -160,7 +178,9 @@ module FileAttachment
160 STYLES.each do |style, options| 178 STYLES.each do |style, options|
161 dest_path = file_path(style) 179 dest_path = file_path(style)
162 FileUtils.mkdir_p(File.dirname(dest_path)) 180 FileUtils.mkdir_p(File.dirname(dest_path))
163 system(MAGICK_ENV, "magick", original_path, *extra_args, *options[:args], dest_path) 181 if !system(MAGICK_ENV, "magick", original_path, *extra_args, *options[:args], dest_path)
182 Rails.logger.warn("Asset##{id}: magick failed for #{style} of #{upload_file_name}")
183 end
164 end 184 end
165 end 185 end
166 186
@@ -180,7 +200,7 @@ module FileAttachment
180 dest_path = file_path(:og) 200 dest_path = file_path(:og)
181 FileUtils.mkdir_p(File.dirname(dest_path)) 201 FileUtils.mkdir_p(File.dirname(dest_path))
182 202
183 if og_full_bleed?(original_path) 203 ok = if og_full_bleed?(original_path)
184 system(MAGICK_ENV, "magick", "#{original_path}[0]", 204 system(MAGICK_ENV, "magick", "#{original_path}[0]",
185 "-resize", "#{OG_WIDTH}x#{OG_HEIGHT}^", 205 "-resize", "#{OG_WIDTH}x#{OG_HEIGHT}^",
186 "-gravity", "center", "-extent", "#{OG_WIDTH}x#{OG_HEIGHT}", 206 "-gravity", "center", "-extent", "#{OG_WIDTH}x#{OG_HEIGHT}",
@@ -189,6 +209,9 @@ module FileAttachment
189 else 209 else
190 system(MAGICK_ENV, *og_template_command(dest_path)) 210 system(MAGICK_ENV, *og_template_command(dest_path))
191 end 211 end
212
213 Rails.logger.warn("Asset##{id}: magick failed for og of #{upload_file_name}") unless ok
214 ok
192 end 215 end
193 216
194 def generate_all_variants(original_path) 217 def generate_all_variants(original_path)
@@ -214,8 +237,11 @@ module FileAttachment
214 end 237 end
215 238
216 def source_dimensions(path) 239 def source_dimensions(path)
217 out, status = Open3.capture2(MAGICK_ENV, "magick", "identify", "-format", "%w %h", "#{path}[0]") 240 out, err, status = Open3.capture3(MAGICK_ENV, "magick", "identify", "-format", "%w %h", "#{path}[0]")
218 return nil unless status.success? 241 unless status.success?
242 Rails.logger.warn("Asset##{id}: magick identify failed for #{path}: #{err.lines.first&.strip}")
243 return nil
244 end
219 245
220 width, height = out.split.map(&:to_i) 246 width, height = out.split.map(&:to_i)
221 (width.positive? && height.positive?) ? [width, height] : nil 247 (width.positive? && height.positive?) ? [width, height] : nil
@@ -288,12 +314,30 @@ module FileAttachment
288 end 314 end
289 end 315 end
290 316
317 def detected_content_type(uploaded_file)
318 path = uploaded_file.try(:tempfile).try(:path) || uploaded_file.try(:path)
319 return nil if path.blank? || !File.exist?(path)
320
321 out, _err, status = Open3.capture3(MAGICK_ENV, "magick", "identify", "-format", "%m", "#{path}[0]")
322 return nil unless status.success?
323
324 MAGICK_FORMAT_CONTENT_TYPES[out.strip.upcase]
325 rescue StandardError
326 nil
327 end
291 328
292 def delete_upload_files 329 def delete_upload_files
293 dir = upload_root.join(id.to_s) 330 dir = upload_root.join(id.to_s)
294 FileUtils.rm_rf(dir) if Dir.exist?(dir) 331 FileUtils.rm_rf(dir) if Dir.exist?(dir)
295 end 332 end
296 333
334 def upload_must_be_readable_when_it_claims_to_be_an_image
335 return unless @upload_unreadable
336 return unless IMAGE_CONTENT_TYPES.include?(upload_content_type)
337
338 errors.add(:upload, :unreadable_image)
339 end
340
297 def file_path(style) 341 def file_path(style)
298 upload_root.join(id.to_s, style.to_s, variant_filename(style)).to_s 342 upload_root.join(id.to_s, style.to_s, variant_filename(style)).to_s
299 end 343 end