package github import ( "archive/zip" "context" "fmt" "io" "net/http" "os" "path/filepath" "regexp" "runtime" "strings" "github.com/google/go-github/v41/github" ) // ZIPReleaseGetter retrieves a release from Github in ZIP format and downloads it // to a directory on host type ZIPReleaseGetter struct { // The name of the asset, i.e. "porter", "portersvr", "static" AssetName string // The host folder destination of the asset AssetFolderDest string // The host folder destination for the .zip file ZipFolderDest string // The name of the .zip file to download to ZipName string // The name of the Github entity whose repo is queried: i.e. "porter-dev" EntityID string // The name of the Github repo to get releases from RepoName string // If the asset is platform dependent IsPlatformDependent bool // The downloader/unzipper Downloader *ZIPDownloader } // GetLatestRelease downloads the latest .zip release from a given Github repository func (z *ZIPReleaseGetter) GetLatestRelease() error { releaseURL, err := z.getLatestReleaseDownloadURL() if err != nil { return err } return z.getReleaseFromURL(releaseURL) } // GetRelease downloads a specific .zip release from a given Github repository func (z *ZIPReleaseGetter) GetRelease(releaseTag string) error { releaseURL, err := z.getReleaseDownloadURL(releaseTag) fmt.Printf("getting release %s\n", releaseURL) if err != nil { return err } return z.getReleaseFromURL(releaseURL) } func (z *ZIPReleaseGetter) getReleaseFromURL(releaseURL string) error { fmt.Printf("getting release %s\n", releaseURL) err := z.Downloader.DownloadToFile(releaseURL) fmt.Printf("downloaded release %s to file %s\n", z.AssetName, filepath.Join(z.ZipFolderDest, z.ZipName)) if err != nil { return err } fmt.Printf("unzipping %s to %s\n", z.AssetName, z.AssetFolderDest) err = z.Downloader.UnzipToDir() return err } // retrieves the download url for the latest release of an asset func (z *ZIPReleaseGetter) getLatestReleaseDownloadURL() (string, error) { client := github.NewClient(nil) rel, _, err := client.Repositories.GetLatestRelease(context.Background(), z.EntityID, z.RepoName) if err != nil { return "", err } re, err := z.getDownloadRegexp() if err != nil { return "", err } releaseURL := "" // iterate through the assets for _, asset := range rel.Assets { if downloadURL := asset.GetBrowserDownloadURL(); re.MatchString(downloadURL) { releaseURL = downloadURL } } return releaseURL, nil } func (z *ZIPReleaseGetter) getReleaseDownloadURL(releaseTag string) (string, error) { client := github.NewClient(nil) rel, _, err := client.Repositories.GetReleaseByTag(context.Background(), z.EntityID, z.RepoName, releaseTag) if err != nil { return "", fmt.Errorf("release %s does not exist", releaseTag) } re, err := z.getDownloadRegexp() if err != nil { return "", err } releaseURL := "" // iterate through the assets for _, asset := range rel.Assets { if downloadURL := asset.GetBrowserDownloadURL(); re.MatchString(downloadURL) { releaseURL = downloadURL } } return releaseURL, nil } func (z *ZIPReleaseGetter) getDownloadRegexp() (*regexp.Regexp, error) { if z.IsPlatformDependent { switch os := runtime.GOOS; os { case "darwin": return regexp.MustCompile(fmt.Sprintf(`(?i)%s_.*_Darwin_x86_64\.zip`, z.AssetName)), nil case "linux": return regexp.MustCompile(fmt.Sprintf(`(?i)%s_.*_Linux_x86_64\.zip`, z.AssetName)), nil default: return nil, fmt.Errorf("%s is not a supported platform for Porter binaries", os) } } return regexp.MustCompile(fmt.Sprintf(`(?i)%s_.*\.zip`, z.AssetName)), nil } type ZIPDownloader struct { ZipFolderDest string ZipName string AssetFolderDest string RemoveAfterDownload bool } func (z *ZIPDownloader) DownloadToFile(url string) error { // Get the data resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() // Create the file out, err := os.Create(filepath.Join(z.ZipFolderDest, z.ZipName)) if err != nil { return err } defer out.Close() // Write the body to file _, err = io.Copy(out, resp.Body) return err } func (z *ZIPDownloader) UnzipToDir() error { r, err := zip.OpenReader(filepath.Join(z.ZipFolderDest, z.ZipName)) if err != nil { return err } defer r.Close() for _, f := range r.File { // Store filename/path for returning and using later on fpath := filepath.Join(z.AssetFolderDest, f.Name) // Check for ZipSlip. More Info: http://bit.ly/2MsjAWE if !strings.HasPrefix(fpath, filepath.Clean(z.AssetFolderDest)+string(os.PathSeparator)) { return fmt.Errorf("%s: illegal file path", fpath) } if f.FileInfo().IsDir() { // Make Folder os.MkdirAll(fpath, os.ModePerm) continue } // delete file if exists os.Remove(fpath) // Make File if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { return err } outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) if err != nil { return err } rc, err := f.Open() if err != nil { return err } _, err = io.Copy(outFile, rc) // Close the file without defer to close before next iteration of loop outFile.Close() rc.Close() if err != nil { return err } } if z.RemoveAfterDownload { os.Remove(filepath.Join(z.ZipFolderDest, z.ZipName)) } return nil }