1
This commit is contained in:
@@ -219,63 +219,179 @@ func searchDockerHub(ctx context.Context, query string, page, pageSize int) (*Se
|
|||||||
return cached.(*SearchResult), nil
|
return cached.(*SearchResult), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重试逻辑
|
// 判断是否是用户/仓库格式的搜索
|
||||||
|
isUserRepo := strings.Contains(query, "/")
|
||||||
|
var namespace, repoName string
|
||||||
|
|
||||||
|
if isUserRepo {
|
||||||
|
parts := strings.Split(query, "/")
|
||||||
|
if len(parts) == 2 {
|
||||||
|
namespace = parts[0]
|
||||||
|
repoName = parts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建搜索URL
|
||||||
|
baseURL := "https://registry.hub.docker.com/v2/repositories/"
|
||||||
|
var fullURL string
|
||||||
|
var params url.Values
|
||||||
|
|
||||||
|
if isUserRepo && namespace != "" {
|
||||||
|
// 如果是用户/仓库格式,直接搜索用户的仓库
|
||||||
|
fullURL = fmt.Sprintf("%s%s/", baseURL, namespace)
|
||||||
|
params = url.Values{
|
||||||
|
"page": {fmt.Sprintf("%d", page)},
|
||||||
|
"page_size": {fmt.Sprintf("%d", pageSize)},
|
||||||
|
}
|
||||||
|
if repoName != "" {
|
||||||
|
params.Set("name", repoName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 普通搜索
|
||||||
|
fullURL = baseURL + "search/"
|
||||||
|
params = url.Values{
|
||||||
|
"query": {query},
|
||||||
|
"page": {fmt.Sprintf("%d", page)},
|
||||||
|
"page_size": {fmt.Sprintf("%d", pageSize)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fullURL = fullURL + "?" + params.Encode()
|
||||||
|
fmt.Printf("搜索URL: %s\n", fullURL)
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建请求失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
MaxIdleConns: 100,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
DisableCompression: true,
|
||||||
|
DisableKeepAlives: false,
|
||||||
|
MaxIdleConnsPerHost: 10,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
var result *SearchResult
|
var result *SearchResult
|
||||||
var lastErr error
|
var lastErr error
|
||||||
|
|
||||||
// 判断是否是用户/仓库格式的搜索
|
// 重试逻辑
|
||||||
isUserRepo := strings.Contains(query, "/")
|
|
||||||
|
|
||||||
for retries := 3; retries > 0; retries-- {
|
for retries := 3; retries > 0; retries-- {
|
||||||
if isUserRepo {
|
resp, err := client.Do(req)
|
||||||
// 对于用户/仓库格式,尝试精确搜索和模糊搜索
|
if err != nil {
|
||||||
parts := strings.Split(query, "/")
|
lastErr = fmt.Errorf("发送请求失败: %v", err)
|
||||||
if len(parts) == 2 {
|
if !isRetryableError(err) {
|
||||||
// 先尝试精确搜索
|
break
|
||||||
result, lastErr = trySearchDockerHub(ctx, query, page, pageSize)
|
}
|
||||||
if lastErr == nil && len(result.Results) == 0 {
|
time.Sleep(time.Second * time.Duration(4-retries))
|
||||||
// 如果精确搜索没有结果,尝试模糊搜索
|
continue
|
||||||
result, lastErr = trySearchDockerHub(ctx, parts[1], page, pageSize)
|
}
|
||||||
if lastErr == nil {
|
defer resp.Body.Close()
|
||||||
// 过滤出属于指定用户的结果
|
|
||||||
filteredResults := make([]Repository, 0)
|
body, err := io.ReadAll(resp.Body)
|
||||||
for _, repo := range result.Results {
|
if err != nil {
|
||||||
if strings.EqualFold(repo.Namespace, parts[0]) ||
|
lastErr = fmt.Errorf("读取响应失败: %v", err)
|
||||||
strings.EqualFold(repo.RepoOwner, parts[0]) {
|
if !isRetryableError(err) {
|
||||||
filteredResults = append(filteredResults, repo)
|
break
|
||||||
}
|
}
|
||||||
}
|
time.Sleep(time.Second * time.Duration(4-retries))
|
||||||
result.Results = filteredResults
|
continue
|
||||||
result.Count = len(filteredResults)
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusTooManyRequests:
|
||||||
|
lastErr = fmt.Errorf("请求过于频繁,请稍后重试")
|
||||||
|
case http.StatusNotFound:
|
||||||
|
lastErr = fmt.Errorf("未找到相关镜像")
|
||||||
|
case http.StatusBadGateway, http.StatusServiceUnavailable:
|
||||||
|
lastErr = fmt.Errorf("Docker Hub服务暂时不可用,请稍后重试")
|
||||||
|
default:
|
||||||
|
lastErr = fmt.Errorf("请求失败: 状态码=%d, 响应=%s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
if !isRetryableError(lastErr) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second * time.Duration(4-retries))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
if isUserRepo && namespace != "" {
|
||||||
|
// 解析用户仓库列表响应
|
||||||
|
var userRepos struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
Next string `json:"next"`
|
||||||
|
Previous string `json:"previous"`
|
||||||
|
Results []Repository `json:"results"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &userRepos); err != nil {
|
||||||
|
lastErr = fmt.Errorf("解析响应失败: %v", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为SearchResult格式
|
||||||
|
result = &SearchResult{
|
||||||
|
Count: userRepos.Count,
|
||||||
|
Next: userRepos.Next,
|
||||||
|
Previous: userRepos.Previous,
|
||||||
|
Results: make([]Repository, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理结果
|
||||||
|
for _, repo := range userRepos.Results {
|
||||||
|
// 如果指定了仓库名,只保留匹配的结果
|
||||||
|
if repoName == "" || strings.Contains(strings.ToLower(repo.Name), strings.ToLower(repoName)) {
|
||||||
|
repo.Namespace = namespace
|
||||||
|
result.Results = append(result.Results, repo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.Count = len(result.Results)
|
||||||
|
} else {
|
||||||
|
// 解析普通搜索响应
|
||||||
|
result = &SearchResult{}
|
||||||
|
if err := json.Unmarshal(body, &result); err != nil {
|
||||||
|
lastErr = fmt.Errorf("解析响应失败: %v", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理搜索结果
|
||||||
|
for i := range result.Results {
|
||||||
|
if result.Results[i].IsOfficial {
|
||||||
|
if !strings.Contains(result.Results[i].Name, "/") {
|
||||||
|
result.Results[i].Name = "library/" + result.Results[i].Name
|
||||||
|
}
|
||||||
|
result.Results[i].Namespace = "library"
|
||||||
|
} else {
|
||||||
|
parts := strings.Split(result.Results[i].Name, "/")
|
||||||
|
if len(parts) > 1 {
|
||||||
|
result.Results[i].Namespace = parts[0]
|
||||||
|
result.Results[i].Name = parts[1]
|
||||||
|
} else if result.Results[i].RepoOwner != "" {
|
||||||
|
result.Results[i].Namespace = result.Results[i].RepoOwner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
result, lastErr = trySearchDockerHub(ctx, query, page, pageSize)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if lastErr == nil {
|
// 成功获取结果,跳出重试循环
|
||||||
break
|
lastErr = nil
|
||||||
}
|
break
|
||||||
|
|
||||||
if !isRetryableError(lastErr) {
|
|
||||||
return nil, fmt.Errorf("搜索失败: %v", lastErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second * time.Duration(4-retries))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if lastErr != nil {
|
if lastErr != nil {
|
||||||
return nil, fmt.Errorf("搜索失败,已重试3次: %v", lastErr)
|
return nil, fmt.Errorf("搜索失败: %v", lastErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤和处理搜索结果
|
|
||||||
result.Results = filterSearchResults(result.Results, query)
|
|
||||||
result.Count = len(result.Results)
|
|
||||||
|
|
||||||
// 缓存结果
|
// 缓存结果
|
||||||
searchCache.Set(cacheKey, result)
|
searchCache.Set(cacheKey, result)
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,94 +412,6 @@ func isRetryableError(err error) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// trySearchDockerHub 执行实际的Docker Hub API请求
|
|
||||||
func trySearchDockerHub(ctx context.Context, query string, page, pageSize int) (*SearchResult, error) {
|
|
||||||
// 构建Docker Hub API请求
|
|
||||||
baseURL := "https://registry.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))
|
|
||||||
|
|
||||||
fullURL := baseURL + "?" + params.Encode()
|
|
||||||
fmt.Printf("搜索URL: %s\n", fullURL)
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("创建请求失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加必要的请求头
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
|
|
||||||
|
|
||||||
// 发送请求
|
|
||||||
client := &http.Client{
|
|
||||||
Timeout: 10 * time.Second,
|
|
||||||
Transport: &http.Transport{
|
|
||||||
MaxIdleConns: 100,
|
|
||||||
IdleConnTimeout: 90 * time.Second,
|
|
||||||
DisableCompression: true,
|
|
||||||
DisableKeepAlives: false,
|
|
||||||
MaxIdleConnsPerHost: 10,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("发送请求失败: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// 读取响应体
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("读取响应失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查响应状态码
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
switch resp.StatusCode {
|
|
||||||
case http.StatusTooManyRequests:
|
|
||||||
return nil, fmt.Errorf("请求过于频繁,请稍后重试")
|
|
||||||
case http.StatusNotFound:
|
|
||||||
return nil, fmt.Errorf("未找到相关镜像")
|
|
||||||
case http.StatusBadGateway, http.StatusServiceUnavailable:
|
|
||||||
return nil, fmt.Errorf("Docker Hub服务暂时不可用,请稍后重试")
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("请求失败: 状态码=%d, 响应=%s", resp.StatusCode, string(body))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析响应
|
|
||||||
var result SearchResult
|
|
||||||
if err := json.Unmarshal(body, &result); err != nil {
|
|
||||||
return nil, fmt.Errorf("解析响应失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理搜索结果
|
|
||||||
for i := range result.Results {
|
|
||||||
if result.Results[i].IsOfficial {
|
|
||||||
// 确保官方镜像有正确的名称格式
|
|
||||||
if !strings.Contains(result.Results[i].Name, "/") {
|
|
||||||
result.Results[i].Name = "library/" + result.Results[i].Name
|
|
||||||
}
|
|
||||||
result.Results[i].Namespace = "library"
|
|
||||||
} else {
|
|
||||||
// 从 repo_name 中提取 namespace
|
|
||||||
parts := strings.Split(result.Results[i].Name, "/")
|
|
||||||
if len(parts) > 1 {
|
|
||||||
result.Results[i].Namespace = parts[0]
|
|
||||||
result.Results[i].Name = parts[1]
|
|
||||||
} else if result.Results[i].RepoOwner != "" {
|
|
||||||
result.Results[i].Namespace = result.Results[i].RepoOwner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getRepositoryTags 获取仓库标签信息
|
// getRepositoryTags 获取仓库标签信息
|
||||||
func getRepositoryTags(ctx context.Context, namespace, name string) ([]TagInfo, error) {
|
func getRepositoryTags(ctx context.Context, namespace, name string) ([]TagInfo, error) {
|
||||||
if namespace == "" || name == "" {
|
if namespace == "" || name == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user