見出し画像

【メモ】esa→GROWI移行備忘録

※雑な作業メモを頼りに思い出しながら書いてるので、所々怪しい箇所があります。ご注意ください。

はじめに

GROWI.cloudというマークダウン記法でのドキュメント管理を行うWikiサービスがあります。
元々類似サービスであるesaは利用していたんですが、経費の問題やユーザーとかグループごとに記事の閲覧権限かけたいよねという要望から、代替・移行先となるサービスを探し求めていたところ、こちらのGROWIというアプリの存在を知りました。
GROWI.cloud自体はよくあるサブスクリプション型のクラウドサービスですが、この大元(という表現が適切かは分からないけど)であるGROWIはオープンソースであり、リポジトリが公開されているのでオンプレで構築することも可能です。
今回は、このOSSである方のGROWIを試しに自前のサーバーに立ててみました。
作業環境であるOSは CentOS 7 です。

事前準備&インストール

何はともあれ、まずはドキュメントに目を通しましょう。ちょいちょいTBDと書かれてるところがありますが、インストールする分には問題ないと思います。
こちらの「管理者ガイド」ページにインストール関連のことが書いてあります。そしてインストール方法の中にはdocker-composeの文字が!
「よっしゃこれはdocker使うしかないな!」てことで以後dockerで動かす前提のお話となります。ご了承を。
docker、docker-compose、gitとか必要になるライブラリの導入については以前書いたPassbolt導入記事辺りを参照してもらえれば。

docker周りのライブラリが用意できたら、growi-docker-composeのリポジトリからソースコードをクローンして来ましょう。

docker-compose upでコンテナを立ち上げる

クローンして来たディレクトリに移動して、あとはdocker-compose upすればGROWIが立ち上がります。でもその前にdocker-compose.ymlの設定を見ておきましょう。
ドキュメントにもありますが、デフォルトの設定ではlocalhost以外からアクセスすることが出来なくなってます。リモートのサーバーにGROWIを立ち上げて、手元のPCからアクセスしたいとかそういう場合はdocker-compose.yml中のポート番号の記述を変更することをお忘れなく。
あとはデフォルトだとファイルのアップロード先がAWSになってるんですが、ローカルストレージにアップ先を変えることも可能です。今回はただのテストなので、ローカルストレージを利用する方向で。『FILE_UPLOAD=local』の部分をコメントアウトして有効化するだけでOK。

で、webサーバーの設定を良しなにしてやって、設定したドメインなりIPアドレスなりにアクセスするとGROWIが動いているのが確認できると思います。一先ずGROWIというものは用意できました。
が、まだ試したいことは残ってます。今回取り組んだ課題としては、大きく分けて以下の二つ。

・esaからデータをGROWIへと移行する
・HackMDと連携する

一つ目の「esaからデータをGROWIへと移行する」は、まあデータ移行先として考えてるので、必要な要件ですね。GROWIはちゃんとesaから記事データを取り込めるようインポート機能を有していますが、微妙に手が回っていないところもあるため、その辺の補足メモを書いていきたいと思います。

二つ目の「HackMDと連携する」ですが、これは要するに同時多人数編集機能を実現させるために必要なツールを立ち上げて、連携させるということですね。esaの方でも同時多人数編集機能は比較的最近リリースされてましたが、それと似たような感じです。

上記二項目と、あと余談なんかをつらづらと書いていきます。

esaからデータをGROWIへと移行する

esaからGROWIへのデータ移行については、こちらの記事を参考にさせていただきました。要はesa側でトークンを発行して、それをGROWIに登録してインポートボタンをポチるといい感じに記事がインポートされるということですね。何事も無ければ。
以下、何事かがあった記録です。

記事データがインポートされない

これは幾つか複合的な理由でインポート出来なかったっぽいので、ざっと箇条書きにしてみました。断定は出来ないですが。

・記事データが大きすぎる
・esa側のAPIのリクエスト数制限に引っかかる
・恐らくesa側のバグで1万件以上前の記事を取得できない

まず「記事データが大きすぎる」件について。
インポートボタンを押して一定時間経過後、『Error occurred in importing pages form esa.io Network Error』と表示され、正常に処理が完了されません。でも部分的にインポートに成功している記事もあります。
どういうこっちゃと思いデータインポートに関わるソースコード部分を読んでみました。
どうやらimporter.jsを見る限り、1ページにつき100件の記事をesaのAPIに投げて取得。取得した記事データをGROWIの形式に変換しcreate。それらの処理が終わったときに、次のページが存在していたら上記の記事取得関数を再び呼び出す……という流れのようです。
そこから、「何かしら画像の多い記事とかソースコードの長い記事とかでjsonファイルが肥大化して、APIとして返せるサイズを超過してる可能性がある」と予想。デフォルトではper_pageが100となってるので、これの数を小さくすればレスポンスで返ってくるbodyのサイズを削減できそうです。
そしてここから超泥臭い対応を取ることにしました。ざっくり言うと以下の通り。

1.GROWIコンテナを立ち上げる
2.コンテナ内にアクセスして、importer.jsのper_pageの数を2〜30にする
3.GROWIコンテナを再起動してソースコードの修正を反映させる
4.API制限にかかるまでデータインポートする
5.importer.jsのfirstPageを「大体この辺の記事までインポートできてるだろ」と当たりを付けて調整
6.3の手順に戻って以下繰り返し

自分で書いてても、もうちょっとどうにかならんかったんかと言いたくなりますね。でも他に方法思いつかなかったからしゃーない。

補足として、コンテナにアクセスする方法も書いておきますね。
まず『docker ps』でコンテナ名を確認しておきましょう。たぶんgrowi_app_1と表示されると思います。
その状態で『docker exec -i -t growi_app_1 bash』と入力すれば、bashシェルでアクセスできる……はずなんですが、どうもbashでアクセスできないケースがある模様。そういう時は、『docker exec -i -t growi_app_1 sh』とshシェルでアクセスするようにしたらいけると思います。中に入れたらimporter.jsの該当箇所を編集しましょう。
あと試してないんですが、『docker-compose exec app bash』みたいにdocker-compose execを使えばオプションを付けなくても指定したシェルでアクセスできるっぽいです。こっちの方が分かりやすいかな。
bashとshの違いについては似て非なるものというか、部分的に動作の差異があるようです。基幹部分が異なるのでbashの方が多機能だけど移植性で言えばshみたいな。ざっくり調べただけなので違うかも。

ともあれこれで一つ目の課題はクリア。次にいきましょう。

次は「esa側のAPIのリクエスト数制限に引っかかる」問題。
esaのAPIドキュメントにもありますが、15分間に75回のリクエストまで受け付けるようになっているそうです。この制限を超えると、インポート時に『Error occurred in importing pages from esa.io -Error: error in page 75: Error: Too Many Requests』って感じのエラーが表示されます。
なのでこの制限を守りつつ前述の記事インポートのためのソース修正作業をする必要があるんですが、15分おきにこれを行うのはなかなかの苦行でした。まあこれも仕方がないことなんですが。

そして「恐らくesa側のバグで1万件以上前の記事を取得できない」問題。
これはesaでposts一覧を遡っていくと、一定ページ数以降のページを開こうとすると 500 Internal Server Error を示すエラー画面が表示されたことからの推測ですね。
つまりデフォルトの設定では1万件を超える記事はインポートできません。が、ソートの設定次第ではもうちょっとインポートできます。
APIドキュメントにはsortとorderをパラメータとして受け付けているので、importer.jsでAPIにパラメータを投げるところをちょっと弄ってやりましょう。
例えば以下のように『order: 'asc'、sort: 'created' 』としてやると記事の作成日時が古い順に取得できます。これでいけるところまで記事をインポートしてやって、限界になったらorderをdescに変えて新しい順にインポートするとかで騙し騙し。

  const importPostsFromEsa = (pageNum, user, errors) => {
   return new Promise((resolve, reject) => {
     esaClient.api.posts({ page: pageNum, per_page: 100, order: 'asc', sort: 'created' }, async(err, res) => {
       const nextPage = res.body.next_page;
       const postsReceived = res.body.posts;
       if (err) {
         reject(new Error(`error in page ${pageNum}: ${err}`));
       }
       const data = convertEsaDataForGrowi(postsReceived, user);
       const newErrors = await createGrowiPages(data);
       if (nextPage) {
         return resolve(importPostsFromEsa(nextPage, user, errors.concat(newErrors)));
       }
       resolve(errors.concat(newErrors));
     });
   });
 };

