Perlでgrowiのバックアップファイルからmarkdownを生成する(version1くらい)

研究室ではwikiとしてgrowiを使っています。

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

まぁ便利なのですが、growiはデータベースがmongoDBなので、心もとないという意見や、せっかく階層構造を持っているのでmarkdown単体で保存しておきたいという意見が出ています。

もともとAPI経由でやろうとしていましたが、まぁせっかくなのでbackupファイルからmarkdownを生成してみます。

記事のbackupファイルの取得

growiでは記事はrevisionsというコレクションで登録されています。 growiのwebページからbackupを作成するか、dockerで動かしている場合は次のようなコマンドでrevisonsのjsonを取得します。

$docker exec growi_mongo_1 mongoexport -d growi -c revisions --out revision_back_1013.json
$docker  cp growi_mongo_1:revision_back_1013.json .
$docker exec growi_mongo_1 rm revision_back_1013.json

ここで取得したjsonですが、それぞれの投稿データは次のようなスキーマになっています。

  {
    "_id": "5ecce2e5fc19b9004a86ec47",
    "format": "markdown",
    "createdAt": "2020-05-26T09:35:33.830Z",
    "path": "/user/anatofuz/note/2020/05/26",
    "body": "ここに内容が入る",
    "author": "5df5ef37d744a60045dd1524",
    "hasDiffToPrev": true,
    "__v": 0
  }

注目すべきはcreatedAtはタイムスタンプになっており、authorはgrowiのuserのidとなっています。

authorが具体的に何かはrevisionsだけでは決まりませんので、とりあえず今回は放置します。

pathはgrowiのエントリのタイトルです。これはunixのファイルパスと対応してそうなので、生成したmarkdownはこのパスを利用します。

bodyはエントリの内容ですので、bodyをpathに書かれた場所に書き込む方針を取ります。

markdown自体はgit/hgなどのバージョン管理ツールで管理するのを想定するので、createdAtは最新のものを1つだけ使うようにしてみます。

Perlで軽く書く

というわけでPerlで書いてみました。

みんな大好きPath::Tinyを使っています。

metacpan.org

#!/usr/bin/env perl
use strict;
use warnings;

use utf8;
use Encode;
use JSON;
use Path::Tiny;

my $json_file = shift or die 'require json file';
my $revision = decode_json(path($json_file)->slurp);


my $paths;

for my $elem (@$revision) {
   push(@{$paths->{$elem->{path}}}, $elem);
}

for my $path (keys %$paths) {
    my $elems = $paths->{$path};
    print encode_utf8 "$path\n";

    my @sorted_elems   = sort { $b->{createdAt} cmp $a->{createdAt}}  @$elems;
    my $latest_elem    = shift @sorted_elems;

    my $emit_file_path = path("./emit/$path.md");
    $emit_file_path->touchpath;
    $emit_file_path->spew_utf8($latest_elem->{body});
}

__END__
  {
    "_id": "5ecce2e5fc19b9004a86ec47",
    "format": "markdown",
    "createdAt": "2020-05-26T09:35:33.830Z",
    "path": "/user/anatofuz/note/2020/05/26",
    "body": "",
    "author": "5df5ef37d744a60045dd1524",
    "hasDiffToPrev": true,
    "__v": 0
  }

$perl parse.pl revisons.json

の様に使います。

今回は/user/anatofuz/note/2020/05/26の場合は./emit/user/anatofuz/note/2020/05/26.mdというファイルが生成されるようにしてみました。

実際に研究室のGrowiのbackupをもとに実行すると

$ ls emit
611/         Agda.md      Christie/    FileSystem/  Haskell/     Linux/       Sandbox/     software/    user/
611.md       CbC/         Christie.md  Gears/       Haskell.md   Raku/        Sandbox.md   software.md  user.md
Agda/        CbC.md       Events/      Gears.md     Linda.md     Raku.md      growi.md     trash/

の様になっており、例えばRaku.mdを見ると

# Raku

ちょっと前までPerl6と呼ばれていたプログラミング言語

# Docs

- 公式Document
    - https://docs.raku.org/
        - `docs.perl6.org`のものは古いので見ない

# contents

$lsx(/Raku)% 

と無事取れていますね!!!

TODO

まぁ色々雑なのでTODOが多々あります

  • 作者の情報を取得する
  • Hugo等の静的サイトジェネレーターとの組み合わせを考えて、メタ情報をつける
  • 生成パスがこれで良いのか問題
  • できればワンバイナリかfatpackしたい

ということでversion1くらいの話です。まぁこういうの書くの楽しいですよね

pukiwikiのattachファイルをutf8なファイル名に変換する

pukiwikiではファイルアップロードをattachプラグイン経由で実行可能になっている。

attachでアップロードしたファイルは、index.phpがあるディレクトリの、attachディレクトリ以下に次のようなファイルとして保存される。

A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730305F536F6C7574696F6E2E706466*
A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730305F536F6C7574696F6E2E706466.log*
A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730315F536F6C7574696F6E2E706466*
A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730315F536F6C7574696F6E2E706466.log*

これらは何かというと、pukiwikiが使う文字コードをさらにURIエンコードしたものになつている。 これだとパット見でpdfなのかpptなのかjpgなのか判断できないため、utf8ベースのファイル名に変換したい。

今回はeuc-jpの文字コードが使われていたので、decodeしてutf8に変換するやつを書いた

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Encode qw/decode encode_utf8/;

while (my $file = <*>) {
  chomp $file;
  $file =~ s/\*//;
  my $ufile = $file;
  $ufile =~s/([0-9A-F]{2})/chr(hex($1))/ge;
  $ufile = decode('euc-jp', $ufile);
  $ufile = encode_utf8 $ufile;
  print "mv $file $ufile\n";
  system("mv", $file, $ufile);
}

このperlをattachディレクトリに置いて実行するといい感じにしてくれる。

mv A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730305F536F6C7574696F6E2E706466 デジタル信号処理/中間テスト_DSPMid'00_Solution.pdf
mv A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730305F536F6C7574696F6E2E706466.log デジタル信号処理/中間テスト_DSPMid'00_Solution.pdf.log
mv A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730315F536F6C7574696F6E2E706466 デジタル信号処理/中間テスト_DSPMid'01_Solution.pdf
mv A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730315F536F6C7574696F6E2E706466.log デジタル信号処理/中間テスト_DSPMid'01_Solution.pdf.log

こんな感じのコマンドが実行されたことになる。いいですね。

なおこれだと自分のプログラムも置換しにいくので、実際は__DATA__以下に変換したいファイル名を書いて実行した。

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Encode qw/decode encode_utf8/;

while (my $file = <DATA>) {
  chomp $file;
  $file =~ s/\*//;
  my $ufile = $file;
  $ufile =~s/([0-9A-F]{2})/chr(hex($1))/ge;
  $ufile = decode('euc-jp', $ufile);
  $ufile = encode_utf8 $ufile;
  print "mv $file $ufile \n";
}

__DATA__
A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730305F536F6C7574696F6E2E706466*
A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730305F536F6C7574696F6E2E706466.log*
A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730315F536F6C7574696F6E2E706466*
A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730315F536F6C7574696F6E2E706466.log*

RustのXML操作ライブラリのquick_xmlを使ってみた

なんとなくRustを勉強している中で、virshのxmlを編集する必要が出てきたため、ググって一番最初に見つかったquick_xmlを使ってみました。

github.com

quick_xmlは特徴としてRustのXMLライブラリの中でも特に早く動くらしいです。

cargo.tomlへの設定

0.20.0が最新っぽいのでこれを使います

[dependencies]
quick-xml = "0.20.0"

実際に使ってみる

SYNOPSISを参考にtemplate.xmlからdump.xmlを作成する例題を書いてみました。

github.com

Readerはquick_xml専用の実装を使う必要があり、Reader::from_readerに任意のReaderを渡すことで作成可能です。

let mut reader = Reader::from_reader(BufReader::new(File::open(file)?));

Writerも同様です。

 let mut writer = Writer::new(BufWriter::new(File::create("dump.xml").unwrap()));

用意したReaderを使って実際にxmlを読んでみます。

公式の通りreader.read_eventで読み進めて、読んだ先の構造を型でマッチングする世界観のようです。

loop {
        match reader.read_event(&mut buf) {
            Ok(Event::Start(ref e)) if e.name() == XML_NAME_ATTRIBUTE => {
                writer
                    .write_event(Event::Start(e.clone()))
                    .expect("faild write event");
                reader.read_event(&mut Vec::new()).expect("faild read event");
                let elem = BytesText::from_plain_str("anatofuz-vm");
                writer.write_event(Event::Text(elem)).unwrap();
            }

            Ok(Event::Text(ref e)) if e.escaped() == b"ie-virsh-template" => {
                let elem = BytesText::from_plain_str("anatofuz-vm");
                writer.write_event(Event::Text(elem)).unwrap();
            }
            Ok(Event::End(ref e)) if e.name() == b"this_tag" => {
                assert!(writer
                    .write_event(Event::End(BytesEnd::borrowed(b"my_elem")))
                    .is_ok());
            }
            Ok(Event::Eof) => break,
            // you can use either `e` or `&e` if you don't want to move the event
            Ok(e) => writer.write_event(&e).unwrap(),
            Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e),
        }
        buf.clear();
    }

この例ではEvent::Eofが読まれるまで無限ループでxmlをパースし続けます。

Eventは名前からなんとなくわかりますが、例えば<hoge>foo</hoge>のような構造の場合は、<hoge>Event::StartfooEvent::Text、最後の</hoge>Event::Endに対応します。

面白いのは後置のifのようなsyntaxで、特定のEventでかつ、特定の文字列が来た場合などの判定が可能なものです。

例えばこのブロックでは</this_tag>のケースのみ実行されます。

            Ok(Event::End(ref e)) if e.name() == b"this_tag" => {
                assert!(writer
                    .write_event(Event::End(BytesEnd::borrowed(b"my_elem")))
                    .is_ok());
            }

ここで<name>template</name>のようなXMLの構造が来た場合に、中のtemplateを書き換える処理を考えてみます。

まず<name>で来た場合にキャプチャをする必要があるので、Event::Startでキャプチャを行います。

const XML_NAME_ATTRIBUTE: &[u8; 4] = b"name";

///省略

Ok(Event::Start(ref e)) if e.name() == XML_NAME_ATTRIBUTE => {

writerの先のxmlへはwriter.write_eventなど、型によってwriter_*の専用メソッドが用意されています。

この例ではEvent単位で読みながらファイルを書き出していくので、基本的にはwriter.write_eventを使用することになります。

writerへの書き込みはref eで受けていた場合はe.clone()する必要があり、refで受けていない場合はポインタを取るのみで可能です。

writer
        .write_event(Event::Start(e.clone()))
        .expect("faild write event");

次に<name>template</name>templateを書き換える為に、read_eventで更に1EVENT読み進めます。

    reader.read_event(&mut Vec::new()).expect("faild read event");

読んだ先は使わないので特に束縛せず、雑にVec::new()しています。

続いて一度BytesTextインスタンスを作ってから、Event::Text型を作り、これをwrite_event経由で書き込みます。 文字列リテラルからのBytesTextの作成はfrom_plain_strを使うと可能です。

docs.rs

    let elem = BytesText::from_plain_str("anatofuz-vm");
    writer.write_event(Event::Text(elem)).unwrap();

そもそも、xmlのcontentを決め打ちでリプレイスする場合は、Event::Textでマッチングすると楽です。

Ok(Event::Text(ref e)) if e.escaped() == b"ie-virsh-template" => {
    let mut elem = BytesText::from_plain_str("anatofuz-vm");
    writer.write_event(Event::Text(elem));
}

調子に乗ってentrypoint.shのshebangを省略しない

元気に$podman runしたところ、下記のようなエラーが出た

standard_init_linux.go:211: exec user process caused "exec format error"

一体何を...バイナリでも壊したかな.......みたいな気分だったが、Dockerfileで最後のCMDとして指定している

CMD ["./entrypoint.sh"]

entrypoint.shの中身が

cd exapmle
python main.py

みたいなシェルスクリプトで、 戦闘に#!/bin/shが無いという落ちだった。。。 ぐぐると様々な人間が引っかかっている。普通のスクリプト言語でプログラミングするときは省略しないはずなのに、やはりdockerスクリプトを書くときみたいな勢いでエイヤしてるときは引っかかりがちなので、気をつけていきたい...

www.lewuathe.com

Rustで文字列が数値かどうかの判定

String変数を.parseしてOkかErrかどうかで判定可能。 たぶん<u8>じゃなくて<u16>とかにすると範囲が拡大する。

    let name = match name.parse::<u8>() {
        Ok(_) => String::from(format!("{}-{}", user_name, name)),
        Err(_) => name,
    };

この例だと数値が来ていた場合はuser_nameとハイフンで結んだ文字列をnameにshadowingしている。anatofuz-01的な感じ。

ISUCON10に出場して再起動試験で泣いてきました

ISUCON10にチームINJとして学生枠で出場し、結果としては学生本戦出場枠には入っていましたが、再起動試験で失格でした。

isucon.net

今年は最後の学生枠みたいなところがあったので、まぁ思い出づくりも兼ねて id:unimarimo (jogo)と研究室の後輩でありシス管のmkくんと出場しました。ちなみにチームは id:unimarimo以外は初参加。

シス管メンバー集結みたいな形になったのだけど、なんとisuconの予選と大学のネットワーク機器の入れ替え工事日が被ったので関係各所にはご迷惑をおかけしました...。ちなみに今回は各自自宅からzoom繋ぎっぱなしで参戦しました。

ちなみにチーム名INJはIikanji Na Jogo の略です。

f:id:anatofuz:20200913045123j:plain

役割分担としては

  • AnaTofuZ
    • 全体, 声掛け
    • webapp
  • Jogo
    • インフラ全般
  • mk
    • 遊軍

みたいな感じでした。 特にJogoくんがインフラ専門として活躍していて、マルチに活躍できるmkくんが遊軍としていろんな領域を見れていたのは良かった気がする。

やったこと

まとめると

  • チームの声掛け
  • デプロイスクリプトの用意
  • ヤバそうなエラーの解決要因
  • dlvを投入してのデバッグ
  • jsonライブラリの変更
  • 細々したSQLの変更
  • いらんORDER BYの削除とか

このあたりをしました

だいたい時系列ごと

2hの猶予が与えられたので、シェルでデプロイスクリプトをしこむ。わりと便利だったのでやってよかった。

ポータルが508騒ぎの中、「当日マニュアルはdiscordにURLがあるぞ」という話になり、みんなでポータルが復旧するまでマニュアルを音読していた。

「とりあえず椅子と資料請求させとけばええんか」「なるほどな」みたいな話をしていた。あとここでBot対策が重要そうだけどまだ実装されてないとかあるので、実装せないとな.....みたいになった。

ポータル回復後みんなでsshまつりをする。ssh先のIPの計算ができなかったが、mkくんがバシッと運営のconfigをupdateしてくれてそれを使うことになった。ポートフォワーディングのconfigの書き方とsshコマンドの使い方を理解する。

gitリポジトリ化とpprofとgo-sql-proxyをしこみ(mysql:proxyの書きミスとかがあり手間取ったが)大体開始1hちょいくらいには全体のボトルネック等をチームで共有した。アクセスログはalpが上手く読めず、全体を通して活用できてなかった。どうもUserAgent周りとアクセス先のエンドポイントが重要そうだったので、ちゃんと見るべきだった気がする。

pprofの結果json関連とnazotteがネックになっていることがわかったので、golangで使っているjsonライブラリを github.com/goccy/go-json にひとまず切り替えるなどを仕込んだ。そこまでスコアが上がらなかった気もするが...。

github.com

なんとなくテンプレートがねぇなという話になり、そもそもこの画面はどこから来ているかを探したところ、「あっ静的HTMLでjs経由か!?」みたいな話になる。「えっ画像もほとんどこのディレクトリじゃん.......」と連鎖的に確認し、jogoくんにnginxのキャッシュとgzip等の最適化を依頼する。

そういえばbotの解決もするかと思い、軽くググるとuser-agentのブロックはnginx側で出来るらしいので、jogo先輩に脳筋if文コピペを以来する。

nazzoteのN+1を解決すれば伸びそうというのはわかったものの、JOINするわけでもないので「ぐぐぐ......」となり、とりあえずSELECT *している不毛な箇所を消してまわろうという作戦に出た。一箇所の変更は問題なくて、ある程度いい感じになった。

ここで調子に乗って「表示に影響しないJSONの要素も落とすか」となったが、この施策をし始める前後でベンチが何も言わず死ぬ減少が多発する。いろいろロールバックしていたが状況がわからず、運営に問い合わせたり色々していたところ、mkくんがレギュレーションに「JSONの変更はNG」との文脈があることを発見。泣きながらロールバックする。ここでだいぶ時間をとられた......。

jogo先輩とmkくんが作ったindexのsyntax errorを解消していたり、自分のSQLのsyntax errorを解決していたりもしていた。デバッグしたかったので本番にgolang 1.15とdlvをいれて気合で見ていたけれど、このへんがあんま良くなかったかも知れない。

index周りだとSELECT * FROM estate ORDER BY rent ASC, id ASCみたいなクエリのindexを有効化しようと頑張っていたが、FORCE INDEXしないとindex貼られないという事件に遭遇していた。感想を見る限りMySQL8を使うかデータ構造に手をいれるのが大正解だったらしい。ぐぐぐ。。。

終盤でMySQLのクエリキャッシュが結構聞くことがわかったので、2台目をDBサーバーにするという背策をjogoとmkくんに頼み、ベンチマークを回していくと過去最高の1200台を記録。かなり盛り上がった。

レギュレーションで学生が上位25チームに入ればそれを除いた学生上位5チームが本戦出場なので、ギリギリいけるかな〜みたいな話をしていた。結果はギリギリ行ける枠にはいたのだけど、再起動試験で失格でした。再起動チャレンジやってなかったので痛いところ。まぁ再起動しても気づいたかどうかは別っぽい。

所感

ということで学生最初で最後のisuconでした。チーム運用もめちゃくちゃ良くて、ギスギスせずに進められたので良かった気がする。(configのコピペ忘れをしたjogo先輩に「舐めプか?」と煽ったのはノーカン)

全然俺はSQLもHTTPもわからん.....なにも...........。みたいになったので、webの勉強ちゃんとしないとなぁと思いました。細々した修正を中心にやってしまい、もう少し責めのコミットができるくらいの知識が必要でした。特に序盤ではsqlx関係のエラーを多発させてしまい、「sqlライブラリの慣れと、SQLの知識が必要だったな.....」と痛感しています。N+1は最初に気づいたにも関わらず、解決できなかったのが痛いです。他にもなんとなく改善できそうなとこは何個かあったけど、歯が立たなかったです。。。nazotteの改善でいろいろミスを踏みましたが、dlvいれてからは冷静に対処できたのでそこは良かった、そもそもミスをしたのは、経験値不足なのでツラミですね。。。

他にはテーブルが2つなのでDBを分けるという話がrandomチャンネルにありましたが、思いつかなかったですね。

あんまり練習時間が取れなかったチーム(全員シス管なので)+初めてのisuconでしたが健闘できたのは良かったですね。もちろん学生上位とのスコア差は大きいですが、普段からweb専門にやってる人がいないチームで1000点超えれたのは大きい気がする。これはid:unimarimoとmkくんの働きがすごかったというのがほとんどですね。これで本戦に行っていればもっと良かったので再起動試験が悔やまれます。一度再起動試験を試すべきだったな.....。ちなみに再起動試験のどこで死んだのかは調査中です。(どうもベンチで使ってたサーバー以外の方をみられていたらしく、そっちはnginxを動かしていた為に勘違いされた or そいつがDBを使いにいってベンチ側のサーバーからDBにアクセスできなかったのではという話を推測しています)

全員就職先の土地がバラバラなので、次回このチームになることはオンライン開催でない限り無理ですが、チームとしても良い経験になったかなと思います。シス管の作業頑張っていこうな...。

チーム関連だと他メンバーへの声掛けとか詰まってるとこを協力して解決ができてよかったです。ISUCON夏期講習の「手が止まっている時間を作らない」がちゃんとできたのはチームに貢献できた点かなぁと思います。

次回のisuconがあれば社会人枠ですが、ここで本戦出場目指してやっていきましょう。バンバンスコア上げたいしもっと貢献したいね。。。。現場からは以上です。

openldapはldapsの代わりにldapを使っていても死なない

今日のツラミポイント。

学科で運用しているOpenLDAPサーバー(slapd)の設定(slapd.conf)で詰まったやつ。 別で作成したslapdをプロバイダにして、いままでプロバイダだったslapdをコンシューマーにしようと、設定を書いていた。

overlay syncprov

syncrepl rid=104
        provider=ldap://example.com:389
        bindmethod=simple
        binddn=""
        credentials=""
        searchbase="ou=ie,o=u-ryukyu,c=jp"
        schemachecking=on
        type=refreshAndPersist
        retry="60 10 300 3"
        tls_reqcert=never
        interval=00:00:00:01


# ミラーモードを有効化
mirrormode on

これでsystemctl restart slapdすると、何もエラーを出さず元気に立ち上がるものの ldapsearch -x uid=anatofuzとかすると綺麗に何も出ていない。 どうもデータベースになにも書き込まれていない状態だった。

ldapsearch-Hでプロバイダを指定するとちゃんと引けるので、困ったね.....となっていたが、落ちはタイトルの通りで

        provider=ldap://example.com:389

ではなく

        provider=ldaps://example.com:389

じゃないといけないというショッパイ落ち.........。気をつけようね。。。。。