// Copyright (C) 2024 Simon Quigley // // 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 }