aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/pkg/net.go
blob: b8d85b910f25454ea9939b38fe20a4cb304b931b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
package pkg

import (
	"context"
	"crypto/sha512"
	"fmt"
	"io"
	"net/http"
	"path"
	"sync"
)

// 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 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
}

var _ KnownChecksum = new(httpArtifact)
var _ fmt.Stringer = new(httpArtifact)

// 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(
	c *http.Client,
	url string,
	checksum Checksum,
) File {
	if c == nil {
		c = http.DefaultClient
	}
	return &httpArtifact{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 caller-supplied checksum.
func (a *httpArtifact) Checksum() Checksum { return a.checksum }

// String returns [path.Base] over the backing url.
func (a *httpArtifact) String() string { return path.Base(a.url) }

// 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(ctx context.Context) (data []byte, err error) {
	var req *http.Request
	req, err = http.NewRequestWithContext(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 completes the http request and returns the resulting response body read
// to EOF. Data does not interact with the filesystem.
func (a *httpArtifact) Cure(ctx context.Context) (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(ctx); 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
}