diff options
| author | Ophestra <cat@gensokyo.uk> | 2026-01-03 15:26:59 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2026-01-03 15:29:58 +0900 |
| commit | ce249d23f1e3d3d90330fd8d02699b4b7c8ccb9b (patch) | |
| tree | 570e686dbbb73180debe99a796bfd1be3fc71c92 /internal/pkg/net.go | |
| parent | dd5d792d14799148cd6ac56cd24a2a4af6d0f5a6 (diff) | |
internal/pkg: implement http artifact
This is useful for downloading source tarballs from the internet.
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/pkg/net.go')
| -rw-r--r-- | internal/pkg/net.go | 156 |
1 files changed, 156 insertions, 0 deletions
diff --git a/internal/pkg/net.go b/internal/pkg/net.go new file mode 100644 index 00000000..16229b25 --- /dev/null +++ b/internal/pkg/net.go @@ -0,0 +1,156 @@ +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 +} |
