ActiveStorageのmetadataをカスタマイズしてファイルの特徴・属性を記録する

# アップロードされたファイルの特徴をどうやって記録するか

アップロードされたファイルの特徴で検索したり分類したりしたくなったりということはないだろうか？私の場合は、写真の撮影日や 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さんの記事](https://qiita.com/github0013@github/items/c91d5ae545e465b249f7 "ActiveStorageで保存するJPEG画像のEXIFをmetaデータにしたい時")がとても参考になる。
だが、ここでは新たなアナライザーを作るのではなく、ImageAnalyzer（image_analyzer.rb）のメソッドをオーバーライドしてしまう。なぜなら、後述するアナライザーの仕組みを知らなくても、とにかく metadata に属性を追加できてしまうし、簡便だからである。

すなわち ImageAnalyzer（またはVideoAnalyzer）の metadata メソッドを乗っ取るだけである。ここでは metadata を乗っ取り、other_properties というメソッドを追加している。

```Ruby:config/initializers/alternative_analyzer.rb
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](https://api.rubyonrails.org/classes/ActiveStorage/Blob/Analyzable.html) の API の説明が薄くてあまり役に立たない。概要次のような流れである。

1. ActiveStorage ではアップロードされたファイルをバックグランド（ActiveJob）で解析するために、
1. `Rails.application.config.active_storage.analyzers`に登録されているアナライザーに順次ファイルを渡し、
1. 「処理できるか？」と問い合わせ（`accept?`をコールする）、
1. 「できる」と答えた（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](https://github.com/RyanV/pdfinfo) という gem を使を使って PDF の作成日とページ数を metadata として取り出して保存する。

```Ruby:config/initializers/pdf_analyzer.rb
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