Git LFS 服务器实现杂谈

on under git
5 minute read

前言

从前言开始,便又要讲起 git 原理,多次讲起不胜其烦,诸位如果要了解 git 原理可以通过 bing google 或者其他搜索引擎从网络上查阅资料,也可以去 git-scm.com 网站查看 《Pro Git》,对于大多数开发者而言已是足够。

倘若开发者熟知 git 原理,便可以知道 git 在管理大文件时,显得尤为麻烦,一来反复修改占用空间巨大,二来,每次修改网络传输占用资源,为了给更多的开发者服务,代码托管系统往往还需要对存储,网络连接进行限制,大文件的传输和存储肯定会陷入了囧境。

开发者用到大文件的地方可能有静态资源,比如 PSD 文件,还有某 SDK 等。如何管理这些文件?善于思考的同学可能会想到,将这些大文件存储在第三方存储,等检出存储库后,再通过程序更新这些资源,实际上 NuGet,Maven 就是这种思路。但是对于 PSD 这类文件此方案就显得不足。

作为全球最大的代码托管平台,Github 在这方面深有感触,为了解决大文件存储的问题,2015年4月,Github 宣布推出 Git LFS , Announcing Git Large File Storage (LFS)

LFS 主要技术原理

在 git 存储库中,有一些特殊文件, .gitattributes.gitignore.gitattributes 顾名思义用来管理存储库下路径的属性,如何过滤,如何 diff,如何合并。而 .gitignore 的作用则是排除指定的路径,不添加到版本库中。

笔者最早了解到 .gitattributes 时是使用 git 管理毕业论文,设置好 .gitattributes 后,就可以到 docx 文档进行 diff 了。利用这一特性,git lfs 在运行命令 git lfs track 后,将路径添加到 .gitattributes 文件中,比如这样:

clang.tar.gz filter=lfs diff=lfs merge=lfs -text

然后修改 git 钩子,包括 post-checkout post-commit post-merge pre-push,其中的内容都是将当前钩子命令当做参数启动 git-lfs。

创建提交的过程中,比如添加了 clang.tar.gz ,git 会启动 lfs 命令,将 clang.tar.gz 文件的 hash 值(目前是 sha256),大小写入到一个名字叫 clang.tar.gz 文件中,然后将此文件作为大文件的 Pointer 提交到版本库中,原始的 clang.tar.gz 会单独存储到 lfs/objects,将更新推送到远程服务器上时,pre-push 钩子启动 git lfs,git lfs 连接到远程的 LFS 服务器,将文件上传到服务器上。

而 checkout 或者 pull 的时候,本地没有的大文件,git lfs 就会将大文件下载下来。

简单的说就是利用 .gitattributes 偷梁换柱。

git lfs 规范可以在 https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md 查看。

LFS 服务器的实现

实现一个 LFS 服务器实际上是比较简单的,在 Github 上搜索 git lfs server 可以找到很多个类似项目。在 git-lfs wiki 上也有 https://github.com/git-lfs/git-lfs/wiki/Implementations ,

按照 v0.5 的协议,

获取大文件的信息:

GET /info/lfs/objects/{oid}

{
    "oid": "oid",
    "size": 123,
    "_links": {
        "download": {
            "href": "http://some.com/oid",
            "header": {
                "Authorization": "some token"
            }
        }
    }
}

下载文件:

GET /lfs/objects/{oid}

而这个 URL 是由 /info/lfs/objects/{oid} 获得的。

如果要上传文件,首先是 POST:

POST /lfs/info/objects/{oid}

{
    "_links": {
        "upload": {
            "href": "https://some-upload.com",
            "header": {
                "Key": "value"
            }
        },
        "verify": {
            "href": "https://some-callback.com",
            "header": {
                "Key": "value"
            }
        }
    }
}

获得 upload 的 URL 后,然后使用 HTTP PUT 方法将文件上传到返回的 URL。PUT 时会将 header 的内容添加到 HTTP 请求头中,这里一般会使用 Authorization 头,这种设计可以将大文件存储到第三方存储上。

Git LFS 2.0 已经移除了 v0.5 版本的 API,目前使用 Git LFS Batch API

Batch 实际上就是将 v0.5 API 的多个请求合并到一个请求中。由于 batch.md 已经说了,虽然不是很明白,也就不多说了。

大多数 Git LFS 服务已经支持 Batch API 了,也就没什么好说的了。

Git LFS 2.0 增加了 Locking API

开发者可以对某路径进行锁定,以避免其他开发者提交新的大文件到存储机器。于是就有了 Locking API。

Locking API 需要实现的有 lock unlock verify (POST),list(GET)。verify 和 list 的区别主要是区分 我的其他的 锁。

创建:

{
  "lock": {
    "id": "6",
    "path": "image/av115.psd",
    "locked_at": "2017-04-14T02:19:04Z",
    "owner": {
      "name": "charlie"
    }
  }
}
{
  "locks": [
    {
      "id": "2",
      "path": "image/av111.psd",
      "locked_at": "2017-04-11T10:47:30Z",
      "owner": {
        "name": "tom"
      }
    },
    {
      "id": "3",
      "path": "image/av112.psd",
      "locked_at": "2017-04-12T06:30:09Z",
      "owner": {
        "name": "jekk"
      }
    },
    {
      "id": "4",
      "path": "image/av113.psd",
      "locked_at": "2017-04-12T06:30:13Z",
      "owner": {
        "name": "other"
      }
    },
    {
      "id": "5",
      "path": "image/av114.psd",
      "locked_at": "2017-04-12T06:30:16Z",
      "owner": {
        "name": "charlie"
      }
    },
    {
      "id": "6",
      "path": "image/av115.psd",
      "locked_at": "2017-04-14T02:19:04Z",
      "owner": {
        "name": "charlie"
      }
    }
  ],
  "next_cursor": "6"
}

验证:

{
  "ours": [
    {
      "id": "5",
      "path": "image/av114.psd",
      "locked_at": "2017-04-12T06:30:16Z",
      "owner": {
        "name": "charlie"
      }
    },
    {
      "id": "6",
      "path": "image/av115.psd",
      "locked_at": "2017-04-14T02:19:04Z",
      "owner": {
        "name": "charlie"
      }
    }
  ],
  "theirs": [
    {
      "id": "2",
      "path": "image/av111.psd",
      "locked_at": "2017-04-11T10:47:30Z",
      "owner": {
        "name": "tom"
      }
    },
    {
      "id": "3",
      "path": "image/av112.psd",
      "locked_at": "2017-04-12T06:30:09Z",
      "owner": {
        "name": "jekk"
      }
    },
    {
      "id": "4",
      "path": "image/av113.psd",
      "locked_at": "2017-04-12T06:30:13Z",
      "owner": {
        "name": "other"
      }
    }
  ],
  "next_cursor": "6"
}

笔者在实现 Git LFS 服务器时,使用的是 C++ , HTTP 库使用 cpprestsdk。锁的设计使用了 sqlite3,即每个存储库拥有一个 sqlite3 db,使用文件锁同步。

GVFS

闲聊

Microsoft,Github 还有 Google ,都拥有版本控制领域的精英,比如微软有 git-for-windows 的维护者 Johannes Schindelin @dscho ,libgit2 LibGit2Sharp 核心维护者 Edward Thomson @ethomson 他还在 Github 待过一段时间。而 Github 有 Carlos Martín Nieto @carlosmn Vincent Marti @vmg,二者参加 Soc2011 为 libgit2 添加了网络栈,vmg 是 github 的主力程序员了,涉猎很广。Google 有 Git 的主要维护者 Junio C Hamano。

SoC2011Projects