
Git Server | Simple Implementation
起因
我有一个服务器,七牛的,主要是因为我在用七牛托管我的这个博客,所以想找一个服务器,可以内网打通对象存储。就开了三年一个低配的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:可能提供直接下载的链接
- 目录tree:列举文件
- 不同的位置
- 浏览目录
计划实现的内容
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。