metis/pkg/image/download.go

187 lines
4.0 KiB
Go

package image
import (
"archive/zip"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
)
// Download fetches url into dest if dest does not exist.
func Download(url, dest string) error {
_, err := DownloadAndVerify(url, dest, "")
return err
}
// DownloadAndVerify fetches the source image, verifies it when a checksum is provided,
// and returns the local raw image path ready for copying or injection.
func DownloadAndVerify(url, dest, checksum string) (string, error) {
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
return "", err
}
if strings.HasSuffix(url, ".xz") {
archive := dest + ".xz"
if err := ensureVerifiedFile(url, archive, checksum); err != nil {
return "", err
}
if err := decompressXZ(archive, dest); err != nil {
return "", err
}
return dest, nil
}
if strings.HasSuffix(url, ".zip") {
archive := dest + ".zip"
if err := ensureVerifiedFile(url, archive, checksum); err != nil {
return "", err
}
if err := decompressZIP(archive, dest); err != nil {
return "", err
}
return dest, nil
}
if err := ensureVerifiedFile(url, dest, checksum); err != nil {
return "", err
}
return dest, nil
}
func ensureVerifiedFile(url, dest, checksum string) error {
if _, err := os.Stat(dest); err == nil {
if err := VerifyChecksum(dest, checksum); err == nil || checksum == "" {
return nil
}
if removeErr := os.Remove(dest); removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) {
return removeErr
}
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
if err := downloadRaw(url, dest); err != nil {
return err
}
return VerifyChecksum(dest, checksum)
}
func downloadRaw(url, dest string) error {
if strings.HasPrefix(url, "file://") {
src := strings.TrimPrefix(url, "file://")
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dest)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
return err
}
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("download failed: %s", resp.Status)
}
out, err := os.Create(dest)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
return err
}
func decompressXZ(src, dest string) error {
out, err := os.Create(dest)
if err != nil {
return err
}
defer out.Close()
cmd := exec.Command("xz", "-dc", src)
cmd.Stdout = out
var stderr strings.Builder
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("xz decompress %s: %w: %s", src, err, stderr.String())
}
return out.Sync()
}
func decompressZIP(src, dest string) error {
reader, err := zip.OpenReader(src)
if err != nil {
return err
}
defer reader.Close()
var target *zip.File
for _, file := range reader.File {
if file.FileInfo().IsDir() {
continue
}
if strings.HasSuffix(strings.ToLower(file.Name), ".img") {
target = file
break
}
if target == nil {
target = file
}
}
if target == nil {
return fmt.Errorf("zip archive %s does not contain a file entry", src)
}
in, err := target.Open()
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dest)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return err
}
return out.Sync()
}
// VerifyChecksum checks sha256 in the form "sha256:<hex>".
func VerifyChecksum(path, checksum string) error {
if checksum == "" {
return nil
}
parts := strings.SplitN(checksum, ":", 2)
if len(parts) != 2 || parts[0] != "sha256" {
return errors.New("unsupported checksum format; use sha256:<hex>")
}
expected := strings.ToLower(parts[1])
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return err
}
sum := hex.EncodeToString(h.Sum(nil))
if sum != expected {
return fmt.Errorf("checksum mismatch: expected %s got %s", expected, sum)
}
return nil
}