というように悪戦苦闘しましたが、ようやく記事はインポートできました。
ですがこれはあくまで記事データを移せただけで、記事に紐づいてる画像はインポートできていません。
なのでこちらの対応も行っていきます。

画像データをインポートする

画像データの移行についてはこちらの記事を参考にさせていただきました。なんと有り難いことにgemを用意してくださっています。感謝。
基本的には記事の手順に沿っていくだけで大丈夫です。gemを扱うのでRubyの実行環境さえ用意すればOKですね。
あとはまあGROWI_ACCESS_TOKENの環境変数に登録するトークンはGROWIで発行したやつを使用する、ぐらいですかね。esaのトークンと間違えないように。

以上で記事データの移行は完了しました。長い!!思い出しながら書く分量じゃない!!!
過ぎ去った時間は振り返らず、次のコーナーにいきましょう。同時多人数編集機能の有効化、即ちHackMDとの連携です。

HackMDと連携する

HackMDと連携するためのドキュメントはこちら。こちらHackMDのコンテナを立てるためのdocker-compose.override.ymlも用意してもらっているので、基本的にはドキュメントの流れを沿っていくだけで大丈夫です。
ですが環境によってはHackMDやDBのポートがローカルのポートと競合してたりするので、その辺は適宜対応しましょう。自分はローカルでMariaDBを動かしていたので、ポート変えてます。
若干古いメモなので間違ってる可能性がありますが、自分はこんな感じに設定してました。使用してるwebサーバーはnginxです。ご参考になれば。

growi.conf

# GROWI本体の設定
server {
 listen 80;
 server_name growi.example.com; #自分のGROWIアプリのドメイン指定
 location / {
   proxy_pass http://172.17.0.1:3000; #docker0のネットワークブリッジにポート指定してアクセス
   proxy_pass_request_headers on;
   proxy_redirect off;
   proxy_set_header Host $http_host;
   proxy_set_header X-Real-IP $remote_addr;
   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 }
 access_log /var/log/nginx/growi_proxy_access_log;
 error_log  /var/log/nginx/growi_proxy_error_log;
}

# HackMDの設定
server {
 listen 80;
 server_name 123.456.789; #GROWIアプリが動いてるIPアドレス指定(本当はドメイン取った方がいいんだろうけど)
 location / {
   proxy_pass http://localhost:3100;
 }
}

docker-compose.yml

version: '3'
services:
 app:
   build:
     context: .
     dockerfile: ./Dockerfile
   ports:
     - 3000:3000    # localhost only by default
   links:
     - mongo:mongo
     - elasticsearch:elasticsearch
   depends_on:
     - mongo
     - elasticsearch
   environment:
     - MONGO_URI=mongodb://mongo:27017/growi
     - ELASTICSEARCH_URI=http://elasticsearch:9200/growi
     - PASSWORD_SEED=changeme
     # - FILE_UPLOAD=mongodb   # activate this line if you use MongoDB GridFS rather than AWS
     - FILE_UPLOAD=local     # activate this line if you use local storage of server rather than AWS
     # - MATHJAX=1             # activate this line if you want to use MathJax
     # - PLANTUML_URI=http://  # activate this line and specify if you use your own PlantUML server rather than public plantuml.com
     - HACKMD_URI=http://123.456.789:3100    # activate this line and specify HackMD server URI which can be accessed from GROWI client browsers
     - HACKMD_URI_FOR_SERVER=http://hackmd:3000  # activate this line and specify HackMD server URI which can be accessed from this server container
     # - FORCE_WIKI_MODE='public'    # activate this line to force wiki public mode
     # - FORCE_WIKI_MODE='private'   # activate this line to force wiki private mode
   command: "dockerize
             -wait tcp://mongo:27017
             -wait tcp://elasticsearch:9200
             -timeout 60s
             npm run server:prod"
   restart: unless-stopped
   volumes:
     - growi_data:/data
 mongo:
   image: mongo:3.6
   restart: unless-stopped
   volumes:
     - mongo_configdb:/data/configdb
     - mongo_db:/data/db
 elasticsearch:
   build:
     context: ./elasticsearch
     dockerfile: ./Dockerfile
   environment:
     - bootstrap.memory_lock=true
     - "ES_JAVA_OPTS=-Xms256m -Xmx256m"  # increase amount if you have enough memory
   ulimits:
     memlock:
       soft: -1
       hard: -1
   restart: unless-stopped
   volumes:
     - es_data:/usr/share/elasticsearch/data
     - ./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
volumes:
 growi_data:
 mongo_configdb:
 mongo_db:
 es_data:

docker-compose.override.yml

version: '3'
services:
 ##
 # HackMD(CodiMD) container
 # see https://github.com/hackmdio/codimd#configuration
 #
 hackmd:
   build:
     context: ./hackmd
   environment:
     - GROWI_URI=http://growi.example.com:3000
     - CMD_DB_URL=mysql://hackmd:hackmdpass@mariadb:3306/hackmd
     - CMD_CSP_ENABLE=false
   ports:
     - 3100:3000   # localhost only by default
   depends_on:
     - mariadb
   restart: unless-stopped
 ##
 # MariaDB
 # see https://hub.docker.com/_/mariadb/
 mariadb:
   image: mariadb:10.3
   command: mysqld --character-set-server=utf8 --collation-server=utf8_general_ci
   environment:
     - MYSQL_USER=hackmd
     - MYSQL_PASSWORD=hackmdpass
     - MYSQL_DATABASE=hackmd
     - MYSQL_RANDOM_ROOT_PASSWORD=true
   restart: unless-stopped
   volumes:
     - mariadb_data:/var/lib/mysql
   ports:
     - 3316:3306 
volumes:
 mariadb_data:

とりあえずこれで完了です!お疲れ様でした!
以下はGROWI触ってみての所感というか余談です。

余談

やっぱ機能的に現段階ではサイドバー未実装なのが辛いですね。たぶん.cloudの方は実装されてると思うんですが、それがOSSの方に降りてくるのはいつになるか。
一応こちらのデモのようにカスタムスクリプトの機能でサイドバーを実装することも出来ますが、このカスタムスクリプトのエディタに使われてるCodeMirror2のバグなのか何なのか、コードにHTMLタグを書いて保存は出来るけど、もう一度エディタ画面を開いた時にタグ部分が勝手にエスケープされてコードが書き換わってたりするので、それもうーんという感じ。

ガチで使うつもりなら魔改造は必須かもしれないですね。記事が多くなると階層構造もどんどん深く複雑になっていくので、上手いことDBから記事へのパス情報を抽出して、抽出した情報をキャッシュ化して、ということが要求されそうな。
GROWIはnode.jsで動いていて、Expressというフレームワークで構築されているようです。DBはMongoDB。やるならここにRedisを組み込んでサーバサイドでキャッシュ機能を持たせるとかそんな感じになるのかな?この辺は知見がないので実際に出来るかどうか分かりませんが……。

あとコメントは移せない点も注意した方がいいですね。この辺はその人の使い方にもよりますが。
個人的に、マークダウンwikiにおけるコメントの運用というのは、その記事に対するチャット(指摘、雑談等)的なものだと思います。
つまりコメント欄には本来重要な情報というのは書かれるべきではなく、もし重要なことが書かれているのなら、それは記事本体に反映されて然るべきだと思うので、消えて困るような情報はコメントに書いたまんまにしておかないことが肝要なのかなと。

それと、あるesa記事で他のesa記事を参照してるリンクとかもいい感じに修正し直したりはしてくれないので、そこも注意ですね。
何にしても、データ移行というのは難しいもんです。

この記事が気に入ったらサポートをしてみませんか?