純規の暇人趣味ブログ

首を突っ込んで足を洗う

git-lfsの仕様(サーバ側)を個人的に解説してみる

      HimaJyun

前回、Gitoliteにgit-lfsを載せるなんて言って無理やりgit-lfsに対応させてみました。

その過程で知った、git-lfsがどのようなやり取りを行うのかを個人的に解説してみようと思います。

注:あくまで個人的な解説なので情報の正確性は保証できませんよ、ただ、こうすれば動いた、程度です。

git-lfsのAPI

英語が読める奴は公式のドキュメントでも読めば良いと思います。

git-lfsのレポジトリ(と言うよりストレージ)サーバはHTTPでやり取りを行います、なんとも現代的ですね。(僕はこのなんでもかんでも「とりあえずHTTPでやっとけよ」みたいな風潮は苦手なのですが……)

基本的に以下の様な手順でpushされます。

  1. (SSHの場合)git-lfs-authenticateコマンドでエンドポイントのURLを取得
  2. エンドポイントに対してjsonをPOSTし、アップ/ダウンロード用のURLを取得
  3. (アップロードの場合)PUTでアップロード/(ダウンロードの場合)GETでダウンロード

LegacyAPIとBatchAPI

git-lfsにはLegacyAPIとBatchAPIの2種類があります。

とは言え、LegacyAPIの方は超古で効率も悪く、BatchAPIを使った方が良いみたいです。

基本的にBatchAPIだけサポートしておけば良いかと思います。(確かGitLabもBatchAPIしかサポートしていなかったかと)

jsonがPOSTされてくる

SSHの場合のgit-lfs-authenticateコマンドは後で説明するとして、まず最初にjsonがPOSTされて来ます。

飛んでくるjsonは以下の通りです。

{
    "objects": [
        {
            "oid": "e5b844cc57f57094ea4585e235f36c78c1cd222262bb89d53c94dcb4d6b3e55d",
            "size": 10485760
        }
    ],
    "operation": "upload"
}

各項目は以下の通りです。

  • objects:アップロードを要求しているオブジェクトの配列
  • operation:ユーザが行いたい操作
  • oid:オブジェクト(データ)の一意なSHA256ハッシュ
  • size:オブジェクトのサイズ

ただし、これらはユーザが送ってくる情報に過ぎないので偽装されている事もあるかも知れません。

サーバ側で実際にPUTやGETを行う際には厳重なチェックを行った方が良いかと思います。

また、このJSONはPOSTの本文に含まれているので、例えばPHPだと「file_get_contents("php://input");」みたいな感じで取得します。

複数ファイルの場合のjson

BatchAPIは1度に複数ファイルの情報を取得しようとします。

その場合のjsonは以下のようになっています。

{
    "objects": [
        {
            "oid": "e5b844cc57f57094ea4585e235f36c78c1cd222262bb89d53c94dcb4d6b3e55d",
            "size": 10485760
        },
        {
            "oid": "0f6d7219c5295725e733b9910ae259ef98106a4bc7b388b513e707440f4da5cf",
            "size": 102401
        },
        {
            "oid": "f627ca4c2c322f15db26152df306bd4f983f0146409b81a4341b9b340c365a16",
            "size": 102400
        }
    ],
    "operation": "upload"
}

リクエストに対するレスポンス

サーバ側はユーザのレスポンスを解釈して、以下のようにレスポンスに対するリクエストを送り返す必要があります。

{
	"objects": [
		{
			"oid": "e5b844cc57f57094ea4585e235f36c78c1cd222262bb89d53c94dcb4d6b3e55d",
			"size": 10485760,
			"actions": {
				"upload": {
					"href": "https://example.com/lfs/upload/hogehoge"
				},
				"download": {
					"href": "https://example.com/lfs/download/hogehoge"
				},
				"verify": {
					"href": "https://example.com/lfs/verify/hogehoge"
				}
			}
		},
		{
			"oid": "0f6d7219c5295725e733b9910ae259ef98106a4bc7b388b513e707440f4da5cf",
			"size": 102401,
			"actions": {
				"upload": {
					"href": "https://example.com/lfs/upload/?hoge=fuga",
					"header": {
						"Key": "value",
						"Authorization": "YWRtaW46MTIz"
					},
					"expires_at": "2015-07-27T21:15:01Z"
				}
			}
		}
	]
}

基本的に見たままですが、actionsは基本的にuploadかdownloadのどちらか、ユーザの要求してきた操作に対するアクションがあれば良いです。(uploadとdownloadの両方含めるのも可、その場合は必要な方が利用される)

verifyアクションはupload操作と合わせて使われるみたいで、アップロードが正常に完了されたかを確認するためのURLみたいです。(無くても動作します)

各アクションの中にはhref、header、expires_atの3つが利用可能で、以下のような意味を持ちます。

  • href:該当の操作を行うためのURL、ここで教えたURLにアクセスしてくるので、例えばクエリ文字などでoidを送らせる手がある
  • header:ここで指定した物がヘッダとして送られてくる、主にBasic認証の回数を減らすために利用されると思われ
  • expires_at:該当の操作が有効な期間、トークン型の認証を利用する際などに利用するみたい

jsonの一部にエラーがあった場合

jsonそのもののエラー(フォーマットが不正、等)の場合は後述の「エラー時のレスポンス」をご覧下さい。

送られてきた要求の中に不正な物(例えば正しくないSHAハッシュなど)があった場合には以下の様にすることでエラーを通知できます。

{
	"objects": [
		{
			"oid": "123",
			"size": -1,
			"error": {
				"code": 400,
				"message": "リクエストが妙ですわ。"
			}
		}
	]
}

GitがHTTP経由での場合の挙動

SSHの場合はgit-lfs-authenticateコマンドでgit-lfsのレポジトリのURLを教えてやれば良いのですが、HTTPの場合はなんとダイレクトに確認しに来ます。

例えば、URLが「https://example.com/hoge.git」だった場合、pushやcloneなどを行った際に「https://example.com/hoge.git/info/lfs/objects/batch」に対してこのjsonを送りつけてきます。(そして、404になるのでcloneがコケる、gitも巻き込んでコケるクソッタレ仕様)

そのため、git-lfsのURLが違う場合、例えば「https://lfs.example.com」などに別途lfsのサーバがある場合は、上記のURLに対するアクセスの際に細工を施す必要があります。(このあたり、仕様がまだまだだなと感じるポイント)

エラー時のレスポンス

次に進む前にエラー時のレスポンスを説明しておきましょう。

以下で説明するエラー処理はjsonをPOSTしてきた場合に限らず、PUTでのアップロードやGETでのダウンロードに対しても可能みたいです。

レスポンスコード

git-lfsは以下のレスポンスコードを認識します。

  • 200:リクエストは正常に処理されました。
  • 202:要求が受け入れられている、クライアントはURLを辿る必要がある(json用?、ぶっちゃけ用途不明)
  • 301:恒久的なリダイレクト、GETとHEADのリクエストに対してしか使えない
  • 302:一時的なリダイレクト、GETとHEADのリクエストに対してしか使えない
  • 303:一時的なリダイレクト、GETとHEADのリクエストに対してしか使えない
  • 307:一時的なリダイレクト、元の要求メソッドを保持したままリダイレクトされる。
  • 400:一般エラー、例えばjsonが不正だとか
  • 401:資格情報が正しくない
  • 403:ユーザはレポジトリに対する読み取り権限を持つが、書き込み権限がない
  • 404:存在しないレポジトリ、またはファイルに対しての読み取り権限を持たない
  • 406:受理出来ない、主にAcceptヘッダがapplication/vnd.git-lfs+jsonでない場合に使用
  • 410:元々存在したんだけど、後から消された、理由なども送る必要あり?(ユーザが消した、検閲、など)
  • 422:要求されたオブジェクトで1つ以上のエラー、アップロード要求(POSTしてきたjson)が不正な場合などに使用
  • 429:ユーザがサーバの帯域制限にヒット(どう言うこっちゃ?、509と同じ気が)
  • 501:サーバは現在この機能を提供していない、将来のために予約されている。
  • 509:帯域幅の制限(上限?)を超えている。

エラーメッセージの送信

エラーが発生した場合に以下のようなjsonを送り返すことでユーザに任意のメッセージを表示させられます。

