All checks were successful
Test / Create distribution (push) Successful in 44s
Test / Sandbox (push) Successful in 2m29s
Test / ShareFS (push) Successful in 3m39s
Test / Hpkg (push) Successful in 4m30s
Test / Sandbox (race detector) (push) Successful in 4m53s
Test / Flake checks (push) Successful in 1m44s
Test / Hakurei (push) Successful in 2m29s
Test / Hakurei (race detector) (push) Successful in 3m14s
This is useful for downloading source tarballs from the internet. Signed-off-by: Ophestra <cat@gensokyo.uk>
157 lines
3.8 KiB
Go
157 lines
3.8 KiB
Go
package pkg
|
|
|
|
import (
|
|
"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(hc *http.Client, url string, checksum Checksum) (File, error) {
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c.NewHTTP(hc, req, checksum), nil
|
|
}
|
|
|
|
// 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
|
|
}
|