とりあえずコード書けよ

技術的なことの備忘録。

cocoonで動的に要素を増やす場合のテスト

結論

cocoonで動的に要素を増やす場合はクラスで対応する。

# view slim&simple_form
.nested-fields
  = f.input :upload_file, as: :file, accept: 'image/jpeg,image/png', input_html: { class: 'upload-images' }
# test
 click_link '画像を追加' # link_to_add_associationに書いた文字列
 attach_file(page.all('.upload-images')[0][:id], Rails.root.join('test', 'fixtures', 'files', 'valid_image.jpg'))

capybara間が空くと書き方忘れてしまう...

pumaのproduction用設定

config/puma.rb

# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
threads min_threads_count, max_threads_count

# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
# port        ENV.fetch("PORT") { 3000 } # ここをコメントアウトなり消すなり
bind "unix://#{Rails.root}/tmp/sockets/puma.sock" # 追記
daemonize # 追記
stdout_redirect "#{Rails.root}/log/stdout", "#{Rails.root}/log/stderr" # 追記

以下、省略
...

実行時

rails s -e production

nodejsのアップデートでハマった

概要

nodejsのアップデートとエラーの対応の方法について

nodejsのアップデート

1.指定のバージョンのダウンロード

distributions/README.md at master · nodesource/distributions · GitHub

curl -sL https://rpm.nodesource.com/setup_12.x | sudo bash -

2.古いバージョンの削除

rm /etc/yum.repos.d/nodesource-el.repo

3.yumのセットアップ

yum clean all

※これをやらないと、インストール時に旧バージョンを見に行って失敗する

4.nodejsの削除

yum remove nodejs

5.nodejsの再インストール

yum install nodejs

yarnの更新

1.yarnの削除

yum remove yarn

2.yarnのインストール

yum install yarn

webpackerの更新

1.webpackerのアップデート(gem)

bundle update webpacker

2.webpackerのアップデート(node_modules)

yarn upgrade @rails/webpacker --latest

3.webpack-dev-serverのアップデート

yarn upgrade webpack-dev-server --latest

トラブルシューティング

1.nodejsのアップデート

yum clean all

これを知らずに、いつまでもnodejsが更新できないという事に...

2.nodejsのアップデートやると、凄まじい数のエラーが出ることがある

yarn install v1.22.5
warning package.json: No license field
warning No license field
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.2.4: The platform "linux" is incompatible with this module.
info "fsevents@1.2.4" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
warning "@rails/webpacker > postcss-cssnext@3.1.0" has unmet peer dependency "caniuse-lite@^1.0.30000697".
warning " > webpack-dev-server@2.11.2" has unmet peer dependency "webpack@^2.2.0 || ^3.0.0".
warning "webpack-dev-server > webpack-dev-middleware@1.12.2" has unmet peer dependency "webpack@^1.0.0 || ^2.0.0 || ^3.0.0".
[4/4] Building fresh packages...
[-/2] ⢀ waiting...
error /to/app/path/node_modules/node-sass: Command failed.
Exit code: 1
Command: node scripts/build.js
...
...
...
In file included from ../src/binding.cpp:1:0:
../../nan/nan.h: In instantiation of ‘void Nan::imp::SetMethodAux(T, v8::Local<v8::String>, v8::Local<v8::FunctionTemplate>, ...) [with T = v8::Local<v8::Object>]’:
../../nan/nan.h:2353:57:   required from ‘void Nan::SetMethod(HandleType<T>, const char*, Nan::FunctionCallback) [with T = v8::Object; HandleType = v8::Local; Nan::FunctionCallback = void (*)(const Nan::FunctionCallbackInfo<v8::Value>&)]’
../src/binding.cpp:351:42:   required from here
../../nan/nan.h:2337:3: warning: ‘bool v8::Object::Set(v8::Local<v8::Value>, v8::Local<v8::Value>)’ is deprecated (declared at /home/user_name/.node-gyp/12.18.4/include/node/v8.h:3498): Use maybe version [-Wdeprecated-declarations]
   recv->Set(name, GetFunction(tpl).ToLocalChecked());
   ^
make: *** [Release/obj.target/binding/src/binding.o] エラー 1
make: ディレクトリ `/to/app/path/node_modules/node-sass/build' から出ます
gyp ERR! build error 
gyp ERR! stack Error: `make` failed with exit code: 2
gyp ERR! stack     at ChildProcess.onExit (/to/app/path/node_modules/node-gyp/lib/build.js:262:23)
gyp ERR! stack     at ChildProcess.emit (events.js:315:20)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:275:12)
gyp ERR! System Linux 4.14.33-51.37.amzn1.x86_64
gyp ERR! command "/usr/bin/node" "/to/app/path/node_modules/node-gyp/bin/node-gyp.js" "rebuild" "--verbose" "--libsass_ext=" "--libsass_cflags=" "--libsass_ldflags=" "--libsass_library="
gyp ERR! cwd /to/app/path/node_modules/node-sass
gyp ERR! node -v v12.18.4
gyp ERR! node-gyp -v v3.7.0
gyp ERR! not ok 
Build failed with error code: 1

こういうときは

yarn why node-sass

を使うと、何に依存してるかとかが分かるので、これを元にアタリをつけると良い。

yarn why v1.22.5
warning package.json: No license field
[1/4] Why do we have the module "node-sass"...?
[2/4] Initialising dependency graph...
warning No license field
[3/4] Finding dependency...
[4/4] Calculating file sizes...
=> Found "node-sass@4.9.2"
info Reasons this module exists
   - "@rails#webpacker" depends on it
   - Hoisted from "@rails#webpacker#node-sass"
info Disk size without dependencies: "11.64MB"
info Disk size with unique dependencies: "16.41MB"
info Disk size with transitive dependencies: "11.64MB"
info Number of shared dependencies: 115
Done in 1.67s.

github actionsにredisを追加する

概要

actionsにredisを追加してActiveJobのCIに対応する。

結論

docs.github.com ここに書いてありますね。

で、Rails用のactionsに置き換えると下記のような感じかなと。

name: Build

on:
  push:
    branches:
      - master
      - development
  pull_request:
    branches:
      - master
      - development

jobs:
  run_test:
    name: Run test

    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:11
        ports:
          - 5432:5432
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
        options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
      redis:
        image: redis
        ports:
          - 6379:6379
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        env:
          REDIS_HOST: redis
          REDIS_PORT: 6379

    steps:
    - uses: actions/checkout@v1
    - name: Set up Ruby 2.6
      uses: ruby/setup-ruby@v1
      with:
        ruby-version: 2.6.6

    - name: install bundler2
      run: gem install bundler

    - name: Install dependent libralies
      run: sudo apt-get install libpq-dev graphviz

    - name: Bundle install
      run: bundle install --jobs 4 --retry 3

    - name: Yarn install
      run: yarn install --check-files

    - name: Setup Database
      run: |
        cp config/database.yml.ci config/database.yml
        bundle exec rake db:create
        bundle exec rake db:schema:load
      env:
        RAILS_ENV: test
        POSTGRES_USER: postgres
        POSTGRES_PASSWORD: postgres

    - name: Run test
      run: |
        COVERAGE=true bundle exec rails test
        COVERAGE=true bundle exec rails test:system
      env:
        LANG: ja_JP.UTF-8
        RAILS_ENV: test
        POSTGRES_USER: postgres
        POSTGRES_PASSWORD: postgres

    - name: Upload
      uses: actions/upload-artifact@v2
      if: failure()
      with:
        name: my-uploads
        path: tmp/screenshots/

ec2にredisを入れる

概要

ActiveJobを使うために必要だったのでとりあえず雑に書き留め。

手順

1.リポジトリの追加

sudo rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm

2.インストール

sudo yum --enablerepo=remi install redis

3.起動

sudo service redis start

postgresqlに依存関係が増えた関係でlinux2にインストールのにハマった

postgresql 11.8をインストールしようとすると llvm-toolset-7-clang >= 4.0.1 の依存でこけるようになりました。
調べてみるとllvm-toolset-7-clangはcentos-sclリポジトリにあるとのことでした。 https://centos.pkgs.org/7/centos-sclo-rh-x86_64/llvm-toolset-7-clang-5.0.1-4.el7.x86_64.rpm.html

yum install centos-release-scl-rh

で追加できるって言うんだけどできないんですよね...。amazon linux2 が原因か。

なので手動でこれをリポジトリに追加していく必要があったんだけど、久しぶりにこの手の作業をしたのでめちゃくちゃハマった。
まず、yum-config-manager でリポジトリを追加

sudo yum-config-manager --add-repo http://mirror.centos.org/centos/7/sclo/x86_64/rh/

これでいけるやろと思って

sudo yum install -y postgresql11 postgresql11-server postgresql11-contrib postgresql11-devel

を実行すると

llvm-toolset-7-clang-libs-5.0.1-4.el7.x86_64.rpm の公開鍵がインストールされていません

と出て途中で終了します。

なので設定を修正

sudo vi /etc/yum.repos.d/リポジトリのファイル名(デフォルトだとadd-repoしたURLだと思われる)

で内容をいかに書き換える

[centos-sclo-rh]
name=centos-sclo-rh
baseurl=http://mirror.centos.org/centos/7/sclo/x86_64/rh/
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-SIG-SCLo

gpgkeyの設定が重要でした。

ファイルがない場合は以下の内容をコピーしてtouchコマンド等でファイルを作ったら良いかと思います。 github.com

sudo yum install -y postgresql11 postgresql11-server postgresql11-contrib postgresql11-devel

これでインストールが通るようになりました。

【Rails】PostgreSQLのバイナリ型で画像を扱う

経緯

  • 画像のアップロード機能を追加したい
  • アップロードしたデータはアップロードしたユーザー以外にみられたくない
  • S3などの外部ストレージに保存したくない

これらの条件を考えた時に、バイナリ型でDBに突っ込むのが一番手取り早いと思いこの実装にいたる。
また、アップローダー系のGemは外部ストレージに送る場合でない限りさほどメリットがなさそうだったので、バリデーションも自前で作る。

環境

概要

  • binaryデータとContentTypeをDBに保存
  • Base64エンコードしてインライン画像化する

実装

1.マイグレーションファイルの作成
rails g model Image user:references data:binary content_type:string uploaded_at:datetime
# db/migrate/xxxxxxxxxxxxxxxx_create_images.rb
class CreateImages < ActiveRecord::Migration[6.0]
  def change
    create_table :images do |t|
      t.references :user, null: false, foreign_key: true, type: :uuid
      t.binary :data, null: false, limit: 10.megabyte, comment: 'データ'
      t.string :content_type, null: false, comment: 'ContentType'
      t.datetime :uploaded_at, default: -> { 'CURRENT_TIMESTAMP' }, null: false, comment: 'アップロード日'

      t.timestamps
    end
  end
end

※binaryのデフォルトのファイルサイズが64kbなので、limitでファイルサイズの設定をする必要あり。

2.マイグレーション実行
rails db:migrate
3.テストファイルの追加
# test/models/image_test.rb
require 'test_helper'

class ImageTest < ActiveSupport::TestCase
  include ActionDispatch::TestProcess

  test 'when upload_file size is over 10MB, it is invalid' do
    file = fixture_file_upload('files/invalid_image_1.jpg', 'image/jpeg')
    image = Image.new(upload_file: file)
    image.valid?
    assert_includes image.errors[:data], 'のファイルサイズは10MB以下にしてください'
  end

  test 'when upload_file content_type is not image/png or image/jpeg, it is invalid' do
    file = fixture_file_upload('files/invalid_image_2.pdf', 'application/pdf')
    image = Image.new(upload_file: file)
    image.valid?
    assert_includes image.errors[:content_type], 'はファイルの種類に問題があります'
  end
end

ActionDispatch::TestProcessをincludeすることでfixture_file_uploadを使えるようになる。
これでActionDispatch::Http::UploadedFileを(フォームからファイルアップロードした際に作られるオブジェクト)を擬似的に扱う。

4.バリデーション追加
# app/validators/content_type_validator.rb
class ContentTypeValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return if value.nil?
    return if options[:allow_content_type].include?(value)

    record.errors.add(attribute, 'はファイルの種類に問題があります')
  end
end
# app/validators/size_limit_validator.rb
class SizeLimitValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return if value.nil?
    return if options[:limit] >= value.size

    record.errors.add(attribute, "のファイルサイズは#{convert_size(options[:limit], value)}以下にしてください")
  end

  def convert_size(limit_size, value)
    return convert_kb(limit_size) if 100.kilobyte >= value.size
    return convert_mb(limit_size) if 100.megabyte >= value.size

    convert_gb(limit_size)
  end

  def convert_kb(limit_size)
    "#{(limit_size / (2 ** 10).to_f).round}KB"
  end

  def convert_mb(limit_size)
    "#{(limit_size / (2 ** 20).to_f).round}MB"
  end

  def convert_gb(limit_size)
    "#{(byte_size / (2 ** 30).to_f).round}GB"
  end
end

ContentTypeとファイルサイズのカスタムバリデータを作成する。
モデルに書くよりもカスタムバリデータとして個別に実装した方が見やすいと思うので、こちらに実装。

5.モデルの実装
# app/models/Image.rb
class Image < ApplicationRecord
  ALLOW_CONTENT_TYPE = ['image/jpeg', 'image/png']

  belongs_to :user

  attr_accessor :upload_file

  before_validation -> { self.data = upload_file.read }, if: -> { upload_file }
  before_validation -> { self.content_type = upload_file.content_type }, if: -> { upload_file }

  validates :data, presence: true, size_limit: { limit: 10.megabyte }
  validates :content_type, presence: true, content_type: { allow_content_type: ALLOW_CONTENT_TYPE }
end
6.テスト実行
$ rails test test/model/image_test.rb

Running via Spring preloader in process 57608
Run options: --seed 3934

# Running:

..

Finished in 0.689962s, 2.8987 runs/s, 5.7974 assertions/s.
2 runs, 4 assertions, 0 failures, 0 errors, 0 skips
7.コントローラーの作成
class ImagesController < ApplicationController
  def new
     @image = Image.new
  end

  def create
     @image = Image.new(image_params)
     if @image.save
        # 省略
     else
        # 省略
     end
  end

  private
     def image_params
        params.require(:image).permit(:upload_file)
     end
end
8.ビューの作成(フォーム)
<%= form_with model: @image do |f| %>
   <%= f.label :upload_file %>
   <%= f.file_field :upload_file %>
   <%= f.submit %>
<% end %>
9.画像表示用のメソッド追加
# app/models/Image.rb
class Image < ApplicationRecord
  ALLOW_CONTENT_TYPE = ['image/jpeg', 'image/png']

  belongs_to :user

  attr_accessor :upload_file

  before_validation -> { self.data = upload_file.read }, if: -> { upload_file }
  before_validation -> { self.content_type = upload_file.content_type }, if: -> { upload_file }

  validates :data, presence: true, size_limit: { limit: 10.megabyte }
  validates :content_type, presence: true, content_type: { allow_content_type: ALLOW_CONTENT_TYPE }

  # 追加
  def image_url
    "data:#{content_type};base64,#{Base64.encode64(image)}"
  end
end

画像のインライン用のフォーマットを作成する。
バイナリデータをBase64エンコードすることで作成可能。

10.ビューの作成(画像表示)
<%= image_tag @image.image_url %>
おまけ fixtures
# test/fixtures/images.yml
image_1:
  user: user_1
  image: !binary <%= Base64.strict_encode64(File.binread(Rails.root.join('test', 'fixtures', 'files', 'valid_image_1.jpg').to_s)) %>
  content_type: image/jpeg
  uploaded_at: 2020-05-25 15:07:23

バイナリデータ突っ込むの地味に苦戦した...

後書き

DBにバイナリデータを保存する形での画像アップロードの実装方法についてでした。
send_dataで画像のエンドポイントを作るパターンもありましたが、手間だったのでdata URIを直接メソッドで実装してしみました。
インライン画像にする場合のメリットとしては

  • httpリクエストが減る

デメリットとしては

  • データ容量が増える(約40%増)
  • キャッシュされない

あたりがあります。

今回はデータへのアクセス制限をRails内で完結したいということでパフォーマンス目的での実装じゃないのでその辺りはあまり考慮していません。
ただ、ライブラリ等無しでも案外簡単にやれたので、さすがRailsという感じ。