{
  "message": "エラーですよん。",
  "documentation_url": "https://example.com/help/errors",
  "request_id": "123"
}

messageはエラーの内容、一応日本語も表示できるみたいですが、環境依存でしょうから英語の方が確実

documentation_urlはエラーに関する情報が載ったページのURL

request_idはリクエストを識別する一意のID

documentation_urlとrequest_idは無くても良いです。

各アクションの処理

さて、一番厄介なjsonのパースが終わったら次に来るのはバカでかいファイルのアップロードか、バカでかいファイルのダウンロードです。(稀にVerification)

よしなに取り計らわないとメモリが死にます、絶対変数(メモリ)に載せたりしちゃダメです。

アップロード処理

PUTとか言うぶっちゃけこんなの使わねぇよ、みたいなメソッドを利用して本文にバイナリがどんぶらこと乗ってきます。

CGIで取得する時、これは多くの場合、標準入力として入って来ると思われるので、ファイルのI/O処理と同じように標準入力に対して(fread的な物で)ループしながら少しずつ読み取り、ファイルへの書き込みを行うのが安全です。

例え手を滑らせたとしても、これをメモリに載せる(変数に入れる)なんて行為は言語道断です、ギガバイト単位のファイルが飛んで来たら死にます、絶対に。

あと、可能性としては少ないですが、アップロード中に他の誰かがダウンロードしに来る可能性も0ではないので排他制御(ロック)を行った方が安全です。

ダウンロード処理

アップロードと同じです、絶対にメモリに載せたりしては行けません。(特にPHPなんかは出力バッファとかがあるので要注意)

やはりアップロードと同じように、fopen、fread的な物の組み合わせで少しずつループしながら読み取り、出力する必要があります。

もしくは、WebサーバとしてApacheを利用している場合はX-Sendfileを、nginxを使用している場合はX-Accel-Redirectを使うという手があります。

Verification処理

ぶっちゃけこれを実装したことはないので分かりませんが、ファイルがサーバ上に存在するか確認するための機能みたいです。

設定してある場合には以下のようなjsonがPOSTされてきます。

{"oid": "0f6d7219c5295725e733b9910ae259ef98106a4bc7b388b513e707440f4da5cf", "size": 102401}

指定されたファイルがサーバに存在する場合は200 OKを、存在しない場合は404 Not Foundなどを返せば良いかと思われます。

git-lfs-authenticateコマンド

最後はこいつ。

SSHでgitのpushやcloneを行う場合はHTTPの(lfsサーバの)URLを確認する必要があります。(アップロードとかをそのままSSHでやらない辺りGitHub社は頑固だと思う)

クライアントがサーバに対して実行するコマンドは以下の様になっています。

git-lfs-authenticate hoge/fuga.git download

git-lfs-authenticateコマンドの引数としてレポジトリ名とアクションが送られてくるわけですね。

レスポンス

やはりjsonで返す模様、以下のようなjsonを返せば良いです。

{
  "header": {
    "Key": "value"
  }
  "href": "https://example.com/hoge.git/info/lfs",
}

hrefではURLを教えてやります、ここで言うURLはjsonをPOSTするためのURLです、例の「info/lfs」ですね。(objects/batchは勝手に付け加えられるので指定してはいけない)

headerに指定された値は例によってリクエストヘッダにつけられて送られてきます。

公式のドキュメントではAuthorizationヘッダを指定して認証を省略する使い方、みたいなのが載っていましたが、ぶっちゃけユーザのアカウント名はともかくとしてパスワードはハッシュ化されていますし、現実的には無理ですね。

エラー時

エラーの内容を示すメッセージを標準エラー出力(STDERR)に出せば良いみたいです。

まぁ、ただ単にURLを教えてやるだけなのであまり細かい事をする必要はないでしょう。

終わりに

たかだかバイナリをアップロードする如きにめんどくさ過ぎる気がします。

僕の不満はlfsのストレージサーバとして良いものがない事です。(ってか公式が提供してるサーバが「lfs-test-server」ってどうなのよ)

ですので、結果的に自分で実装しています。

より良いlfsのストレージサーバが増えるとありがたい限りです。(どうでも良いけどGitLabはメモリ食い過ぎだ)

 - プログラミング