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.
snapd-extra-utils/snapd-seed-glue/download.go

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
}