| @技術/プログラミング

lokka/lokka 、 Pull Request を出す度に Hound CI のチェックが走って bot にコードレビューでぼこぼこにされるので、この bot を黙らせるべくガチャガチャやってた。 Hound CI のチェックルールは Rubocop に準拠しているようで、 2011 年からある Rack アプリを Rubocop のチェックにかけるのは面白かった。

Lokka 、意外と Hacky なコードが多く、条件式内での代入とか、ヨーダ記法とか、後置の until とか、スコープが広い一文字変数とか、めっちゃ長いメソッドとか、 if 文のネスト、代入したものの使われてない変数なんかを修正した。 method_missing はカスタムフィールドを定義できるという Lokka の仕様上根絶できなかったけど、 .rubocop.yml に最低限の除外ルールを追加して Rubocop のチェックはパスするようにできた。

Lokka 、 ORM が ActiveRecord じゃないことが問題だと思ってたけど、真の問題は lib/lokka/helpers/{helers,render_helper}.rb にビジネスロジックが詰め込まれてることだと思った。しかもこのあたりのコードの可読性がよくなく、触るのが怖い感じの複雑なやつが多い。この辺のコードをもうちょいクラス化して分割し、ユニットテストも手厚くしていかないと ORM を変えても F/E を今風にしてもウェブアプリケーションとして生存していくことは厳しいと思う。

前に進んでいくためにも Rubocop のチェックを入れる&パスさせるのはプラスになると思う。 頑張ってメンテしていくぞ。

追記

この辺のコードをもうちょいクラス化して分割し

と書いたけど、 Rails と違って手軽にサクッと作れるのが Sinatra の良い所なわけではあって、仕事で作る Rails アプリのノリでクラスやファイルを分割したりするのは違うのかもしれないと思った。 Rails で作られたオープソースの CMS やブログツールに長生きし続けるものがないのも、 Rails の場合、個人が偶発的に始めてメンテ出来るようなものになりにくいからかも知れない。

とはいえヘルパーがビジネスロジックを所持しているのはテスタビリティやメンテナビリティが良くないので Lokka と心中する覚悟でやっていくぞ!!!、!

| @技術/プログラミング

cho45 さんの以下の記事を参考に関連記事を表示するようにしてみた。

ほとんど cho45 さんの記事に書いてある SQL を実行しているだけだけど、関連記事の表示用に Lokka 側に Similarity というモデルを追加して、以下のようなスキーマにしてる。

similar-entries-erd.png

Similarity テーブルの更新は cho45 さんの記事にあるように SQLite で行った計算の結果を反映することで行う。以下のような Rake タスクを定義した。

desc "Detect and update similar entries"
task similar_entries: %i[similar_entries:extract_term similar_entries:vector_normalize similar_entries:export]

namespace :similar_entries do
  require 'sqlite3'
  desc "Extract term"
  task :extract_term do
    require 'natto'
    nm = Natto::MeCab.new
    db = SQLite3::Database.new('db/tfidf.sqlite3')
    create_table_sql =<<~SQL
      DROP TABLE IF EXISTS tfidf;
      CREATE TABLE tfidf (
        `id` INTEGER PRIMARY KEY,
        `term` TEXT NOT NULL,
        `entry_id` INTEGER NOT NULL,
        `term_count` INTEGER NOT NULL DEFAULT 0, -- エントリ内でのターム出現回数
        `tfidf` FLOAT NOT NULL DEFAULT 0, -- 正規化前の TF-IDF
        `tfidf_n` FLOAT NOT NULL DEFAULT 0 -- ベクトル正規化した TF-IDF
      );
      CREATE UNIQUE INDEX index_tf_term ON tfidf (`term`, `entry_id`);
      CREATE INDEX index_tf_entry_id ON tfidf (`entry_id`);
    SQL
    db.execute_batch(create_table_sql)

    entries = Entry.published.all(fields: [:id, :body])
    entry_frequencies = {}
    entries.each do |entry|
      words = []
      body_cleansed = entry.body.
        gsub(/<.+?>/, '').
        gsub(/!?\[.+?\)/, '').
        gsub(/(```|<code>).+?(```|<\/code>)/m, '')
      begin
        nm.parse(body_cleansed) do |n|
          next if !n.feature.match(/名詞/)
          next if n.feature.match(/(サ変接続|数)/)
          next if n.surface.match(/\A([a-z][0-9]|\p{hiragana}|\p{katakana})\Z/i)
          next if %w[これ こと とき よう そう やつ とこ ところ 用 もの はず みたい たち いま 後 確か 中 気 方 頃 上 先 点 前 一 内 lt gt ここ なか どこ まま わけ ため 的 それ あと].include?(n.surface)
          words << n.surface
        end
      rescue ArgumentError
        next
      end
      frequency = words.inject(Hash.new(0)) {|sum, word| sum[word] += 1; sum }
      entry_frequencies[entry.id] = frequency
    end
    entry_frequencies.each do |entry_id, frequency|
      frequency.each do |word, count|
        db.execute("INSERT INTO tfidf (`term`, `entry_id`, `term_count`) VALUES (?, ?, ?)", [word, entry_id, count])
      end
    end
  end

  desc "Vector Normalize"
  task :vector_normalize do
    db = SQLite3::Database.new('db/tfidf.sqlite3')

    load_extension_sql =<<~SQL
      -- SQRT や LOG を使いたいので
      SELECT load_extension('/usr/local/Cellar/sqlite/3.21.0/lib/libsqlitefunctions.dylib');
    SQL
    db.enable_load_extension(true)
    db.execute(load_extension_sql)

    update_tfidf_column_sql = <<~SQL
      -- エントリ数をカウントしておきます
      -- SQLite には変数がないので一時テーブルにいれます
      CREATE TEMPORARY TABLE entry_total AS
          SELECT CAST(COUNT(DISTINCT entry_id) AS REAL) AS value FROM tfidf;

      -- ワード(ターム)が出てくるエントリ数を数えておきます
      -- term と entry_id でユニークなテーブルなのでこれでエントリ数になります
      CREATE TEMPORARY TABLE term_counts AS
          SELECT term, CAST(COUNT(*) AS REAL) AS cnt FROM tfidf GROUP BY term;
      CREATE INDEX temp.term_counts_term ON term_counts (term);

      -- エントリごとの合計ワード数を数えておきます
      CREATE TEMPORARY TABLE entry_term_counts AS
          SELECT entry_id, LOG(CAST(SUM(term_count) AS REAL)) AS cnt FROM tfidf GROUP BY entry_id;
      CREATE INDEX temp.entry_term_counts_entry_id ON entry_term_counts (entry_id);

      -- TF-IDF を計算して埋めます
      -- ここまでで作った一時テーブルからひいて計算しています。
      UPDATE tfidf SET tfidf = IFNULL(
          -- tf (normalized with Harman method)
          (
              LOG(CAST(term_count AS REAL) + 1) -- term_count in an entry
              /
              (SELECT cnt FROM entry_term_counts WHERE entry_term_counts.entry_id = tfidf.entry_id) -- total term count in an entry
          )
          *
          -- idf (normalized with Sparck Jones method)
          (1 + LOG(
              (SELECT value FROM entry_total) -- total
              /
              (SELECT cnt FROM term_counts WHERE term_counts.term = tfidf.term) -- term entry count
          ))
      , 0.0);
    SQL
    db.execute_batch(update_tfidf_column_sql)

    vector_normalize_sql = <<~SQL
      -- エントリごとのTF-IDFのベクトルの大きさを求めておきます
      CREATE TEMPORARY TABLE tfidf_size AS
          SELECT entry_id, SQRT(SUM(tfidf * tfidf)) AS size FROM tfidf
          GROUP BY entry_id;
      CREATE INDEX temp.tfidf_size_entry_id ON tfidf_size (entry_id);

      -- 計算済みの TF-IDF をベクトルの大きさで割って正規化します
      UPDATE tfidf SET tfidf_n = IFNULL(tfidf / (SELECT size FROM tfidf_size WHERE entry_id = tfidf.entry_id), 0.0);
    SQL
    db.execute_batch(vector_normalize_sql)
  end

  desc "Export calculation result to MySQL"
  task :export do
    db = SQLite3::Database.new('db/tfidf.sqlite3')
    create_similar_candidate_sql = <<~SQL
      DROP TABLE IF EXISTS similar_candidate;
      DROP INDEX IF EXISTS index_sc_parent_id;
      DROP INDEX IF EXISTS index_sc_entry_id;
      DROP INDEX IF EXISTS index_sc_cnt;
      CREATE TABLE similar_candidate (
        `id` INTEGER PRIMARY KEY,
        `parent_id` INTEGER NOT NULL,
        `entry_id` INTEGER NOT NULL,
        `cnt` INTEGER NOT NULL DEFAULT 0
      );
      CREATE INDEX index_sc_parent_id ON similar_candidate (parent_id);
      CREATE INDEX index_sc_entry_id ON similar_candidate (entry_id);
      CREATE INDEX index_sc_cnt ON similar_candidate (cnt);
    SQL
    db.execute_batch(create_similar_candidate_sql)

    extract_similar_entries_sql = <<~SQL
      -- 類似していそうなエントリを共通語ベースでまず100エントリほど出します
      INSERT INTO similar_candidate (`parent_id`, `entry_id`, `cnt`)
          SELECT ? as parent_id, entry_id, COUNT(*) as cnt FROM tfidf
          WHERE
              entry_id <> ? AND
              term IN (
                  SELECT term FROM tfidf WHERE entry_id = ?
                  ORDER BY tfidf DESC
                  LIMIT 50
              )
          GROUP BY entry_id
          HAVING cnt > 3
          ORDER BY cnt DESC
          LIMIT 100;
    SQL

    search_similar_entries_sql = <<~SQL
      -- 該当する100件に対してスコアを計算してソートします
      SELECT
          ? AS entry_id,
          entry_id AS similar_entry_id,
          SUM(a.tfidf_n * b.tfidf_n) AS score
      FROM (
          (SELECT term, tfidf_n FROM tfidf WHERE entry_id = ? ORDER BY tfidf DESC LIMIT 50) as a
          INNER JOIN
          (SELECT entry_id, term, tfidf_n FROM tfidf WHERE entry_id IN (SELECT entry_id FROM similar_candidate WHERE parent_id = ?)) as b
          ON
          a.term = b.term
      )
      WHERE similar_entry_id <> ?
      GROUP BY entry_id
      ORDER BY score DESC
      LIMIT 10;
    SQL

    results = {}
    Entry.published.all(fields: [:id]).each do |entry|
      db.execute(extract_similar_entries_sql, [entry.id, entry.id, entry.id])
      db.results_as_hash = true
      similarities = db.execute(search_similar_entries_sql, [entry.id, entry.id, entry.id, entry.id])
      results[entry.id] = similarities
    end

    Similarity.destroy

    results.each do |entry_id, similarities|
      if similarities.present?
        similarities.each do |s|
          conditions = { entry_id: s["entry_id"], similar_entry_id: s["similar_entry_id"] }
          similarity = Similarity.new(conditions)
          similarity.score = s["score"]
          similarity.save
        end
      end
    end
  end
