2025-02-04 17:14:00 +01:00
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
}