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 }