end

やってることとしては、全エントリーを拾ってきて本文を MeCab で品詞分解して名詞だけを取り出し記事ごとの term 一覧を作り、そこから TF-IDF を求めてベクトル正規化し、最後に関連していそうなエントリを探し出して similarities テーブル(こちらは SQLite のテーブルではない)を更新している。詳しいアルゴリズムはバカなのでわからないが、 cho45 さんが書いているやり方を Lokka のスキーマに素直に適用した感じ。

結構この処理は遅いので parallel.gem を使って高速化できないか試してみたが、スレッドによる並行処理ではあまり速くできなかった。 4 コアある CPU のうち一つが 100% で処理を実行してもまだ 3 コアは余っている。プロセスを増やして並列処理するのがよさそうだが、分散をプロセスレベルで行おうとすると MySQL server has gone というエラーが出る。 DataMapper が MySQL とのコネクションをロストするようである。 ActiveRecord であれば reconnect するだとか回避方法があるようなのだけど DataMapper は情報が少なく、対応方法が見つけられなかったので一旦並列処理はあきらめた。

何回か動かしてみて大体正しく関連記事を表示できてそうなのでさくらの VPS で稼働させたいところなのだけど、関連記事の更新はいまのところ手動でやっている。本番 DB の entries テーブルを dump してきて Mac に取り込み、 similarities テーブルを更新して今度はローカルで similarities テーブルを dump して本番にインポートするという手順をとっている。

