コミットを比較
2 コミット
a820ce4c3e
...
c2102c8f96
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
| c2102c8f96 | |||
| 510cbb0d78 |
@@ -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
|
||||||
@@ -53,21 +54,27 @@ class MaterialsController < ApplicationController
|
|||||||
block = MaterialImportBlockMatcher.match_for_sha256(file_sha256)
|
block = MaterialImportBlockMatcher.match_for_sha256(file_sha256)
|
||||||
return render_material_import_block(block) if block
|
return render_material_import_block(block) if block
|
||||||
|
|
||||||
|
uploaded_blob = build_uploaded_material_blob!(file, file_sha256)
|
||||||
material = nil
|
material = nil
|
||||||
Material.transaction do
|
|
||||||
tag_name = TagName.find_undiscard_or_create_by!(name: tag_name_raw)
|
|
||||||
tag = tag_name.tag
|
|
||||||
tag = Tag.create!(tag_name:, category: :material) unless tag
|
|
||||||
|
|
||||||
material = Material.new(tag:, url:,
|
begin
|
||||||
created_by_user: current_user,
|
Material.transaction do
|
||||||
updated_by_user: current_user)
|
tag_name = TagName.find_undiscard_or_create_by!(name: tag_name_raw)
|
||||||
material.file.attach(file) if file
|
tag = tag_name.tag
|
||||||
apply_file_sha256!(material, file_sha256)
|
tag = Tag.create!(tag_name:, category: :material) unless tag
|
||||||
material.save!
|
|
||||||
upsert_export_paths!(material)
|
material = Material.new(tag:, url:,
|
||||||
MaterialVersionRecorder.record!(material:, event_type: :create,
|
created_by_user: current_user,
|
||||||
created_by_user: current_user)
|
updated_by_user: current_user)
|
||||||
|
material.file.attach(uploaded_blob) if uploaded_blob
|
||||||
|
material.save!
|
||||||
|
upsert_export_paths!(material)
|
||||||
|
MaterialVersionRecorder.record!(material:, event_type: :create,
|
||||||
|
created_by_user: current_user)
|
||||||
|
end
|
||||||
|
rescue StandardError
|
||||||
|
uploaded_blob&.purge_later
|
||||||
|
raise
|
||||||
end
|
end
|
||||||
|
|
||||||
if material
|
if material
|
||||||
@@ -96,19 +103,30 @@ class MaterialsController < ApplicationController
|
|||||||
block = MaterialImportBlockMatcher.match_for_sha256(file_sha256)
|
block = MaterialImportBlockMatcher.match_for_sha256(file_sha256)
|
||||||
return render_material_import_block(block) if block
|
return render_material_import_block(block) if block
|
||||||
|
|
||||||
Material.transaction do
|
uploaded_blob = build_uploaded_material_blob!(file, file_sha256)
|
||||||
MaterialVersionRecorder.ensure_snapshot!(material, created_by_user: current_user)
|
|
||||||
tag_name = TagName.find_undiscard_or_create_by!(name: tag_name_raw)
|
|
||||||
tag = tag_name.tag
|
|
||||||
tag = Tag.create!(tag_name:, category: :material) unless tag
|
|
||||||
|
|
||||||
material.update!(tag:, url:, updated_by_user: current_user)
|
begin
|
||||||
material.file.attach(file) if file
|
Material.transaction do
|
||||||
apply_file_sha256!(material, file_sha256)
|
MaterialVersionRecorder.ensure_snapshot!(material, created_by_user: current_user)
|
||||||
material.file.detach if params.key?(:url) && url.present? && file.blank?
|
tag_name = TagName.find_undiscard_or_create_by!(name: tag_name_raw)
|
||||||
upsert_export_paths!(material)
|
tag = tag_name.tag
|
||||||
MaterialVersionRecorder.record!(material:, event_type: :update,
|
tag = Tag.create!(tag_name:, category: :material) unless tag
|
||||||
created_by_user: current_user)
|
|
||||||
|
material.assign_attributes(tag:, url:, updated_by_user: current_user)
|
||||||
|
if uploaded_blob
|
||||||
|
material.file.attach(uploaded_blob)
|
||||||
|
clear_file_suppression!(material)
|
||||||
|
elsif params.key?(:url) && url.present? && file.blank?
|
||||||
|
material.file.detach
|
||||||
|
end
|
||||||
|
material.save!
|
||||||
|
upsert_export_paths!(material)
|
||||||
|
MaterialVersionRecorder.record!(material:, event_type: :update,
|
||||||
|
created_by_user: current_user)
|
||||||
|
end
|
||||||
|
rescue StandardError
|
||||||
|
uploaded_blob&.purge_later
|
||||||
|
raise
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: MaterialRepr.base(material, host: request.base_url)
|
render json: MaterialRepr.base(material, host: request.base_url)
|
||||||
@@ -203,13 +221,47 @@ class MaterialsController < ApplicationController
|
|||||||
render_unprocessable_entity("ZIP export path が重複してゐます: #{ error.message }")
|
render_unprocessable_entity("ZIP export path が重複してゐます: #{ error.message }")
|
||||||
end
|
end
|
||||||
|
|
||||||
def apply_file_sha256! material, file_sha256
|
def render_zip_missing_file error
|
||||||
return if file_sha256.blank?
|
missing_files = error.missing_files.map do |missing_file|
|
||||||
return unless material.file.attached?
|
{ material_id: missing_file.material_id,
|
||||||
|
export_path: missing_file.export_path,
|
||||||
|
blob_id: missing_file.blob_id,
|
||||||
|
filename: missing_file.filename }
|
||||||
|
end
|
||||||
|
|
||||||
blob = material.file.blob
|
render json: { type: 'validation_error',
|
||||||
blob.metadata['sha256'] = file_sha256
|
message: 'ZIP export に必要な素材ファイルが欠損しています.',
|
||||||
blob.save! if blob.changed?
|
errors: { },
|
||||||
|
base_errors: ['ZIP export に必要な素材ファイルが欠損しています.'],
|
||||||
|
missing_files: },
|
||||||
|
status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_uploaded_material_blob! file, file_sha256
|
||||||
|
return nil unless file
|
||||||
|
|
||||||
|
file.tempfile.rewind
|
||||||
|
|
||||||
|
blob = ActiveStorage::Blob.create_and_upload!(
|
||||||
|
io: file.tempfile,
|
||||||
|
filename: file.original_filename,
|
||||||
|
content_type: file.content_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
if file_sha256.present?
|
||||||
|
blob.metadata['sha256'] = file_sha256
|
||||||
|
blob.save! if blob.changed?
|
||||||
|
end
|
||||||
|
|
||||||
|
blob
|
||||||
|
ensure
|
||||||
|
file.tempfile.rewind if file&.tempfile
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_file_suppression! material
|
||||||
|
material.file_suppressed_at = nil
|
||||||
|
material.file_suppressed_by_user = nil
|
||||||
|
material.file_suppression_reason = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def purge_material_file_snapshot material
|
def purge_material_file_snapshot material
|
||||||
@@ -222,7 +274,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
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする