package pkg import ( "context" "crypto/sha512" "io" "net/http" "sync" "syscall" "hakurei.app/container/check" ) // An httpArtifact is an [Artifact] backed by a [http] url string. The method is // hardcoded as [http.MethodGet]. Request body is not allowed because it cannot // be deterministically represented by Params. type httpArtifact struct { // Caller-supplied context. ctx context.Context // Caller-supplied url string. url string // Caller-supplied checksum of the response body. This is validated during // curing and the first call to Data. checksum Checksum // doFunc is the Do method of [http.Client] supplied by the caller. doFunc func(req *http.Request) (*http.Response, error) // Response body read to EOF. data []byte // Synchronises access to data. mu sync.Mutex } // NewHTTPGet returns a new [File] backed by the supplied client. A GET request // is set up for url. If c is nil, [http.DefaultClient] is used instead. func NewHTTPGet( ctx context.Context, c *http.Client, url string, checksum Checksum, ) File { if ctx == nil { ctx = context.Background() } if c == nil { c = http.DefaultClient } return &httpArtifact{ctx: ctx, url: url, checksum: checksum, doFunc: c.Do} } // Kind returns the hardcoded [Kind] constant. func (a *httpArtifact) Kind() Kind { return KindHTTPGet } // Params returns the backing url string. Context is not represented as it does // not affect [Cache.Cure] outcome. func (a *httpArtifact) Params() []byte { return []byte(a.url) } // Dependencies returns a nil slice. func (a *httpArtifact) Dependencies() []Artifact { return nil } // Checksum returns the address to the caller-supplied checksum. func (a *httpArtifact) Checksum() *Checksum { return &a.checksum } // ResponseStatusError is returned for a response returned by an [http.Client] // with a status code other than [http.StatusOK]. type ResponseStatusError int func (e ResponseStatusError) Error() string { return "the requested URL returned non-OK status: " + http.StatusText(int(e)) } // do sends the caller-supplied request on the caller-supplied [http.Client] // and reads its response body to EOF and returns the resulting bytes. func (a *httpArtifact) do() (data []byte, err error) { var req *http.Request req, err = http.NewRequestWithContext(a.ctx, http.MethodGet, a.url, nil) if err != nil { return } var resp *http.Response if resp, err = a.doFunc(req); err != nil { return } if resp.StatusCode != http.StatusOK { _ = resp.Body.Close() return nil, ResponseStatusError(resp.StatusCode) } if data, err = io.ReadAll(resp.Body); err != nil { _ = resp.Body.Close() return } err = resp.Body.Close() return } // Cure returns syscall.ENOTSUP. Callers should use Data instead. func (a *httpArtifact) Cure(*check.Absolute, CacheDataFunc) error { return syscall.ENOTSUP } // Data completes the http request and returns the resulting response body read // to EOF. Data does not interact with the filesystem. func (a *httpArtifact) Data() (data []byte, err error) { a.mu.Lock() defer a.mu.Unlock() if a.data != nil { // validated by cache or a previous call to Data return a.data, nil } if data, err = a.do(); err != nil { return } h := sha512.New384() h.Write(data) if got := (Checksum)(h.Sum(nil)); got != a.checksum { return nil, &ChecksumMismatchError{got, a.checksum} } a.data = data return }