package installer import ( "context" "fmt" "io" "path/filepath" ) // DownloadAndExtract downloads a file and extracts it to the target directory // This is a common pattern used across Windows and macOS installers. // The downloaded archive is verified against expectedHash (BLAKE3) after // download; a missing hash and a mismatch are both hard failures. func DownloadAndExtract(installer *Installer, cr *CommandRunner, url, targetDir, archiveName, expectedHash string) error { if url == "" { return fmt.Errorf("release manifest unavailable or missing a download URL - check your internet connection and use Retry, then try again") } if cr != nil { if err := installer.EnsureDirWithOutput(cr, targetDir); err != nil { return err } } else { if err := installer.EnsureDir(targetDir); err != nil { return err } } archivePath := filepath.Join(targetDir, archiveName) if cr != nil { if err := installer.DownloadFileWithProgressToChannel(cr, url, archivePath); err != nil { return fmt.Errorf("download failed: %w", err) } if err := installer.verifyDownload(cr, archivePath, expectedHash); err != nil { return fmt.Errorf("checksum verification failed: %w", err) } if err := installer.ExtractArchiveWithOutput(cr, archivePath, targetDir); err != nil { return fmt.Errorf("extraction failed: %w", err) } } else { if err := installer.DownloadFile(url, archivePath); err != nil { return fmt.Errorf("download failed: %w", err) } if err := installer.verifyDownload(nil, archivePath, expectedHash); err != nil { return fmt.Errorf("checksum verification failed: %w", err) } if err := installer.ExtractArchive(archivePath, targetDir); err != nil { return fmt.Errorf("extraction failed: %w", err) } } return nil } // contextReader checks cancellation between local archive/hash reads. Network // downloads additionally carry this context on their HTTP request. type contextReader struct { ctx context.Context reader io.Reader } func (r contextReader) Read(p []byte) (int, error) { if err := r.ctx.Err(); err != nil { return 0, err } return r.reader.Read(p) }