railsゆるふわリハビリ開発日記

ほんとにゆるいので、学科民が読んだら薄味すぎて死ぬんじゃないかな。やめたほうがいいよ!(予防線)(学科民が怖い)
 
目次

なんの文章ですか

なんか気分になって軽いrailsアプリを作ってみたので、作業ログというか気持ちの日記みたいなのを公開してみます。マジでただの気持ちなので誰の役に立つかというのはよくわからないのですが、railsでなんかやってみたいけどイメージ掴めないなという人にはいいかもしれません。あとまあactive storage使ったのでそのへんかな。

なんのアプリですか

pdfをアップロードするとjpegに変換して一覧表示してくれる&最終閲覧順に勝手に並んでくれる というアプリです。気持ちとしてはスマホでpdf見ようとするとダウンロードされる上にその後ローカルで迷宮入りしてクソうざいという現象があり、webで一元管理できたらいいよなあという気分になったというのがあります。

……とか言いつつ、これ本気でやるならモバイルで開発してファイルのアップロードを楽にするべきなのでこういうwebアプリとしてやる意味は特になく、単にrailsの練習がしたかっただけです。railsはn年前くらいに一通り学んだのですがそれっきりで記憶が風化してしまったのでリハビリをしたく、また単純なCRUDを作るだけなのも味気ないなと思ったのでファイル処理でもやろうという気持ちになったというのがあります。まあただのトレーニングというわけですね。

ちなみにモノですが、こちらになります。
github: https://github.com/iwannatto/mypdfviewer
heroku: https://mypdfviewer.herokuapp.com/pdfs

※herokuの方は触る前に下の方にある注意事項を読んでください

環境構築編

まあ手持ちはmacだしできないってことはないやろ、粛々とやっていきまーす……

とかナメてたらドブった。どうもrubyのビルド時にopensslの場所を正しくお伝えしなければならない様子なのだが、./configureのオプションでお伝えしたはずなのに一切うまくいかない。いろいろ試してググってグネグネやってようやくrailsが入れられてセーフと思ったら、「rails s」をするとセグフォするという最悪の状況が出現。ここで更にググったけどなんかopenssl自体の入れ直しを迫られて非常に嫌になり、結局cloud9に逃げ込む。(もともとアカウントは持ってた)

cloud9にしたら1分でいけました。

結論:ローカルはクソ。時代はクラウド

アプリ全体について考えてみる編

要件としては「pdfをアップロードし、jpegに変換して表示できる」「最終閲覧順に並べられる」というものなので、これを満たすにはどうすればいいかを考えます。
まず最初の要件に関して、ファイルを受け取って保存できる必要がありますね。ちょっと調べてみると、最新のrails5.2には「active storage」というファイルを扱う用の枠組みがあることがわかりました。まあ普通にこれでオッケーそうだし、最新機能というのは響きがいいのでこれでいくことにしました。
もう一つ、pdf→jpegの変換ですが、これはimagemagickという一般的なソフトウェアで普通にいけるらしいということがわかりました。rubyからimagemagickを触れるようなgemがある(minimagick)ので、たぶんこれを使えばオッケーでしょう。
あとは最終閲覧順に並べるというのですが、これは単純に最終閲覧日時のカラムを用意すればそれでよし。
というわけでなんとなくやることが決まりましたね。

pdfを受け取る編

まあ骨組みから作っていくわけですが、railsはscaffoldingとかいうハイパー骨組み作成機能があり、ちょちょっとやるだけで基本的なCRUDを備えたMVCが全部できちゃうんですよね。

$ rails new mypdfviewer
$ cd mypdfviewer
$ rails g scaffold pdf name:string last_access:datetime
$ rails db:migrate

 これでもうできました。この段階ではまだnameとlast_accessしか扱えないのですが、それでももうこの時点でアプリケーションとしては動作するんですよね。ほえー。
というわけでCRUDができてしまったので、あとはこのpdfモデルでpdfファイルを扱えるようにしてやればよいです。
まずconfigを弄る必要があるんですが、なんか自動生成されたconfigそのままで問題ないっぽいのでよし。
モデルを弄るのは超簡単。

# app/models/pdf.rb内のクラス内に追記
has_one_attached :pdf

この1行を入れるだけでpdfという項目名でファイルを扱えるようになります。ウオー。
あとはformにfile_fieldを入れてファイルアップロードできるようにして、ストロングパラメーターの部分だけいじれば、
「@pdf = Pdf.new(pdf_params)」みたいな普通の記述でpdfモデルへのpdfファイルの紐付けができてしまいます。
というわけでこれにて終了。なんというか、圧巻の簡単さですね。すごいなあ。

