package github import ( "fmt" "io" "net/http" "net/url" "os" "path" "path/filepath" "regexp" "time" ) type Client struct { *jsonClient } func NewClient() *Client { jsonClient := newJSONClient(http.DefaultClient, "https://api.github.com") return &Client{ jsonClient: jsonClient, } } type Asset struct { Name string `json:"name"` BrowserDownloadURL string `json:"browser_download_url"` } type Release struct { ID int `json:"id"` TagName string `json:"tag_name"` PublishedAt time.Time `json:"published_at"` TarballURL string `json:"tarball_url"` ZipballURL string `json:"zipball_url"` Assets []Asset `json:"assets"` } func intRef(v int) *int { return &v } func (c *Client) ListReleases(owner string, project string) ([]Release, error) { var resp []Release _, err := c.jsonClient.Request(http.MethodGet, intRef(http.StatusOK), path.Join("repos", owner, project, "releases"), nil, &resp) if err != nil { return nil, err } return resp, nil } func (c *Client) DownloadAsset(w io.Writer, owner string, project string, asset *Asset) error { resp, err := http.Get(asset.BrowserDownloadURL) if err != nil { return err } _, err = io.Copy(w, resp.Body) if err != nil { return err } return nil } func (c *Client) DownloadRelease( dirPath string, org string, project string, release *Release, ) error { for _, asset := range release.Assets { fp := path.Join(dirPath, path.Base(asset.Name)) f, err := os.Create(fp) if err != nil { return err } defer f.Close() resp, err := http.Get(asset.BrowserDownloadURL) if err != nil { return err } _, err = io.Copy(f, resp.Body) if err != nil { return err } } fp := path.Join(dirPath, releaseSourceFileName(project, release.TagName, "tar.gz")) if exist, err := isFileExist(fp); err != nil { return fmt.Errorf("downloading tarball: %w", err) } else if !exist { f, err := os.Create(fp) if err != nil { return err } defer f.Close() resp, err := http.Get(release.TarballURL) if err != nil { return err } _, err = io.Copy(f, resp.Body) if err != nil { return err } } fp = path.Join(dirPath, releaseSourceFileName(project, release.TagName, "zip")) if exist, err := isFileExist(fp); err != nil { return err } else if !exist { f, err := os.Create(fp) if err != nil { return fmt.Errorf("downloading zipball: %w", err) } defer f.Close() resp, err := http.Get(release.ZipballURL) if err != nil { return err } _, err = io.Copy(f, resp.Body) if err != nil { return err } } return nil } func releaseDownloads( project string, release *Release, ) map[string]string { files := make(map[string]string) for _, asset := range release.Assets { files[path.Base(asset.Name)] = asset.BrowserDownloadURL } fileName := releaseSourceFileName(project, release.TagName, "tar.gz") files[fileName] = release.TarballURL fileName = releaseSourceFileName(project, release.TagName, "zip") files[fileName] = release.ZipballURL return files } func (c *Client) download(dst string, src string) error { resp, err := http.Head(src) if err != nil { return err } info, err := os.Stat(dst) if !os.IsNotExist(err) && err != nil { return err } if info != nil { if info.Size() == resp.ContentLength { return nil } err := os.Remove(dst) if !os.IsNotExist(err) && err != nil { return fmt.Errorf("could not remove '%s': %w", dst, err) } } resp, err = http.Get(src) if err != nil { return err } f, err := os.Create(dst) if err != nil { return fmt.Errorf("creating '%s': %w", dst, err) } defer f.Close() _, err = io.Copy(f, resp.Body) if err != nil { return err } return nil } func (c *Client) MirrorAssets(dst *url.URL, src *url.URL) error { if src.Hostname() != "github.com" { return fmt.Errorf("host must be github.com") } if dst.Scheme != "file:///" && dst.Scheme != "" { return fmt.Errorf("unsupported destination scheme '%s'", dst.Scheme) } matches := regexp.MustCompilePOSIX("/(.*?)/(.*?)").FindAllStringSubmatch(src.Path, 1) if len(matches) != 1 && len(matches[0]) != 2 { return fmt.Errorf("must be a full path to the project") } owner := matches[0][1] project := matches[0][2] releases, err := c.ListReleases(owner, project) if err != nil { return fmt.Errorf("fetching list of releases: %w", err) } existingFiles := make(map[string]bool) filepath.WalkDir(dst.Path, func(path string, _ os.DirEntry, err error) error { if err != nil { return err } path, err = filepath.Abs(path) if err != nil { return err } existingFiles[path] = true return nil }) delete(existingFiles, dst.Path) for _, release := range releases { localDir := localReleaseFilePath(dst, release.TagName) localDir, err = filepath.Abs(localDir) if err != nil { return err } err := os.MkdirAll(localDir, 0777) if err != nil { return fmt.Errorf("creating '%s': %w", localDir, err) } delete(existingFiles, localDir) for fileName, srcURL := range releaseDownloads(project, &release) { localFile := path.Join(localDir, fileName) delete(existingFiles, localFile) err := c.download(localFile, srcURL) if err != nil { return fmt.Errorf("downloading '%s': %w", srcURL, err) } } } for fp := range existingFiles { _ = os.RemoveAll(fp) } return nil }