diff --git a/.golangci.yml b/.golangci.yml index ca90fbe..823f7f1 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -12,3 +12,6 @@ linters: - gocyclo - ineffassign - misspell +linters-settings: + gofmt: + simplify: true diff --git a/README.md b/README.md index 6c22a81..ef1039c 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ automatedgo -file -os -arch This will check the specified file for the current Go version, compare it with the latest available version, and download the new version if an update is available. > [!NOTE] -> If you don't specify the `os` and `arch` type, the tool will download the latest version for your current operating system and architecture. +> If you don't specify the `os` and `arch` type, the tool will download the latest version by detecting your current operating system and architecture. ### Command-line Options @@ -57,25 +57,25 @@ This will check the specified file for the current Go version, compare it with t ### Examples -1. Get version from a Dockerfile: +1. Download latest version from Dockerfile: ```sh automatedgo -f Dockerfile ``` ![Dockerfile Example](https://github.com/Nicconike/AutomatedGo/blob/master/assets/dockerfile_example.png) -2. Get version from go.mod: +2. Download latest version from go.mod: ```sh automatedgo -f go.mod ``` ![Go Mod Example](https://github.com/Nicconike/AutomatedGo/blob/master/assets/gomod_example.png) -3. Specify version directly: +3. Download latest version by just specifying version directly as argument: ```sh automatedgo -v 1.18 ``` ![Direct Example](https://github.com/Nicconike/AutomatedGo/blob/master/assets/direct_example.png) -4. Download for a specific OS and architecture: +4. Download latest version from JSON for a specific OS and architecture: ```sh automatedgo -f version.json -os linux -arch arm64 ``` @@ -98,7 +98,7 @@ Missing any file types you expected to see? Let me know via [discussions](https: ## Contributing -Star⭐ and Fork🍴 the Repo to start with your feature request(or bug) and experiment with the project to implement whatever Idea you might have and sent the Pull Request through 🤙 +Star⭐ and Fork🍴 the repo to support and start with your feature request(or bug) and experiment with the project to implement whatever new idea you might have and send the pull request through 🤙 Please refer [Contributing.md](https://github.com/Nicconike/AutomatedGo/blob/master/.github/CONTRIBUTING.md) to get to know how to contribute to this project. And thank you for considering to contribute. diff --git a/doc.go b/doc.go index b01519f..d4c7f18 100644 --- a/doc.go +++ b/doc.go @@ -1,97 +1,88 @@ /* -Package AutomatedGo provides tools for automating Go version checks and downloads. +**AutomatedGo Package** + +AutomatedGo provides tools for automated Go version management and updates. Features: - - Detect current Go version from various file types (Dockerfile, go.mod, JSON configs, etc.) + - Get the current Go version from a specified file or input - Check for the latest available Go version - - Download the latest Go version if an update is available - - Support for different operating systems and architectures + - Compare Go versions to determine if an update is available + - Download the latest Go version for different operating systems and architectures - Checksum validation for downloaded Go versions to ensure integrity -Do's: - - 1. Always check for errors returned by functions in this package. - 2. Use GetLatestVersion() to fetch the most recent Go version information. - 3. Specify the correct target OS and architecture when using DownloadGo(). - 4. Ensure you have necessary permissions to download and write files. - 5. Define Go version in your project files using one of these formats: - - In go.mod: go 1.x - - In Dockerfile: - * FROM golang:1.x.x - * ENV GO_VERSION=1.x.x - - In other files: - * go_version = "1.x.x" - * GO_VERSION: 1.x.x - * golang_version: "1.x.x" - 6. Use the package to automate version checks in your CI/CD pipelines. - 7. Verify checksums of downloaded Go versions for security. +Usage Example: -Don'ts: +package main - 1. Don't assume the latest version is always compatible with your project. - 2. Avoid using this package to modify your system's Go installation directly. - 3. Don't use this package in production environments without thorough testing. - 4. Don't ignore version constraints specified in your go.mod file. - 5. Avoid manually modifying files downloaded by this package. - 6. Don't use non-standard formats for specifying Go versions in your project files. +import ( -Example usage: + "fmt" + "log" - // Get the latest Go version - latestVersion, err := pkg.GetLatestVersion() - if err != nil { - log.Fatal(err) - } - fmt.Printf("Latest Go version: %s\n", latestVersion) + "github.com/Nicconike/AutomatedGo/v2/pkg" - // Get current version from a file - currentVersion, err := pkg.GetCurrentVersion("go.mod", "") - if err != nil { - log.Fatal(err) - } - fmt.Printf("Current Go version: %s\n", currentVersion) +) - // Check if update is needed - if pkg.IsNewer(latestVersion, currentVersion) { - fmt.Println("An update is available") + func main() { + // Create a new VersionService + service := pkg.NewVersionService( + &pkg.DefaultDownloader{}, + &pkg.DefaultRemover{}, + &pkg.DefaultChecksumCalculator{}, + ) - // Download the latest Go version - err = pkg.DownloadGo(latestVersion, "linux", "amd64") + // Get the current Go version + currentVersion, err := service.GetCurrentVersion("", "") if err != nil { - log.Fatal(err) + log.Fatalf("Error getting current version: %v", err) } - fmt.Println("Successfully downloaded new Go version") + fmt.Printf("Current Go version: %s\n", currentVersion) - // Verify checksum - filename := fmt.Sprintf("go%s.linux-amd64.tar.gz", latestVersion) - checksum, err := pkg.CalculateFileChecksum(filename) + // Check for the latest Go version + latestVersion, err := service.GetLatestVersion() if err != nil { - log.Fatal(err) + log.Fatalf("Error getting latest version: %v", err) + } + fmt.Printf("Latest Go version: %s\n", latestVersion) + + // Check if an update is available + if service.IsNewer(latestVersion, currentVersion) { + fmt.Println("An update is available!") + + // Download the latest version + err = service.DownloadGo(latestVersion, "", "", "/tmp", nil, nil) + if err != nil { + log.Fatalf("Error downloading Go: %v", err) + } + fmt.Printf("Successfully downloaded Go %s to /tmp\n", latestVersion) + } else { + fmt.Println("You have the latest version of Go.") } - fmt.Printf("Checksum of downloaded file: %s\n", checksum) - } else { - fmt.Println("You have the latest version") } -Functions: - - - GetLatestVersion() (string, error) - Fetches the latest available Go version. +This example demonstrates how to use the AutomatedGo package to check for updates, +compare versions, and download the latest version of Go if an update is available. - - GetCurrentVersion(filename, version string) (string, error) - Detects the current Go version from a file or uses the provided version. +Do's: - - IsNewer(version1, version2 string) bool - Compares two version strings and returns true if version1 is newer. + 1. Always check for errors returned by functions in this package. + 2. Use GetLatestVersion() to fetch the most recent Go version information. + 3. Use GetCurrentVersion() with appropriate parameters to determine the current version. + 4. Specify the correct target OS and architecture when using DownloadGo() if needed. + 5. Ensure you have necessary permissions to download and write files. + 6. Use IsNewer() to compare versions and determine if an update is needed. + 7. Use the package to automate version checks in your CI/CD pipelines. - - DownloadGo(version, targetOS, arch string) error - Downloads the specified Go version for the given OS and architecture. +Don'ts: - - CalculateFileChecksum(filename string) (string, error) - Calculates the SHA256 checksum of the specified file. + 1. Don't assume the latest version is always compatible with your project. + 2. Avoid using this package to modify your system's Go installation directly. + 3. Don't use this package in production environments without thorough testing. + 4. Don't ignore version constraints specified in your go.mod file. + 5. Avoid manually modifying files downloaded by this package. -For more detailed information and advanced usage, refer to the README.md file -and the package documentation at https://pkg.go.dev/github.com/Nicconike/AutomatedGo/v2. +Note: The package allows flexible version checking and downloading. You can provide +specific version information or let the package determine versions automatically. */ package AutomatedGo diff --git a/go.mod b/go.mod index bfcf77c..87c0854 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/Nicconike/AutomatedGo/v2 -go 1.22 +go 1.23 require ( github.com/schollz/progressbar/v3 v3.16.1 diff --git a/pkg/downloader.go b/pkg/downloader.go index 6a9c14c..46850ef 100644 --- a/pkg/downloader.go +++ b/pkg/downloader.go @@ -81,69 +81,116 @@ type DownloadConfig struct { Output io.Writer } -func DownloadGo(config DownloadConfig) error { - version := strings.TrimPrefix(config.Version, "go") - fmt.Fprintf(config.Output, "Preparing to download Go version %s\n", version) - +func promptForTargetOS(config *DownloadConfig) error { if config.TargetOS == "" { fmt.Fprint(config.Output, "Enter target OS (windows, linux, darwin): ") - fmt.Fscan(config.Input, &config.TargetOS) - } - - validArchs, ok := validPlatforms[config.TargetOS] - if !ok { - return fmt.Errorf("unsupported operating system: %s", config.TargetOS) + if _, err := fmt.Fscan(config.Input, &config.TargetOS); err != nil { + return fmt.Errorf("failed to read target OS: %w", err) + } } + return nil +} +func promptForArchitecture(config *DownloadConfig, validArchs []string) error { if config.Arch == "" { fmt.Fprintf(config.Output, "Enter target architecture %v: ", validArchs) - fmt.Fscan(config.Input, &config.Arch) + if _, err := fmt.Fscan(config.Input, &config.Arch); err != nil { + return fmt.Errorf("failed to read architecture: %w", err) + } } + return nil +} - valid := false +func isValidArchitecture(arch string, validArchs []string) bool { for _, validArch := range validArchs { - if config.Arch == validArch { - valid = true - break + if arch == validArch { + return true } } - if !valid { - return fmt.Errorf("unsupported architecture %s for OS %s", config.Arch, config.TargetOS) - } + return false +} - extension := "tar.gz" - if config.TargetOS == "windows" { - extension = "zip" +func getExtension(targetOS string) string { + if targetOS == "windows" { + return "zip" } + return "tar.gz" +} - filename := fmt.Sprintf("go%s.%s-%s.%s", version, config.TargetOS, config.Arch, extension) - fmt.Fprintf(config.Output, "Fetching Official Checksum for %s\n", filename) +func getFilename(version string, config DownloadConfig) string { + return fmt.Sprintf("go%s.%s-%s.%s", version, config.TargetOS, config.Arch, getExtension(config.TargetOS)) +} - officialChecksum, err := config.Checksum.GetOfficialChecksum(filename) +func fetchOfficialChecksum(config DownloadConfig, filename string) (string, error) { + checksum, err := config.Checksum.GetOfficialChecksum(filename) if err != nil { fmt.Fprintf(config.Output, "Failed to get official checksum: %s\n", err) - return err + return "", err } - fmt.Fprintf(config.Output, "Successfully fetched official checksum: %s\n", officialChecksum) + fmt.Fprintf(config.Output, "Successfully fetched official checksum: %s\n", checksum) + return checksum, nil +} - url := fmt.Sprintf(DownloadURLFormat, version, config.TargetOS, config.Arch, extension) - err = config.Downloader.Download(url, filename) +func downloadFile(config DownloadConfig, url string, filename string) error { + err := config.Downloader.Download(url, filename) if err != nil { fmt.Fprintf(config.Output, "Error downloading file: %s\n", err) - return err } + return err +} +func handleChecksumMismatch(config DownloadConfig, filename string) { + if removeErr := config.Remover.Remove(filename); removeErr != nil { + fmt.Fprintf(config.Output, "Error removing file %s after failed checksum calculation: %s\n", filename, removeErr) + } +} + +func verifyChecksum(config DownloadConfig, filename string, officialChecksum string) error { fmt.Fprintf(config.Output, "\nCalculating checksum for %s\n", filename) calculatedChecksum, err := config.Checksum.Calculate(filename) if err != nil || calculatedChecksum != officialChecksum { - if removeErr := config.Remover.Remove(filename); removeErr != nil { - fmt.Fprintf(config.Output, "Error removing file %s after failed checksum calculation: %s\n", filename, removeErr) - } + handleChecksumMismatch(config, filename) errMsg := fmt.Sprintf("Checksum mismatch: expected %s, got %s for file %s", officialChecksum, calculatedChecksum, filename) fmt.Fprintln(config.Output, errMsg) return errors.New(errMsg) } - fmt.Fprintln(config.Output, "Checksum verification successful!") return nil } + +func DownloadGo(config DownloadConfig) error { + version := strings.TrimPrefix(config.Version, "go") + fmt.Fprintf(config.Output, "Preparing to download Go version %s\n", version) + + if err := promptForTargetOS(&config); err != nil { + return err + } + + validArchs, ok := validPlatforms[config.TargetOS] + if !ok { + return fmt.Errorf("unsupported operating system: %s", config.TargetOS) + } + + if err := promptForArchitecture(&config, validArchs); err != nil { + return err + } + + if !isValidArchitecture(config.Arch, validArchs) { + return fmt.Errorf("unsupported architecture %s for OS %s", config.Arch, config.TargetOS) + } + + filename := getFilename(version, config) + fmt.Fprintf(config.Output, "Fetching Official Checksum for %s\n", filename) + + officialChecksum, err := fetchOfficialChecksum(config, filename) + if err != nil { + return err + } + + url := fmt.Sprintf(DownloadURLFormat, version, config.TargetOS, config.Arch, getExtension(config.TargetOS)) + if err = downloadFile(config, url, filename); err != nil { + return err + } + + return verifyChecksum(config, filename, officialChecksum) +} diff --git a/pkg/run.go b/pkg/run.go index 37f2607..b45cd88 100644 --- a/pkg/run.go +++ b/pkg/run.go @@ -16,7 +16,7 @@ func confirmDownload(input io.Reader, output io.Writer) bool { return response == "yes" } -func getDownloadPath(input io.Reader, output io.Writer) string { +func GetDownloadPath(input io.Reader, output io.Writer) string { reader := bufio.NewReader(input) for { fmt.Fprint(output, "Enter the path where you want to download the file (press Enter for current directory, or 'cancel' to abort): ") @@ -66,7 +66,7 @@ func Run(service VersionChecker, versionFile, currentVersion, targetOS, targetAr if service.IsNewer(latestVersion, cv) { fmt.Fprintln(output, "A newer version is available") if confirmDownload(input, output) { - downloadPath := getDownloadPath(input, output) + downloadPath := GetDownloadPath(input, output) if downloadPath == "" { fmt.Fprintln(output, "Download cancelled by user") return nil diff --git a/tests/unit/checksum_test.go b/tests/unit/checksum_test.go index 1b720ec..0b41e04 100644 --- a/tests/unit/checksum_test.go +++ b/tests/unit/checksum_test.go @@ -39,7 +39,11 @@ func createServerFunc(filename, sha256 string) func(http.ResponseWriter, *http.R func assertChecksumResult(t *testing.T, got string, err error, want string, wantErr string) { if wantErr != "" { - if err == nil || !strings.Contains(err.Error(), wantErr) { + if err == nil { + t.Errorf("GetOfficialChecksum() error = nil, wantErr %v", wantErr) + return + } + if !strings.Contains(err.Error(), wantErr) { t.Errorf("GetOfficialChecksum() error = %v, wantErr %v", err, wantErr) } return @@ -77,6 +81,25 @@ func TestGetOfficialChecksum(t *testing.T) { want: "", wantErr: "checksum not found for invalid.tar.gz", }, + { + name: "Network error", + serverFunc: func(w http.ResponseWriter, r *http.Request) { + hj, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "Server doesn't support hijacking", http.StatusInternalServerError) + return + } + conn, _, err := hj.Hijack() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + conn.Close() + }, + filename: goBinary, + want: "", + wantErr: "failed to fetch Go releases", + }, { name: "HTTP failure", serverFunc: func(w http.ResponseWriter, r *http.Request) { diff --git a/tests/unit/downloader_test.go b/tests/unit/downloader_test.go index 6b206b9..0156175 100644 --- a/tests/unit/downloader_test.go +++ b/tests/unit/downloader_test.go @@ -3,8 +3,11 @@ package tests import ( "bytes" "errors" + "log" "net/http" "net/http/httptest" + "os" + "path/filepath" "strings" "testing" @@ -33,6 +36,35 @@ func (m *MockRemover) Remove(filename string) error { return args.Error(0) } +func TestRemove(t *testing.T) { + t.Run("Remove existing file", func(t *testing.T) { + // Create a temporary file + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "testfile.txt") + file, err := os.Create(tmpFile) + assert.NoError(t, err) + file.Close() // Close the file after creation + + remover := &pkg.DefaultRemover{} + err = remover.Remove(tmpFile) + assert.NoError(t, err) + + // Check that the file no longer exists + _, err = os.Stat(tmpFile) + assert.True(t, os.IsNotExist(err)) + }) + + t.Run("Remove non-existent file", func(t *testing.T) { + filename := filepath.Join(t.TempDir(), "non_existent_file.txt") + + remover := &pkg.DefaultRemover{} + err := remover.Remove(filename) + + assert.Error(t, err) + assert.True(t, os.IsNotExist(err)) + }) +} + func TestDownload(t *testing.T) { tests := []struct { name string @@ -43,7 +75,10 @@ func TestDownload(t *testing.T) { name: "Successful download", serverResponse: func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) - w.Write([]byte("file content")) + _, err := w.Write([]byte("file content")) + if err != nil { + log.Printf("Failed to write response: %v", err) + } }, expectedError: "", }, diff --git a/tests/unit/run_test.go b/tests/unit/run_test.go index 6af44f1..941ee54 100644 --- a/tests/unit/run_test.go +++ b/tests/unit/run_test.go @@ -3,6 +3,7 @@ package tests import ( "bytes" "errors" + "fmt" "io" "os" "strings" @@ -37,6 +38,43 @@ func (m *MockVersionChecker) DownloadGo(version, targetOS, arch, path string, in return args.Error(0) } +// Helper function to simulate user input and capture output +func simulateGetDownloadPath(input string) (string, string) { + in := strings.NewReader(input) + out := new(bytes.Buffer) + path := pkg.GetDownloadPath(in, out) + return path, out.String() +} + +func TestGetDownloadPath(t *testing.T) { + t.Run("Cancel input", func(t *testing.T) { + path, output := simulateGetDownloadPath("cancel\n") + assert.Equal(t, "", path) + assert.Contains(t, output, "Enter the path where you want to download the file") + }) + + t.Run("Empty input uses current directory", func(t *testing.T) { + currentDir, _ := os.Getwd() + path, output := simulateGetDownloadPath("\n") + assert.Equal(t, currentDir, path) + assert.Contains(t, output, fmt.Sprintf("Using current directory: %s", currentDir)) + }) + + // t.Run("Non-existent path", func(t *testing.T) { + // nonExistentPath := "/this/path/does/not/exist" + // path, output := simulateGetDownloadPath(nonExistentPath + "\n") + // assert.Equal(t, "", path) + // assert.Contains(t, output, "Specified path does not exist. Please try again.") + // }) + + t.Run("Valid path", func(t *testing.T) { + tempDir := os.TempDir() + path, output := simulateGetDownloadPath(tempDir + "\n") + assert.Equal(t, tempDir, path) + assert.NotContains(t, output, "Specified path does not exist. Please try again") + }) +} + func TestRun(t *testing.T) { const version = "version.txt" tests := []struct {