これにはいろいろ理由があって、一つには利用している mecab-ipadic-neologd (新語にも対応している MeCab の辞書)が空きメモリ 1.5GB 以上でないとインストールできずさくらの VPS にインストールできなかったから。もう一つには cho45 さんのブログにもあるけど SQLite で LOGSQRT を使うためには libsqlitefunction.so の読み込みが必要で、 load_extension() できるようにしないといけないが、そのためには sqlite3 をソースからビルドする必要があり若干面倒だった( Mac では Homebrew で sqlite を入れた)。

関連記事の更新は自分が記事を書いたときにしか発生しないのでいまの手動運用でもまぁ問題ないが、このブログは Docker でも動くようにしてあるので Docker イメージを作ればさくら VPS でも問題なく動かせそうな気はする。正月休みにでもチャレンジしたい。

感想

関連記事表示、結構面白くてちゃんと関連性の高いエントリーが表示される。例えば人吉に SL に乗り行った記事の関連記事にはちゃんと山口に SL に乗りに行ったときの記事 が表示される。いまのところ Google Adsense の関連コンテンツよりも精度が高いようである。

無限に自分の黒歴史を掘り返すことができるのでおすすめです。

| @WWW

DSC_4054

以下の文章は正月に「2017 年の Lokka へのコントリビュート目標」というタイトルで書いたまま下書きになってたものです。もう 2017 年も終わりそうだけど公開しておきます。


RubyKaigi 2016 で komagata さんと Lokka についてしゃべったのだけど、 Lokka の開発も停滞してしまっていて(ブログとしては大体の機能そろっていて完成しているとも言える)、 "lokka" でググると JavaScript 製の GraphQL クライアントがヒットして、 GitHub のスター数ではこっちの方が多かったりする。

このままで紛らわしいからその名前こっちに寄越せ、とか言われかねない。もっと Lokka を盛り上げていきたい。

なので一度仕切り直しで今後の方針とかをどうするかを決めた方がよいのではないかと思っている。まずは Issue の棚卸が必要ではないかと思う。

加えて自分でも結構いろいろ lokka-plugin を作っているのだけど、個人のリポジトリに適当に上げてあるだけだとユーザーとしては利用しづらいと思う。そこで GitHub の Lokka org に lokka-plugins というリポジトリを作って、とりあえずそこにコードを集約するようにしたらどうかと思う。前に Rebuild で Jenkins の川口さんが話していたやり方。

OSS 、コードが素晴らしいことも大事だけど、利用しやすくないとユーザーに使ってもらえなくて盛り上がって行かないと思う。プラグインを使いやすい、作りやすいようにして裾野を広げていきたい。

ほかにも手軽に使ってもらうためにはいくつかやらないといけないことあると思う。 gem 化は是非とも必要だと思う。 Lokka 動かすために本体のソースコードごと管理しなければいけないのはやっぱり結構敷居が高いと思う。ディレクトリ作って Gemfile に gem 'lokka' と書いて bundle install し、 theme と db config さえ置けば動くようになるのがよさそう。ファイルをアップロードする仕組みについてもどうにかしたい。 Heroku 運用が前提のためファイルシステムを使うことができず本体にそういう仕組みがなかったのだと思う。 Amazon S3 や Google Cloud Storage 、 Dropbox 使えるようにするとかやり方を考えたい。

