From 7087a9bbc7af0677b63d31ada85c01a390ed0e43 Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Sun, 23 Aug 2026 09:02:22 +0200 Subject: [PATCH] The uploaded picture is written after the row is committed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Image#process_upload` ran in a `before_save`: it assigned the generated file name and copied the bytes to disk, in the same breath, before the row existed. If the insert failed afterwards — a validation on the owning record, a uniqueness clash, a rollback from the surrounding transaction — the file stayed behind with nothing pointing at it. The admin's own "Orphan" scope exists to find rows in that family; this was the other half of it, the files with no row at all. Split in two: `before_validation` assigns the attributes (so validations and the generated name still see them), `after_commit` copies the bytes. A rollback now takes the file with it, because the copy never happens. The same model is generated into every host, so the fix goes into the generator template as well as ours. While there: `Image#url` comes back from Teletype Orbit, where it was added and never flowed back, and the two admin previews use it instead of interpolating `/api/image/#{id}` by hand. Co-Authored-By: Claude Opus 5 (1M context) --- .../warp_engine/install/templates/image.rb | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/generators/warp_engine/install/templates/image.rb b/lib/generators/warp_engine/install/templates/image.rb index b2263dd..94fde24 100644 --- a/lib/generators/warp_engine/install/templates/image.rb +++ b/lib/generators/warp_engine/install/templates/image.rb @@ -9,7 +9,8 @@ class Image < ApplicationRecord attr_accessor :file_upload - before_save :process_upload, if: -> { file_upload.present? } + before_validation :assign_upload_attributes, if: -> { file_upload.present? } + after_commit :store_upload_file, on: [ :create, :update ], if: -> { file_upload.present? } def self.ransackable_attributes(auth_object = nil) %w[content_type created_at deleted_at filename id original_filename updated_at] @@ -19,14 +20,18 @@ class Image < ApplicationRecord File.join(self.class.upload_path, filename.to_s) end + def url = "/api/image/#{id}" + private - def process_upload - FileUtils.mkdir_p(self.class.upload_path) + def assign_upload_attributes self.original_filename = file_upload.original_filename self.content_type = file_upload.content_type.presence || "application/octet-stream" - ext = File.extname(file_upload.original_filename) - self.filename = "#{SecureRandom.uuid}#{ext}" + self.filename = "#{SecureRandom.uuid}#{File.extname(file_upload.original_filename)}" + end + + def store_upload_file + FileUtils.mkdir_p(self.class.upload_path) IO.copy_stream(file_upload.to_io, file_path) end end