Skip to content

Commit 319600d

Browse files
committed
feat: Implement proper container pull count fetching
- Added Docker Hub API integration for pull counts - Added GHCR API integration (though GitHub doesn't expose download counts) - Removed fake pull count increments - Now fetches real data where available: - Docker Hub: Returns actual pull counts - GHCR: Returns 0 (API limitation, not available even with auth) - Other registries: Returns 0 The tool now only updates with real data, no fake increments.
1 parent a7657dc commit 319600d

File tree

1 file changed

+208
-9
lines changed

1 file changed

+208
-9
lines changed

cmd/regup/main.go

Lines changed: 208 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -167,24 +167,40 @@ func updateServerInfo(server serverWithName) error {
167167

168168
// Extract owner and repo from repository URL
169169
var newStars, newPulls int
170+
171+
// Get GitHub stars if we have a repository URL
170172
if repoURL != "" {
171173
owner, repo, err := extractOwnerRepo(repoURL)
172174
if err != nil {
173175
logger.Warnf("Failed to extract owner/repo from URL %s: %v", repoURL, err)
176+
newStars = currentStars
174177
} else {
175178
// Get repository info from GitHub API
176-
stars, pulls, err := getGitHubRepoInfo(owner, repo, server.name, currentPulls)
179+
stars, _, err := getGitHubRepoInfo(owner, repo, server.name, currentPulls)
177180
if err != nil {
178181
logger.Warnf("Failed to get GitHub repo info for %s: %v", server.name, err)
179182
newStars = currentStars
180-
newPulls = currentPulls
181183
} else {
182184
newStars = stars
183-
newPulls = pulls
184185
}
185186
}
186187
} else {
187188
newStars = currentStars
189+
}
190+
191+
// Get container pull count if we have an image
192+
if server.entry.Image != "" {
193+
pullCount, err := getContainerPullCount(server.entry.Image)
194+
if err != nil {
195+
logger.Warnf("Failed to get pull count for image %s: %v", server.entry.Image, err)
196+
newPulls = currentPulls
197+
} else if pullCount > 0 {
198+
newPulls = pullCount
199+
} else {
200+
// No pull count available (GHCR or private registry)
201+
newPulls = currentPulls
202+
}
203+
} else {
188204
newPulls = currentPulls
189205
}
190206

@@ -376,7 +392,7 @@ func extractOwnerRepo(url string) (string, string, error) {
376392
return owner, repo, nil
377393
}
378394

379-
// getGitHubRepoInfo gets the stars and downloads count for a GitHub repository
395+
// getGitHubRepoInfo gets the stars count for a GitHub repository
380396
func getGitHubRepoInfo(owner, repo, serverName string, currentPulls int) (stars int, pulls int, err error) {
381397
// Create HTTP client with timeout
382398
client := &http.Client{
@@ -417,10 +433,193 @@ func getGitHubRepoInfo(owner, repo, serverName string, currentPulls int) (stars
417433
return 0, 0, fmt.Errorf("failed to parse response: %w", err)
418434
}
419435

420-
// For pulls/downloads, increment by a small amount
421-
// In a real implementation, you would query Docker Hub API for actual pull counts
422-
increment := 50 + (len(serverName) % 100)
423-
pulls = currentPulls + increment
436+
// Return current pulls - we'll fetch container pulls separately
437+
return repoInfo.StargazersCount, currentPulls, nil
438+
}
439+
440+
// getContainerPullCount fetches the pull count for a container image
441+
func getContainerPullCount(image string) (int, error) {
442+
// Parse the image reference
443+
parts := strings.Split(image, ":")
444+
if len(parts) < 1 {
445+
return 0, fmt.Errorf("invalid image format: %s", image)
446+
}
447+
448+
imageName := parts[0]
449+
450+
// Determine registry and fetch accordingly
451+
if strings.HasPrefix(imageName, "ghcr.io/") {
452+
return getGHCRPullCount(imageName)
453+
} else if strings.Contains(imageName, "/") && !strings.Contains(imageName, ".") {
454+
// Likely Docker Hub (no dots in the hostname part)
455+
return getDockerHubPullCount(imageName)
456+
}
457+
458+
// Unknown registry, return 0
459+
logger.Warnf("Unknown registry for image %s, cannot fetch pull count", image)
460+
return 0, nil
461+
}
462+
463+
// getGHCRPullCount fetches pull count for GitHub Container Registry images
464+
func getGHCRPullCount(imageName string) (int, error) {
465+
// GHCR requires authentication to get package statistics
466+
if githubToken == "" {
467+
logger.Debugf("No GitHub token available, cannot fetch GHCR pull count for %s", imageName)
468+
return 0, nil
469+
}
470+
471+
// Parse the image name: ghcr.io/owner/repo/package or ghcr.io/owner/package
472+
imageName = strings.TrimPrefix(imageName, "ghcr.io/")
473+
parts := strings.Split(imageName, "/")
474+
if len(parts) < 2 {
475+
return 0, fmt.Errorf("invalid GHCR image format: %s", imageName)
476+
}
477+
478+
owner := parts[0]
479+
// The package name is everything after the owner
480+
packageName := strings.Join(parts[1:], "/")
481+
482+
// GitHub Packages API endpoint for container packages
483+
// Note: This requires the package to be public or the token to have package:read scope
484+
url := fmt.Sprintf("https://api.github.com/users/%s/packages/container/%s", owner, packageName)
485+
486+
client := &http.Client{
487+
Timeout: 10 * time.Second,
488+
}
489+
490+
req, err := http.NewRequestWithContext(context.Background(), "GET", url, nil)
491+
if err != nil {
492+
// Try org endpoint if user endpoint fails
493+
url = fmt.Sprintf("https://api.github.com/orgs/%s/packages/container/%s", owner, packageName)
494+
req, err = http.NewRequestWithContext(context.Background(), "GET", url, nil)
495+
if err != nil {
496+
return 0, fmt.Errorf("failed to create request: %w", err)
497+
}
498+
}
499+
500+
req.Header.Add("Accept", "application/vnd.github.v3+json")
501+
req.Header.Add("Authorization", "token "+githubToken)
502+
503+
resp, err := client.Do(req)
504+
if err != nil {
505+
return 0, fmt.Errorf("failed to send request: %w", err)
506+
}
507+
defer resp.Body.Close()
508+
509+
if resp.StatusCode == http.StatusNotFound {
510+
// Try org endpoint if user endpoint returned 404
511+
if strings.Contains(url, "/users/") {
512+
url = strings.Replace(url, "/users/", "/orgs/", 1)
513+
req, err = http.NewRequestWithContext(context.Background(), "GET", url, nil)
514+
if err != nil {
515+
return 0, fmt.Errorf("failed to create request: %w", err)
516+
}
517+
518+
req.Header.Add("Accept", "application/vnd.github.v3+json")
519+
req.Header.Add("Authorization", "token "+githubToken)
520+
521+
resp, err = client.Do(req)
522+
if err != nil {
523+
return 0, fmt.Errorf("failed to send request: %w", err)
524+
}
525+
defer resp.Body.Close()
526+
}
527+
}
528+
529+
if resp.StatusCode != http.StatusOK {
530+
// Package not found or no access - return 0
531+
logger.Debugf("Could not fetch GHCR package stats (status %d) for %s", resp.StatusCode, imageName)
532+
return 0, nil
533+
}
534+
535+
// GitHub Packages API doesn't directly expose download count in the package endpoint
536+
// We need to get package versions and sum their download counts
537+
var packageInfo struct {
538+
ID int `json:"id"`
539+
Name string `json:"name"`
540+
}
541+
542+
if err := json.NewDecoder(resp.Body).Decode(&packageInfo); err != nil {
543+
return 0, fmt.Errorf("failed to parse response: %w", err)
544+
}
545+
546+
// Now get all versions to sum download counts
547+
versionsURL := fmt.Sprintf("%s/versions?per_page=100", url)
548+
req, err = http.NewRequestWithContext(context.Background(), "GET", versionsURL, nil)
549+
if err != nil {
550+
return 0, fmt.Errorf("failed to create versions request: %w", err)
551+
}
552+
553+
req.Header.Add("Accept", "application/vnd.github.v3+json")
554+
req.Header.Add("Authorization", "token "+githubToken)
555+
556+
resp, err = client.Do(req)
557+
if err != nil {
558+
return 0, fmt.Errorf("failed to send versions request: %w", err)
559+
}
560+
defer resp.Body.Close()
561+
562+
if resp.StatusCode != http.StatusOK {
563+
logger.Debugf("Could not fetch GHCR package versions (status %d) for %s", resp.StatusCode, imageName)
564+
return 0, nil
565+
}
566+
567+
var versions []struct {
568+
Metadata struct {
569+
Container struct {
570+
Tags []string `json:"tags"`
571+
} `json:"container"`
572+
} `json:"metadata"`
573+
// Unfortunately, GitHub API doesn't expose download_count for container packages
574+
// in the same way it does for other package types
575+
}
576+
577+
if err := json.NewDecoder(resp.Body).Decode(&versions); err != nil {
578+
return 0, fmt.Errorf("failed to parse versions response: %w", err)
579+
}
580+
581+
// GitHub doesn't expose container download counts through the API
582+
// even with authentication. This is a known limitation.
583+
// Return 0 to indicate we couldn't get the data
584+
logger.Debugf("GHCR package found but download count not available through API for %s", imageName)
585+
return 0, nil
586+
}
424587

425-
return repoInfo.StargazersCount, pulls, nil
588+
// getDockerHubPullCount fetches pull count for Docker Hub images
589+
func getDockerHubPullCount(imageName string) (int, error) {
590+
// Remove docker.io prefix if present
591+
imageName = strings.TrimPrefix(imageName, "docker.io/")
592+
593+
// Docker Hub API endpoint
594+
url := fmt.Sprintf("https://hub.docker.com/v2/repositories/%s/", imageName)
595+
596+
client := &http.Client{
597+
Timeout: 10 * time.Second,
598+
}
599+
600+
req, err := http.NewRequestWithContext(context.Background(), "GET", url, nil)
601+
if err != nil {
602+
return 0, fmt.Errorf("failed to create request: %w", err)
603+
}
604+
605+
resp, err := client.Do(req)
606+
if err != nil {
607+
return 0, fmt.Errorf("failed to send request: %w", err)
608+
}
609+
defer resp.Body.Close()
610+
611+
if resp.StatusCode != http.StatusOK {
612+
// Not found or error - return 0
613+
return 0, nil
614+
}
615+
616+
var dockerHubResp struct {
617+
PullCount int `json:"pull_count"`
618+
}
619+
620+
if err := json.NewDecoder(resp.Body).Decode(&dockerHubResp); err != nil {
621+
return 0, fmt.Errorf("failed to parse response: %w", err)
622+
}
623+
624+
return dockerHubResp.PullCount, nil
426625
}

0 commit comments

Comments
 (0)