From 0dd9505fb3b988c0eb86a6c80e098ae2d448a0f8 Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 22 May 2024 10:06:49 -0700 Subject: [PATCH] latest --- fastmirror.go | 385 -------------------------------------------------- main.go | 56 ++++++++ mirror.go | 121 ++++++++++++++++ sources.go | 177 +++++++++++++++++++++++ ubuntu.go | 52 +++++++ 5 files changed, 406 insertions(+), 385 deletions(-) delete mode 100644 fastmirror.go create mode 100644 main.go create mode 100644 mirror.go create mode 100644 sources.go create mode 100644 ubuntu.go diff --git a/fastmirror.go b/fastmirror.go deleted file mode 100644 index 793ade0..0000000 --- a/fastmirror.go +++ /dev/null @@ -1,385 +0,0 @@ -package fastmirror - -import ( - "bufio" - _ "embed" - "fmt" - "github.com/schollz/progressbar/v3" - "html/template" - "io" - "log" - "net/http" - "os" - "os/exec" - "path/filepath" - "runtime" - "strconv" - "strings" - "time" -) - -//go:embed sources_list_legacy.tmpl -var legacyTemplate string - -//go:embed sources_list_modern.tmpl -var modernTemplate string - -func CLIEntry() { - log.Println("Starting Fastmirror CLI") - - if runtime.GOOS != "linux" { - log.Fatal("This program is only for Linux") - } - if err := isUbuntu(); err != nil { - log.Fatal(err) - } - - sourcesFilePath, err := findSourcesFilePath() - if err != nil { - log.Fatal(err) - } - log.Printf("Found sources file: %s", sourcesFilePath) - - // Extract suites from the existing sources file - suites, err := extractSuites(sourcesFilePath) - if err != nil { - log.Fatal(err) - } - log.Printf("Extracted suites: %v", suites) - - // Create backup of the sources file - if err := createBackup(sourcesFilePath); err != nil { - log.Fatal(err) - } - - // Create sources.list.d directory if it doesn't exist - if err := createSourcesListD(); err != nil { - log.Fatal(err) - } - - // Fetch and find the fastest mirror - fastestMirrorURL, fastestTime, latencies, err := findFastestMirror() - if err != nil { - log.Fatal(err) - } - log.Printf("Fastest mirror: %s", fastestMirrorURL) - - // Display latency statistics - displayLatencyStatistics(latencies, fastestMirrorURL, fastestTime) - - // Update sources file with the fastest mirror - if err := updateSourcesFile(sourcesFilePath, fastestMirrorURL, suites); err != nil { - log.Fatal(err) - } - log.Println("Fastmirror CLI finished successfully") -} - -func getUbuntuCodename() (string, error) { - cmd := exec.Command("lsb_release", "-cs") - output, err := cmd.Output() - if err != nil { - return "", fmt.Errorf("failed to run lsb_release: %v", err) - } - return strings.TrimSpace(string(output)), nil -} - -func getUbuntuVersion() (int, int, error) { - cmd := exec.Command("lsb_release", "-rs") - output, err := cmd.Output() - if err != nil { - return 0, 0, fmt.Errorf("failed to run lsb_release: %v", err) - } - version := strings.TrimSpace(string(output)) - parts := strings.Split(version, ".") - if len(parts) != 2 { - return 0, 0, fmt.Errorf("unexpected version format: %s", version) - } - majorVersion, err := strconv.Atoi(parts[0]) - if err != nil { - return 0, 0, fmt.Errorf("failed to parse major version: %v", err) - } - minorVersion, err := strconv.Atoi(parts[1]) - if err != nil { - return 0, 0, fmt.Errorf("failed to parse minor version: %v", err) - } - return majorVersion, minorVersion, nil -} - -func isUbuntu() error { - cmd := exec.Command("lsb_release", "-is") - output, err := cmd.Output() - if err != nil { - return fmt.Errorf("failed to run lsb_release: %v", err) - } - - if strings.TrimSpace(string(output)) != "Ubuntu" { - return fmt.Errorf("this program is only for Ubuntu") - } - return nil -} - -func findSourcesFilePath() (string, error) { - const sourcesListPath = "/etc/apt/sources.list" - const sourcesListDPath = "/etc/apt/sources.list.d/" - if isUbuntuSourcesFile(sourcesListPath) { - log.Printf("Found Ubuntu sources file: %s", sourcesListPath) - return sourcesListPath, nil - } - files, err := os.ReadDir(sourcesListDPath) - if err != nil { - return "", fmt.Errorf("failed to read directory %s: %v", sourcesListDPath, err) - } - - for _, file := range files { - if file.IsDir() { - continue - } - - filePath := filepath.Join(sourcesListDPath, file.Name()) - if isUbuntuSourcesFile(filePath) { - log.Printf("Found Ubuntu sources file: %s", filePath) - return filePath, nil - } - } - return "", fmt.Errorf("no Ubuntu sources file found") -} - -func isUbuntuSourcesFile(filePath string) bool { - content, err := os.ReadFile(filePath) - if err != nil { - log.Printf("Failed to read file %s: %v", filePath, err) - return false - } - - return strings.Contains(strings.ToLower(string(content)), "ubuntu.com") -} - -func createBackup(filePath string) error { - backupPath := filePath + ".bak" - 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) - } - log.Printf("Created backup: %s", backupPath) - return nil -} - -func createSourcesListD() error { - const sourcesListDPath = "/etc/apt/sources.list.d/" - if _, err := os.Stat(sourcesListDPath); os.IsNotExist(err) { - if err := os.Mkdir(sourcesListDPath, 0755); err != nil { - return fmt.Errorf("failed to create directory %s: %v", sourcesListDPath, err) - } - log.Printf("Created directory: %s", sourcesListDPath) - } - return nil -} - -func findFastestMirror() (string, time.Duration, []time.Duration, error) { - log.Println("Fetching mirrors list") - resp, err := http.Get("http://mirrors.ubuntu.com/mirrors.txt") - if err != nil { - return "", 0, nil, fmt.Errorf("failed to fetch mirrors list: %v", err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", 0, nil, fmt.Errorf("failed to read mirrors list: %v", err) - } - - mirrors := strings.Split(string(body), "\n") - log.Printf("Found %d mirrors", len(mirrors)) - - var fastestMirrorURL string - var fastestTime time.Duration - var latencies []time.Duration - downCount := 0 - noIndexCount := 0 - - codename, err := getUbuntuCodename() - if err != nil { - return "", 0, nil, err - } - - httpClient := http.Client{ - Timeout: 1 * time.Second, - } - - log.Println("Testing mirrors for latency") - bar := progressbar.Default(int64(len(mirrors))) - - for _, mirror := range mirrors { - if err := bar.Add(1); err != nil { - log.Printf("Error updating progress bar: %v", err) - } - if strings.HasPrefix(mirror, "https://") { - mirror = strings.TrimSuffix(mirror, "/") - 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 - } - } else { - noIndexCount++ - } - } else { - downCount++ - } - } - - if fastestMirrorURL == "" { - return "", 0, nil, fmt.Errorf("no suitable HTTPS mirror found") - } - - 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 -} - -func isValidMirror(httpClient http.Client, mirrorURL, codename string) bool { - 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 -} - -func extractSuites(filePath string) ([]string, error) { - content, err := os.ReadFile(filePath) - if err != nil { - return nil, fmt.Errorf("failed to read file %s: %v", filePath, err) - } - - var suites []string - scanner := bufio.NewScanner(strings.NewReader(string(content))) - - isModernFormat := false - for scanner.Scan() { - line := scanner.Text() - if strings.HasPrefix(line, "Types:") { - isModernFormat = true - break - } - } - - scanner = bufio.NewScanner(strings.NewReader(string(content))) - if isModernFormat { - for scanner.Scan() { - line := scanner.Text() - if strings.HasPrefix(line, "Suites:") { - parts := strings.Fields(line) - suites = append(suites, parts[1:]...) - } - } - } else { - 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 { - return nil, fmt.Errorf("failed to scan file %s: %v", filePath, err) - } - return suites, nil -} - -func displayLatencyStatistics(latencies []time.Duration, fastestMirrorURL string, fastestTime time.Duration) { - if len(latencies) == 0 { - log.Println("No latencies recorded") - return - } - - 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 - } - - suitesString := strings.Join(suites, " ") - - var content string - data := struct { - MirrorURL string - Codename string - Suites string - }{ - MirrorURL: mirrorURL, - Codename: codename, - Suites: suitesString, - } - - if majorVersion > 24 || (majorVersion == 24 && minorVersion >= 4) { - 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() - } else { - 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() - } - - err = os.WriteFile(filePath, []byte(content), 0644) - if err != nil { - return fmt.Errorf("failed to update sources file %s: %v", filePath, err) - } - log.Printf("Updated sources file: %s", filePath) - return nil -} diff --git a/main.go b/main.go new file mode 100644 index 0000000..3ad5d1a --- /dev/null +++ b/main.go @@ -0,0 +1,56 @@ +package fastmirror + +import ( + "log" + "runtime" +) + +func CLIEntry() { + log.Println("Starting Fastmirror CLI") + + if runtime.GOOS != "linux" { + log.Fatal("This program is only for Linux") + } + if err := isUbuntu(); err != nil { + log.Fatal(err) + } + + sourcesFilePath, err := findSourcesFilePath() + if err != nil { + log.Fatal(err) + } + log.Printf("Found sources file: %s", sourcesFilePath) + + // Extract suites from the existing sources file + suites, err := extractSuites(sourcesFilePath) + if err != nil { + log.Fatal(err) + } + log.Printf("Extracted suites: %v", suites) + + // Create backup of the sources file + if err := createBackup(sourcesFilePath); err != nil { + log.Fatal(err) + } + + // Create sources.list.d directory if it doesn't exist + if err := createSourcesListD(); err != nil { + log.Fatal(err) + } + + // Fetch and find the fastest mirror + fastestMirrorURL, fastestTime, latencies, err := findFastestMirror(suites) + if err != nil { + log.Fatal(err) + } + log.Printf("Fastest mirror: %s", fastestMirrorURL) + + // Display latency statistics + displayLatencyStatistics(latencies, fastestMirrorURL, fastestTime) + + // Update sources file with the fastest mirror + if err := updateSourcesFile(sourcesFilePath, fastestMirrorURL, suites); err != nil { + log.Fatal(err) + } + log.Println("Fastmirror CLI finished successfully") +} diff --git a/mirror.go b/mirror.go new file mode 100644 index 0000000..dce1a52 --- /dev/null +++ b/mirror.go @@ -0,0 +1,121 @@ +package fastmirror + +import ( + "fmt" + "io" + "log" + "net/http" + "strings" + "time" + + "github.com/schollz/progressbar/v3" +) + +func findFastestMirror(suites []string) (string, time.Duration, []time.Duration, error) { + log.Println("Fetching mirrors list") + resp, err := http.Get("http://mirrors.ubuntu.com/mirrors.txt") + if err != nil { + return "", 0, nil, fmt.Errorf("failed to fetch mirrors list: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", 0, nil, fmt.Errorf("failed to read mirrors list: %v", err) + } + + mirrors := strings.Split(string(body), "\n") + log.Printf("Found %d mirrors", len(mirrors)) + + var fastestMirrorURL string + var fastestTime time.Duration + var latencies []time.Duration + downCount := 0 + noIndexCount := 0 + + codename, err := getUbuntuCodename() + if err != nil { + return "", 0, nil, err + } + + httpClient := http.Client{ + Timeout: 1 * time.Second, + } + + log.Println("Testing mirrors for latency") + bar := progressbar.Default(int64(len(mirrors))) + + for _, mirror := range mirrors { + if err := bar.Add(1); err != nil { + log.Printf("Error updating progress bar: %v", err) + } + if strings.HasPrefix(mirror, "https://") { + mirror = strings.TrimSuffix(mirror, "/") + startTime := time.Now() + isValid := isValidMirror(httpClient, mirror, codename, suites) + elapsedTime := time.Since(startTime) + + if isValid { + latencies = append(latencies, elapsedTime) + if fastestMirrorURL == "" || elapsedTime < fastestTime { + fastestMirrorURL = mirror + fastestTime = elapsedTime + } + } else { + noIndexCount++ + } + } else { + downCount++ + } + } + + if fastestMirrorURL == "" { + return "", 0, nil, fmt.Errorf("no suitable HTTPS mirror found") + } + + 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 +} + +func isValidMirror(httpClient http.Client, mirrorURL, codename string, suites []string) bool { + for _, suite := range suites { + uri := fmt.Sprintf("%s/dists/%s/Release", mirrorURL, suite) + resp, err := httpClient.Get(uri) + if err != nil || resp.StatusCode != http.StatusOK { + return false + } + resp.Body.Close() + } + return true +} + +func displayLatencyStatistics(latencies []time.Duration, fastestMirrorURL string, fastestTime time.Duration) { + if len(latencies) == 0 { + log.Println("No latencies recorded") + return + } + + 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) +} diff --git a/sources.go b/sources.go new file mode 100644 index 0000000..deae704 --- /dev/null +++ b/sources.go @@ -0,0 +1,177 @@ +package fastmirror + +import ( + "bufio" + _ "embed" + "fmt" + "html/template" + "log" + "os" + "path/filepath" + "strings" +) + +//go:embed sources_list_legacy.tmpl +var legacyTemplate string + +//go:embed sources_list_modern.tmpl +var modernTemplate string + +func findSourcesFilePath() (string, error) { + const sourcesListPath = "/etc/apt/sources.list" + const sourcesListDPath = "/etc/apt/sources.list.d/" + if isUbuntuSourcesFile(sourcesListPath) { + log.Printf("Found Ubuntu sources file: %s", sourcesListPath) + return sourcesListPath, nil + } + files, err := os.ReadDir(sourcesListDPath) + if err != nil { + return "", fmt.Errorf("failed to read directory %s: %v", sourcesListDPath, err) + } + + for _, file := range files { + if file.IsDir() { + continue + } + + filePath := filepath.Join(sourcesListDPath, file.Name()) + if isUbuntuSourcesFile(filePath) { + log.Printf("Found Ubuntu sources file: %s", filePath) + return filePath, nil + } + } + return "", fmt.Errorf("no Ubuntu sources file found") +} + +func isUbuntuSourcesFile(filePath string) bool { + content, err := os.ReadFile(filePath) + if err != nil { + log.Printf("Failed to read file %s: %v", filePath, err) + return false + } + + return strings.Contains(strings.ToLower(string(content)), "ubuntu.com") +} + +func createBackup(filePath string) error { + backupPath := filePath + ".bak" + 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) + } + log.Printf("Created backup: %s", backupPath) + return nil +} + +func createSourcesListD() error { + const sourcesListDPath = "/etc/apt/sources.list.d/" + if _, err := os.Stat(sourcesListDPath); os.IsNotExist(err) { + if err := os.Mkdir(sourcesListDPath, 0755); err != nil { + return fmt.Errorf("failed to create directory %s: %v", sourcesListDPath, err) + } + log.Printf("Created directory: %s", sourcesListDPath) + } + return nil +} + +func extractSuites(filePath string) ([]string, error) { + content, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read file %s: %v", filePath, err) + } + + var suites []string + scanner := bufio.NewScanner(strings.NewReader(string(content))) + + isModernFormat := false + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "Types:") { + isModernFormat = true + break + } + } + + scanner = bufio.NewScanner(strings.NewReader(string(content))) + if isModernFormat { + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "Suites:") { + parts := strings.Fields(line) + suites = append(suites, parts[1:]...) + } + } + } else { + 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 { + return nil, fmt.Errorf("failed to scan file %s: %v", filePath, err) + } + return suites, nil +} + +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 + } + + suitesString := strings.Join(suites, " ") + + var content string + data := struct { + MirrorURL string + Codename string + Suites string + }{ + MirrorURL: mirrorURL, + Codename: codename, + Suites: suitesString, + } + + if majorVersion > 24 || (majorVersion == 24 && minorVersion >= 4) { + 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() + } else { + 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() + } + + err = os.WriteFile(filePath, []byte(content), 0644) + if err != nil { + return fmt.Errorf("failed to update sources file %s: %v", filePath, err) + } + log.Printf("Updated sources file: %s", filePath) + return nil +} diff --git a/ubuntu.go b/ubuntu.go new file mode 100644 index 0000000..f4eb9ee --- /dev/null +++ b/ubuntu.go @@ -0,0 +1,52 @@ +package fastmirror + +import ( + "fmt" + "os/exec" + "strconv" + "strings" +) + +func getUbuntuCodename() (string, error) { + cmd := exec.Command("lsb_release", "-cs") + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to run lsb_release: %v", err) + } + return strings.TrimSpace(string(output)), nil +} + +func getUbuntuVersion() (int, int, error) { + cmd := exec.Command("lsb_release", "-rs") + output, err := cmd.Output() + if err != nil { + return 0, 0, fmt.Errorf("failed to run lsb_release: %v", err) + } + version := strings.TrimSpace(string(output)) + parts := strings.Split(version, ".") + if len(parts) != 2 { + return 0, 0, fmt.Errorf("unexpected version format: %s", version) + } + majorVersion, err := strconv.Atoi(parts[0]) + if err != nil { + return 0, 0, fmt.Errorf("failed to parse major version: %v", err) + } + minorVersion, err := strconv.Atoi(parts[1]) + if err != nil { + return 0, 0, fmt.Errorf("failed to parse minor version: %v", err) + } + return majorVersion, minorVersion, nil +} + +func isUbuntu() error { + cmd := exec.Command("lsb_release", "-is") + output, err := cmd.Output() + if err != nil { + return fmt.Errorf("failed to run lsb_release: %v", err) + } + + if strings.TrimSpace(string(output)) != "Ubuntu" { + return fmt.Errorf("this program is only for Ubuntu") + } + return nil +}