Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
121 changes: 89 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,43 @@ 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)
manualChecksumPath := filepath.Join(rm.ChecksumsPath, fmt.Sprintf("checksums-%s.txt", version))

// First, check if there's already a checksum file (either manually placed or previously downloaded)
// and honor that, skipping download entirely in such case
var downloadErr error
if _, err := os.Stat(manualChecksumPath); err == nil {
log.Printf("Using existing checksums from: %s", manualChecksumPath)
checksumPath = manualChecksumPath
} else if _, err := os.Stat(checksumPath); err == nil {
log.Printf("Using existing checksums from: %s", checksumPath)
} else {
// No existing checksum file found, try to download
downloadErr = rm.downloadFile(checksumURL, checksumPath, nil)

if downloadErr != nil {
log.Printf("Warning: failed to download checksums: %v", downloadErr)
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
}
}

// Verify the checksum
if err := rm.VerifyChecksum(localPath, checksumPath, binaryName); err != nil {
return fmt.Errorf("checksum verification failed: %w", 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 err := rm.saveChecksums(version, checksumPath, binaryName); err != nil {
log.Printf("Warning: failed to save checksums: %v", err)
// 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 +198,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