114 lines
2.4 KiB
Go
114 lines
2.4 KiB
Go
package api
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
func makeCachedClient(httpClient *http.Client, cacheTTL time.Duration) *http.Client {
|
|
cacheDir := filepath.Join(os.TempDir(), "gh-cli-cache")
|
|
return &http.Client{
|
|
Transport: CacheReponse(cacheTTL, cacheDir)(httpClient.Transport),
|
|
}
|
|
}
|
|
|
|
// CacheReponse produces a RoundTripper that caches HTTP responses to disk for a specified amount of time
|
|
func CacheReponse(ttl time.Duration, dir string) ClientOption {
|
|
return func(tr http.RoundTripper) http.RoundTripper {
|
|
return &funcTripper{roundTrip: func(req *http.Request) (*http.Response, error) {
|
|
key, keyErr := cacheKey(req)
|
|
cacheFile := filepath.Join(dir, key)
|
|
if keyErr == nil {
|
|
// TODO: make thread-safe
|
|
if res, err := readCache(ttl, cacheFile, req); err == nil {
|
|
return res, nil
|
|
}
|
|
}
|
|
res, err := tr.RoundTrip(req)
|
|
if err == nil && keyErr == nil {
|
|
// TODO: make thread-safe
|
|
_ = writeCache(cacheFile, res)
|
|
}
|
|
return res, err
|
|
}}
|
|
}
|
|
}
|
|
|
|
func cacheKey(req *http.Request) (string, error) {
|
|
h := sha256.New()
|
|
fmt.Fprintf(h, "%s:", req.Method)
|
|
fmt.Fprintf(h, "%s:", req.URL.String())
|
|
|
|
if req.Body != nil {
|
|
bodyCopy := &bytes.Buffer{}
|
|
defer req.Body.Close()
|
|
_, err := io.Copy(h, io.TeeReader(req.Body, bodyCopy))
|
|
req.Body = ioutil.NopCloser(bodyCopy)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
digest := h.Sum(nil)
|
|
return fmt.Sprintf("%x", digest), nil
|
|
}
|
|
|
|
type readCloser struct {
|
|
io.Reader
|
|
io.Closer
|
|
}
|
|
|
|
func readCache(ttl time.Duration, cacheFile string, req *http.Request) (*http.Response, error) {
|
|
f, err := os.Open(cacheFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fs, err := f.Stat()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
age := time.Since(fs.ModTime())
|
|
if age > ttl {
|
|
return nil, errors.New("cache expired")
|
|
}
|
|
|
|
res, err := http.ReadResponse(bufio.NewReader(f), req)
|
|
if res != nil {
|
|
res.Body = &readCloser{
|
|
Reader: res.Body,
|
|
Closer: f,
|
|
}
|
|
}
|
|
return res, err
|
|
}
|
|
|
|
func writeCache(cacheFile string, res *http.Response) error {
|
|
err := os.MkdirAll(filepath.Dir(cacheFile), 0755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
f, err := os.OpenFile(cacheFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
bodyCopy := &bytes.Buffer{}
|
|
defer res.Body.Close()
|
|
res.Body = ioutil.NopCloser(io.TeeReader(res.Body, bodyCopy))
|
|
err = res.Write(f)
|
|
res.Body = ioutil.NopCloser(bodyCopy)
|
|
return err
|
|
}
|