diff --git a/ghproxy/main.go b/ghproxy/main.go index e428c41..8e994d5 100644 --- a/ghproxy/main.go +++ b/ghproxy/main.go @@ -71,7 +71,7 @@ func main() { } }() - // 初始化Skopeo相关路由 - 必须在任何通配符路由之前注册 + // 初始化Skopeo相关路由 - 在任何通配符路由之前注册 initSkopeoRoutes(router) // 单独处理根路径请求,避免冲突 @@ -82,7 +82,7 @@ func main() { // 指定具体的静态文件路径,避免使用通配符 router.Static("/public", "./public") - // 对于.html等特定文件也直接注册 + // 对于.html等特定文件注册 router.GET("/skopeo.html", func(c *gin.Context) { c.File("./public/skopeo.html") }) @@ -95,7 +95,8 @@ func main() { router.GET("/bj.svg", func(c *gin.Context) { c.File("./public/bj.svg") }) - + // 注册dockerhub搜索路由 + dockerhub.RegisterSearchRoute(router) // 创建GitHub文件下载专用的限流器 githubLimiter := NewIPRateLimiter() diff --git a/ghproxy/proxysh.go b/ghproxy/proxysh.go index aa6d980..d8a5661 100644 --- a/ghproxy/proxysh.go +++ b/ghproxy/proxysh.go @@ -35,17 +35,8 @@ func debugPrintf(format string, args ...interface{}) { } // ProcessGitHubURLs 处理数据流中的GitHub URL,将其替换为代理URL。 -// 此功能借鉴了https://github.com/WJQSERVER-STUDIO/ghproxy,版权归`WJQSERVER-STUDIO`所有。 -// 参数: -// - input: 输入数据流 -// - isCompressed: 是否为gzip压缩数据 -// - host: 代理服务器域名 -// - isShellFile: 是否为.sh文件 (如果为true,则会处理其中的GitHub URL) -// -// 返回: -// - io.Reader: 处理后的数据流 -// - int64: 写入的字节数 -// - error: 错误信息 +// 此处思路借鉴了 https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/proxy/nest.go + func ProcessGitHubURLs(input io.ReadCloser, isCompressed bool, host string, isShellFile bool) (io.Reader, int64, error) { debugPrintf("开始处理文件: isCompressed=%v, host=%s, isShellFile=%v\n", isCompressed, host, isShellFile) diff --git a/ghproxy/public/index.html b/ghproxy/public/index.html index 2e31a52..54a0832 100644 --- a/ghproxy/public/index.html +++ b/ghproxy/public/index.html @@ -395,6 +395,7 @@ hosts 镜像包下载 + 镜像搜索

Github文件加速

diff --git a/ghproxy/public/search.html b/ghproxy/public/search.html new file mode 100644 index 0000000..2ab2d52 --- /dev/null +++ b/ghproxy/public/search.html @@ -0,0 +1,377 @@ + + + + + + + + + Docker镜像搜索 + + + + + + + 返回 +
+

Docker镜像搜索

+ +
+
+ +
+ +
+
+
+ +
+
+

正在搜索...

+
+ +
+ + +
+ +
+ + + + \ No newline at end of file diff --git a/ghproxy/search.go b/ghproxy/search.go new file mode 100644 index 0000000..58a5a35 --- /dev/null +++ b/ghproxy/search.go @@ -0,0 +1,137 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "sync" + "time" + + "github.com/gin-gonic/gin" +) + +type DockerHubSearchResult struct { + Count int `json:"count"` + Next string `json:"next"` + Previous string `json:"previous"` + Results []Repository `json:"results"` +} + +type Repository struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + RepositoryType string `json:"repository_type"` + Status int `json:"status"` + Description string `json:"description"` + IsOfficial bool `json:"is_official"` + IsPrivate bool `json:"is_private"` + StarCount int `json:"star_count"` + PullCount int `json:"pull_count"` +} + +type cacheEntry struct { + data *DockerHubSearchResult + timestamp time.Time +} + +var ( + cache = make(map[string]cacheEntry) + cacheLock sync.RWMutex + cacheTTL = 8 * time.Hour +) + +func getCachedResult(key string) (*DockerHubSearchResult, bool) { + cacheLock.RLock() + defer cacheLock.RUnlock() + entry, exists := cache[key] + if !exists { + return nil, false + } + if time.Since(entry.timestamp) > cacheTTL { + return nil, false + } + return entry.data, true +} + +func setCacheResult(key string, data *DockerHubSearchResult) { + cacheLock.Lock() + defer cacheLock.Unlock() + cache[key] = cacheEntry{ + data: data, + timestamp: time.Now(), + } +} + +// SearchDockerHub 独立函数 +func SearchDockerHub(ctx context.Context, query string, page, pageSize int, userAgent string) (*DockerHubSearchResult, error) { + if query == "" { + return nil, fmt.Errorf("query 不能为空") + } + cacheKey := fmt.Sprintf("q=%s&p=%d&ps=%d", query, page, pageSize) + if cached, ok := getCachedResult(cacheKey); ok { + return cached, nil + } + + baseURL := "https://hub.docker.com/v2/search/repositories/" + params := url.Values{} + params.Set("query", query) + params.Set("page", fmt.Sprintf("%d", page)) + params.Set("page_size", fmt.Sprintf("%d", pageSize)) + + reqURL := baseURL + "?" + params.Encode() + req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil) + if err != nil { + return nil, err + } + + if userAgent != "" { + req.Header.Set("User-Agent", userAgent) + } else { + req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; MyDockerHubClient/1.0)") + } + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("docker hub api 返回状态 %d", resp.StatusCode) + } + + var result DockerHubSearchResult + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + + setCacheResult(cacheKey, &result) + return &result, nil +} + +// RegisterSearchRoute 注册 /search 路由 +func RegisterSearchRoute(r *gin.Engine) { + r.GET("/search", func(c *gin.Context) { + query := c.Query("q") + page := 1 + pageSize := 10 + if p := c.Query("page"); p != "" { + fmt.Sscanf(p, "%d", &page) + } + if ps := c.Query("page_size"); ps != "" { + fmt.Sscanf(ps, "%d", &pageSize) + } + userAgent := c.GetHeader("User-Agent") + + result, err := SearchDockerHub(c.Request.Context(), query, page, pageSize, userAgent) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, result) + }) +}