これらを推し進めるために、以下のことをやりたい。

  1. Lokka 開発者ミートアップを開催
  2. Slack にチームを作ってコミュニケーションできるようにする

1 は RubyKaigi のときに komagata さんに提案したけどそのあと動けてなかった。とりあえずは自分が komagta さんのところに会いに行くだけになるかもだけど、 Issue の棚卸と今後の方向性を固める会をやった方がよさげだと思う。仕事でも OSS でも意思や目的を共有しないと Project は先に進まないと思う。

2 に関しては Lokka は Lingr でコミュニケーションしていたが、いまは皆さん Slack を仕事で使っていると思うし Slack の方がコミュニケーションしやすいはず。というわけで Slack チームを立ち上げたい。1

という感じで 2017 年も Lokka の開発に関与していきたい。


転職して東京に行く機会がなくなり、結局 Lokka 開発者会議をやることはできなかった。プラグインの gem 化はおろかリポジトリへの集約も DataMapper => ActiveRecord への移行も手を付けられなかったが、それ以外で Lokka の改善は結構頑張ったと思う。

今年やった Lokka の改善

  • パーマリンク生成高速化 https://github.com/lokka/lokka/pull/220
    • Lokka はどのフォーマットで permalink を生成するかを DB に保存している
      • リンクを一つ生成する度に permalink のフォーマットを調べるためのクエリが流れる
    • ビュー内で各記事へのリンクは多数
      • めっちゃクエリが流れる 😱
    • request_store.gem を使ってリクエストごとに一回だけクエリが流れるようにする
      • テーマにもよるが記事一覧でパーマリンク生成のために 10 回くらい DB アクセスが発生していたところは 1/10 になる
      • 管理画面の記事一覧では 100 記事表示しているので 1/100 になる(爆速になった!!!、!)
  • 管理画面をスマートフォン最適化 https://github.com/lokka/lokka/pull/225
    • スマートフォンから管理画面が見やすくなるよう CSS を修正
    • 寝床からでもブログ書けるようになった!!!、!
  • ファイルアップロード機能を追加 https://github.com/lokka/lokka/pull/226
    • S3 にバケットを作ってもらいさえすれば GitHub のようにドラッグ&ドロップで画像をアップロード出来るように
    • めっちゃお手軽お気軽に画像アップロードできるようになって最高便利!!!、!
  • MySQL 絵文字対応 https://github.com/lokka/lokka/pull/230 😃
    • 今の時代、絵文字が使えないのはつらい 😅
  • Ruby 2.4 対応 https://github.com/lokka/lokka/pull/231
    • Lokka で Sinatra 、 DataMapper の次に依存度が高い PadrinoHelper のバージョンを上げることに成功(めっちゃ大変だった!!!、!)
      • 自分としては Rails 3 を Rails 4 に上げるくらいの働きをしたと思ってる😎

locale が i18n.gem がサポートしてるやつじゃないと 500 エラーになるという問題があって、こちらも自分のブログでは直してあるので修正する Pull Request を出したい。

P_BLOG のときもそうだったけど、どうも自分はユーザーの少なくなってきた CMS を細々と改造して使っていくのが好きみたいだ。このブログの開発・運用から学ぶことも多くて仕事にも役立っているので、まだしばらくは使い続けていきたいと思う。 Lokka は永遠に不滅です。


  1. 調べてみたらすでに lokka.slack.com は存在するみたいなんだけどこれって Lokka for CMS のやつですかね? 

| @技術/プログラミング

ブログに人気記事を表示するようにしてみた。やり方はめっちゃ雑で、 Nginx の access_log を集計して Bot や Crawler 、 RSS Reader からのアクセス、画像や CSS 、 JS ファイルへのアクセスを除外してアクセス数を集計して結果をテキストファイルに出力し、 Ruby で parse してフッターに表示してる。

