diff options
Diffstat (limited to 'internal/github/github.go')
-rw-r--r-- | internal/github/github.go | 262 |
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 +} |