この記事はQiitaで公開されていました
この記事は、Goアドベントカレンダー(その2)の19日目です。
何を言っているのか分からないかもしれませんので、動画を用意しました。
アドベントカレンダーはこれを作った話です。本当はPlan 9対応までやりたかった。 pic.twitter.com/9PpZA5hENe
— kadota (@plan9user) 2016年12月18日
何をするものなのか
これは、GitHubのIssueやその他サービスにおける同等なものを、ファイルツリーとしてひとつのディレクトリにマウントするものです。
現在の職場では、色々な事情があり、
など、いくつかのプロジェクト管理ツールを使って業務を行なっています。その中には、自社で用意したものもあれば、お客様によって用意されたものもあって、理由はわかるけれど自分のタスクが分散されてしんどいなーと思っていました。
一方で、今年のアドベントカレンダーはPlan 9と絡めた話にしようと心に決めていたので、せっかくだし9PのLinux版とも言えるFUSE(Filesystem in Userspace)を使って、色々なプロジェクト管理ツールのうち自分が担当者となっているチケットを読むファイルサーバを作ろうと思って実装しました。
現時点では、GitHub, GitHub Enterprise, GitLabに対応しています。
FUSEとは
FUSE(9P)というのは、ディスクに限らず、プロセスもネットワークも全てをファイルとして表現することができるプロトコルです。Linuxではprocfsやsshfs、最近はs3fsとかgoofysが有名ですし、本家のPlan 9ではTCPやHTTP、ウィンドウとかエディタのようなものもファイルとして表現されています。ファイルにするメリットは、普通のread, writeだけでなんでもできる点かなと思います。シェルやAwk等のツールでもある程度のことができますし、本格的なプログラミングが必要になったとしても、裏で実行されているAPIを気にする必要なく単純にファイルの読み書きで済むのは便利なんじゃないかなと思います。
macOSは、標準ではFUSEを使えませんが、FUSE for macOSを導入すればLinuxと同様に使えることを確認しています(macOS 10.12.1)。また、WindowsにはDokanというものがあるみたいですが、こっちは使えるかどうか分かりません。
ソースコードはlufia/taskfsで公開しています。
インストール方法
Linuxでの準備
CentOSまたはRHELの場合、fuse
パッケージが必要です。
$ sudo yum install fuse
DebianやUbuntu等も、同じようなパッケージが提供されていると思います。
macOSでの準備
macOSではFUSE for macOSからインストーラをダウンロードして実行するのが簡単です。私は試していませんが、Homebrewでも提供されているようです。
コマンドのインストール
go get
でインストールできます。
$ go get github.com/lufia/taskfs
使い方
taskfs
を実行すると、mtptにファイルツリーを構築します。mtptが省略された場合は/mnt/taskfsを使います。
$ taskfs [-d] [mtpt]
このコマンドは、アンマウントされるまでプロンプトへ戻りません。必要なら&
を付けてバックグラウンドで実行させてください。
ファイル操作
最初は、mtpt以下にctlというファイルだけ存在しています。このファイルに、以下の書式で文字列を書くことによって、書き込んだURLのドメイン名でディレクトリが作られます。
GitHubの場合
GitLabの場合
add gitlab {gitlab_token} {gitlab_api_url}
このうち、{github_api_url}は、github.comの場合のみ省略可能です。GitHub EnterpriseやGitLabの場合は省略できません。
$ cd /mnt/taskfs $ echo add github $github_token >ctl $ echo add github $ghe_token $ghe_url >ctl $ echo add gitlab $lab_token $lab_url >ctl $ ls ctl ghe.example.com github.com lab.example.com
ドメインディレクトリの中にはctlというファイルと、repository@organization#numberのルールで複数のディレクトリがあります。ディレクトリがひとつのIssueに対応していて、例えばtaskfs@lufia#1/messageを読むと、#1のIssueに書かれたコメントが読めます。
また、ドメインディレクトリにあるctlファイルは、refresh
という文字列を書くとIssueを再取得します。
$ cd github.com $ ls ctl taskfs@lufia#1 $ cat taskfs@lufia#1/message メッセージ内容 $ echo refresh >ctl $ ls ctl taskfs@lufia#1 taskfs@lufia#2
アンマウントする
Linuxの場合は、fusermount -u
コマンドを使います。
$ fusermount -u /mnt/taskfs
macOSは普通のumount
コマンドでアンマウントできます。
$ umount /mnt/taskfs
実装について
今回の実装ではhanwen/go-fuseと、そのサブパッケージnodefs
を使ったので、これを基準に、FUSEの実装はどんな感じなのかを簡単ですが紹介します。
ファイルツリーのマウント
ファイルツリーをユーザに公開するため、最初のディレクトリをマウントする必要があります。これはnodefs.MountRoot(mtpt, root, opts)
で行います。root引数は、nodefs.Node
インターフェイスを実装している必要があります。nodefs.Node
はFUSEで必要になるメソッドが30個ほど定義されている、とても大きなインターフェイスですが、nodefs.NewDefaultNode()
でデフォルト実装を用意してくれているので、必要なメソッドだけ自分で実装すればいいようになっています。
import ( "github.com/hanwen/go-fuse/fuse" "github.com/hanwen/go-fuse/fuse/nodefs" ) // ルートディレクトリをあらわす型 type Root struct { nodefs.Node } // 必要なメソッドだけ自分で実装する func (root *Root) GetAttr(out *fuse.Attr, file nodefs.File, ctx *fuse.Context) fuse.Status { // GetAttrは必要な属性やパーミッションでoutを更新しなければならない // ルートはディレクトリなので、fuse.S_IFDIRフラグと0755をセットする out.Mode = fuse.S_IFDIR | 0755 out.Atime = uint64(time.Now().Unix()) out.Mtime = uint64(time.Now().Unix()) // 正常な場合はfuse.OKを返す return fuse.OK } func main() { // 中略 root := &Root{ // デフォルト実装を埋め込む Node: nodefs.NewDefaultNode(), } s, _, err := nodefs.MountRoot("/mnt/taskfs", root, &opts) s.Serve() }
これだけでは、ルートはマウントできるけど空のディレクトリでしかありません。
ファイルツリーの構築
ルート以下のファイルは、nodefs.Inode
のNewChild(name, isDir, fsi)
を使って構築していきます。
NewChild
のfsi
引数はnodefs.Node
インターフェイスを実装する型でなければいけません。
type File struct { nodefs.Node } func (root *Root) CreateChildren() { p := root.Inode() // ルート以下にfile1というファイルを作成する // ファイルの内容はhelloという文字列になっている file1 := &File{ // ファイルの場合はnodefs.NewDefaultNodeより、 // nodefs.NewDataFileの方が便利 Node: nodefs.NewDataFile([]byte("hello")), } p.NewChild("file1", false, file1) // ルート以下にdirというディレクトリを作成する // ディレクトリの中にはfile2というファイルがある dir := &File{ Node: nodefs.NewDefaultNode(), } p.NewChild("dir", true, dir) p1 := dir.Inode() file2 := &File{ Node: nodefs.NewDataFile([]byte("hello")), } p1.NewChild("file2", false, file2) }
これで、以下のようなファイルツリーになりました。
mtpt/ ├── dir/ │ └── file2 └── file1
通常のファイル操作(cat
やls
等)でfile1へのアクセスを行うと、FUSEによってfile1
が実装したnodefs.Node
のメソッドが実行されるようになります。同様にdirへのアクセスはdir
のメソッドが実行されます。あとは、必要に応じてnodefs.Node
のメソッドを実装すれば良いです。
代表的なメソッド
nodefs.Node
に定義されているメソッドで、よく使うと思われるものを抜き出しました。
メソッド名 | どういう時に呼ばれるか |
---|---|
GetAttr | ファイルの情報を取得する時に呼ばれる(ls 等) |
Lookup | ディレクトリ内で特定のファイルを探す時に呼ばれる |
OpenDir | ディレクトリ内のファイル取得時に呼ばれる(ls 等) |
Open | ファイルを開く時に呼ばれる(cat 等) |
Read/Write | ファイルを読み書きする場合に呼ばれる |
Truncate | ファイルサイズを切り詰める時に呼ばれる(> 等) |
Mkdir | ディレクトリ作成時に呼ばれる(mkdir 等) |
Create | ファイルを新規作成する時に呼ばれる(touch 等) |
Unlink | ファイルを削除する時に呼ばれる(rm 等) |
基本的には、nodefs.NewDefaultNode()
とnodefs.NewDataFile()
のどちらも、ファイルの基本的な読み書き等といった、よくある動作は行ってくれるため、足りない動作だけ実装するのが良いと思います。
まとめ
ファイルとして表現する方法を考えるという手間がかかるので、Webやコマンドラインツールと比べるとお手軽ではありませんが、程度でいえば少しめんどくさい程度です。ファイルというUIがマッチする場面は比較的多いと思いますので、アイデアがあるならぜひやってみてください。
taskfs
自身については、今回は間に合わなかったのですが、Plan 9(9P)でも動かすように対応したいですね。あと、業務ではBacklogを広く使っているので、これは近いうちに対応します。または、新しいIssueの登録と編集もファイルシステム経由でできたら便利かもしれないなと思っているので、これも対応するかもしれません。
その他の情報
FUSEで実装したけどうまく動作しない
動作は間違っていないのにうまくファイルとして扱えない場合、大半はGetAttr
で必要な値をセットしていないか、間違っていることが多いです。このあたりを見直してみましょう。
macOSでno FUSE devices foundエラー
macOSで実行した時、
no FUSE devices found
というエラーになる場合は、おそらくosxfuse.kextの拡張がロードされていません。以下のコマンドで、FUSE for macOSのカーネル拡張をロードしてから試してみてください。
$ sudo kextload /Library/Filesystems/osxfuse.fs/Contents/Extensions/10.12/osxfuse.kext