このコミットが含まれているのは:
2026-06-24 00:38:29 +09:00
コミット 510cbb0d78
4個のファイルの変更66行の追加11行の削除
+19 -1
ファイルの表示
@@ -1,6 +1,7 @@
class MaterialsController < ApplicationController class MaterialsController < ApplicationController
rescue_from MaterialZipExporter::EmptyExportError, with: :render_zip_empty rescue_from MaterialZipExporter::EmptyExportError, with: :render_zip_empty
rescue_from MaterialZipExporter::DuplicatePathError, with: :render_zip_duplicate_path rescue_from MaterialZipExporter::DuplicatePathError, with: :render_zip_duplicate_path
rescue_from MaterialZipExporter::MissingFileError, with: :render_zip_missing_file
def index def index
page = (params[:page].presence || 1).to_i page = (params[:page].presence || 1).to_i
@@ -203,6 +204,22 @@ class MaterialsController < ApplicationController
render_unprocessable_entity("ZIP export path が重複してゐます: #{ error.message }") render_unprocessable_entity("ZIP export path が重複してゐます: #{ error.message }")
end end
def render_zip_missing_file error
missing_files = error.missing_files.map do |missing_file|
{ material_id: missing_file.material_id,
export_path: missing_file.export_path,
blob_id: missing_file.blob_id,
filename: missing_file.filename }
end
render json: { type: 'validation_error',
message: 'ZIP export に必要な素材ファイルが欠損しています.',
errors: { },
base_errors: ['ZIP export に必要な素材ファイルが欠損しています.'],
missing_files: },
status: :unprocessable_entity
end
def apply_file_sha256! material, file_sha256 def apply_file_sha256! material, file_sha256
return if file_sha256.blank? return if file_sha256.blank?
return unless material.file.attached? return unless material.file.attached?
@@ -222,7 +239,8 @@ class MaterialsController < ApplicationController
file_content_type: blob.content_type, file_content_type: blob.content_type,
file_byte_size: blob.byte_size, file_byte_size: blob.byte_size,
file_checksum: blob.checksum, file_checksum: blob.checksum,
file_sha256: blob.metadata['sha256'] || MaterialFileSha256.from_blob(blob) } file_sha256: blob.metadata['sha256'] ||
MaterialFileSha256.from_blob(blob, allow_download: true) }
end end
def render_material_import_block block def render_material_import_block block
+15 -6
ファイルの表示
@@ -1,15 +1,24 @@
require 'digest' require 'digest'
class MaterialFileSha256 class MaterialFileSha256
def self.from_blob blob def self.from_blob blob, allow_download: false
sha256 = blob.metadata['sha256'] sha256 = blob.metadata['sha256']
return sha256 if sha256.present? return sha256 if sha256.present?
return nil unless allow_download
blob.open do |file| begin
sha256 = Digest::SHA256.file(file.path).hexdigest blob.open do |file|
blob.metadata['sha256'] = sha256 sha256 = Digest::SHA256.file(file.path).hexdigest
blob.save! if blob.changed? blob.metadata['sha256'] = sha256
sha256 blob.save! if blob.changed?
sha256
end
rescue ActiveStorage::FileNotFoundError, ArgumentError => error
Rails.logger.warn(
"MaterialFileSha256.from_blob failed for blob_id=#{blob.id}: " \
"#{error.class}: #{error.message}",
)
nil
end end
end end
-3
ファイルの表示
@@ -49,9 +49,6 @@ class MaterialVersionRecorder < VersionRecorder
return @file_snapshot if @file_snapshot return @file_snapshot if @file_snapshot
return empty_file_snapshot unless blob return empty_file_snapshot unless blob
blob.metadata['sha256'] ||= MaterialFileSha256.from_blob(blob)
blob.save! if blob.changed?
{ file_blob_id: blob.id, { file_blob_id: blob.id,
file_filename: blob.filename.to_s, file_filename: blob.filename.to_s,
file_content_type: blob.content_type, file_content_type: blob.content_type,
+32 -1
ファイルの表示
@@ -5,9 +5,19 @@ require 'zlib'
# Keep this service boundary stable so job/cached export paths can replace it later. # Keep this service boundary stable so job/cached export paths can replace it later.
class MaterialZipExporter class MaterialZipExporter
Entry = Struct.new(:path, :data, :mtime, keyword_init: true) Entry = Struct.new(:path, :data, :mtime, keyword_init: true)
MissingFile = Struct.new(:material_id, :export_path, :blob_id, :filename, keyword_init: true)
class EmptyExportError < StandardError; end class EmptyExportError < StandardError; end
class DuplicatePathError < StandardError; end class DuplicatePathError < StandardError; end
class MissingFileError < StandardError
attr_reader :missing_files
def initialize missing_files
@missing_files = missing_files
super("Missing files: #{missing_files.map(&:export_path).join(', ')}")
end
end
def initialize profile: 'legacy_drive', tag_id: nil def initialize profile: 'legacy_drive', tag_id: nil
@profile = profile.presence || 'legacy_drive' @profile = profile.presence || 'legacy_drive'
@@ -35,15 +45,22 @@ class MaterialZipExporter
rows = rows.where(materials: { tag_id: @tag_id }) if @tag_id rows = rows.where(materials: { tag_id: @tag_id }) if @tag_id
missing_files = []
entries = rows.filter_map do |item| entries = rows.filter_map do |item|
material = item.material material = item.material
next unless material.file.attached? next unless material.file.attached?
data = download_blob(item, missing_files)
next unless data
Entry.new(path: item.export_path, Entry.new(path: item.export_path,
data: material.file.blob.download, data:,
mtime: material.updated_at || Time.current) mtime: material.updated_at || Time.current)
end end
raise MissingFileError.new(missing_files) if missing_files.any?
paths = entries.map(&:path) paths = entries.map(&:path)
duplicated = paths.find { |path| paths.count(path) > 1 } duplicated = paths.find { |path| paths.count(path) > 1 }
raise DuplicatePathError, duplicated if duplicated raise DuplicatePathError, duplicated if duplicated
@@ -51,6 +68,20 @@ class MaterialZipExporter
entries entries
end end
def download_blob item, missing_files
blob = item.material.file.blob
blob.download
rescue ActiveStorage::FileNotFoundError
missing_files << MissingFile.new(
material_id: item.material_id,
export_path: item.export_path,
blob_id: blob.id,
filename: blob.filename.to_s,
)
nil
end
class ZipWriter class ZipWriter
VERSION_NEEDED = 20 VERSION_NEEDED = 20
GP_FLAG = 0x0800 GP_FLAG = 0x0800