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
16 changes: 15 additions & 1 deletion cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/abiosoft/colima/environment/container/incus"
"github.com/abiosoft/colima/environment/container/kubernetes"
"github.com/abiosoft/colima/util"
"github.com/abiosoft/colima/util/downloader"
"github.com/abiosoft/colima/util/osutil"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -106,6 +107,15 @@ Run 'colima template' to set the default configurations or 'colima start --edit'
}
}

// validate and set downloader if flag is specified (takes precedence over env var)
if cmd.Flag("downloader").Changed {
normalized, err := downloader.ValidateDownloader(startCmdArgs.Flags.Downloader)
if err != nil {
return err
}
downloader.SetDownloader(normalized)
}

return nil
},
}
Expand Down Expand Up @@ -141,8 +151,9 @@ var startCmdArgs struct {
DNSHosts []string
Foreground bool
SaveConfig bool
LegacyCPU int // for backward compatibility
LegacyCPU int // for backward compatibility
Template bool
Downloader string // downloader to use (native, curl)
}
}

Expand Down Expand Up @@ -255,6 +266,9 @@ func init() {
// dns
startCmd.Flags().IPSliceVarP(&startCmdArgs.Network.DNSResolvers, "dns", "n", nil, "DNS resolvers for the VM")
startCmd.Flags().StringSliceVar(&startCmdArgs.Flags.DNSHosts, "dns-host", nil, "custom DNS names to provide to resolver")

// download options
startCmd.Flags().StringVar(&startCmdArgs.Flags.Downloader, "downloader", downloader.DownloaderNative, "downloader to use (native, curl)")
}

func dnsHostsFromFlag(hosts []string) map[string]string {
Expand Down
93 changes: 93 additions & 0 deletions util/downloader/curl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package downloader

import (
"fmt"
"os"
"os/exec"
"path"
"strings"

"github.com/abiosoft/colima/util/osutil"
"github.com/abiosoft/colima/util/terminal"
)

const (
// DownloaderNative uses Go's native HTTP client
DownloaderNative = "native"
// DownloaderCurl uses the curl command (honors .curlrc)
DownloaderCurl = "curl"

envDownloader = "COLIMA_DOWNLOADER"
)

// ValidateDownloader validates the downloader value (case-insensitive).
// Returns the normalized value or an error if invalid.
func ValidateDownloader(v string) (string, error) {
switch strings.ToLower(v) {
case DownloaderNative:
return DownloaderNative, nil
case DownloaderCurl:
return DownloaderCurl, nil
default:
return "", fmt.Errorf("invalid downloader %q: must be one of %s, %s", v, DownloaderNative, DownloaderCurl)
}
}

// Downloader returns the configured downloader type.
// Flag takes precedence over environment variable when explicitly set.
func Downloader() string {
if downloaderOverride != nil {
return *downloaderOverride
}
if v := osutil.EnvVar(envDownloader).Val(); v != "" {
// normalize env var value
if normalized, err := ValidateDownloader(v); err == nil {
return normalized
}
}
return DownloaderNative
}

// UseCurl returns true if curl should be used for downloads.
func UseCurl() bool {
return Downloader() == DownloaderCurl
}

// downloaderOverride is set by the --downloader flag (nil means not set)
var downloaderOverride *string

// SetDownloader sets the downloader override (called from start command when flag is explicitly set).
// The value should be validated before calling this function.
func SetDownloader(v string) {
downloaderOverride = &v
}

// curlDownloader handles downloads using the curl command
type curlDownloader struct{}

// downloadFile downloads a file using curl
func (c curlDownloader) downloadFile(r Request, destPath string) error {
// check if curl is available
if _, err := exec.LookPath("curl"); err != nil {
return fmt.Errorf("curl not found in PATH: %w", err)
}

args := []string{
"-fSL", // fail on HTTP errors, show errors, follow redirects
"-C", "-", // resume if possible (auto-detect offset)
"--progress-bar", // show progress bar
"-o", destPath, // output file
r.URL,
}

cmd := exec.Command("curl", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Run(); err != nil {
return fmt.Errorf("curl download failed for '%s': %w", path.Base(r.URL), err)
}

terminal.ClearLine()
return nil
}
52 changes: 36 additions & 16 deletions util/downloader/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,40 @@ func (d downloader) downloadFile(r Request) (err error) {
return fmt.Errorf("error preparing cache dir: %w", err)
}

// use curl if enabled (supports .curlrc for proxy auth, SSPI, etc.)
if UseCurl() {
if err := d.downloadWithCurl(r, cacheDownloadingFilename); err != nil {
return err
}
} else {
if err := d.downloadWithHTTP(r, cacheDownloadingFilename); err != nil {
return err
}
}

// validate download if SHA is present
if r.SHA != nil {
if err := r.SHA.validateDownload(r.URL, cacheDownloadingFilename); err != nil {
// move file to allow subsequent re-download
_ = os.Rename(cacheDownloadingFilename, cacheDownloadingFilename+".invalid")
return fmt.Errorf("error validating SHA sum for '%s': %w", path.Base(r.URL), err)
}
}

// move completed download to final location
if err := os.Rename(cacheDownloadingFilename, CacheFilename(r.URL)); err != nil {
return fmt.Errorf("error finalizing download: %w", err)
}

return nil
}

func (d downloader) downloadWithCurl(r Request, destPath string) error {
curl := curlDownloader{}
return curl.downloadFile(r, destPath)
}

func (d downloader) downloadWithHTTP(r Request, destPath string) error {
// check for existing partial download and resume info
var resumeInfo ResumeInfo
resumeInfoPath := d.resumeInfoPath(r.URL)
Expand All @@ -91,7 +125,7 @@ func (d downloader) downloadFile(r Request) (err error) {

// get existing file size for resume
var existingSize int64
if stat, err := os.Stat(cacheDownloadingFilename); err == nil {
if stat, err := os.Stat(destPath); err == nil {
existingSize = stat.Size()
}

Expand All @@ -111,7 +145,7 @@ func (d downloader) downloadFile(r Request) (err error) {
// download the file
result, err := client.Download(ctx, DownloadOptions{
URL: finalURL,
DestPath: cacheDownloadingFilename,
DestPath: destPath,
ExpectedETag: resumeInfo.ETag,
ResumeFromByte: existingSize,
ShowProgress: true,
Expand All @@ -127,20 +161,6 @@ func (d downloader) downloadFile(r Request) (err error) {
// clean up resume info on successful download
_ = os.Remove(resumeInfoPath)

// validate download if SHA is present
if r.SHA != nil {
if err := r.SHA.validateDownload(r.URL, cacheDownloadingFilename); err != nil {
// move file to allow subsequent re-download
_ = os.Rename(cacheDownloadingFilename, cacheDownloadingFilename+".invalid")
return fmt.Errorf("error validating SHA sum for '%s': %w", path.Base(r.URL), err)
}
}

// move completed download to final location
if err := os.Rename(cacheDownloadingFilename, CacheFilename(r.URL)); err != nil {
return fmt.Errorf("error finalizing download: %w", err)
}

return nil
}

Expand Down