今回始めてActive Storageを触る機会があったのでその備忘録です。
テスト環境
- Ruby 2.6.3
- Rails 6.0.0.rc1
Rails 6.0.0.rc1である理由は特にないです。
今回たまたま機会があったからです。
URLが欲しい
オプションや設定が足らないせいなのでしょうか?ビューでurl_for
を使うと/rails
から始まるパスが返って来て困りました。コンソールだとURLが返って来てるのできっと解決策あると思うのですが、とりあえずpolymorphic_url
で無事取得できました。
Rails.application.configure do
...
default_url_options[:host] = "localhost:3000"
end
irb(main):001:0> app.url_for Test.first.file
=> "http://localhost:3000/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--f21f99c34dd58d1224cad0c1cf7c68d31c4e4e85/test.txt"
irb(main):002:0> app.polymorphic_url Test.first.file
=> "http://localhost:3000/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--f21f99c34dd58d1224cad0c1cf7c68d31c4e4e85/test.txt"
アップロードしたファイルをダウンロードしたい
Active Storageでアップロードしたファイルをダウンロードしたい場合、これまではActiveStorage::Downloading
をインクルードしていたようですが、どうもRails 6からはDeprecatedらしく、さらに6.1では消されちゃうようです。
ですが、代わりにActiveStorage::Blob#open
が使えるみたいです。
こちらは特別何かをインクルードする必要もなさそうです。
class Test < ApplicationRecord
has_one_attached :file
end
irb(main):001:0> t = Test.new test: "test"
=> #<Test id: nil, test: "test", created_at: nil, updated_at: nil>
irb(main):002:0> f = open "tmp/test.txt"
=> #<File:tmp/test.txt>
irb(main):003:0> t.file.attach io: f, filename: File.basename(f)
=> #<ActiveStorage::Attached::Changes::CreateOne:0x00007fabf9f8b6d0 @name="file", @record=#<Test id: nil, test: "test", created_at: nil, updated_at: nil>, @attachable={:io=>#<File:tmp/test.txt>, :filename=>"test.txt"}>
irb(main):004:0> t.save
...
irb(main):009:0> t.file.blob.open { |tmp| tmp.path }
=> "/var/folders/q1/4wl3c44s73jg_s34cbnbttk4w4lkcd/T/ActiveStorage-1-20190526-24240-ebxwry.txt"
irb(main):010:0> t.file.blob.open { |tmp| tmp.read }
=> "test\n"
irb(main):011:0> File.exist? t.file.blob.open { |tmp| tmp.path }
=> false
ブロックを抜けたらちゃんとファイルが削除されているので、Tempfile.create
を使っているんですかね。
わざわざ自分で URI.open
を使って書いた後に知ったのでとても悔しい気持ちになりましたw
複数ファイルをダウンロードして使いたい
(この話はActive Storageとは直接関係ないです)
仕様や設計の都合によって1レコード・1ファイルとしてアップロードする必要があったのですが、同時にwhere
で取得したレコードに紐づくファイルをまとめてダウンロードして一度にローカルで処理する必要がありました。しかしActiveStorage::Blob#open
はブロックを抜けた後ちゃんとファイルが存在しなくなるので、全てのファイルが存在する状況をどうやって作るかが問題になりました。
今回は以下のように再帰を用いてwhere
で取得したレコードのファイルを全て配列に加えていき、全てのファイルが配列に加えられたら与えられたブロックを評価するという手法にしました。
class Test < ApplicationRecord
has_one_attached :file
def on_local
file.blob.open { |tmp| yield tmp }
end
class << self
def on_local_recursive instances, files, &block
return block.call files unless files.count < instances.count
instances[files.count].on_local do |file|
on_local_recursive instances, files + [file], &block
end
end
def on_local &block
on_local_recursive all, [], &block
end
end
end
irb(main):001:0> Test.count
=> 14
irb(main):002:0> Test.where(id: 1..2).ids
=> [1, 2]
irb(main):003:0> Test.where(id: 1..2).on_local { |files| files.map(&:path) }
=> ["/var/folders/q1/4wl3c44s73jg_s34cbnbttk4w4lkcd/T/ActiveStorage-1-20190616-15932-1lovhy.txt", "/var/folders/q1/4wl3c44s73jg_s34cbnbttk4w4lkcd/T/ActiveStorage-2-20190616-15932-1ldwync.txt"]
irb(main):004:0> Test.where(id: 1..2).on_local { |files| files.all? { |file| File.exist? file } }
=> true
irb(main):005:0> Test.where(id: 1..2).on_local { |files| files.map(&:path) }.map { |file| File.exist? file }
=> [false, false]
この手法の利点はActive Storageが提供するActiveStorage::Blob#open
をそのまま使っているので実装が楽なことと、使い方もブロックを渡す形が同じであること、かつブロックを抜けたらファイルが削除される挙動も同じであることだと思います。ただもっと良い方法があればぜひとも教えてもらいたいですね〜。
感想
Active Storageは思っていたよりも全然使いやすくて良かったです。まだまだ基本的なところしか使っていないだけかもしれませんがw