mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
Nothing on the /store/* wire was compressed, while the corpus it carries is markdown and source. Compression lands as a pure transport concern: content addressing, the storage layout and the journal format all stay over the uncompressed bytes. The two legs are not symmetric. Pull needs no negotiation — net/http already sends Accept-Encoding: gzip and inflates transparently — so devices built before this get it the day the hub ships; a real pre-compression binary receives 19,958 bytes for a 148 KB corpus (7.4x) with no client change. Push is negotiated through sign()'s accept_encoding, because a gzip body posted to an old hub would be stored under the sha256 of its plaintext. The hub inflates ABOVE spool — the sha a key promises, the ops a journal carries and the size that gets billed are all plaintext properties — and the inflate is bounded at 256 MiB, because Content-Encoding severs the one-wire-byte-one-disk-byte relationship that made spool safe unbounded. The presigned direct-to-storage leg stays raw and is asserted to. Known deployment caveat: a compressed push clears ContentLength, so it goes out chunked where every push was sized before. A reverse proxy that buffers or rejects chunked request bodies would fail pushes (degrading to Offline and retrying, not losing data).
188 lines
6.5 KiB
Go
188 lines
6.5 KiB
Go
package remote
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The mixed-fleet contract for the push leg, both directions, at the one place
|
|
// it is decided: what sign() advertised.
|
|
//
|
|
// New client ↔ OLD hub is the dangerous half. An old hub does not inflate, so a
|
|
// gzipped body would be stored verbatim under the sha256 of its plaintext —
|
|
// rejected outright for a blob, silently mis-stored for a journal. The old hub
|
|
// says nothing about encodings, so the client must send raw.
|
|
func TestPushCompressesOnlyWhenTheHubAdvertisesIt(t *testing.T) {
|
|
payload := strings.Repeat("# notes\nthe corpus is markdown and source, which gzips well\n", 500)
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
signAnswer string
|
|
wantGzip bool
|
|
}{
|
|
{"old hub says nothing", `{"mode":"server"}`, false},
|
|
{"old hub with an empty list", `{"mode":"server","accept_encoding":[]}`, false},
|
|
{"hub speaks another codec", `{"mode":"server","accept_encoding":["zstd"]}`, false},
|
|
{"new hub", `{"mode":"server","accept_encoding":["gzip"]}`, true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var gotBody []byte
|
|
var gotEncoding string
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasSuffix(r.URL.Path, "/store/sign") {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(tc.signAnswer))
|
|
return
|
|
}
|
|
gotEncoding = r.Header.Get("Content-Encoding")
|
|
gotBody, _ = io.ReadAll(r.Body)
|
|
w.Write([]byte(`{"ok":true}`))
|
|
}))
|
|
defer ts.Close()
|
|
|
|
be, err := Open(context.Background(), ts.URL+"/p/p-0123abcd")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer be.Close()
|
|
if err := be.Put(context.Background(), "blobs/"+strings.Repeat("a", 64),
|
|
strings.NewReader(payload), int64(len(payload))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !tc.wantGzip {
|
|
if gotEncoding != "" {
|
|
t.Fatalf("Content-Encoding = %q, want none", gotEncoding)
|
|
}
|
|
if string(gotBody) != payload {
|
|
t.Fatal("body is not the plaintext it was handed")
|
|
}
|
|
return
|
|
}
|
|
if gotEncoding != "gzip" {
|
|
t.Fatalf("Content-Encoding = %q, want gzip", gotEncoding)
|
|
}
|
|
if len(gotBody) >= len(payload) {
|
|
t.Fatalf("compressed body is %d bytes, larger than the %d it started as", len(gotBody), len(payload))
|
|
}
|
|
gz, err := gzip.NewReader(bytes.NewReader(gotBody))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plain, err := io.ReadAll(gz)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Content addressing is over the UNCOMPRESSED bytes: what the hub
|
|
// inflates has to be exactly what the key names.
|
|
if string(plain) != payload {
|
|
t.Fatal("the body does not inflate to what was pushed")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Incompressible content must cross untouched even against a hub that offers
|
|
// gzip — chunked large files are mostly already-compressed binary, where gzip
|
|
// pays CPU to make the payload ~0.1% bigger.
|
|
func TestPushSkipsIncompressibleContent(t *testing.T) {
|
|
payload := randomBytes(300 << 10)
|
|
var gotEncoding string
|
|
var gotLen int
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasSuffix(r.URL.Path, "/store/sign") {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"mode":"server","accept_encoding":["gzip"]}`))
|
|
return
|
|
}
|
|
gotEncoding = r.Header.Get("Content-Encoding")
|
|
body, _ := io.ReadAll(r.Body)
|
|
gotLen = len(body)
|
|
w.Write([]byte(`{"ok":true}`))
|
|
}))
|
|
defer ts.Close()
|
|
|
|
be, err := Open(context.Background(), ts.URL+"/p/p-0123abcd")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer be.Close()
|
|
if err := be.Put(context.Background(), "blobs/"+strings.Repeat("b", 64),
|
|
bytes.NewReader(payload), int64(len(payload))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if gotEncoding != "" {
|
|
t.Fatalf("Content-Encoding = %q on incompressible content, want none", gotEncoding)
|
|
}
|
|
if gotLen != len(payload) {
|
|
t.Fatalf("wire carried %d bytes for a %d-byte payload", gotLen, len(payload))
|
|
}
|
|
}
|
|
|
|
// The presigned leg must stay raw even when the hub advertises gzip on the
|
|
// same sign() response. A direct upload goes to the object store under the
|
|
// sha256 of the PLAINTEXT with no hub in the path to inflate it, so a stray
|
|
// compression here would corrupt content addressing at rest — the one failure
|
|
// in this change that storage would keep forever rather than reject.
|
|
//
|
|
// Not reachable through any hub fixture in the tree: they all run on file://
|
|
// storage, which implements no PutSigner, so every plan is mode:"server".
|
|
// Managed hubs are S3/GCS-backed, which makes this the production path.
|
|
func TestPresignedUploadIsNeverCompressed(t *testing.T) {
|
|
payload := strings.Repeat("# highly compressible markdown\n", 2000)
|
|
|
|
var gotBody []byte
|
|
var gotEncoding string
|
|
var relayed bool
|
|
// One origin serving both roles: directTargetOK refuses a presign target
|
|
// that is neither https nor the hub's own origin, so a second httptest
|
|
// server would be declined and silently relayed instead — which would pass
|
|
// this test while proving nothing about putDirect.
|
|
var hub *httptest.Server
|
|
hub = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.HasSuffix(r.URL.Path, "/store/sign"):
|
|
w.Header().Set("Content-Type", "application/json")
|
|
// The hub advertises gzip — it does on every sign answer — AND
|
|
// hands back a presigned destination. The client must honor the
|
|
// second and ignore the first.
|
|
w.Write([]byte(`{"mode":"direct","exists":false,"accept_encoding":["gzip"],"url":"` +
|
|
hub.URL + `/presigned-blob","method":"PUT"}`))
|
|
case r.URL.Path == "/presigned-blob":
|
|
gotEncoding = r.Header.Get("Content-Encoding")
|
|
gotBody, _ = io.ReadAll(r.Body)
|
|
w.WriteHeader(http.StatusOK)
|
|
default:
|
|
relayed = true
|
|
http.Error(w, "relayed through the hub", http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer hub.Close()
|
|
|
|
be, err := Open(context.Background(), hub.URL+"/p/p-0123abcd")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer be.Close()
|
|
if err := be.Put(context.Background(), "blobs/"+strings.Repeat("c", 64),
|
|
strings.NewReader(payload), int64(len(payload))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if relayed {
|
|
t.Fatal("the upload was relayed through the hub; putDirect was never exercised")
|
|
}
|
|
if gotEncoding != "" {
|
|
t.Fatalf("presigned upload carried Content-Encoding %q — the object store has no hub to inflate it", gotEncoding)
|
|
}
|
|
if string(gotBody) != payload {
|
|
t.Fatalf("presigned upload sent %d bytes, want the %d-byte plaintext the key is the hash of",
|
|
len(gotBody), len(payload))
|
|
}
|
|
}
|