git-lagrgefile のコードリーディング


[git-lagrgefile](http://dsas.blog.klab.org/archives/52105107.html) は、
>登録しておいた拡張子のファイルはハッシュ値だけをリポジトリに格納し、ファイルの内容は別のディレクトリやAmazon S3に格納する

ものです。

今回はこちらの[golangのコード](https://github.com/methane/git-largefile/blob/master/gits3.go)を読んでみます。


では早速、

```go:キャッシュのファイルパス生成部分
func cachePath(sha1hex string) (dirpath, filename string) {
	dirpath = filepath.Join(assetDir(), "data", string(sha1hex[0:2]), string(sha1hex[2:4]))
	filename = string(sha1hex[4:])
	return
}
```
ディレクトリ名とファイル名は
`assetDir/sha1先頭２文字/sha1の先頭3文字目から２文字/sha1の4文字目以降`
という感じなので、ファイル数が数万個等になっても1ディレクトリで多すぎて死ぬ事はなさそう。

```go:S3に保存
func storeToS3(hex string, data []byte) error {
	bucket := getBucket()
	_, err := bucket.GetReader(hex)
	if err == nil {
		log.Println("Already exists in S3: ", hex)
		return err
	}
	return bucket.Put(hex, data, "application/octet-stream", s3.Private)
}
```
ファイルがS3上に既に存在すると ~~エラーになっちゃう・・・~~
~~これはエラーじゃない方が、複数の人が同じバケットを使った場合は、便利だと思ったんだけど違うんだろうか。~~
logにメッセージを吐いて、errはnilで返しているので処理続行ですね。

後、気になるのはメモリにすべてファイルを読み出している前提になってるけど、事前にハッシュを計算ために読む必要があるから仕方ないかなぁ

```go:gitのフィルタから呼ばれるstore
func store() {
	data, err := ioutil.ReadAll(os.Stdin)
	if err != nil {
		log.Fatal(err)
	}
	sha1hex := calcSha1String(data)
	storeToCache(sha1hex, data)
	if !localMode {
		if err = storeToS3(sha1hex, data); err != nil {
			log.Fatal(err)
		}
	}
	writeStdout([]byte(sha1hex))
}
```
標準入力から全部メモリに読み取って
ハッシュを計算して
キャッシュディレクトリにファイル保存
localModeフラグがfalseのとき(default)S3にストアする
そしてハッシュを標準出力に返す


```go:gitのフィルタから呼ばれるload
func load() {
	hash, err := ioutil.ReadAll(os.Stdin)
	if err != nil {
		log.Fatal(err)
	}
	hex := string(hash)
	if !isValidHash(hex) {
		writeStdout(hash)
		return
	}
	contents, err := loadFromCache(hex)
	if os.IsNotExist(err) {
		contents, err = loadFromS3(hex)
		if err != nil {
			log.Fatal(err)
		}
		storeToCache(hex, contents)
	} else if err != nil {
		log.Fatal(err)
	}
	writeStdout(contents)
}
```
標準入力からハッシュを読み取る
ハッシュのバリデーション
キャッシュディレクトリにファイルがあれば読む
無ければS3から全部メモリに読む −> キャッシュに保存
メモリに読んだコンテンツを標準出力で返す

```go:事後アップロード用?
func upload() {
	datadir := filepath.Join(assetDir(), "data")
	store := func(path string, info os.FileInfo, err error) error {
		if err != nil {
			log.Println("Skip:", path, err)
			return nil // Skip this directory.
		}
		if info.IsDir() {
			return nil
		}
		data, err := ioutil.ReadFile(path)
		if err != nil {
			log.Println("Skip:", path, err)
			return nil // Skip this file.
		}
		sha1hex := calcSha1String(data)
		return storeToS3(sha1hex, data)
	}
	filepath.Walk(datadir, store)
}
```
これは、localModeをtrueして使った後に事後S3アップロードするときに使う機能っぽい
キャッシュディレクトリのファイルを１つづアップロードする


感想
====

* S3はmd5を自動で保存されたりするので、sha1よりmd5を使った方が何か効率かできるような気がする
* ファイルは全部読み出す前提の作りなので、でっかいファイルが来たときに逝っちゃうかもしれないのと、若干効率は良くないかも?
* 並列処理化による高速を考えていたが、gitから１ファイルづつコマンドで起動されて標準入力でファイルを渡されるために、並列処理化はこのままだと難しそう。queue等を挟んでプロセスを別にして非同期化する必要がある
* ただ、localModeを使う運用にしてしまえば、upload()だけを並列化するのは簡単

追記
====

並列アップロードの実装を入れてみた。 https://github.com/masahide/git-largefile/tree/add-parallel-upload


以下は79個のファイルをアップロードの実験です。

```
$ find ~/.gitasset/data/ -type f |wc
79      79    5767
```

まずは並列なし

```
$ time ~/gits3 -n=1 upload
real	0m8.841s
user	0m0.548s
sys	0m0.183s
```
20並列で実行

```
$ time ./gits3 -n 20 upload
real	0m0.787s
user	0m0.493s
sys	0m0.121s
```