ちなみに、DB触らなくていいの?となるかもしれないのですが、active storageはDBではない場所にファイルを保存するので、これ以上DBを触る必要がないんですよね。ただ、内部的にはメタデータを扱うためのDBを作るようなので、一度なんかのコマンドを走らせる必要だけあったかも(忘れた)。それと、今回はローカルに保存する設定になっていますが、その気になればS3とかが使えます。

あとついでに、ファイルの中身をpdfに制限したいので、モデルにバリデーターを作ったりもしました。これは後に間違いであったことが判明します。

pdf→jpeg変換編

とりあえずimagemagickを入れます。これ後で本番環境に入れるの忘れそうで怖いな。

$ sudo yum -y install ImageMagick

gemとしてはminimagickを使います。railsのscaffoldingで生成されたデフォルトのGemfileに書いてあるからという安直な採用理由で。

# Gemfileのmini-magickの行のコメントアウトを外す
$ bundle install

あとpdfモデルにjpegを付与できるようにしておく(モデルに1行追記

ここまでやれば、あとはminimagickを使って変換処理を書けばいいんですね。
……ところがここでハイパー詰まります。active storageにもminimagickにも不慣れなせいで、ファイルの取得・読み込ませ・変換 の各段階でガツンガツン引っかかった。
まず入力、imagemagickで拡張子変換をするのにはconvertコマンドを使うのだが、minimagickでそれに対応する(と思っていた)convertオブジェクトは入力でパス渡しをしなければいけない。ところがactive storageから取ってきたファイルはちゃんと使えるパスがないっぽく、わりと調べたが?????となり、結局今回はconvertオブジェクトは使えないということに。active storageからpdfを読んでそのバイナリを取り出してimageオブジェクトというのに渡し、そこでメソッドで変換するらしい。
取り出しにもなんか一癖あり、active storageへの保存メソッドにはストリームを渡す必要があるため割と考えなければいけなかった。最終的にはimageオブジェクトからはstring型でデータが取り出せるのを突き止め、そいつをrubyのstringioを使ってストリームにして渡しました。これstringioのことを元から知っていなければ詰んでいた感じがある。

require 'mini_magick'
require 'stringio'
binary = @pdf.pdf.download
pdf = MiniMagick::Image.read(binary)
jpeg = pdf.format('jpeg', 0)
sio_jpeg = StringIO.new(jpeg.to_blob)
@pdf.jpeg.attach(io: sio_jpeg, filename: '0.jpeg', content_type: 'image/jpeg')

というわけでどうにか変換処理には成功。ただ、デフォルトの変換というのは画質が悪く、この画質を調整するには変換時にdensityオプションを指定する必要があるんですね。というわけで、そこを改良。

……しようとしたところでまた詰まるんですね。オプションを足すなんて簡単にできるのでは?という僕の考えは非常に甘く、上のやり方ではdensityオプションを正しく指定できない。これは「imagemagickのコマンドにおいて、densityオプションは入力ファイルの前につける必要がある」というのと「imageオブジェクトのconvertメソッドを使う方法では入力ファイルの後に付けるオプションしか発行できない」というのが合わさりですね……。これできないの全然信じられなくて相当ググり回ったんだけど、不可能らしいということを確認するに終わりました。
まあそういうわけでどうすんのという感じだったのですが、ここでさっき切り捨てたconvertオブジェクトが使えました。imageオブジェクトをバイナリから読み込んだ後は実はパスが発行できるようになるので、これを渡せばok。convertオブジェクトは入出力両方パス渡しなので出力をどうするかと散々悩んだのですが、結局rubyのtempfileを使えばいいじゃんという話になりました(ここで1時間くらい悩んでいたのは普通にアホだった)。

require 'mini_magick'
require 'tempfile'
binary = @pdf.pdf.download
pdf = MiniMagick::Image.read(binary)
Tempfile.create(["", ".jpeg"]) do |jpeg|
  MiniMagick::Tool::Convert.new do |convert|
    convert.density(600)
    convert << pdf.path
    convert << jpeg.path
  end
  @pdf.jpeg.attach(io: jpeg, filename: '0.jpeg', content_type: 'image/jpeg')
end

まあなんとかできました。この辺の時間対成果の低さはやばかったんですができてよかった。
ちなみにこの後ファイル入力の仕方が変わるので結局全部パスになり、結局バイナリ関連で調べたことは全部無駄になりました。フエーン

最終アクセス時間

普通に書いたら終わった。
この辺は箸休めだった。

各種改良編

まあそういうわけで↑の段階で一応要件を満たしてかつ動くものは作れたので、あとは気になるところをちょこちょこ直す作業に入ります。ざっくり2つありました。

まず1つ目は、現行のコードだと pdf受け取る→active storageに保存→active storageからpdf取り出す→jpegに変換 という動きになっていて、これは自明に無駄なので直します。まあこれはそもそもリクエストデータの中にあるファイルをどうやって扱っていいか調べるのがめんどいからごまかして書いてただけなので、最初からやるべき作業とわかっていたやつ。
これも結局調べて数行書いただけ。結論から言うと、リクエストデータの中にあるファイルは一時ファイルとなって保存されており、そのパスが普通に手に入った。というわけでimagemagickにはパス渡しだけしてればよくなった。

もう1つはバリデーションに関して。railsではモデルにバリデーションを設定することができて、データをDBに保存する直前に自動で検証してダメだったら差し戻してくれます。というわけでpdfモデルのpdf(ファイル)にも.pdf以外を弾くという簡易な拡張子検証を入れておいたのですが、サーバのログを見ているとあることに気づきます。
pdfファイルに関しては、どう見てもデータ保存した後にバリデーションが動いているんですよ。
そういえばactive storage周りを調べている時に「active storageにはバリデーションがない」という文言はいくつか見ていて、これは「デフォルトでactive storage用のバリデーションマクロがないから手動で書かなきゃいけない」程度の意味だと思っていたんですが、どうも動きが根本的に違うようで。試してみた結果やはりバリデーションはダメで、「active storageに対応する属性はセットした瞬間にsaveされてしまう」「このsaveの際にバリデーションは働かない」という仕様のようでした。一応後からバリデーションを走らせることはできるけど、二度手間もしくは無意味になると。
railsは基本的に「コントローラでバリデーションすると分厚くなるからダメで、モデルに書きなさい」という思想だったのでうーんという感じでしたが、最終的には大人しくコントローラにバリデーションを載せたら終わりました。ちなみにrails開発陣によるとactive storageのバリデーションは普通に今後実装する予定らしいですね。まあそうか。

heroku編

herokuにあげようとしたらimagemagickが入ってないなと気づく。
HerokuでImageMagickのconvertができるまで
↑をまんまやって終了。先人は神
と思っていたが、クラッシュしたり何なりがあったのでProcfileを書いた。この辺よくわかっていない。

完成品

github: https://github.com/iwannatto/mypdfviewer
heroku: https://mypdfviewer.herokuapp.com/pdfs
一応上げてみました。どうぞ。

herokuの方の注意事項を諸々と。

  • ユーザーとかの概念がないのでファイルは全部完全に公開されます
  • 上げたファイルが保存されている保証が全然ないです
  • ある程度でかいファイルを上げるとなぜかクラッシュします(は?)(具体的には100KB前後とかにしておいてほしい)

というわけでまとめると、試してみるなら「誰に見られても問題ない&バックアップのある&100KBくらいの」pdfにしてください。
あとこれセキュリティとかその辺一切考えてないので完全に善意頼りですヨロシク。

ちなみに先ほどファイルの保存を保証しないと言いましたが、もっと具体的に言うとファイルは勝手にバシバシ消えていきます。
詳細はここに書いてあるのですが、要はherokuサーバにファイルとか作ってもアプリが再起動すると消滅するというheroku側の仕様です。つまりherokuでactive storageをlocalで使うのはそもそもできないので、本来ならS3とかのストレージサービスを使うべきという話です。ただまあ即消えるわけじゃないので動作確認ならなんとかなるし、何よりめんどいということでこのままいっています。
ちなみにちゃんとS3とかにしたい場合はアカウント取ってconfigいじればいけるっぽいです。

感想

すべてを覆い隠すフレームワークの息吹を感じる。
いやほんとこれで、webアプリを作ったかと言われればyesなんでしょうが、プログラミングをしたかと言われれば気分的にはnoなんですよね。人の書いたものを使っている感がすごいというか、クソでかいconfigを延々弄っている みたいな気持ちになります。
と言ってもこれは決してdisっているわけではありません。むしろこれはおそらくrailsの目指しているものなはずで、良いことだと思います。モノが秒でできてほしいという人間に対して最適な方法を提供していると言いますか、スタートアップでガンガン使われるのは伊達じゃないなあと。実際、「rails」でtwitter検索をかけてみると、なんていうかウォッとなるような人間がいっぱい登場するんですが、そういう人たちが開発に参加することができるようになるからこそ強いんだろうなあ。この面では結局圧倒的というか、railsすげーという気持ちになりました。
あと個人的に気になるところなんですけど、railsを極めるとどういうエンジニアになっていくんでしょうね。railsに精通していくのか、あるいはrails自体の開発に噛んでいくのか。ちょっとググった感じ、この記事みたいな感じの見通しなんでしょうかね。
まあともかく、作りたいものがあった時に強力なフレームワークであるのは間違いないなという気持ちになったので、なんかあったら今後もrailsで作ってみたいですね(ふわっふわ)。


(おわりです)