2018年12月11日火曜日
Active Storage移行記:データ移行編
こんにちは、あさのです。
前にActive Storageの記事を書きましたが、今回もそのネタで行こうと思います。
Active Storage移行記:バリデーション編|sitateru tech blog
今回は、Paperclip管理のデータをActive Storage管理に移行したときのお話です。
データ構造
Paperclipではファイルのデータは各モデルのカラムに保存されています。
userクラスのiconというファイルであれば、userテーブルに以下のようなカラムがあります。
t.string "icon_file_name", comment: "ファイル名"
t.string "icon_content_type", comment: "ファイルのcontent type"
t.integer "icon_file_size", comment: "ファイルサイズ"
t.datetime "icon_updated_at", comment: "ファイル更新日時"
対してActive Storageでは全クラスの全ファイルをまとめて2つのテーブルで管理します。
create_table "active_storage_attachments", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
t.bigint "record_id", null: false
t.bigint "blob_id", null: false
t.datetime "created_at", null: false
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end
create_table "active_storage_blobs", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
t.string "key", null: false
t.string "filename", null: false
t.string "content_type"
t.text "metadata"
t.bigint "byte_size", null: false
t.string "checksum", null: false
t.datetime "created_at", null: false
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
たとえば、
- userクラスのicon
- user.id=15
- ファイル名 prof.jpg
というファイルだと以下のようなレコードになります。
2つのテーブルは blob_id
で関連づけられるんですね。
id name record_type record_id blob_id created_at
15 icon User 3 16 2017-12-28 19:23:03
id key filename content_type metadata byte_size checksum created_at
16 3e0ca647-18e3-43ee-85c4-4de552bac46f prof.jpg image/jpeg {} 44202 isFyYNJIs/6pyptfRUMUlA== 2017-12-28 19:23:03
データ移行
さて、それぞれのデータ構造がわかったのでマイグレーションをしていきます。
このあたりの記事を参考にさせてもらい、rakeタスクで実装しました。
namespace :import_to_active_storage do
desc 'Import to ActiveStorage'
task run_all: :environment do
models = ActiveRecord::Base.descendants.reject(&:abstract_class?)
import_data(models)
end
private
def import_data(models)
conn = ActiveRecord::Base.connection
models.each do |model|
attachments = model.column_names.map do |c|
c =~ /(.+)_file_name$/ && Regexp.last_match(1)
end.compact
model.find_each do |instance|
attachments.each do |attachment|
puts "Process #{model.name}/#{instance.id}"
next unless instance.send("#{attachment}?")
next if conn.select_one("SELECT `id` FROM active_storage_attachments WHERE `record_type` = '#{model.name}' AND `record_id` = #{instance.id}").present?
puts "Migrate #{instance.send(attachment.to_s)}"
ActiveRecord::Base.transaction do
execute_statement(conn, model, instance, attachment)
end
end
end
end
end
def execute_statement(connection, model, instance, attachment)
active_storage_blob_statement(connection).execute(
key,
instance.send("#{attachment}_file_name"),
instance.send("#{attachment}_content_type"),
instance.send("#{attachment}_file_size"),
checksum(instance.send(attachment)),
instance.updated_at.strftime('%Y-%m-%d %H:%M:%S')
)
active_storage_attachment_statement(connection).execute(
attachment,
model.name,
instance.id,
instance.updated_at.strftime('%Y-%m-%d %H:%M:%S')
)
end
def active_storage_blob_statement(connection)
connection.raw_connection.prepare(<<-SQL)
INSERT INTO active_storage_blobs (
`key`, `filename`, `content_type`, `metadata`, `byte_size`, `checksum`, `created_at`
) VALUES (?, ?, ?, '{}', ?, ?, ?)
SQL
end
def active_storage_attachment_statement(connection)
connection.raw_connection.prepare(<<-SQL)
INSERT INTO active_storage_attachments (
`name`, `record_type`, `record_id`, `blob_id`, `created_at`
) VALUES (?, ?, ?, LAST_INSERT_ID(), ?)
SQL
end
def key
SecureRandom.uuid
end
def checksum(image)
uri = URI.parse("https:#{image.url}")
body = Net::HTTP.get(uri)
Digest::MD5.base64digest(body)
end
end
そこそこ長いですが、結局は各モデルの各ファイルごとにPaperclipのカラム情報とファイルのハッシュ値を取り、
Active Storageのテーブルに入れるという繰り返しです。
なお、各モデルの実装をActive Storage用に書き換えた後ではこのタスクは動かないので、
Paperclip時に実行する必要があります。
実際にやったところ数万件のデータがあったために5~6時間かかるという大引っ越しになってしましましたが、
なんとか乗り切りました。
ということで今回はActive Storageのデータ移行について書きました。
まだもう少しActive Storageネタで書けるかもしれません。
それでは。
2018年11月26日月曜日
Active Storage移行記:バリデーション編
こんにちはあさのです。
今回は、Ruby on Railsのファイルアップロード機能をPaperclipからActive Storageに移行したときの話を書こうと思います。
Paperclip
Paperclipはファイルアップロード用のGemです。
https://github.com/thoughtbot/paperclip
比較的導入が簡単であり、アプリケーションにファイルアップロード機能をつけるためによく使われています。
ですが、現在は "Deprecated" とされていて、Rails 5.2から標準機能の1つとなったActive Storageへの移行が推奨されています。
Active Storage
Active StorageはRailsのバージョン5.2から標準搭載されている機能の一つで、Paperclipと同様Active Recordのオブジェクトと紐づけたファイルアップロード機能を提供しています。
https://github.com/rails/rails/tree/master/activestorage
シタテルでもRails 5.2へのアップグレードが落ち着いたところでこの移行を行いました。
そんなわけで、移行にあたっていくつか注意点となったところを振り返っていきたいと思います。
今回はモデルのバリデーションの話です。
Active Storage でのバリデーション
Papaerclipにはバリデーション機能があります。
以下はiconという名前で扱う添付ファイルのバリデートをする例で、モデル内にこう書いておけば保存時に自動でチェックされます。
見てのとおりですが、ファイル形式は[jpg, jpeg, gif, png]のどれか、ファイルサイズが10MB以下である必要があります。
validates_attachment :icon,
content_type: {
content_type: [
'image/jpg',
'image/jpeg',
'image/gif',
'image/png'
]
},
size: {
less_than_or_equal_to: 10.megabytes,
message: I18n.t('errors.messages.file_too_large')
}
しかしActive Storageにはこのようなバリデーション機能はありません。
じゃあどうするんだ!という話ですが、素直にActive Recordのカスタムバリデーションを使いましょう。
これで同じようなことができます。
validate :validate_icon
def validate_icon
return unless icon.attached?
if icon.blob.byte_size > 10.megabytes
icon.purge
errors.add(:icon, I18n.t('errors.messages.file_too_large')
elsif !image?
icon.purge
errors.add(:icon, I18n.t('errors.messages.file_type_not_image'))
end
end
def image?
%w[image/jpg image/jpeg image/gif image/png].include?(icon.blob.content_type)
end
ちなみに、
attached?
はファイルが存在するかどうかblob.byte_size
はファイルサイズblob.content_type
はファイルタイプ
を取得しています。
ということでActive Storageのバリデーションについて書いてみました。
Paperclipからの移行はいろいろと作業があったので他のトピックについてもまた書いていきたいと思います。
それでは。
2018年11月19日月曜日
スネーク?キャメル?ケバブ?命名規則のカオスが発生
こんにちは!
SCSチームのいしづかです。
シタテルでは主にRails + Vue.jsにてシステムを開発しています。
生産管理を行うシタテルコントロールシステム(SCS)もRailsで書かれており、Viewは基本的にhaml + scssです。
SCS全体のAPI + SPA化によるマイクロサービス化計画も進んでいる中、ここ最近作られている画面はhamlの中にVue.jsを埋め込むという方法が取られています。
その埋め込み方法は別の機会に書くとして、そこで出てきた 命名規則のアレコレ についてレポートしたいと思います。
Railsはパスカル + スネーク、Javascriptはキャメル、htmlはケバブ・・・
表題の通り、いろいろ混ざりました。
Railsでは、ネームスペースやクラス名はパスカルケース(ClassName)、それ以外のメソッド名などはスネークケース(method_name)で書きます。
定数などはアッパーケース(CONST_VALUE)で書きますね。いつの間にか3種類使い分けていました。
私自身、前職ではC#.NETでWindowsアプリケーションを作っていたのでパスカルケースを見ると安心します(笑)
Vueを使いだしてから、メソッド名はキャメルケース(methodName)、htmlやcssのidやクラスなどはケバブケース(class-name)が登場してきました。
・・・ついに1つのRailsプロジェクトに、ほぼすべての命名規則が揃ってしまったのです。
RailsとVueで分かれていればまだよかったかもしれませんが、Railsで書いたAPIのjsonがスネークケースで出力されてしまうため、Vue側にもスネークケースが進出してしまいました。
そこで、さすがにルールを設けようということになったのです。
統一ルール発令
Railsで使っているパスカルケース・スネークケースは崩すことができません。これはこのまま。
Vue側は データに関わるもの(data・computed)はスネークケース、処理に関わるもの(methods)はキャメルケース、html・css周りはケバブケース というルールに統一しました。
なんとなく複雑っぽいですが、メソッドとhtml周りはそのままで、データに関わるところだけちょっとRailsに寄せたという形です。
これでRails側はこれまでのソースを流用できますし、Vue側ではJsonをパースしてバインドする際、スネークケースが出てきてもOKです。
Vueが入ってきて発生した命名規則の混乱は、この統一ルール発令によってひとまずしばらくは沈静化するでしょう。。。
API + SPA化計画では、フロントはキャメル統一の予定
とはいえ!Javascriptは基本的にキャメルケース。
現在実装中のSCS APIでは、RailsからJsonを出力する際にキャメルケースに変換するように実装しています。
これでSPA側でデータを受け取る時だけスネークケースにしなくても済みそうです。
まとめ
これまでとは異なる言語やプラットフォームを使う時、既存のものとの整合性が取れなくなるときがありますね。
今回、私たちは「命名規則がカオスになる」という形でそれが表れました。
シタテルではそんなカオスとも仲良くしながら(闘いながら?)、日々新しいものにも取り組みつつシステムを構築しています。
2018年10月29日月曜日
Gem Brakeman でRailsのセキュリティチェック
どうもこんにちは
シタテルでエンジニアをしている朝野です。
主に、セキュリティーや開発インフラを担当しています。
シタテルではRuby on Railsを使っているプロダクトがいくつかあるのですが、そこでのセキュリティ施策の1つとしてBrakemanというGemを使っています。
セキュリティチェック用のGemとしては有名どころですが、理解の確認も兼ねてまとめてみました。
Brakemanとは
Railsのソースコードを解析し、SQLインジェクションやXSS等の脆弱性になりかねない危険なコードを見つけ出すGemです。
brakeman | RubyGems.org | your community gem host
Brakeman — Rails Security Scanner
インストール
Gemなので、
$ gem install brakeman
とするか、Gemfileに
gem ‘brakeman’, require: false
と書いて bundle install
すればOKです。
実行
$ bundle exec brakeman
で実行でき、ソースコード内を確認してくれます。
オプションもいろいろあるのですが、現在は
-A
すべての種類の脆弱性を検査
-w1
すべてのレベルの脆弱性を検査
-z
脆弱性が見つかった場合の終了コードを0以外に
というオプションを使っています。
-A
と -w1
はセキュリティのため、 -z
はCircleCIのためです。
脆弱性が見つかった時ビルド失敗になり、開発中のブランチで脆弱性が見つかっていたらmergeできなくなるわけです。
ちなみにその他オプションはこちら。
ただ毎回オプションを打つのも面倒なので、設定ファイルにまとめてしまうと便利です。-C
をつけて実行すれば、同時につけたオプションに相当する設定ファイルの内容が出力されます。
$ bundle exec brakeman -A -w1 -z -C
---
:run_all_checks: true
:min_confidence: 2
:exit_on_warn: true
これを config/brakeman.yml に保存しておけば自動でその設定が読み込まれるので、オプションをつけなくてよくなります。
ちょっと楽ですね。
警告に対応しないとき
Brakemanを実行して出力された脆弱性を修正していくことになります。
ただ、修正のしようがないときやそのままでも問題ないときというのもたまにあります。
そんな場合は safe_methods と brakeman.ignore の設定が役に立ちます。
safe_methods は実行時のオプションの一つで、名指ししたメソッドは安全だからチェックしなくていいよ!とするものです。
例えば、config/brakeman.yml に
:safe_methods:
- :display_hoge
- :change_fuga
と書けば、 display_hoge
と change_fuga
のメソッド内はスキャンされません。
brakeman.ignore は無視する警告のリストです。
brakemanは
$ brakeman -I
と -I
オプションで実行すると対話モードになるのですが、
------------------------------
Actions:
i - Add warning to ignore list
n - Add warning to ignore list and add note
s - Skip this warning (will remain ignored or shown)
u - Remove this warning from ignore list
a - Ignore this warning and all remaining warnings
k - Skip this warning and all remaining warnings
q - Quit, do not update ignored warnings
? - Display this help
-------- 1/1 -----------------
Confidence: Weak
Category: Cross-Site Scripting
Message: Unescaped model attribute
Code: display_hoge(Item.hoge)
File: app/views/some/path/edit.html.haml
Line: 19
Action: (i, n, k, u, a, s, q, ?)
というふうに見つかった脆弱性をどうするか聞いてきます。
ここで i
を入力すればその脆弱性を無視リスト(デフォルトでは config/brakeman.ignore
)に追加できます。
無視リストに入れられた警告は、以降出力されなくなります。
このように不要な警告をスルーする手もちゃんとあるのがbrakemanのよくできているところですが、うっかり対象から外した部分に危険なコードを書き足してしまわないように注意しなければいけないですね。
と、ざっとまとめてみました。
最初からセキュリティを意識してコーディングするのがまずは重要ですが、このようなチェックがあると安心がアップしますね。