e51f0bfee7
Account incineration could leave orphaned records due to missing cascade declarations and async jobs failing when the account was already gone. Cascade fixes: - Add Account::Searchable concern to clean up Search::Query (delete_all) and Search::Record (destroy_all, respects SQLite FTS dependent: :destroy) - Add before_destroy in Account::Storage to delete storage entries - Suppress storage entry recording during incineration so attachment purge callbacks don't create entries or enqueue materialize jobs - Guard Access#clean_inaccessible_data_later with unless user.destroyed? to avoid enqueuing pointless jobs during user cascade Job tenancy: - Extract AccountTenanted concern from the global ActiveJob initializer into ApplicationJob (include) with targeted prepends for ActionMailer::MailDeliveryJob and Turbo broadcast jobs - Defer account resolution from deserialize to perform so that missing accounts raise DeserializationError inside the execution path where discard_on can handle it Tests: - Comprehensive incineration test covering 35 model types with before/after assertions and full enqueued job processing - Mailer deliver_later test verifying account context survives job serialization for multi-account users - Turbo broadcast test verifying account-scoped URLs in rendered partials
100 lines
2.8 KiB
Ruby
100 lines
2.8 KiB
Ruby
class Account::Import < ApplicationRecord
|
|
broadcasts_refreshes
|
|
|
|
belongs_to :account
|
|
belongs_to :identity
|
|
|
|
has_one_attached :file, dependent: :purge_later
|
|
|
|
enum :status, %w[ pending processing completed failed ].index_by(&:itself), default: :pending
|
|
enum :failure_reason, %w[ conflict invalid_export ].index_by(&:itself), prefix: :failed_due_to, scopes: false
|
|
|
|
scope :expired, -> { where(completed_at: ...24.hours.ago).or(where(status: :failed, created_at: ...7.days.ago)) }
|
|
|
|
def self.cleanup
|
|
expired.each(&:cleanup)
|
|
end
|
|
|
|
def process_later
|
|
Account::DataImportJob.perform_later(self)
|
|
end
|
|
|
|
def check(start: nil, callback: nil)
|
|
processing!
|
|
|
|
ZipFile.read_from(file.blob) do |zip|
|
|
Account::DataTransfer::Manifest.new(account).each_record_set(start: start) do |record_set, last_id|
|
|
record_set.check(from: zip, start: last_id, callback: callback)
|
|
end
|
|
end
|
|
rescue Account::DataTransfer::RecordSet::ConflictError => e
|
|
mark_as_failed(:conflict)
|
|
raise e
|
|
rescue Account::DataTransfer::RecordSet::IntegrityError, ZipFile::InvalidFileError => e
|
|
mark_as_failed(:invalid_export)
|
|
raise e
|
|
rescue => e
|
|
mark_as_failed
|
|
raise e
|
|
end
|
|
|
|
def process(start: nil, callback: nil)
|
|
processing!
|
|
|
|
ZipFile.read_from(file.blob) do |zip|
|
|
Account::DataTransfer::Manifest.new(account).each_record_set(start: start) do |record_set, last_id|
|
|
record_set.import(from: zip, start: last_id, callback: callback)
|
|
end
|
|
end
|
|
|
|
add_importer_to_all_access_boards
|
|
reconcile_cards_count
|
|
reconcile_account_storage
|
|
|
|
mark_completed
|
|
rescue Account::DataTransfer::RecordSet::ConflictError => e
|
|
mark_as_failed(:conflict)
|
|
raise e
|
|
rescue Account::DataTransfer::RecordSet::IntegrityError, ZipFile::InvalidFileError => e
|
|
mark_as_failed(:invalid_export)
|
|
raise e
|
|
rescue => e
|
|
mark_as_failed
|
|
raise e
|
|
end
|
|
|
|
def cleanup
|
|
destroy
|
|
account.destroy if failed?
|
|
end
|
|
|
|
private
|
|
def mark_completed
|
|
update!(status: :completed, completed_at: Time.current)
|
|
ImportMailer.completed(identity, account).deliver_later
|
|
end
|
|
|
|
def mark_as_failed(failure_reason = nil)
|
|
update!(status: :failed, failure_reason: failure_reason)
|
|
ImportMailer.failed(self).deliver_later
|
|
end
|
|
|
|
def reconcile_cards_count
|
|
account.update_column :cards_count, [ account.cards_count, account.cards.maximum(:number).to_i ].max
|
|
end
|
|
|
|
def add_importer_to_all_access_boards
|
|
importer = account.users.find_by!(identity: identity)
|
|
|
|
account.boards.all_access.find_each do |board|
|
|
board.accesses.grant_to(importer)
|
|
end
|
|
end
|
|
|
|
def reconcile_account_storage
|
|
account.boards.each(&:reconcile_storage)
|
|
account.reconcile_storage
|
|
account.materialize_storage
|
|
end
|
|
end
|