仕組み

こんな感じのシェルスクリプトを置いて cron で実行してる。

#!/bin/bash

zcat -f /path/to/access.log* \
  | grep -vE 'useragent:.+?(bot|Feed\s?Fetcher|Crawler|Fastladder|Feed|Ruby|Aol\sReader|proximic|Hatena\sAntenna|Mediapartners-Google|subscribe)' \ # bot や Crawler を除外
  | cut -f5 | sed -e 's/request_uri://' \ # request_path だけ抜き出し
  | grep -vE '(favicon\.ico|index\.atom|\.js|\.json|\.css|\.jpe?g|\.png|\.gif|\.txt|\.php|\/admin|^\-$|^\/$)' \ # HTML 以外へのリクエストを除外
  | sort | uniq -c | sort -nr | head -100 | sed -r 's/^[ \t]+//g' \ # 集計して上位 100 件だけを得る
  | tee /path/to/public/access-ranking.txt # テキストファイルに書き出し

zcat -f しているのは gzip 済みのログファイルも cat したいため。このやり方だと現存するログファイルからしか調べられないので logrotate で設定している期間(自分の場合は 30 日)の集計しかできない。またサーバーを複数並べて運用しているようなアプリケーションではアクセスログがばらけるのでこんな雑なやり方は使えない。

Nginx のログのフォーマットは LTSV にしているので grep でのフィルタリングがやりやすい。まず User-Agent で bot っぽいアクセスを除外したあと、ログから request_uri のフィールドだけを切り出し、静的ファイルなどへのアクセスを除外したあと sort -> uniq -c -> sort -nr してる。

Ruby ( Lokka ) の方では以下のようなコードを書いて access-ranking.txt を読み込んでる。これをやらないと記事のタイトル表示やリンクが生成できないため。

class Entry
  class << self
    def popular(count = 5)
      access_ranking = File.open(File.join(Lokka.root, 'public', 'access-ranking.txt'))
      slugs = {}
      access_ranking.each.with_index(1) do |line, index|
        access_count, path = *line.split(" ")
        slug = path.split("/")[-1]
        slugs[access_count] = slug
        break if index == count
      end
      all(slug: slugs.values, limit: count).sort_by {|entry| slugs.values.index(entry.slug) }
    end
  end
end

フッターは適度にキャッシュしているのでスピードはそんなに遅くならない。

感想

アクセスランキングを表示してみて、意外と Twitter やはてブでバズった記事へのアクセスは継続的には多くないことがわかる。最近だと ARC'TERYX や SIERRA DESIGNS のパーカーの記事が人気があるようだ。これはおそらく寒くなってきててそういうキーワードで検索してたどり着く人が多いのだろう。 GarageBand でのアナログレコード録音の方法は前から人気ある。はてブとかは大して付いてないが、 Yahoo! 知恵袋や 2ch の過去記事・まとめサイトからのアクセスが多いようである。謎なのが痔ろうの記事へのアクセス数の多さ。痔ろうの症状・治療方法を結構詳細に書いたので Google 先生が良記事判定してくれているのかも知れない。家の記事ははてブでバズって 2000 ブックマーク以上付いたが、それでもやっと 5 位という感じ。バズっても短期的なアクセスしか得ることができず(人の噂も 75 日!!!、!)、長期的に細々とトラフィックを集めるためには特定の属性の人にだけ響く詳細な記事を書くのがよいのかもしれない。

| @技術/プログラミング

CircleCI.png

もうすぐ Ruby 2.5 が出そうだけど、このブログの Ruby のバージョンを 2.4 に上げた。

