Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 87 additions & 32 deletions cmd/launcher/internal/release_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ type ReleaseManager struct {
ChecksumsPath string
// MetadataPath is where version metadata is stored
MetadataPath string
// HTTPClient is the HTTP client used for downloads
HTTPClient *http.Client
}

// NewReleaseManager creates a new release manager
Expand All @@ -65,14 +67,17 @@ func NewReleaseManager() *ReleaseManager {
CurrentVersion: internal.PrintableVersion(),
ChecksumsPath: checksumsPath,
MetadataPath: metadataPath,
HTTPClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}

// GetLatestRelease fetches the latest release information from GitHub
func (rm *ReleaseManager) GetLatestRelease() (*Release, error) {
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", rm.GitHubOwner, rm.GitHubRepo)

resp, err := http.Get(url)
resp, err := rm.HTTPClient.Get(url)
if err != nil {
return nil, fmt.Errorf("failed to fetch latest release: %w", err)
}
Expand Down Expand Up @@ -125,18 +130,41 @@ func (rm *ReleaseManager) DownloadRelease(version string, progressCallback func(
rm.GitHubOwner, rm.GitHubRepo, version, version)

checksumPath := filepath.Join(rm.BinaryPath, "checksums.txt")
if err := rm.downloadFile(checksumURL, checksumPath, nil); err != nil {
return fmt.Errorf("failed to download checksums: %w", err)
}

// Verify the checksum
if err := rm.VerifyChecksum(localPath, checksumPath, binaryName); err != nil {
return fmt.Errorf("checksum verification failed: %w", err)
// Try to download checksums with retry logic
downloadErr := rm.downloadFile(checksumURL, checksumPath, nil)

// If download fails, check for manually placed checksum file
if downloadErr != nil {
log.Printf("Warning: failed to download checksums: %v", downloadErr)

// Check if user has manually placed a checksums file
manualChecksumPath := filepath.Join(rm.ChecksumsPath, fmt.Sprintf("checksums-%s.txt", version))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would actually make sense here to check if first there is already a checksum file and honor that, and skipping download entirely in such case

if _, err := os.Stat(manualChecksumPath); err == nil {
log.Printf("Using manually placed checksums from: %s", manualChecksumPath)
checksumPath = manualChecksumPath
} else {
log.Printf("Warning: Checksum verification will be skipped. For security, you can manually place checksums at: %s", manualChecksumPath)
log.Printf("Download checksums from: %s", checksumURL)
// Continue without verification - log warning but don't fail
}
}

// Save checksums persistently for future verification
if err := rm.saveChecksums(version, checksumPath, binaryName); err != nil {
log.Printf("Warning: failed to save checksums: %v", err)
// Verify the checksum if we have a checksum file
if _, err := os.Stat(checksumPath); err == nil {
if err := rm.VerifyChecksum(localPath, checksumPath, binaryName); err != nil {
return fmt.Errorf("checksum verification failed: %w", err)
}
log.Printf("Checksum verification successful")

// Save checksums persistently for future verification
if downloadErr == nil {
if err := rm.saveChecksums(version, checksumPath, binaryName); err != nil {
log.Printf("Warning: failed to save checksums: %v", err)
}
}
} else {
log.Printf("Warning: Proceeding without checksum verification")
}

// Make the binary executable
Expand Down Expand Up @@ -168,34 +196,61 @@ func (rm *ReleaseManager) GetBinaryName(version string) string {

// downloadFile downloads a file from a URL to a local path with optional progress callback
func (rm *ReleaseManager) downloadFile(url, filepath string, progressCallback func(float64)) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
return rm.downloadFileWithRetry(url, filepath, progressCallback, 3)
}

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad status: %s", resp.Status)
}
// downloadFileWithRetry downloads a file from a URL with retry logic
func (rm *ReleaseManager) downloadFileWithRetry(url, filepath string, progressCallback func(float64), maxRetries int) error {
var lastErr error

out, err := os.Create(filepath)
if err != nil {
return err
}
defer out.Close()
for attempt := 1; attempt <= maxRetries; attempt++ {
if attempt > 1 {
log.Printf("Retrying download (attempt %d/%d): %s", attempt, maxRetries, url)
time.Sleep(time.Duration(attempt) * time.Second)
}

resp, err := rm.HTTPClient.Get(url)
if err != nil {
lastErr = err
continue
}

// Create a progress reader if callback is provided
var reader io.Reader = resp.Body
if progressCallback != nil && resp.ContentLength > 0 {
reader = &progressReader{
Reader: resp.Body,
Total: resp.ContentLength,
Callback: progressCallback,
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
lastErr = fmt.Errorf("bad status: %s", resp.Status)
continue
}

out, err := os.Create(filepath)
if err != nil {
resp.Body.Close()
return err
}

// Create a progress reader if callback is provided
var reader io.Reader = resp.Body
if progressCallback != nil && resp.ContentLength > 0 {
reader = &progressReader{
Reader: resp.Body,
Total: resp.ContentLength,
Callback: progressCallback,
}
}

_, err = io.Copy(out, reader)
resp.Body.Close()
out.Close()

if err != nil {
lastErr = err
os.Remove(filepath)
continue
}

return nil
}

_, err = io.Copy(out, reader)
return err
return fmt.Errorf("failed after %d attempts: %w", maxRetries, lastErr)
}

// saveChecksums saves checksums persistently for future verification
Expand Down
3 changes: 3 additions & 0 deletions cmd/launcher/internal/release_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"os"
"path/filepath"
"runtime"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -37,6 +38,8 @@ var _ = Describe("ReleaseManager", func() {
Expect(newRM.GitHubOwner).To(Equal("mudler"))
Expect(newRM.GitHubRepo).To(Equal("LocalAI"))
Expect(newRM.BinaryPath).To(ContainSubstring(".localai"))
Expect(newRM.HTTPClient).ToNot(BeNil())
Expect(newRM.HTTPClient.Timeout).To(Equal(30 * time.Second))
})
})

Expand Down
2 changes: 1 addition & 1 deletion cmd/launcher/internal/systray_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ func (sm *SystrayManager) showStatusDetails(status, version string) {
// showErrorDialog shows a simple error dialog
func (sm *SystrayManager) showErrorDialog(title, message string) {
fyne.DoAndWait(func() {
dialog.ShowError(fmt.Errorf(message), sm.window)
dialog.ShowError(fmt.Errorf("%s", message), sm.window)
})
}

Expand Down
Loading