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

225 lines
5.4 KiB
Go

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)
})
}