package pkg import ( "context" "crypto/sha512" "errors" "io" "net/http" "os" "sync" "hakurei.app/container/check" ) // An httpArtifact is an [Artifact] backed by an [http] request. type httpArtifact struct { // Caller-supplied request. req *http.Request // Caller-supplied checksum of the response body, also used as the // identifier. This is validated during curing. id ID // doFunc is the Do method of [http.Client] supplied by the caller. doFunc func(req *http.Request) (*http.Response, error) // Instance of [Cache] to submit the cured artifact to. c *Cache // Response body read to EOF. data []byte // Populated when submitting to or loading from [Cache]. pathname *check.Absolute // Synchronises access to pathname and data. mu sync.Mutex } // NewHTTP returns a new [File] backed by the supplied client and request. If // c is nil, [http.DefaultClient] is used instead. func (c *Cache) NewHTTP(hc *http.Client, req *http.Request, checksum Checksum) File { if hc == nil { hc = http.DefaultClient } return &httpArtifact{req: req, id: checksum, doFunc: hc.Do, c: c} } // 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 (c *Cache) NewHTTPGet( ctx context.Context, hc *http.Client, url string, checksum Checksum, ) (File, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } return c.NewHTTP(hc, req, checksum), nil } // Kind returns the hardcoded [Kind] constant. func (a *httpArtifact) Kind() Kind { return KindHTTP } // ID returns the caller-supplied hash of the response body. func (a *httpArtifact) ID() ID { return a.id } // 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 resp *http.Response if resp, err = a.doFunc(a.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 } // Hash cures the [Artifact] and returns its hash. The return value is always // identical to that of the ID method. func (a *httpArtifact) Hash() (Checksum, error) { _, err := a.Pathname(); return a.id, err } // Pathname cures the [Artifact] and returns its pathname in the [Cache]. func (a *httpArtifact) Pathname() (pathname *check.Absolute, err error) { a.mu.Lock() defer a.mu.Unlock() if a.pathname != nil { return a.pathname, nil } if a.data != nil { pathname, err = a.c.StoreFile( a.id, a.data, (*Checksum)(&a.id), true, ) if err == nil { a.pathname = pathname } return } else { a.pathname, a.data, _, err = a.c.LoadOrStoreFile( a.id, a.do, (*Checksum)(&a.id), true, ) if err != nil { a.pathname, a.data = nil, nil } return a.pathname, err } } // Data completes the http request and returns the resulting response body read // to EOF. Data does not write to the underlying [Cache]. 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 a.pathname, a.data, err = a.c.LoadFile(a.id); err == nil { return a.data, nil } else { a.pathname, a.data = nil, nil if !errors.Is(err, os.ErrNotExist) { return } } if data, err = a.do(); err != nil { return } h := sha512.New384() h.Write(data) if got := (Checksum)(h.Sum(nil)); got != a.id { return nil, &ChecksumMismatchError{got, a.id} } a.data = data return }