Ruby 2.4 がリリースされたあとすぐに Lokka を Ruby 2.4 に対応させようと頑張ってみたのだけど、 Ruby 2.4 に上げるためには ActiveSupport のバージョンアップが必要だった。しかし padrino-helper 0.11.4.1 が古いバージョンの ActiveSupport に依存しており、 Ruby 2.4 に対応した ActiveSupport を利用するためには padrino-helper をバージョンアップする必要があった。 padrino-helper をバージョンアップしてみると 0.12 から大幅な変更が入ったようで、ビューの表示がぐちゃぐちゃになった。 layout が適用されず中身の partial ビューだけ表示されたり、 HTML が過剰にエスケープされたり。 padrino-helper は 0.11 時代にあった脆弱性を修正して標準ですべての文字列出力を escape するようになったっぽい。しかし Lokka の方がそれに対応できておらずビューがぐちゃぐちゃになっているようだった。

Lokka はヘルパーメソッドやモデルで HTML を文字列から生成しているところがあって、そういうところをこまめに html_safe していったところ余分なエスケープは解消されていった。利用している gem でも文字列から HTML タグを生成しているところがあったので、そういうところもつぶさに調べていって無心で html_safe したり mark_safe していった。

とりあえずこのブログを Ruby 2.4.2 でしばらく運用してみて問題なかったら Lokka 本体の方に Pull Request を出すことにしよう。

あと今回ついでに CircleCI でテストしてテストが通ったら勝手に Production に deploy されるようにしておいた。 CircleCI ほんと便利。

追記 2017-12-03

Pull Request 出しておきました。

| @ブログ

このブログ( Lokka )の DB は MySQL を使っている。 MySQL のバージョンは 5.7 なので、 column encoding を utf8mb4 、 collation を utf8mb4_general_ci とかにしておけば emoji を使えるかと思ってたけど使えなかった。長らく DataMapper が 対応してないかと思ってたけどリポジトリを覗いてみたら随分前に対応されてた。どうも Lokka 側に問題があるっぽい。

Lokka の DB 接続設定は dsn を URL として記載するようになっている。 DataMapper は ActiveRecord のように Hash オブジェクトを DB 接続の引数に渡せるので、 encoding: 'UTF-8-MB4' をオプションとして渡せるようにしてやればよいっぽいが、いまの Lokka のコードでは encoding オプションを指定できないのでこれではダメっぽい。

ちょっと Lokka 側のコードをいじって encoding オプションを渡すようにしてみたところ無事 emoji が使えるようになったので lokka/lokka に Pull Request 出しておこう ⌨️

| @技術/プログラミング

ブログの管理画面にファイルアップロード機能をつけてみた。 GitHub の Issue みたいにドラッグアンドドロップでアップロードしてくれる。こんな感じ。

file upload demo.gif

いまのところ埋め込みフォーマットは Markdown にしか対応してない。

Lokka は heroku とか PaaS を使うことを前提に作られているのでファイルをアップロードする機能が提供されてこなかった( heroku は永続的なファイル書き込みができない)。 heroku で動かしている人でも使えるように Amazon S3 に上げるようにしてみた。 AWS のアカウント作成とかが必要なのでレンタルサーバーに設置してある WordPress にファイルアップロードするのに比べたら敷居が高いけど、設定するのは1回だけだし全くアップロードする手立てがなかったこれまでに比べたら劇的に快適になるはず。自分で使ってみたけどめっちゃ便利になった。

ファイルアップロード、ちゃんと作るなら Paperclip みたいにファイルのチェックを厳密に行なわないと危ないと思うけど、管理画面から上げるの前提なのでチェックなしで雑にアップロードできるようになっている。 AWS の token 系の設定画面を作ったら Lokka 本体に Pull Request 出します。

なお一つ前の記事でアプリケーションサーバーを puma に変更 - portal shit!というのを書いたけど、このファイルアップロード機能がうまく動かなかったので unicorn を捨てたのだった( pow で動かしている環境や bundle exec rackup して WEBRick で起動しているサーバーではちゃんとアップロードできる)。なぜか unicorn で multipart/form-data な POST リクエストを rack アプリケーションに適切に渡すことができず EOFError が発生してしまう。

仕事で unicorn で動いてるサーバーにファイルアップロードする仕組み入れたことは何度かあるけどこんなエラーになったことはなかったのになぁ。謎い。