mirror of
https://github.com/WJQSERVER-STUDIO/ghproxy.git
synced 2026-02-03 00:01:10 +08:00
25w16a
This commit is contained in:
parent
d26f6d1e1b
commit
a18660121a
18 changed files with 638 additions and 118 deletions
120
gitclone/git-client.go
Normal file
120
gitclone/git-client.go
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
package gitclone
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/pierrec/lz4"
|
||||
)
|
||||
|
||||
func CloneRepo(dir string, repoName string, repoUrl string) error {
|
||||
repoPath := dir
|
||||
_, err := git.PlainClone(repoPath, true, &git.CloneOptions{
|
||||
URL: repoUrl,
|
||||
Progress: os.Stdout,
|
||||
Mirror: true,
|
||||
})
|
||||
if err != nil && !errors.Is(err, git.ErrRepositoryAlreadyExists) {
|
||||
fmt.Printf("Fail to clone: %v\n", err)
|
||||
} else if err != nil && errors.Is(err, git.ErrRepositoryAlreadyExists) {
|
||||
// 移除文件夹
|
||||
fmt.Printf("Repository already exists\n")
|
||||
err = os.RemoveAll(repoPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Fail to remove: %v\n", err)
|
||||
return err
|
||||
}
|
||||
_, err = git.PlainClone(repoPath, true, &git.CloneOptions{
|
||||
URL: repoUrl,
|
||||
Progress: os.Stdout,
|
||||
Mirror: true,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Fail to clone: %v\n", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 压缩
|
||||
err = CompressRepo(repoPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Fail to compress: %v\n", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompressRepo 将指定的仓库压缩成 LZ4 格式的压缩包
|
||||
func CompressRepo(repoPath string) error {
|
||||
lz4File, err := os.Create(repoPath + ".lz4")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create LZ4 file: %w", err)
|
||||
}
|
||||
defer lz4File.Close()
|
||||
|
||||
// 创建 LZ4 编码器
|
||||
lz4Writer := lz4.NewWriter(lz4File)
|
||||
defer lz4Writer.Close()
|
||||
|
||||
// 创建 tar.Writer
|
||||
tarBuffer := new(bytes.Buffer)
|
||||
tarWriter := tar.NewWriter(tarBuffer)
|
||||
|
||||
// 遍历仓库目录并打包
|
||||
err = filepath.Walk(repoPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建 tar 文件头
|
||||
header, err := tar.FileInfoHeader(info, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name, err = filepath.Rel(repoPath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 写入 tar 文件头
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果是文件,写入文件内容
|
||||
if !info.IsDir() {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(tarWriter, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to walk through repo directory: %w", err)
|
||||
}
|
||||
|
||||
// 关闭 tar.Writer
|
||||
if err := tarWriter.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close tar writer: %w", err)
|
||||
}
|
||||
|
||||
// 将 tar 数据写入 LZ4 压缩包
|
||||
if _, err := lz4Writer.Write(tarBuffer.Bytes()); err != nil {
|
||||
return fmt.Errorf("failed to write to LZ4 file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
14
gitclone/gitclone.go
Normal file
14
gitclone/gitclone.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package gitclone
|
||||
|
||||
import (
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
logw = logger.Logw
|
||||
logDump = logger.LogDump
|
||||
logDebug = logger.LogDebug
|
||||
logInfo = logger.LogInfo
|
||||
logWarning = logger.LogWarning
|
||||
logError = logger.LogError
|
||||
)
|
||||
159
gitclone/smart-http.go
Normal file
159
gitclone/smart-http.go
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
package gitclone
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"ghproxy/config"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-git/go-billy/v5/osfs"
|
||||
"github.com/go-git/go-git/v5/plumbing/format/pktline"
|
||||
"github.com/go-git/go-git/v5/plumbing/protocol/packp"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/server"
|
||||
)
|
||||
|
||||
// MIT https://github.com/erred/gitreposerver
|
||||
|
||||
// httpInfoRefs 函数处理 /info/refs 请求,用于 Git 客户端获取仓库的引用信息。
|
||||
// 返回一个 gin.HandlerFunc 类型的处理函数。
|
||||
func HttpInfoRefs(cfg *config.Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
repo := c.Param("repo") // 从 Gin 上下文中获取路由参数 "repo",即仓库名
|
||||
username := c.Param("username")
|
||||
repoName := repo
|
||||
dir := cfg.GitClone.Dir + "/" + username + "/" + repo
|
||||
url := "https://github.com/" + username + "/" + repo
|
||||
|
||||
// 输出 repo user dir url
|
||||
logInfo("Repo: %s, User: %s, Dir: %s, Url: %s\n", repoName, username, dir, url)
|
||||
|
||||
_, err := os.Stat(dir) // 检查目录是否存在
|
||||
if os.IsNotExist(err) {
|
||||
CloneRepo(dir, repoName, url)
|
||||
}
|
||||
|
||||
// 检查请求参数 "service" 是否为 "git-upload-pack"。
|
||||
// 这是为了确保只处理 smart git 的 upload-pack 服务请求。
|
||||
if c.Query("service") != "git-upload-pack" {
|
||||
c.String(http.StatusForbidden, "only smart git") // 如果 service 参数不正确,返回 403 Forbidden 状态码和错误信息
|
||||
log.Printf("Request to /info/refs with invalid service: %s, repo: %s\n", c.Query("service"), repoName) // 记录无效 service 参数的日志
|
||||
return // 结束处理
|
||||
}
|
||||
|
||||
c.Header("content-type", "application/x-git-upload-pack-advertisement") // 设置 HTTP 响应头的 Content-Type 为 advertisement 类型。
|
||||
// 这种类型用于告知客户端服务器支持的 Git 服务。
|
||||
|
||||
ep, err := transport.NewEndpoint("/") // 创建一个新的传输端点 (Endpoint)。这里使用根路径 "/" 作为端点,表示本地文件系统。
|
||||
if err != nil { // 检查创建端点是否出错
|
||||
log.Printf("Error creating endpoint: %v, repo: %s\n", err, repoName) // 记录创建端点错误日志
|
||||
c.String(http.StatusInternalServerError, err.Error()) // 返回 500 Internal Server Error 状态码和错误信息
|
||||
return // 结束处理
|
||||
}
|
||||
|
||||
bfs := osfs.New(dir) // 创建一个基于本地文件系统的 billy Filesystem (bfs)。dir 变量指定了仓库的根目录。
|
||||
ld := server.NewFilesystemLoader(bfs) // 创建一个基于文件系统的仓库加载器 (Loader)。Loader 负责从文件系统中加载仓库。
|
||||
svr := server.NewServer(ld) // 创建一个新的 Git 服务器 (Server)。Server 负责处理 Git 服务请求。
|
||||
sess, err := svr.NewUploadPackSession(ep, nil) // 创建一个新的 upload-pack 会话 (Session)。Session 用于处理客户端的 upload-pack 请求。
|
||||
if err != nil { // 检查创建会话是否出错
|
||||
log.Printf("Error creating upload pack session: %v, repo: %s\n", err, repoName) // 记录创建会话错误日志
|
||||
c.String(http.StatusInternalServerError, err.Error()) // 返回 500 Internal Server Error 状态码和错误信息
|
||||
return // 结束处理
|
||||
}
|
||||
|
||||
ar, err := sess.AdvertisedReferencesContext(c.Request.Context()) // 获取已通告的引用 (Advertised References)。Advertised References 包含了仓库的分支、标签等信息。
|
||||
if err != nil { // 检查获取 Advertised References 是否出错
|
||||
c.String(http.StatusInternalServerError, err.Error()) // 返回 500 Internal Server Error 状态码和错误信息
|
||||
log.Printf("Error getting advertised references: %v, repo: %s\n", err, repoName) // 记录获取 Advertised References 错误日志
|
||||
return // 结束处理
|
||||
}
|
||||
|
||||
// 设置 Advertised References 的前缀 (Prefix)。
|
||||
// Prefix 通常包含 # service=git-upload-pack 和 pktline.Flush。
|
||||
// # service=git-upload-pack 用于告知客户端服务器提供的是 upload-pack 服务。
|
||||
// pktline.Flush 用于在 pkt-line 格式中发送 flush-pkt。
|
||||
ar.Prefix = [][]byte{
|
||||
[]byte("# service=git-upload-pack"), // 服务类型声明
|
||||
pktline.Flush, // pkt-line flush 信号
|
||||
}
|
||||
err = ar.Encode(c.Writer) // 将 Advertised References 编码并写入 HTTP 响应。使用 pkt-line 格式进行编码。
|
||||
if err != nil { // 检查编码和写入是否出错
|
||||
log.Printf("Error encoding advertised references: %v, repo: %s\n", err, repoName) // 记录编码错误日志
|
||||
c.String(http.StatusInternalServerError, err.Error()) // 返回 500 Internal Server Error 状态码和错误信息
|
||||
return // 结束处理
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// httpGitUploadPack 函数处理 /git-upload-pack 请求,用于处理 Git 客户端的推送 (push) 操作。
|
||||
// 返回一个 gin.HandlerFunc 类型的处理函数。
|
||||
func HttpGitUploadPack(cfg *config.Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
repo := c.Param("repo") // 从 Gin 上下文中获取路由参数 "repo",即仓库名
|
||||
username := c.Param("username")
|
||||
repoName := repo
|
||||
dir := cfg.GitClone.Dir + "/" + username + "/" + repo
|
||||
|
||||
c.Header("content-type", "application/x-git-upload-pack-result") // 设置 HTTP 响应头的 Content-Type 为 result 类型。
|
||||
// 这种类型用于返回 upload-pack 操作的结果。
|
||||
|
||||
var bodyReader io.Reader = c.Request.Body // 初始化 bodyReader 为 HTTP 请求的 body。用于读取客户端发送的数据。
|
||||
// 检查请求头 "Content-Encoding" 是否为 "gzip"。
|
||||
// 如果是 gzip,则需要使用 gzip 解压缩请求 body。
|
||||
if c.GetHeader("Content-Encoding") == "gzip" {
|
||||
gzipReader, err := gzip.NewReader(c.Request.Body) // 创建一个新的 gzip Reader,用于解压缩请求 body。
|
||||
if err != nil { // 检查创建 gzip Reader 是否出错
|
||||
log.Printf("Error creating gzip reader: %v, repo: %s\n", err, repoName) // 记录创建 gzip Reader 错误日志
|
||||
c.String(http.StatusInternalServerError, err.Error()) // 返回 500 Internal Server Error 状态码和错误信息
|
||||
return // 结束处理
|
||||
}
|
||||
defer gzipReader.Close() // 延迟关闭 gzip Reader,确保资源释放
|
||||
bodyReader = gzipReader // 将 bodyReader 替换为 gzip Reader,后续从 gzip Reader 中读取数据
|
||||
}
|
||||
|
||||
upr := packp.NewUploadPackRequest() // 创建一个新的 UploadPackRequest 对象。UploadPackRequest 用于解码客户端发送的 upload-pack 请求数据。
|
||||
err := upr.Decode(bodyReader) // 解码请求 body 中的数据到 UploadPackRequest 对象中。使用 packp 协议格式进行解码。
|
||||
if err != nil { // 检查解码是否出错
|
||||
log.Printf("Error decoding upload pack request: %v, repo: %s\n", err, repoName) // 记录解码错误日志
|
||||
c.String(http.StatusInternalServerError, err.Error()) // 返回 500 Internal Server Error 状态码和错误信息
|
||||
return // 结束处理
|
||||
}
|
||||
|
||||
ep, err := transport.NewEndpoint("/") // 创建一个新的传输端点 (Endpoint)。这里使用根路径 "/" 作为端点,表示本地文件系统。
|
||||
if err != nil { // 检查创建端点是否出错
|
||||
log.Printf("Error creating endpoint: %v, repo: %s\n", err, repoName) // 记录创建端点错误日志
|
||||
c.String(http.StatusInternalServerError, err.Error()) // 返回 500 Internal Server Error 状态码和错误信息
|
||||
return // 结束处理
|
||||
}
|
||||
|
||||
bfs := osfs.New(dir) // 创建一个基于本地文件系统的 billy Filesystem (bfs)。dir 变量指定了仓库的根目录。
|
||||
ld := server.NewFilesystemLoader(bfs) // 创建一个基于文件系统的仓库加载器 (Loader)。Loader 负责从文件系统中加载仓库。
|
||||
svr := server.NewServer(ld) // 创建一个新的 Git 服务器 (Server)。Server 负责处理 Git 服务请求。
|
||||
sess, err := svr.NewUploadPackSession(ep, nil) // 创建一个新的 upload-pack 会话 (Session)。Session 用于处理客户端的 upload-pack 请求。
|
||||
if err != nil { // 检查创建会话是否出错
|
||||
log.Printf("Error creating upload pack session: %v, repo: %s\n", err, repoName) // 记录创建会话错误日志
|
||||
c.String(http.StatusInternalServerError, err.Error()) // 返回 500 Internal Server Error 状态码和错误信息
|
||||
return // 结束处理
|
||||
}
|
||||
|
||||
res, err := sess.UploadPack(c.Request.Context(), upr) // 处理 upload-pack 请求,执行实际的仓库推送操作。
|
||||
// sess.UploadPack 函数接收 context 和 UploadPackRequest 对象作为参数,返回 UploadPackResult 和 error。
|
||||
if err != nil { // 检查 UploadPack 操作是否出错
|
||||
c.String(http.StatusInternalServerError, err.Error()) // 返回 500 Internal Server Error 状态码和错误信息
|
||||
log.Printf("Error during upload pack: %v, repo: %s\n", err, repoName) // 记录 UploadPack 操作错误日志
|
||||
return // 结束处理
|
||||
}
|
||||
|
||||
err = res.Encode(c.Writer) // 将 UploadPackResult 编码并写入 HTTP 响应。使用 pkt-line 格式进行编码。
|
||||
if err != nil { // 检查编码和写入是否出错
|
||||
log.Printf("Error encoding upload pack result: %v, repo: %s\n", err, repoName) // 记录编码错误日志
|
||||
c.String(http.StatusInternalServerError, err.Error()) // 返回 500 Internal Server Error 状态码和错误信息
|
||||
return // 结束处理
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue