fastmirror/fastmirror.go

364 lines
9.3 KiB
Go
Raw Normal View History

2024-05-22 15:50:21 +00:00
package fastmirror
import (
2024-05-22 16:14:11 +00:00
"bufio"
2024-05-22 16:37:29 +00:00
_ "embed"
2024-05-22 15:50:21 +00:00
"fmt"
2024-05-22 16:37:29 +00:00
"github.com/schollz/progressbar/v3"
"html/template"
2024-05-22 16:14:11 +00:00
"io"
2024-05-22 15:50:21 +00:00
"io/ioutil"
"log"
2024-05-22 16:14:11 +00:00
"net/http"
2024-05-22 15:50:21 +00:00
"os"
"os/exec"
"path/filepath"
"runtime"
2024-05-22 16:14:11 +00:00
"strconv"
2024-05-22 15:50:21 +00:00
"strings"
2024-05-22 16:14:11 +00:00
"time"
2024-05-22 15:50:21 +00:00
)
2024-05-22 16:37:29 +00:00
//go:embed sources_list_legacy.tmpl
var legacyTemplate string
//go:embed sources_list_modern.tmpl
var modernTemplate string
2024-05-22 15:53:09 +00:00
func CLIEntry() {
2024-05-22 16:37:29 +00:00
log.Println("Starting Fastmirror CLI")
2024-05-22 15:50:21 +00:00
if runtime.GOOS != "linux" {
log.Fatal("This program is only for Linux")
}
2024-05-22 16:37:29 +00:00
if err := isUbuntu(); err != nil {
log.Fatal(err)
2024-05-22 15:50:21 +00:00
}
2024-05-22 16:37:29 +00:00
sourcesFilePath, err := findSourcesFilePath()
if err != nil {
log.Fatal(err)
}
log.Printf("Found sources file: %s", sourcesFilePath)
2024-05-22 15:50:21 +00:00
2024-05-22 16:37:29 +00:00
// Extract suites from the existing sources file
suites, err := extractSuites(sourcesFilePath)
if err != nil {
log.Fatal(err)
}
log.Printf("Extracted suites: %v", suites)
2024-05-22 15:50:21 +00:00
2024-05-22 16:14:11 +00:00
// Create backup of the sources file
2024-05-22 16:37:29 +00:00
if err := createBackup(sourcesFilePath); err != nil {
log.Fatal(err)
}
2024-05-22 16:14:11 +00:00
// Create sources.list.d directory if it doesn't exist
2024-05-22 16:37:29 +00:00
if err := createSourcesListD(); err != nil {
log.Fatal(err)
}
2024-05-22 16:14:11 +00:00
// Fetch and find the fastest mirror
2024-05-22 16:37:29 +00:00
fastestMirrorURL, fastestTime, latencies, err := findFastestMirror()
if err != nil {
log.Fatal(err)
}
log.Printf("Fastest mirror: %s", fastestMirrorURL)
2024-05-22 16:14:11 +00:00
2024-05-22 16:37:29 +00:00
// Display latency statistics
displayLatencyStatistics(latencies, fastestMirrorURL, fastestTime)
2024-05-22 16:14:11 +00:00
// Update sources file with the fastest mirror
2024-05-22 16:37:29 +00:00
if err := updateSourcesFile(sourcesFilePath, fastestMirrorURL, suites); err != nil {
log.Fatal(err)
}
log.Println("Fastmirror CLI finished successfully")
2024-05-22 15:50:21 +00:00
}
2024-05-22 16:37:29 +00:00
func getUbuntuCodename() (string, error) {
2024-05-22 15:50:21 +00:00
cmd := exec.Command("lsb_release", "-cs")
output, err := cmd.Output()
if err != nil {
2024-05-22 16:37:29 +00:00
return "", fmt.Errorf("failed to run lsb_release: %v", err)
2024-05-22 15:50:21 +00:00
}
2024-05-22 16:37:29 +00:00
return strings.TrimSpace(string(output)), nil
2024-05-22 15:50:21 +00:00
}
2024-05-22 16:37:29 +00:00
func getUbuntuVersion() (int, int, error) {
2024-05-22 16:14:11 +00:00
cmd := exec.Command("lsb_release", "-rs")
output, err := cmd.Output()
if err != nil {
2024-05-22 16:37:29 +00:00
return 0, 0, fmt.Errorf("failed to run lsb_release: %v", err)
2024-05-22 16:14:11 +00:00
}
version := strings.TrimSpace(string(output))
parts := strings.Split(version, ".")
if len(parts) != 2 {
2024-05-22 16:37:29 +00:00
return 0, 0, fmt.Errorf("unexpected version format: %s", version)
2024-05-22 16:14:11 +00:00
}
majorVersion, err := strconv.Atoi(parts[0])
if err != nil {
2024-05-22 16:37:29 +00:00
return 0, 0, fmt.Errorf("failed to parse major version: %v", err)
2024-05-22 16:14:11 +00:00
}
minorVersion, err := strconv.Atoi(parts[1])
if err != nil {
2024-05-22 16:37:29 +00:00
return 0, 0, fmt.Errorf("failed to parse minor version: %v", err)
2024-05-22 16:14:11 +00:00
}
2024-05-22 16:37:29 +00:00
return majorVersion, minorVersion, nil
2024-05-22 16:14:11 +00:00
}
2024-05-22 16:37:29 +00:00
func isUbuntu() error {
2024-05-22 15:50:21 +00:00
cmd := exec.Command("lsb_release", "-is")
output, err := cmd.Output()
if err != nil {
2024-05-22 16:37:29 +00:00
return fmt.Errorf("failed to run lsb_release: %v", err)
2024-05-22 15:50:21 +00:00
}
2024-05-22 16:37:29 +00:00
if strings.TrimSpace(string(output)) != "Ubuntu" {
return fmt.Errorf("this program is only for Ubuntu")
}
return nil
2024-05-22 15:50:21 +00:00
}
2024-05-22 16:37:29 +00:00
func findSourcesFilePath() (string, error) {
2024-05-22 16:14:11 +00:00
const sourcesListPath = "/etc/apt/sources.list"
const sourcesListDPath = "/etc/apt/sources.list.d/"
if isUbuntuSourcesFile(sourcesListPath) {
2024-05-22 16:37:29 +00:00
log.Printf("Found Ubuntu sources file: %s", sourcesListPath)
return sourcesListPath, nil
2024-05-22 15:50:21 +00:00
}
2024-05-22 16:14:11 +00:00
files, err := ioutil.ReadDir(sourcesListDPath)
2024-05-22 15:50:21 +00:00
if err != nil {
2024-05-22 16:37:29 +00:00
return "", fmt.Errorf("failed to read directory %s: %v", sourcesListDPath, err)
2024-05-22 15:50:21 +00:00
}
for _, file := range files {
if file.IsDir() {
continue
}
2024-05-22 16:14:11 +00:00
filePath := filepath.Join(sourcesListDPath, file.Name())
if isUbuntuSourcesFile(filePath) {
2024-05-22 16:37:29 +00:00
log.Printf("Found Ubuntu sources file: %s", filePath)
return filePath, nil
2024-05-22 15:50:21 +00:00
}
}
2024-05-22 16:37:29 +00:00
return "", fmt.Errorf("no Ubuntu sources file found")
2024-05-22 15:50:21 +00:00
}
2024-05-22 16:14:11 +00:00
func isUbuntuSourcesFile(filePath string) bool {
2024-05-22 15:50:21 +00:00
content, err := ioutil.ReadFile(filePath)
if err != nil {
2024-05-22 16:37:29 +00:00
log.Printf("Failed to read file %s: %v", filePath, err)
2024-05-22 15:50:21 +00:00
return false
}
2024-05-22 16:14:11 +00:00
return strings.Contains(strings.ToLower(string(content)), "ubuntu.com")
}
2024-05-22 16:37:29 +00:00
func createBackup(filePath string) error {
2024-05-22 16:14:11 +00:00
backupPath := filePath + ".bak"
2024-05-22 16:37:29 +00:00
if _, err := os.Stat(backupPath); err == nil {
return fmt.Errorf("backup file %s already exists", backupPath)
}
if err := os.Rename(filePath, backupPath); err != nil {
return fmt.Errorf("failed to create backup of %s: %v", filePath, err)
2024-05-22 16:14:11 +00:00
}
2024-05-22 16:37:29 +00:00
log.Printf("Created backup: %s", backupPath)
return nil
2024-05-22 16:14:11 +00:00
}
2024-05-22 16:37:29 +00:00
func createSourcesListD() error {
2024-05-22 16:14:11 +00:00
const sourcesListDPath = "/etc/apt/sources.list.d/"
if _, err := os.Stat(sourcesListDPath); os.IsNotExist(err) {
2024-05-22 16:37:29 +00:00
if err := os.Mkdir(sourcesListDPath, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %v", sourcesListDPath, err)
2024-05-22 16:14:11 +00:00
}
2024-05-22 16:37:29 +00:00
log.Printf("Created directory: %s", sourcesListDPath)
2024-05-22 16:14:11 +00:00
}
2024-05-22 16:37:29 +00:00
return nil
2024-05-22 16:14:11 +00:00
}
2024-05-22 16:37:29 +00:00
func findFastestMirror() (string, time.Duration, []time.Duration, error) {
log.Println("Fetching mirrors list")
2024-05-22 16:14:11 +00:00
resp, err := http.Get("http://mirrors.ubuntu.com/mirrors.txt")
if err != nil {
2024-05-22 16:37:29 +00:00
return "", 0, nil, fmt.Errorf("failed to fetch mirrors list: %v", err)
2024-05-22 16:14:11 +00:00
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
2024-05-22 16:37:29 +00:00
return "", 0, nil, fmt.Errorf("failed to read mirrors list: %v", err)
2024-05-22 16:14:11 +00:00
}
mirrors := strings.Split(string(body), "\n")
2024-05-22 16:37:29 +00:00
log.Printf("Found %d mirrors", len(mirrors))
2024-05-22 16:14:11 +00:00
var fastestMirrorURL string
var fastestTime time.Duration
2024-05-22 16:37:29 +00:00
var latencies []time.Duration
downCount := 0
noIndexCount := 0
codename, err := getUbuntuCodename()
if err != nil {
return "", 0, nil, err
}
2024-05-22 16:14:11 +00:00
httpClient := http.Client{
Timeout: 1 * time.Second,
}
2024-05-22 16:37:29 +00:00
log.Println("Testing mirrors for latency")
bar := progressbar.Default(int64(len(mirrors)))
2024-05-22 16:14:11 +00:00
for _, mirror := range mirrors {
2024-05-22 16:37:29 +00:00
bar.Add(1)
2024-05-22 16:14:11 +00:00
if strings.HasPrefix(mirror, "https://") {
mirror = strings.TrimSuffix(mirror, "/")
2024-05-22 16:37:29 +00:00
startTime := time.Now()
isValid := isValidMirror(httpClient, mirror, codename)
elapsedTime := time.Since(startTime)
if isValid {
latencies = append(latencies, elapsedTime)
if fastestMirrorURL == "" || elapsedTime < fastestTime {
fastestMirrorURL = mirror
fastestTime = elapsedTime
2024-05-22 16:14:11 +00:00
}
2024-05-22 16:37:29 +00:00
} else {
noIndexCount++
2024-05-22 16:14:11 +00:00
}
2024-05-22 16:37:29 +00:00
} else {
downCount++
2024-05-22 16:14:11 +00:00
}
}
if fastestMirrorURL == "" {
2024-05-22 16:37:29 +00:00
return "", 0, nil, fmt.Errorf("no suitable HTTPS mirror found")
2024-05-22 16:14:11 +00:00
}
2024-05-22 16:37:29 +00:00
displayLatencyStatistics(latencies, fastestMirrorURL, fastestTime)
log.Printf("Number of mirrors that were down: %d", downCount)
log.Printf("Number of mirrors without required package index: %d", noIndexCount)
return fastestMirrorURL, fastestTime, latencies, nil
2024-05-22 16:14:11 +00:00
}
2024-05-22 16:37:29 +00:00
func isValidMirror(httpClient http.Client, mirrorURL, codename string) bool {
2024-05-22 16:14:11 +00:00
uri := fmt.Sprintf("%s/dists/%s/Release", mirrorURL, codename)
resp, err := httpClient.Get(uri)
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == http.StatusOK
}
2024-05-22 16:37:29 +00:00
func extractSuites(filePath string) ([]string, error) {
2024-05-22 16:14:11 +00:00
content, err := ioutil.ReadFile(filePath)
if err != nil {
2024-05-22 16:37:29 +00:00
return nil, fmt.Errorf("failed to read file %s: %v", filePath, err)
2024-05-22 15:50:21 +00:00
}
2024-05-22 16:14:11 +00:00
// Extract suites from the sources file
2024-05-22 16:37:29 +00:00
var suites []string
2024-05-22 16:14:11 +00:00
scanner := bufio.NewScanner(strings.NewReader(string(content)))
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "deb ") || strings.HasPrefix(line, "deb-src ") {
parts := strings.Fields(line)
if len(parts) >= 4 {
suites = append(suites, parts[3:]...)
}
}
}
if err := scanner.Err(); err != nil {
2024-05-22 16:37:29 +00:00
return nil, fmt.Errorf("failed to scan file %s: %v", filePath, err)
2024-05-22 16:14:11 +00:00
}
2024-05-22 16:37:29 +00:00
return suites, nil
2024-05-22 16:14:11 +00:00
}
2024-05-22 16:37:29 +00:00
func displayLatencyStatistics(latencies []time.Duration, fastestMirrorURL string, fastestTime time.Duration) {
if len(latencies) == 0 {
log.Println("No latencies recorded")
return
}
2024-05-22 16:14:11 +00:00
2024-05-22 16:37:29 +00:00
var totalLatency time.Duration
var minLatency time.Duration = latencies[0]
var maxLatency time.Duration = latencies[0]
for _, latency := range latencies {
totalLatency += latency
if latency < minLatency {
minLatency = latency
}
if latency > maxLatency {
maxLatency = latency
}
}
averageLatency := totalLatency / time.Duration(len(latencies))
log.Printf("Minimum latency: %s", minLatency)
log.Printf("Maximum latency: %s", maxLatency)
log.Printf("Average latency: %s", averageLatency)
log.Printf("Chosen fastest mirror: %s with latency: %s", fastestMirrorURL, fastestTime)
}
func updateSourcesFile(filePath, mirrorURL string, suites []string) error {
codename, err := getUbuntuCodename()
if err != nil {
return err
}
majorVersion, minorVersion, err := getUbuntuVersion()
if err != nil {
return err
2024-05-22 16:14:11 +00:00
}
suitesString := strings.Join(suites, " ")
var content string
2024-05-22 16:37:29 +00:00
data := struct {
MirrorURL string
Codename string
Suites string
}{
MirrorURL: mirrorURL,
Codename: codename,
Suites: suitesString,
}
2024-05-22 16:14:11 +00:00
if majorVersion > 24 || (majorVersion == 24 && minorVersion >= 4) {
2024-05-22 16:37:29 +00:00
tmpl, err := template.New("modern").Parse(modernTemplate)
if err != nil {
return fmt.Errorf("failed to parse modern template: %v", err)
}
var buf strings.Builder
err = tmpl.Execute(&buf, data)
if err != nil {
return fmt.Errorf("failed to execute modern template: %v", err)
}
content = buf.String()
2024-05-22 16:14:11 +00:00
} else {
2024-05-22 16:37:29 +00:00
tmpl, err := template.New("legacy").Parse(legacyTemplate)
if err != nil {
return fmt.Errorf("failed to parse legacy template: %v", err)
}
var buf strings.Builder
err = tmpl.Execute(&buf, data)
if err != nil {
return fmt.Errorf("failed to execute legacy template: %v", err)
}
content = buf.String()
2024-05-22 16:14:11 +00:00
}
2024-05-22 16:37:29 +00:00
err = ioutil.WriteFile(filePath, []byte(content), 0644)
2024-05-22 16:14:11 +00:00
if err != nil {
2024-05-22 16:37:29 +00:00
return fmt.Errorf("failed to update sources file %s: %v", filePath, err)
2024-05-22 16:14:11 +00:00
}
2024-05-22 16:37:29 +00:00
log.Printf("Updated sources file: %s", filePath)
return nil
2024-05-22 15:50:21 +00:00
}