github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/release/github/api.go (about) 1 // Modified from https://github.com/aktau/github-release/blob/master/api.go 2 3 package github 4 5 import ( 6 "bytes" 7 "encoding/json" 8 "errors" 9 "fmt" 10 "io" 11 "net/http" 12 "net/url" 13 "os" 14 ) 15 16 const ( 17 githubAPIURL = "https://api.github.com" 18 ) 19 20 func githubURL(host string) (u *url.URL, err error) { 21 u, err = url.Parse(host) 22 if err != nil { 23 return 24 } 25 data := url.Values{} 26 u.RawQuery = data.Encode() 27 return 28 } 29 30 // materializeFile takes a physical file or stream (named pipe, user input, 31 // ...) and returns an io.Reader and the number of bytes that can be read 32 // from it. 33 func materializeFile(f *os.File) (io.Reader, int64, error) { 34 fi, err := f.Stat() 35 if err != nil { 36 return nil, 0, err 37 } 38 39 // If the file is actually a char device (like user typed input) 40 // or a named pipe (like a streamed in file), buffer it up. 41 // 42 // When uploading a file, you need to either explicitly set the 43 // Content-Length header or send a chunked request. Since the 44 // github upload server doesn't accept chunked encoding, we have 45 // to set the size of the file manually. Since a stream doesn't have a 46 // predefined length, it's read entirely into a byte buffer. 47 if fi.Mode()&(os.ModeCharDevice|os.ModeNamedPipe) == 1 { 48 var buf bytes.Buffer 49 n, readErr := buf.ReadFrom(f) 50 if readErr != nil { 51 return nil, 0, errors.New("req: could not buffer up input stream: " + readErr.Error()) 52 } 53 return &buf, n, readErr 54 } 55 56 // We know the os.File is most likely an actual file now. 57 n, err := getFileSize(f) 58 return f, n, err 59 } 60 61 // NewAuthRequest creates a new request that sends the auth token 62 func NewAuthRequest(method, url, bodyType, token string, headers map[string]string, body io.Reader) (*http.Request, error) { 63 var n int64 // content length 64 var err error 65 if f, ok := body.(*os.File); ok { 66 // Retrieve the content-length and buffer up if necessary. 67 body, n, err = materializeFile(f) 68 if err != nil { 69 return nil, err 70 } 71 } 72 73 req, err := http.NewRequest(method, url, body) 74 if err != nil { 75 return nil, err 76 } 77 78 if n != 0 { 79 req.ContentLength = n 80 } 81 82 if bodyType != "" { 83 req.Header.Set("Content-Type", bodyType) 84 } 85 req.Header.Set("Authorization", fmt.Sprintf("token %s", token)) 86 87 for k, v := range headers { 88 req.Header.Set(k, v) 89 } 90 91 return req, nil 92 } 93 94 // DoAuthRequest does an authenticated request to Github 95 func DoAuthRequest(method, url, bodyType, token string, headers map[string]string, body io.Reader) (*http.Response, error) { 96 req, err := NewAuthRequest(method, url, bodyType, token, headers, body) 97 if err != nil { 98 return nil, err 99 } 100 101 resp, err := http.DefaultClient.Do(req) 102 if err != nil { 103 return nil, err 104 } 105 106 return resp, nil 107 } 108 109 // Get does a GET request to the Github API 110 func Get(token string, url string, v interface{}) error { 111 resp, err := DoAuthRequest("GET", url, "", token, nil, nil) 112 if resp != nil { 113 defer func() { _ = resp.Body.Close() }() 114 } 115 if err != nil { 116 return fmt.Errorf("Error in http Get %v", err) 117 } 118 return get(resp, url, v) 119 } 120 121 func get(resp *http.Response, url, v interface{}) error { 122 if resp.StatusCode != http.StatusOK { 123 return fmt.Errorf("%s responded with %v", url, resp.Status) 124 } 125 126 var r io.Reader = resp.Body 127 if err := json.NewDecoder(r).Decode(v); err != nil { 128 return fmt.Errorf("could not unmarshall JSON into Release struct, %v", err) 129 } 130 return nil 131 }