このコミットが含まれているのは:
@@ -1,6 +1,7 @@
|
||||
class MaterialsController < ApplicationController
|
||||
rescue_from MaterialZipExporter::EmptyExportError, with: :render_zip_empty
|
||||
rescue_from MaterialZipExporter::DuplicatePathError, with: :render_zip_duplicate_path
|
||||
rescue_from MaterialZipExporter::MissingFileError, with: :render_zip_missing_file
|
||||
|
||||
def index
|
||||
page = (params[:page].presence || 1).to_i
|
||||
@@ -203,6 +204,22 @@ class MaterialsController < ApplicationController
|
||||
render_unprocessable_entity("ZIP export path が重複してゐます: #{ error.message }")
|
||||
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
|
||||
return if file_sha256.blank?
|
||||
return unless material.file.attached?
|
||||
@@ -222,7 +239,8 @@ class MaterialsController < ApplicationController
|
||||
file_content_type: blob.content_type,
|
||||
file_byte_size: blob.byte_size,
|
||||
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
|
||||
|
||||
def render_material_import_block block
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
require 'digest'
|
||||
|
||||
class MaterialFileSha256
|
||||
def self.from_blob blob
|
||||
def self.from_blob blob, allow_download: false
|
||||
sha256 = blob.metadata['sha256']
|
||||
return sha256 if sha256.present?
|
||||
return nil unless allow_download
|
||||
|
||||
begin
|
||||
blob.open do |file|
|
||||
sha256 = Digest::SHA256.file(file.path).hexdigest
|
||||
blob.metadata['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
|
||||
|
||||
def self.from_upload upload
|
||||
|
||||
@@ -49,9 +49,6 @@ class MaterialVersionRecorder < VersionRecorder
|
||||
return @file_snapshot if @file_snapshot
|
||||
return empty_file_snapshot unless blob
|
||||
|
||||
blob.metadata['sha256'] ||= MaterialFileSha256.from_blob(blob)
|
||||
blob.save! if blob.changed?
|
||||
|
||||
{ file_blob_id: blob.id,
|
||||
file_filename: blob.filename.to_s,
|
||||
file_content_type: blob.content_type,
|
||||
|
||||
@@ -5,9 +5,19 @@ require 'zlib'
|
||||
# Keep this service boundary stable so job/cached export paths can replace it later.
|
||||
class MaterialZipExporter
|
||||
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 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
|
||||
@profile = profile.presence || 'legacy_drive'
|
||||
@@ -35,15 +45,22 @@ class MaterialZipExporter
|
||||
|
||||
rows = rows.where(materials: { tag_id: @tag_id }) if @tag_id
|
||||
|
||||
missing_files = []
|
||||
|
||||
entries = rows.filter_map do |item|
|
||||
material = item.material
|
||||
next unless material.file.attached?
|
||||
|
||||
data = download_blob(item, missing_files)
|
||||
next unless data
|
||||
|
||||
Entry.new(path: item.export_path,
|
||||
data: material.file.blob.download,
|
||||
data:,
|
||||
mtime: material.updated_at || Time.current)
|
||||
end
|
||||
|
||||
raise MissingFileError.new(missing_files) if missing_files.any?
|
||||
|
||||
paths = entries.map(&:path)
|
||||
duplicated = paths.find { |path| paths.count(path) > 1 }
|
||||
raise DuplicatePathError, duplicated if duplicated
|
||||
@@ -51,6 +68,20 @@ class MaterialZipExporter
|
||||
entries
|
||||
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
|
||||
VERSION_NEEDED = 20
|
||||
GP_FLAG = 0x0800
|
||||
|
||||
新しい課題から参照
ユーザをブロックする