You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
165 lines
7.1 KiB
165 lines
7.1 KiB
// Copyright (C) 2024 Simon Quigley <tsimonq2@ubuntu.com>
|
|
//
|
|
// This program is free software; you can redistribute it and/or
|
|
// modify it under the terms of the GNU General Public License
|
|
// as published by the Free Software Foundation; either version 3
|
|
// of the License, or (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/snapcore/snapd/snap"
|
|
"github.com/snapcore/snapd/store"
|
|
)
|
|
|
|
// downloadSnap downloads a snap file with retry logic
|
|
func downloadSnap(storeClient *store.Store, snapInfo *snap.Info, downloadPath string) error {
|
|
downloadInfo := &snap.DownloadInfo{
|
|
DownloadURL: snapInfo.DownloadURL,
|
|
}
|
|
|
|
pbar := NewProgressMeter(snapInfo.SuggestedName, snapInfo.Version, false)
|
|
progressTracker.UpdateStepProgress(0)
|
|
|
|
for attempts := 1; attempts <= 5; attempts++ {
|
|
verboseLog("Attempt %d to download snap: %s", attempts, downloadPath)
|
|
err := storeClient.Download(ctx, snapInfo.SnapID, downloadPath, downloadInfo, pbar, nil, nil)
|
|
if err == nil {
|
|
pbar.Finished()
|
|
return nil // Successful download
|
|
}
|
|
if verbose {
|
|
verboseLog("Attempt %d to download %s failed: %v", attempts, snapInfo.SuggestedName, err)
|
|
} else if progressTracker != nil {
|
|
progressTracker.UpdateStepProgress(0)
|
|
}
|
|
if attempts == 5 {
|
|
return fmt.Errorf("snap download failed after 5 attempts: %v", err)
|
|
}
|
|
}
|
|
return fmt.Errorf("snap download failed after 5 attempts")
|
|
}
|
|
|
|
// downloadSnapDeltaWithRetries downloads the delta file with retry logic and exponential backoff.
|
|
func downloadSnapDeltaWithRetries(storeClient *store.Store, delta *snap.DeltaInfo, result *store.SnapActionResult, deltaPath string, maxRetries int, snapName string) error {
|
|
if !verbose {
|
|
verboseLog("Downloading delta for %s", snapName)
|
|
}
|
|
var lastErr error
|
|
backoff := 1 * time.Second
|
|
|
|
for attempts := 1; attempts <= maxRetries; attempts++ {
|
|
verboseLog("Attempt %d to download delta: %s", attempts, deltaPath)
|
|
err := downloadSnapDelta(storeClient, delta, result, deltaPath)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
lastErr = err
|
|
verboseLog("Attempt %d to download delta failed: %v", attempts, err)
|
|
time.Sleep(backoff)
|
|
backoff *= 2
|
|
}
|
|
return fmt.Errorf("delta download failed after %d attempts: %v", maxRetries, lastErr)
|
|
}
|
|
|
|
// downloadSnapDelta downloads the delta file.
|
|
func downloadSnapDelta(storeClient *store.Store, delta *snap.DeltaInfo, result *store.SnapActionResult, deltaPath string) error {
|
|
verboseLog("Downloading delta from revision %d to %d from: %s", delta.FromRevision, delta.ToRevision, delta.DownloadURL)
|
|
|
|
downloadInfo := &snap.DownloadInfo{
|
|
DownloadURL: delta.DownloadURL,
|
|
Size: delta.Size,
|
|
Sha3_384: delta.Sha3_384,
|
|
}
|
|
|
|
// Use the SnapID from the associated SnapActionResult's Info
|
|
snapID := result.Info.SnapID
|
|
|
|
pbar := NewProgressMeter(result.Info.SuggestedName, result.Info.Version, true)
|
|
progressTracker.UpdateStepProgress(0)
|
|
|
|
// Download the delta file
|
|
if err := storeClient.Download(ctx, snapID, deltaPath, downloadInfo, pbar, nil, nil); err != nil {
|
|
progressTracker.UpdateStepProgress(0)
|
|
return fmt.Errorf("delta download failed: %v", err)
|
|
}
|
|
verboseLog("Downloaded %s to %s", delta.DownloadURL, deltaPath)
|
|
pbar.Finished()
|
|
|
|
return nil
|
|
}
|
|
|
|
// downloadAndApplySnap handles the downloading and delta application process.
|
|
// It returns the snap information and an error if any.
|
|
func downloadAndApplySnap(storeClient *store.Store, result *store.SnapActionResult, snapsDir, assertionsDir string, currentSnap *store.CurrentSnap) (*snap.Info, error) {
|
|
if result == nil || result.Info == nil {
|
|
verboseLog("No updates available for snap. Skipping download and assertions.")
|
|
return nil, nil
|
|
}
|
|
|
|
snapInfo := result.Info
|
|
downloadPath := filepath.Join(snapsDir, fmt.Sprintf("%s_%d.snap", snapInfo.SuggestedName, snapInfo.Revision.N))
|
|
|
|
// Check if a delta can be applied
|
|
if currentSnap != nil && len(result.Deltas) > 0 {
|
|
for _, delta := range result.Deltas {
|
|
deltaPath := filepath.Join(snapsDir, fmt.Sprintf("%s_%d_to_%d.delta", snapInfo.SuggestedName, delta.FromRevision, delta.ToRevision))
|
|
if err := downloadSnapDeltaWithRetries(storeClient, &delta, result, deltaPath, 5, snapInfo.SuggestedName); err == nil {
|
|
oldSnapPath := filepath.Join(snapsDir, fmt.Sprintf("%s_%d.snap", snapInfo.SuggestedName, delta.FromRevision))
|
|
if fileExists(oldSnapPath) {
|
|
if err := applyDelta(oldSnapPath, deltaPath, downloadPath); err == nil {
|
|
verboseLog("Delta applied successfully for snap %s", snapInfo.SuggestedName)
|
|
// Download assertions after successful snap download
|
|
if err := downloadAssertions(storeClient, snapInfo, assertionsDir); err != nil {
|
|
return nil, fmt.Errorf("failed to download assertions for snap %s: %w", snapInfo.SuggestedName, err)
|
|
}
|
|
return snapInfo, nil // Successful delta application
|
|
} else {
|
|
verboseLog("Failed to apply delta for snap %s: %v", snapInfo.SuggestedName, err)
|
|
}
|
|
} else {
|
|
verboseLog("Old snap file %s does not exist. Cannot apply delta.", oldSnapPath)
|
|
}
|
|
} else {
|
|
verboseLog("Attempt to download delta for snap %s failed: %v", snapInfo.SuggestedName, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no delta was applied or no deltas are available, fallback to downloading the full snap
|
|
if err := downloadSnap(storeClient, snapInfo, downloadPath); err != nil {
|
|
return nil, fmt.Errorf("failed to download snap %s: %w", snapInfo.SuggestedName, err)
|
|
}
|
|
|
|
// Download assertions after successful snap download
|
|
if err := downloadAssertions(storeClient, snapInfo, assertionsDir); err != nil {
|
|
return nil, fmt.Errorf("failed to download assertions for snap %s: %w", snapInfo.SuggestedName, err)
|
|
}
|
|
|
|
verboseLog("Downloaded and applied snap: %s, revision: %d", snapInfo.SuggestedName, snapInfo.Revision.N)
|
|
return snapInfo, nil
|
|
}
|
|
|
|
// applyDelta applies the downloaded delta using xdelta3.
|
|
func applyDelta(oldSnapPath, deltaPath, newSnapPath string) error {
|
|
verboseLog("Applying delta from %s to %s using %s", oldSnapPath, newSnapPath, deltaPath)
|
|
|
|
cmd := exec.Command("xdelta3", "-d", "-s", oldSnapPath, deltaPath, newSnapPath)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
verboseLog("xdelta3 output: %s", string(output))
|
|
return fmt.Errorf("failed to apply delta: %v - %s", err, string(output))
|
|
}
|
|
return nil
|
|
}
|