restic/internal/selfupdate/github.go
Michael Eischer ff7ef5007e Replace most usages of ioutil with the underlying function
The ioutil functions are deprecated since Go 1.17 and only wrap another
library function. Thus directly call the underlying function.

This commit only mechanically replaces the function calls.
2022-12-02 19:36:43 +01:00

166 lines
3.8 KiB
Go

package selfupdate
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/pkg/errors"
)
// Release collects data about a single release on GitHub.
type Release struct {
Name string `json:"name"`
TagName string `json:"tag_name"`
Draft bool `json:"draft"`
PreRelease bool `json:"prerelease"`
PublishedAt time.Time `json:"published_at"`
Assets []Asset `json:"assets"`
Version string `json:"-"` // set manually in the code
}
// Asset is a file uploaded and attached to a release.
type Asset struct {
ID int `json:"id"`
Name string `json:"name"`
URL string `json:"url"`
}
func (r Release) String() string {
return fmt.Sprintf("%v %v, %d assets",
r.TagName,
r.PublishedAt.Local().Format("2006-01-02 15:04:05"),
len(r.Assets))
}
const githubAPITimeout = 30 * time.Second
// githubError is returned by the GitHub API, e.g. for rate-limiting.
type githubError struct {
Message string
}
// GitHubLatestRelease uses the GitHub API to get information about the latest
// release of a repository.
func GitHubLatestRelease(ctx context.Context, owner, repo string) (Release, error) {
ctx, cancel := context.WithTimeout(ctx, githubAPITimeout)
defer cancel()
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return Release{}, err
}
// pin API version 3
req.Header.Set("Accept", "application/vnd.github.v3+json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return Release{}, err
}
if res.StatusCode != http.StatusOK {
content := res.Header.Get("Content-Type")
if strings.Contains(content, "application/json") {
// try to decode error message
var msg githubError
jerr := json.NewDecoder(res.Body).Decode(&msg)
if jerr == nil {
return Release{}, fmt.Errorf("unexpected status %v (%v) returned, message:\n %v", res.StatusCode, res.Status, msg.Message)
}
}
_ = res.Body.Close()
return Release{}, fmt.Errorf("unexpected status %v (%v) returned", res.StatusCode, res.Status)
}
buf, err := io.ReadAll(res.Body)
if err != nil {
_ = res.Body.Close()
return Release{}, err
}
err = res.Body.Close()
if err != nil {
return Release{}, err
}
var release Release
err = json.Unmarshal(buf, &release)
if err != nil {
return Release{}, err
}
if release.TagName == "" {
return Release{}, errors.New("tag name for latest release is empty")
}
if !strings.HasPrefix(release.TagName, "v") {
return Release{}, errors.Errorf("tag name %q is invalid, does not start with 'v'", release.TagName)
}
release.Version = release.TagName[1:]
return release, nil
}
func getGithubData(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
// request binary data
req.Header.Set("Accept", "application/octet-stream")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status %v (%v) returned", res.StatusCode, res.Status)
}
buf, err := io.ReadAll(res.Body)
if err != nil {
_ = res.Body.Close()
return nil, err
}
err = res.Body.Close()
if err != nil {
return nil, err
}
return buf, nil
}
func getGithubDataFile(ctx context.Context, assets []Asset, suffix string, printf func(string, ...interface{})) (filename string, data []byte, err error) {
var url string
for _, a := range assets {
if strings.HasSuffix(a.Name, suffix) {
url = a.URL
filename = a.Name
break
}
}
if url == "" {
return "", nil, fmt.Errorf("unable to find file with suffix %v", suffix)
}
printf("download %v\n", filename)
data, err = getGithubData(ctx, url)
if err != nil {
return "", nil, err
}
return filename, data, nil
}