write ahead log

ロールフォワード用

Windows10でDockerを使う

自宅のPCをWindowsにした直後に「そうだ, docker使ってみよう」とふと思いついた.

必要なものと前提

まず, WindowsにおいてはDockerは2種類あるようだ.

  1. VirtualBox上のLinux + docker(Docker Toolbox)
  2. Hyper-V上のLinux + docker(Docker for Windows)

Docker自体, Linuxコンテナプロセスを利用するものなので, Windowsではどうしてもいびつになる.

まぁ, そりゃそうだろうという感じだが.

ところで上記の内で2を使いたいと思ったのだが(だって純正Tool使いたいじゃん)Hyper-VはWindows10では Proにしか入っていない.

私はHomeのパッケージを購入していたので泣く泣くProにアップグレードする必要があった.愚かである. でもまぁ, Windows10はしばらく使おうかなと思うのでよかったかな.

導入

そんなに難しくはない.というか下記のリンク通りにやるとうまくいく.

無償の「Docker for Windows」で手軽にLinuxコンテナを利用する

ただ, MSYS2環境下では以下のようなメッセージが出ることがある.

$ docker run -it ubuntu bash
the input device is not a TTY.  If you are using mintty, try prefixing the command with 'winpty'

メッセージの通りなのだが, TTYが無い(minttyでは無い)からwinpty使ってみれば?ということらしい.

Docker ToolboxのIssueにも同様の内容が挙がっているみたい.

解決策としてはターミナルメッセージの通り, winptyを使えと.

$ pacman -S winpty
...[いい感じにインストール]...
$ docker run -it ubuntu bash
...[いい感じに動く]...

この問題, Windowsな人からしたら当然みたいなんだけど, へっぽこな技術者の私は「へ?」という感じだった. ほへー, ttyないんだー.みたいな.

以下のリンクは大変に詳しい. winptyが生まれる過程のコミュニティの動きには感謝しかない.というかオープンソースコミュニティってすごいね.

参考:MinGW の mintty で対話モード、ついでに vim 設定

App Engine + Golangでチートシートを作るだけのアプリを作った

作った.

https://cheat-sheet.tech

だって面倒なんだもの

チートシートって

  • 便利な割に作るのが面倒.
  • 他人が作ったものはしっくりこない

っていうパターンにいつもなるので, 多少は楽が出来るものを作りました.

どんなものか

Markdownの劣化版みたいな記法で書くと以下の様にチートシートっぽくなります. CheatSheet 記法

  • テーブルタグ書くのが面倒
  • 段組みしたい
  • 1ページで見たい(できるだけスクロールしたくない)
  • いちいち作り直したくない(既存のものをFork -> ちょっと変更で使い回したい)

という自分の欲求を満たせるようにしました.

見た目や機能は残念な感じですが, まぁ, 使っていて不便なところから改良するかも.

技術的な覚書

実質ここからがこの記事のメイン.
ブックマークと試行錯誤のメモが大量になったのでここに覚書.

入門記事

読んでおけば何となくそれっぽいものが作れるようになります.

この程度の掲示板ならこれだけで作れるはずです.

GAE + Golang 基礎
Datastore

基本操作は以下と公式で大丈夫だと思います.非常にわかりやすいです. トランザクションは使うことはありませんでした.

戸惑ったのは登録直後にクエリを発行しても登録したデータが表示されないことでした.
(しかもF5とかで再表示すると出てくる)

クラウド慣れしてる方なら問題ないのでしょうが, 結果整合性でスケールする事が重視されてるDBを普段取り扱わない私は割と戸惑いました.

Ancestorクエリを使えば最新の結果が返される事が保証されるそうです.

以下も戸惑いを減らしてくれました.

クエリカーソルは使いたかったのですが, 機能実装ごと投げ出していたりする部分が多くて使いませんでした.
実際は必須でしょう.

ハマりポイントを抑えてくれている記事もあります.
(1500byte制限でハマりました)

あと, 基本的なところで
string <=> Key
をそれぞれどうやって変換するんだー.というのがありましたが, 割と単純でした.

string -> key

id := "[ID String]"
// 数値へ変換
int_id, err := strconv.ParseInt(id, 10, 64) 
if err != nil {
  fmt.Fprint(w, "parse error")
  return;
}
// Keyを作る
key := datastore.NewKey(ctx, kind, "", int_id, nil)

key -> string

// IntID()を使います.keyをInt値へ
int_key := key.IntID()
// 後は何とでも
key_str := strconv.Itoa(int_key)
静的ファイルの配信

app.yamlを変更します. こんなディレクトリ構成だったら

/
|- public
    |
    |- css
    |- js
    |- img

こんな感じ.

handlers:
-   url: /public
    static_dir: public

これで/publicは公開されます.うっかり大事なファイル置いちゃだめです.

HTMLからは以下の様な感じでアクセスできます.

<link rel="stylesheet" href="/public/css/layout.css">
ロギング

logパッケージが用意されています.
こんな感じ.

import (
    "google.golang.org/appengine/log"
    "net/http"
)

func init() {
    http.HandleFunc("/", handler)
}

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := appengine.NewContext(r)
    log.Debugf(ctx, "Debug:%v", err.Error())
    log.Infof(ctx, "Info:%v", err.Error())
    log.Warning(ctx, "Warning:%v", err.Error())
    log.Criticalf(ctx, "Critical:%v", err.Error())
}
ローカル開発時のDataStoreのデータの見方
goapp serve

した状態で, ブラウザで

http://localhost:8000

へアクセスするとDataStore viewerから見れます.

memcacheやsearch indexも見れて便利です.

SSL設定

以下が本当に詳しいです.ありがとうございます.本当にありがとうございます.

Google AppEngine - Custom Domain + SSLが簡単になった

上記以外だと, 常時SSL接続を要求したい場合の設定がわかりませんでした.

app.yamlへの記述が必要でした.

-   url: /.*
    script: _go_app
    secure: always

secure: alwaysと書いとけばOKです.

OAuth

OAuthと言いつつGoogleOpenID Connectだけ使いましたが.(まぁ, OAuthの上ですし)
GitHubとかにも対応したかったんですが. OAuthは認可であって認証ではないと考えると, OpenID Connectを使うのが無難なのでしょう. この辺りは特にGAEに固有というわけではないですね.

バッチ処理

cronが使えます. 下記はPHPの記事ですが, goでも特に変わりません.

URLで指定なのでアプリにコードを書く必要が出て若干面倒ではあります.

Cron によるジョブのスケジューリング

読んでおくと楽になる親切な記事があります.

公式

何だかんだ公式は充実してます.
英語だけなので若干苦労はしますが.

どうでもいいこと

正月にゴリゴリ書いて終わらせるつもりだったのですが, 病気に罹って地獄の年越しとなりました.

ノロウィルスは怖いです.

完成度は低いけど, 自分が使いたいものは出来たのでまぁ満足かな.
(バグとかたくさんありそう)

Web屋さんのスキルに敬意を払う2ヶ月となりました.

Happy Hacking.

vsftpdでディレクトリが削除できない

WindowsからFTPクライアントで(私の場合はエクスプローラで)ディレクトリを削除しようとすると以下のエラーが出た.

550 Create directory operation failed.

本当に原因がわからなくて30分くらい悩んだのだけど,どうも.付きのファイルがFTPで取得できていないのが問題らしい.

そこで/etc/vsftpd/vsftpd.conf内に以下を追記してサービス再起動で解決した.

force_dot_files=YES

マジでハマった.

golangでオブジェクトをシリアライズしたい

cacheに乗せたい時やちょっとしたものを作るときには簡単にシリアライズ/デシリアライズできる パッケージがあると便利だ.

gobパッケージを使うと簡単にできるようだ.
rpc向けのパッケージらしい.

他ではencoding/jsonやencoding/xmlだろうか.ここら辺はググればすぐ出ると思う. encoding/binaryも使い道によっては良いと思う.

それでもgobを使ってみたのはgobの方が早いとの記述を何処かで見たからだ.(が,測定はしていない)

使い方は絶対忘れるので作ったサンプルを書いておく.

package main

import (
    "bytes"
    "time"
    "fmt"
    "encoding/gob"
)

// サンプル用の構造体
// 何となくブログ風味
type Entry struct {
    ID         int64 `datastore:"-"`
    Title      string
    Contents   string
    CreateAt   time.Time
    UpdateAt   time.Time
    DeleteAt   time.Time
}

// シリアライズ用のメソッド
// レシーバ(e)の値をシリアライズしてbyte配列にする
func (e *Entry) GobEncode() ([]byte, error) {
    // エンコーダを作る.
    // io.Writerであれば何でも良いのでここではbytes.Bufferを使う
    w := new(bytes.Buffer)
    encoder := gob.NewEncoder(w)

    // 先頭から順番にエンコードしていく
    // 必ず先頭から1つずつエンコードを実行していく必要があるらしい

    if err := encoder.Encode(e.ID); err != nil {
        return nil, err
    }
    if err := encoder.Encode(e.Title); err != nil {
        return nil, err
    }
    if err := encoder.Encode(e.Contents); err != nil {
        return nil, err
    }
    if err := encoder.Encode(e.CreateAt); err != nil {
        return nil, err
    }
    if err := encoder.Encode(e.UpdateAt); err != nil {
        return nil, err
    }
    if err := encoder.Encode(e.DeleteAt); err != nil {
        return nil, err
    }
    return w.Bytes(), nil
}

// デシリアライズするためのデコーダ
// byte配列を渡してレシーバへ値を入れる
func (e *Entry) GobDecode(buf []byte) error {
    // デコーダを作る.
    // io.Readerであれば何でも良いのでここではbytes.Bufferを使う
    r := bytes.NewBuffer(buf)
    decoder := gob.NewDecoder(r)

    // 先頭から順番にデコードしていく
    // 必ず先頭から1つずつデコードを実行していく必要があるらしい

    if err := decoder.Decode(&e.ID); err != nil {
        return err
    }
    if err := decoder.Decode(&e.Title); err != nil {
        return err
    }
    if err := decoder.Decode(&e.Contents); err != nil {
        return err
    }
    if err := decoder.Decode(&e.CreateAt); err != nil {
        return err
    }
    if err := decoder.Decode(&e.UpdateAt); err != nil {
        return err
    }
    if err := decoder.Decode(&e.DeleteAt); err != nil {
        return err
    }
    return nil
}

func main() {
    // 記録するデータ
    e1 := &Entry{
        ID: 1,
        Title: "title",
        Contents: "contents",
        CreateAt: time.Now(),
        UpdateAt: time.Now(),
        DeleteAt: time.Now(),
    }
    // bにシリアライズして入れる
    b, err := e1.GobEncode()
    if err != nil {
        fmt.Println("Error occured.", err)
    }

    // デシリアライズするために用意
    e2 := &Entry{}

    // bからe2へデシリアライズ
    if err = e2.GobDecode(b); err != nil {
        fmt.Println("Error occured.", err)
    }

    // デシリアライズしたものを表示
    fmt.Println(e2.ID)
    fmt.Println(e2.Title)
    fmt.Println(e2.Contents)
    fmt.Println(e2.CreateAt)
    fmt.Println(e2.UpdateAt)
    fmt.Println(e2.DeleteAt)
}

コメントも書いたし,多分思い出せるだろう.

golangのtime型のゼロ値はどうやって判定するのか

と思ってちょっとググったらちゃんと公式に記載があった.

time - The Go Programming Language

1年1月1日の0時だそうだ.特に驚かない.

ここがこの言語のいいところだと思う. (Rubyにも驚き最小の法則ってあったね)

いちいち判定に上記時刻を使うのは面倒なのでちゃんとIsZero()が用意されてる あたり気が利いている.

こんな感じ.

t := time.Now()
if t.IsZero() == false {
    fmt.Println("初期値じゃないよ")
}

というわけでtime型のゼロ値(初期値)判定には(t time)IsZero()を使えばいいそうです.

golang + Google App Engineを試したかったので掲示板を作った

ふと, Google App Engine(GAE)を試してみたかったのでなにか作ろうと思った.

「掲示板が作れれば大体なんとかなる」とか言いますし,とりあえず掲示板を作りました.

出来たものは以下.適当に時間が経って邪魔になったら多分インスタンス消します.

app engine - golang BBS

ソースコードは以下です.

github.com

あとは以下の感じで覚書です.

  1. 環境構築の方法
  2. Hello,worldする
  3. デプロイしてHello,worldする
  4. Datastoreを使う(掲示板アプリのちょっとした解説)

1. 開発環境の構築

やることは以下です.

  1. SDKのインストール
  2. Googleアカウントの準備

流石にGoogleアカウントが無いってことはないだろうということで,SDKだけメモります. ちなみに試した環境はUbuntu16.04 LTSです.

1-1. SDKをダウンロード

以下にリンクがあるのでダウンロードします.

Quickstart for Go App Engine Standard Environment  |  App Engine standard environment for Go  |  Google Cloud Platform

ちなみにクイックスタートなのでここの通りにやればHello, world出来ます.流石Googleです.

1-2. 解凍して配置

ダウンロードしたディレクトリを解凍して,適当な場所に配置します. 僕は面倒なのでHOMEに直で置きました.

1-3. PATHの設定

上記までで解凍・配置したディレクトリにパスを通します. 僕の場合は.bashrcに以下を書いています.

export PATH=$PATH:$HOME/go_appengine

ここまでで完了ですが,念の為コマンドを動かしておきます.

$ goapp version

上記でダメな場合はPythonのバージョンが大事なそうなので確認するほうがよいかもしれません.

/usr/bin/env python -V

公式曰く, 2.7系でないとダメだそうです.

2. Hello worldする

公式に記載がありますが,一応書いておきます.

GitHubに公式のリポジトリが用意されています.

GitHub - GoogleCloudPlatform/appengine-guestbook-go at part1-helloworld

転載させてもらうと以下の通りの非常に普通のgoコードです.

package hello

import (
    "fmt"
    "net/http"
)

func init() {
    http.HandleFunc("/", handler)
}

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello, world!")
}

これを書いて以下のように配置します.

go_appengine(DLして解凍してPATH通したディレクトリ)
  |
  |_ goroot
         |
         |_hello
              |
              |_ hello.go
              |_ app.yaml

helloディレクトリの下へ移動して以下のコマンドを実行するとローカルで動作確認ができます.

$ goapp serve

あとはlocalhost:8000へアクセスすれば世界に挨拶できます.

3. GAE上でHello worldする

3-1.プロジェクトを作る

Google Cloud Consoleへアクセスしましょう.

Google Cloud Platform

IAMと管理 => すべてのプロジェクト => プロジェクトの作成

でプロジェクトが作れます.

ここで作成したプロジェクトIDをapp.yamlに記述することになります.

3-2. デプロイ

まず,app.yamlを編集します.

application: [登録したプロジェクトID]
version: 1
runtime: go
api_version: go1
handlers:
-   url: /.*
    script: _go_app

公式だとappcfg.pyを使えとなってますね.

僕は色々な他サイトも合わせて見ていたので直接yamlを編集しました.

あとはデプロイのコマンドをコンソールから入力するだけです.

$ goapp deploy

初回は認証が必要です.

デプロイが済んだら以下のようなURLでアクセスします.

http://[登録したプロジェクトID].appspot.com/.

これでGAE上でHello worldできます.

4.Datastoreを使う(掲示板アプリの解説を若干)

ここまで来ると普通のgolangでのWebアプリケーション開発と変わらないのですが, データの永続化にDatastoreを使うのにはやはり少し調べる必要がありました.

とはいえまぁ,安いのでDatastoreを使うのは良いかと思います. 安いし早いし癖はありますが良いみたいですので.なにせ無料枠がありますからね.

App Engine の料金  |  App Engine Documentation  |  Google Cloud Platform

CRUDが出来ればなんとかなるはずなのでコードを記載して簡単にメモ書きしておきます. 詳細は冒頭でリンクを貼ったGitHubリポジトリへどうぞ.

なお,以下のコードは下記の構造体が用意されていることが前提です.

type Comment struct {
    Id int64 `datastore:"-"`
    HandleName string
    Comment string
    Like int64
    EntryTime time.Time
}

また,GAE on Goではリクエストパラメータからコンテキストを生成して,これをたらい回しにして 使うことになります.

c := appengine.NewContext(r)

なので以下のコードでcという名の変数があればすべてコンテキストです.

4-1. Create
        var com *Comment
    key := datastore.NewIncompleteKey(c, "comment", nil)
    key, err := datastore.Put(c, key, com)

NewIncompleteKeyメソッドでコンテキストとkind(2番目のパラメータ.テーブル名みたいなもん)を渡して上げてキーを作成します. その後Putメソッドでキー・バリューを対応付けして値を登録といった感じ.

4-2. Refere(複数件)
   q := datastore.NewQuery("comment").Order("-EntryTime")

    comments := make([]Comment, 0)
    iter := q.Run(c)
    for {
        var com Comment
        key, err := iter.Next(&com)
        if err == datastore.Done {
            break
        } else if err != nil {
            return nil, err
        }
        com.Id = key.IntID()
        comments = append(comments, com)
    }

NewQueryでkind指定ですべて取得しています.

Orderでソートできますが,先頭に-をつけると降順になるようです.

Queryを実際に実行するにはq.Runでイテレータを取って,iter.Nextで実際に値を取得します.

イテレータの完了はdatastore.Doneをiter.Nextの戻り値のerrと比較すれば判断出来ます.

iter.NextはKeyも返しますが,key.IntIDでキーのint値を得ることができます.

これで特定の値を一意に特定できるのでフォームに埋め込んでやったりと何かと便利です.

4-3. Refere(単一)
   keyInt, err := strconv.ParseInt(keyStr, 10, 64)
    if err != nil {
        return err
    }
    key := datastore.NewKey(c, "comment", "", keyInt, nil)

    var com Comment
    err = datastore.Get(c, key, &com)

単一のデータを得るときにはキーIDからキーオブジェクトを新たに作成し(NewKey)これを使ってGetを呼び出すことで得られます.

キーIDは上記のkey.IntIDでの情報ですね.

4-4. Update

RefereとCreateの組み合わせなので省略

4-5. Delete
   q := datastore.NewQuery("comment").Filter("__key__=", key).KeysOnly()
    iter := q.Run(c)
    var com Comment
    _, err = iter.Next(&com)
    if err == datastore.Done {
        return fmt.Errorf("key not found")
    }
    if err != nil {
        return err
    }
    if err = datastore.Delete(c, key); err != nil {
        return err
    }

Deleteを使います.

削除するオブジェクトのキーを得るのに上記のコードではNewQueryでFilterを利用しています.

Filterはその名の通り構造体のメンバでフィルターをかけれるメソッドですが, key予約語になっていて キーとマッチさせるようです.

またKeysOnlyメソッドを使っていますが,これはキーのみを取得するため,若干パフォーマンスが良くなるようです.

その他

開発中,ローカルでデータを見たくなると思いますが,そんな時は

$ goapp serve

の後に

localhost:8080

へブラウザでアクセスするとローカルのDatastoreの中身を見ることができてとても便利です.

まとめ

解説らしい解説をほぼ入れていないので, 実際はソースを見て頂きたいところです.

とはいえ

  • 割と簡単に使えて
  • (ある程度は)無料で
  • パフォーマンスも良い(課金すれば青天井)

というのは素晴らしい環境です.

次は認証API全文検索APIを使ってみたいなぁと思います.

ubuntu16.04でkindle for PCを動かす

Ubuntuに変えてもほとんど困りはしないんだけど, 致命的なものの一つにKindleがある.

私は最近ほとんどの書籍をKindleで購入しているので, 技術書なんかはとても困る.

そういうわけでUbuntukindleを導入したのだが,ちょっとハマる箇所があったのでメモ.

1. wineを入れる

やっぱりwineを使います.

32bit版の方が良いという記事があったので従っておいた.64bitでも問題ないのかもしれない.

$ sudo dpkg --add-architecture i386
$ sudo add-apt-repository -y ppa:wine/wine-builds
$ sudo apt update
$ sudo apt install winehq-devel

バージョンは以下の通り

$ wine --version
wine-1.9.21

2. wineを起動

$ wineboot

3. kindle(Windows版)をダウンロード

これは公式からどうぞ

Amazon - kindle for PC

4. wineの設定を変更

最初はこの工程を行わなかったためどうしてもうまく動かずにハマってしまった.

まず以下のコマンドを実行

$ winecfg

すると新しいウィンドウが現れる.

f:id:twinbird_htn:20161030000114p:plain

ここで「Windowsバージョン」をWindows10にすると動作した. 8.1やXPではダメだった.

参考

Ubuntu Weekly Recipe 第433回 Kindle UnlimitedをUbuntuで利用する

Ubuntu 16.04: WineでKindle for PC (Windows)を動かす