Active Storageの基本で困ったこと

今回始めてActive Storageを触る機会があったのでその備忘録です。

# テスト環境

* Ruby 2.6.3
* Rails 6.0.0.rc1

Rails 6.0.0.rc1である理由は特にないです。
今回たまたま機会があったからです。

# URLが欲しい

オプションや設定が足らないせいなのでしょうか？ビューで`url_for`を使うと`/rails`から始まるパスが返って来て困りました。コンソールだとURLが返って来てるのできっと解決策あると思うのですが、とりあえず`polymorphic_url`で無事取得できました。

```config/environments/development.rb
Rails.application.configure do
  ...
  default_url_options[:host] = "localhost:3000"
end
```

```ruby
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`が使えるみたいです。
こちらは特別何かをインクルードする必要もなさそうです。

https://github.com/rails/rails/blob/01b7e9b73ac9dca92be0969e047b9cb0c767f261/activestorage/lib/active_storage/downloading.rb#L8-L13

```app/model/test.rb
class Test < ApplicationRecord
  has_one_attached :file
end
```

```ruby
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`で取得したレコードのファイルを全て配列に加えていき、全てのファイルが配列に加えられたら与えられたブロックを評価するという手法にしました。

```app/model/test.rb
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
```

```ruby
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