アップロードされたファイルの特徴をどうやって記録するか
アップロードされたファイルの特徴で検索したり分類したりしたくなったりということはないだろうか?私の場合は、写真の撮影日や PDF の作成日で並べ替えたり、抽出したりしたかった。もちろん、そのためにはその日時を ActiveRecord(ActiveModel)の属性値としなければならない。その方法として新たに関連モデルを作成するのも一つだろう。あるいは(禁じ手の匂いがするが) ActiveStorage:Blobs にカラムを追加するという手もある。
だが、ActiveStorage:Blobs には metadata というカラム(フィールド)が用意されている。metadata は Rails で扱うときは Hash であり、JSON 形式のテキストで永続化される。なので、仕組みがデフォルトで備わっているので簡便であるし、Hash(JSON)なので必要な属性をどんどん突っ込んでいけばよい。
(ただし、where や find_by などで直接的に検索したい場合は、別途にモデルを作る必要があろう。)
そして ActiveStorage では Analyzer::ImageAnalyzer と Analyzer::VideoAnalyzer というアナライザーがもともと用意されており、例えば画像ファイルから metadata として幅と高さを取り出すことをしている。これを参考にすればよい。
つまりアップロードされたファイルの特徴をどうやって記録するか、という問いにはアナライザーを使うというのが答えである。
オーバーライドで metadata を追加する
ということで実践である。それには@roarkさんの記事がとても参考になる。
だが、ここでは新たなアナライザーを作るのではなく、ImageAnalyzer(image_analyzer.rb)のメソッドをオーバーライドしてしまう。なぜなら、後述するアナライザーの仕組みを知らなくても、とにかく metadata に属性を追加できてしまうし、簡便だからである。
すなわち ImageAnalyzer(またはVideoAnalyzer)の metadata メソッドを乗っ取るだけである。ここでは metadata を乗っ取り、other_properties というメソッドを追加している。
module ActiveStorage
# This is Override `image_analyzer.rb'
# at /usr/local/bundle/gems/activestorage-*/lib/active_storage/analyzer/
# Extracts date and time from an image blob.
class Analyzer::ImageAnalyzer < Analyzer
def metadata
read_image do |image|
if rotated_image?(image)
{ width: image.height, height: image.width }
else
{ width: image.width, height: image.height }
end.merge(other_properties(image) || {}) # 説明1
end
end
private
def other_properties(image) # 説明2
case image.mime_type
when "image/jpeg"
time = image.exif['DateTimeOriginal']
zone = image.exif['OffsetTimeOriginal']||"+09:00"
result = DateTime.strptime(time + zone, "%Y:%m:%d %H:%M:%S%z") rescue nil
return {creation_at: result}
when "image/png"
datetime = image.data['properties']['xmp:MetadataDate']
result = DateTime.parse(datetime) rescue nil
return {creation_at: result}
else
return {creation_at: nil}
end
end
end
end
説明1:ここで既存の属性に追加している。プライベートメソッドで属性が取得できない場合(返値が nil )に備えて {} になるようにしてある。
説明2:新たな属性取得の実体である。デフォルトの ImageMagick(MiniMagick)から取得できる。
アナライザーの仕組み
ActiveStorage::Blob::Analyzable の API の説明が薄くてあまり役に立たない。概要次のような流れである。
- ActiveStorage ではアップロードされたファイルをバックグランド(ActiveJob)で解析するために、
-
Rails.application.config.active_storage.analyzers
に登録されているアナライザーに順次ファイルを渡し、 - 「処理できるか?」と問い合わせ(
accept?
をコールする)、 - 「できる」と答えた(True を返した)最初のアナライザーに metadata の抽出を任せる。
なので、Analyzerクラスにはdef self.accept?(blob)
とdef metadata
が最低限必要である。
老婆心ながらaccept?
は Boolean を返さなければならないし、metadata
は Hash を返さなければならない。
さらに、Rails.application.config.active_storage.analyzers
にも追加しておかなければならない。
また、解析するのは『最初に』 True を返したアナライザーだけである。
ということなので、オーバーライドできた方が簡便であるのが理解できたであろう。
PDF用のアナライザーを作る(新たなアナライザーの作成)
PDF は ImageAnalyzer も VideoAnalyzer も accpet?
のコールはFalse
を返すだけであるから、新たにアナライザーを作らなければならない。他のタイプのファイル(PSとかSVGとかDOCXとか色々応用できる)も同様である。
ここでは pdfinfo という gem を使を使って PDF の作成日とページ数を metadata として取り出して保存する。
module ActiveStorage
# Extracts datetime and number of pages from a pdf blob.
#
# Example:
#
# ActiveStorage::Analyzer::PdfAnalyzer.new(blob).metadata
# # => { creation_at: 2020-11-08T10:53:08.000+00:00, page_count: 2 }
#
# This analyzer relies on the third-party {pdfinfo}[https://github.com/RyanV/pdfinfo] gem. pdfinfo requires
# the {poppler-utils}[https://poppler.freedesktop.org] system library.
class Analyzer::PdfAnalyzer < Analyzer
def self.accept?(blob)
blob.content_type == "application/pdf"
end
def metadata
read_pdf do |pdf|
date = pdf.creation_date rescue nil
count = pdf.page_count
{creation_at: date, page_count: count}
end
end
private
def read_pdf
download_blob_to_tempfile do |file|
require 'pdfinfo' # 説明3
pdf = Pdfinfo.new(file.path) rescue nil
if pdf
yield pdf
else
logger.info "Skipping pdf analysis because Poppler doesn't support the file"
{}
end
end
rescue LoadError
logger.info "Skipping pdf analysis because the pdfinfo gem isn't installed"
{}
end
end
end
Rails.application.config.active_storage.analyzers.append ActiveStorage::Analyzer::PdfAnalyzer # 説明4
説明3:この位置で require することで、pdfinfo がインストールし忘れいる場合でもエラーを出さずにすむ。
説明4:アナライザーを追加してやらないと、metadata が抽出・保存されない。
環境
ruby 2.6.6p146 (2020-03-31 revision 67876) [x86_64-linux-musl]
Rails 6.0.3.4
Linux version 4.9.184-linuxkit (root@a8c33e955a82) (gcc version 8.3.0 (Alpine 8.3.0) )
poppler-utils-0.88.0-r0 x86_64
poppler-0.88.0-r0 x86_64