From a4bec6e407eeed85502eb4ac9c002e9208d20146 Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Tue, 3 Feb 2026 13:51:22 +0100 Subject: [PATCH 1/3] Ignore missing attachments --- .../action_text_rich_text_record_set.rb | 23 ++++++++++++------- test/models/account/export_test.rb | 17 ++++++++++++++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/app/models/account/data_transfer/action_text_rich_text_record_set.rb b/app/models/account/data_transfer/action_text_rich_text_record_set.rb index 44c6b7bd8..0ce2944af 100644 --- a/app/models/account/data_transfer/action_text_rich_text_record_set.rb +++ b/app/models/account/data_transfer/action_text_rich_text_record_set.rb @@ -57,11 +57,17 @@ class Account::DataTransfer::ActionTextRichTextRecordSet < Account::DataTransfer content.send(:attachment_nodes).each do |node| sgid = SignedGlobalID.parse(node["sgid"], for: ActionText::Attachable::LOCATOR_NAME) - record = sgid&.find - next if record&.account_id != account.id - node["gid"] = record.to_global_id.to_s - node.remove_attribute("sgid") + record = begin + sgid&.find + rescue ActiveRecord::RecordNotFound + nil + end + + if record&.account_id == account.id + node["gid"] = record.to_global_id.to_s + node.remove_attribute("sgid") + end end content.fragment.source.to_html @@ -74,11 +80,12 @@ class Account::DataTransfer::ActionTextRichTextRecordSet < Account::DataTransfer fragment.css("action-text-attachment[gid]").each do |node| gid = GlobalID.parse(node["gid"]) - next unless gid - record = gid.find - node["sgid"] = record.attachable_sgid - node.remove_attribute("gid") + if gid + record = gid.find + node["sgid"] = record.attachable_sgid + node.remove_attribute("gid") + end end fragment.to_html diff --git a/test/models/account/export_test.rb b/test/models/account/export_test.rb index 68f95c5a0..d449764b8 100644 --- a/test/models/account/export_test.rb +++ b/test/models/account/export_test.rb @@ -59,4 +59,21 @@ class Account::ExportTest < ActiveSupport::TestCase assert entry, "Expected blob file in zip" end end + + test "build succeeds when rich text references missing blob" do + blob = ActiveStorage::Blob.create_and_upload!( + io: file_fixture("moon.jpg").open, + filename: "moon.jpg", + content_type: "image/jpeg" + ) + card = cards(:logo) + card.update!(description: "") + ActiveStorage::Blob.where(id: blob.id).delete_all + + export = Account::Export.create!(account: Current.account, user: users(:david)) + + export.build + + assert export.completed? + end end From ffce51a42fcb56cf74fe532aabb9000d1b9a104a Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Tue, 3 Feb 2026 14:04:54 +0100 Subject: [PATCH 2/3] Ignore faulty ZIP files --- app/jobs/account/data_import_job.rb | 2 +- app/models/zip_file.rb | 8 ++++++++ app/models/zip_file/reader.rb | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/jobs/account/data_import_job.rb b/app/jobs/account/data_import_job.rb index ec2877bd5..67862d4c7 100644 --- a/app/jobs/account/data_import_job.rb +++ b/app/jobs/account/data_import_job.rb @@ -2,7 +2,7 @@ class Account::DataImportJob < ApplicationJob include ActiveJob::Continuable queue_as :backend - discard_on Account::DataTransfer::RecordSet::IntegrityError + discard_on Account::DataTransfer::RecordSet::IntegrityError, ZipFile::InvalidFileError def perform(import) step :check do |step| diff --git a/app/models/zip_file.rb b/app/models/zip_file.rb index bdacc441d..dc5b7ee8d 100644 --- a/app/models/zip_file.rb +++ b/app/models/zip_file.rb @@ -1,4 +1,6 @@ class ZipFile + class InvalidFileError < StandardError; end + class << self def create_for(attachment, filename:) raise ArgumentError, "No block given" unless block_given? @@ -57,6 +59,12 @@ class ZipFile blob.update!(byte_size: writer.byte_size, checksum: writer.checksum) attachment.attach(blob) + rescue Aws::S3::MultipartUploadError => e + if e.errors.any? + raise e.errors.first + else + raise e + end end def create_for_disk(attachment, filename:) diff --git a/app/models/zip_file/reader.rb b/app/models/zip_file/reader.rb index 1b9cd3dcb..2dc563f96 100644 --- a/app/models/zip_file/reader.rb +++ b/app/models/zip_file/reader.rb @@ -2,6 +2,8 @@ class ZipFile::Reader def initialize(io) @io = io @reader = ZipKit::FileReader.read_zip_structure(io: io) + rescue ZipKit::FileReader::InvalidStructure => e + raise ZipFile::InvalidFileError, e.message end def read(file_path) From 1c1a73c57ce45c0302e3c5a10be641ef6a124fe2 Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Tue, 3 Feb 2026 15:01:10 +0100 Subject: [PATCH 3/3] Check if the file exists before adding it ZipKit creates a placeholder record when add_file is called, this record is then filled with the content that's passxed to write. But if the blob's file doesn't exist then an error is rased and the placeholder is never filled. This results in a ZIP that has a place in its catalog for the blob, but no contant for it, which makes it invalid. --- app/models/account/data_transfer/blob_file_record_set.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/account/data_transfer/blob_file_record_set.rb b/app/models/account/data_transfer/blob_file_record_set.rb index a8927dd13..4bbfcba10 100644 --- a/app/models/account/data_transfer/blob_file_record_set.rb +++ b/app/models/account/data_transfer/blob_file_record_set.rb @@ -9,11 +9,11 @@ class Account::DataTransfer::BlobFileRecordSet < Account::DataTransfer::RecordSe end def export_record(blob) - zip.add_file("storage/#{blob.key}", compress: false) do |out| - blob.download { |chunk| out.write(chunk) } + if blob.service.exist?(blob.key) + zip.add_file("storage/#{blob.key}", compress: false) do |out| + blob.download { |chunk| out.write(chunk) } + end end - rescue ActiveStorage::FileNotFoundError - # Skip blobs where the file is missing from storage end def files