cosmos-sdk/x/upgrade/plan/downloader.go

142 lines
5.3 KiB
Go

package plan
import (
"errors"
"fmt"
neturl "net/url"
"os"
"path/filepath"
"strings"
"github.com/hashicorp/go-getter"
)
// DownloadUpgrade downloads the given url into the provided directory and ensures it's valid.
// The provided url must contain a checksum parameter that matches the file being downloaded.
// If this returns nil, the download was successful, and {dstRoot}/bin/{daemonName} is a regular executable file.
// This is an opinionated directory structure that corresponds with Cosmovisor requirements.
// If the url is not an archive, it is downloaded and saved to {dstRoot}/bin/{daemonName}.
// If the url is an archive, it is downloaded and unpacked to {dstRoot}.
// If the archive does not contain a /bin/{daemonName} file, then this will attempt to move /{daemonName} to /bin/{daemonName}.
// If the archive does not contain either /bin/{daemonName} or /{daemonName}, an error is returned.
// Note: Because a checksum is required, this function cannot be used to download non-archive directories.
// If dstRoot already exists, some or all of its contents might be updated.
func DownloadUpgrade(dstRoot, url, daemonName string) error {
if err := ValidateIsURLWithChecksum(url); err != nil {
return err
}
target := filepath.Join(dstRoot, "bin", daemonName)
// First try to download it as a single file. If there's no error, it's okay and we're done.
if err := getter.GetFile(target, url); err != nil {
// If it was a checksum error, no need to try as directory.
if _, ok := err.(*getter.ChecksumError); ok {
return err
}
// File download didn't work, try it as an archive.
if err = downloadUpgradeAsArchive(dstRoot, url, daemonName); err != nil {
// Out of options, send back the error.
return err
}
}
return EnsureBinary(target)
}
// downloadUpgradeAsArchive tries to download the given url as an archive.
// The archive is unpacked and saved in dstDir.
// If the archive contains /{daemonName} and not /bin/{daemonName}, then /{daemonName} will be moved to /bin/{daemonName}.
// If this returns nil, the download was successful, and {dstDir}/bin/{daemonName} is a regular executable file.
func downloadUpgradeAsArchive(dstDir, url, daemonName string) error {
err := getter.Get(dstDir, url)
if err != nil {
return err
}
// If bin/{daemonName} exists, we're done.
dstDirBinFile := filepath.Join(dstDir, "bin", daemonName)
err = EnsureBinary(dstDirBinFile)
if err == nil {
return nil
}
// Otherwise, check for a root {daemonName} file and move it to the bin/ directory if found.
dstDirFile := filepath.Join(dstDir, daemonName)
err = EnsureBinary(dstDirFile)
if err == nil {
err = os.Rename(dstDirFile, dstDirBinFile)
if err != nil {
return fmt.Errorf("could not move %s to the bin directory: %w", daemonName, err)
}
return nil
}
return fmt.Errorf("url \"%s\" result does not contain a bin/%s or %s file", url, daemonName, daemonName)
}
// EnsureBinary checks that the given file exists as a regular file and is executable.
// An error is returned if:
// - The file does not exist.
// - The path exists, but is one of: Dir, Symlink, NamedPipe, Socket, Device, CharDevice, or Irregular.
// - The file exists, is not executable by all three of User, Group, and Other, and cannot be made executable.
func EnsureBinary(path string) error {
info, err := os.Stat(path)
if err != nil {
return err
}
if !info.Mode().IsRegular() {
_, f := filepath.Split(path)
return fmt.Errorf("%s is not a regular file", f)
}
// Make sure all executable bits are set.
oldMode := info.Mode().Perm()
newMode := oldMode | 0111 // Set the three execute bits to on (a+x).
if oldMode != newMode {
return os.Chmod(path, newMode)
}
return nil
}
// DownloadURLWithChecksum gets the contents of the given url, ensuring the checksum is correct.
// The provided url must contain a checksum parameter that matches the file being downloaded.
// If there isn't an error, the content returned by the url will be returned as a string.
// Returns an error if:
// - The url is not a URL or does not contain a checksum parameter.
// - Downloading the URL fails.
// - The checksum does not match what is returned by the URL.
// - The URL does not return a regular file.
// - The downloaded file is empty or only whitespace.
func DownloadURLWithChecksum(url string) (string, error) {
if err := ValidateIsURLWithChecksum(url); err != nil {
return "", err
}
tempDir, err := os.MkdirTemp("", "reference")
if err != nil {
return "", fmt.Errorf("could not create temp directory: %w", err)
}
defer os.RemoveAll(tempDir)
tempFile := filepath.Join(tempDir, "content")
if err = getter.GetFile(tempFile, url); err != nil {
return "", fmt.Errorf("could not download url \"%s\": %w", url, err)
}
tempFileBz, rerr := os.ReadFile(tempFile)
if rerr != nil {
return "", fmt.Errorf("could not read downloaded temporary file: %w", rerr)
}
tempFileStr := strings.TrimSpace(string(tempFileBz))
if len(tempFileStr) == 0 {
return "", fmt.Errorf("no content returned by \"%s\"", url)
}
return tempFileStr, nil
}
// ValidateIsURLWithChecksum checks that the given string is a url and contains a checksum query parameter.
func ValidateIsURLWithChecksum(urlStr string) error {
url, err := neturl.Parse(urlStr)
if err != nil {
return err
}
if len(url.Query().Get("checksum")) == 0 {
return errors.New("missing checksum query parameter")
}
return nil
}