260 行
7.7 KiB
Ruby
260 行
7.7 KiB
Ruby
require 'digest'
|
|
|
|
class MaterialSyncImporter
|
|
Result = Struct.new(:material, :action, :suppressed, :suppression, keyword_init: true)
|
|
|
|
def self.import! attributes
|
|
new(attributes).import!
|
|
end
|
|
|
|
def initialize attributes
|
|
@attributes = attributes
|
|
end
|
|
|
|
def import!
|
|
source = source_attributes
|
|
key = normalized_source_key(source)
|
|
raise ArgumentError, 'normalized_source_key is required for material sync' if key.blank?
|
|
|
|
suppression = suppression_for(source)
|
|
if suppression
|
|
return Result.new(material: nil,
|
|
action: :suppressed,
|
|
suppressed: true,
|
|
suppression:)
|
|
end
|
|
|
|
material =
|
|
Material
|
|
.unscoped
|
|
.find_or_initialize_by(normalized_source_key: key)
|
|
if unchanged?(material, source)
|
|
return Result.new(material:,
|
|
action: :unchanged,
|
|
suppressed: false,
|
|
suppression: nil)
|
|
end
|
|
|
|
event_type =
|
|
if material.new_record?
|
|
:create
|
|
elsif material.discarded?
|
|
:restore
|
|
else
|
|
:update
|
|
end
|
|
uploaded_blob = uploaded_blob!
|
|
if @import_block
|
|
return Result.new(material: nil,
|
|
action: :suppressed,
|
|
suppressed: true,
|
|
suppression: @import_block)
|
|
end
|
|
|
|
Material.transaction do
|
|
if material.persisted?
|
|
MaterialVersionRecorder.ensure_snapshot!(material,
|
|
created_by_user: @attributes[:updated_by_user])
|
|
end
|
|
material.assign_attributes(material_attributes_for(material).merge(source))
|
|
material.discarded_at = nil if material.respond_to?(:discarded_at=)
|
|
material.file.attach(uploaded_blob) if uploaded_blob
|
|
material.save!
|
|
upsert_export_item!(material)
|
|
MaterialVersionRecorder.record!(material:,
|
|
event_type:,
|
|
created_by_user: @attributes[:updated_by_user])
|
|
end
|
|
MaterialThumbnailGenerator.generate!(material)
|
|
|
|
Result.new(material:,
|
|
action: event_type == :create ? :imported : :updated,
|
|
suppressed: false,
|
|
suppression: nil)
|
|
rescue StandardError
|
|
uploaded_blob&.purge_later
|
|
raise
|
|
ensure
|
|
close_tempfile!
|
|
end
|
|
|
|
private
|
|
|
|
def source_attributes
|
|
{ source_kind: @attributes[:source_kind],
|
|
source_uri: @attributes[:source_uri],
|
|
source_path: @attributes[:source_path],
|
|
source_file_id: @attributes[:source_file_id] }
|
|
end
|
|
|
|
def normalized_source_key source
|
|
MaterialSyncSuppression.normalize_source_key(source_kind: source[:source_kind],
|
|
source_uri: source[:source_uri],
|
|
source_path: source[:source_path],
|
|
source_file_id: source[:source_file_id])
|
|
end
|
|
|
|
def material_attributes_for material
|
|
attrs =
|
|
@attributes
|
|
.except(:source_kind, :source_uri, :source_path, :source_file_id,
|
|
:file_blob, :file_tempfile, :file_downloader,
|
|
:filename, :content_type,
|
|
:file_sha256, :export_path, :profile, :tag, :url)
|
|
|
|
if material.new_record?
|
|
attrs[:tag] = @attributes[:tag] if @attributes.key?(:tag)
|
|
attrs[:url] = @attributes[:url] if @attributes.key?(:url)
|
|
return attrs
|
|
end
|
|
|
|
if @attributes.key?(:tag) && @attributes[:tag].present?
|
|
attrs[:tag] = @attributes[:tag]
|
|
end
|
|
if @attributes.key?(:url) && @attributes[:url].present?
|
|
attrs[:url] = @attributes[:url]
|
|
end
|
|
|
|
attrs
|
|
end
|
|
|
|
def suppression_for source
|
|
if source[:source_kind] == 'google_drive_file' && source[:source_path].present?
|
|
return MaterialSyncSuppressionMatcher.match_google_drive_candidate(
|
|
drive_file_id: source[:source_file_id],
|
|
relative_path: source[:source_path])
|
|
end
|
|
|
|
MaterialSyncSuppressionMatcher.match(**source)
|
|
end
|
|
|
|
def uploaded_blob!
|
|
return @attributes[:file_blob] if @attributes[:file_blob]
|
|
|
|
tempfile = upload_tempfile
|
|
return nil unless tempfile
|
|
|
|
tempfile.rewind
|
|
file_sha256 = @attributes[:file_sha256] || Digest::SHA256.file(tempfile.path).hexdigest
|
|
@attributes[:file_sha256] = file_sha256
|
|
|
|
@import_block = MaterialImportBlockMatcher.match_for_sha256(file_sha256)
|
|
if @import_block
|
|
tempfile.rewind
|
|
return nil
|
|
end
|
|
|
|
blob = ActiveStorage::Blob.create_and_upload!(
|
|
io: tempfile,
|
|
filename: @attributes[:filename],
|
|
content_type: @attributes[:content_type])
|
|
MaterialFileSha256.assign_metadata_sha256!(blob, file_sha256)
|
|
tempfile.rewind
|
|
blob
|
|
end
|
|
|
|
def upload_tempfile
|
|
return @upload_tempfile if defined?(@upload_tempfile)
|
|
|
|
@upload_tempfile = @attributes[:file_tempfile]
|
|
if @upload_tempfile.blank? && @attributes[:file_downloader]
|
|
@upload_tempfile = @attributes[:file_downloader].call
|
|
end
|
|
@upload_tempfile
|
|
end
|
|
|
|
def upsert_export_item! material
|
|
export_path = resolved_export_path(material)
|
|
return if export_path.blank?
|
|
|
|
item = material.material_export_items.find_or_initialize_by(profile: effective_profile)
|
|
item.export_path = export_path
|
|
item.enabled = true
|
|
item.created_by_user ||= @attributes[:created_by_user]
|
|
item.save!
|
|
end
|
|
|
|
def export_path_taken_by_other? material, profile, export_path
|
|
MaterialExportItem
|
|
.where(profile:, export_path:)
|
|
.where.not(material_id: material.id)
|
|
.exists?
|
|
end
|
|
|
|
def close_tempfile!
|
|
tempfile = upload_tempfile
|
|
return unless tempfile
|
|
|
|
tempfile.close! unless tempfile.closed?
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
|
|
def unchanged? material, source
|
|
return false if material.new_record? || material.discarded?
|
|
return false unless same_source_attributes?(material, source)
|
|
return false unless same_export_path?(material)
|
|
return false unless same_material_attributes?(material)
|
|
|
|
same_file_snapshot?(material)
|
|
end
|
|
|
|
def same_source_attributes? material, source
|
|
material.source_kind == source[:source_kind] \
|
|
&& material.source_uri == source[:source_uri] \
|
|
&& material.source_path == source[:source_path] \
|
|
&& material.source_file_id == source[:source_file_id]
|
|
end
|
|
|
|
def same_export_path? material
|
|
expected = resolved_export_path(material)
|
|
current = material.material_export_items.find { |item| item.profile == effective_profile }
|
|
|
|
return current.blank? if expected.blank?
|
|
|
|
current&.enabled && current.export_path == expected
|
|
end
|
|
|
|
def same_material_attributes? material
|
|
same_tag_attribute?(material) && same_url_attribute?(material)
|
|
end
|
|
|
|
def same_file_snapshot? material
|
|
expected_sha256 = @attributes[:file_sha256].to_s.presence
|
|
|
|
if expected_sha256.present?
|
|
return false unless material.file.attached?
|
|
|
|
return MaterialFileSha256.metadata_sha256(material.file.blob) == expected_sha256
|
|
end
|
|
|
|
!material.file.attached?
|
|
end
|
|
|
|
def effective_profile
|
|
@attributes[:profile].presence || 'legacy_drive'
|
|
end
|
|
|
|
def resolved_export_path material
|
|
export_path = @attributes[:export_path].to_s.strip
|
|
return export_path if export_path.blank?
|
|
return export_path unless export_path_taken_by_other?(material, effective_profile, export_path)
|
|
|
|
MaterialSyncExportPath.uniquify(export_path, @attributes[:source_file_id])
|
|
end
|
|
|
|
def same_tag_attribute? material
|
|
return true unless @attributes.key?(:tag)
|
|
return true if @attributes[:tag].blank?
|
|
|
|
material.tag_id == @attributes[:tag].id
|
|
end
|
|
|
|
def same_url_attribute? material
|
|
return true unless @attributes.key?(:url)
|
|
return true if @attributes[:url].blank?
|
|
|
|
material.url == @attributes[:url]
|
|
end
|
|
end
|