//go:build mage package main import ( "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "github.com/magefile/mage/sh" ) // Download downloads the binaries for a given service. func Download(name string) error { cfg, err := newConfig(configFile) if err != nil { return fmt.Errorf("unable to load the configuration; %v", err) } switch name { case "forgejo": if err := downloadForgejo(cfg.Forgejo.Version); err != nil { return fmt.Errorf("an error occurred whilst getting the forgejo binary; %w", err) } case "gotosocial": if err := downloadGoToSocial(cfg.GoToSocial.Version); err != nil { return fmt.Errorf("an error occurred whilst getting the packages for GoToSocial; %w", err) } default: fmt.Printf("'%s' has no files to download.\n", name) } return nil } // downloadForgejo downloads and validates the Forgejo files. func downloadForgejo(version string) error { var ( forgejoBinaryFileFormat = "forgejo-%s-linux-amd64" forgejoDigestExtension = ".sha256" forgejoSignatureExtension = ".asc" forgejoJson = "./magefiles/forgejo.json" ) destinationDir := filepath.Join(rootBuildDir, "forgejo") binaryPath := filepath.Join( destinationDir, fmt.Sprintf(forgejoBinaryFileFormat, version), ) signaturePath := binaryPath + forgejoSignatureExtension checksumPath := binaryPath + forgejoDigestExtension data, err := newForgejoInfo(forgejoJson) if err != nil { return err } pack := downloadPack{ destinationDir: destinationDir, packages: []pack{ { file: object{ source: data.Downloads[version].Binary, destination: binaryPath, }, gpgSignature: object{ source: data.Downloads[version].Signature, destination: signaturePath, }, }, }, validateGPGSignature: true, checksum: object{ source: data.Downloads[version].Digest, destination: checksumPath, }, validateChecksum: true, } if err := download(pack); err != nil { return err } return nil } // downloadGoToSocial downloads and validates the files for GoToSocial. func downloadGoToSocial(version string) error { destinationDir := filepath.Join(rootBuildDir, "gotosocial") binaryTarUrl := fmt.Sprintf( "https://github.com/superseriousbusiness/gotosocial/releases/download/v%s/gotosocial_%s_linux_amd64.tar.gz", version, version, ) binaryTarFilepath := filepath.Join(destinationDir, fmt.Sprintf("gotosocial_%s_linux_amd64.tar.gz", version)) webAssetsTarUrl := fmt.Sprintf( "https://github.com/superseriousbusiness/gotosocial/releases/download/v%s/gotosocial_%s_web-assets.tar.gz", version, version, ) webAssetsFilepath := filepath.Join(destinationDir, fmt.Sprintf("gotosocial_%s_web-assets.tar.gz", version)) checksumUrl := fmt.Sprintf( "https://github.com/superseriousbusiness/gotosocial/releases/download/v%s/checksums.txt", version, ) checksumFilePath := filepath.Join(destinationDir, fmt.Sprintf("gotosocial_%s_checksums.txt", version)) pack := downloadPack{ destinationDir: destinationDir, packages: []pack{ { file: object{ source: binaryTarUrl, destination: binaryTarFilepath, }, }, { file: object{ source: webAssetsTarUrl, destination: webAssetsFilepath, }, }, }, validateGPGSignature: false, checksum: object{ source: checksumUrl, destination: checksumFilePath, }, validateChecksum: true, } if err := download(pack); err != nil { return err } return nil } type downloadPack struct { destinationDir string packages []pack checksum object validateGPGSignature bool validateChecksum bool } type pack struct { file object gpgSignature object } type object struct { source string destination string } // download downloads all the files in the download pack, // verifies all the GPG signatures (if enabled) and // verifies the checksums (if enabled). func download(pack downloadPack) error { if err := os.MkdirAll(pack.destinationDir, 0o750); err != nil { return fmt.Errorf("unable to make '%s'; %w", pack.destinationDir, err) } var objects []object for i := range pack.packages { objects = append(objects, pack.packages[i].file) if pack.validateGPGSignature { objects = append(objects, pack.packages[i].gpgSignature) } } if pack.validateChecksum { objects = append(objects, pack.checksum) } for _, object := range objects { if err := func() error { _, err := os.Stat(object.destination) if err == nil { fmt.Printf("%s is already downloaded.\n", object.destination) return nil } file, err := os.Create(object.destination) if err != nil { return fmt.Errorf("unable to create %s; %w", object.destination, err) } defer file.Close() client := http.Client{ CheckRedirect: func(r *http.Request, _ []*http.Request) error { r.URL.Opaque = r.URL.Path return nil }, } resp, err := client.Get(object.source) if err != nil { return err } defer resp.Body.Close() size, err := io.Copy(file, resp.Body) if err != nil { return err } fmt.Printf("Downloaded %s with size %d.\n", object.destination, size) return nil }(); err != nil { return err } } if pack.validateGPGSignature { for i := range pack.packages { if err := sh.Run("gpg", "--verify", pack.packages[i].gpgSignature.destination, pack.packages[i].file.destination); err != nil { return fmt.Errorf("GPG verification failed for '%s'; %w", pack.packages[i].file.destination, err) } } } if pack.validateChecksum { startDir, err := os.Getwd() if err != nil { return err } if err := os.Chdir(pack.destinationDir); err != nil { return err } checksum := filepath.Base(pack.checksum.destination) if err := sh.Run("sha256sum", "--check", "--ignore-missing", checksum); err != nil { return err } if err := os.Chdir(startDir); err != nil { return err } } return nil } type forgejoInfo struct { Downloads map[string]forgejoFiles `json:"downloads"` } type forgejoFiles struct { Binary string `json:"binary"` Signature string `json:"signature"` Digest string `json:"digest"` } func newForgejoInfo(path string) (forgejoInfo, error) { var info forgejoInfo f, err := os.Open(path) if err != nil { return info, err } defer f.Close() decoder := json.NewDecoder(f) if err = decoder.Decode(&info); err != nil { return info, err } return info, nil }