Files
hubproxy/ghproxy/search.go
2025-05-20 16:23:49 +08:00

225 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os/exec"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
)
// SearchResult Docker Hub搜索结果
type SearchResult struct {
Count int `json:"count"`
Next string `json:"next"`
Previous string `json:"previous"`
Results []Repository `json:"results"`
}
// Repository 仓库信息
type Repository struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Description string `json:"description"`
IsOfficial bool `json:"is_official"`
StarCount int `json:"star_count"`
PullCount int `json:"pull_count"`
LastUpdated time.Time `json:"last_updated"`
Status int `json:"status"`
Organization string `json:"organization,omitempty"`
}
// TagInfo 标签信息
type TagInfo struct {
Name string `json:"name"`
FullSize int64 `json:"full_size"`
LastUpdated time.Time `json:"last_updated"`
LastPusher string `json:"last_pusher"`
Images []Image `json:"images"`
Vulnerabilities struct {
Critical int `json:"critical"`
High int `json:"high"`
Medium int `json:"medium"`
Low int `json:"low"`
Unknown int `json:"unknown"`
} `json:"vulnerabilities"`
}
// Image 镜像信息
type Image struct {
Architecture string `json:"architecture"`
Features string `json:"features"`
Variant string `json:"variant,omitempty"`
Digest string `json:"digest"`
OS string `json:"os"`
OSFeatures string `json:"os_features"`
Size int64 `json:"size"`
}
type cacheEntry struct {
data interface{}
timestamp time.Time
}
var (
cache = make(map[string]cacheEntry)
cacheLock sync.RWMutex
cacheTTL = 30 * time.Minute
)
func getCachedResult(key string) (interface{}, 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 interface{}) {
cacheLock.Lock()
defer cacheLock.Unlock()
cache[key] = cacheEntry{
data: data,
timestamp: time.Now(),
}
}
// searchWithSkopeo 使用skopeo搜索镜像
func searchWithSkopeo(ctx context.Context, query string) (*SearchResult, error) {
// 执行skopeo search命令
cmd := exec.CommandContext(ctx, "skopeo", "list-tags", fmt.Sprintf("docker://docker.io/%s", query))
output, err := cmd.CombinedOutput()
if err != nil {
// 如果是因为找不到镜像,尝试搜索
cmd = exec.CommandContext(ctx, "skopeo", "search", fmt.Sprintf("docker://%s", query))
output, err = cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("搜索失败: %v, 输出: %s", err, string(output))
}
}
// 解析输出
var result SearchResult
result.Results = make([]Repository, 0)
// 按行解析输出
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// 解析仓库信息
parts := strings.Fields(line)
if len(parts) < 1 {
continue
}
fullName := parts[0]
nameParts := strings.Split(fullName, "/")
repo := Repository{}
if len(nameParts) == 1 {
repo.Name = nameParts[0]
repo.Namespace = "library"
repo.IsOfficial = true
} else {
repo.Name = nameParts[len(nameParts)-1]
repo.Namespace = strings.Join(nameParts[:len(nameParts)-1], "/")
}
if len(parts) > 1 {
repo.Description = strings.Join(parts[1:], " ")
}
result.Results = append(result.Results, repo)
}
result.Count = len(result.Results)
return &result, nil
}
// getTagsWithSkopeo 使用skopeo获取标签信息
func getTagsWithSkopeo(ctx context.Context, namespace, name string) ([]TagInfo, error) {
repoName := name
if namespace != "library" {
repoName = namespace + "/" + name
}
// 执行skopeo list-tags命令
cmd := exec.CommandContext(ctx, "skopeo", "list-tags", fmt.Sprintf("docker://docker.io/%s", repoName))
output, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("获取标签失败: %v, 输出: %s", err, string(output))
}
var tags []TagInfo
if err := json.Unmarshal(output, &tags); err != nil {
// 如果解析JSON失败尝试按行解析
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
tag := TagInfo{
Name: line,
LastUpdated: time.Now(),
}
tags = append(tags, tag)
}
}
return tags, nil
}
// RegisterSearchRoute 注册搜索相关路由
func RegisterSearchRoute(r *gin.Engine) {
// 搜索镜像
r.GET("/search", func(c *gin.Context) {
query := c.Query("q")
if query == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "搜索关键词不能为空"})
return
}
result, err := searchWithSkopeo(c.Request.Context(), query)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
})
// 获取标签信息
r.GET("/tags/:namespace/:name", func(c *gin.Context) {
namespace := c.Param("namespace")
name := c.Param("name")
fmt.Printf("获取标签请求: namespace=%s, name=%s\n", namespace, name)
tags, err := getTagsWithSkopeo(c.Request.Context(), namespace, name)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, tags)
})
}