ActiveStorageでblobのファイルを加工したい場合はActiveStorage::Downloadingをつかう

  • ActiveStorage 5.2

ActiveStorageでアップロードしたファイルをバッチなどで加工したり中身を分析したい場合の話。
ActiveStorage::VariantActiveStorage::Previewerを読む限り、ActiveStorage::Downloadingを使うのがいいらしい。

使い方

インスタンスメソッドblobを持つクラスにincludeすればOK。

class User < ApplicationRecord
  has_one_attached :avatar
end

class Foo
  include ActiveStorage::Downloading

  def initialize(user)
    @user = user
  end

  # ActiveStorage::Downloadingを使うためにはblobが必要。
  def blob
    user.avatar.attachment.blob
  end

  def run
    download_blob_to_tempfile do |file|
      # リモート(service: :localでも)から file にblobの内容をダウンロードしてくれる
      # このブロックの中で加工なり操作を行う。
    end
  end
end

ActiveStorageのソースコード内での使い方

ActiveStorage::Variantでの使い方。MiniMagick::Image.createは内部でtempfileを用意しているのでActiveStorage::Downloading#download_blob_toを使っている。

activestorage/app/models/active_storage/variant.rb
class ActiveStorage::Variant
  include ActiveStorage::Downloading

  attr_reader :blob, :variation
  delegate :service, to: :blob

  def initialize(blob, variation_or_variation_key)
    @blob, @variation = blob, ActiveStorage::Variation.wrap(variation_or_variation_key)
  end

  private
    def process
      open_image do |image|
        transform image
        format image
        upload image
      end
    end

    def open_image(&block)
      image = download_image

      begin
        yield image
      ensure
        image.destroy!
      end
    end

    def download_image
      require "mini_magick"
      MiniMagick::Image.create(blob.filename.extension_with_delimiter) { |file| download_blob_to(file) }
    end
end

ActiveStorage::Previewer::VideoPreviewerでの使い方。こっちはdownload_blob_to_tempfileを使っている。

activestorage/lib/active_storage/previewer/video_previewer.rb
module ActiveStorage
  class Previewer::VideoPreviewer < Previewer
    def self.accept?(blob)
      blob.video?
    end

    def preview
      download_blob_to_tempfile do |input|
        draw_relevant_frame_from input do |output|
          yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
        end
      end
    end

    private
      def draw_relevant_frame_from(file, &block)
        draw ffmpeg_path, "-i", file.path, "-y", "-vcodec", "png",
          "-vf", "thumbnail", "-vframes", "1", "-f", "image2", "-", &block
      end

      def ffmpeg_path
        ActiveStorage.paths[:ffmpeg] || "ffmpeg"
      end
  end
end

初めはpreviewの中のyieldでキーワード引数?って勘違いしていましたが、これはHashとして処理されてるだけっぽいです(ちゃんとソースコードを追ってはいない。最終的にはメソッドのキーワード引数として渡されているっぽいけど)

# 関連しそうな場所を抜粋
class ActiveStorage::Preview
  private
    def process
      previewer.preview { |attachable| image.attach(attachable) }
    end
end

module ActiveStorage
  class Attached::One < Attached
    def attach(attachable)
      blob_was = blob if attached?
      blob = create_blob_from(attachable)
    end
  end
end

module ActiveStorage
  class Attached
    private
      def create_blob_from(attachable)
        case attachable
        when Hash
          ActiveStorage::Blob.create_after_upload!(attachable)
        end
      end
  end
end

class ActiveStorage::Blob < ActiveRecord::Base
  class << self
    def create_after_upload!(io:, filename:, content_type: nil, metadata: nil)
      build_after_upload(io: io, filename: filename, content_type: content_type, metadata: metadata).tap(&:save!)
    end
  end
end
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.