From 2c41acab4c97f584e8e199b31dc456194b8d7e6c Mon Sep 17 00:00:00 2001 From: tsne Date: Mon, 6 Jul 2026 16:52:20 +0200 Subject: initial --- internal/bunny/api_upload.go | 50 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 internal/bunny/api_upload.go (limited to 'internal/bunny/api_upload.go') diff --git a/internal/bunny/api_upload.go b/internal/bunny/api_upload.go new file mode 100644 index 0000000..f2af534 --- /dev/null +++ b/internal/bunny/api_upload.go @@ -0,0 +1,50 @@ +package bunny + +import ( + "bytes" + "context" + "crypto/sha256" + "fmt" + "io" + "net/http" + "unsafe" +) + +// Upload performs a raw PUT with AccessKey and Checksum headers, applying the +// retry policy: a 429 with Retry-After is retried up to maxRetries times, +// honoring the header; every other non-2xx (429 without Retry-After, 5xx, +// other 4xx) and any network error fails. The Checksum header is the +// hex-encoded SHA256 of body, computed here so callers need not know Bunny's +// integrity scheme. +func (api *API) Upload(ctx context.Context, path string, body []byte) error { + sum := sha256.Sum256(body) + checksum := upperHexEncode(sum[:]) + + // bytes.NewReader lets net/http set req.GetBody automatically, so do can + // replay the body on each retry attempt. + req, err := http.NewRequestWithContext(ctx, http.MethodPut, api.url(path), bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("build request: %w", err) + } + req.Header.Set("Checksum", checksum) + req.ContentLength = int64(len(body)) + + resp, err := api.do(ctx, req) + if err != nil { + return err + } + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + return nil +} + +func upperHexEncode(src []byte) string { + const alphabet = "0123456789ABCDEF" + + dst := make([]byte, len(src)*2) + for i, b := range src { + dst[i*2] = alphabet[b>>4] + dst[i*2+1] = alphabet[b&0x0f] + } + return unsafe.String(unsafe.SliceData(dst), len(dst)) +} -- cgit v1.3