This commit is contained in:
Dev
2025-09-13 02:47:20 +03:00
commit 67b170415b
16 changed files with 2187 additions and 0 deletions

269
internal/mirror/fetcher.go Normal file
View File

@@ -0,0 +1,269 @@
package mirror
import (
"bufio"
"context"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"time"
)
// MirrorFetcher handles fetching mirror lists from official sources
type MirrorFetcher struct {
client *http.Client
}
// NewMirrorFetcher creates a new mirror fetcher
func NewMirrorFetcher() *MirrorFetcher {
return &MirrorFetcher{
client: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// FetchMirrors gets the complete list of mirrors for a distribution family
func (mf *MirrorFetcher) FetchMirrors(ctx context.Context, family string) ([]string, error) {
switch family {
case "arch":
return mf.fetchArchMirrors(ctx)
case "debian":
return mf.fetchDebianMirrors(ctx)
case "fedora":
return mf.fetchFedoraMirrors(ctx)
default:
// Fallback to hardcoded mirrors for unsupported families
return mf.getDefaultMirrors(family), nil
}
}
// fetchArchMirrors fetches Arch Linux mirrors from the official mirror status
func (mf *MirrorFetcher) fetchArchMirrors(ctx context.Context) ([]string, error) {
// Arch Linux provides a JSON API for mirror status
url := "https://archlinux.org/mirrorlist/?protocol=https&use_mirror_status=on"
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
resp, err := mf.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch Arch mirrors: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d from Arch mirror API", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
return mf.parseArchMirrorList(string(body))
}
// parseArchMirrorList parses the Arch Linux mirror list format
func (mf *MirrorFetcher) parseArchMirrorList(content string) ([]string, error) {
var mirrors []string
scanner := bufio.NewScanner(strings.NewReader(content))
// Look for lines like: #Server = https://mirror.example.com/archlinux/$repo/os/$arch
serverRegex := regexp.MustCompile(`^#?Server\s*=\s*(.+)`)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if matches := serverRegex.FindStringSubmatch(line); matches != nil {
mirrorURL := strings.TrimSpace(matches[1])
// Remove the $repo/os/$arch part for base URL
if strings.Contains(mirrorURL, "$repo") {
mirrorURL = strings.Replace(mirrorURL, "/$repo/os/$arch", "/", 1)
}
if mirrorURL != "" && !strings.Contains(mirrorURL, "$") {
mirrors = append(mirrors, mirrorURL)
}
}
}
if len(mirrors) == 0 {
return nil, fmt.Errorf("no mirrors found in Arch Linux mirror list")
}
return mirrors, nil
}
// fetchDebianMirrors fetches Debian mirrors from the official mirror list
func (mf *MirrorFetcher) fetchDebianMirrors(ctx context.Context) ([]string, error) {
// Debian maintains a list of mirrors
url := "https://www.debian.org/mirror/list-full"
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
resp, err := mf.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch Debian mirrors: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d from Debian mirror list", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
return mf.parseDebianMirrorList(string(body))
}
// parseDebianMirrorList parses the Debian mirror list HTML
func (mf *MirrorFetcher) parseDebianMirrorList(content string) ([]string, error) {
var mirrors []string
// Look for HTTP/HTTPS URLs in the HTML
urlRegex := regexp.MustCompile(`https?://[a-zA-Z0-9.-]+[a-zA-Z0-9.-/]*debian[a-zA-Z0-9.-/]*/?`)
matches := urlRegex.FindAllString(content, -1)
seen := make(map[string]bool)
for _, match := range matches {
// Clean up and normalize the URL
mirror := strings.TrimSpace(match)
mirror = strings.TrimSuffix(mirror, "/") + "/"
// Avoid duplicates and ensure it's a proper mirror
if !seen[mirror] && strings.Contains(mirror, "debian") {
mirrors = append(mirrors, mirror)
seen[mirror] = true
}
}
// Add some known good mirrors if we don't find enough
knownMirrors := []string{
"http://deb.debian.org/debian/",
"http://ftp.us.debian.org/debian/",
"http://ftp.uk.debian.org/debian/",
"http://ftp.de.debian.org/debian/",
"http://mirrors.kernel.org/debian/",
}
for _, known := range knownMirrors {
if !seen[known] {
mirrors = append(mirrors, known)
seen[known] = true
}
}
if len(mirrors) == 0 {
return nil, fmt.Errorf("no mirrors found in Debian mirror list")
}
return mirrors, nil
}
// fetchFedoraMirrors fetches Fedora mirrors from the official mirror list
func (mf *MirrorFetcher) fetchFedoraMirrors(ctx context.Context) ([]string, error) {
// Fedora mirror list URL
url := "https://mirrors.fedoraproject.org/mirrorlist?repo=fedora-39&arch=x86_64"
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
resp, err := mf.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch Fedora mirrors: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d from Fedora mirror list", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
return mf.parseFedoraMirrorList(string(body))
}
// parseFedoraMirrorList parses the Fedora mirror list format
func (mf *MirrorFetcher) parseFedoraMirrorList(content string) ([]string, error) {
var mirrors []string
scanner := bufio.NewScanner(strings.NewReader(content))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "http") {
// Extract base URL (remove specific paths)
if idx := strings.Index(line, "/linux/"); idx != -1 {
baseURL := line[:idx] + "/fedora/linux/"
mirrors = append(mirrors, baseURL)
} else {
mirrors = append(mirrors, line)
}
}
}
if len(mirrors) == 0 {
return nil, fmt.Errorf("no mirrors found in Fedora mirror list")
}
return mirrors, nil
}
// getDefaultMirrors returns hardcoded mirrors as fallback
func (mf *MirrorFetcher) getDefaultMirrors(family string) []string {
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://mirrors.kernel.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://mirrors.liquidweb.com/archlinux/",
},
"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/",
},
"opensuse": {
"http://download.opensuse.org/distribution/",
"http://ftp.gwdg.de/pub/linux/opensuse/distribution/",
"http://ftp.halifax.rwth-aachen.de/opensuse/distribution/",
},
"gentoo": {
"https://gentoo.osuosl.org/",
"http://mirrors.kernel.org/gentoo/",
"https://mirror.bytemark.co.uk/gentoo/",
},
"alpine": {
"http://dl-cdn.alpinelinux.org/alpine/",
"http://mirrors.dotsrc.org/alpine/",
"http://mirror.fit.cvut.cz/alpine/",
},
}
if mirrors, exists := defaultMirrors[family]; exists {
return mirrors
}
return []string{}
}

463
internal/mirror/tester.go Normal file
View File

@@ -0,0 +1,463 @@
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
}