Edited at

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

More than 1 year has passed since last update.


追記

Rails-5.2より新しいリリースでは ActiveStorage::Blob#open という便利メソッドが追加されてActiveStorage::Downloading がDeprecatedになっている。



  • 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