Git Server | Simple Implementation

2022 01 3, Mon

起因

我有一个服务器,七牛的,主要是因为我在用七牛托管我的这个博客,所以想找一个服务器,可以内网打通对象存储。就开了三年一个低配的1核2G的服务器。

但是呢,这个服务器内存2G就很难利用,keycloak啊ghost啊这类服务有点占内存,PHP的我又不大信任,即使是gitea这种go写的服务,内存占用也要400M。我又不需要太复杂的功能,所以在逐个重写这些服务。

上次我们重写了单点登录的服务,这次我们就把git托管的服务重写一下。

需求

这次写git服务主要是考虑几方面:

  • 一般使用ssh推送拉取仓库
  • 用lfs管理assets
  • 需要一个类似于gitile的简单的Web界面
  • 内存占用小,不总是占用很多内存,占用内存也可以很快释放掉

除此之外的考虑是我想入门rust,之前都只是写一些简单的代码片段,这样肯定不能入门。加上rust通过ownership的构思可以在不需要对象时马上drop掉,非常省内存,符合我的期待。所以

  • 这次的项目完全使用rust编写

构思

Web

Web的部分我计划使用actix web,是一个lgtm的框架,通过macro可以自动做很多事情。

相对的,数据库使用了diesel这个ORM,可以完整的管理数据库schema的版本和migration,可以构造SQL,有连接池r2d2。阻塞的部分有actix web的block来放到线程池里面进行。

而模板我最后使用askama,是一个类似于jinja2的库,语法也很像,只增加了对match的支持。对actix web的方面,askama actix为template实现了一个into response的trait,非常的方便。

有一些需要JSON序列化的地方就用serde了,actix web的http response类型也有一个工具函数叫json用来返回json数据。

在写的过程中有一些命令行参数需要解析,最后选择了clap-rs,既可以类似于structopt、定义一个struct,也可以用。

而界面的风格我打算使用bootstrap的默认theme,和登录的部分统一。代码高亮在写博客时没有做,计划和markdown一起通过js在浏览器端解决。

推送拉取

git推送方面我打算完全依赖于ssh,没有打算做基于http的推送。甚至我打算不使用自己的ssh实现,直接使用系统的sshd。

有问题的地方在于ssh推送以后正常情况下没有办法通知web这一侧,而且ssh没有办法告知被调起的程序是使用哪个key登陆的,所以找了很多workaround,差点就自己写一个ssh服务了,最后找到一个办法是打开sshd的PermitUserEnvironment开关,这样可以在authorized_keys中给每一个key设置环境变量。

有git的ssh协议支持,我们其实除了上面说的配置ssh以外基本上不用做什么。除了LFS。

LFS

Git在传输数据和积攒数据时会压缩数据,但是二进制格式大都有自己的压缩过程,git支持的并不友好,所以出现了很多git插件,用来解决大文件和二进制文件的问题。其中现在算是行业标准的叫Git LFS。

使用托管Git服务时,LFS通常已经被支持的很好了,而且LFS的默认配置也都对这些服务的https协议很友好。但是我们是自托管,大多数时候使用ssh的,就会有些问题。读了很多lfs的文档以后,大致来说lfs给了几种比较简易的方案:

  • 直接在客户端一侧配置lfs服务。这种方法太不友好了,每个仓库或者每个机器都要配置一次。不考虑
  • 使用https协议推送。有ssh不用我是傻子,不考虑
  • Server Discovery中提到lfs会通过ssh在服务器上执行一个命令,尝试获取到lfs的各种信息。这是我最终使用的方法

交互/设计

整个交互有几个部分

  • ssh push/pull: 默认的
  • ssh lfs discovery: 用来发现lfs服务和给出身份验证信息
  • web: 简单查看文件以及lfs server的实现

比如说我有个博客

  • ssh推送
    • lfs通过ssh获得lfs server的信息
    • 通过https服务获得lfs object对应的URL和Headers
    • lfs客户端上传文件(计划使用s3)
    • git推送文件
  • ssh拉取
    • git拉取文件
    • lfs通过ssh获得lfs server的信息
    • 通过https服务获得lfs object对应的URL和Headers
    • lfs客户端下载文件(计划使用s3)
  • web浏览
    • 浏览目录 /{path:.*}/
      • 可能需要根据名单隐藏一些目录
    • 浏览仓库 /{repo_path:.*\.git}/
      • 不同的位置 /{repo_path:.*\.git}/{object_type:(tree|blob)}/{ref_name}/{object_path:.*}
        • branch
        • commit
        • tag
        • tree
      • 路径
        • 目录tree:列举文件
          • 如果有README,渲染markdown
        • 文件blob:展示内容
          • LFS:可能提供直接下载的链接

计划实现的内容

git-lfs-authenticate

LFS发现的客户端。

现在比较简单,ssh已经验证过身份了,我不需要担心身份验证问题。只是生成http头的token,给http服务做身份认证。

TODO: 未来如果要分用户判断路径的话,需要在这里检查使用的key对应的ACL

git-server

列举目录

没啥写的,std::fs::read_dir读就完事儿了。

TODO: 未来如果要分用户判断路径的话,需要在这里检查使用的key对应的ACL

仓库tree

tree就是git的tree对象,route内会尝试着找到branch/tag/commit/tree,并且最后定位到tree对象。然后walk就可以获取到所有的entry。

仓库blob

和tree很像,只不过是文件数据,直接拿到ref_name对应的tree,然后打开对应的文件就行。

LFS Batch

LFS的API都是JSON,Request指明是要上传还是下载,有哪些object,支持什么协议,这些文件是什么reference持有的。而response就是选一个协议(一般就是basic),然后列举哪些的object需要动作(上传下载验证),动作里面会指定是什么url。最终会从客户端走动作里面的url,上传put,下载get。

我瞅了一眼各种存储协议,s3可以presign一个url用来上传下载,正好适合这个事情。除此之外理论上腾讯云COS阿里云OSS在这方面都是类似的。其他的可以用PUT方法直接上传的也都可以试试,比如说坚果云的Webdav啊什么的。

对于正儿八经的对象存储来说,一般都有预签名链接,可以直接put/get。对于webdav来说,也只要加个authorization的header。

TODO: 未来如果要分用户判断路径的话,需要在这里检查使用的key对应的ACL

LFS Lock(Not implemented)

看起来Redis的SET NX和EXPIRE配合就可以很好的做这件事情了。暂时不实现似乎也不影响什么。

初步成就

内存消耗:7.5M。

地址: https://git.jeffthecoder.xyz/