aboutsummaryrefslogtreecommitdiff
path: root/internal/github/github.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/github/github.go')
-rw-r--r--internal/github/github.go262
1 files changed, 262 insertions, 0 deletions
diff --git a/internal/github/github.go b/internal/github/github.go
new file mode 100644
index 0000000..bf35a6a
--- /dev/null
+++ b/internal/github/github.go
@@ -0,0 +1,262 @@
+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
+}