このコミットが含まれているのは:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする