Vim で引き籠る

# <i class="fa fa-cubes" style="font-size:1em;"></i> 2015年総括

今年も沢山、良い Vim プラグインが誕生しました。
また皆さんからも幾度か vim-jp に vim の不具合報告を頂き、vim-dev にパッチとして還元する事が出来ました。本当にありがとうございました。さらに個人的には技術評論社出版の「[Software Design](http://gihyo.jp/magazine/SD "Software Design｜gihyo.jp … 技術評論社")」で連載記事「Vim の細道」を執筆させて頂く事になり[^1]、自他共にビムビムしい1年だったと思います。:book: しかし今後も皆さんが使うテキストエディタは皆さん自身が考えて良くしていく、そういう気持ちを持ちながら引き続き Vim 活動を続けて行きたいと思います。

さて 2015年は如何だったでしょうか。Vimmer になりたいと思っている人たちは Vimmer になれたでしょうか。Vimmer の世間一般のイメージと言えば

* vimrc ばかり弄っている
* プラグインばかり作っている
* 現代でも vim が最強だと思ってる
* 引き籠っている

悔しいですが、あながち間違いではないですね(え)。

「vimrc ばかり弄っている」... **ぜひ弄って下さい！**
「プラグインばかり作っている」... **ぜひ作ってください！**
「現代でも vim が最強だと思ってる」... **僕もそう思います！**
「引き籠っている」... **引き籠りましょう！**

# <i class="fa fa-home" style="font-size:1em;"></i> そうだ、引き籠ろう

周りの人からすると、コソコソやっている様に見えるかもしれません。:sun_with_face:

実際そうかもしれません。**しかしそういったコソコソから生まれる新しい技術もあるのです。**
Git でファイルを管理するのもいいけど、出来れば直接ファイルを弄りたい。そして直接書き込めるの、カッコイイ！(中二病)

**ならば書いたらいいじゃない！**
**ネットワークの向こう側の、俺たちのファイルを直接**
**`:w` で保存したらいいじゃない！** (BGMスタート)

## <i class="fa fa-wifi" style="font-size:1em;"></i> ネットワークを超えろ

Vim には Netrw というプラグインが標準で同梱されています。HTTP であれば読み取り専用でリソースを開く事が出来ます。また FTP や SCP も使えます。さらに　WebDAV もサポートしています。

しかしこの WebDAV を読み書きする機能、実際は [cadaver](http://www.webdav.org/cadaver/ "cadaver - command-line WebDAV client") というコマンドラインプログラムを使っており、Windows からだと Cygwin を使わない限り扱う事が出来ません。Cygwin 嫌いの僕にとってはとても苛立ちを覚えます。:-1:

## <i class="fa fa-gavel" style="font-size:1em;"></i> それでも僕は Windows の Vim から WebDAV 上のファイルを読み書きしたい

Windows から WebDAV 上のファイルを読み書きする方法が無い訳ではありません。それどころか Windows にはネットワークプレースという機能があり、エクスプローラから登録する事で UNC パスとして参照する事ができ、さらにネットワークドライブとして登録する事も可能です[^2]。コマンドプロンプトは UNC パスをカレントディレクトリとして起動する事が出来ないため Windows を UNIX っぽく扱いたい人にとっては色々と問題が発生しますが、ネットワークドライブとして割り当てる事で通常のドライブとして扱える様になります。:beers:

1. ネットワークプレースを追加する
![box.com を WebDAV として扱う](https://qiita-image-store.s3.amazonaws.com/0/665/368cccef-794e-5b54-57e1-f943d0b82a7c.png)

2. Basic 認証に Box.com のアカウント情報を入力する
![box.com の Basic 認証](https://qiita-image-store.s3.amazonaws.com/0/665/ab591364-cca2-fde9-f23e-7f2f6e02ebec.png)

3. エクスプローラから参照可能になる
![box.com がネットワークプレースに追加される](https://qiita-image-store.s3.amazonaws.com/0/665/c41ca822-2fa5-9045-1a72-8f9b0faa762d.png)

4. UNC パスとしても参照可能
![UNC パスとして参照可能](https://qiita-image-store.s3.amazonaws.com/0/665/0e77a552-7381-2f92-f192-750c9ba94109.png)

ネットワークプレースをネットワークドライブとして割り当てる場合は以下を実行します。

```bat
net use * https://dav.box.com/dav
```

ただし作成したネットワークプレースをエクスプローラから削除した場合、接続情報がキャッシュされたままになり次回登録する際にエラーが発生してしまいます。その場合は以下のコマンドを実行して解除して下さい。

```bat
net use \\dav.box.com@SSL\dav /delete
```

## <i class="fa fa-hotel" style="font-size:1em;"></i> 負けた感じがする

でもこれ、完全に Windows の機能に乗っかってしまっていて、とても負けた感じがしますね。新しい接続先を得るたびに毎回この設定が必要です。同じ方法で Linux や Mac OS X から実現できませんし、異なる環境で手法や vimrc を共有したい Vimmer からすると完全に敗北です。だからといって Cygwin 版の cadaver を使う気にはなれないのです。大事な事なのでもう一度言います。

**なれないのです！**

ならば作ってしまおうと思ったのです。Vim プラグインから扱いやすい WebDAV クライアントを作ってしまおう。そう思ったのです。

---
<i class="fa fa-graduation-cap" style="font-size:1em;"></i> WebDAV とは

[RFC 2518](https://tools.ietf.org/html/rfc2518 "HTTP Extensions for Distributed Authoring -- WEBDAV ") で定義されている HTTP の拡張プロトコルで、通常の GET/HEAD/PUT/POST/DELETE といったメソッド以外に以下の拡張メソッドを使用します。

|メソッド |説明                            |
|---------|--------------------------------|
|PROPFIND |プロパティの取得                |
|PROPPATCH|プロパティの変更                |
|MKCOL    |ディレクトリ(コレクション)の作成|
|COPY     |リソースの複製                  |
|MOVE     |リソースの移動                  |
|LOCK     |リソースのロック                |
|UNLOCK   |リソースのロック解除            |

また GET/HEAD/PUT/POST/DELETE についてはリソースの取得や作成、更新、削除に用いられます。

通常の WebDAV クライアントは接続後に OPTIONS メソッドを発行し、サーバがどのメソッドをサポートしているかを調べます。

```shell-session

$ curl -X OPTIONS -u XXX:XXX -s -i https://dav.box.com
HTTP/1.1 200 OK
Server: ATS
Date: Fri, 11 Dec 2015 02:17:38 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 0
Accept-Ranges: bytes
MS-Author-Via: DAV
DAV: 1, 3, extended-mkcol, 2
Allow: OPTIONS, GET, HEAD, DELETE, PROPFIND, PUT, PROPPATCH, COPY, MOVE, REPORT,
 LOCK, UNLOCK
Vary: Accept-Encoding
Age: 0
Connection: keep-alive
```
次にパスを指定して PROPFIND メソッドを実行します。その際、以下のプロパティを要求します。

```xml
<d:propfind xmlns:d="DAV:">
  <d:prop>
    <d:displayname/>
    <d:resourcetype/>
    <d:getcontentlength/>
    <d:getlastmodified/>
  </d:prop>
</d:propfind>
```
ファイルシステムとしての属性が返却されます。

```shell-session
$ curl -X PROPFIND --data @req.xml -u XXX:XXX -s -i https://dav.box.com/dav/
HTTP/1.1 207 Multi-Status
Server: ATS
Date: Fri, 11 Dec 2015 02:24:05 GMT
Content-Type: application/xml; charset=utf-8
Content-Length: 3507
DAV: 1, 3, extended-mkcol, 2
Vary: Accept-Encoding
Age: 0
Connection: keep-alive
```
```xml
<?xml version="1.0" encoding="utf-8"?>
<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">
  <d:response>
    <d:href>/dav/</d:href>
    <d:propstat>
      <d:prop>
        <d:resourcetype><d:collection/></d:resourcetype>
        <d:getcontentlength>725280</d:getcontentlength>
        <d:getlastmodified xmlns:b="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/" b:dt="dateTime.rfc1123">Fri, 11 Dec 2015 02:24:05 GMT</d:getlastmodified>
      </d:prop>
      <d:status>HTTP/1.1 200 OK</d:status>
    </d:propstat>
    <d:propstat>
      <d:prop><d:displayname/></d:prop>
      <d:status>HTTP/1.1 404 Not Found</d:status>
    </d:propstat>
  </d:response>
  <d:response>
    <d:href>/dav/Documents/</d:href>
    <d:propstat>
      <d:prop>
        <d:resourcetype><d:collection/></d:resourcetype>
        <d:getcontentlength>0</d:getcontentlength>
        <d:getlastmodified xmlns:b="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/" b:dt="dateTime.rfc1123">Fri, 15 Jun 2007 07:38:29 GMT</d:getlastmodified>
      </d:prop>
      <d:status>HTTP/1.1 200 OK</d:status>
    </d:propstat>
    <d:propstat>
      <d:prop><d:displayname/></d:prop>
      <d:status>HTTP/1.1 404 Not Found</d:status>
    </d:propstat>
  </d:response>
</d:multistatus>
```
この様に WebDAV では属性の要求と応答を XML を使ってやり取りし、リソースの操作は GET/PUT/POST/DELETE といった通常の HTTP メソッドを使う事で実現しています。詳しくは [RFC 2518](https://tools.ietf.org/html/rfc2518 "HTTP Extensions for Distributed Authoring -- WEBDAV ") を参照して下さい。

## <i class="fa fa-wrench" style="font-size:1em;"></i> WebDAV クライアントを作ろう

閑話休題。要件としては

* readline っぽく動作してコマンドをバシバシと叩けて
* 出来れば色付きでファイル一覧が表示されて
* それでいて Windows からも使えて
* Vim プラグインからも扱える物(ちょっと！ここが本題よ)

となります。上記の要件を満たせるプログラミング言語としては golang が最適であろうという事で今回も golang で作りました。

![davc](https://qiita-image-store.s3.amazonaws.com/0/665/63ac848a-475a-d073-c802-a581aceebde4.gif)

> https://github.com/mattn/davc

インストールは以下の様に実行します。

```shell-session
$ go get github.com/mattn/davc
```

WebDAV プロトコル実装部分は [gowebdav](https://github.com/studio-b12/gowebdav/) を使用しています。[liner](github.com/peterh/liner) を使っており、リモートでもローカルでもファイル名補完も効いて、とても使いやすくなっていると思います。扱えるコマンドは以下の通りです。

|コマンド|説明                                |
|--------|------------------------------------|
|lpwd    |ローカルのカレントディレクトリを表示|
|pwd     |リモートのカレントディレクトリを表示|
|lcd     |ローカルのカレントディレクトリを変更|
|cd      |リモートのカレントディレクトリを変更|
|lmkdir  |ローカルにディレクトリを作成        |
|mkdir   |リモートにディレクトリを作成        |
|lls     |ローカルのファイル一覧を表示        |
|ls      |リモートのファイル一覧を表示        |
|ll      |リモートのファイル詳細一覧を表示    |
|lrm     |ローカルのファイルを削除            |
|rm      |リモートのファイルを削除            |
|lrmdir  |ローカルのディレクトリを削除        |
|rmdir   |リモートのディレクトリを削除        |
|put     |ファイルをリモートに転送            |
|get     |ファイルをローカルに転送            |
|cp      |リモートでのファイルコピー          |
|mv      |リモートでのファイル移動            |
|cat     |リモートのファイルをcat             |
|write   |標準入力の内容をリモートに転送      |
|edit/vim|リモートのファイルをvimで編集し更新 |
|exit    |終了                                |

~~これだけで記事を終えても良いのですが(良くない)、気付いたらこれ Go Advent Calendar 2015 じゃなくて Vim Advent Calendar 2015 だったので~~ これを Vim から扱えるプラグインを作る必要があります。こういった用途に使えるのがお馴染み metarw です。

> https://github.com/kana/vim-metarw

metarw の拡張プラグインとして metarw-webdavというのを作りました。

![vim-metarw-webdav](https://qiita-image-store.s3.amazonaws.com/0/665/ea9f9b78-f266-5ca5-fa55-0c4e2bc75bad.png)

> https://github.com/mattn/vim-metarw-webdav

これを使えば

```
:e webdav://server/path/to/file.txt
```

といった様にリモートのファイルを直接読み込み、編集し、`:w` で保存する事が出来ます。Basic 認証が掛かっている場合は

```
:e webdav://user:password@server/path/to/file.txt
```

でアクセスするか davc 用の環境変数 `DAVC_CRED` を `user:pass` の形式で設定して下さい。 

ようやく Vim から WebDAV 上のファイルを読み書きできる様になりました。おしまい。

# <i class="fa fa-legal" style="font-size:1em;"></i> WebDAV 扱えるサーバ、実はそんなに無い

しかし WebDAV サーバって構築がそれほど簡単じゃない。僕もこの davc や metarw-webdav を作っていて薄々と感じ取っていました。またオンラインストレージに関しても WebDAV が扱える有名どころのファイルストレージサーバと言えば [Box.com](https://app.box.com/files "Box | Secure Content & Online File Sharing for Businesses") くらいしか無い様に思います。(もし知ってたら教えて下さい)

**ならば作ったらいいんじゃね？**:hocho:
そう思った僕は、迷いもなく Vim でソースを書き始めていました。

> https://github.com/mattn/davfs

インストールは以下の様に行います。

```shell-session
$ go get github.com/mattn/davfs/cmd/davfs
```

同じく golang で書いた WebDAV サーバです。リソースの管理方式は

* ファイル
* メモリ
* データベース

のどれかを選択出来ます。データベースを使う場合はサーバ側に同じファイルシステムが構築される訳ではありません。あくまでデータベース内にファイルシステムが構築されます。

引き籠り度が強くてとても良いですね。

---
ここでちょっと休憩

実は golang には `x/net/webdav` という webdav を扱う為のパッケージが用意されています。WebDAV 上のリソースが os.FileInfo を使って透過的に表現されています。

https://github.com/golang/net/blob/3a29182c25eeabbaaf94daaeecbc7823d86261e7/webdav/file.go#L39-L45

```go:webdav/file.go
// A FileSystem implements access to a collection of named files. The elements
// in a file path are separated by slash ('/', U+002F) characters, regardless
// of host operating system convention.
//
// Each method has the same semantics as the os package's function of the same
// name.
//
// Note that the os.Rename documentation says that "OS-specific restrictions
// might apply". In particular, whether or not renaming a file or directory
// overwriting another existing file or directory is an error is OS-dependent.
type FileSystem interface {
	Mkdir(name string, perm os.FileMode) error
	OpenFile(name string, flag int, perm os.FileMode) (File, error)
	RemoveAll(name string) error
	Rename(oldName, newName string) error
	Stat(name string) (os.FileInfo, error)
}
```

ですので webdav.FileSystem インタフェースに合うように実装を行えば、それだけで WebDAV 上のファイルシステムとして投入出来る様になっています。

webdav パッケージには、デフォルトでファイルシステムを表した Filesystem (そのままやんけ)と、メモリファイルシステムが用意されており、実際のファイルシステムを使って WebDAV したい人がいれば直ぐにでもサーバを実装出来る様になっています。:sushi:

---

さて davfs ですが、扱えるデータベースは以下の3つです。

* SQLite3
* PostgreSQL
* MySQL

試してないのでこれは推測ですが、おそらく PostgreSQL を使えば Heroku 上に不揮発な WebDAV を作る事も出来るかと思います。SQLite3 については go-sqlite3 を使っているのでビルドすると静的リンクされたコマンドが生成でき、USB メモリに仕込ませておけば複数人数で扱う作業の中で「どこでもファイルシステムー！」的な事が出来て意外と便利かもしれません。

![ウェーブダーブー!](https://qiita-image-store.s3.amazonaws.com/0/665/4576e126-6c76-2f63-51d4-02a3912d4611.png)

起動方法は以下の通り。

```shell-session
$ davfs -driver sqlite3 -source foo.db
```

driver と source を指定して起動します。MySQL(ドライバ名:mysql) や PostgreSQL(ドライバ名:postgres) のソース指定は各ドライバの接続文字列に従って下さい。なお初回のみ以下を実行してデータベース上にファイルシステムを作成する必要があります。

```shell-session
$ davfs -driver sqlite3 -source foo.db -create
```

実際には以下のテーブルと初期レコードが作成されます。

```sql
create table filesystem(
	name text not null, 
	content text not null,
	mode bigint not null,
	mod_time timestamp not null,
	primary key (name)
);

insert into filesystem(name, content, mode, mod_time) values('/', '', 2147484159, current_timestamp);
```

content はバイナリが扱える様に Hex 文字で格納します。本当は Blob で扱いたかったのですが、ファイルシステム上でファイルを追加モードで開いて何度か書き込む場合、Blob の結合を行う必要があるのですが、その際ローカル側に追加前のコンテンツを持ってくるとメモリをアロケーションが多発するので(SQLだけで完結したかった)この様な形式になりました[^3]。簡単なファイルシステムなのでやろうと思えば SQL だけでファイルを作成する事も出来るかもしれませんね。
ファイル、もしくはディレクトリが1行のレコードで格納されます。ファイル名をキーにしている為、例えばディレクトリ内のファイル検索であればディレクトリ名を LIKE で前方一致させる SQL でファイル検索が出来ます。

ただし MySQL の場合は残念な事にインデックスに text 型を使うと255バイトまでしか扱えない為、結果ファイル名が255バイトまでしか扱えなくなります。:gun:

とは言えファイルシステム上で画像ファイルを上書き保存したり、動画ファイルをコピーしたりもしてみましたが、ひとまず問題無いようです。

先に説明したファイルシステム Filesystem と、メモリ Filesystem も扱える様にしてありますので、データベースを用意しなくても社内 LAN 内で何時でも簡単に WebDAV サーバがコマンド1つで起動出来ます[^4]。

# <i class="fa fa-archive" style="font-size:1em;"></i> まとめ

davc や davfs、はたまた vim-metarw-webdav により Vim から簡単に WebDAV 上のファイルを読み書き出来る様になりました。また Linux であれば davfs2 を使えば簡単に webdav 上のファイルシステムをマウントする事が出来ますし、Windows でも先に説明した様にネットワークプレースを使う事で簡単に WebDAV を扱う事が出来ます。**コソコソと、新しい技術を身に着け、無事に引き籠った後には大きなアウトプット**を皆に見せて下さい。

おしまい。

[^1]: http://mattn.kaoriya.net/software/vim/20150820112724.htm
[^2]: https://technet.microsoft.com/ja-jp/library/ee155455.aspx
[^3]: 実際には SQLite3 だけ Blob の結合が SQL で出来なかった。
[^4]: もちろんメモリファイルシステムで起動すると終了時にファイルが消え去ります。