aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/pkg/net.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/pkg/net.go')
-rw-r--r--internal/pkg/net.go156
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
+}