464 lines
12 KiB
Go
464 lines
12 KiB
Go
package mirror
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Mirror represents a package repository mirror
|
|
type Mirror struct {
|
|
URL string
|
|
Country string
|
|
Score float64
|
|
Latency time.Duration
|
|
Speed float64 // MB/s
|
|
LastTest time.Time
|
|
Error error
|
|
}
|
|
|
|
// MirrorList manages a collection of mirrors
|
|
type MirrorList struct {
|
|
Mirrors []Mirror
|
|
mutex sync.RWMutex
|
|
}
|
|
|
|
// TestResult contains the results of testing a mirror
|
|
type TestResult struct {
|
|
Mirror Mirror
|
|
Success bool
|
|
Error error
|
|
}
|
|
|
|
// DefaultMirrors contains known mirrors for each distribution family
|
|
var DefaultMirrors = map[string][]string{
|
|
"debian": {
|
|
"http://deb.debian.org/debian/",
|
|
"http://ftp.us.debian.org/debian/",
|
|
"http://ftp.uk.debian.org/debian/",
|
|
"http://ftp.de.debian.org/debian/",
|
|
"http://mirror.kakao.com/debian/",
|
|
"http://mirrors.kernel.org/debian/",
|
|
"http://mirror.math.princeton.edu/pub/debian/",
|
|
"http://debian.osuosl.org/debian/",
|
|
},
|
|
"arch": {
|
|
"https://mirror.rackspace.com/archlinux/",
|
|
"https://mirrors.kernel.org/archlinux/",
|
|
"https://mirror.math.princeton.edu/pub/archlinux/",
|
|
"https://mirrors.mit.edu/archlinux/",
|
|
"https://mirror.cs.vt.edu/pub/ArchLinux/",
|
|
"https://mirrors.liquidweb.com/archlinux/",
|
|
"https://arch.mirror.constant.com/",
|
|
"https://america.mirror.pkgbuild.com/",
|
|
},
|
|
"fedora": {
|
|
"https://download.fedoraproject.org/pub/fedora/linux/",
|
|
"https://mirrors.kernel.org/fedora/",
|
|
"https://mirror.math.princeton.edu/pub/fedora/linux/",
|
|
"https://mirrors.mit.edu/fedora/linux/",
|
|
"https://fedora.mirror.constant.com/",
|
|
"https://mirror.cs.vt.edu/pub/fedora/linux/",
|
|
"https://mirrors.liquidweb.com/fedora/",
|
|
"https://ftp.osuosl.org/pub/fedora/linux/",
|
|
},
|
|
}
|
|
|
|
// NewMirrorList creates a new mirror list for the given distribution family
|
|
func NewMirrorList(family string) *MirrorList {
|
|
ml := &MirrorList{
|
|
Mirrors: make([]Mirror, 0),
|
|
}
|
|
return ml
|
|
}
|
|
|
|
// LoadMirrors fetches and loads mirrors for the distribution family
|
|
func (ml *MirrorList) LoadMirrors(ctx context.Context, family string) error {
|
|
fetcher := NewMirrorFetcher()
|
|
|
|
fmt.Printf("Fetching complete mirror list for %s...\n", family)
|
|
|
|
mirrors, err := fetcher.FetchMirrors(ctx, family)
|
|
if err != nil {
|
|
fmt.Printf("Failed to fetch mirrors online, using fallback list: %v\n", err)
|
|
mirrors = fetcher.getDefaultMirrors(family)
|
|
}
|
|
|
|
if len(mirrors) == 0 {
|
|
return fmt.Errorf("no mirrors available for %s", family)
|
|
}
|
|
|
|
ml.mutex.Lock()
|
|
defer ml.mutex.Unlock()
|
|
|
|
ml.Mirrors = make([]Mirror, 0, len(mirrors))
|
|
for _, mirrorURL := range mirrors {
|
|
ml.Mirrors = append(ml.Mirrors, Mirror{
|
|
URL: mirrorURL,
|
|
})
|
|
}
|
|
|
|
fmt.Printf("Loaded %d mirrors for testing\n", len(ml.Mirrors))
|
|
return nil
|
|
}
|
|
|
|
// TestMirrors tests all mirrors concurrently and updates their scores
|
|
func (ml *MirrorList) TestMirrors(ctx context.Context, timeout time.Duration) error {
|
|
if len(ml.Mirrors) == 0 {
|
|
return fmt.Errorf("no mirrors to test - this is awkward")
|
|
}
|
|
|
|
ml.mutex.Lock()
|
|
defer ml.mutex.Unlock()
|
|
|
|
results := make(chan TestResult, len(ml.Mirrors))
|
|
var wg sync.WaitGroup
|
|
|
|
fmt.Printf("Testing %d mirrors concurrently...\n", len(ml.Mirrors))
|
|
|
|
// Test each mirror concurrently
|
|
for i := range ml.Mirrors {
|
|
wg.Add(1)
|
|
go func(mirror *Mirror) {
|
|
defer wg.Done()
|
|
testResult := ml.testSingleMirror(ctx, mirror, timeout)
|
|
results <- testResult
|
|
}(&ml.Mirrors[i])
|
|
}
|
|
|
|
// Wait for all tests to complete
|
|
go func() {
|
|
wg.Wait()
|
|
close(results)
|
|
}()
|
|
|
|
// Collect results and count successes
|
|
successCount := 0
|
|
failureCount := 0
|
|
|
|
for result := range results {
|
|
for i := range ml.Mirrors {
|
|
if ml.Mirrors[i].URL == result.Mirror.URL {
|
|
ml.Mirrors[i] = result.Mirror
|
|
if result.Success && result.Mirror.Error == nil {
|
|
successCount++
|
|
} else {
|
|
failureCount++
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Printf("Testing complete: %d successful, %d failed\n", successCount, failureCount)
|
|
|
|
// Sort by score (higher is better), but put failed mirrors at the end
|
|
sort.Slice(ml.Mirrors, func(i, j int) bool {
|
|
// If one has an error and the other doesn't, prioritize the working one
|
|
if (ml.Mirrors[i].Error != nil) != (ml.Mirrors[j].Error != nil) {
|
|
return ml.Mirrors[j].Error != nil // Working mirrors come first
|
|
}
|
|
// If both are working or both have errors, sort by score
|
|
return ml.Mirrors[i].Score > ml.Mirrors[j].Score
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// testSingleMirror tests a single mirror's speed and latency
|
|
func (ml *MirrorList) testSingleMirror(ctx context.Context, mirror *Mirror, timeout time.Duration) TestResult {
|
|
testCtx, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
|
|
// Test latency with HEAD request first
|
|
latency, err := ml.testLatency(testCtx, mirror.URL)
|
|
if err != nil {
|
|
return TestResult{
|
|
Mirror: Mirror{
|
|
URL: mirror.URL,
|
|
Country: mirror.Country,
|
|
Error: fmt.Errorf("latency test failed: %w", err),
|
|
LastTest: time.Now(),
|
|
},
|
|
Success: false,
|
|
Error: err,
|
|
}
|
|
}
|
|
|
|
// Validate that the mirror actually serves the required package databases
|
|
err = ml.validateMirror(testCtx, mirror.URL)
|
|
if err != nil {
|
|
return TestResult{
|
|
Mirror: Mirror{
|
|
URL: mirror.URL,
|
|
Country: mirror.Country,
|
|
Latency: latency,
|
|
Error: fmt.Errorf("validation failed: %w", err),
|
|
LastTest: time.Now(),
|
|
},
|
|
Success: false,
|
|
Error: err,
|
|
}
|
|
}
|
|
|
|
// Test download speed with a small file
|
|
speed, err := ml.testSpeed(testCtx, mirror.URL)
|
|
if err != nil {
|
|
return TestResult{
|
|
Mirror: Mirror{
|
|
URL: mirror.URL,
|
|
Country: mirror.Country,
|
|
Latency: latency,
|
|
Error: fmt.Errorf("speed test failed: %w", err),
|
|
LastTest: time.Now(),
|
|
},
|
|
Success: false,
|
|
Error: err,
|
|
}
|
|
}
|
|
|
|
// Calculate score based on speed and latency
|
|
score := ml.calculateScore(speed, latency)
|
|
|
|
return TestResult{
|
|
Mirror: Mirror{
|
|
URL: mirror.URL,
|
|
Country: mirror.Country,
|
|
Score: score,
|
|
Latency: latency,
|
|
Speed: speed,
|
|
LastTest: time.Now(),
|
|
Error: nil,
|
|
},
|
|
Success: true,
|
|
}
|
|
}
|
|
|
|
// testLatency measures response time to a mirror
|
|
func (ml *MirrorList) testLatency(ctx context.Context, mirrorURL string) (time.Duration, error) {
|
|
start := time.Now()
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "HEAD", mirrorURL, nil)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
client := &http.Client{
|
|
Timeout: 2 * time.Second, // 2000ms timeout as requested
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return 0, fmt.Errorf("HTTP %d: mirror seems to be having a bad day", resp.StatusCode)
|
|
}
|
|
|
|
return time.Since(start), nil
|
|
}
|
|
|
|
// testSpeed measures download speed from a mirror
|
|
func (ml *MirrorList) testSpeed(ctx context.Context, mirrorURL string) (float64, error) {
|
|
// Try to download a standard test file (like Release file for Debian)
|
|
testPath := ml.getTestPath(mirrorURL)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", testPath, nil)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
client := &http.Client{
|
|
Timeout: 15 * time.Second, // Reasonable timeout for downloading test files
|
|
}
|
|
|
|
start := time.Now()
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return 0, fmt.Errorf("HTTP %d: couldn't download test file", resp.StatusCode)
|
|
}
|
|
|
|
// Read response and measure time
|
|
bytesRead, err := io.Copy(io.Discard, resp.Body)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
duration := time.Since(start)
|
|
if duration == 0 {
|
|
return 0, fmt.Errorf("download completed too quickly to measure")
|
|
}
|
|
|
|
// Calculate speed in MB/s
|
|
speed := float64(bytesRead) / duration.Seconds() / (1024 * 1024)
|
|
return speed, nil
|
|
}
|
|
|
|
// validateMirror checks if the mirror actually serves the required package databases
|
|
func (ml *MirrorList) validateMirror(ctx context.Context, mirrorURL string) error {
|
|
// For Arch mirrors, test multiple repositories to ensure they're properly formatted
|
|
if contains(mirrorURL, "archlinux") {
|
|
// Test all major repositories that pacman needs
|
|
repositories := []string{"core", "extra", "multilib"}
|
|
|
|
for _, repo := range repositories {
|
|
testURL := mirrorURL + repo + "/os/x86_64/" + repo + ".db"
|
|
err := ml.checkFileExists(ctx, testURL)
|
|
if err != nil {
|
|
return fmt.Errorf("repository %s not found: %w", repo, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// For Debian mirrors, test Release file
|
|
if contains(mirrorURL, "debian") {
|
|
testURL := mirrorURL + "dists/stable/Release"
|
|
return ml.checkFileExists(ctx, testURL)
|
|
}
|
|
|
|
// For Fedora, test repomd.xml
|
|
if contains(mirrorURL, "fedora") {
|
|
testURL := mirrorURL + "releases/39/Everything/x86_64/os/repodata/repomd.xml"
|
|
return ml.checkFileExists(ctx, testURL)
|
|
}
|
|
|
|
// For unknown mirrors, just test basic connectivity
|
|
return nil
|
|
}
|
|
|
|
// checkFileExists verifies that a specific file exists on the mirror
|
|
func (ml *MirrorList) checkFileExists(ctx context.Context, fileURL string) error {
|
|
req, err := http.NewRequestWithContext(ctx, "HEAD", fileURL, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client := &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == 404 {
|
|
return fmt.Errorf("required package database not found (404)")
|
|
}
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return fmt.Errorf("HTTP %d: mirror doesn't serve required files", resp.StatusCode)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getTestPath returns an appropriate test file path for the mirror
|
|
func (ml *MirrorList) getTestPath(mirrorURL string) string {
|
|
// For Debian-based mirrors, try Release file
|
|
if contains(mirrorURL, "debian") {
|
|
return mirrorURL + "dists/stable/Release"
|
|
}
|
|
|
|
// For Arch mirrors, try core.db
|
|
if contains(mirrorURL, "archlinux") {
|
|
return mirrorURL + "core/os/x86_64/core.db"
|
|
}
|
|
|
|
// For Fedora, try repomd.xml
|
|
if contains(mirrorURL, "fedora") {
|
|
return mirrorURL + "releases/39/Everything/x86_64/os/repodata/repomd.xml"
|
|
}
|
|
|
|
// Fallback: just try the base URL
|
|
return mirrorURL
|
|
}
|
|
|
|
// calculateScore calculates a score based on speed and latency
|
|
func (ml *MirrorList) calculateScore(speed float64, latency time.Duration) float64 {
|
|
// Higher speed is better, lower latency is better
|
|
// Normalize and combine metrics
|
|
speedScore := speed * 10 // Weight speed heavily
|
|
latencyScore := 1000.0 / float64(latency.Milliseconds()) // Lower latency = higher score
|
|
|
|
return speedScore + latencyScore
|
|
}
|
|
|
|
// GetBest returns the best performing mirror
|
|
func (ml *MirrorList) GetBest() *Mirror {
|
|
ml.mutex.RLock()
|
|
defer ml.mutex.RUnlock()
|
|
|
|
if len(ml.Mirrors) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Return the first mirror (should be highest scoring after sorting)
|
|
for _, mirror := range ml.Mirrors {
|
|
if mirror.Error == nil {
|
|
return &mirror
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetTop returns the top N mirrors (only successful ones)
|
|
func (ml *MirrorList) GetTop(n int) []Mirror {
|
|
ml.mutex.RLock()
|
|
defer ml.mutex.RUnlock()
|
|
|
|
result := make([]Mirror, 0, n)
|
|
count := 0
|
|
|
|
for _, mirror := range ml.Mirrors {
|
|
if mirror.Error == nil && count < n {
|
|
result = append(result, mirror)
|
|
count++
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// GetAll returns all mirrors with their test results
|
|
func (ml *MirrorList) GetAll() []Mirror {
|
|
ml.mutex.RLock()
|
|
defer ml.mutex.RUnlock()
|
|
|
|
result := make([]Mirror, len(ml.Mirrors))
|
|
copy(result, ml.Mirrors)
|
|
return result
|
|
}
|
|
|
|
// contains is a helper function to check if a string contains a substring
|
|
func contains(s, substr string) bool {
|
|
return len(s) >= len(substr) &&
|
|
(s == substr ||
|
|
s[:len(substr)] == substr ||
|
|
s[len(s)-len(substr):] == substr ||
|
|
findInString(s, substr))
|
|
}
|
|
|
|
func findInString(s, substr string) bool {
|
|
for i := 0; i <= len(s)-len(substr); i++ {
|
|
if s[i:i+len(substr)] == substr {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|