diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 1518877dc3..c39aa5fedf 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -70,10 +70,12 @@ module Admin block: "start" ).broadcast + blob = ReportBlob.create_for_upload_later!(report_filename) + ReportJob.perform_later( report_class:, user: spree_current_user, params:, format: report_format, - filename: report_filename, + blob:, channel: ScopedChannel.for_id(params[:uuid]), ) diff --git a/app/jobs/report_job.rb b/app/jobs/report_job.rb index 3c5aecab25..0367ddd259 100644 --- a/app/jobs/report_job.rb +++ b/app/jobs/report_job.rb @@ -9,12 +9,12 @@ class ReportJob < ApplicationJob NOTIFICATION_TIME = 5.seconds - def perform(report_class:, user:, params:, format:, filename:, channel: nil) + def perform(report_class:, user:, params:, format:, blob:, channel: nil) start_time = Time.zone.now report = report_class.new(user, params, render: true) result = report.render_as(format) - blob = ReportBlob.create!(filename, result) + blob.store(result) execution_time = Time.zone.now - start_time diff --git a/app/models/report_blob.rb b/app/models/report_blob.rb index b9d3927b68..17fda07005 100644 --- a/app/models/report_blob.rb +++ b/app/models/report_blob.rb @@ -5,7 +5,7 @@ class ReportBlob < ActiveStorage::Blob # AWS S3 limits URL expiry to one week. LIFETIME = 1.week - def self.create!(filename, content) + def self.create_locally!(filename, content) create_and_upload!( io: StringIO.new(content), filename:, @@ -15,11 +15,34 @@ class ReportBlob < ActiveStorage::Blob ) end + def self.create_for_upload_later!(filename) + # ActiveStorage discourages modifying a blob later but we need a blob + # before we know anything about the report file. It enables us to use the + # same blob in the controller to read the result. + create_before_direct_upload!( + filename:, + byte_size: 0, + checksum: "0", + content_type: content_type(filename), + service_name: :local, + ).tap do |blob| + ActiveStorage::PurgeJob.set(wait: LIFETIME).perform_later(blob) + end + end + def self.content_type(filename) MIME::Types.of(filename).first&.to_s || "application/octet-stream" end + def store(content) + io = StringIO.new(content) + upload(io, identify: false) + save! + end + def result + return if checksum == "0" + @result ||= download.force_encoding(Encoding::UTF_8) end diff --git a/spec/jobs/report_job_spec.rb b/spec/jobs/report_job_spec.rb index e1ce4a1a56..fd8c9aaf86 100644 --- a/spec/jobs/report_job_spec.rb +++ b/spec/jobs/report_job_spec.rb @@ -6,13 +6,14 @@ RSpec.describe ReportJob do include CableReady::Broadcaster let(:report_args) { - { report_class:, user:, params:, format:, filename: } + { report_class:, user:, params:, format:, blob: } } let(:report_class) { Reporting::Reports::UsersAndEnterprises::Base } let(:user) { enterprise.owner } let(:enterprise) { create(:enterprise) } let(:params) { {} } let(:format) { :csv } + let(:blob) { ReportBlob.create_for_upload_later!(filename) } let(:filename) { "report.csv" } it "generates a report" do @@ -25,11 +26,12 @@ RSpec.describe ReportJob do it "enqueues a job for async processing" do expect { ReportJob.perform_later(**report_args) - }.not_to change { ActiveStorage::Blob.count } + }.not_to change { blob.checksum } expect { perform_enqueued_jobs(only: ReportJob) - }.to change { ActiveStorage::Blob.count } + blob.reload + }.to change { blob.checksum } expect_csv_report end @@ -44,7 +46,8 @@ RSpec.describe ReportJob do expect { perform_enqueued_jobs(only: ReportJob) - }.to change { ActiveStorage::Blob.count } + blob.reload + }.to change { blob.checksum } end it "triggers an email when the report is done" do diff --git a/spec/mailers/report_mailer_spec.rb b/spec/mailers/report_mailer_spec.rb index b23932d3a3..92bf7032c1 100644 --- a/spec/mailers/report_mailer_spec.rb +++ b/spec/mailers/report_mailer_spec.rb @@ -10,7 +10,7 @@ RSpec.describe ReportMailer do blob:, ).report_ready } - let(:blob) { ReportBlob.create!("customers.csv", "report content") } + let(:blob) { ReportBlob.create_locally!("customers.csv", "report content") } it "notifies about a report" do expect(email.subject).to eq "Report ready" diff --git a/spec/models/report_blob_spec.rb b/spec/models/report_blob_spec.rb index 764925ba11..32cfd69a9d 100644 --- a/spec/models/report_blob_spec.rb +++ b/spec/models/report_blob_spec.rb @@ -7,8 +7,16 @@ RSpec.describe ReportBlob, type: :model do content = "This works. ✓" expect do - blob = ReportBlob.create!("customers.html", content) + blob = ReportBlob.create_locally!("customers.html", content) content = blob.result end.not_to change { content.encoding }.from(Encoding::UTF_8) end + + it "can be created first and filled later" do + blob = ReportBlob.create_for_upload_later!("customers.html") + + expect { blob.store("Hello") } + .to change { blob.checksum }.from("0") + .and change { blob.result }.from(nil).to("Hello") + end end