mirror

Mirror free and open-source projects you like with minimal effort
git clone git://git.server.ky/slackcoder/mirror
Log | Files | Refs | README

github.go (5427B)


      1 package github
      2 
      3 import (
      4 	"fmt"
      5 	"io"
      6 	"net/http"
      7 	"os"
      8 	"path"
      9 	"path/filepath"
     10 	"regexp"
     11 	"time"
     12 
     13 	"git.server.ky/slackcoder/mirror/internal"
     14 )
     15 
     16 type Client struct {
     17 	*jsonClient
     18 }
     19 
     20 func NewClient() *Client {
     21 	jsonClient := newJSONClient(http.DefaultClient, "https://api.github.com")
     22 	return &Client{
     23 		jsonClient: jsonClient,
     24 	}
     25 }
     26 
     27 type Asset struct {
     28 	Name               string `json:"name"`
     29 	BrowserDownloadURL string `json:"browser_download_url"`
     30 }
     31 
     32 type Release struct {
     33 	ID          int       `json:"id"`
     34 	TagName     string    `json:"tag_name"`
     35 	PublishedAt time.Time `json:"published_at"`
     36 	TarballURL  string    `json:"tarball_url"`
     37 	ZipballURL  string    `json:"zipball_url"`
     38 	Assets      []Asset   `json:"assets"`
     39 }
     40 
     41 func intRef(v int) *int {
     42 	return &v
     43 }
     44 
     45 func (c *Client) ListReleases(owner string, project string) ([]Release, error) {
     46 	var resp []Release
     47 
     48 	_, err := c.jsonClient.Request(http.MethodGet, intRef(http.StatusOK), path.Join("repos", owner, project, "releases"), nil, &resp)
     49 	if err != nil {
     50 		return nil, err
     51 	}
     52 
     53 	return resp, nil
     54 }
     55 
     56 func (c *Client) DownloadAsset(w io.Writer, owner string, project string, asset *Asset) error {
     57 	resp, err := http.Get(asset.BrowserDownloadURL)
     58 	if err != nil {
     59 		return err
     60 	}
     61 
     62 	_, err = io.Copy(w, resp.Body)
     63 	if err != nil {
     64 		return err
     65 	}
     66 	return nil
     67 
     68 }
     69 
     70 func (c *Client) DownloadRelease(
     71 	dirPath string,
     72 	org string,
     73 	project string,
     74 	release *Release,
     75 ) error {
     76 	for _, asset := range release.Assets {
     77 		fp := path.Join(dirPath, path.Base(asset.Name))
     78 		f, err := os.Create(fp)
     79 		if err != nil {
     80 			return err
     81 		}
     82 		defer f.Close()
     83 
     84 		resp, err := http.Get(asset.BrowserDownloadURL)
     85 		if err != nil {
     86 			return err
     87 		}
     88 		_, err = io.Copy(f, resp.Body)
     89 		if err != nil {
     90 			return err
     91 		}
     92 	}
     93 
     94 	fp := path.Join(dirPath, releaseSourceFileName(project, release.TagName, "tar.gz"))
     95 	if exist, err := isFileExist(fp); err != nil {
     96 		return fmt.Errorf("downloading tarball: %w", err)
     97 	} else if !exist {
     98 		f, err := os.Create(fp)
     99 		if err != nil {
    100 			return err
    101 		}
    102 		defer f.Close()
    103 
    104 		resp, err := http.Get(release.TarballURL)
    105 		if err != nil {
    106 			return err
    107 		}
    108 		_, err = io.Copy(f, resp.Body)
    109 		if err != nil {
    110 			return err
    111 		}
    112 	}
    113 
    114 	fp = path.Join(dirPath, releaseSourceFileName(project, release.TagName, "zip"))
    115 	if exist, err := isFileExist(fp); err != nil {
    116 		return err
    117 	} else if !exist {
    118 		f, err := os.Create(fp)
    119 		if err != nil {
    120 			return fmt.Errorf("downloading zipball: %w", err)
    121 		}
    122 		defer f.Close()
    123 
    124 		resp, err := http.Get(release.ZipballURL)
    125 		if err != nil {
    126 			return err
    127 		}
    128 		_, err = io.Copy(f, resp.Body)
    129 		if err != nil {
    130 			return err
    131 		}
    132 	}
    133 
    134 	return nil
    135 }
    136 
    137 func releaseDownloads(
    138 	project string,
    139 	release *Release,
    140 ) map[string]string {
    141 	files := make(map[string]string)
    142 
    143 	for _, asset := range release.Assets {
    144 		files[path.Base(asset.Name)] = asset.BrowserDownloadURL
    145 	}
    146 
    147 	fileName := releaseSourceFileName(project, release.TagName, "tar.gz")
    148 	files[fileName] = release.TarballURL
    149 
    150 	fileName = releaseSourceFileName(project, release.TagName, "zip")
    151 	files[fileName] = release.ZipballURL
    152 
    153 	return files
    154 }
    155 
    156 func (c *Client) download(dst string, src string) error {
    157 	resp, err := http.Head(src)
    158 	if err != nil {
    159 		return err
    160 	}
    161 
    162 	info, err := os.Stat(dst)
    163 	if !os.IsNotExist(err) && err != nil {
    164 		return err
    165 	}
    166 	if info != nil {
    167 		if info.Size() == resp.ContentLength {
    168 			return nil
    169 		}
    170 
    171 		err := os.Remove(dst)
    172 		if !os.IsNotExist(err) && err != nil {
    173 			return fmt.Errorf("could not remove '%s': %w", dst, err)
    174 		}
    175 	}
    176 
    177 	resp, err = http.Get(src)
    178 	if err != nil {
    179 		return err
    180 	}
    181 
    182 	f, err := os.Create(dst)
    183 	if err != nil {
    184 		return fmt.Errorf("creating '%s': %w", dst, err)
    185 	}
    186 	defer f.Close()
    187 
    188 	_, err = io.Copy(f, resp.Body)
    189 	if err != nil {
    190 		return err
    191 	}
    192 
    193 	return nil
    194 }
    195 
    196 func (c *Client) MirrorAssets(dst *internal.URL, src *internal.URL) error {
    197 	if src.Hostname() != "github.com" {
    198 		return fmt.Errorf("host must be github.com")
    199 	}
    200 	if dst.Scheme != "file:///" && dst.Scheme != "" {
    201 		return fmt.Errorf("unsupported destination scheme '%s'", dst.Scheme)
    202 	}
    203 
    204 	matches := regexp.MustCompilePOSIX("/(.*?)/(.*?)").FindAllStringSubmatch(src.Path, 1)
    205 	if len(matches) != 1 && len(matches[0]) != 2 {
    206 		return fmt.Errorf("must be a full path to the project")
    207 	}
    208 
    209 	owner := matches[0][1]
    210 	project := matches[0][2]
    211 
    212 	releases, err := c.ListReleases(owner, project)
    213 	if err != nil {
    214 		return fmt.Errorf("fetching list of releases: %w", err)
    215 	}
    216 
    217 	existingFiles := make(map[string]bool)
    218 
    219 	filepath.WalkDir(dst.Path, func(path string, _ os.DirEntry, err error) error {
    220 		if err != nil {
    221 			return err
    222 		}
    223 
    224 		path, err = filepath.Abs(path)
    225 		if err != nil {
    226 			return err
    227 		}
    228 		existingFiles[path] = true
    229 
    230 		return nil
    231 	})
    232 	delete(existingFiles, dst.Path)
    233 
    234 	for _, release := range releases {
    235 		localDir := localReleaseFilePath(dst, release.TagName)
    236 		localDir, err = filepath.Abs(localDir)
    237 		if err != nil {
    238 			return err
    239 		}
    240 
    241 		err := os.MkdirAll(localDir, 0777)
    242 		if err != nil {
    243 			return fmt.Errorf("creating '%s': %w", localDir, err)
    244 		}
    245 		delete(existingFiles, localDir)
    246 
    247 		for fileName, srcURL := range releaseDownloads(project, &release) {
    248 			localFile := path.Join(localDir, fileName)
    249 			delete(existingFiles, localFile)
    250 
    251 			err := c.download(localFile, srcURL)
    252 			if err != nil {
    253 				return fmt.Errorf("downloading '%s': %w", srcURL, err)
    254 			}
    255 		}
    256 	}
    257 
    258 	for fp := range existingFiles {
    259 		_ = os.RemoveAll(fp)
    260 	}
    261 
    262 	return nil
    263 }