From b4a7b4bfbdc7293562708b21a6ffdda56d1b5e4f Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Mon, 29 Apr 2024 16:53:21 +0200 Subject: [PATCH] create a job for all image treatments --- app/jobs/image_processor_job.rb | 59 +++++++++++++++++++ app/jobs/titre_identite_watermark_job.rb | 28 --------- .../attachment_auto_rotate_concern.rb | 22 ------- .../attachment_image_processor_concern.rb | 21 +++++++ ...ern.rb => blob_image_processor_concern.rb} | 8 +-- app/services/auto_rotate_service.rb | 6 +- config/initializers/active_storage.rb | 5 +- ...ob_spec.rb => image_processor_job_spec.rb} | 2 +- 8 files changed, 91 insertions(+), 60 deletions(-) create mode 100644 app/jobs/image_processor_job.rb delete mode 100644 app/jobs/titre_identite_watermark_job.rb delete mode 100644 app/models/concerns/attachment_auto_rotate_concern.rb create mode 100644 app/models/concerns/attachment_image_processor_concern.rb rename app/models/concerns/{blob_titre_identite_watermark_concern.rb => blob_image_processor_concern.rb} (58%) rename spec/jobs/{titre_identite_watermark_job_spec.rb => image_processor_job_spec.rb} (97%) diff --git a/app/jobs/image_processor_job.rb b/app/jobs/image_processor_job.rb new file mode 100644 index 000000000..6fa027e93 --- /dev/null +++ b/app/jobs/image_processor_job.rb @@ -0,0 +1,59 @@ +class ImageProcessorJob < ApplicationJob + class FileNotScannedYetError < StandardError + end + + # If by the time the job runs the blob has been deleted, ignore the error + discard_on ActiveRecord::RecordNotFound + # If the file is deleted during the scan, ignore the error + discard_on ActiveStorage::FileNotFoundError + # If the file is not analyzed or scanned for viruses yet, retry later + # (to avoid modifying the file while it is being scanned). + retry_on FileNotScannedYetError, wait: :exponentially_longer, attempts: 10 + + def perform(blob) + return if blob.nil? + raise FileNotScannedYetError if blob.virus_scanner.pending? + return if ActiveStorage::Attachment.find_by(blob_id: blob.id).record_type == "ActiveStorage::VariantRecord" + + auto_rotate(blob) if ["image/jpeg", "image/jpg"].include?(blob.content_type) + create_variants(blob) if blob.variant_required? + add_watermark(blob) if blob.watermark_pending? + end + + private + + def auto_rotate(blob) + blob.open do |file| + Tempfile.create(["rotated", File.extname(file)]) do |output| + processed = AutoRotateService.new.process(file, output) + return if processed.blank? + + blob.upload(processed) # also update checksum & byte_size accordingly + blob.save! + end + end + end + + def create_variants(blob) + blob.attachments.each do |attachment| + next unless attachment&.representable? + attachment.representation(resize_to_limit: [300, 300]).processed + attachment.representation(resize_to_limit: [400, 400]).processed + end + end + + def add_watermark(blob) + return if blob.watermark_done? + + blob.open do |file| + Tempfile.create(["watermarked", File.extname(file)]) do |output| + processed = WatermarkService.new.process(file, output) + return if processed.blank? + + blob.upload(processed) # also update checksum & byte_size accordingly + blob.watermarked_at = Time.current + blob.save! + end + end + end +end diff --git a/app/jobs/titre_identite_watermark_job.rb b/app/jobs/titre_identite_watermark_job.rb deleted file mode 100644 index 1b261e291..000000000 --- a/app/jobs/titre_identite_watermark_job.rb +++ /dev/null @@ -1,28 +0,0 @@ -class TitreIdentiteWatermarkJob < ApplicationJob - class FileNotScannedYetError < StandardError - end - - # If by the time the job runs the blob has been deleted, ignore the error - discard_on ActiveRecord::RecordNotFound - # If the file is deleted during the scan, ignore the error - discard_on ActiveStorage::FileNotFoundError - # If the file is not analyzed or scanned for viruses yet, retry later - # (to avoid modifying the file while it is being scanned). - retry_on FileNotScannedYetError, wait: :exponentially_longer, attempts: 10 - - def perform(blob) - return if blob.watermark_done? - raise FileNotScannedYetError if blob.virus_scanner.pending? - - blob.open do |file| - Tempfile.create(["watermarked", File.extname(file)]) do |output| - processed = WatermarkService.new.process(file, output) - return if processed.blank? - - blob.upload(processed) # also update checksum & byte_size accordingly - blob.watermarked_at = Time.current - blob.save! - end - end - end -end diff --git a/app/models/concerns/attachment_auto_rotate_concern.rb b/app/models/concerns/attachment_auto_rotate_concern.rb deleted file mode 100644 index 9fba8a9ab..000000000 --- a/app/models/concerns/attachment_auto_rotate_concern.rb +++ /dev/null @@ -1,22 +0,0 @@ -module AttachmentAutoRotateConcern - extend ActiveSupport::Concern - - included do - after_create_commit :auto_rotate - end - - private - - def auto_rotate - return if blob.nil? - return if ["image/jpeg", "image/jpg"].exclude?(blob.content_type) - - blob.open do |file| - Tempfile.create(["rotated", File.extname(file)]) do |output| - processed = AutoRotateService.new.process(file, output) - blob.upload(processed) # also update checksum & byte_size accordingly - blob.save! - end - end - end -end diff --git a/app/models/concerns/attachment_image_processor_concern.rb b/app/models/concerns/attachment_image_processor_concern.rb new file mode 100644 index 000000000..d1c44609c --- /dev/null +++ b/app/models/concerns/attachment_image_processor_concern.rb @@ -0,0 +1,21 @@ +# Run a virus scan on all attachments after they are analyzed. +# +# We're using a class extension to ensure that all attachments get scanned, +# regardless on how they were created. This could be an ActiveStorage::Analyzer, +# but as of Rails 6.1 only the first matching analyzer is ever run on +# a blob (and we may want to analyze the dimension of a picture as well +# as scanning it). +module AttachmentImageProcessorConcern + extend ActiveSupport::Concern + + included do + after_create_commit :process_image + end + + private + + def process_image + return if blob.nil? + ImageProcessorJob.perform_later(blob) + end +end diff --git a/app/models/concerns/blob_titre_identite_watermark_concern.rb b/app/models/concerns/blob_image_processor_concern.rb similarity index 58% rename from app/models/concerns/blob_titre_identite_watermark_concern.rb rename to app/models/concerns/blob_image_processor_concern.rb index 152c512f9..46b56b955 100644 --- a/app/models/concerns/blob_titre_identite_watermark_concern.rb +++ b/app/models/concerns/blob_image_processor_concern.rb @@ -1,4 +1,4 @@ -module BlobTitreIdentiteWatermarkConcern +module BlobImageProcessorConcern def watermark_pending? watermark_required? && !watermark_done? end @@ -7,10 +7,8 @@ module BlobTitreIdentiteWatermarkConcern watermarked_at.present? end - def watermark_later - if watermark_pending? - TitreIdentiteWatermarkJob.perform_later(self) - end + def variant_required? + attachments.any? { _1.record.class == Champs::TitreIdentiteChamp || _1.record.class == Champs::PieceJustificativeChamp } end private diff --git a/app/services/auto_rotate_service.rb b/app/services/auto_rotate_service.rb index 39813c8c5..9cec29a95 100644 --- a/app/services/auto_rotate_service.rb +++ b/app/services/auto_rotate_service.rb @@ -1,7 +1,6 @@ class AutoRotateService def process(file, output) auto_rotate_image(file, output) - output end private @@ -9,6 +8,8 @@ class AutoRotateService def auto_rotate_image(file, output) image = MiniMagick::Image.new(file.to_path) + return nil if !image.valid? + case image["%[orientation]"] when 'LeftBottom' rotate_image(file, output, 90) @@ -16,6 +17,8 @@ class AutoRotateService rotate_image(file, output, 180) when 'RightTop' rotate_image(file, output, 270) + else + nil end end @@ -26,5 +29,6 @@ class AutoRotateService convert.auto_orient convert << output.to_path end + output end end diff --git a/config/initializers/active_storage.rb b/config/initializers/active_storage.rb index c20df224f..5598525ad 100644 --- a/config/initializers/active_storage.rb +++ b/config/initializers/active_storage.rb @@ -4,7 +4,7 @@ Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer ActiveSupport.on_load(:active_storage_blob) do - include BlobTitreIdentiteWatermarkConcern + include BlobImageProcessorConcern include BlobVirusScannerConcern include BlobSignedIdConcern @@ -15,9 +15,8 @@ ActiveSupport.on_load(:active_storage_blob) do end ActiveSupport.on_load(:active_storage_attachment) do - include AttachmentTitreIdentiteWatermarkConcern + include AttachmentImageProcessorConcern include AttachmentVirusScannerConcern - include AttachmentAutoRotateConcern end Rails.application.reloader.to_prepare do diff --git a/spec/jobs/titre_identite_watermark_job_spec.rb b/spec/jobs/image_processor_job_spec.rb similarity index 97% rename from spec/jobs/titre_identite_watermark_job_spec.rb rename to spec/jobs/image_processor_job_spec.rb index ebb9497cc..e337a825f 100644 --- a/spec/jobs/titre_identite_watermark_job_spec.rb +++ b/spec/jobs/image_processor_job_spec.rb @@ -1,4 +1,4 @@ -describe TitreIdentiteWatermarkJob, type: :job do +describe ImageProcessorJob, type: :job do let(:blob) do ActiveStorage::Blob.create_and_upload!(io: StringIO.new("toto"), filename: "toto.png") end