Blog

  • docker-compose で建てた CodiMD(HackMD) を 1.2.0 から 1.3.1 に更新する

    docker-compose で建てた CodiMD(HackMD) を 1.2.0 から 1.3.1 に更新する

    DB のバックアップをしてコンテナを削除する

    最初に構築したディレクトリ(docker-compose.yml があるところ)に移動して PostgreSQL のバックアップを取得しておきます。

    データはホストに残っていますが、念のため実行しておきます。

    $ cd docker-hackmd
    $ sudo docker-compose exec database pg_dump hackmd -U hackmd  > backup.sql
    $ sudo docker-compose down
    Stopping docker-hackmd_app_1      ... done
    Stopping docker-hackmd_database_1 ... done
    Removing docker-hackmd_app_1      ... done
    Removing docker-hackmd_database_1 ... done
    Removing network docker-hackmd_backend

    docker-compose.yml を編集する

    services – app – image と辿って "hackmdio/hackmd:1.2.0" となっている部分を "hackmdio/hackmd:1.3.1" に変更します。

    ### 略 ###
    services:
      ### 略 ###
      app:
        ### 略 ###
        image: hackmdio/hackmd:1.3.1
    ### 略 ###

    新バージョンの image を取得する

    この手順は飛ばしても次の手順で自動で拾ってきます。

    $ sudo docker-compose pull
    Pulling database ... done
    Pulling app      ... done

    新バージョンでコンテナを作成して立ち上げる

    $ sudo docker-compose up -d
    Creating network "docker-hackmd_backend" with the default driver
    Creating docker-hackmd_database_1 ... done
    Creating docker-hackmd_app_1      ... done

    以上で完了です。
    データもそのまま残っているはずですが、バックアップからリストアするなら次を実行します。

    $ cat backup.sql | sudo docker exec -i $(docker-compose ps -q database) psql -U hackmd
  • シェルスクリプト (bash) でファイル名が ASCII 文字から始まるファイルのみを抽出する

    シェルスクリプト (bash) でファイル名が ASCII 文字から始まるファイルのみを抽出する

    TL; DR

    ^[[:graph:][:space:][:cntrl:]]+$
    

    ファイル名が上記の正規表現を満し、かつ下の条件を満たす場合に ASCII 文字から始まるファイル名と判断できます。

    echo -n "${filename:0:1}" | wc -c` == "1"
    

    [toc]

    経緯

    bash のシェルスクリプトでファイル名のマッチングを行っているときに正規表現の制限に見事にはまった記録です。

    目的はディレクトリパスを除いたファイル名が ASCII 文字から始まるものだけを抽出することです。
    ここで注意すべきなのは、Linux ではファイル名に含めない文字は NULL 文字(0x00)と /(0x4F) のみであることです。
    意外に感じるかもしれませんが ASCII 制御文字もファイル名に含めます。

    今回は作業を記録しながら進めたので、順を追って書いてあります。
    結論だけ欲しい場合は最後だけ読んでください。

    ディレクトリの中身を取得する

    #!/bin/bash
    
    for path in "$1"/*; do
        echo $path
    done
    

    これを “lsre.sh” と保存します。
    次のように実行権限を与え、引数を与えて実行してみます。

    $ chmod +x lsre.sh
    $ ./lsre.sh .
    ./lsre.sh
    ./test_dir
    $ ./lsre.sh ./test_dir
    ./test_dir/alnum
    ./test_dir/*asterisk
    ./test_dir/@atmark
    ./test_dir/+plus
    ./test_dir/日本語
    

    ファイル名だけを抽出

    basename というコマンドを使う手もありますが、bash には変数展開という便利なものがあるのでこれを利用します。
    注意点としては、変数展開は正規表現ではなくてワイルドカードになります。

    #!/bin/bash
    
    for path in "$1"/*; do
            filename="${path##*/}"
            echo "$filename"
    done
    

    変数展開の前方除去(最長一致) ##

    ここでの変数展開には前方除去(最長一致)の ## を使って パターン */ を指定していますが、考え方としては次のようになります。

    例としてパス /foo/bar/hoge を考えます。
    ワイルドカードでパターン */ にマッチする部分は次の通り3つあります。

    • /
    • /foo/
    • /foo/bar/

    この内、最長のものは /foo/bar/ なので、その部分を除いたファイル名 hoge が得られます。

    他にも後方除去や最短一致などの便利な変数展開があります。
    詳しくは参考に挙げたページをご覧ください。

    ファイル名が ASCII 文字だけで構成されているかの判定

    本題です。
    ここで前提として、正規表現は実装によって微妙に使える表現に違いがあります。
    これが今回の落し穴でした。

    ASCII 文字は16進表記で表すと、0x00 から 0x7F までです。
    これをそのまま表せば次のように書けると思ったのです。

    ^[\x00-\x7F]+$
    

    これを先程のシェルスクリプトに入れて、ファイル名が ASCII 文字のみの場合だった時に “[ASCII]” と表示するようにしたのが次のものになります。

    #!/bin/bash
    
    ASCII_PATTERN="^[\x00-\x7F]+$"
    
    for path in "$1"/*; do
            filename=${path##*/}
            echo "$filename"
            if [[ "$filename" =~ "$ASCII_PATTERN" ]]; then
                    echo "[ASCII]"
            fi
    done
    

    実行してみると、”[ASCII]” は一つも表示されません。

    ここからいくつも試していたのですが、それを全部書いていたら無駄に長くなるので試してだめだったパターンだけを示します。

    • "^[\x00-\x7F]+$"
    • "^[\u0000-\u007F]+$"
    • "^[\u00-\u7F]+$"

    $'\x00' の記法もだめでした(上に書こうとしたらドル記号が2個あると数式になるようで書けない)。

    ちなみにサクラエディタは [\u00-\u7F]+ の記法を受け付けてくれました。

    悩んだ挙句に「POSIX クラス」を発見して、次の正規表現に行き着きました。

    "^[[:graph:][:space:][:cntrl:]]+$"
    

    これを先程のシェルスクリプトに加えます。

    #!/bin/bash
    
    ASCII_PATTERN="^[[:graph:][:space:][:cntrl:]]+$"
    
    for path in "$1"/*; do
            filename="${path##*/}"
            echo "${filename}"
            if [[ "$filename" =~ "$ASCII_PATTERN" ]]; then
                    echo "[ASCII]"
            fi
    done
    

    動作確認もうまくいった、と思ったら日本語のファイル名も ASCII 扱いになってしまいました。

    $ ./lsre ./test_dir
    alnum
    [ASCII]
    *asterisk
    [ASCII]
    @atmark
    [ASCII]
    +plus
    [ASCII]
    日本語
    [ASCII]
    

    UTF-8 をちゃんと扱えていないのかと思って最初の1文字を取得してみたのが次です。

    #!/bin/bash
    
    ASCII_PATTERN="^[[:graph:][:space:][:cntrl:]]+$"
    
    for path in "$1"/*; do
            filename="${path##*/}"
            echo "$filename"
            first="${filename:0:1}"
            echo "$first"
            if [[ "$filename" =~ "$ASCII_PATTERN" ]]; then
                    echo "[ASCII]"
            fi
    done
    
    $ ./lsre ./test_dir
    alnum
    a
    [ASCII]
    *asterisk
    *
    [ASCII]
    @atmark
    @
    [ASCII]
    +plus
    +
    [ASCII]
    日本語
    日
    [ASCII]
    

    1文字目はちゃんと取れているので UTF-8 としては扱えているようなのですが……。

    バイト数も考慮して ASCII と判別する

    最終的には上記の1文字目を wc -c して何バイトかを見て判断するようにしました。

    #!/bin/bash
    
    ASCII_PATTERN="^[[:graph:][:space:][:cntrl:]]+$"
    
    for path in "$1"/*; do
            filename="${path##*/}"
            echo "${filename}""
            if [[ "$filename" =~ "$ASCII_PATTERN" ]] && \
                     [ `echo -n "${filename:0:1}" | wc -c` == "1" ]; then
                    echo "[ASCII]"
            fi
    done
    
    $ ./lsre ./test_dir
    alnum
    [ASCII]
    *asterisk
    [ASCII]
    @atmark
    [ASCII]
    +plus
    [ASCII]
    日本語
    

    ようやく欲しいものが得られました。

    ちなみに [:punct:] も試してみましたが結果は同じだったので書いていません。

    まとめ

    • bash の正規表現だけで制御文字の範囲まで含めた ASCII 文字は判定できない
    • 文字が何バイトかを求めて併せ技で解決
    • bash で ASCII 文字判定するのは大変

    参考

  • docker-compose で imgur を使わない HackMD 環境を構築

    docker-compose で imgur を使わない HackMD 環境を構築

    HackMD(https://hackmd.io/) は Webブラウザで動く Markdown エディタです。

    HackMD の素晴しい特徴として複数のクライアントによるリアルタイム同時編集機能があります。1人でも Web ブラウザを複数起動させれば試せるのでぜひ実際に試してみていただきたいのですが、複数の編集がリアルタイムに画面に反映されるのは今までにない体験で、かなり楽しいです。使いどころを問われると答えに窮しますが、それはこれから探っていくとして……。

    この HackMD の OSS として CodiMD があります。CodiMD は公式が docker-compose.yaml を提供してくれているので自分でも簡単に環境構築を行えます。

    この際、一つだけ落し穴があって注意する必要があります。HackMD は編集画面上に画像をそのままペーストできるのですが、その時の画像の保存先がデフォルトで imgur なので Ctrl + V した瞬間に全世界に公開されてしまうのです。これはうっかり大惨事を引き起しかねません。

    docker-compose.yaml を数行変更するだけでこれを回避できるので、今回はその手順で環境を構築してみます。普段は Debian なのですが、今回は使っているサーバの都合で Ubuntu 18.04 LTS です。

    ubuntu@dev:~$ cat /etc/os-release | grep VERSION=
     VERSION="18.04.2 LTS (Bionic Beaver)"
    ubuntu@dev:~$ git clone https://github.com/hackmdio/docker-hackmd.git
     Cloning into 'docker-hackmd'…
     remote: Enumerating objects: 731, done.
     remote: Total 731 (delta 0), reused 0 (delta 0), pack-reused 731
     Receiving objects: 100% (731/731), 222.28 KiB | 702.00 KiB/s, done.
     Resolving deltas: 100% (343/343), done.

    docker-hackmd/docker-compose.yml を編集します。app の environment 下に CMD_IMAGE_UPLOAD_TYPE と CMD_DOMIN を追加します。CMD_IMAGE_UPLOAD_TYPE の指定だけではエラーを吐くらしいので CMD_DOMAIN も指定します。

    もう一つ、自分の場合は CMD_URL_ADDPORT = false を追加しました。これを入れないとすべての URL に :3000 が付加されてしまい、サイトの表示が崩れてしまいました。直接ポートを指定して使う場合は問題になりませんが、今回は Nginx によるリバースプロキシを経由してサブドメインで運用する予定なのでポートの指定は不要でした。

       - HMD_DB_URL=postgres://hackmd:hackmdpass@database:5432/hackmd
       - CMD_IMAGE_UPLOAD_TYPE = filesystem
       - CMD_DOMAIN = hmd.kuratsuki.net
       - CMD_URL_ADDPORT = false
     ports: 

    編集が完了したら docker-compose up して HackMD を起動します。

    ubuntu@dev:~/docker-hackmd$ sudo docker-compose up -d
     [sudo] password for ubuntu:
     Pulling database (postgres:9.6-alpine)…
     9.6-alpine: Pulling from library/postgres
     bdf0201b3a05: Pull complete
     365f27dc05d7: Pull complete
     bf541d40dfbc: Pull complete
     2042c4eafdd8: Pull complete
     925cd0367a3b: Pull complete
     47c141dded4c: Pull complete
     751c0956cd65: Pull complete
     a9a85eacf5bb: Pull complete
     ba84e9ab01c0: Pull complete
     Pulling app (hackmdio/hackmd:1.2.0)…
     1.2.0: Pulling from hackmdio/hackmd
     f189db1b88b3: Pull complete
     3d06cf2f1b5e: Pull complete
     687ebdda822c: Pull complete
     99119ca3f34e: Pull complete
     e771d6006054: Pull complete
     b0cc28d0be2c: Pull complete
     7225c154ac40: Pull complete
     7659da3c5093: Pull complete
     8138c3c27b70: Pull complete
     3ac8c3e5ade3: Pull complete
     b2c8cd9cc032: Pull complete
     53f21b305291: Pull complete
     bb60fd15638c: Pull complete
     6b6e9067f162: Pull complete
     Creating docker-hackmd_database_1 … done
     Creating docker-hackmd_app_1      … done

    起動したら http://CMD_DOMAINで指定した名前またはIPアドレス:3000/ を開きましょう。ここで CMD_DOMAIN とは異なる名前でアクセスすると画面が思いっきり崩れます。CSS を相対 URL 指定ではなく絶対 URL 指定しているようです。

    docker-compose up は初回だけで、2回目以降は docker-compose start/stop を使いましょう。環境を再構築したいときは docker-compose rm で一旦削除するだけです。docker-compose は便利ですね。

    参考

    • https://github.com/hackmdio/codimd/blob/master/README.md
  • Python3 で QR コードを作る

    Python3 で QR コードを作る

    パッケージのインストール

    pip で qrcode と内部的に使われている pillow をインストールします。今回は Windows 10 (64bit) 環境を使いました。

    >python --version
    Python 3.7.2
    
    >pip install qrcode pillow
    Collecting qrcode
      Downloading https://files.pythonhosted.org/packages/42/87/4a3a77e59ab7493d64da1f69bf1c2e899a4cf81e51b2baa855e8cc8115be/qrcode-6.1-py2.py3-none-any.whl
    Collecting pillow
      Downloading https://files.pythonhosted.org/packages/40/f2/a424d4d5dd6aa8c26636969decbb3da1c01286d344e71429b1d648bccb64/Pillow-6.0.0-cp37-cp37m-win_amd64.whl (2.0MB)
        100% |████████████████████████████████| 2.0MB 1.7MB/s
    Requirement already satisfied: six in c:\users\user\appdata\roaming\python\python37\site-packages (from qrcode) (1.12.0)
    Requirement already satisfied: colorama; platform_system == "Windows" in c:\users\user\appdata\roaming\python\python37\site-packages (from qrcode) (0.4.1)
    Installing collected packages: qrcode, pillow
    Successfully installed pillow-6.0.0 qrcode-6.1

    qr コマンドで作成

    qrcode をインストールすると qr コマンドが使えるようになるので、コマンドプロンプトで次のように叩くと即座に QR コードが作成できます。

    >qr 'qr code sample text'
     █████████████████████████████
     █████████████████████████████
     ████ ▄▄▄▄▄ █▀▀▄▀ █ ▄▄▄▄▄ ████
     ████ █   █ █ ▀ ▄██ █   █ ████
     ████ █▄▄▄█ ██▄▀▀▄█ █▄▄▄█ ████
     ████▄▄▄▄▄▄▄█ █ █▄█▄▄▄▄▄▄▄████
     ████▄▀▀▀▀█▄▀▄█▀ ▀ ▄▀▀▄  █████
     ████▄▄▄▀█▀▄█  ▄▀ ▀  █   ▀████
     █████▄███▄▄▄  ▀▄ ▄▄▄▄█▀█ ████
     ████ ▄▄▄▄▄ ███▄▄█▄▀█▄█▀▀█████
     ████ █   █ ████ ▀ ▀▀▄▀▄██████
     ████ █▄▄▄█ ██▀▄   ▀ ▄ █▄▄████
     ████▄▄▄▄▄▄▄█▄▄██▄██▄██▄▄█████
     █████████████████████████████
     ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
    qr コマンドで QR コードを作成

    どうやってコマンドプロンプトに画像を表示しているのかと思いましたが、これは色を変えて文字を並べているだけというカラクリでした。おもしろいですね。

    Python から作成

    特にオプションを指定しない最も簡単な作成方法を示します。

    文字列で作成

    import qrcode
    
    src = 'string to convert'
    
    qr = qrcode.make(src)
    qr.save('./qrcode.png')

    qrcode.make() はショートカット関数という扱いですが、エラー訂正符号やサイズを設定しなくてもよい場合はこれで OK です。

    ファイルの内容で作成

    普通の Python のお作法でファイルを読み込むだけで、あとは上記と同じです。encoding にさえ注意すれば日本語を含んでいても問題ありません。

    import qrcode
    
    filename = 'source.txt'
    
    with open(filename, encoding = 'utf-8') as f:
        qr = qrcode.make(f.read())
        qr.save('./qrcode.png')

    参考

  • Raspbian (Debian 9 Stretch) の SSH 有効化

    Raspbian (Debian 9 Stretch) の SSH 有効化

    user/password = pi/raspberry でログインして “sudo raspi-config” を実行します。

    “5 Interfacing Options” の “P2 SSH” を開いて “YES” を選択して完了です。

  • Debian 9 Stretch で IP アドレスの固定化

    Debian 9 Stretch で IP アドレスの固定化

    普段は DHCP で問題ないのですが、Raspberry Pi をサーバ用途で使うことが多いため、IP アドレスを固定します。Debian 8 Jessie 以前とは方法が変っているため、注意が必要です。Raspbian で実行しています。

    編集するのは “/etc/dhcpcd.conf” です。今回は有線 LAN のみ設定するので、コメントアウトを外して次のように記述します。

    interface eth0
    static ip_address=192.168.0.100/24
    static routers192.168.0.1
    static domain_name_servers=192.168.0.1 8.8.8.8

    domain_name_servers は基本的に routers と同じにしておけばいいと思いますが、1.1.1.1(Cloudflare) や 8.8.8.8(Google) 等を使ってもいいでしょう。スペース区切りで複数指定できます。

    無線 LAN は interface wlan0 で設定できると思いますが未検証です。

  • Ubuntu 18.04 に apt で Nginx をインストールしたら “[emerg] socket() [::]:80 failed (97: Address family not supported by protocol)”

    Ubuntu 18.04 に apt で Nginx をインストールしたら “[emerg] socket() [::]:80 failed (97: Address family not supported by protocol)”

    IPv4 環境なのに IPv6 の設定があるために発生します。/etc/nginx 下に “:80” の記述があるファイルがないか grep してみます。

    $ grep -R ":80" /etc/nginx/*
    sites-available/default: listen [::]:80 default_server;
    sites-available/default:# listen [::]:80;
    sites-enabled/default: listen [::]:80 default_server;
    sites-enabled/default:# listen [::]:80

    ありました。Debian のお作法的に /etc/nginx/sites-enable/default => /etc/nginx/sites-available/default のシンボリックリンクになっているはずなので sites-available/default を編集します。

    $ sudo vi /etc/nginx/sites-available/default
    .....
    listen 80 default_server;
    - listen [::]:80 default_server;
    + # listen [::]:80 default_server;
    .....

    “listen [::]:80 default_server;” をコメントアウトして保存します。

    Nginx が正常に起動するか確認します。

    $ sudo systemctl start nginx