Growiのバックアップツールをgolangで書き直した

anatofuz.hatenablog.com

↑で書いていたtoolをgolangに書き直しました。ついでに最新のGrowiのスキーマにも対応しました。

mercurialからGitにリポジトリを変換したので、試したい方はこちらをご利用ください。

github.com

使い方

$./growibackup ${revison.json} ${backup_dir}

エントリの内容はrevison.jsonに書かれているので、それを指定します。 docker-composeで動かしている場合は、こんな感じのシェルスクリプト化すると楽です。

DAY=`date "+%Y-%m-%d"`
REVJSON=revision_back_${DAY}.json
docker exec growi_mongo_1 mongoexport -d growi -c revisions --pretty --jsonArray --out ${REVJSON}
docker cp growi_mongo_1:${REVJSON} .
docker exec growi_mongo_1 rm ${REVJSON}
./growibackup ${REVJSON} Growi

生成されたmarkdownPerlの時と同様にシンプルなファイルになっています。 研究室ではmercurialを使っているので、hgリポジトリ化してみました。

www.cr.ie.u-ryukyu.ac.jp

DB関係

revisionsのスキーマが変わったのか、mongodbのバージョンアップをしたためかわかりませんが、以前実装した時とrevisionsの要素の型が異なっていました。

以前はこんな感じでしたが

 type Revision struct {
       ID            string    `json:"_id"`
       Format        string    `json:"format"`
       CreatedAt     time.Time `json:"createdAt"`
       Path          string    `json:"path"`
       Body          string    `json:"body"`
       Author        string    `json:"author"`
       HasDiffToPrev bool      `json:"hasDiffToPrev"`
       V             int       `json:"__v"`
 }

今はこんな感じです

type Revision struct {
  ID struct {
    Oid string `json:"$oid"`
  } `json:"_id"`
  Format    string `json:"format"`
  CreatedAt struct {
    Date time.Time `json:"$date"`
  } `json:"createdAt"`
  Path   string `json:"path"`
  Body   string `json:"body"`
  Author struct {
    Oid string `json:"$oid"`
  } `json:"author"`
  HasDiffToPrev bool `json:"hasDiffToPrev"`
  V             int  `json:"__v"`
}

jsonからgolangの構造体を作るにはJSON-to-Goを使うと便利です。

また以前はdocker exec growi_mongo_1 mongoexport -d growi -c revisions --out ${REVJSON} みたいな感じでjson化していましたが、今日やってみたところrevisonの配列ではなくて、revisionが1件1件乗っている、invalidなjsonが返ってきました。

{
  "_id": {
    "$oid": "5df9ce81f7f7970046c44609"
  },
  "format": "markdown",
  "createdAt": {
    "$date": "2019-12-18T07:00:17.357Z"
  },
  "path": "/user/anatofuz/note/2019/12/18",
  "body": "# 日報\n\n- nkmr先生の講義で発表\n- 実験2のTAをした\n- GearsOSの書き換え作業\n\n## GearsOSの書き換え\n\n- なんか`sys_read_impl.h`時代の名残があったので幾つかファイル
を削除した\n    - interface_impl headerimplの2種類\n- そろそろsyscall interfaceを書くべき?\n    - interfaceの仕様が結構混乱を招きそうな気配を感じている\n    - チュートリアル>的な資料の充実...?",
  "author": {
    "$oid": "5df5ef37d744a60045dd1524"
  },
  "__v": 0
}
{
  "_id": {
    "$oid": "5df9e9baf7f7970046c4460b"
  },
  "format": "markdown",
  "createdAt": {
    "$date": "2019-12-18T08:56:26.446Z"
  },
  "path": "/user/anatofuz/note/2019/12/18",
  "body": "# 日報\n\n- nkmr先生の講義で発表\n- 実験2のTAをした\n- GearsOSの書き換え作業\n\n## GearsOSの書き換え\n\n- なんか`sys_read_impl.h`時代の名残があったので幾つかファイル
を削除した\n    - interface_impl headerimplの2種類\n- そろそろsyscall interfaceを書くべき?\n    - interfaceの仕様が結構混乱を招きそうな気配を感じている\n    - チュートリアル>的な資料の充実...?\n    \n## 次の継続に行く書き方\n\n`__code next(int ret_val,...)`\n- この `ret_val`は実装している方のitnerfaceに記述されている必要がある",
  "author": {
    "$oid": "5df5ef37d744a60045dd1524"
  },
  "hasDiffToPrev": true,
  "__v": 0
}

色々試したところ、--jsonArrayをつければvalidなjsonとして返ってくるらしく、つけたところちゃんとrevisionの配列のjsonとして返ってきました。

Perlからの移植

自分で使う分にはPerlでいいんですが、後輩の引き継ぎとビルドの手軽さを考えるとgolangでの書き直しをしてみました。バイナリのポン置きしたいし。

もともとメインルーチンはPerlで書いていたので素朴に移植するみたいな感じでした。 ポインタ周りをあまり使って無くて、じゃぶじゃぶインスタンスを作りまくる感じにしたのでメモリには優しくなさそう....。

戸惑ったのはtime.Time型の比較をするBeforeAfterというメソッドがあるのですが、当初「prevよりrevが新しかったら更新をする」という意図でこう書いていました。 これはrevisions.jsonにはエントリのすべての更新記録があるのですが、CVSで管理するので最新の1件だけあればいいので、それを特定する必要がある為です。

    if prevRev, ok := path2Revision[rev.Path]; ok {
      if prevRev.CreatedAt.Date.After(rev.CreatedAt.Date) {
        path2Revision[rev.Path] = rev
      }
      continue

実はこの場合、prev自体がBeforeかAfterで考えないといけないという設計らしく、実際はAfterじゃなくてBeforeでした。

    if prevRev, ok := path2Revision[rev.Path]; ok {
      if prevRev.CreatedAt.Date.Before(rev.CreatedAt.Date) {
        path2Revision[rev.Path] = rev
      }
      continue