package spm import ( "fmt" "io" "net/http" "os" "path/filepath" "runtime" "time" ) // DownloadPackageFromAppIndex selects and downloads the correct package from the APPINDEX. func DownloadPackageFromAppIndex(appIndexPath string, packageName string, release string, pkgType string, destDir string) error { // Parse the APPINDEX entries, err := ParseAppIndex(appIndexPath) if err != nil { return fmt.Errorf("failed to parse APPINDEX: %w", err) } // Find the right entry var selected *AppIndexEntry for _, e := range entries { if e.Name == packageName && e.Release == release && e.Type == pkgType && e.OS == runtime.GOOS && e.Arch == runtime.GOARCH { selected = &e break } } // Handle no matching entry if selected == nil { return fmt.Errorf("package not found in APPINDEX: %s (release: %s, type: %s, os: %s, arch: %s)", packageName, release, pkgType, runtime.GOOS, runtime.GOARCH) } // Check if the package is already installed and up-to-date installDir, err := GetInstallDir() if err != nil { return fmt.Errorf("failed to get install directory: %w", err) } needsUpdate, err := IsUpdateNeeded(installDir, packageName, release, selected.Version, selected.Arch, selected.OS) if err != nil { return fmt.Errorf("failed to check update status: %w", err) } if !needsUpdate { UpdateProgress(0, "Already up-to-date, skipping download.") return nil // Skip download } // Download the package UpdateProgress(0, fmt.Sprintf("Downloading %s %s (%s)...", packageName, selected.Version, selected.Type)) resp, err := http.Get(selected.DownloadURL) if err != nil { return fmt.Errorf("failed to download package: %w", err) } defer resp.Body.Close() // Save the downloaded file downloadedFileName := filepath.Base(selected.DownloadURL) downloadedFilePath := filepath.Join(destDir, downloadedFileName) out, err := os.OpenFile(downloadedFilePath, os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return fmt.Errorf("failed to create output file: %w", err) } defer out.Close() totalSize := resp.ContentLength var downloaded int64 buf := make([]byte, 32*1024) // Use a larger buffer for efficiency for { n, errRead := resp.Body.Read(buf) if n > 0 { downloaded += int64(n) percentage := int(float64(downloaded) / float64(totalSize) * 100) UpdateProgress(percentage, fmt.Sprintf("Downloading %s %s (%s)...", packageName, selected.Version, selected.Type)) if _, errWrite := out.Write(buf[:n]); errWrite != nil { return fmt.Errorf("failed to write to output file: %w", errWrite) } } if errRead == io.EOF { break } if errRead != nil { return fmt.Errorf("error while reading response: %w", errRead) } } // Ensure the file handle is closed before renaming out.Close() // Construct the expected filename expectedFileName := fmt.Sprintf("%s@%s@%s@%s@%s@%s.tar.gz", packageName, selected.Arch, selected.OS, selected.Type, selected.Release, selected.Version) expectedFilePath := filepath.Join(destDir, expectedFileName) // I dont know why is this happening, I dont want to know but sometimes some process is helding up the donwloaded files so thats why it retries here maxRetries := 5 for i := 0; i < maxRetries; i++ { err = os.Rename(downloadedFilePath, expectedFilePath) if err == nil { break } // Check if file is in use f, checkErr := os.Open(downloadedFilePath) if checkErr != nil { return fmt.Errorf("file is locked by another process: %w", checkErr) } f.Close() if i < maxRetries-1 { time.Sleep(500 * time.Millisecond) // Wait before retrying } } if err != nil { return fmt.Errorf("failed to rename downloaded file after retries: %w", err) } UpdateProgress(100, fmt.Sprintf("Downloaded %s %s (%s).", packageName, selected.Version, selected.Type)) return nil }